Store Mobile app pending updates when enabling back an entity (#156026)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Timothy
2025-11-18 12:26:13 +01:00
committed by GitHub
parent 343ea1b82d
commit 4eedc88935
7 changed files with 719 additions and 40 deletions
@@ -41,9 +41,11 @@ from .const import (
DATA_CONFIG_ENTRIES,
DATA_DELETED_IDS,
DATA_DEVICES,
DATA_PENDING_UPDATES,
DATA_PUSH_CHANNEL,
DATA_STORE,
DOMAIN,
SENSOR_TYPES,
STORAGE_KEY,
STORAGE_VERSION,
)
@@ -75,6 +77,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
DATA_DEVICES: {},
DATA_PUSH_CHANNEL: {},
DATA_STORE: store,
DATA_PENDING_UPDATES: {sensor_type: {} for sensor_type in SENSOR_TYPES},
}
hass.http.register_view(RegistrationsView())
@@ -4,7 +4,7 @@ from typing import Any
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_WEBHOOK_ID, STATE_ON
from homeassistant.const import CONF_WEBHOOK_ID, STATE_ON, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -75,8 +75,9 @@ class MobileAppBinarySensor(MobileAppEntity, BinarySensorEntity):
async def async_restore_last_state(self, last_state: State) -> None:
"""Restore previous state."""
await super().async_restore_last_state(last_state)
self._config[ATTR_SENSOR_STATE] = last_state.state == STATE_ON
if self._config[ATTR_SENSOR_STATE] in (None, STATE_UNKNOWN):
await super().async_restore_last_state(last_state)
self._config[ATTR_SENSOR_STATE] = last_state.state == STATE_ON
self._async_update_attr_from_config()
@callback
@@ -20,6 +20,7 @@ DATA_DEVICES = "devices"
DATA_STORE = "store"
DATA_NOTIFY = "notify"
DATA_PUSH_CHANNEL = "push_channel"
DATA_PENDING_UPDATES = "pending_updates"
ATTR_APP_DATA = "app_data"
ATTR_APP_ID = "app_id"
@@ -94,3 +95,5 @@ SCHEMA_APP_DATA = vol.Schema(
},
extra=vol.ALLOW_EXTRA,
)
SENSOR_TYPES = (ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR)
+42 -12
View File
@@ -2,10 +2,16 @@
from __future__ import annotations
from typing import Any
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ICON, CONF_NAME, CONF_UNIQUE_ID, STATE_UNAVAILABLE
from homeassistant.const import (
ATTR_ICON,
CONF_NAME,
CONF_UNIQUE_ID,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import State, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.restore_state import RestoreEntity
@@ -18,10 +24,15 @@ from .const import (
ATTR_SENSOR_ICON,
ATTR_SENSOR_STATE,
ATTR_SENSOR_STATE_CLASS,
ATTR_SENSOR_TYPE,
DATA_PENDING_UPDATES,
DOMAIN,
SIGNAL_SENSOR_UPDATE,
)
from .helpers import device_info
_LOGGER = logging.getLogger(__name__)
class MobileAppEntity(RestoreEntity):
"""Representation of a mobile app entity."""
@@ -56,11 +67,14 @@ class MobileAppEntity(RestoreEntity):
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{SIGNAL_SENSOR_UPDATE}-{self._attr_unique_id}",
f"{SIGNAL_SENSOR_UPDATE}-{self._config[ATTR_SENSOR_TYPE]}-{self._attr_unique_id}",
self._handle_update,
)
)
# Apply any pending updates
self._handle_update()
if (state := await self.async_get_last_state()) is None:
return
@@ -69,13 +83,16 @@ class MobileAppEntity(RestoreEntity):
async def async_restore_last_state(self, last_state: State) -> None:
"""Restore previous state."""
config = self._config
config[ATTR_SENSOR_STATE] = last_state.state
config[ATTR_SENSOR_ATTRIBUTES] = {
**last_state.attributes,
**self._config[ATTR_SENSOR_ATTRIBUTES],
}
if ATTR_ICON in last_state.attributes:
config[ATTR_SENSOR_ICON] = last_state.attributes[ATTR_ICON]
# Only restore state if we don't have one already, since it can be set by a pending update
if config[ATTR_SENSOR_STATE] in (None, STATE_UNKNOWN):
config[ATTR_SENSOR_STATE] = last_state.state
config[ATTR_SENSOR_ATTRIBUTES] = {
**last_state.attributes,
**self._config[ATTR_SENSOR_ATTRIBUTES],
}
if ATTR_ICON in last_state.attributes:
config[ATTR_SENSOR_ICON] = last_state.attributes[ATTR_ICON]
@property
def device_info(self):
@@ -83,8 +100,21 @@ class MobileAppEntity(RestoreEntity):
return device_info(self._registration)
@callback
def _handle_update(self, data: dict[str, Any]) -> None:
def _handle_update(self) -> None:
"""Handle async event updates."""
self._config.update(data)
self._apply_pending_update()
self._async_update_attr_from_config()
self.async_write_ha_state()
def _apply_pending_update(self) -> None:
"""Restore any pending update for this entity."""
entity_type = self._config[ATTR_SENSOR_TYPE]
pending_updates = self.hass.data[DOMAIN][DATA_PENDING_UPDATES][entity_type]
if update := pending_updates.pop(self._attr_unique_id, None):
_LOGGER.debug(
"Applying pending update for %s: %s",
self._attr_unique_id,
update,
)
# Apply the pending update
self._config.update(update)
+19 -17
View File
@@ -86,24 +86,26 @@ class MobileAppSensor(MobileAppEntity, RestoreSensor):
async def async_restore_last_state(self, last_state: State) -> None:
"""Restore previous state."""
await super().async_restore_last_state(last_state)
config = self._config
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
config[ATTR_SENSOR_STATE] = None
webhook_id = self._entry.data[CONF_WEBHOOK_ID]
if TYPE_CHECKING:
assert self.unique_id is not None
sensor_unique_id = _extract_sensor_unique_id(webhook_id, self.unique_id)
if (
self.device_class == SensorDeviceClass.TEMPERATURE
and sensor_unique_id == "battery_temperature"
):
config[ATTR_SENSOR_UOM] = UnitOfTemperature.CELSIUS
else:
config[ATTR_SENSOR_STATE] = last_sensor_data.native_value
config[ATTR_SENSOR_UOM] = last_sensor_data.native_unit_of_measurement
if config[ATTR_SENSOR_STATE] in (None, STATE_UNKNOWN):
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
config[ATTR_SENSOR_STATE] = None
webhook_id = self._entry.data[CONF_WEBHOOK_ID]
if TYPE_CHECKING:
assert self.unique_id is not None
sensor_unique_id = _extract_sensor_unique_id(webhook_id, self.unique_id)
if (
self.device_class == SensorDeviceClass.TEMPERATURE
and sensor_unique_id == "battery_temperature"
):
config[ATTR_SENSOR_UOM] = UnitOfTemperature.CELSIUS
else:
config[ATTR_SENSOR_STATE] = last_sensor_data.native_value
config[ATTR_SENSOR_UOM] = last_sensor_data.native_unit_of_measurement
self._async_update_attr_from_config()
+26 -8
View File
@@ -79,7 +79,6 @@ from .const import (
ATTR_SENSOR_STATE,
ATTR_SENSOR_STATE_CLASS,
ATTR_SENSOR_TYPE,
ATTR_SENSOR_TYPE_BINARY_SENSOR,
ATTR_SENSOR_TYPE_SENSOR,
ATTR_SENSOR_UNIQUE_ID,
ATTR_SENSOR_UOM,
@@ -98,12 +97,14 @@ from .const import (
DATA_CONFIG_ENTRIES,
DATA_DELETED_IDS,
DATA_DEVICES,
DATA_PENDING_UPDATES,
DOMAIN,
ERR_ENCRYPTION_ALREADY_ENABLED,
ERR_ENCRYPTION_REQUIRED,
ERR_INVALID_FORMAT,
ERR_SENSOR_NOT_REGISTERED,
SCHEMA_APP_DATA,
SENSOR_TYPES,
SIGNAL_LOCATION_UPDATE,
SIGNAL_SENSOR_UPDATE,
)
@@ -125,8 +126,6 @@ WEBHOOK_COMMANDS: Registry[
str, Callable[[HomeAssistant, ConfigEntry, Any], Coroutine[Any, Any, Response]]
] = Registry()
SENSOR_TYPES = (ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR)
WEBHOOK_PAYLOAD_SCHEMA = vol.Any(
vol.Schema(
{
@@ -601,14 +600,16 @@ async def webhook_register_sensor(
if changes:
entity_registry.async_update_entity(existing_sensor, **changes)
async_dispatcher_send(hass, f"{SIGNAL_SENSOR_UPDATE}-{unique_store_key}", data)
_async_update_sensor_entity(
hass, entity_type=entity_type, unique_store_key=unique_store_key, data=data
)
else:
data[CONF_UNIQUE_ID] = unique_store_key
data[CONF_NAME] = (
f"{config_entry.data[ATTR_DEVICE_NAME]} {data[ATTR_SENSOR_NAME]}"
)
register_signal = f"{DOMAIN}_{data[ATTR_SENSOR_TYPE]}_register"
register_signal = f"{DOMAIN}_{entity_type}_register"
async_dispatcher_send(hass, register_signal, data)
return webhook_response(
@@ -685,10 +686,12 @@ async def webhook_update_sensor_states(
continue
sensor[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID]
async_dispatcher_send(
_async_update_sensor_entity(
hass,
f"{SIGNAL_SENSOR_UPDATE}-{unique_store_key}",
sensor,
entity_type=entity_type,
unique_store_key=unique_store_key,
data=sensor,
)
resp[unique_id] = {"success": True}
@@ -697,11 +700,26 @@ async def webhook_update_sensor_states(
entry = entity_registry.async_get(entity_id)
if entry and entry.disabled_by:
# Inform the app that the entity is disabled
resp[unique_id]["is_disabled"] = True
return webhook_response(resp, registration=config_entry.data)
def _async_update_sensor_entity(
hass: HomeAssistant, entity_type: str, unique_store_key: str, data: dict[str, Any]
) -> None:
"""Update a sensor entity with new data."""
# Replace existing pending update with the latest sensor data.
hass.data[DOMAIN][DATA_PENDING_UPDATES][entity_type][unique_store_key] = data
# The signal might not be handled if the entity was just enabled, but the data is stored
# in pending updates and will be applied on entity initialization.
async_dispatcher_send(
hass, f"{SIGNAL_SENSOR_UPDATE}-{entity_type}-{unique_store_key}"
)
@WEBHOOK_COMMANDS.register("get_zones")
async def webhook_get_zones(
hass: HomeAssistant, config_entry: ConfigEntry, data: Any
@@ -0,0 +1,622 @@
"""Tests for mobile_app pending updates functionality."""
from http import HTTPStatus
from typing import Any
from aiohttp.test_utils import TestClient
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
async def test_pending_update_applied_when_entity_enabled(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
create_registrations: tuple[dict[str, Any], dict[str, Any]],
webhook_client: TestClient,
) -> None:
"""Test that updates sent while disabled are applied when entity is re-enabled."""
webhook_id = create_registrations[1]["webhook_id"]
webhook_url = f"/api/webhook/{webhook_id}"
# Register a sensor
reg_resp = await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": "Battery State",
"state": 100,
"type": "sensor",
"unique_id": "battery_state",
"unit_of_measurement": PERCENTAGE,
},
},
)
assert reg_resp.status == HTTPStatus.CREATED
await hass.async_block_till_done()
entity = hass.states.get("sensor.test_1_battery_state")
assert entity is not None
assert entity.state == "100"
# Disable the entity
entity_registry.async_update_entity(
"sensor.test_1_battery_state", disabled_by=er.RegistryEntryDisabler.USER
)
await hass.async_block_till_done()
# Send update while disabled
reg_resp = await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": "Battery State",
"state": 50,
"type": "sensor",
"unique_id": "battery_state",
"unit_of_measurement": PERCENTAGE,
},
},
)
assert reg_resp.status == HTTPStatus.CREATED
await hass.async_block_till_done()
# Re-enable the entity
entity_registry.async_update_entity("sensor.test_1_battery_state", disabled_by=None)
# Reload the config entry to trigger entity re-creation
config_entry = hass.config_entries.async_entries("mobile_app")[1]
await hass.config_entries.async_reload(config_entry.entry_id)
await hass.async_block_till_done()
# Verify the update sent while disabled was applied
entity = hass.states.get("sensor.test_1_battery_state")
assert entity is not None
assert entity.state == "50"
async def test_pending_update_with_attributes(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
create_registrations: tuple[dict[str, Any], dict[str, Any]],
webhook_client: TestClient,
) -> None:
"""Test that pending updates preserve all attributes."""
webhook_id = create_registrations[1]["webhook_id"]
webhook_url = f"/api/webhook/{webhook_id}"
# Register a sensor
reg_resp = await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": "Battery State",
"state": 100,
"type": "sensor",
"unique_id": "battery_state",
"attributes": {"charging": True, "voltage": 4.2},
"icon": "mdi:battery-charging",
},
},
)
assert reg_resp.status == HTTPStatus.CREATED
await hass.async_block_till_done()
# Disable the entity
entity_registry.async_update_entity(
"sensor.test_1_battery_state", disabled_by=er.RegistryEntryDisabler.USER
)
await hass.async_block_till_done()
# Send update with different attributes while disabled
reg_resp = await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": "Battery State",
"state": 50,
"type": "sensor",
"unique_id": "battery_state",
"attributes": {"charging": False, "voltage": 3.7},
"icon": "mdi:battery-50",
},
},
)
assert reg_resp.status == HTTPStatus.CREATED
await hass.async_block_till_done()
# Re-enable the entity
entity_registry.async_update_entity("sensor.test_1_battery_state", disabled_by=None)
# Reload the config entry
config_entry = hass.config_entries.async_entries("mobile_app")[1]
await hass.config_entries.async_reload(config_entry.entry_id)
await hass.async_block_till_done()
# Verify all attributes were applied
entity = hass.states.get("sensor.test_1_battery_state")
assert entity is not None
assert entity.state == "50"
assert entity.attributes["charging"] is False
assert entity.attributes["voltage"] == 3.7
assert entity.attributes["icon"] == "mdi:battery-50"
async def test_pending_update_overwritten_by_newer_update(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
create_registrations: tuple[dict[str, Any], dict[str, Any]],
webhook_client: TestClient,
) -> None:
"""Test that newer pending updates overwrite older ones."""
webhook_id = create_registrations[1]["webhook_id"]
webhook_url = f"/api/webhook/{webhook_id}"
# Register a sensor
reg_resp = await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": "Battery State",
"state": 100,
"type": "sensor",
"unique_id": "battery_state",
},
},
)
assert reg_resp.status == HTTPStatus.CREATED
await hass.async_block_till_done()
# Disable the entity
entity_registry.async_update_entity(
"sensor.test_1_battery_state", disabled_by=er.RegistryEntryDisabler.USER
)
await hass.async_block_till_done()
# Send first update while disabled
await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": "Battery State",
"state": 75,
"type": "sensor",
"unique_id": "battery_state",
},
},
)
await hass.async_block_till_done()
# Send second update while still disabled - should overwrite
await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": "Battery State",
"state": 25,
"type": "sensor",
"unique_id": "battery_state",
},
},
)
await hass.async_block_till_done()
# Re-enable the entity
entity_registry.async_update_entity("sensor.test_1_battery_state", disabled_by=None)
# Reload the config entry
config_entry = hass.config_entries.async_entries("mobile_app")[1]
await hass.config_entries.async_reload(config_entry.entry_id)
await hass.async_block_till_done()
# Verify the latest update was applied (25, not 75)
entity = hass.states.get("sensor.test_1_battery_state")
assert entity is not None
assert entity.state == "25"
async def test_pending_update_not_stored_on_enabled_entities(
hass: HomeAssistant,
create_registrations: tuple[dict[str, Any], dict[str, Any]],
webhook_client: TestClient,
) -> None:
"""Test that enabled entities receive updates immediately."""
webhook_id = create_registrations[1]["webhook_id"]
webhook_url = f"/api/webhook/{webhook_id}"
# Register a sensor
reg_resp = await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": "Battery State",
"state": 100,
"type": "sensor",
"unique_id": "battery_state",
},
},
)
assert reg_resp.status == HTTPStatus.CREATED
await hass.async_block_till_done()
entity = hass.states.get("sensor.test_1_battery_state")
assert entity is not None
assert entity.state == "100"
# Send update while enabled - should apply immediately
reg_resp = await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": "Battery State",
"state": 50,
"type": "sensor",
"unique_id": "battery_state",
},
},
)
assert reg_resp.status == HTTPStatus.CREATED
await hass.async_block_till_done()
# Verify update was applied immediately
entity = hass.states.get("sensor.test_1_battery_state")
assert entity is not None
assert entity.state == "50"
async def test_pending_update_fallback_to_restore_state(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
create_registrations: tuple[dict[str, Any], dict[str, Any]],
webhook_client: TestClient,
) -> None:
"""Test that restored state is used when no pending update exists."""
webhook_id = create_registrations[1]["webhook_id"]
webhook_url = f"/api/webhook/{webhook_id}"
# Register a sensor
reg_resp = await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": "Battery State",
"state": 100,
"type": "sensor",
"unique_id": "battery_state",
},
},
)
assert reg_resp.status == HTTPStatus.CREATED
await hass.async_block_till_done()
entity = hass.states.get("sensor.test_1_battery_state")
assert entity is not None
assert entity.state == "100"
# Update to a new state
await webhook_client.post(
webhook_url,
json={
"type": "update_sensor_states",
"data": [
{
"state": 75,
"type": "sensor",
"unique_id": "battery_state",
}
],
},
)
await hass.async_block_till_done()
entity = hass.states.get("sensor.test_1_battery_state")
assert entity is not None
assert entity.state == "75"
# Reload without pending updates
config_entry = hass.config_entries.async_entries("mobile_app")[1]
await hass.config_entries.async_reload(config_entry.entry_id)
await hass.async_block_till_done()
# Verify restored state was used
entity = hass.states.get("sensor.test_1_battery_state")
assert entity is not None
assert entity.state == "75"
async def test_multiple_pending_updates_for_different_sensors(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
create_registrations: tuple[dict[str, Any], dict[str, Any]],
webhook_client: TestClient,
) -> None:
"""Test that multiple sensors can be updated while disabled and applied when re-enabled."""
webhook_id = create_registrations[1]["webhook_id"]
webhook_url = f"/api/webhook/{webhook_id}"
# Register two sensors
for unique_id, state in (("battery_state", 100), ("battery_temp", 25)):
reg_resp = await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": unique_id.replace("_", " ").title(),
"state": state,
"type": "sensor",
"unique_id": unique_id,
},
},
)
assert reg_resp.status == HTTPStatus.CREATED
await hass.async_block_till_done()
# Disable both entities
entity_registry.async_update_entity(
"sensor.test_1_battery_state", disabled_by=er.RegistryEntryDisabler.USER
)
entity_registry.async_update_entity(
"sensor.test_1_battery_temp", disabled_by=er.RegistryEntryDisabler.USER
)
await hass.async_block_till_done()
# Send updates for both while disabled
await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": "Battery State",
"state": 50,
"type": "sensor",
"unique_id": "battery_state",
},
},
)
await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": "Battery Temp",
"state": 30,
"type": "sensor",
"unique_id": "battery_temp",
},
},
)
await hass.async_block_till_done()
# Re-enable both entities
entity_registry.async_update_entity("sensor.test_1_battery_state", disabled_by=None)
entity_registry.async_update_entity("sensor.test_1_battery_temp", disabled_by=None)
# Reload the config entry
config_entry = hass.config_entries.async_entries("mobile_app")[1]
await hass.config_entries.async_reload(config_entry.entry_id)
await hass.async_block_till_done()
# Verify both updates sent while disabled were applied
battery_state = hass.states.get("sensor.test_1_battery_state")
battery_temp = hass.states.get("sensor.test_1_battery_temp")
assert battery_state is not None
assert battery_state.state == "50"
assert battery_temp is not None
assert battery_temp.state == "30"
async def test_update_sensor_states_with_pending_updates(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
create_registrations: tuple[dict[str, Any], dict[str, Any]],
webhook_client: TestClient,
) -> None:
"""Test that update_sensor_states updates are applied when entity is re-enabled."""
webhook_id = create_registrations[1]["webhook_id"]
webhook_url = f"/api/webhook/{webhook_id}"
# Register a sensor
reg_resp = await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": "Battery State",
"state": 100,
"type": "sensor",
"unique_id": "battery_state",
"unit_of_measurement": PERCENTAGE,
},
},
)
assert reg_resp.status == HTTPStatus.CREATED
await hass.async_block_till_done()
entity = hass.states.get("sensor.test_1_battery_state")
assert entity is not None
assert entity.state == "100"
# Disable the entity
entity_registry.async_update_entity(
"sensor.test_1_battery_state", disabled_by=er.RegistryEntryDisabler.USER
)
await hass.async_block_till_done()
# Use update_sensor_states while disabled
resp = await webhook_client.post(
webhook_url,
json={
"type": "update_sensor_states",
"data": [
{
"state": 75,
"type": "sensor",
"unique_id": "battery_state",
}
],
},
)
assert resp.status == HTTPStatus.OK
await hass.async_block_till_done()
# Re-enable the entity
entity_registry.async_update_entity("sensor.test_1_battery_state", disabled_by=None)
# Reload the config entry to trigger entity re-creation
config_entry = hass.config_entries.async_entries("mobile_app")[1]
await hass.config_entries.async_reload(config_entry.entry_id)
await hass.async_block_till_done()
# Verify the update sent while disabled was applied
entity = hass.states.get("sensor.test_1_battery_state")
assert entity is not None
assert entity.state == "75"
async def test_update_sensor_states_always_stores_pending(
hass: HomeAssistant,
create_registrations: tuple[dict[str, Any], dict[str, Any]],
webhook_client: TestClient,
) -> None:
"""Test that update_sensor_states applies updates to enabled entities."""
webhook_id = create_registrations[1]["webhook_id"]
webhook_url = f"/api/webhook/{webhook_id}"
# Register a sensor
reg_resp = await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": "Battery State",
"state": 100,
"type": "sensor",
"unique_id": "battery_state",
},
},
)
assert reg_resp.status == HTTPStatus.CREATED
await hass.async_block_till_done()
entity = hass.states.get("sensor.test_1_battery_state")
assert entity is not None
assert entity.state == "100"
# Use update_sensor_states while enabled
resp = await webhook_client.post(
webhook_url,
json={
"type": "update_sensor_states",
"data": [
{
"state": 50,
"type": "sensor",
"unique_id": "battery_state",
}
],
},
)
assert resp.status == HTTPStatus.OK
await hass.async_block_till_done()
# Verify update was applied
entity = hass.states.get("sensor.test_1_battery_state")
assert entity is not None
assert entity.state == "50"
async def test_binary_sensor_pending_update(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
create_registrations: tuple[dict[str, Any], dict[str, Any]],
webhook_client: TestClient,
) -> None:
"""Test that binary sensor updates are applied when entity is re-enabled."""
webhook_id = create_registrations[1]["webhook_id"]
webhook_url = f"/api/webhook/{webhook_id}"
# Register a binary sensor
reg_resp = await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": "Motion Detected",
"state": False,
"type": "binary_sensor",
"unique_id": "motion_sensor",
},
},
)
assert reg_resp.status == HTTPStatus.CREATED
await hass.async_block_till_done()
entity = hass.states.get("binary_sensor.test_1_motion_detected")
assert entity is not None
assert entity.state == "off"
# Disable the entity
entity_registry.async_update_entity(
"binary_sensor.test_1_motion_detected",
disabled_by=er.RegistryEntryDisabler.USER,
)
await hass.async_block_till_done()
# Send update while disabled
reg_resp = await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": "Motion Detected",
"state": True,
"type": "binary_sensor",
"unique_id": "motion_sensor",
},
},
)
assert reg_resp.status == HTTPStatus.CREATED
await hass.async_block_till_done()
# Re-enable the entity
entity_registry.async_update_entity(
"binary_sensor.test_1_motion_detected", disabled_by=None
)
# Reload the config entry
config_entry = hass.config_entries.async_entries("mobile_app")[1]
await hass.config_entries.async_reload(config_entry.entry_id)
await hass.async_block_till_done()
# Verify the update sent while disabled was applied
entity = hass.states.get("binary_sensor.test_1_motion_detected")
assert entity is not None
assert entity.state == "on"