mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 10:17:09 +00:00
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:
parent
70ed120c6e
commit
e48a25e952
@ -53,6 +53,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
PLATFORMS = [
|
PLATFORMS = [
|
||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
|
Platform.BUTTON,
|
||||||
Platform.MEDIA_PLAYER,
|
Platform.MEDIA_PLAYER,
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
]
|
]
|
||||||
|
155
homeassistant/components/squeezebox/button.py
Normal file
155
homeassistant/components/squeezebox/button.py
Normal 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)
|
@ -1,11 +1,37 @@
|
|||||||
"""Base class for Squeezebox Sensor entities."""
|
"""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.entity import EntityDescription
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN, STATUS_QUERY_UUID
|
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]):
|
class LMSStatusEntity(CoordinatorEntity[LMSStatusDataUpdateCoordinator]):
|
||||||
|
@ -35,15 +35,10 @@ from homeassistant.helpers import (
|
|||||||
entity_platform,
|
entity_platform,
|
||||||
entity_registry as er,
|
entity_registry as er,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.device_registry import (
|
from homeassistant.helpers.device_registry import format_mac
|
||||||
CONNECTION_NETWORK_MAC,
|
|
||||||
DeviceInfo,
|
|
||||||
format_mac,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.helpers.start import async_at_start
|
from homeassistant.helpers.start import async_at_start
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from .browse_media import (
|
from .browse_media import (
|
||||||
@ -68,6 +63,7 @@ from .const import (
|
|||||||
SQUEEZEBOX_SOURCE_STRINGS,
|
SQUEEZEBOX_SOURCE_STRINGS,
|
||||||
)
|
)
|
||||||
from .coordinator import SqueezeBoxPlayerUpdateCoordinator
|
from .coordinator import SqueezeBoxPlayerUpdateCoordinator
|
||||||
|
from .entity import SqueezeboxEntity
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import SqueezeboxConfigEntry
|
from . import SqueezeboxConfigEntry
|
||||||
@ -181,9 +177,7 @@ def get_announce_timeout(extra: dict) -> int | None:
|
|||||||
return announce_timeout
|
return announce_timeout
|
||||||
|
|
||||||
|
|
||||||
class SqueezeBoxMediaPlayerEntity(
|
class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||||
CoordinatorEntity[SqueezeBoxPlayerUpdateCoordinator], MediaPlayerEntity
|
|
||||||
):
|
|
||||||
"""Representation of the media player features of a SqueezeBox device.
|
"""Representation of the media player features of a SqueezeBox device.
|
||||||
|
|
||||||
Wraps a pysqueezebox.Player() object.
|
Wraps a pysqueezebox.Player() object.
|
||||||
@ -217,30 +211,10 @@ class SqueezeBoxMediaPlayerEntity(
|
|||||||
def __init__(self, coordinator: SqueezeBoxPlayerUpdateCoordinator) -> None:
|
def __init__(self, coordinator: SqueezeBoxPlayerUpdateCoordinator) -> None:
|
||||||
"""Initialize the SqueezeBox device."""
|
"""Initialize the SqueezeBox device."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
player = coordinator.player
|
|
||||||
self._player = player
|
|
||||||
self._query_result: bool | dict = {}
|
self._query_result: bool | dict = {}
|
||||||
self._remove_dispatcher: Callable | None = None
|
self._remove_dispatcher: Callable | None = None
|
||||||
self._previous_media_position = 0
|
self._previous_media_position = 0
|
||||||
self._attr_unique_id = format_mac(player.player_id)
|
self._attr_unique_id = format_mac(self._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._browse_data = BrowseData()
|
self._browse_data = BrowseData()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -63,6 +63,29 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"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": {
|
"binary_sensor": {
|
||||||
"rescan": {
|
"rescan": {
|
||||||
"name": "Library rescan"
|
"name": "Library rescan"
|
||||||
|
@ -269,6 +269,7 @@ def mock_pysqueezebox_player(uuid: str) -> MagicMock:
|
|||||||
mock_player.title = None
|
mock_player.title = None
|
||||||
mock_player.image_url = None
|
mock_player.image_url = None
|
||||||
mock_player.model = "SqueezeLite"
|
mock_player.model = "SqueezeLite"
|
||||||
|
mock_player.creator = "Ralph Irving & Adrian Smith"
|
||||||
|
|
||||||
return mock_player
|
return mock_player
|
||||||
|
|
||||||
@ -309,7 +310,27 @@ async def configure_squeezebox_media_player_platform(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Configure a squeezebox config entry with appropriate mocks for media_player."""
|
"""Configure a squeezebox config entry with appropriate mocks for media_player."""
|
||||||
with (
|
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),
|
patch("homeassistant.components.squeezebox.Server", return_value=lms),
|
||||||
):
|
):
|
||||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
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]
|
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
|
@pytest.fixture
|
||||||
async def configured_players(
|
async def configured_players(
|
||||||
hass: HomeAssistant, config_entry: MockConfigEntry, lms_factory: MagicMock
|
hass: HomeAssistant, config_entry: MockConfigEntry, lms_factory: MagicMock
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
'is_new': False,
|
'is_new': False,
|
||||||
'labels': set({
|
'labels': set({
|
||||||
}),
|
}),
|
||||||
'manufacturer': 'Ralph Irving',
|
'manufacturer': 'Ralph Irving & Adrian Smith',
|
||||||
'model': 'SqueezeLite',
|
'model': 'SqueezeLite',
|
||||||
'model_id': None,
|
'model_id': None,
|
||||||
'name': 'Test Player',
|
'name': 'Test Player',
|
||||||
|
23
tests/components/squeezebox/test_button.py
Normal file
23
tests/components/squeezebox/test_button.py
Normal 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"
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user