Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions homeassistant/components/onkyo/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -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."""
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/onkyo/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion homeassistant/components/onkyo/receiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand All @@ -43,6 +45,7 @@ class ReceiverManager:
entry: OnkyoConfigEntry
info: ReceiverInfo
receiver: Receiver | None = None
connected: bool = False
callbacks: Callbacks

_started: asyncio.Event
Expand Down Expand Up @@ -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:
Expand All @@ -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."""
Expand All @@ -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)

Expand Down
11 changes: 10 additions & 1 deletion tests/components/onkyo/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
25 changes: 25 additions & 0 deletions tests/components/onkyo/test_media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET,
SERVICE_VOLUME_UP,
STATE_UNAVAILABLE,
Platform,
)
from homeassistant.core import HomeAssistant
Expand Down Expand Up @@ -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"),
[
Expand Down
Loading