mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 07:07:28 +00:00
Replace custom actions for sleep timer with buttons in bluesound integration (#133604)
* Use entity services * Add buttons for sleep timer * Fix merge * Replace hass.data with runtime_data from config_entries * Disable button by default * Remove duplicate dispatchers * Add tests for buttons * Fix merge commit * Fix merge commit * Update deprecation version * Remove update_before_add * Use entity_registry_enabled_by_default * Use EnitiyDescriptions for buttons * Update version for deprecate * Use tranlation_key; Move default disable to EntityDescription * Fix merge commit * Fix callback type; fix breaks version * Use normal issue * Apply suggestions from code review --------- Co-authored-by: Franck Nijhof <git@frenck.dev> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
b6c4b06fc7
commit
4cecb6c851
@ -21,6 +21,7 @@ from .coordinator import (
|
|||||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
PLATFORMS = [
|
PLATFORMS = [
|
||||||
|
Platform.BUTTON,
|
||||||
Platform.MEDIA_PLAYER,
|
Platform.MEDIA_PLAYER,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
128
homeassistant/components/bluesound/button.py
Normal file
128
homeassistant/components/bluesound/button.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
"""Button entities for Bluesound."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from pyblu import Player
|
||||||
|
|
||||||
|
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||||
|
from homeassistant.const import CONF_PORT
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.device_registry import (
|
||||||
|
CONNECTION_NETWORK_MAC,
|
||||||
|
DeviceInfo,
|
||||||
|
format_mac,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import BluesoundCoordinator
|
||||||
|
from .media_player import DEFAULT_PORT
|
||||||
|
from .utils import format_unique_id
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import BluesoundConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: BluesoundConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the Bluesound entry."""
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
BluesoundButton(
|
||||||
|
config_entry.runtime_data.coordinator,
|
||||||
|
config_entry.runtime_data.player,
|
||||||
|
config_entry.data[CONF_PORT],
|
||||||
|
description,
|
||||||
|
)
|
||||||
|
for description in BUTTON_DESCRIPTIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True, frozen=True)
|
||||||
|
class BluesoundButtonEntityDescription(ButtonEntityDescription):
|
||||||
|
"""Description for Bluesound button entities."""
|
||||||
|
|
||||||
|
press_fn: Callable[[Player], Awaitable[None]]
|
||||||
|
|
||||||
|
|
||||||
|
async def clear_sleep_timer(player: Player) -> None:
|
||||||
|
"""Clear the sleep timer."""
|
||||||
|
sleep = -1
|
||||||
|
while sleep != 0:
|
||||||
|
sleep = await player.sleep_timer()
|
||||||
|
|
||||||
|
|
||||||
|
async def set_sleep_timer(player: Player) -> None:
|
||||||
|
"""Set the sleep timer."""
|
||||||
|
await player.sleep_timer()
|
||||||
|
|
||||||
|
|
||||||
|
BUTTON_DESCRIPTIONS = [
|
||||||
|
BluesoundButtonEntityDescription(
|
||||||
|
key="set_sleep_timer",
|
||||||
|
translation_key="set_sleep_timer",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
press_fn=set_sleep_timer,
|
||||||
|
),
|
||||||
|
BluesoundButtonEntityDescription(
|
||||||
|
key="clear_sleep_timer",
|
||||||
|
translation_key="clear_sleep_timer",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
press_fn=clear_sleep_timer,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BluesoundButton(CoordinatorEntity[BluesoundCoordinator], ButtonEntity):
|
||||||
|
"""Base class for Bluesound buttons."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
entity_description: BluesoundButtonEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: BluesoundCoordinator,
|
||||||
|
player: Player,
|
||||||
|
port: int,
|
||||||
|
description: BluesoundButtonEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Bluesound button."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
sync_status = coordinator.data.sync_status
|
||||||
|
|
||||||
|
self.entity_description = description
|
||||||
|
self._player = player
|
||||||
|
self._attr_unique_id = (
|
||||||
|
f"{description.key}-{format_unique_id(sync_status.mac, port)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if port == DEFAULT_PORT:
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, format_mac(sync_status.mac))},
|
||||||
|
connections={(CONNECTION_NETWORK_MAC, format_mac(sync_status.mac))},
|
||||||
|
name=sync_status.name,
|
||||||
|
manufacturer=sync_status.brand,
|
||||||
|
model=sync_status.model_name,
|
||||||
|
model_id=sync_status.model,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, format_unique_id(sync_status.mac, port))},
|
||||||
|
name=sync_status.name,
|
||||||
|
manufacturer=sync_status.brand,
|
||||||
|
model=sync_status.model_name,
|
||||||
|
model_id=sync_status.model,
|
||||||
|
via_device=(DOMAIN, format_mac(sync_status.mac)),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_press(self) -> None:
|
||||||
|
"""Handle the button press."""
|
||||||
|
await self.entity_description.press_fn(self._player)
|
@ -22,7 +22,11 @@ from homeassistant.components.media_player import (
|
|||||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
from homeassistant.helpers import (
|
||||||
|
config_validation as cv,
|
||||||
|
entity_platform,
|
||||||
|
issue_registry as ir,
|
||||||
|
)
|
||||||
from homeassistant.helpers.device_registry import (
|
from homeassistant.helpers.device_registry import (
|
||||||
CONNECTION_NETWORK_MAC,
|
CONNECTION_NETWORK_MAC,
|
||||||
DeviceInfo,
|
DeviceInfo,
|
||||||
@ -34,7 +38,7 @@ from homeassistant.helpers.dispatcher import (
|
|||||||
)
|
)
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util, slugify
|
||||||
|
|
||||||
from .const import ATTR_BLUESOUND_GROUP, ATTR_MASTER, DOMAIN
|
from .const import ATTR_BLUESOUND_GROUP, ATTR_MASTER, DOMAIN
|
||||||
from .coordinator import BluesoundCoordinator
|
from .coordinator import BluesoundCoordinator
|
||||||
@ -488,10 +492,36 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
|
|||||||
|
|
||||||
async def async_increase_timer(self) -> int:
|
async def async_increase_timer(self) -> int:
|
||||||
"""Increase sleep time on player."""
|
"""Increase sleep time on player."""
|
||||||
|
ir.async_create_issue(
|
||||||
|
self.hass,
|
||||||
|
DOMAIN,
|
||||||
|
f"deprecated_service_{SERVICE_SET_TIMER}",
|
||||||
|
is_fixable=False,
|
||||||
|
breaks_in_ha_version="2025.12.0",
|
||||||
|
issue_domain=DOMAIN,
|
||||||
|
severity=ir.IssueSeverity.WARNING,
|
||||||
|
translation_key="deprecated_service_set_sleep_timer",
|
||||||
|
translation_placeholders={
|
||||||
|
"name": slugify(self.sync_status.name),
|
||||||
|
},
|
||||||
|
)
|
||||||
return await self._player.sleep_timer()
|
return await self._player.sleep_timer()
|
||||||
|
|
||||||
async def async_clear_timer(self) -> None:
|
async def async_clear_timer(self) -> None:
|
||||||
"""Clear sleep timer on player."""
|
"""Clear sleep timer on player."""
|
||||||
|
ir.async_create_issue(
|
||||||
|
self.hass,
|
||||||
|
DOMAIN,
|
||||||
|
f"deprecated_service_{SERVICE_CLEAR_TIMER}",
|
||||||
|
is_fixable=False,
|
||||||
|
breaks_in_ha_version="2025.12.0",
|
||||||
|
issue_domain=DOMAIN,
|
||||||
|
severity=ir.IssueSeverity.WARNING,
|
||||||
|
translation_key="deprecated_service_clear_sleep_timer",
|
||||||
|
translation_placeholders={
|
||||||
|
"name": slugify(self.sync_status.name),
|
||||||
|
},
|
||||||
|
)
|
||||||
sleep = 1
|
sleep = 1
|
||||||
while sleep > 0:
|
while sleep > 0:
|
||||||
sleep = await self._player.sleep_timer()
|
sleep = await self._player.sleep_timer()
|
||||||
|
@ -26,6 +26,16 @@
|
|||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"issues": {
|
||||||
|
"deprecated_service_set_sleep_timer": {
|
||||||
|
"title": "Detected use of deprecated action bluesound.set_sleep_timer",
|
||||||
|
"description": "Use `button.{name}_set_sleep_timer` instead.\n\nPlease replace this action and adjust your automations and scripts."
|
||||||
|
},
|
||||||
|
"deprecated_service_clear_sleep_timer": {
|
||||||
|
"title": "Detected use of deprecated action bluesound.clear_sleep_timer",
|
||||||
|
"description": "Use `button.{name}_clear_sleep_timer` instead.\n\nPlease replace this action and adjust your automations and scripts."
|
||||||
|
}
|
||||||
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"join": {
|
"join": {
|
||||||
"name": "Join",
|
"name": "Join",
|
||||||
@ -71,5 +81,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"button": {
|
||||||
|
"set_sleep_timer": {
|
||||||
|
"name": "Set sleep timer"
|
||||||
|
},
|
||||||
|
"clear_sleep_timer": {
|
||||||
|
"name": "Clear sleep timer"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
47
tests/components/bluesound/test_button.py
Normal file
47
tests/components/bluesound/test_button.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"""Test for bluesound buttons."""
|
||||||
|
|
||||||
|
from unittest.mock import call
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .conftest import PlayerMocks
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_set_sleep_timer(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
player_mocks: PlayerMocks,
|
||||||
|
setup_config_entry: None,
|
||||||
|
) -> None:
|
||||||
|
"""Test the media player volume set."""
|
||||||
|
await hass.services.async_call(
|
||||||
|
BUTTON_DOMAIN,
|
||||||
|
SERVICE_PRESS,
|
||||||
|
{ATTR_ENTITY_ID: "button.player_name1111_set_sleep_timer"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
player_mocks.player_data.player.sleep_timer.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_clear_sleep_timer(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
player_mocks: PlayerMocks,
|
||||||
|
setup_config_entry: None,
|
||||||
|
) -> None:
|
||||||
|
"""Test the media player volume set."""
|
||||||
|
player_mocks.player_data.player.sleep_timer.side_effect = [15, 30, 45, 60, 90, 0]
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
BUTTON_DOMAIN,
|
||||||
|
SERVICE_PRESS,
|
||||||
|
{ATTR_ENTITY_ID: "button.player_name1111_clear_sleep_timer"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
player_mocks.player_data.player.sleep_timer.assert_has_calls([call()] * 6)
|
Loading…
x
Reference in New Issue
Block a user