diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 8faa5ab44c1bc..a0078f9b2e37b 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -115,6 +115,12 @@ async def connect_callback(reconnect: bool) -> None: if entity.enabled: await entity.query_state() + async def disconnect_callback() -> None: + for entity in entities.values(): + if entity.enabled: + entity.cancel_tasks() + entity.async_write_ha_state() + async def update_callback(message: Status) -> None: if isinstance(message, status.Raw): return @@ -146,6 +152,7 @@ async def update_callback(message: Status) -> None: async_add_entities([zone_entity]) manager.callbacks.connect.append(connect_callback) + manager.callbacks.disconnect.append(disconnect_callback) manager.callbacks.update.append(update_callback) @@ -225,13 +232,13 @@ async def async_added_to_hass(self) -> None: await self.query_state() async def async_will_remove_from_hass(self) -> None: - """Cancel the tasks when the entity is removed.""" - if self._query_state_task is not None: - self._query_state_task.cancel() - self._query_state_task = None - if self._query_av_info_task is not None: - self._query_av_info_task.cancel() - self._query_av_info_task = None + """Entity will be removed from hass.""" + self.cancel_tasks() + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self._manager.connected async def query_state(self) -> None: """Query the receiver for all the info, that we care about.""" @@ -247,6 +254,15 @@ async def query_state(self) -> None: await self._manager.write(query.AudioInformation()) await self._manager.write(query.VideoInformation()) + def cancel_tasks(self) -> None: + """Cancel the tasks.""" + if self._query_state_task is not None: + self._query_state_task.cancel() + self._query_state_task = None + if self._query_av_info_task is not None: + self._query_av_info_task.cancel() + self._query_av_info_task = None + async def async_turn_on(self) -> None: """Turn the media player on.""" message = command.Power(self._zone, command.Power.Param.ON) diff --git a/homeassistant/components/onkyo/quality_scale.yaml b/homeassistant/components/onkyo/quality_scale.yaml index 758055a974c07..dcc2221c929d4 100644 --- a/homeassistant/components/onkyo/quality_scale.yaml +++ b/homeassistant/components/onkyo/quality_scale.yaml @@ -30,9 +30,9 @@ rules: config-entry-unloading: done docs-configuration-parameters: done docs-installation-parameters: done - entity-unavailable: todo + entity-unavailable: done integration-owner: done - log-when-unavailable: todo + log-when-unavailable: done parallel-updates: todo reauthentication-flow: status: exempt diff --git a/homeassistant/components/onkyo/receiver.py b/homeassistant/components/onkyo/receiver.py index 8fc5c5e7e0df9..f7542e40bee7c 100644 --- a/homeassistant/components/onkyo/receiver.py +++ b/homeassistant/components/onkyo/receiver.py @@ -28,11 +28,13 @@ class Callbacks: """Receiver callbacks.""" connect: list[Callable[[bool], Awaitable[None]]] = field(default_factory=list) + disconnect: list[Callable[[], Awaitable[None]]] = field(default_factory=list) update: list[Callable[[Status], Awaitable[None]]] = field(default_factory=list) def clear(self) -> None: """Clear all callbacks.""" self.connect.clear() + self.disconnect.clear() self.update.clear() @@ -43,6 +45,7 @@ class ReceiverManager: entry: OnkyoConfigEntry info: ReceiverInfo receiver: Receiver | None = None + connected: bool = False callbacks: Callbacks _started: asyncio.Event @@ -83,6 +86,7 @@ async def _run(self) -> None: while True: try: async with connect(self.info, retry=reconnect) as self.receiver: + self.connected = True if not reconnect: self._started.set() else: @@ -96,7 +100,9 @@ async def _run(self) -> None: reconnect = True finally: + self.connected = False _LOGGER.info("Disconnected: %s", self.info) + await self.on_disconnect() async def on_connect(self, reconnect: bool) -> None: """Receiver (re)connected.""" @@ -109,8 +115,13 @@ async def on_connect(self, reconnect: bool) -> None: for callback in self.callbacks.connect: await callback(reconnect) + async def on_disconnect(self) -> None: + """Receiver disconnected.""" + for callback in self.callbacks.disconnect: + await callback() + async def on_update(self, message: Status) -> None: - """Process new message from the receiver.""" + """New message from the receiver.""" for callback in self.callbacks.update: await callback(message) diff --git a/tests/components/onkyo/test_init.py b/tests/components/onkyo/test_init.py index 144947dcbe1e2..db4c8a640ebca 100644 --- a/tests/components/onkyo/test_init.py +++ b/tests/components/onkyo/test_init.py @@ -76,11 +76,20 @@ async def test_reconnect( assert mock_config_entry.state is ConfigEntryState.LOADED + manager = mock_config_entry.runtime_data.manager + assert manager.connected is True + + async def disconnect_assert() -> None: + assert manager.connected is False + + manager.callbacks.disconnect.append(disconnect_assert) + mock_connect.reset_mock() assert mock_connect.call_count == 0 - read_queue.put_nowait(None) # Simulate a disconnect + # Simulate a disconnect + read_queue.put_nowait(None) await asyncio.sleep(0) assert mock_connect.call_count == 1 diff --git a/tests/components/onkyo/test_media_player.py b/tests/components/onkyo/test_media_player.py index a2e71fd3f71c6..654d277ca988b 100644 --- a/tests/components/onkyo/test_media_player.py +++ b/tests/components/onkyo/test_media_player.py @@ -32,6 +32,7 @@ SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, + STATE_UNAVAILABLE, Platform, ) from homeassistant.core import HomeAssistant @@ -81,6 +82,30 @@ async def test_entities( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) +async def test_availability(hass: HomeAssistant, read_queue: asyncio.Queue) -> None: + """Test entity availability on disconnect and reconnect.""" + assert (state := hass.states.get(ENTITY_ID)) is not None + assert state.state != STATE_UNAVAILABLE + + # Simulate a disconnect + read_queue.put_nowait(None) + await asyncio.sleep(0) + + assert (state := hass.states.get(ENTITY_ID)) is not None + assert state.state == STATE_UNAVAILABLE + + # Simulate first status update after reconnect + read_queue.put_nowait( + status.Power( + Code.from_kind_zone(Kind.POWER, Zone.MAIN), None, status.Power.Param.ON + ) + ) + await asyncio.sleep(0) + + assert (state := hass.states.get(ENTITY_ID)) is not None + assert state.state != STATE_UNAVAILABLE + + @pytest.mark.parametrize( ("action", "action_data", "message"), [