From fdc2442cb2feef67b9ad70c34292ee87b29eb3ae Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 20:47:56 +0000 Subject: [PATCH] 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 --- homeassistant/components/yoto/icons.json | 3 ++ homeassistant/components/yoto/number.py | 23 +++++++++---- tests/components/yoto/conftest.py | 4 +-- .../yoto/snapshots/test_diagnostics.ambr | 4 +-- .../yoto/snapshots/test_number.ambr | 20 +++++------ tests/components/yoto/test_number.py | 33 ++++++++++++------- 6 files changed, 54 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/yoto/icons.json b/homeassistant/components/yoto/icons.json index c3e24b84296a..8cf216fa45d0 100644 --- a/homeassistant/components/yoto/icons.json +++ b/homeassistant/components/yoto/icons.json @@ -95,6 +95,9 @@ }, "pause_volume_down": { "default": "mdi:volume-minus" + }, + "repeat_all": { + "default": "mdi:repeat" } }, "time": { diff --git a/homeassistant/components/yoto/number.py b/homeassistant/components/yoto/number.py index 922ab72184b4..9d6a56ff96b0 100644 --- a/homeassistant/components/yoto/number.py +++ b/homeassistant/components/yoto/number.py @@ -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.""" diff --git a/tests/components/yoto/conftest.py b/tests/components/yoto/conftest.py index 7e50bb3acb69..e9a8dd239b63 100644 --- a/tests/components/yoto/conftest.py +++ b/tests/components/yoto/conftest.py @@ -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, diff --git a/tests/components/yoto/snapshots/test_diagnostics.ambr b/tests/components/yoto/snapshots/test_diagnostics.ambr index 2131405a412f..e329ee03df07 100644 --- a/tests/components/yoto/snapshots/test_diagnostics.ambr +++ b/tests/components/yoto/snapshots/test_diagnostics.ambr @@ -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': "", @@ -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': "", diff --git a/tests/components/yoto/snapshots/test_number.ambr b/tests/components/yoto/snapshots/test_number.ambr index af975dad46c4..38b050c8f4e1 100644 --- a/tests/components/yoto/snapshots/test_number.ambr +++ b/tests/components/yoto/snapshots/test_number.ambr @@ -117,7 +117,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - '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': , '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': , 'step': 1, - 'unit_of_measurement': '%', }), 'context': , 'entity_id': 'number.nursery_yoto_day_maximum_volume', 'last_changed': , 'last_reported': , 'last_updated': , - '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': , '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': , 'step': 1, - 'unit_of_measurement': '%', }), 'context': , 'entity_id': 'number.nursery_yoto_night_maximum_volume', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '50', + 'state': '8', }) # --- diff --git a/tests/components/yoto/test_number.py b/tests/components/yoto/test_number.py index 1de14b2b244f..c1e1e1298942 100644 --- a/tests/components/yoto/test_number.py +++ b/tests/components/yoto/test_number.py @@ -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, )