Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bump pyheos to 1.0.0 #135415

Merged
merged 1 commit into from
Jan 12, 2025
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
120 changes: 61 additions & 59 deletions homeassistant/components/heos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import Any

from pyheos import (
Credentials,
Heos,
HeosError,
HeosOptions,
HeosPlayer,
PlayerUpdateResult,
SignalHeosEvent,
const as heos_const,
)

Expand Down Expand Up @@ -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_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.
Expand Down Expand Up @@ -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):
Expand All @@ -181,56 +181,59 @@ async def connect_listeners(self):
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)
)
Expand All @@ -249,7 +252,7 @@ def __init__(
) -> 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
Expand All @@ -268,7 +271,7 @@ async def async_get_group_membership(self) -> dict[str, list[str]]:
}

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
Expand Down Expand Up @@ -326,37 +329,34 @@ async def async_unjoin_player(self, player_id: int, player_entity_id: str):
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")
# Let players know to update
async_dispatcher_send(self._hass, SIGNAL_HEOS_UPDATED)
else:
_LOGGER.debug("Groups empty")
if groups := await self.async_get_group_membership():
self._group_membership = groups
_LOGGER.debug("Groups updated due to change event")
# Let players know to update
async_dispatcher_send(self._hass, SIGNAL_HEOS_UPDATED)
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():
# Avoid calling async_update_groups when the entity_id map has not been
# 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
Expand Down Expand Up @@ -462,7 +462,8 @@ def get_current_source(self, now_playing_media):
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
Expand Down Expand Up @@ -492,21 +493,22 @@ async def get_sources():
else:
return favorites, inputs

async def update_sources(event, data=None):
async def _update_sources() -> None:
# If throttled, it will return None
if sources := await get_sources():
self.favorites, self.inputs = sources
self.source_list = self._build_source_list()
_LOGGER.debug("Sources updated due to changed event")
# Let players know to update
async_dispatcher_send(hass, SIGNAL_HEOS_UPDATED)

async def _on_controller_event(event: str, data: Any | None) -> None:
if event in (
heos_const.EVENT_SOURCES_CHANGED,
heos_const.EVENT_USER_CHANGED,
heos_const.EVENT_CONNECTED,
):
# If throttled, it will return None
if sources := await get_sources():
self.favorites, self.inputs = sources
self.source_list = self._build_source_list()
_LOGGER.debug("Sources updated due to changed event")
# 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)
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)
12 changes: 4 additions & 8 deletions homeassistant/components/heos/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
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
Expand Down Expand Up @@ -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
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")
except CommandAuthenticationError as err:
errors["base"] = "invalid_auth"
_LOGGER.warning("Failed to sign-in to HEOS Account: %s", err)
return False
except HeosError:
errors["base"] = "unknown"
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/heos/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down
52 changes: 28 additions & 24 deletions homeassistant/components/heos/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
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 (
Expand Down Expand Up @@ -47,25 +54,25 @@
)

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__)
Expand Down Expand Up @@ -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)
Expand All @@ -134,10 +144,8 @@ def __init__(
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)
Expand All @@ -149,11 +157,7 @@ async def _heos_updated(self) -> None:
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)
Expand Down
Loading