mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Add OptionsFlow to Squeezebox to allow setting Browse Limit and Volume Step (#129578)
* Initial * prettier strings * Updates * remove error strings * prettier again * Update strings.json vscode prettier fails check * update test to remove invalid value * Remove config_entry __init__ * remove param * Review updates * ruff fixes * Review changes * Shorten options flow ui string * Review changes * Remove errant mock attib --------- Co-authored-by: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com>
This commit is contained in:
parent
09df6c8706
commit
bdeb24cb61
@ -129,10 +129,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) -
|
||||
|
||||
server_coordinator = LMSStatusDataUpdateCoordinator(hass, entry, lms)
|
||||
|
||||
entry.runtime_data = SqueezeboxData(
|
||||
coordinator=server_coordinator,
|
||||
server=lms,
|
||||
)
|
||||
entry.runtime_data = SqueezeboxData(coordinator=server_coordinator, server=lms)
|
||||
|
||||
# set up player discovery
|
||||
known_servers = hass.data.setdefault(DOMAIN, {}).setdefault(KNOWN_SERVERS, {})
|
||||
|
@ -81,11 +81,12 @@ CONTENT_TYPE_TO_CHILD_TYPE = {
|
||||
"New Music": MediaType.ALBUM,
|
||||
}
|
||||
|
||||
BROWSE_LIMIT = 1000
|
||||
|
||||
|
||||
async def build_item_response(
|
||||
entity: MediaPlayerEntity, player: Player, payload: dict[str, str | None]
|
||||
entity: MediaPlayerEntity,
|
||||
player: Player,
|
||||
payload: dict[str, str | None],
|
||||
browse_limit: int,
|
||||
) -> BrowseMedia:
|
||||
"""Create response payload for search described by payload."""
|
||||
|
||||
@ -107,7 +108,7 @@ async def build_item_response(
|
||||
|
||||
result = await player.async_browse(
|
||||
MEDIA_TYPE_TO_SQUEEZEBOX[search_type],
|
||||
limit=BROWSE_LIMIT,
|
||||
limit=browse_limit,
|
||||
browse_id=browse_id,
|
||||
)
|
||||
|
||||
@ -237,7 +238,11 @@ def media_source_content_filter(item: BrowseMedia) -> bool:
|
||||
return item.media_content_type.startswith("audio/")
|
||||
|
||||
|
||||
async def generate_playlist(player: Player, payload: dict[str, str]) -> list | None:
|
||||
async def generate_playlist(
|
||||
player: Player,
|
||||
payload: dict[str, str],
|
||||
browse_limit: int,
|
||||
) -> list | None:
|
||||
"""Generate playlist from browsing payload."""
|
||||
media_type = payload["search_type"]
|
||||
media_id = payload["search_id"]
|
||||
@ -247,7 +252,7 @@ async def generate_playlist(player: Player, payload: dict[str, str]) -> list | N
|
||||
|
||||
browse_id = (SQUEEZEBOX_ID_BY_TYPE[media_type], media_id)
|
||||
result = await player.async_browse(
|
||||
"titles", limit=BROWSE_LIMIT, browse_id=browse_id
|
||||
"titles", limit=browse_limit, browse_id=browse_id
|
||||
)
|
||||
if result and "items" in result:
|
||||
items: list = result["items"]
|
||||
|
@ -11,15 +11,34 @@ from pysqueezebox import Server, async_discover
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.selector import (
|
||||
NumberSelector,
|
||||
NumberSelectorConfig,
|
||||
NumberSelectorMode,
|
||||
)
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
|
||||
from .const import CONF_HTTPS, DEFAULT_PORT, DOMAIN
|
||||
from .const import (
|
||||
CONF_BROWSE_LIMIT,
|
||||
CONF_HTTPS,
|
||||
CONF_VOLUME_STEP,
|
||||
DEFAULT_BROWSE_LIMIT,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_VOLUME_STEP,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -77,6 +96,12 @@ class SqueezeboxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self.data_schema = _base_schema()
|
||||
self.discovery_info: dict[str, Any] | None = None
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler()
|
||||
|
||||
async def _discover(self, uuid: str | None = None) -> None:
|
||||
"""Discover an unconfigured LMS server."""
|
||||
self.discovery_info = None
|
||||
@ -222,3 +247,48 @@ class SqueezeboxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
# if the player is unknown, then we likely need to configure its server
|
||||
return await self.async_step_user()
|
||||
|
||||
|
||||
OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BROWSE_LIMIT): vol.All(
|
||||
NumberSelector(
|
||||
NumberSelectorConfig(min=1, max=65534, mode=NumberSelectorMode.BOX)
|
||||
),
|
||||
vol.Coerce(int),
|
||||
),
|
||||
vol.Required(CONF_VOLUME_STEP): vol.All(
|
||||
NumberSelector(
|
||||
NumberSelectorConfig(min=1, max=20, mode=NumberSelectorMode.SLIDER)
|
||||
),
|
||||
vol.Coerce(int),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
"""Options Flow Handler."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Options Flow Steps."""
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
OPTIONS_SCHEMA,
|
||||
{
|
||||
CONF_BROWSE_LIMIT: self.config_entry.options.get(
|
||||
CONF_BROWSE_LIMIT, DEFAULT_BROWSE_LIMIT
|
||||
),
|
||||
CONF_VOLUME_STEP: self.config_entry.options.get(
|
||||
CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
@ -32,3 +32,7 @@ SIGNAL_PLAYER_DISCOVERED = "squeezebox_player_discovered"
|
||||
SIGNAL_PLAYER_REDISCOVERED = "squeezebox_player_rediscovered"
|
||||
DISCOVERY_INTERVAL = 60
|
||||
PLAYER_UPDATE_INTERVAL = 5
|
||||
CONF_BROWSE_LIMIT = "browse_limit"
|
||||
CONF_VOLUME_STEP = "volume_step"
|
||||
DEFAULT_BROWSE_LIMIT = 1000
|
||||
DEFAULT_VOLUME_STEP = 5
|
||||
|
@ -52,6 +52,10 @@ from .browse_media import (
|
||||
media_source_content_filter,
|
||||
)
|
||||
from .const import (
|
||||
CONF_BROWSE_LIMIT,
|
||||
CONF_VOLUME_STEP,
|
||||
DEFAULT_BROWSE_LIMIT,
|
||||
DEFAULT_VOLUME_STEP,
|
||||
DISCOVERY_TASK,
|
||||
DOMAIN,
|
||||
KNOWN_PLAYERS,
|
||||
@ -166,6 +170,7 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
| MediaPlayerEntityFeature.PAUSE
|
||||
| MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
||||
| MediaPlayerEntityFeature.NEXT_TRACK
|
||||
| MediaPlayerEntityFeature.SEEK
|
||||
@ -184,10 +189,7 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
_attr_name = None
|
||||
_last_update: datetime | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: SqueezeBoxPlayerUpdateCoordinator,
|
||||
) -> None:
|
||||
def __init__(self, coordinator: SqueezeBoxPlayerUpdateCoordinator) -> None:
|
||||
"""Initialize the SqueezeBox device."""
|
||||
super().__init__(coordinator)
|
||||
player = coordinator.player
|
||||
@ -223,6 +225,23 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
self._last_update = utcnow()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def volume_step(self) -> float:
|
||||
"""Return the step to be used for volume up down."""
|
||||
return float(
|
||||
self.coordinator.config_entry.options.get(
|
||||
CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP
|
||||
)
|
||||
/ 100
|
||||
)
|
||||
|
||||
@property
|
||||
def browse_limit(self) -> int:
|
||||
"""Return the step to be used for volume up down."""
|
||||
return self.coordinator.config_entry.options.get(
|
||||
CONF_BROWSE_LIMIT, DEFAULT_BROWSE_LIMIT
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
@ -366,16 +385,6 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
await self._player.async_set_power(False)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Volume up media player."""
|
||||
await self._player.async_set_volume("+5")
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Volume down media player."""
|
||||
await self._player.async_set_volume("-5")
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
volume_percent = str(int(volume * 100))
|
||||
@ -466,7 +475,11 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
"search_id": media_id,
|
||||
"search_type": MediaType.PLAYLIST,
|
||||
}
|
||||
playlist = await generate_playlist(self._player, payload)
|
||||
playlist = await generate_playlist(
|
||||
self._player,
|
||||
payload,
|
||||
self.browse_limit,
|
||||
)
|
||||
except BrowseError:
|
||||
# a list of urls
|
||||
content = json.loads(media_id)
|
||||
@ -477,7 +490,11 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
"search_id": media_id,
|
||||
"search_type": media_type,
|
||||
}
|
||||
playlist = await generate_playlist(self._player, payload)
|
||||
playlist = await generate_playlist(
|
||||
self._player,
|
||||
payload,
|
||||
self.browse_limit,
|
||||
)
|
||||
|
||||
_LOGGER.debug("Generated playlist: %s", playlist)
|
||||
|
||||
@ -587,7 +604,12 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
"search_id": media_content_id,
|
||||
}
|
||||
|
||||
return await build_item_response(self, self._player, payload)
|
||||
return await build_item_response(
|
||||
self,
|
||||
self._player,
|
||||
payload,
|
||||
self.browse_limit,
|
||||
)
|
||||
|
||||
async def async_get_browse_image(
|
||||
self,
|
||||
|
@ -103,5 +103,20 @@
|
||||
"unit_of_measurement": "[%key:component::squeezebox::entity::sensor::player_count::unit_of_measurement%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "LMS Configuration",
|
||||
"data": {
|
||||
"browse_limit": "Browse limit",
|
||||
"volume_step": "Volume step"
|
||||
},
|
||||
"data_description": {
|
||||
"browse_limit": "Maximum number of items when browsing or in a playlist.",
|
||||
"volume_step": "Amount to adjust the volume when turning volume up or down."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,9 @@ from homeassistant.helpers.device_registry import format_mac
|
||||
# from homeassistant.setup import async_setup_component
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
CONF_VOLUME_STEP = "volume_step"
|
||||
TEST_VOLUME_STEP = 10
|
||||
|
||||
TEST_HOST = "1.2.3.4"
|
||||
TEST_PORT = "9000"
|
||||
TEST_USE_HTTPS = False
|
||||
@ -109,6 +112,9 @@ def config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
CONF_PORT: TEST_PORT,
|
||||
const.CONF_HTTPS: TEST_USE_HTTPS,
|
||||
},
|
||||
options={
|
||||
CONF_VOLUME_STEP: TEST_VOLUME_STEP,
|
||||
},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
return config_entry
|
||||
|
@ -65,7 +65,7 @@
|
||||
'original_name': None,
|
||||
'platform': 'squeezebox',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <MediaPlayerEntityFeature: 3077055>,
|
||||
'supported_features': <MediaPlayerEntityFeature: 3078079>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff',
|
||||
'unit_of_measurement': None,
|
||||
@ -88,7 +88,7 @@
|
||||
}),
|
||||
'repeat': <RepeatMode.OFF: 'off'>,
|
||||
'shuffle': False,
|
||||
'supported_features': <MediaPlayerEntityFeature: 3077055>,
|
||||
'supported_features': <MediaPlayerEntityFeature: 3078079>,
|
||||
'volume_level': 0.01,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
|
@ -6,7 +6,12 @@ from unittest.mock import patch
|
||||
from pysqueezebox import Server
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.squeezebox.const import CONF_HTTPS, DOMAIN
|
||||
from homeassistant.components.squeezebox.const import (
|
||||
CONF_BROWSE_LIMIT,
|
||||
CONF_HTTPS,
|
||||
CONF_VOLUME_STEP,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
@ -19,6 +24,8 @@ HOST2 = "2.2.2.2"
|
||||
PORT = 9000
|
||||
UUID = "test-uuid"
|
||||
UNKNOWN_ERROR = "1234"
|
||||
BROWSE_LIMIT = 10
|
||||
VOLUME_STEP = 1
|
||||
|
||||
|
||||
async def mock_discover(_discovery_callback):
|
||||
@ -87,6 +94,45 @@ async def test_user_form(hass: HomeAssistant) -> None:
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_options_form(hass: HomeAssistant) -> None:
|
||||
"""Test we can configure options."""
|
||||
entry = MockConfigEntry(
|
||||
data={
|
||||
CONF_HOST: HOST,
|
||||
CONF_PORT: PORT,
|
||||
CONF_USERNAME: "",
|
||||
CONF_PASSWORD: "",
|
||||
CONF_HTTPS: False,
|
||||
},
|
||||
unique_id=UUID,
|
||||
domain=DOMAIN,
|
||||
options={CONF_BROWSE_LIMIT: 1000, CONF_VOLUME_STEP: 5},
|
||||
)
|
||||
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
# simulate manual input of options
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_BROWSE_LIMIT: BROWSE_LIMIT, CONF_VOLUME_STEP: VOLUME_STEP},
|
||||
)
|
||||
|
||||
# put some meaningful asserts here
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
assert result["data"] == {
|
||||
CONF_BROWSE_LIMIT: BROWSE_LIMIT,
|
||||
CONF_VOLUME_STEP: VOLUME_STEP,
|
||||
}
|
||||
|
||||
|
||||
async def test_user_form_timeout(hass: HomeAssistant) -> None:
|
||||
"""Test we handle server search timeout."""
|
||||
with (
|
||||
|
@ -68,7 +68,7 @@ from homeassistant.helpers.device_registry import DeviceRegistry
|
||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .conftest import FAKE_VALID_ITEM_ID, TEST_MAC
|
||||
from .conftest import FAKE_VALID_ITEM_ID, TEST_MAC, TEST_VOLUME_STEP
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
@ -183,26 +183,32 @@ async def test_squeezebox_volume_up(
|
||||
hass: HomeAssistant, configured_player: MagicMock
|
||||
) -> None:
|
||||
"""Test volume up service call."""
|
||||
configured_player.volume = 50
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_VOLUME_UP,
|
||||
{ATTR_ENTITY_ID: "media_player.test_player"},
|
||||
blocking=True,
|
||||
)
|
||||
configured_player.async_set_volume.assert_called_once_with("+5")
|
||||
configured_player.async_set_volume.assert_called_once_with(
|
||||
str(configured_player.volume + TEST_VOLUME_STEP)
|
||||
)
|
||||
|
||||
|
||||
async def test_squeezebox_volume_down(
|
||||
hass: HomeAssistant, configured_player: MagicMock
|
||||
) -> None:
|
||||
"""Test volume down service call."""
|
||||
configured_player.volume = 50
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_VOLUME_DOWN,
|
||||
{ATTR_ENTITY_ID: "media_player.test_player"},
|
||||
blocking=True,
|
||||
)
|
||||
configured_player.async_set_volume.assert_called_once_with("-5")
|
||||
configured_player.async_set_volume.assert_called_once_with(
|
||||
str(configured_player.volume - TEST_VOLUME_STEP)
|
||||
)
|
||||
|
||||
|
||||
async def test_squeezebox_volume_set(
|
||||
|
Loading…
x
Reference in New Issue
Block a user