diff --git a/homeassistant/components/alexa_devices/__init__.py b/homeassistant/components/alexa_devices/__init__.py index f5ba46e534ac..067f36b9fc5d 100644 --- a/homeassistant/components/alexa_devices/__init__.py +++ b/homeassistant/components/alexa_devices/__init__.py @@ -61,7 +61,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo for sync_call in ( coordinator.sync_todo_list_items, coordinator.sync_history_state, - coordinator.sync_media_state, ): await _async_initial_sync(sync_call) diff --git a/homeassistant/components/alexa_devices/coordinator.py b/homeassistant/components/alexa_devices/coordinator.py index faf557f3c740..3e7b23c19a04 100644 --- a/homeassistant/components/alexa_devices/coordinator.py +++ b/homeassistant/components/alexa_devices/coordinator.py @@ -216,10 +216,16 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]): ) from err else: current_devices = set(data.keys()) + device_list_changed = current_devices != self.previous_devices if stale_devices := self.previous_devices - current_devices: await self._async_remove_device_stale(stale_devices) self.previous_devices = current_devices + # sync data on first refresh and after the device list changes + # self.data is None only on the first refresh + if self.data is None or device_list_changed: + await self._async_sync_on_device_list_change() + current_routines = { f"{slugify(self.config_entry.unique_id)}-{slugify(routine)}" for routine in self.api.routines @@ -238,6 +244,18 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]): return data + async def _async_sync_on_device_list_change(self) -> None: + """Sync per-device state on first refresh and after the device list changes.""" + for sync_call in (self.sync_media_state,): + try: + await sync_call() + except ConfigEntryNotReady as err: + LOGGER.warning( + "Sync failed for %s: %s. Data may be missing or incomplete until updates are pushed by Amazon", + sync_call.__name__, + err, + ) + async def _async_remove_device_stale( self, stale_devices: set[str], diff --git a/tests/components/alexa_devices/test_coordinator.py b/tests/components/alexa_devices/test_coordinator.py index 07c50e63f8ce..b2a720d520f1 100644 --- a/tests/components/alexa_devices/test_coordinator.py +++ b/tests/components/alexa_devices/test_coordinator.py @@ -213,3 +213,71 @@ async def test_sync_media_state_auth_failed( await setup_integration(hass, mock_config_entry) assert mock_config_entry.state is expected_state + + +@pytest.mark.parametrize( + "error", + [ + pytest.param( + CannotConnect("429 - Too Many Requests"), + id="http_429_too_many_requests", + ), + pytest.param( + CannotRetrieveData("503 - Service Unavailable"), + id="http_503_service_unavailable", + ), + ], +) +async def test_media_state_sync_failure_logged_on_first_refresh( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + error: Exception, +) -> None: + """Test a failing media sync on first refresh is logged but does not block setup.""" + mock_amazon_devices_client.sync_media_state.side_effect = error + + await setup_integration(hass, mock_config_entry) + + assert "Sync failed for sync_media_state:" in caplog.text + assert str(error) in caplog.text + assert ( + "Data may be missing or incomplete until updates are pushed by Amazon" + in caplog.text + ) + assert mock_config_entry.state is ConfigEntryState.LOADED + + +async def test_media_state_sync_on_device_list_change( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test media state is resynced when a device is added.""" + mock_amazon_devices_client.get_devices_data.return_value = { + TEST_DEVICE_1_SN: TEST_DEVICE_1, + } + + await setup_integration(hass, mock_config_entry) + + mock_amazon_devices_client.sync_media_state.assert_awaited_once() + + mock_amazon_devices_client.get_devices_data.return_value = { + TEST_DEVICE_1_SN: TEST_DEVICE_1, + TEST_DEVICE_2_SN: TEST_DEVICE_2, + } + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert mock_amazon_devices_client.sync_media_state.call_count == 2 + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Device list unchanged: no additional resync + assert mock_amazon_devices_client.sync_media_state.call_count == 2 diff --git a/tests/components/alexa_devices/test_init.py b/tests/components/alexa_devices/test_init.py index b0e781d02ca1..2ce63291cc9e 100644 --- a/tests/components/alexa_devices/test_init.py +++ b/tests/components/alexa_devices/test_init.py @@ -43,11 +43,6 @@ def _fail_history_state(client: AsyncMock, error: Exception) -> None: client.sync_history_state.side_effect = error -def _fail_media_state(client: AsyncMock, error: Exception) -> None: - """Make the media state sync fail.""" - client.sync_media_state.side_effect = error - - async def test_device_info( hass: HomeAssistant, snapshot: SnapshotAssertion, @@ -252,7 +247,6 @@ async def test_http2_stop_processing_called_on_shutdown( [ (_fail_todo_list_items, "sync_todo_list_items"), (_fail_history_state, "sync_history_state"), - (_fail_media_state, "sync_media_state"), ], ) @pytest.mark.parametrize(