mirror of
https://github.com/home-assistant/core.git
synced 2025-07-08 22:07:10 +00:00
Create HEOS devices after integration setup (#138721)
* Create entities for new players * Fix docstring typo
This commit is contained in:
parent
82f2e72327
commit
34a33e0465
@ -16,6 +16,7 @@ from pyheos import (
|
|||||||
HeosError,
|
HeosError,
|
||||||
HeosNowPlayingMedia,
|
HeosNowPlayingMedia,
|
||||||
HeosOptions,
|
HeosOptions,
|
||||||
|
HeosPlayer,
|
||||||
MediaItem,
|
MediaItem,
|
||||||
MediaType,
|
MediaType,
|
||||||
PlayerUpdateResult,
|
PlayerUpdateResult,
|
||||||
@ -58,6 +59,7 @@ class HeosCoordinator(DataUpdateCoordinator[None]):
|
|||||||
credentials=credentials,
|
credentials=credentials,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self._platform_callbacks: list[Callable[[Sequence[HeosPlayer]], None]] = []
|
||||||
self._update_sources_pending: bool = False
|
self._update_sources_pending: bool = False
|
||||||
self._source_list: list[str] = []
|
self._source_list: list[str] = []
|
||||||
self._favorites: dict[int, MediaItem] = {}
|
self._favorites: dict[int, MediaItem] = {}
|
||||||
@ -124,6 +126,27 @@ class HeosCoordinator(DataUpdateCoordinator[None]):
|
|||||||
self.async_update_listeners()
|
self.async_update_listeners()
|
||||||
return remove_listener
|
return remove_listener
|
||||||
|
|
||||||
|
def async_add_platform_callback(
|
||||||
|
self, add_entities_callback: Callable[[Sequence[HeosPlayer]], None]
|
||||||
|
) -> None:
|
||||||
|
"""Add a callback to add entities for a platform."""
|
||||||
|
self._platform_callbacks.append(add_entities_callback)
|
||||||
|
|
||||||
|
def _async_handle_player_update_result(
|
||||||
|
self, update_result: PlayerUpdateResult
|
||||||
|
) -> None:
|
||||||
|
"""Handle a player update result."""
|
||||||
|
if update_result.added_player_ids and self._platform_callbacks:
|
||||||
|
new_players = [
|
||||||
|
self.heos.players[player_id]
|
||||||
|
for player_id in update_result.added_player_ids
|
||||||
|
]
|
||||||
|
for add_entities_callback in self._platform_callbacks:
|
||||||
|
add_entities_callback(new_players)
|
||||||
|
|
||||||
|
if update_result.updated_player_ids:
|
||||||
|
self._async_update_player_ids(update_result.updated_player_ids)
|
||||||
|
|
||||||
async def _async_on_auth_failure(self) -> None:
|
async def _async_on_auth_failure(self) -> None:
|
||||||
"""Handle when the user credentials are no longer valid."""
|
"""Handle when the user credentials are no longer valid."""
|
||||||
assert self.config_entry is not None
|
assert self.config_entry is not None
|
||||||
@ -147,8 +170,7 @@ class HeosCoordinator(DataUpdateCoordinator[None]):
|
|||||||
"""Handle a controller event, such as players or groups changed."""
|
"""Handle a controller event, such as players or groups changed."""
|
||||||
if event == const.EVENT_PLAYERS_CHANGED:
|
if event == const.EVENT_PLAYERS_CHANGED:
|
||||||
assert data is not None
|
assert data is not None
|
||||||
if data.updated_player_ids:
|
self._async_handle_player_update_result(data)
|
||||||
self._async_update_player_ids(data.updated_player_ids)
|
|
||||||
elif (
|
elif (
|
||||||
event in (const.EVENT_SOURCES_CHANGED, const.EVENT_USER_CHANGED)
|
event in (const.EVENT_SOURCES_CHANGED, const.EVENT_USER_CHANGED)
|
||||||
and not self._update_sources_pending
|
and not self._update_sources_pending
|
||||||
@ -242,9 +264,7 @@ class HeosCoordinator(DataUpdateCoordinator[None]):
|
|||||||
except HeosError as error:
|
except HeosError as error:
|
||||||
_LOGGER.error("Unable to refresh players: %s", error)
|
_LOGGER.error("Unable to refresh players: %s", error)
|
||||||
return
|
return
|
||||||
# After reconnecting, player_id may have changed
|
self._async_handle_player_update_result(player_updates)
|
||||||
if player_updates.updated_player_ids:
|
|
||||||
self._async_update_player_ids(player_updates.updated_player_ids)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_get_source_list(self) -> list[str]:
|
def async_get_source_list(self) -> list[str]:
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable, Coroutine
|
from collections.abc import Awaitable, Callable, Coroutine, Sequence
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import reduce, wraps
|
from functools import reduce, wraps
|
||||||
from operator import ior
|
from operator import ior
|
||||||
@ -93,11 +93,16 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add media players for a config entry."""
|
"""Add media players for a config entry."""
|
||||||
devices = [
|
|
||||||
HeosMediaPlayer(entry.runtime_data, player)
|
def add_entities_callback(players: Sequence[HeosPlayer]) -> None:
|
||||||
for player in entry.runtime_data.heos.players.values()
|
"""Add entities for each player."""
|
||||||
]
|
async_add_entities(
|
||||||
async_add_entities(devices)
|
[HeosMediaPlayer(entry.runtime_data, player) for player in players]
|
||||||
|
)
|
||||||
|
|
||||||
|
coordinator = entry.runtime_data
|
||||||
|
coordinator.async_add_platform_callback(add_entities_callback)
|
||||||
|
add_entities_callback(list(coordinator.heos.players.values()))
|
||||||
|
|
||||||
|
|
||||||
type _FuncType[**_P] = Callable[_P, Awaitable[Any]]
|
type _FuncType[**_P] = Callable[_P, Awaitable[Any]]
|
||||||
|
@ -49,7 +49,7 @@ rules:
|
|||||||
docs-supported-functions: done
|
docs-supported-functions: done
|
||||||
docs-troubleshooting: done
|
docs-troubleshooting: done
|
||||||
docs-use-cases: done
|
docs-use-cases: done
|
||||||
dynamic-devices: todo
|
dynamic-devices: done
|
||||||
entity-category: done
|
entity-category: done
|
||||||
entity-device-class: done
|
entity-device-class: done
|
||||||
entity-disabled-by-default: done
|
entity-disabled-by-default: done
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Iterator
|
from collections.abc import Callable, Iterator
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
from pyheos import (
|
from pyheos import (
|
||||||
@ -130,16 +130,17 @@ def system_info_fixture() -> HeosSystem:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="players")
|
@pytest.fixture(name="player_factory")
|
||||||
def players_fixture() -> dict[int, HeosPlayer]:
|
def player_factory_fixture() -> Callable[[int, str, str], HeosPlayer]:
|
||||||
"""Create two mock HeosPlayers."""
|
"""Return a method that creates players."""
|
||||||
players = {}
|
|
||||||
for i in (1, 2):
|
def factory(player_id: int, name: str, model: str) -> HeosPlayer:
|
||||||
player = HeosPlayer(
|
"""Create a player."""
|
||||||
player_id=i,
|
return HeosPlayer(
|
||||||
|
player_id=player_id,
|
||||||
group_id=999,
|
group_id=999,
|
||||||
name="Test Player" if i == 1 else f"Test Player {i}",
|
name=name,
|
||||||
model="HEOS Drive HS2" if i == 1 else "Speaker",
|
model=model,
|
||||||
serial="123456",
|
serial="123456",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
supported_version=True,
|
supported_version=True,
|
||||||
@ -147,26 +148,37 @@ def players_fixture() -> dict[int, HeosPlayer]:
|
|||||||
is_muted=False,
|
is_muted=False,
|
||||||
available=True,
|
available=True,
|
||||||
state=PlayState.STOP,
|
state=PlayState.STOP,
|
||||||
ip_address=f"127.0.0.{i}",
|
ip_address=f"127.0.0.{player_id}",
|
||||||
network=NetworkType.WIRED,
|
network=NetworkType.WIRED,
|
||||||
shuffle=False,
|
shuffle=False,
|
||||||
repeat=RepeatType.OFF,
|
repeat=RepeatType.OFF,
|
||||||
volume=25,
|
volume=25,
|
||||||
|
now_playing_media=HeosNowPlayingMedia(
|
||||||
|
type=MediaType.STATION,
|
||||||
|
song="Song",
|
||||||
|
station="Station Name",
|
||||||
|
album="Album",
|
||||||
|
artist="Artist",
|
||||||
|
image_url="http://",
|
||||||
|
album_id="1",
|
||||||
|
media_id="1",
|
||||||
|
queue_id=1,
|
||||||
|
source_id=10,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
player.now_playing_media = HeosNowPlayingMedia(
|
|
||||||
type=MediaType.STATION,
|
return factory
|
||||||
song="Song",
|
|
||||||
station="Station Name",
|
|
||||||
album="Album",
|
@pytest.fixture(name="players")
|
||||||
artist="Artist",
|
def players_fixture(
|
||||||
image_url="http://",
|
player_factory: Callable[[int, str, str], HeosPlayer],
|
||||||
album_id="1",
|
) -> dict[int, HeosPlayer]:
|
||||||
media_id="1",
|
"""Create two mock HeosPlayers."""
|
||||||
queue_id=1,
|
return {
|
||||||
source_id=10,
|
1: player_factory(1, "Test Player", "HEOS Drive HS2"),
|
||||||
)
|
2: player_factory(2, "Test Player 2", "Speaker"),
|
||||||
players[player.player_id] = player
|
}
|
||||||
return players
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="group")
|
@pytest.fixture(name="group")
|
||||||
|
@ -1,16 +1,26 @@
|
|||||||
"""Tests for the init module."""
|
"""Tests for the init module."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
from pyheos import HeosError, HeosOptions, SignalHeosEvent, SignalType
|
from pyheos import (
|
||||||
|
HeosError,
|
||||||
|
HeosOptions,
|
||||||
|
HeosPlayer,
|
||||||
|
PlayerUpdateResult,
|
||||||
|
SignalHeosEvent,
|
||||||
|
SignalType,
|
||||||
|
const,
|
||||||
|
)
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.heos.const import DOMAIN
|
from homeassistant.components.heos.const import DOMAIN
|
||||||
|
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from . import MockHeos
|
from . import MockHeos
|
||||||
@ -255,3 +265,64 @@ async def test_remove_config_entry_device(
|
|||||||
ws_client = await hass_ws_client(hass)
|
ws_client = await hass_ws_client(hass)
|
||||||
response = await ws_client.remove_device(device_entry.id, config_entry.entry_id)
|
response = await ws_client.remove_device(device_entry.id, config_entry.entry_id)
|
||||||
assert response["success"] == expected_result
|
assert response["success"] == expected_result
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reconnected_new_entities_created(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
controller: MockHeos,
|
||||||
|
player_factory: Callable[[int, str, str], HeosPlayer],
|
||||||
|
) -> None:
|
||||||
|
"""Test new entities are created for new players after reconnecting."""
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
|
||||||
|
# Assert initial entity doesn't exist
|
||||||
|
assert not entity_registry.async_get_entity_id(MEDIA_PLAYER_DOMAIN, DOMAIN, "3")
|
||||||
|
|
||||||
|
# Create player
|
||||||
|
players = controller.players.copy()
|
||||||
|
players[3] = player_factory(3, "Test Player 3", "HEOS Link")
|
||||||
|
controller.mock_set_players(players)
|
||||||
|
controller.load_players.return_value = PlayerUpdateResult([3], [], {})
|
||||||
|
|
||||||
|
# Simulate reconnection
|
||||||
|
await controller.dispatcher.wait_send(
|
||||||
|
SignalType.HEOS_EVENT, SignalHeosEvent.CONNECTED
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Assert new entity created
|
||||||
|
assert entity_registry.async_get_entity_id(MEDIA_PLAYER_DOMAIN, DOMAIN, "3")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_players_changed_new_entities_created(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
controller: MockHeos,
|
||||||
|
player_factory: Callable[[int, str, str], HeosPlayer],
|
||||||
|
) -> None:
|
||||||
|
"""Test new entities are created for new players on change event."""
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
|
||||||
|
# Assert initial entity doesn't exist
|
||||||
|
assert not entity_registry.async_get_entity_id(MEDIA_PLAYER_DOMAIN, DOMAIN, "3")
|
||||||
|
|
||||||
|
# Create player
|
||||||
|
players = controller.players.copy()
|
||||||
|
players[3] = player_factory(3, "Test Player 3", "HEOS Link")
|
||||||
|
controller.mock_set_players(players)
|
||||||
|
|
||||||
|
# Simulate players changed event
|
||||||
|
await controller.dispatcher.wait_send(
|
||||||
|
SignalType.CONTROLLER_EVENT,
|
||||||
|
const.EVENT_PLAYERS_CHANGED,
|
||||||
|
PlayerUpdateResult([3], [], {}),
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Assert new entity created
|
||||||
|
assert entity_registry.async_get_entity_id(MEDIA_PLAYER_DOMAIN, DOMAIN, "3")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user