Bump pyheos to 1.0.0 (#135415)

pull/130408/head^2
Andrew Sayre 2025-01-11 23:06:06 -06:00 committed by GitHub
parent 52c57eb2e5
commit 11fa6b2e4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 217 additions and 227 deletions

View File

@ -6,6 +6,7 @@ import asyncio
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import Any
from pyheos import (
Credentials,
@ -13,6 +14,8 @@ from pyheos import (
HeosError,
HeosOptions,
HeosPlayer,
PlayerUpdateResult,
SignalHeosEvent,
const as heos_const,
)
@ -98,14 +101,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: HeosConfigEntry) -> bool
# Auth failure handler must be added before connecting to the host, otherwise
# the event will be missed when login fails during connection.
async def auth_failure(event: str) -> None:
async def auth_failure() -> None:
"""Handle authentication failure."""
if event == heos_const.EVENT_USER_CREDENTIALS_INVALID:
entry.async_start_reauth(hass)
entry.async_on_unload(
controller.dispatcher.connect(heos_const.SIGNAL_HEOS_EVENT, auth_failure)
)
entry.async_on_unload(controller.add_on_user_credentials_invalid(auth_failure))
try:
# Auto reconnect only operates if initial connection was successful.
@ -168,11 +168,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: HeosConfigEntry) -> boo
class ControllerManager:
"""Class that manages events of the controller."""
def __init__(self, hass, controller):
def __init__(self, hass: HomeAssistant, controller: Heos) -> None:
"""Init the controller manager."""
self._hass = hass
self._device_registry = None
self._entity_registry = None
self._device_registry: dr.DeviceRegistry | None = None
self._entity_registry: er.EntityRegistry | None = None
self.controller = controller
async def connect_listeners(self):
@ -181,56 +181,59 @@ class ControllerManager:
self._entity_registry = er.async_get(self._hass)
# Handle controller events
self.controller.dispatcher.connect(
heos_const.SIGNAL_CONTROLLER_EVENT, self._controller_event
)
self.controller.add_on_controller_event(self._controller_event)
# Handle connection-related events
self.controller.dispatcher.connect(
heos_const.SIGNAL_HEOS_EVENT, self._heos_event
)
self.controller.add_on_heos_event(self._heos_event)
async def disconnect(self):
"""Disconnect subscriptions."""
self.controller.dispatcher.disconnect_all()
await self.controller.disconnect()
async def _controller_event(self, event, data):
async def _controller_event(
self, event: str, data: PlayerUpdateResult | None
) -> None:
"""Handle controller event."""
if event == heos_const.EVENT_PLAYERS_CHANGED:
self.update_ids(data[heos_const.DATA_MAPPED_IDS])
assert data is not None
self.update_ids(data.updated_player_ids)
# Update players
async_dispatcher_send(self._hass, SIGNAL_HEOS_UPDATED)
async def _heos_event(self, event):
"""Handle connection event."""
if event == heos_const.EVENT_CONNECTED:
if event == SignalHeosEvent.CONNECTED:
try:
# Retrieve latest players and refresh status
data = await self.controller.load_players()
self.update_ids(data[heos_const.DATA_MAPPED_IDS])
self.update_ids(data.updated_player_ids)
except HeosError as ex:
_LOGGER.error("Unable to refresh players: %s", ex)
# Update players
_LOGGER.debug("HEOS Controller event called, calling dispatcher")
async_dispatcher_send(self._hass, SIGNAL_HEOS_UPDATED)
def update_ids(self, mapped_ids: dict[int, int]):
"""Update the IDs in the device and entity registry."""
# mapped_ids contains the mapped IDs (new:old)
for new_id, old_id in mapped_ids.items():
for old_id, new_id in mapped_ids.items():
# update device registry
assert self._device_registry is not None
entry = self._device_registry.async_get_device(
identifiers={(DOMAIN, old_id)}
identifiers={(DOMAIN, old_id)} # type: ignore[arg-type] # Fix in the future
)
new_identifiers = {(DOMAIN, new_id)}
if entry:
self._device_registry.async_update_device(
entry.id, new_identifiers=new_identifiers
entry.id,
new_identifiers=new_identifiers, # type: ignore[arg-type] # Fix in the future
)
_LOGGER.debug(
"Updated device %s identifiers to %s", entry.id, new_identifiers
)
# update entity registry
assert self._entity_registry is not None
entity_id = self._entity_registry.async_get_entity_id(
Platform.MEDIA_PLAYER, DOMAIN, str(old_id)
)
@ -249,7 +252,7 @@ class GroupManager:
) -> None:
"""Init group manager."""
self._hass = hass
self._group_membership: dict[str, str] = {}
self._group_membership: dict[str, list[str]] = {}
self._disconnect_player_added = None
self._initialized = False
self.controller = controller
@ -268,7 +271,7 @@ class GroupManager:
}
try:
groups = await self.controller.get_groups(refresh=True)
groups = await self.controller.get_groups()
except HeosError as err:
_LOGGER.error("Unable to get HEOS group info: %s", err)
return group_info_by_entity_id
@ -326,13 +329,8 @@ class GroupManager:
err,
)
async def async_update_groups(self, event, data=None):
async def async_update_groups(self) -> None:
"""Update the group membership from the controller."""
if event in (
heos_const.EVENT_GROUPS_CHANGED,
heos_const.EVENT_CONNECTED,
SIGNAL_HEOS_PLAYER_ADDED,
):
if groups := await self.async_get_group_membership():
self._group_membership = groups
_LOGGER.debug("Groups updated due to change event")
@ -341,14 +339,16 @@ class GroupManager:
else:
_LOGGER.debug("Groups empty")
@callback
def connect_update(self):
"""Connect listener for when groups change and signal player update."""
self.controller.dispatcher.connect(
heos_const.SIGNAL_CONTROLLER_EVENT, self.async_update_groups
)
self.controller.dispatcher.connect(
heos_const.SIGNAL_HEOS_EVENT, self.async_update_groups
)
async def _on_controller_event(event: str, data: Any | None) -> None:
if event == heos_const.EVENT_GROUPS_CHANGED:
await self.async_update_groups()
self.controller.add_on_controller_event(_on_controller_event)
self.controller.add_on_connected(self.async_update_groups)
# When adding a new HEOS player we need to update the groups.
async def _async_handle_player_added():
@ -356,7 +356,7 @@ class GroupManager:
# fully populated yet. This may only happen during early startup.
if len(self.players) <= len(self.entity_id_map) and not self._initialized:
self._initialized = True
await self.async_update_groups(SIGNAL_HEOS_PLAYER_ADDED)
await self.async_update_groups()
self._disconnect_player_added = async_dispatcher_connect(
self._hass, SIGNAL_HEOS_PLAYER_ADDED, _async_handle_player_added
@ -462,7 +462,8 @@ class SourceManager:
None,
)
def connect_update(self, hass, controller):
@callback
def connect_update(self, hass: HomeAssistant, controller: Heos) -> None:
"""Connect listener for when sources change and signal player update.
EVENT_SOURCES_CHANGED is often raised multiple times in response to a
@ -492,12 +493,7 @@ class SourceManager:
else:
return favorites, inputs
async def update_sources(event, data=None):
if event in (
heos_const.EVENT_SOURCES_CHANGED,
heos_const.EVENT_USER_CHANGED,
heos_const.EVENT_CONNECTED,
):
async def _update_sources() -> None:
# If throttled, it will return None
if sources := await get_sources():
self.favorites, self.inputs = sources
@ -506,7 +502,13 @@ class SourceManager:
# Let players know to update
async_dispatcher_send(hass, SIGNAL_HEOS_UPDATED)
controller.dispatcher.connect(
heos_const.SIGNAL_CONTROLLER_EVENT, update_sources
)
controller.dispatcher.connect(heos_const.SIGNAL_HEOS_EVENT, update_sources)
async def _on_controller_event(event: str, data: Any | None) -> None:
if event in (
heos_const.EVENT_SOURCES_CHANGED,
heos_const.EVENT_USER_CHANGED,
):
await _update_sources()
controller.add_on_connected(_update_sources)
controller.add_on_user_credentials_invalid(_update_sources)
controller.add_on_controller_event(_on_controller_event)

View File

@ -5,7 +5,7 @@ import logging
from typing import TYPE_CHECKING, Any, cast
from urllib.parse import urlparse
from pyheos import CommandFailedError, Heos, HeosError, HeosOptions
from pyheos import CommandAuthenticationError, Heos, HeosError, HeosOptions
import voluptuous as vol
from homeassistant.components import ssdp
@ -79,13 +79,9 @@ async def _validate_auth(
# Attempt to login (both username and password provided)
try:
await heos.sign_in(user_input[CONF_USERNAME], user_input[CONF_PASSWORD])
except CommandFailedError as err:
if err.error_id in (6, 8, 10): # Auth-specific errors
except CommandAuthenticationError as err:
errors["base"] = "invalid_auth"
_LOGGER.warning("Failed to sign-in to HEOS Account: %s", err)
else:
errors["base"] = "unknown"
_LOGGER.exception("Unexpected error occurred during sign-in")
return False
except HeosError:
errors["base"] = "unknown"

View File

@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/heos",
"iot_class": "local_push",
"loggers": ["pyheos"],
"requirements": ["pyheos==0.9.0"],
"requirements": ["pyheos==1.0.0"],
"single_config_entry": true,
"ssdp": [
{

View File

@ -8,7 +8,14 @@ import logging
from operator import ior
from typing import Any
from pyheos import HeosError, const as heos_const
from pyheos import (
AddCriteriaType,
ControlType,
HeosError,
HeosPlayer,
PlayState,
const as heos_const,
)
from homeassistant.components import media_source
from homeassistant.components.media_player import (
@ -47,25 +54,25 @@ BASE_SUPPORTED_FEATURES = (
)
PLAY_STATE_TO_STATE = {
heos_const.PlayState.PLAY: MediaPlayerState.PLAYING,
heos_const.PlayState.STOP: MediaPlayerState.IDLE,
heos_const.PlayState.PAUSE: MediaPlayerState.PAUSED,
PlayState.PLAY: MediaPlayerState.PLAYING,
PlayState.STOP: MediaPlayerState.IDLE,
PlayState.PAUSE: MediaPlayerState.PAUSED,
}
CONTROL_TO_SUPPORT = {
heos_const.CONTROL_PLAY: MediaPlayerEntityFeature.PLAY,
heos_const.CONTROL_PAUSE: MediaPlayerEntityFeature.PAUSE,
heos_const.CONTROL_STOP: MediaPlayerEntityFeature.STOP,
heos_const.CONTROL_PLAY_PREVIOUS: MediaPlayerEntityFeature.PREVIOUS_TRACK,
heos_const.CONTROL_PLAY_NEXT: MediaPlayerEntityFeature.NEXT_TRACK,
ControlType.PLAY: MediaPlayerEntityFeature.PLAY,
ControlType.PAUSE: MediaPlayerEntityFeature.PAUSE,
ControlType.STOP: MediaPlayerEntityFeature.STOP,
ControlType.PLAY_PREVIOUS: MediaPlayerEntityFeature.PREVIOUS_TRACK,
ControlType.PLAY_NEXT: MediaPlayerEntityFeature.NEXT_TRACK,
}
HA_HEOS_ENQUEUE_MAP = {
None: heos_const.AddCriteriaType.REPLACE_AND_PLAY,
MediaPlayerEnqueue.ADD: heos_const.AddCriteriaType.ADD_TO_END,
MediaPlayerEnqueue.REPLACE: heos_const.AddCriteriaType.REPLACE_AND_PLAY,
MediaPlayerEnqueue.NEXT: heos_const.AddCriteriaType.PLAY_NEXT,
MediaPlayerEnqueue.PLAY: heos_const.AddCriteriaType.PLAY_NOW,
None: AddCriteriaType.REPLACE_AND_PLAY,
MediaPlayerEnqueue.ADD: AddCriteriaType.ADD_TO_END,
MediaPlayerEnqueue.REPLACE: AddCriteriaType.REPLACE_AND_PLAY,
MediaPlayerEnqueue.NEXT: AddCriteriaType.PLAY_NEXT,
MediaPlayerEnqueue.PLAY: AddCriteriaType.PLAY_NOW,
}
_LOGGER = logging.getLogger(__name__)
@ -118,11 +125,14 @@ class HeosMediaPlayer(MediaPlayerEntity):
_attr_name = None
def __init__(
self, player, source_manager: SourceManager, group_manager: GroupManager
self,
player: HeosPlayer,
source_manager: SourceManager,
group_manager: GroupManager,
) -> None:
"""Initialize."""
self._media_position_updated_at = None
self._player = player
self._player: HeosPlayer = player
self._source_manager = source_manager
self._group_manager = group_manager
self._attr_unique_id = str(player.player_id)
@ -134,10 +144,8 @@ class HeosMediaPlayer(MediaPlayerEntity):
sw_version=player.version,
)
async def _player_update(self, player_id, event):
async def _player_update(self, event):
"""Handle player attribute updated."""
if self._player.player_id != player_id:
return
if event == heos_const.EVENT_PLAYER_NOW_PLAYING_PROGRESS:
self._media_position_updated_at = utcnow()
await self.async_update_ha_state(True)
@ -149,11 +157,7 @@ class HeosMediaPlayer(MediaPlayerEntity):
async def async_added_to_hass(self) -> None:
"""Device added to hass."""
# Update state when attributes of the player change
self.async_on_remove(
self._player.heos.dispatcher.connect(
heos_const.SIGNAL_PLAYER_EVENT, self._player_update
)
)
self.async_on_remove(self._player.add_on_player_event(self._player_update))
# Update state when heos changes
self.async_on_remove(
async_dispatcher_connect(self.hass, SIGNAL_HEOS_UPDATED, self._heos_updated)

View File

@ -2,7 +2,7 @@
import logging
from pyheos import CommandFailedError, Heos, HeosError, const
from pyheos import CommandAuthenticationError, Heos, HeosError
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
@ -69,16 +69,12 @@ def _get_controller(hass: HomeAssistant) -> Heos:
async def _sign_in_handler(service: ServiceCall) -> None:
"""Sign in to the HEOS account."""
controller = _get_controller(service.hass)
if controller.connection_state != const.STATE_CONNECTED:
_LOGGER.error("Unable to sign in because HEOS is not connected")
return
username = service.data[ATTR_USERNAME]
password = service.data[ATTR_PASSWORD]
try:
await controller.sign_in(username, password)
except CommandFailedError as err:
except CommandAuthenticationError as err:
_LOGGER.error("Sign in failed: %s", err)
except HeosError as err:
_LOGGER.error("Unable to sign in: %s", err)
@ -88,9 +84,6 @@ async def _sign_out_handler(service: ServiceCall) -> None:
"""Sign out of the HEOS account."""
controller = _get_controller(service.hass)
if controller.connection_state != const.STATE_CONNECTED:
_LOGGER.error("Unable to sign out because HEOS is not connected")
return
try:
await controller.sign_out()
except HeosError as err:

2
requirements_all.txt generated
View File

@ -1980,7 +1980,7 @@ pygti==0.9.4
pyhaversion==22.8.0
# homeassistant.components.heos
pyheos==0.9.0
pyheos==1.0.0
# homeassistant.components.hive
pyhiveapi==0.5.16

View File

@ -1609,7 +1609,7 @@ pygti==0.9.4
pyhaversion==22.8.0
# homeassistant.components.heos
pyheos==0.9.0
pyheos==1.0.0
# homeassistant.components.hive
pyhiveapi==0.5.16

View File

@ -3,9 +3,24 @@
from __future__ import annotations
from collections.abc import Sequence
from unittest.mock import Mock, patch
from unittest.mock import AsyncMock, Mock, patch
from pyheos import Dispatcher, Heos, HeosGroup, HeosPlayer, MediaItem, const
from pyheos import (
CONTROLS_ALL,
Dispatcher,
Heos,
HeosGroup,
HeosOptions,
HeosPlayer,
LineOutLevelType,
MediaItem,
MediaType,
NetworkType,
PlayerUpdateResult,
PlayState,
RepeatType,
const,
)
import pytest
import pytest_asyncio
@ -71,26 +86,27 @@ def controller_fixture(
players, favorites, input_sources, playlists, change_data, dispatcher, group
):
"""Create a mock Heos controller fixture."""
mock_heos = Mock(Heos)
mock_heos = Heos(HeosOptions(host="127.0.0.1", dispatcher=dispatcher))
for player in players.values():
player.heos = mock_heos
mock_heos.return_value = mock_heos
mock_heos.dispatcher = dispatcher
mock_heos.get_players.return_value = players
mock_heos.players = players
mock_heos.get_favorites.return_value = favorites
mock_heos.get_input_sources.return_value = input_sources
mock_heos.get_playlists.return_value = playlists
mock_heos.load_players.return_value = change_data
mock_heos.is_signed_in = True
mock_heos.signed_in_username = "user@user.com"
mock_heos.connection_state = const.STATE_CONNECTED
mock_heos.get_groups.return_value = group
mock_heos.create_group.return_value = None
mock_heos.connect = AsyncMock()
mock_heos.disconnect = AsyncMock()
mock_heos.sign_in = AsyncMock()
mock_heos.sign_out = AsyncMock()
mock_heos.get_players = AsyncMock(return_value=players)
mock_heos._players = players
mock_heos.get_favorites = AsyncMock(return_value=favorites)
mock_heos.get_input_sources = AsyncMock(return_value=input_sources)
mock_heos.get_playlists = AsyncMock(return_value=playlists)
mock_heos.load_players = AsyncMock(return_value=change_data)
mock_heos._signed_in_username = "user@user.com"
mock_heos.get_groups = AsyncMock(return_value=group)
mock_heos.create_group = AsyncMock(return_value=None)
new_mock = Mock(return_value=mock_heos)
mock_heos.new_mock = new_mock
with (
patch("homeassistant.components.heos.Heos", new=mock_heos),
patch("homeassistant.components.heos.config_flow.Heos", new=mock_heos),
patch("homeassistant.components.heos.Heos", new=new_mock),
patch("homeassistant.components.heos.config_flow.Heos", new=new_mock),
):
yield mock_heos
@ -106,24 +122,25 @@ def player_fixture(quick_selects):
"""Create two mock HeosPlayers."""
players = {}
for i in (1, 2):
player = Mock(HeosPlayer)
player.player_id = i
if i > 1:
player.name = f"Test Player {i}"
else:
player.name = "Test Player"
player.model = "Test Model"
player.version = "1.0.0"
player.is_muted = False
player.available = True
player.state = const.PlayState.STOP
player.ip_address = f"127.0.0.{i}"
player.network = "wired"
player.shuffle = False
player.repeat = const.RepeatType.OFF
player.volume = 25
player = HeosPlayer(
player_id=i,
name="Test Player" if i == 1 else f"Test Player {i}",
model="Test Model",
serial="",
version="1.0.0",
line_out=LineOutLevelType.VARIABLE,
is_muted=False,
available=True,
state=PlayState.STOP,
ip_address=f"127.0.0.{i}",
network=NetworkType.WIRED,
shuffle=False,
repeat=RepeatType.OFF,
volume=25,
heos=None,
)
player.now_playing_media = Mock()
player.now_playing_media.supported_controls = const.CONTROLS_ALL
player.now_playing_media.supported_controls = CONTROLS_ALL
player.now_playing_media.album_id = 1
player.now_playing_media.queue_id = 1
player.now_playing_media.source_id = 1
@ -136,13 +153,30 @@ def player_fixture(quick_selects):
player.now_playing_media.current_position = None
player.now_playing_media.image_url = "http://"
player.now_playing_media.song = "Song"
player.get_quick_selects.return_value = quick_selects
player.add_to_queue = AsyncMock()
player.clear_queue = AsyncMock()
player.get_quick_selects = AsyncMock(return_value=quick_selects)
player.mute = AsyncMock()
player.pause = AsyncMock()
player.play = AsyncMock()
player.play_input_source = AsyncMock()
player.play_next = AsyncMock()
player.play_previous = AsyncMock()
player.play_preset_station = AsyncMock()
player.play_quick_select = AsyncMock()
player.play_url = AsyncMock()
player.set_mute = AsyncMock()
player.set_play_mode = AsyncMock()
player.set_quick_select = AsyncMock()
player.set_volume = AsyncMock()
player.stop = AsyncMock()
player.unmute = AsyncMock()
players[player.player_id] = player
return players
@pytest.fixture(name="group")
def group_fixture(players):
def group_fixture():
"""Create a HEOS group consisting of two players."""
group = HeosGroup(
name="Group", group_id=999, lead_player_id=1, member_player_ids=[2]
@ -158,7 +192,7 @@ def favorites_fixture() -> dict[int, MediaItem]:
source_id=const.MUSIC_SOURCE_PANDORA,
name="Today's Hits Radio",
media_id="123456789",
type=const.MediaType.STATION,
type=MediaType.STATION,
playable=True,
browsable=False,
image_url="",
@ -168,7 +202,7 @@ def favorites_fixture() -> dict[int, MediaItem]:
source_id=const.MUSIC_SOURCE_TUNEIN,
name="Classical MPR (Classical Music)",
media_id="s1234",
type=const.MediaType.STATION,
type=MediaType.STATION,
playable=True,
browsable=False,
image_url="",
@ -184,7 +218,7 @@ def input_sources_fixture() -> Sequence[MediaItem]:
source_id=1,
name="HEOS Drive - Line In 1",
media_id=const.INPUT_AUX_IN_1,
type=const.MediaType.STATION,
type=MediaType.STATION,
playable=True,
browsable=False,
image_url="",
@ -256,7 +290,7 @@ def playlists_fixture() -> Sequence[MediaItem]:
playlist = MediaItem(
source_id=const.MUSIC_SOURCE_PLAYLISTS,
name="Awesome Music",
type=const.MediaType.PLAYLIST,
type=MediaType.PLAYLIST,
playable=True,
browsable=True,
image_url="",
@ -268,10 +302,10 @@ def playlists_fixture() -> Sequence[MediaItem]:
@pytest.fixture(name="change_data")
def change_data_fixture() -> dict:
"""Create player change data for testing."""
return {const.DATA_MAPPED_IDS: {}, const.DATA_NEW: []}
return PlayerUpdateResult()
@pytest.fixture(name="change_data_mapped_ids")
def change_data_mapped_ids_fixture() -> dict:
"""Create player change data for testing."""
return {const.DATA_MAPPED_IDS: {101: 1}, const.DATA_NEW: []}
return PlayerUpdateResult(updated_player_ids={1: 101})

View File

@ -1,6 +1,6 @@
"""Tests for the Heos config flow module."""
from pyheos import CommandFailedError, HeosError
from pyheos import CommandAuthenticationError, CommandFailedError, HeosError
import pytest
from homeassistant.components import heos, ssdp
@ -199,14 +199,9 @@ async def test_reconfigure_cannot_connect_recovers(
("error", "expected_error_key"),
[
(
CommandFailedError("sign_in", "Invalid credentials", 6),
CommandAuthenticationError("sign_in", "Invalid credentials", 6),
"invalid_auth",
),
(
CommandFailedError("sign_in", "User not logged in", 8),
"invalid_auth",
),
(CommandFailedError("sign_in", "user not found", 10), "invalid_auth"),
(CommandFailedError("sign_in", "System error", 12), "unknown"),
(HeosError(), "unknown"),
],
@ -337,14 +332,9 @@ async def test_options_flow_missing_one_param_recovers(
("error", "expected_error_key"),
[
(
CommandFailedError("sign_in", "Invalid credentials", 6),
CommandAuthenticationError("sign_in", "Invalid credentials", 6),
"invalid_auth",
),
(
CommandFailedError("sign_in", "User not logged in", 8),
"invalid_auth",
),
(CommandFailedError("sign_in", "user not found", 10), "invalid_auth"),
(CommandFailedError("sign_in", "System error", 12), "unknown"),
(HeosError(), "unknown"),
],

View File

@ -4,7 +4,7 @@ import asyncio
from typing import cast
from unittest.mock import Mock, patch
from pyheos import CommandFailedError, HeosError, const
from pyheos import CommandFailedError, HeosError, SignalHeosEvent, SignalType, const
import pytest
from homeassistant.components.heos import (
@ -82,7 +82,7 @@ async def test_async_setup_entry_with_options_loads_platforms(
# Assert options passed and methods called
assert config_entry_options.state is ConfigEntryState.LOADED
options = cast(HeosOptions, controller.call_args[0][0])
options = cast(HeosOptions, controller.new_mock.call_args[0][0])
assert options.host == config_entry_options.data[CONF_HOST]
assert options.credentials.username == config_entry_options.options[CONF_USERNAME]
assert options.credentials.password == config_entry_options.options[CONF_PASSWORD]
@ -103,10 +103,9 @@ async def test_async_setup_entry_auth_failure_starts_reauth(
# Simulates what happens when the controller can't sign-in during connection
async def connect_send_auth_failure() -> None:
controller.is_signed_in = False
controller.signed_in_username = None
controller._signed_in_username = None
controller.dispatcher.send(
const.SIGNAL_HEOS_EVENT, const.EVENT_USER_CREDENTIALS_INVALID
SignalType.HEOS_EVENT, SignalHeosEvent.USER_CREDENTIALS_INVALID
)
controller.connect.side_effect = connect_send_auth_failure
@ -133,8 +132,7 @@ async def test_async_setup_entry_not_signed_in_loads_platforms(
) -> None:
"""Test setup does not retrieve favorites when not logged in."""
config_entry.add_to_hass(hass)
controller.is_signed_in = False
controller.signed_in_username = None
controller._signed_in_username = None
with patch.object(
hass.config_entries, "async_forward_entry_setups"
) as forward_mock:
@ -213,7 +211,7 @@ async def test_update_sources_retry(
source_manager.max_retry_attempts = 1
controller.get_favorites.side_effect = CommandFailedError("Test", "test", 0)
controller.dispatcher.send(
const.SIGNAL_CONTROLLER_EVENT, const.EVENT_SOURCES_CHANGED, {}
SignalType.CONTROLLER_EVENT, const.EVENT_SOURCES_CHANGED, {}
)
# Wait until it's finished
while "Unable to update sources" not in caplog.text:

View File

@ -3,8 +3,15 @@
import asyncio
from typing import Any
from pyheos import CommandFailedError, const
from pyheos.error import HeosError
from pyheos import (
AddCriteriaType,
CommandFailedError,
HeosError,
PlayState,
SignalHeosEvent,
SignalType,
const,
)
import pytest
from homeassistant.components.heos import media_player
@ -115,18 +122,18 @@ async def test_updates_from_signals(
player = controller.players[1]
# Test player does not update for other players
player.state = const.PlayState.PLAY
player.state = PlayState.PLAY
player.heos.dispatcher.send(
const.SIGNAL_PLAYER_EVENT, 2, const.EVENT_PLAYER_STATE_CHANGED
SignalType.PLAYER_EVENT, 2, const.EVENT_PLAYER_STATE_CHANGED
)
await hass.async_block_till_done()
state = hass.states.get("media_player.test_player")
assert state.state == STATE_IDLE
# Test player_update standard events
player.state = const.PlayState.PLAY
player.state = PlayState.PLAY
player.heos.dispatcher.send(
const.SIGNAL_PLAYER_EVENT, player.player_id, const.EVENT_PLAYER_STATE_CHANGED
SignalType.PLAYER_EVENT, player.player_id, const.EVENT_PLAYER_STATE_CHANGED
)
await hass.async_block_till_done()
@ -137,7 +144,7 @@ async def test_updates_from_signals(
player.now_playing_media.duration = 360000
player.now_playing_media.current_position = 1000
player.heos.dispatcher.send(
const.SIGNAL_PLAYER_EVENT,
SignalType.PLAYER_EVENT,
player.player_id,
const.EVENT_PLAYER_NOW_PLAYING_PROGRESS,
)
@ -167,7 +174,7 @@ async def test_updates_from_connection_event(
# Connected
player.available = True
player.heos.dispatcher.send(const.SIGNAL_HEOS_EVENT, const.EVENT_CONNECTED)
player.heos.dispatcher.send(SignalType.HEOS_EVENT, SignalHeosEvent.CONNECTED)
await event.wait()
state = hass.states.get("media_player.test_player")
assert state.state == STATE_IDLE
@ -175,10 +182,9 @@ async def test_updates_from_connection_event(
# Disconnected
event.clear()
player.reset_mock()
controller.load_players.reset_mock()
player.available = False
player.heos.dispatcher.send(const.SIGNAL_HEOS_EVENT, const.EVENT_DISCONNECTED)
player.heos.dispatcher.send(SignalType.HEOS_EVENT, SignalHeosEvent.DISCONNECTED)
await event.wait()
state = hass.states.get("media_player.test_player")
assert state.state == STATE_UNAVAILABLE
@ -186,11 +192,10 @@ async def test_updates_from_connection_event(
# Connected handles refresh failure
event.clear()
player.reset_mock()
controller.load_players.reset_mock()
controller.load_players.side_effect = CommandFailedError(None, "Failure", 1)
player.available = True
player.heos.dispatcher.send(const.SIGNAL_HEOS_EVENT, const.EVENT_CONNECTED)
player.heos.dispatcher.send(SignalType.HEOS_EVENT, SignalHeosEvent.CONNECTED)
await event.wait()
state = hass.states.get("media_player.test_player")
assert state.state == STATE_IDLE
@ -213,7 +218,7 @@ async def test_updates_from_sources_updated(
input_sources.clear()
player.heos.dispatcher.send(
const.SIGNAL_CONTROLLER_EVENT, const.EVENT_SOURCES_CHANGED, {}
SignalType.CONTROLLER_EVENT, const.EVENT_SOURCES_CHANGED, {}
)
await event.wait()
source_list = config_entry.runtime_data.source_manager.source_list
@ -241,9 +246,9 @@ async def test_updates_from_players_changed(
async_dispatcher_connect(hass, SIGNAL_HEOS_UPDATED, set_signal)
assert hass.states.get("media_player.test_player").state == STATE_IDLE
player.state = const.PlayState.PLAY
player.state = PlayState.PLAY
player.heos.dispatcher.send(
const.SIGNAL_CONTROLLER_EVENT, const.EVENT_PLAYERS_CHANGED, change_data
SignalType.CONTROLLER_EVENT, const.EVENT_PLAYERS_CHANGED, change_data
)
await event.wait()
await hass.async_block_till_done()
@ -279,7 +284,7 @@ async def test_updates_from_players_changed_new_ids(
async_dispatcher_connect(hass, SIGNAL_HEOS_UPDATED, set_signal)
player.heos.dispatcher.send(
const.SIGNAL_CONTROLLER_EVENT,
SignalType.CONTROLLER_EVENT,
const.EVENT_PLAYERS_CHANGED,
change_data_mapped_ids,
)
@ -309,10 +314,9 @@ async def test_updates_from_user_changed(
async_dispatcher_connect(hass, SIGNAL_HEOS_UPDATED, set_signal)
controller.is_signed_in = False
controller.signed_in_username = None
controller._signed_in_username = None
player.heos.dispatcher.send(
const.SIGNAL_CONTROLLER_EVENT, const.EVENT_USER_CHANGED, None
SignalType.CONTROLLER_EVENT, const.EVENT_USER_CHANGED, None
)
await event.wait()
source_list = config_entry.runtime_data.source_manager.source_list
@ -555,7 +559,7 @@ async def test_select_favorite(
# Test state is matched by station name
player.now_playing_media.station = favorite.name
player.heos.dispatcher.send(
const.SIGNAL_PLAYER_EVENT, player.player_id, const.EVENT_PLAYER_STATE_CHANGED
SignalType.PLAYER_EVENT, player.player_id, const.EVENT_PLAYER_STATE_CHANGED
)
await hass.async_block_till_done()
state = hass.states.get("media_player.test_player")
@ -581,7 +585,7 @@ async def test_select_radio_favorite(
player.now_playing_media.station = "Classical"
player.now_playing_media.album_id = favorite.media_id
player.heos.dispatcher.send(
const.SIGNAL_PLAYER_EVENT, player.player_id, const.EVENT_PLAYER_STATE_CHANGED
SignalType.PLAYER_EVENT, player.player_id, const.EVENT_PLAYER_STATE_CHANGED
)
await hass.async_block_till_done()
state = hass.states.get("media_player.test_player")
@ -634,7 +638,7 @@ async def test_select_input_source(
player.now_playing_media.source_id = const.MUSIC_SOURCE_AUX_INPUT
player.now_playing_media.media_id = const.INPUT_AUX_IN_1
player.heos.dispatcher.send(
const.SIGNAL_PLAYER_EVENT, player.player_id, const.EVENT_PLAYER_STATE_CHANGED
SignalType.PLAYER_EVENT, player.player_id, const.EVENT_PLAYER_STATE_CHANGED
)
await hass.async_block_till_done()
state = hass.states.get("media_player.test_player")
@ -831,7 +835,7 @@ async def test_play_media_playlist(
blocking=True,
)
player.add_to_queue.assert_called_once_with(
playlist, const.AddCriteriaType.REPLACE_AND_PLAY
playlist, AddCriteriaType.REPLACE_AND_PLAY
)
# Play with enqueuing
player.add_to_queue.reset_mock()
@ -846,9 +850,7 @@ async def test_play_media_playlist(
},
blocking=True,
)
player.add_to_queue.assert_called_once_with(
playlist, const.AddCriteriaType.ADD_TO_END
)
player.add_to_queue.assert_called_once_with(playlist, AddCriteriaType.ADD_TO_END)
# Invalid name
player.add_to_queue.reset_mock()
await hass.services.async_call(
@ -1028,7 +1030,7 @@ async def test_media_player_unjoin_group(
player = controller.players[1]
player.heos.dispatcher.send(
const.SIGNAL_PLAYER_EVENT,
SignalType.PLAYER_EVENT,
player.player_id,
const.EVENT_PLAYER_STATE_CHANGED,
)

View File

@ -1,6 +1,6 @@
"""Tests for the services module."""
from pyheos import CommandFailedError, HeosError, const
from pyheos import CommandAuthenticationError, HeosError
import pytest
from homeassistant.components.heos.const import (
@ -38,30 +38,14 @@ async def test_sign_in(hass: HomeAssistant, config_entry, controller) -> None:
controller.sign_in.assert_called_once_with("test@test.com", "password")
async def test_sign_in_not_connected(
hass: HomeAssistant, config_entry, controller, caplog: pytest.LogCaptureFixture
) -> None:
"""Test sign-in service logs error when not connected."""
await setup_component(hass, config_entry)
controller.connection_state = const.STATE_RECONNECTING
await hass.services.async_call(
DOMAIN,
SERVICE_SIGN_IN,
{ATTR_USERNAME: "test@test.com", ATTR_PASSWORD: "password"},
blocking=True,
)
assert controller.sign_in.call_count == 0
assert "Unable to sign in because HEOS is not connected" in caplog.text
async def test_sign_in_failed(
hass: HomeAssistant, config_entry, controller, caplog: pytest.LogCaptureFixture
) -> None:
"""Test sign-in service logs error when not connected."""
await setup_component(hass, config_entry)
controller.sign_in.side_effect = CommandFailedError("", "Invalid credentials", 6)
controller.sign_in.side_effect = CommandAuthenticationError(
"", "Invalid credentials", 6
)
await hass.services.async_call(
DOMAIN,
@ -115,19 +99,6 @@ async def test_sign_out(hass: HomeAssistant, config_entry, controller) -> None:
assert controller.sign_out.call_count == 1
async def test_sign_out_not_connected(
hass: HomeAssistant, config_entry, controller, caplog: pytest.LogCaptureFixture
) -> None:
"""Test the sign-out service."""
await setup_component(hass, config_entry)
controller.connection_state = const.STATE_RECONNECTING
await hass.services.async_call(DOMAIN, SERVICE_SIGN_OUT, {}, blocking=True)
assert controller.sign_out.call_count == 0
assert "Unable to sign out because HEOS is not connected" in caplog.text
async def test_sign_out_not_loaded_raises(hass: HomeAssistant, config_entry) -> None:
"""Test the sign-out service when entry not loaded raises exception."""
await setup_component(hass, config_entry)