Add button platform for Squeezebox integration (#140697)

* initial

* trans key correction

* base class updates

* model tidy up

* Update homeassistant/components/squeezebox/strings.json

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/squeezebox/entity.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/squeezebox/media_player.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/squeezebox/media_player.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/squeezebox/button.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* review updates

* update

* move manufacturer to library

* updates

* list concat

* review updates

* Update tests/components/squeezebox/test_button.py

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
peteS-UK 2025-03-20 16:18:08 +00:00 committed by GitHub
parent 70ed120c6e
commit e48a25e952
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 266 additions and 34 deletions

View File

@ -53,6 +53,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.MEDIA_PLAYER,
Platform.SENSOR,
]

View File

@ -0,0 +1,155 @@
"""Platform for button integration for squeezebox."""
from __future__ import annotations
from dataclasses import dataclass
import logging
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SqueezeboxConfigEntry
from .const import SIGNAL_PLAYER_DISCOVERED
from .coordinator import SqueezeBoxPlayerUpdateCoordinator
from .entity import SqueezeboxEntity
_LOGGER = logging.getLogger(__name__)
HARDWARE_MODELS_WITH_SCREEN = [
"Squeezebox Boom",
"Squeezebox Radio",
"Transporter",
"Squeezebox Touch",
"Squeezebox",
"SliMP3",
"Squeezebox 1",
"Squeezebox 2",
"Squeezebox 3",
]
HARDWARE_MODELS_WITH_TONE = [
*HARDWARE_MODELS_WITH_SCREEN,
"Squeezebox Receiver",
]
@dataclass(frozen=True, kw_only=True)
class SqueezeboxButtonEntityDescription(ButtonEntityDescription):
"""Squeezebox Button description."""
press_action: str
BUTTON_ENTITIES: tuple[SqueezeboxButtonEntityDescription, ...] = tuple(
SqueezeboxButtonEntityDescription(
key=f"preset_{i}",
translation_key="preset",
translation_placeholders={"index": str(i)},
press_action=f"preset_{i}.single",
)
for i in range(1, 7)
)
SCREEN_BUTTON_ENTITIES: tuple[SqueezeboxButtonEntityDescription, ...] = (
SqueezeboxButtonEntityDescription(
key="brightness_up",
translation_key="brightness_up",
press_action="brightness_up",
),
SqueezeboxButtonEntityDescription(
key="brightness_down",
translation_key="brightness_down",
press_action="brightness_down",
),
)
TONE_BUTTON_ENTITIES: tuple[SqueezeboxButtonEntityDescription, ...] = (
SqueezeboxButtonEntityDescription(
key="bass_up",
translation_key="bass_up",
press_action="bass_up",
),
SqueezeboxButtonEntityDescription(
key="bass_down",
translation_key="bass_down",
press_action="bass_down",
),
SqueezeboxButtonEntityDescription(
key="treble_up",
translation_key="treble_up",
press_action="treble_up",
),
SqueezeboxButtonEntityDescription(
key="treble_down",
translation_key="treble_down",
press_action="treble_down",
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: SqueezeboxConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Squeezebox button platform from a server config entry."""
# Add button entities when player discovered
async def _player_discovered(
player_coordinator: SqueezeBoxPlayerUpdateCoordinator,
) -> None:
_LOGGER.debug(
"Setting up button entity for player %s, model %s",
player_coordinator.player.name,
player_coordinator.player.model,
)
entities: list[SqueezeboxButtonEntity] = []
entities.extend(
SqueezeboxButtonEntity(player_coordinator, description)
for description in BUTTON_ENTITIES
)
entities.extend(
SqueezeboxButtonEntity(player_coordinator, description)
for description in TONE_BUTTON_ENTITIES
if player_coordinator.player.model in HARDWARE_MODELS_WITH_TONE
)
entities.extend(
SqueezeboxButtonEntity(player_coordinator, description)
for description in SCREEN_BUTTON_ENTITIES
if player_coordinator.player.model in HARDWARE_MODELS_WITH_SCREEN
)
async_add_entities(entities)
entry.async_on_unload(
async_dispatcher_connect(hass, SIGNAL_PLAYER_DISCOVERED, _player_discovered)
)
class SqueezeboxButtonEntity(SqueezeboxEntity, ButtonEntity):
"""Representation of Buttons for Squeezebox entities."""
entity_description: SqueezeboxButtonEntityDescription
def __init__(
self,
coordinator: SqueezeBoxPlayerUpdateCoordinator,
entity_description: SqueezeboxButtonEntityDescription,
) -> None:
"""Initialize the SqueezeBox Button."""
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_unique_id = (
f"{format_mac(self._player.player_id)}_{entity_description.key}"
)
async def async_press(self) -> None:
"""Execute the button action."""
await self._player.async_query("button", self.entity_description.press_action)

View File

@ -1,11 +1,37 @@
"""Base class for Squeezebox Sensor entities."""
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, STATUS_QUERY_UUID
from .coordinator import LMSStatusDataUpdateCoordinator
from .coordinator import (
LMSStatusDataUpdateCoordinator,
SqueezeBoxPlayerUpdateCoordinator,
)
class SqueezeboxEntity(CoordinatorEntity[SqueezeBoxPlayerUpdateCoordinator]):
"""Base entity class for Squeezebox entities."""
_attr_has_entity_name = True
def __init__(self, coordinator: SqueezeBoxPlayerUpdateCoordinator) -> None:
"""Initialize the SqueezeBox entity."""
super().__init__(coordinator)
self._player = coordinator.player
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, format_mac(self._player.player_id))},
name=self._player.name,
connections={(CONNECTION_NETWORK_MAC, format_mac(self._player.player_id))},
via_device=(DOMAIN, coordinator.server_uuid),
model=self._player.model,
manufacturer=self._player.creator,
)
class LMSStatusEntity(CoordinatorEntity[LMSStatusDataUpdateCoordinator]):

View File

@ -35,15 +35,10 @@ from homeassistant.helpers import (
entity_platform,
entity_registry as er,
)
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.start import async_at_start
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.dt import utcnow
from .browse_media import (
@ -68,6 +63,7 @@ from .const import (
SQUEEZEBOX_SOURCE_STRINGS,
)
from .coordinator import SqueezeBoxPlayerUpdateCoordinator
from .entity import SqueezeboxEntity
if TYPE_CHECKING:
from . import SqueezeboxConfigEntry
@ -181,9 +177,7 @@ def get_announce_timeout(extra: dict) -> int | None:
return announce_timeout
class SqueezeBoxMediaPlayerEntity(
CoordinatorEntity[SqueezeBoxPlayerUpdateCoordinator], MediaPlayerEntity
):
class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
"""Representation of the media player features of a SqueezeBox device.
Wraps a pysqueezebox.Player() object.
@ -217,30 +211,10 @@ class SqueezeBoxMediaPlayerEntity(
def __init__(self, coordinator: SqueezeBoxPlayerUpdateCoordinator) -> None:
"""Initialize the SqueezeBox device."""
super().__init__(coordinator)
player = coordinator.player
self._player = player
self._query_result: bool | dict = {}
self._remove_dispatcher: Callable | None = None
self._previous_media_position = 0
self._attr_unique_id = format_mac(player.player_id)
_manufacturer = None
if player.model.startswith("SqueezeLite") or "SqueezePlay" in player.model:
_manufacturer = "Ralph Irving"
elif (
"Squeezebox" in player.model
or "Transporter" in player.model
or "Slim" in player.model
):
_manufacturer = "Logitech"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
name=player.name,
connections={(CONNECTION_NETWORK_MAC, self._attr_unique_id)},
via_device=(DOMAIN, coordinator.server_uuid),
model=player.model,
manufacturer=_manufacturer,
)
self._attr_unique_id = format_mac(self._player.player_id)
self._browse_data = BrowseData()
@callback

View File

@ -63,6 +63,29 @@
}
},
"entity": {
"button": {
"preset": {
"name": "Preset {index}"
},
"brightness_up": {
"name": "Brightness up"
},
"brightness_down": {
"name": "Brightness down"
},
"bass_up": {
"name": "Bass up"
},
"bass_down": {
"name": "Bass down"
},
"treble_up": {
"name": "Treble up"
},
"treble_down": {
"name": "Treble down"
}
},
"binary_sensor": {
"rescan": {
"name": "Library rescan"

View File

@ -269,6 +269,7 @@ def mock_pysqueezebox_player(uuid: str) -> MagicMock:
mock_player.title = None
mock_player.image_url = None
mock_player.model = "SqueezeLite"
mock_player.creator = "Ralph Irving & Adrian Smith"
return mock_player
@ -309,7 +310,27 @@ async def configure_squeezebox_media_player_platform(
) -> None:
"""Configure a squeezebox config entry with appropriate mocks for media_player."""
with (
patch("homeassistant.components.squeezebox.PLATFORMS", [Platform.MEDIA_PLAYER]),
patch(
"homeassistant.components.squeezebox.PLATFORMS",
[Platform.MEDIA_PLAYER],
),
patch("homeassistant.components.squeezebox.Server", return_value=lms),
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done(wait_background_tasks=True)
async def configure_squeezebox_media_player_button_platform(
hass: HomeAssistant,
config_entry: MockConfigEntry,
lms: MagicMock,
) -> None:
"""Configure a squeezebox config entry with appropriate mocks for media_player."""
with (
patch(
"homeassistant.components.squeezebox.PLATFORMS",
[Platform.BUTTON],
),
patch("homeassistant.components.squeezebox.Server", return_value=lms),
):
await hass.config_entries.async_setup(config_entry.entry_id)
@ -325,6 +346,15 @@ async def configured_player(
return (await lms.async_get_players())[0]
@pytest.fixture
async def configured_player_with_button(
hass: HomeAssistant, config_entry: MockConfigEntry, lms: MagicMock
) -> MagicMock:
"""Fixture mocking calls to pysqueezebox Player from a configured squeezebox."""
await configure_squeezebox_media_player_button_platform(hass, config_entry, lms)
return (await lms.async_get_players())[0]
@pytest.fixture
async def configured_players(
hass: HomeAssistant, config_entry: MockConfigEntry, lms_factory: MagicMock

View File

@ -24,7 +24,7 @@
'is_new': False,
'labels': set({
}),
'manufacturer': 'Ralph Irving',
'manufacturer': 'Ralph Irving & Adrian Smith',
'model': 'SqueezeLite',
'model_id': None,
'name': 'Test Player',

View File

@ -0,0 +1,23 @@
"""Tests for the squeezebox button component."""
from unittest.mock import MagicMock
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
async def test_squeezebox_press(
hass: HomeAssistant, configured_player_with_button: MagicMock
) -> None:
"""Test press service call."""
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: "button.test_player_preset_1"},
blocking=True,
)
configured_player_with_button.async_query.assert_called_with(
"button", "preset_1.single"
)