|
1 | | -"""Test the ThermoPro config flow.""" |
| 1 | +"""Test the ThermoPro sensors.""" |
2 | 2 |
|
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 |
4 | 13 | from homeassistant.components.thermopro.const import DOMAIN |
5 | 14 | from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT |
6 | 15 | from homeassistant.core import HomeAssistant |
@@ -125,3 +134,125 @@ async def test_sensors(hass: HomeAssistant) -> None: |
125 | 134 |
|
126 | 135 | assert await hass.config_entries.async_unload(entry.entry_id) |
127 | 136 | 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