Correct restoring of mobile_app sensors (#76886)

This commit is contained in:
Erik Montnemery 2022-08-17 10:53:05 +02:00 committed by GitHub
parent b4323108b1
commit 0ed265e2be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 172 additions and 26 deletions

View File

@ -75,9 +75,8 @@ class MobileAppBinarySensor(MobileAppEntity, BinarySensorEntity):
"""Return the state of the binary sensor.""" """Return the state of the binary sensor."""
return self._config[ATTR_SENSOR_STATE] return self._config[ATTR_SENSOR_STATE]
@callback async def async_restore_last_state(self, last_state):
def async_restore_last_state(self, last_state):
"""Restore previous state.""" """Restore previous state."""
super().async_restore_last_state(last_state) await super().async_restore_last_state(last_state)
self._config[ATTR_SENSOR_STATE] = last_state.state == STATE_ON self._config[ATTR_SENSOR_STATE] = last_state.state == STATE_ON

View File

@ -43,10 +43,9 @@ class MobileAppEntity(RestoreEntity):
if (state := await self.async_get_last_state()) is None: if (state := await self.async_get_last_state()) is None:
return return
self.async_restore_last_state(state) await self.async_restore_last_state(state)
@callback async def async_restore_last_state(self, last_state):
def async_restore_last_state(self, last_state):
"""Restore previous state.""" """Restore previous state."""
self._config[ATTR_SENSOR_STATE] = last_state.state self._config[ATTR_SENSOR_STATE] = last_state.state
self._config[ATTR_SENSOR_ATTRIBUTES] = { self._config[ATTR_SENSOR_ATTRIBUTES] = {

View File

@ -3,9 +3,9 @@ from __future__ import annotations
from typing import Any from typing import Any
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.components.sensor import RestoreSensor, SensorDeviceClass
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_WEBHOOK_ID, STATE_UNKNOWN from homeassistant.const import CONF_WEBHOOK_ID, STATE_UNKNOWN, TEMP_CELSIUS
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -27,6 +27,7 @@ from .const import (
DOMAIN, DOMAIN,
) )
from .entity import MobileAppEntity from .entity import MobileAppEntity
from .webhook import _extract_sensor_unique_id
async def async_setup_entry( async def async_setup_entry(
@ -73,9 +74,30 @@ async def async_setup_entry(
) )
class MobileAppSensor(MobileAppEntity, SensorEntity): class MobileAppSensor(MobileAppEntity, RestoreSensor):
"""Representation of an mobile app sensor.""" """Representation of an mobile app sensor."""
async def async_restore_last_state(self, last_state):
"""Restore previous state."""
await super().async_restore_last_state(last_state)
if not (last_sensor_data := await self.async_get_last_sensor_data()):
# Workaround to handle migration to RestoreSensor, can be removed
# in HA Core 2023.4
self._config[ATTR_SENSOR_STATE] = None
webhook_id = self._entry.data[CONF_WEBHOOK_ID]
sensor_unique_id = _extract_sensor_unique_id(webhook_id, self.unique_id)
if (
self.device_class == SensorDeviceClass.TEMPERATURE
and sensor_unique_id == "battery_temperature"
):
self._config[ATTR_SENSOR_UOM] = TEMP_CELSIUS
return
self._config[ATTR_SENSOR_STATE] = last_sensor_data.native_value
self._config[ATTR_SENSOR_UOM] = last_sensor_data.native_unit_of_measurement
@property @property
def native_value(self): def native_value(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""

View File

@ -1,15 +1,34 @@
"""Entity tests for mobile_app.""" """Entity tests for mobile_app."""
from http import HTTPStatus from http import HTTPStatus
from unittest.mock import patch
import pytest import pytest
from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import PERCENTAGE, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.const import (
PERCENTAGE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
async def test_sensor(hass, create_registrations, webhook_client): @pytest.mark.parametrize(
"unit_system, state_unit, state1, state2",
(
(METRIC_SYSTEM, TEMP_CELSIUS, "100", "123"),
(IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, "212", "253"),
),
)
async def test_sensor(
hass, create_registrations, webhook_client, unit_system, state_unit, state1, state2
):
"""Test that sensors can be registered and updated.""" """Test that sensors can be registered and updated."""
hass.config.units = unit_system
webhook_id = create_registrations[1]["webhook_id"] webhook_id = create_registrations[1]["webhook_id"]
webhook_url = f"/api/webhook/{webhook_id}" webhook_url = f"/api/webhook/{webhook_id}"
@ -19,15 +38,15 @@ async def test_sensor(hass, create_registrations, webhook_client):
"type": "register_sensor", "type": "register_sensor",
"data": { "data": {
"attributes": {"foo": "bar"}, "attributes": {"foo": "bar"},
"device_class": "battery", "device_class": "temperature",
"icon": "mdi:battery", "icon": "mdi:battery",
"name": "Battery State", "name": "Battery Temperature",
"state": 100, "state": 100,
"type": "sensor", "type": "sensor",
"entity_category": "diagnostic", "entity_category": "diagnostic",
"unique_id": "battery_state", "unique_id": "battery_temp",
"state_class": "total", "state_class": "total",
"unit_of_measurement": PERCENTAGE, "unit_of_measurement": TEMP_CELSIUS,
}, },
}, },
) )
@ -38,20 +57,23 @@ async def test_sensor(hass, create_registrations, webhook_client):
assert json == {"success": True} assert json == {"success": True}
await hass.async_block_till_done() await hass.async_block_till_done()
entity = hass.states.get("sensor.test_1_battery_state") entity = hass.states.get("sensor.test_1_battery_temperature")
assert entity is not None assert entity is not None
assert entity.attributes["device_class"] == "battery" assert entity.attributes["device_class"] == "temperature"
assert entity.attributes["icon"] == "mdi:battery" assert entity.attributes["icon"] == "mdi:battery"
assert entity.attributes["unit_of_measurement"] == PERCENTAGE # unit of temperature sensor is automatically converted to the system UoM
assert entity.attributes["unit_of_measurement"] == state_unit
assert entity.attributes["foo"] == "bar" assert entity.attributes["foo"] == "bar"
assert entity.attributes["state_class"] == "total" assert entity.attributes["state_class"] == "total"
assert entity.domain == "sensor" assert entity.domain == "sensor"
assert entity.name == "Test 1 Battery State" assert entity.name == "Test 1 Battery Temperature"
assert entity.state == "100" assert entity.state == state1
assert ( assert (
er.async_get(hass).async_get("sensor.test_1_battery_state").entity_category er.async_get(hass)
.async_get("sensor.test_1_battery_temperature")
.entity_category
== "diagnostic" == "diagnostic"
) )
@ -64,7 +86,7 @@ async def test_sensor(hass, create_registrations, webhook_client):
"icon": "mdi:battery-unknown", "icon": "mdi:battery-unknown",
"state": 123, "state": 123,
"type": "sensor", "type": "sensor",
"unique_id": "battery_state", "unique_id": "battery_temp",
}, },
# This invalid data should not invalidate whole request # This invalid data should not invalidate whole request
{"type": "sensor", "unique_id": "invalid_state", "invalid": "data"}, {"type": "sensor", "unique_id": "invalid_state", "invalid": "data"},
@ -77,8 +99,8 @@ async def test_sensor(hass, create_registrations, webhook_client):
json = await update_resp.json() json = await update_resp.json()
assert json["invalid_state"]["success"] is False assert json["invalid_state"]["success"] is False
updated_entity = hass.states.get("sensor.test_1_battery_state") updated_entity = hass.states.get("sensor.test_1_battery_temperature")
assert updated_entity.state == "123" assert updated_entity.state == state2
assert "foo" not in updated_entity.attributes assert "foo" not in updated_entity.attributes
dev_reg = dr.async_get(hass) dev_reg = dr.async_get(hass)
@ -88,16 +110,120 @@ async def test_sensor(hass, create_registrations, webhook_client):
config_entry = hass.config_entries.async_entries("mobile_app")[1] config_entry = hass.config_entries.async_entries("mobile_app")[1]
await hass.config_entries.async_unload(config_entry.entry_id) await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
unloaded_entity = hass.states.get("sensor.test_1_battery_state") unloaded_entity = hass.states.get("sensor.test_1_battery_temperature")
assert unloaded_entity.state == STATE_UNAVAILABLE assert unloaded_entity.state == STATE_UNAVAILABLE
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
restored_entity = hass.states.get("sensor.test_1_battery_state") restored_entity = hass.states.get("sensor.test_1_battery_temperature")
assert restored_entity.state == updated_entity.state assert restored_entity.state == updated_entity.state
assert restored_entity.attributes == updated_entity.attributes assert restored_entity.attributes == updated_entity.attributes
@pytest.mark.parametrize(
"unique_id, unit_system, state_unit, state1, state2",
(
("battery_temperature", METRIC_SYSTEM, TEMP_CELSIUS, "100", "123"),
("battery_temperature", IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, "212", "253"),
# The unique_id doesn't match that of the mobile app's battery temperature sensor
("battery_temp", IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, "212", "123"),
),
)
async def test_sensor_migration(
hass,
create_registrations,
webhook_client,
unique_id,
unit_system,
state_unit,
state1,
state2,
):
"""Test migration to RestoreSensor."""
hass.config.units = unit_system
webhook_id = create_registrations[1]["webhook_id"]
webhook_url = f"/api/webhook/{webhook_id}"
reg_resp = await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"attributes": {"foo": "bar"},
"device_class": "temperature",
"icon": "mdi:battery",
"name": "Battery Temperature",
"state": 100,
"type": "sensor",
"entity_category": "diagnostic",
"unique_id": unique_id,
"state_class": "total",
"unit_of_measurement": TEMP_CELSIUS,
},
},
)
assert reg_resp.status == HTTPStatus.CREATED
json = await reg_resp.json()
assert json == {"success": True}
await hass.async_block_till_done()
entity = hass.states.get("sensor.test_1_battery_temperature")
assert entity is not None
assert entity.attributes["device_class"] == "temperature"
assert entity.attributes["icon"] == "mdi:battery"
# unit of temperature sensor is automatically converted to the system UoM
assert entity.attributes["unit_of_measurement"] == state_unit
assert entity.attributes["foo"] == "bar"
assert entity.attributes["state_class"] == "total"
assert entity.domain == "sensor"
assert entity.name == "Test 1 Battery Temperature"
assert entity.state == state1
# Reload to verify state is restored
config_entry = hass.config_entries.async_entries("mobile_app")[1]
await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
unloaded_entity = hass.states.get("sensor.test_1_battery_temperature")
assert unloaded_entity.state == STATE_UNAVAILABLE
# Simulate migration to RestoreSensor
with patch(
"homeassistant.helpers.restore_state.RestoreEntity.async_get_last_extra_data",
return_value=None,
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
restored_entity = hass.states.get("sensor.test_1_battery_temperature")
assert restored_entity.state == "unknown"
assert restored_entity.attributes == entity.attributes
# Test unit conversion is working
update_resp = await webhook_client.post(
webhook_url,
json={
"type": "update_sensor_states",
"data": [
{
"icon": "mdi:battery-unknown",
"state": 123,
"type": "sensor",
"unique_id": unique_id,
},
],
},
)
assert update_resp.status == HTTPStatus.OK
updated_entity = hass.states.get("sensor.test_1_battery_temperature")
assert updated_entity.state == state2
assert "foo" not in updated_entity.attributes
async def test_sensor_must_register(hass, create_registrations, webhook_client): async def test_sensor_must_register(hass, create_registrations, webhook_client):
"""Test that sensors must be registered before updating.""" """Test that sensors must be registered before updating."""
webhook_id = create_registrations[1]["webhook_id"] webhook_id = create_registrations[1]["webhook_id"]