Skip to content

Commit 41e42b9

Browse files
Fix Thermopro 'Device not available' on Restart (home-assistant#155929)
1 parent 51f68f2 commit 41e42b9

File tree

2 files changed

+136
-3
lines changed

2 files changed

+136
-3
lines changed

homeassistant/components/thermopro/sensor.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ async def async_setup_entry(
123123
ThermoProBluetoothSensorEntity, async_add_entities
124124
)
125125
)
126-
entry.async_on_unload(coordinator.async_register_processor(processor))
126+
entry.async_on_unload(
127+
coordinator.async_register_processor(processor, SensorEntityDescription)
128+
)
127129

128130

129131
class ThermoProBluetoothSensorEntity(

tests/components/thermopro/test_sensor.py

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1-
"""Test the ThermoPro config flow."""
1+
"""Test the ThermoPro sensors."""
22

3-
from homeassistant.components.sensor import ATTR_STATE_CLASS
3+
from unittest.mock import MagicMock
4+
5+
import pytest
6+
7+
from homeassistant.components.bluetooth.passive_update_processor import (
8+
PassiveBluetoothDataProcessor,
9+
)
10+
from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntityDescription
11+
import homeassistant.components.thermopro as thermopro_integration
12+
from homeassistant.components.thermopro import sensor as thermopro_sensor
413
from homeassistant.components.thermopro.const import DOMAIN
514
from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT
615
from homeassistant.core import HomeAssistant
@@ -125,3 +134,125 @@ async def test_sensors(hass: HomeAssistant) -> None:
125134

126135
assert await hass.config_entries.async_unload(entry.entry_id)
127136
await hass.async_block_till_done()
137+
138+
139+
class CoordinatorStub:
140+
"""Coordinator stub for testing entity restoration behavior."""
141+
142+
instances: list["CoordinatorStub"] = []
143+
144+
def __init__(
145+
self,
146+
hass: HomeAssistant | None = None,
147+
logger: MagicMock | None = None,
148+
*,
149+
address: str | None = None,
150+
mode: MagicMock | None = None,
151+
update_method: MagicMock | None = None,
152+
) -> None:
153+
"""Initialize coordinator stub with signature matching real coordinator."""
154+
# Track created instances to avoid direct hass.data access in tests
155+
CoordinatorStub.instances.append(self)
156+
self.calls: list[tuple[MagicMock, type | None]] = []
157+
self._saw_sensor_entity_description = False
158+
self._restore_cb: MagicMock | None = None
159+
160+
def async_register_processor(
161+
self, processor: MagicMock, entity_description_cls: type | None = None
162+
) -> MagicMock:
163+
"""Register a processor and track if SensorEntityDescription was provided."""
164+
self.calls.append((processor, entity_description_cls))
165+
166+
if entity_description_cls is SensorEntityDescription:
167+
self._saw_sensor_entity_description = True
168+
169+
return lambda: None
170+
171+
def async_start(self) -> MagicMock:
172+
"""Return a no-op unsub function for start lifecycle."""
173+
return lambda: None
174+
175+
def trigger_restore_from_test(self) -> None:
176+
"""Trigger restoration callback if available."""
177+
if self._saw_sensor_entity_description and self._restore_cb:
178+
self._restore_cb([])
179+
180+
def set_restore_callback(self, callback: MagicMock) -> None:
181+
"""Set the callback used to restore entities during the test."""
182+
self._restore_cb = callback
183+
184+
185+
async def test_thermopro_restores_entities_on_restart_behavior(
186+
hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch
187+
) -> None:
188+
"""Test that entities are restored on restart via SensorEntityDescription."""
189+
190+
add_entities_callbacks: list[MagicMock] = []
191+
192+
orig_add_listener = PassiveBluetoothDataProcessor.async_add_entities_listener
193+
194+
def wrapped_add_listener(
195+
self: PassiveBluetoothDataProcessor,
196+
entity_cls: type,
197+
add_entities: MagicMock,
198+
) -> MagicMock:
199+
add_entities_callbacks.append(add_entities)
200+
return orig_add_listener(self, entity_cls, add_entities)
201+
202+
monkeypatch.setattr(
203+
PassiveBluetoothDataProcessor,
204+
"async_add_entities_listener",
205+
wrapped_add_listener,
206+
)
207+
208+
first_called = {"v": False}
209+
second_called = {"v": False}
210+
211+
def add_entities_first(entities: list) -> None:
212+
first_called["v"] = True
213+
214+
def add_entities_second(entities: list) -> None:
215+
second_called["v"] = True
216+
217+
# Patch the integration to avoid platform forwarding and use the coordinator stub
218+
monkeypatch.setattr(thermopro_integration, "PLATFORMS", [])
219+
monkeypatch.setattr(
220+
thermopro_integration, "PassiveBluetoothProcessorCoordinator", CoordinatorStub
221+
)
222+
# Ensure a clean slate for stub instance tracking
223+
CoordinatorStub.instances.clear()
224+
225+
# First setup using real config entry setup to populate hass.data
226+
entry1 = MockConfigEntry(domain=DOMAIN, unique_id="00:11:22:33:44:55")
227+
entry1.add_to_hass(hass)
228+
assert await hass.config_entries.async_setup(entry1.entry_id)
229+
await hass.async_block_till_done()
230+
231+
# Manually set up sensor platform with our callback
232+
await thermopro_sensor.async_setup_entry(hass, entry1, add_entities_first)
233+
await hass.async_block_till_done()
234+
235+
coord = CoordinatorStub.instances[0]
236+
assert coord.calls, "Processor was not registered on first setup"
237+
assert not first_called["v"]
238+
239+
# Second setup (simulating restart)
240+
entry2 = MockConfigEntry(domain=DOMAIN, unique_id="AA:BB:CC:DD:EE:FF")
241+
entry2.add_to_hass(hass)
242+
assert await hass.config_entries.async_setup(entry2.entry_id)
243+
await hass.async_block_till_done()
244+
245+
await thermopro_sensor.async_setup_entry(hass, entry2, add_entities_second)
246+
await hass.async_block_till_done()
247+
248+
assert add_entities_callbacks, "No add_entities callback was registered"
249+
coord2 = CoordinatorStub.instances[1]
250+
coord2.set_restore_callback(add_entities_callbacks[-1])
251+
252+
coord2.trigger_restore_from_test()
253+
await hass.async_block_till_done()
254+
255+
assert second_called["v"], (
256+
"ThermoPro did not trigger restoration on startup. "
257+
"Ensure async_register_processor(processor, SensorEntityDescription) is used."
258+
)

0 commit comments

Comments
 (0)