mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Correct restoring of mobile_app sensors (#76886)
This commit is contained in:
parent
b4323108b1
commit
0ed265e2be
@ -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
|
||||||
|
@ -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] = {
|
||||||
|
@ -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."""
|
||||||
|
@ -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"]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user