1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-20 02:48:57 +00:00

Fix Thermopro 'Device not available' on Restart (#155929)

This commit is contained in:
Jeremiah
2025-11-23 15:36:51 +01:00
committed by GitHub
parent 51f68f2776
commit 41e42b9581
2 changed files with 136 additions and 3 deletions

View File

@@ -123,7 +123,9 @@ async def async_setup_entry(
ThermoProBluetoothSensorEntity, async_add_entities ThermoProBluetoothSensorEntity, async_add_entities
) )
) )
entry.async_on_unload(coordinator.async_register_processor(processor)) entry.async_on_unload(
coordinator.async_register_processor(processor, SensorEntityDescription)
)
class ThermoProBluetoothSensorEntity( class ThermoProBluetoothSensorEntity(

View File

@@ -1,6 +1,15 @@
"""Test the ThermoPro config flow.""" """Test the ThermoPro sensors."""
from homeassistant.components.sensor import ATTR_STATE_CLASS from unittest.mock import MagicMock
import pytest
from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothDataProcessor,
)
from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntityDescription
import homeassistant.components.thermopro as thermopro_integration
from homeassistant.components.thermopro import sensor as thermopro_sensor
from homeassistant.components.thermopro.const import DOMAIN from homeassistant.components.thermopro.const import DOMAIN
from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -125,3 +134,125 @@ async def test_sensors(hass: HomeAssistant) -> None:
assert await hass.config_entries.async_unload(entry.entry_id) assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
class CoordinatorStub:
"""Coordinator stub for testing entity restoration behavior."""
instances: list["CoordinatorStub"] = []
def __init__(
self,
hass: HomeAssistant | None = None,
logger: MagicMock | None = None,
*,
address: str | None = None,
mode: MagicMock | None = None,
update_method: MagicMock | None = None,
) -> None:
"""Initialize coordinator stub with signature matching real coordinator."""
# Track created instances to avoid direct hass.data access in tests
CoordinatorStub.instances.append(self)
self.calls: list[tuple[MagicMock, type | None]] = []
self._saw_sensor_entity_description = False
self._restore_cb: MagicMock | None = None
def async_register_processor(
self, processor: MagicMock, entity_description_cls: type | None = None
) -> MagicMock:
"""Register a processor and track if SensorEntityDescription was provided."""
self.calls.append((processor, entity_description_cls))
if entity_description_cls is SensorEntityDescription:
self._saw_sensor_entity_description = True
return lambda: None
def async_start(self) -> MagicMock:
"""Return a no-op unsub function for start lifecycle."""
return lambda: None
def trigger_restore_from_test(self) -> None:
"""Trigger restoration callback if available."""
if self._saw_sensor_entity_description and self._restore_cb:
self._restore_cb([])
def set_restore_callback(self, callback: MagicMock) -> None:
"""Set the callback used to restore entities during the test."""
self._restore_cb = callback
async def test_thermopro_restores_entities_on_restart_behavior(
hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test that entities are restored on restart via SensorEntityDescription."""
add_entities_callbacks: list[MagicMock] = []
orig_add_listener = PassiveBluetoothDataProcessor.async_add_entities_listener
def wrapped_add_listener(
self: PassiveBluetoothDataProcessor,
entity_cls: type,
add_entities: MagicMock,
) -> MagicMock:
add_entities_callbacks.append(add_entities)
return orig_add_listener(self, entity_cls, add_entities)
monkeypatch.setattr(
PassiveBluetoothDataProcessor,
"async_add_entities_listener",
wrapped_add_listener,
)
first_called = {"v": False}
second_called = {"v": False}
def add_entities_first(entities: list) -> None:
first_called["v"] = True
def add_entities_second(entities: list) -> None:
second_called["v"] = True
# Patch the integration to avoid platform forwarding and use the coordinator stub
monkeypatch.setattr(thermopro_integration, "PLATFORMS", [])
monkeypatch.setattr(
thermopro_integration, "PassiveBluetoothProcessorCoordinator", CoordinatorStub
)
# Ensure a clean slate for stub instance tracking
CoordinatorStub.instances.clear()
# First setup using real config entry setup to populate hass.data
entry1 = MockConfigEntry(domain=DOMAIN, unique_id="00:11:22:33:44:55")
entry1.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry1.entry_id)
await hass.async_block_till_done()
# Manually set up sensor platform with our callback
await thermopro_sensor.async_setup_entry(hass, entry1, add_entities_first)
await hass.async_block_till_done()
coord = CoordinatorStub.instances[0]
assert coord.calls, "Processor was not registered on first setup"
assert not first_called["v"]
# Second setup (simulating restart)
entry2 = MockConfigEntry(domain=DOMAIN, unique_id="AA:BB:CC:DD:EE:FF")
entry2.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry2.entry_id)
await hass.async_block_till_done()
await thermopro_sensor.async_setup_entry(hass, entry2, add_entities_second)
await hass.async_block_till_done()
assert add_entities_callbacks, "No add_entities callback was registered"
coord2 = CoordinatorStub.instances[1]
coord2.set_restore_callback(add_entities_callbacks[-1])
coord2.trigger_restore_from_test()
await hass.async_block_till_done()
assert second_called["v"], (
"ThermoPro did not trigger restoration on startup. "
"Ensure async_register_processor(processor, SensorEntityDescription) is used."
)