Store Mobile app pending updates when enabling back an entity (#156026)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user