core/homeassistant/components/heos/services.py

188 lines
5.7 KiB
Python

"""Services for the HEOS integration."""
from dataclasses import dataclass
import logging
from typing import Final
from pyheos import CommandAuthenticationError, Heos, HeosError
import voluptuous as vol
from homeassistant.components.media_player import ATTR_MEDIA_VOLUME_LEVEL
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import (
config_validation as cv,
entity_platform,
issue_registry as ir,
)
from homeassistant.helpers.typing import VolDictType, VolSchemaType
from .const import (
ATTR_DESTINATION_POSITION,
ATTR_PASSWORD,
ATTR_QUEUE_IDS,
ATTR_USERNAME,
DOMAIN,
SERVICE_GET_QUEUE,
SERVICE_GROUP_VOLUME_DOWN,
SERVICE_GROUP_VOLUME_SET,
SERVICE_GROUP_VOLUME_UP,
SERVICE_MOVE_QUEUE_ITEM,
SERVICE_REMOVE_FROM_QUEUE,
SERVICE_SIGN_IN,
SERVICE_SIGN_OUT,
)
from .coordinator import HeosConfigEntry
_LOGGER = logging.getLogger(__name__)
HEOS_SIGN_IN_SCHEMA = vol.Schema(
{vol.Required(ATTR_USERNAME): cv.string, vol.Required(ATTR_PASSWORD): cv.string}
)
HEOS_SIGN_OUT_SCHEMA = vol.Schema({})
def register(hass: HomeAssistant) -> None:
"""Register HEOS services."""
hass.services.async_register(
DOMAIN,
SERVICE_SIGN_IN,
_sign_in_handler,
schema=HEOS_SIGN_IN_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_SIGN_OUT,
_sign_out_handler,
schema=HEOS_SIGN_OUT_SCHEMA,
)
@dataclass(frozen=True)
class EntityServiceDescription:
"""Describe an entity service."""
name: str
method_name: str
schema: VolDictType | VolSchemaType | None = None
supports_response: SupportsResponse = SupportsResponse.NONE
def async_register(self, platform: entity_platform.EntityPlatform) -> None:
"""Register the service with the platform."""
platform.async_register_entity_service(
self.name,
self.schema,
self.method_name,
supports_response=self.supports_response,
)
REMOVE_FROM_QUEUE_SCHEMA: Final[VolDictType] = {
vol.Required(ATTR_QUEUE_IDS): vol.All(
cv.ensure_list,
[vol.All(cv.positive_int, vol.Range(min=1))],
vol.Unique(),
)
}
GROUP_VOLUME_SET_SCHEMA: Final[VolDictType] = {
vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float
}
MOVE_QEUEUE_ITEM_SCHEMA: Final[VolDictType] = {
vol.Required(ATTR_QUEUE_IDS): vol.All(
cv.ensure_list,
[vol.All(vol.Coerce(int), vol.Range(min=1, max=1000))],
vol.Unique(),
),
vol.Required(ATTR_DESTINATION_POSITION): vol.All(
vol.Coerce(int), vol.Range(min=1, max=1000)
),
}
MEDIA_PLAYER_ENTITY_SERVICES: Final = (
# Player queue services
EntityServiceDescription(
SERVICE_GET_QUEUE, "async_get_queue", supports_response=SupportsResponse.ONLY
),
EntityServiceDescription(
SERVICE_REMOVE_FROM_QUEUE, "async_remove_from_queue", REMOVE_FROM_QUEUE_SCHEMA
),
EntityServiceDescription(
SERVICE_MOVE_QUEUE_ITEM, "async_move_queue_item", MOVE_QEUEUE_ITEM_SCHEMA
),
# Group volume services
EntityServiceDescription(
SERVICE_GROUP_VOLUME_SET,
"async_set_group_volume_level",
GROUP_VOLUME_SET_SCHEMA,
),
EntityServiceDescription(SERVICE_GROUP_VOLUME_DOWN, "async_group_volume_down"),
EntityServiceDescription(SERVICE_GROUP_VOLUME_UP, "async_group_volume_up"),
)
def register_media_player_services() -> None:
"""Register media_player entity services."""
platform = entity_platform.async_get_current_platform()
for service in MEDIA_PLAYER_ENTITY_SERVICES:
service.async_register(platform)
def _get_controller(hass: HomeAssistant) -> Heos:
"""Get the HEOS controller instance."""
_LOGGER.warning(
"Actions 'heos.sign_in' and 'heos.sign_out' are deprecated and will be removed in the 2025.8.0 release"
)
ir.async_create_issue(
hass,
DOMAIN,
"sign_in_out_deprecated",
breaks_in_ha_version="2025.8.0",
is_fixable=False,
severity=ir.IssueSeverity.WARNING,
translation_key="sign_in_out_deprecated",
)
entry: HeosConfigEntry | None = (
hass.config_entries.async_entry_for_domain_unique_id(DOMAIN, DOMAIN)
)
if not entry or not entry.state == ConfigEntryState.LOADED:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="integration_not_loaded"
)
return entry.runtime_data.heos
async def _sign_in_handler(service: ServiceCall) -> None:
"""Sign in to the HEOS account."""
controller = _get_controller(service.hass)
username = service.data[ATTR_USERNAME]
password = service.data[ATTR_PASSWORD]
try:
await controller.sign_in(username, password)
except CommandAuthenticationError as err:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="sign_in_auth_error"
) from err
except HeosError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="sign_in_error",
translation_placeholders={"error": str(err)},
) from err
async def _sign_out_handler(service: ServiceCall) -> None:
"""Sign out of the HEOS account."""
controller = _get_controller(service.hass)
try:
await controller.sign_out()
except HeosError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="sign_out_error",
translation_placeholders={"error": str(err)},
) from err