Use hardware volume scale and gate manual brightness on auto mode

Max volume limits are on the player's 0-16 hardware step scale, not a
percentage. Manual day/night display brightness sliders are unavailable
while automatic brightness is active. Also add the missing repeat all
icon.

https://claude.ai/code/session_015G3sygJ3js3qaMvw7Kn2Jo
This commit is contained in:
Claude
2026-06-11 20:47:56 +00:00
parent a5a674224d
commit fdc2442cb2
6 changed files with 54 additions and 33 deletions
+3
View File
@@ -95,6 +95,9 @@
},
"pause_volume_down": {
"default": "mdi:volume-minus"
},
"repeat_all": {
"default": "mdi:repeat"
}
},
"time": {
+17 -6
View File
@@ -30,16 +30,18 @@ class YotoNumberEntityDescription(NumberEntityDescription):
value_fn: Callable[[PlayerConfig], int | None]
config_field: str
available_fn: Callable[[PlayerConfig], bool] = lambda config: True
NUMBERS: tuple[YotoNumberEntityDescription, ...] = (
# Max volume limits use the player's 16 hardware volume steps, not a
# percentage — the same scale the firmware reports for playback volume.
YotoNumberEntityDescription(
key="day_max_volume_limit",
translation_key="day_max_volume_limit",
native_min_value=0,
native_max_value=100,
native_max_value=16,
native_step=1,
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.CONFIG,
value_fn=lambda config: config.day_max_volume_limit,
config_field="day_max_volume_limit",
@@ -48,15 +50,15 @@ NUMBERS: tuple[YotoNumberEntityDescription, ...] = (
key="night_max_volume_limit",
translation_key="night_max_volume_limit",
native_min_value=0,
native_max_value=100,
native_max_value=16,
native_step=1,
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.CONFIG,
value_fn=lambda config: config.night_max_volume_limit,
config_field="night_max_volume_limit",
),
# Day/night display brightness report None while auto brightness is
# active; writing a value switches the player to manual brightness.
# Manual day/night display brightness only applies while automatic
# brightness is off; the sliders go unavailable while auto is active
# (turn the matching auto-brightness switch off to use them).
YotoNumberEntityDescription(
key="day_display_brightness",
translation_key="day_display_brightness",
@@ -67,6 +69,7 @@ NUMBERS: tuple[YotoNumberEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
value_fn=lambda config: config.day_display_brightness,
config_field="day_display_brightness",
available_fn=lambda config: not config.day_display_brightness_auto,
),
YotoNumberEntityDescription(
key="night_display_brightness",
@@ -78,6 +81,7 @@ NUMBERS: tuple[YotoNumberEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
value_fn=lambda config: config.night_display_brightness,
config_field="night_display_brightness",
available_fn=lambda config: not config.night_display_brightness_auto,
),
YotoNumberEntityDescription(
key="display_dim_brightness",
@@ -149,6 +153,13 @@ class YotoNumber(YotoEntity, NumberEntity):
self.entity_description = description
self._attr_unique_id = f"{player.id}_{description.key}"
@property
def available(self) -> bool:
"""Return if the entity is available."""
return super().available and self.entity_description.available_fn(
self.player.info.config
)
@property
def native_value(self) -> int | None:
"""Return the number value."""
+2 -2
View File
@@ -95,13 +95,13 @@ def _build_player() -> YotoPlayer:
day_time=dt_time(7, 0),
day_display_brightness_auto=True,
day_ambient_colour="#40bfd9",
day_max_volume_limit=80,
day_max_volume_limit=12,
day_sounds_off=False,
night_time=dt_time(19, 0),
night_display_brightness_auto=False,
night_display_brightness=40,
night_ambient_colour="#000000",
night_max_volume_limit=50,
night_max_volume_limit=8,
night_sounds_off=True,
hour_format=12,
bluetooth_enabled=True,
@@ -61,7 +61,7 @@
'day_ambient_colour': '#40bfd9',
'day_display_brightness': None,
'day_display_brightness_auto': True,
'day_max_volume_limit': 80,
'day_max_volume_limit': 12,
'day_sounds_off': False,
'day_time': dict({
'__type': "<class 'datetime.time'>",
@@ -78,7 +78,7 @@
'night_ambient_colour': '#000000',
'night_display_brightness': 40,
'night_display_brightness_auto': False,
'night_max_volume_limit': 50,
'night_max_volume_limit': 8,
'night_sounds_off': True,
'night_time': dict({
'__type': "<class 'datetime.time'>",
@@ -117,7 +117,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': 'unavailable',
})
# ---
# name: test_all_entities[number.nursery_yoto_day_maximum_volume-entry]
@@ -127,7 +127,7 @@
]),
'area_id': None,
'capabilities': dict({
'max': 100,
'max': 16,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
@@ -159,25 +159,24 @@
'supported_features': 0,
'translation_key': 'day_max_volume_limit',
'unique_id': 'player-test_day_max_volume_limit',
'unit_of_measurement': '%',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[number.nursery_yoto_day_maximum_volume-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Nursery Yoto Day maximum volume',
'max': 100,
'max': 16,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.nursery_yoto_day_maximum_volume',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '80',
'state': '12',
})
# ---
# name: test_all_entities[number.nursery_yoto_dimmed_display_brightness-entry]
@@ -368,7 +367,7 @@
]),
'area_id': None,
'capabilities': dict({
'max': 100,
'max': 16,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
@@ -400,24 +399,23 @@
'supported_features': 0,
'translation_key': 'night_max_volume_limit',
'unique_id': 'player-test_night_max_volume_limit',
'unit_of_measurement': '%',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[number.nursery_yoto_night_maximum_volume-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Nursery Yoto Night maximum volume',
'max': 100,
'max': 16,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.nursery_yoto_night_maximum_volume',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '50',
'state': '8',
})
# ---
+21 -12
View File
@@ -11,7 +11,7 @@ from homeassistant.components.number import (
DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
@@ -48,22 +48,16 @@ async def test_all_entities(
[
pytest.param(
"number.nursery_yoto_day_maximum_volume",
70,
{"day_max_volume_limit": 70},
14,
{"day_max_volume_limit": 14},
id="day-max-volume",
),
pytest.param(
"number.nursery_yoto_night_maximum_volume",
30,
{"night_max_volume_limit": 30},
5,
{"night_max_volume_limit": 5},
id="night-max-volume",
),
pytest.param(
"number.nursery_yoto_day_display_brightness",
90,
{"day_display_brightness": 90},
id="day-display-brightness",
),
pytest.param(
"number.nursery_yoto_night_display_brightness",
20,
@@ -114,6 +108,21 @@ async def test_set_value(
mock_yoto_client.update_player_info.assert_awaited_once_with(PLAYER_ID)
@pytest.mark.usefixtures("mock_yoto_client")
async def test_brightness_unavailable_while_auto(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Manual brightness sliders are unavailable while auto brightness is on."""
await _setup(hass, mock_config_entry)
assert (
hass.states.get("number.nursery_yoto_day_display_brightness").state
== STATE_UNAVAILABLE
)
assert hass.states.get("number.nursery_yoto_night_display_brightness").state == "40"
async def test_set_value_failure(
hass: HomeAssistant,
mock_yoto_client: MagicMock,
@@ -129,6 +138,6 @@ async def test_set_value_failure(
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: "number.nursery_yoto_day_maximum_volume", ATTR_VALUE: 50},
{ATTR_ENTITY_ID: "number.nursery_yoto_day_maximum_volume", ATTR_VALUE: 10},
blocking=True,
)