Skip to content

Commit 4eedc88

Browse files
Store Mobile app pending updates when enabling back an entity (home-assistant#156026)
Co-authored-by: Martin Hjelmare <[email protected]>
1 parent 343ea1b commit 4eedc88

File tree

7 files changed

+719
-40
lines changed

7 files changed

+719
-40
lines changed

homeassistant/components/mobile_app/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@
4141
DATA_CONFIG_ENTRIES,
4242
DATA_DELETED_IDS,
4343
DATA_DEVICES,
44+
DATA_PENDING_UPDATES,
4445
DATA_PUSH_CHANNEL,
4546
DATA_STORE,
4647
DOMAIN,
48+
SENSOR_TYPES,
4749
STORAGE_KEY,
4850
STORAGE_VERSION,
4951
)
@@ -75,6 +77,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
7577
DATA_DEVICES: {},
7678
DATA_PUSH_CHANNEL: {},
7779
DATA_STORE: store,
80+
DATA_PENDING_UPDATES: {sensor_type: {} for sensor_type in SENSOR_TYPES},
7881
}
7982

8083
hass.http.register_view(RegistrationsView())

homeassistant/components/mobile_app/binary_sensor.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from homeassistant.components.binary_sensor import BinarySensorEntity
66
from homeassistant.config_entries import ConfigEntry
7-
from homeassistant.const import CONF_WEBHOOK_ID, STATE_ON
7+
from homeassistant.const import CONF_WEBHOOK_ID, STATE_ON, STATE_UNKNOWN
88
from homeassistant.core import HomeAssistant, State, callback
99
from homeassistant.helpers import entity_registry as er
1010
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -75,8 +75,9 @@ class MobileAppBinarySensor(MobileAppEntity, BinarySensorEntity):
7575

7676
async def async_restore_last_state(self, last_state: State) -> None:
7777
"""Restore previous state."""
78-
await super().async_restore_last_state(last_state)
79-
self._config[ATTR_SENSOR_STATE] = last_state.state == STATE_ON
78+
if self._config[ATTR_SENSOR_STATE] in (None, STATE_UNKNOWN):
79+
await super().async_restore_last_state(last_state)
80+
self._config[ATTR_SENSOR_STATE] = last_state.state == STATE_ON
8081
self._async_update_attr_from_config()
8182

8283
@callback

homeassistant/components/mobile_app/const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
DATA_STORE = "store"
2121
DATA_NOTIFY = "notify"
2222
DATA_PUSH_CHANNEL = "push_channel"
23+
DATA_PENDING_UPDATES = "pending_updates"
2324

2425
ATTR_APP_DATA = "app_data"
2526
ATTR_APP_ID = "app_id"
@@ -94,3 +95,5 @@
9495
},
9596
extra=vol.ALLOW_EXTRA,
9697
)
98+
99+
SENSOR_TYPES = (ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR)

homeassistant/components/mobile_app/entity.py

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@
22

33
from __future__ import annotations
44

5-
from typing import Any
5+
import logging
66

77
from homeassistant.config_entries import ConfigEntry
8-
from homeassistant.const import ATTR_ICON, CONF_NAME, CONF_UNIQUE_ID, STATE_UNAVAILABLE
8+
from homeassistant.const import (
9+
ATTR_ICON,
10+
CONF_NAME,
11+
CONF_UNIQUE_ID,
12+
STATE_UNAVAILABLE,
13+
STATE_UNKNOWN,
14+
)
915
from homeassistant.core import State, callback
1016
from homeassistant.helpers.dispatcher import async_dispatcher_connect
1117
from homeassistant.helpers.restore_state import RestoreEntity
@@ -18,10 +24,15 @@
1824
ATTR_SENSOR_ICON,
1925
ATTR_SENSOR_STATE,
2026
ATTR_SENSOR_STATE_CLASS,
27+
ATTR_SENSOR_TYPE,
28+
DATA_PENDING_UPDATES,
29+
DOMAIN,
2130
SIGNAL_SENSOR_UPDATE,
2231
)
2332
from .helpers import device_info
2433

34+
_LOGGER = logging.getLogger(__name__)
35+
2536

2637
class MobileAppEntity(RestoreEntity):
2738
"""Representation of a mobile app entity."""
@@ -56,11 +67,14 @@ async def async_added_to_hass(self) -> None:
5667
self.async_on_remove(
5768
async_dispatcher_connect(
5869
self.hass,
59-
f"{SIGNAL_SENSOR_UPDATE}-{self._attr_unique_id}",
70+
f"{SIGNAL_SENSOR_UPDATE}-{self._config[ATTR_SENSOR_TYPE]}-{self._attr_unique_id}",
6071
self._handle_update,
6172
)
6273
)
6374

75+
# Apply any pending updates
76+
self._handle_update()
77+
6478
if (state := await self.async_get_last_state()) is None:
6579
return
6680

@@ -69,22 +83,38 @@ async def async_added_to_hass(self) -> None:
6983
async def async_restore_last_state(self, last_state: State) -> None:
7084
"""Restore previous state."""
7185
config = self._config
72-
config[ATTR_SENSOR_STATE] = last_state.state
73-
config[ATTR_SENSOR_ATTRIBUTES] = {
74-
**last_state.attributes,
75-
**self._config[ATTR_SENSOR_ATTRIBUTES],
76-
}
77-
if ATTR_ICON in last_state.attributes:
78-
config[ATTR_SENSOR_ICON] = last_state.attributes[ATTR_ICON]
86+
87+
# Only restore state if we don't have one already, since it can be set by a pending update
88+
if config[ATTR_SENSOR_STATE] in (None, STATE_UNKNOWN):
89+
config[ATTR_SENSOR_STATE] = last_state.state
90+
config[ATTR_SENSOR_ATTRIBUTES] = {
91+
**last_state.attributes,
92+
**self._config[ATTR_SENSOR_ATTRIBUTES],
93+
}
94+
if ATTR_ICON in last_state.attributes:
95+
config[ATTR_SENSOR_ICON] = last_state.attributes[ATTR_ICON]
7996

8097
@property
8198
def device_info(self):
8299
"""Return device registry information for this entity."""
83100
return device_info(self._registration)
84101

85102
@callback
86-
def _handle_update(self, data: dict[str, Any]) -> None:
103+
def _handle_update(self) -> None:
87104
"""Handle async event updates."""
88-
self._config.update(data)
105+
self._apply_pending_update()
89106
self._async_update_attr_from_config()
90107
self.async_write_ha_state()
108+
109+
def _apply_pending_update(self) -> None:
110+
"""Restore any pending update for this entity."""
111+
entity_type = self._config[ATTR_SENSOR_TYPE]
112+
pending_updates = self.hass.data[DOMAIN][DATA_PENDING_UPDATES][entity_type]
113+
if update := pending_updates.pop(self._attr_unique_id, None):
114+
_LOGGER.debug(
115+
"Applying pending update for %s: %s",
116+
self._attr_unique_id,
117+
update,
118+
)
119+
# Apply the pending update
120+
self._config.update(update)

homeassistant/components/mobile_app/sensor.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -86,24 +86,26 @@ class MobileAppSensor(MobileAppEntity, RestoreSensor):
8686

8787
async def async_restore_last_state(self, last_state: State) -> None:
8888
"""Restore previous state."""
89-
await super().async_restore_last_state(last_state)
9089
config = self._config
91-
if not (last_sensor_data := await self.async_get_last_sensor_data()):
92-
# Workaround to handle migration to RestoreSensor, can be removed
93-
# in HA Core 2023.4
94-
config[ATTR_SENSOR_STATE] = None
95-
webhook_id = self._entry.data[CONF_WEBHOOK_ID]
96-
if TYPE_CHECKING:
97-
assert self.unique_id is not None
98-
sensor_unique_id = _extract_sensor_unique_id(webhook_id, self.unique_id)
99-
if (
100-
self.device_class == SensorDeviceClass.TEMPERATURE
101-
and sensor_unique_id == "battery_temperature"
102-
):
103-
config[ATTR_SENSOR_UOM] = UnitOfTemperature.CELSIUS
104-
else:
105-
config[ATTR_SENSOR_STATE] = last_sensor_data.native_value
106-
config[ATTR_SENSOR_UOM] = last_sensor_data.native_unit_of_measurement
90+
if config[ATTR_SENSOR_STATE] in (None, STATE_UNKNOWN):
91+
await super().async_restore_last_state(last_state)
92+
93+
if not (last_sensor_data := await self.async_get_last_sensor_data()):
94+
# Workaround to handle migration to RestoreSensor, can be removed
95+
# in HA Core 2023.4
96+
config[ATTR_SENSOR_STATE] = None
97+
webhook_id = self._entry.data[CONF_WEBHOOK_ID]
98+
if TYPE_CHECKING:
99+
assert self.unique_id is not None
100+
sensor_unique_id = _extract_sensor_unique_id(webhook_id, self.unique_id)
101+
if (
102+
self.device_class == SensorDeviceClass.TEMPERATURE
103+
and sensor_unique_id == "battery_temperature"
104+
):
105+
config[ATTR_SENSOR_UOM] = UnitOfTemperature.CELSIUS
106+
else:
107+
config[ATTR_SENSOR_STATE] = last_sensor_data.native_value
108+
config[ATTR_SENSOR_UOM] = last_sensor_data.native_unit_of_measurement
107109

108110
self._async_update_attr_from_config()
109111

homeassistant/components/mobile_app/webhook.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@
7979
ATTR_SENSOR_STATE,
8080
ATTR_SENSOR_STATE_CLASS,
8181
ATTR_SENSOR_TYPE,
82-
ATTR_SENSOR_TYPE_BINARY_SENSOR,
8382
ATTR_SENSOR_TYPE_SENSOR,
8483
ATTR_SENSOR_UNIQUE_ID,
8584
ATTR_SENSOR_UOM,
@@ -98,12 +97,14 @@
9897
DATA_CONFIG_ENTRIES,
9998
DATA_DELETED_IDS,
10099
DATA_DEVICES,
100+
DATA_PENDING_UPDATES,
101101
DOMAIN,
102102
ERR_ENCRYPTION_ALREADY_ENABLED,
103103
ERR_ENCRYPTION_REQUIRED,
104104
ERR_INVALID_FORMAT,
105105
ERR_SENSOR_NOT_REGISTERED,
106106
SCHEMA_APP_DATA,
107+
SENSOR_TYPES,
107108
SIGNAL_LOCATION_UPDATE,
108109
SIGNAL_SENSOR_UPDATE,
109110
)
@@ -125,8 +126,6 @@
125126
str, Callable[[HomeAssistant, ConfigEntry, Any], Coroutine[Any, Any, Response]]
126127
] = Registry()
127128

128-
SENSOR_TYPES = (ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR)
129-
130129
WEBHOOK_PAYLOAD_SCHEMA = vol.Any(
131130
vol.Schema(
132131
{
@@ -601,14 +600,16 @@ async def webhook_register_sensor(
601600
if changes:
602601
entity_registry.async_update_entity(existing_sensor, **changes)
603602

604-
async_dispatcher_send(hass, f"{SIGNAL_SENSOR_UPDATE}-{unique_store_key}", data)
603+
_async_update_sensor_entity(
604+
hass, entity_type=entity_type, unique_store_key=unique_store_key, data=data
605+
)
605606
else:
606607
data[CONF_UNIQUE_ID] = unique_store_key
607608
data[CONF_NAME] = (
608609
f"{config_entry.data[ATTR_DEVICE_NAME]} {data[ATTR_SENSOR_NAME]}"
609610
)
610611

611-
register_signal = f"{DOMAIN}_{data[ATTR_SENSOR_TYPE]}_register"
612+
register_signal = f"{DOMAIN}_{entity_type}_register"
612613
async_dispatcher_send(hass, register_signal, data)
613614

614615
return webhook_response(
@@ -685,10 +686,12 @@ async def webhook_update_sensor_states(
685686
continue
686687

687688
sensor[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID]
688-
async_dispatcher_send(
689+
690+
_async_update_sensor_entity(
689691
hass,
690-
f"{SIGNAL_SENSOR_UPDATE}-{unique_store_key}",
691-
sensor,
692+
entity_type=entity_type,
693+
unique_store_key=unique_store_key,
694+
data=sensor,
692695
)
693696

694697
resp[unique_id] = {"success": True}
@@ -697,11 +700,26 @@ async def webhook_update_sensor_states(
697700
entry = entity_registry.async_get(entity_id)
698701

699702
if entry and entry.disabled_by:
703+
# Inform the app that the entity is disabled
700704
resp[unique_id]["is_disabled"] = True
701705

702706
return webhook_response(resp, registration=config_entry.data)
703707

704708

709+
def _async_update_sensor_entity(
710+
hass: HomeAssistant, entity_type: str, unique_store_key: str, data: dict[str, Any]
711+
) -> None:
712+
"""Update a sensor entity with new data."""
713+
# Replace existing pending update with the latest sensor data.
714+
hass.data[DOMAIN][DATA_PENDING_UPDATES][entity_type][unique_store_key] = data
715+
716+
# The signal might not be handled if the entity was just enabled, but the data is stored
717+
# in pending updates and will be applied on entity initialization.
718+
async_dispatcher_send(
719+
hass, f"{SIGNAL_SENSOR_UPDATE}-{entity_type}-{unique_store_key}"
720+
)
721+
722+
705723
@WEBHOOK_COMMANDS.register("get_zones")
706724
async def webhook_get_zones(
707725
hass: HomeAssistant, config_entry: ConfigEntry, data: Any

0 commit comments

Comments
 (0)