mirror of
https://github.com/home-assistant/core.git
synced 2026-04-20 00:19:02 +02:00
Compare commits
3 Commits
ariel-pyth
...
claude/med
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6a3381081 | ||
|
|
2fb6cb15f6 | ||
|
|
33d0029afb |
@@ -52,7 +52,7 @@ from homeassistant.const import ( # noqa: F401
|
||||
STATE_PLAYING,
|
||||
STATE_STANDBY,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, SupportsResponse
|
||||
from homeassistant.core import HomeAssistant, SupportsResponse, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
@@ -102,6 +102,7 @@ from .const import ( # noqa: F401
|
||||
ATTR_MEDIA_VOLUME_MUTED,
|
||||
ATTR_SOUND_MODE,
|
||||
ATTR_SOUND_MODE_LIST,
|
||||
CONF_MAX_VOLUME,
|
||||
CONTENT_AUTH_EXPIRY_TIME,
|
||||
DOMAIN,
|
||||
INTENT_MEDIA_SEARCH_AND_PLAY,
|
||||
@@ -303,13 +304,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
component.async_register_entity_service(
|
||||
SERVICE_VOLUME_UP,
|
||||
None,
|
||||
"async_volume_up",
|
||||
"_async_handle_volume_up",
|
||||
[MediaPlayerEntityFeature.VOLUME_SET, MediaPlayerEntityFeature.VOLUME_STEP],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_VOLUME_DOWN,
|
||||
None,
|
||||
"async_volume_down",
|
||||
"_async_handle_volume_down",
|
||||
[MediaPlayerEntityFeature.VOLUME_SET, MediaPlayerEntityFeature.VOLUME_STEP],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
@@ -353,7 +354,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
),
|
||||
_rename_keys(volume=ATTR_MEDIA_VOLUME_LEVEL),
|
||||
),
|
||||
"async_set_volume_level",
|
||||
"_async_handle_set_volume_level",
|
||||
[MediaPlayerEntityFeature.VOLUME_SET],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
@@ -552,6 +553,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
|
||||
entity_description: MediaPlayerEntityDescription
|
||||
_access_token: str | None = None
|
||||
_media_player_option_max_volume: float | None = None
|
||||
|
||||
_attr_app_id: str | None = None
|
||||
_attr_app_name: str | None = None
|
||||
@@ -799,6 +801,33 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Flag media player features that are supported."""
|
||||
return self._attr_supported_features
|
||||
|
||||
async def async_internal_added_to_hass(self) -> None:
|
||||
"""Call when the media player entity is added to hass."""
|
||||
await super().async_internal_added_to_hass()
|
||||
if not self.registry_entry:
|
||||
return
|
||||
self._async_read_entity_options()
|
||||
|
||||
@callback
|
||||
def async_registry_entry_updated(self) -> None:
|
||||
"""Run when the entity registry entry has been updated."""
|
||||
self._async_read_entity_options()
|
||||
|
||||
@callback
|
||||
def _async_read_entity_options(self) -> None:
|
||||
"""Read entity options from entity registry.
|
||||
|
||||
Called when the entity registry entry has been updated and before the
|
||||
media player is added to the state machine.
|
||||
"""
|
||||
assert self.registry_entry
|
||||
if (
|
||||
media_player_options := self.registry_entry.options.get(DOMAIN)
|
||||
) and (max_volume := media_player_options.get(CONF_MAX_VOLUME)) is not None:
|
||||
self._media_player_option_max_volume = max_volume
|
||||
return
|
||||
self._media_player_option_max_volume = None
|
||||
|
||||
def turn_on(self) -> None:
|
||||
"""Turn the media player on."""
|
||||
raise NotImplementedError
|
||||
@@ -831,6 +860,51 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Set volume level, range 0..1."""
|
||||
await self.hass.async_add_executor_job(self.set_volume_level, volume)
|
||||
|
||||
@final
|
||||
async def _async_handle_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, rescaling to max volume if set."""
|
||||
if self._media_player_option_max_volume is not None:
|
||||
volume = volume * self._media_player_option_max_volume
|
||||
await self.async_set_volume_level(volume)
|
||||
|
||||
@final
|
||||
async def _async_handle_volume_up(self) -> None:
|
||||
"""Handle volume up, respecting max volume setting."""
|
||||
if self._media_player_option_max_volume is not None:
|
||||
max_volume = self._media_player_option_max_volume
|
||||
if (
|
||||
self.volume_level is not None
|
||||
and self.volume_level < max_volume
|
||||
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features
|
||||
):
|
||||
await self.async_set_volume_level(
|
||||
min(
|
||||
max_volume,
|
||||
self.volume_level + self.volume_step * max_volume,
|
||||
)
|
||||
)
|
||||
return
|
||||
await self.async_volume_up()
|
||||
|
||||
@final
|
||||
async def _async_handle_volume_down(self) -> None:
|
||||
"""Handle volume down, respecting max volume setting."""
|
||||
if self._media_player_option_max_volume is not None:
|
||||
max_volume = self._media_player_option_max_volume
|
||||
if (
|
||||
self.volume_level is not None
|
||||
and self.volume_level > 0
|
||||
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features
|
||||
):
|
||||
await self.async_set_volume_level(
|
||||
max(
|
||||
0,
|
||||
self.volume_level - self.volume_step * max_volume,
|
||||
)
|
||||
)
|
||||
return
|
||||
await self.async_volume_down()
|
||||
|
||||
def media_play(self) -> None:
|
||||
"""Send play command."""
|
||||
raise NotImplementedError
|
||||
@@ -1136,6 +1210,16 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if (value := getattr(self, attr)) is not None:
|
||||
state_attr[attr] = value
|
||||
|
||||
if (
|
||||
self._media_player_option_max_volume is not None
|
||||
and ATTR_MEDIA_VOLUME_LEVEL in state_attr
|
||||
):
|
||||
state_attr[ATTR_MEDIA_VOLUME_LEVEL] = min(
|
||||
1.0,
|
||||
state_attr[ATTR_MEDIA_VOLUME_LEVEL]
|
||||
/ self._media_player_option_max_volume,
|
||||
)
|
||||
|
||||
if self.media_image_remotely_accessible:
|
||||
state_attr[ATTR_ENTITY_PICTURE_LOCAL] = self.media_image_local
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ ATTR_MEDIA_VOLUME_MUTED = "is_volume_muted"
|
||||
ATTR_SOUND_MODE = "sound_mode"
|
||||
ATTR_SOUND_MODE_LIST = "sound_mode_list"
|
||||
|
||||
CONF_MAX_VOLUME = "max_volume"
|
||||
|
||||
DOMAIN = "media_player"
|
||||
|
||||
INTENT_MEDIA_PAUSE = "HassMediaPause"
|
||||
|
||||
207
tests/components/media_player/conftest.py
Normal file
207
tests/components/media_player/conftest.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""Fixtures for the media player entity platform tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
DOMAIN,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
MockModule,
|
||||
MockPlatform,
|
||||
mock_config_flow,
|
||||
mock_integration,
|
||||
mock_platform,
|
||||
)
|
||||
|
||||
TEST_DOMAIN = "test"
|
||||
|
||||
|
||||
class MockMediaPlayer(MediaPlayerEntity):
|
||||
"""Mocked media player entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
supported_features: MediaPlayerEntityFeature = MediaPlayerEntityFeature(0),
|
||||
) -> None:
|
||||
"""Initialize the media player."""
|
||||
self._volume = 0.0
|
||||
self.calls_set_volume = MagicMock()
|
||||
self._attr_supported_features = supported_features
|
||||
self._attr_has_entity_name = True
|
||||
self._attr_name = "test_media_player"
|
||||
self._attr_unique_id = "very_unique_media_player_id"
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
def volume_level(self) -> float:
|
||||
"""Volume level of the media player (0..1)."""
|
||||
return self._volume
|
||||
|
||||
def set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
self._volume = volume
|
||||
self.calls_set_volume(volume=volume)
|
||||
|
||||
|
||||
class MockMediaPlayerVolumeUpDown(MockMediaPlayer):
|
||||
"""Mocked media player entity with custom volume_up/volume_down."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
supported_features: MediaPlayerEntityFeature = MediaPlayerEntityFeature(0),
|
||||
) -> None:
|
||||
"""Initialize the media player."""
|
||||
super().__init__(supported_features)
|
||||
self.calls_volume_up = MagicMock()
|
||||
self.calls_volume_down = MagicMock()
|
||||
|
||||
def volume_up(self) -> None:
|
||||
"""Turn volume up for media player."""
|
||||
self.calls_volume_up()
|
||||
if self.volume_level < 1:
|
||||
self.set_volume_level(min(1, self.volume_level + 0.1))
|
||||
|
||||
def volume_down(self) -> None:
|
||||
"""Turn volume down for media player."""
|
||||
self.calls_volume_down()
|
||||
if self.volume_level > 0:
|
||||
self.set_volume_level(max(0, self.volume_level - 0.1))
|
||||
|
||||
|
||||
class MockFlow(ConfigFlow):
|
||||
"""Test flow."""
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def config_flow_fixture(hass: HomeAssistant) -> Generator[None]:
|
||||
"""Mock config flow."""
|
||||
mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
|
||||
|
||||
with mock_config_flow(TEST_DOMAIN, MockFlow):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="supported_features")
|
||||
async def media_player_supported_features() -> MediaPlayerEntityFeature:
|
||||
"""Return the supported features for the test media player entity."""
|
||||
return (
|
||||
MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_media_player_entity")
|
||||
async def setup_media_player_platform_test_entity(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
supported_features: MediaPlayerEntityFeature,
|
||||
) -> MockMediaPlayer:
|
||||
"""Set up media player entity using an entity platform."""
|
||||
|
||||
async def async_setup_entry_init(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> bool:
|
||||
"""Set up test config entry."""
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
config_entry, [Platform.MEDIA_PLAYER]
|
||||
)
|
||||
return True
|
||||
|
||||
mock_integration(
|
||||
hass,
|
||||
MockModule(
|
||||
TEST_DOMAIN,
|
||||
async_setup_entry=async_setup_entry_init,
|
||||
),
|
||||
)
|
||||
|
||||
entity = MockMediaPlayer(supported_features=supported_features)
|
||||
|
||||
async def async_setup_entry_platform(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up test media player platform via config entry."""
|
||||
async_add_entities([entity])
|
||||
|
||||
mock_platform(
|
||||
hass,
|
||||
f"{TEST_DOMAIN}.{DOMAIN}",
|
||||
MockPlatform(async_setup_entry=async_setup_entry_platform),
|
||||
)
|
||||
|
||||
config_entry = MockConfigEntry(domain=TEST_DOMAIN)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity.entity_id)
|
||||
assert state is not None
|
||||
|
||||
return entity
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_media_player_custom_vol_entity")
|
||||
async def setup_media_player_custom_vol_platform_test_entity(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
supported_features: MediaPlayerEntityFeature,
|
||||
) -> MockMediaPlayerVolumeUpDown:
|
||||
"""Set up media player entity with custom volume methods."""
|
||||
|
||||
async def async_setup_entry_init(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> bool:
|
||||
"""Set up test config entry."""
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
config_entry, [Platform.MEDIA_PLAYER]
|
||||
)
|
||||
return True
|
||||
|
||||
mock_integration(
|
||||
hass,
|
||||
MockModule(
|
||||
TEST_DOMAIN,
|
||||
async_setup_entry=async_setup_entry_init,
|
||||
),
|
||||
)
|
||||
|
||||
entity = MockMediaPlayerVolumeUpDown(supported_features=supported_features)
|
||||
|
||||
async def async_setup_entry_platform(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up test media player platform via config entry."""
|
||||
async_add_entities([entity])
|
||||
|
||||
mock_platform(
|
||||
hass,
|
||||
f"{TEST_DOMAIN}.{DOMAIN}",
|
||||
MockPlatform(async_setup_entry=async_setup_entry_platform),
|
||||
)
|
||||
|
||||
config_entry = MockConfigEntry(domain=TEST_DOMAIN)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity.entity_id)
|
||||
assert state is not None
|
||||
|
||||
return entity
|
||||
@@ -12,6 +12,7 @@ from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_CONTENT_TYPE,
|
||||
ATTR_MEDIA_FILTER_CLASSES,
|
||||
ATTR_MEDIA_SEARCH_QUERY,
|
||||
ATTR_MEDIA_VOLUME_LEVEL,
|
||||
BrowseMedia,
|
||||
MediaClass,
|
||||
MediaPlayerEnqueue,
|
||||
@@ -20,14 +21,25 @@ from homeassistant.components.media_player import (
|
||||
SearchMediaQuery,
|
||||
)
|
||||
from homeassistant.components.media_player.const import (
|
||||
CONF_MAX_VOLUME,
|
||||
DOMAIN,
|
||||
SERVICE_BROWSE_MEDIA,
|
||||
SERVICE_SEARCH_MEDIA,
|
||||
)
|
||||
from homeassistant.components.websocket_api import TYPE_RESULT
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_VOLUME_DOWN,
|
||||
SERVICE_VOLUME_SET,
|
||||
SERVICE_VOLUME_UP,
|
||||
STATE_OFF,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .conftest import MockMediaPlayer, MockMediaPlayerVolumeUpDown
|
||||
|
||||
from tests.common import MockEntityPlatform
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
||||
@@ -634,3 +646,444 @@ async def test_play_media_via_selector(hass: HomeAssistant) -> None:
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_max_volume_option_default(
|
||||
hass: HomeAssistant,
|
||||
mock_media_player_entity: MockMediaPlayer,
|
||||
) -> None:
|
||||
"""Test that max_volume option is None by default."""
|
||||
assert mock_media_player_entity._media_player_option_max_volume is None
|
||||
|
||||
|
||||
async def test_max_volume_option_set(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_media_player_entity: MockMediaPlayer,
|
||||
) -> None:
|
||||
"""Test that max_volume option is read from the entity registry."""
|
||||
entity_registry.async_update_entity_options(
|
||||
mock_media_player_entity.entity_id,
|
||||
DOMAIN,
|
||||
{CONF_MAX_VOLUME: 0.5},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_media_player_entity._media_player_option_max_volume == 0.5
|
||||
|
||||
|
||||
async def test_max_volume_option_cleared(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_media_player_entity: MockMediaPlayer,
|
||||
) -> None:
|
||||
"""Test that max_volume option can be cleared."""
|
||||
entity_registry.async_update_entity_options(
|
||||
mock_media_player_entity.entity_id,
|
||||
DOMAIN,
|
||||
{CONF_MAX_VOLUME: 0.5},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_media_player_entity._media_player_option_max_volume == 0.5
|
||||
|
||||
entity_registry.async_update_entity_options(
|
||||
mock_media_player_entity.entity_id,
|
||||
DOMAIN,
|
||||
None,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_media_player_entity._media_player_option_max_volume is None
|
||||
|
||||
|
||||
async def test_volume_set_rescaled_by_max_volume(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_media_player_entity: MockMediaPlayer,
|
||||
) -> None:
|
||||
"""Test that volume_set service rescales to max_volume.
|
||||
|
||||
Setting 80% with max_volume=0.5 should send 0.4 to the device (80% of 0.5).
|
||||
"""
|
||||
entity_registry.async_update_entity_options(
|
||||
mock_media_player_entity.entity_id,
|
||||
DOMAIN,
|
||||
{CONF_MAX_VOLUME: 0.5},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_SET,
|
||||
{
|
||||
ATTR_ENTITY_ID: mock_media_player_entity.entity_id,
|
||||
ATTR_MEDIA_VOLUME_LEVEL: 0.8,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Device receives 0.8 * 0.5 = 0.4
|
||||
assert mock_media_player_entity.volume_level == pytest.approx(0.4)
|
||||
|
||||
|
||||
async def test_volume_set_half_with_max_volume(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_media_player_entity: MockMediaPlayer,
|
||||
) -> None:
|
||||
"""Test that setting 50% with max_volume=0.6 sends 0.3 to the device."""
|
||||
entity_registry.async_update_entity_options(
|
||||
mock_media_player_entity.entity_id,
|
||||
DOMAIN,
|
||||
{CONF_MAX_VOLUME: 0.6},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_SET,
|
||||
{
|
||||
ATTR_ENTITY_ID: mock_media_player_entity.entity_id,
|
||||
ATTR_MEDIA_VOLUME_LEVEL: 0.5,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Device receives 0.5 * 0.6 = 0.3
|
||||
assert mock_media_player_entity.volume_level == pytest.approx(0.3)
|
||||
|
||||
|
||||
async def test_volume_set_full_with_max_volume(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_media_player_entity: MockMediaPlayer,
|
||||
) -> None:
|
||||
"""Test that setting 100% with max_volume sends max_volume to device."""
|
||||
entity_registry.async_update_entity_options(
|
||||
mock_media_player_entity.entity_id,
|
||||
DOMAIN,
|
||||
{CONF_MAX_VOLUME: 0.6},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_SET,
|
||||
{
|
||||
ATTR_ENTITY_ID: mock_media_player_entity.entity_id,
|
||||
ATTR_MEDIA_VOLUME_LEVEL: 1.0,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Device receives 1.0 * 0.6 = 0.6
|
||||
assert mock_media_player_entity.volume_level == pytest.approx(0.6)
|
||||
|
||||
|
||||
async def test_volume_set_without_max_volume(
|
||||
hass: HomeAssistant,
|
||||
mock_media_player_entity: MockMediaPlayer,
|
||||
) -> None:
|
||||
"""Test that volume_set without max_volume passes through unchanged."""
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_SET,
|
||||
{
|
||||
ATTR_ENTITY_ID: mock_media_player_entity.entity_id,
|
||||
ATTR_MEDIA_VOLUME_LEVEL: 0.9,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_media_player_entity.volume_level == 0.9
|
||||
|
||||
|
||||
async def test_volume_state_rescaled_by_max_volume(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_media_player_entity: MockMediaPlayer,
|
||||
) -> None:
|
||||
"""Test that volume_level in state is rescaled back to 0..1 range.
|
||||
|
||||
Device at 0.3 with max_volume=0.6 should report 0.5 (50%) in the state.
|
||||
"""
|
||||
entity_registry.async_update_entity_options(
|
||||
mock_media_player_entity.entity_id,
|
||||
DOMAIN,
|
||||
{CONF_MAX_VOLUME: 0.6},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Set 50% → device gets 0.3
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_SET,
|
||||
{
|
||||
ATTR_ENTITY_ID: mock_media_player_entity.entity_id,
|
||||
ATTR_MEDIA_VOLUME_LEVEL: 0.5,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Raw device value is 0.3
|
||||
assert mock_media_player_entity.volume_level == pytest.approx(0.3)
|
||||
|
||||
# Trigger a state write so the state attributes are updated
|
||||
mock_media_player_entity.async_write_ha_state()
|
||||
state = hass.states.get(mock_media_player_entity.entity_id)
|
||||
assert state is not None
|
||||
# State should report rescaled value: 0.3 / 0.6 = 0.5
|
||||
assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == pytest.approx(0.5)
|
||||
|
||||
|
||||
async def test_volume_up_rescaled_with_max_volume(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_media_player_entity: MockMediaPlayer,
|
||||
) -> None:
|
||||
"""Test that volume_up uses rescaled step with max_volume.
|
||||
|
||||
With max_volume=0.5 and step=0.1, a volume_up should add
|
||||
0.1 * 0.5 = 0.05 to the device volume.
|
||||
"""
|
||||
entity_registry.async_update_entity_options(
|
||||
mock_media_player_entity.entity_id,
|
||||
DOMAIN,
|
||||
{CONF_MAX_VOLUME: 0.5},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Set volume to 80% → device gets 0.8 * 0.5 = 0.4
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_SET,
|
||||
{
|
||||
ATTR_ENTITY_ID: mock_media_player_entity.entity_id,
|
||||
ATTR_MEDIA_VOLUME_LEVEL: 0.8,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_media_player_entity.volume_level == pytest.approx(0.4)
|
||||
|
||||
# Volume up: step in device space = 0.1 * 0.5 = 0.05
|
||||
# New device volume = 0.4 + 0.05 = 0.45
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_UP,
|
||||
{ATTR_ENTITY_ID: mock_media_player_entity.entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_media_player_entity.volume_level == pytest.approx(0.45)
|
||||
|
||||
|
||||
async def test_volume_up_does_not_exceed_max_volume(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_media_player_entity: MockMediaPlayer,
|
||||
) -> None:
|
||||
"""Test that volume_up does nothing when already at max_volume."""
|
||||
entity_registry.async_update_entity_options(
|
||||
mock_media_player_entity.entity_id,
|
||||
DOMAIN,
|
||||
{CONF_MAX_VOLUME: 0.5},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Set volume to 100% → device gets 1.0 * 0.5 = 0.5
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_SET,
|
||||
{
|
||||
ATTR_ENTITY_ID: mock_media_player_entity.entity_id,
|
||||
ATTR_MEDIA_VOLUME_LEVEL: 1.0,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_media_player_entity.volume_level == pytest.approx(0.5)
|
||||
|
||||
# Volume up should not go above max_volume
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_UP,
|
||||
{ATTR_ENTITY_ID: mock_media_player_entity.entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_media_player_entity.volume_level == pytest.approx(0.5)
|
||||
|
||||
|
||||
async def test_volume_down_rescaled_with_max_volume(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_media_player_entity: MockMediaPlayer,
|
||||
) -> None:
|
||||
"""Test that volume_down uses rescaled step with max_volume."""
|
||||
entity_registry.async_update_entity_options(
|
||||
mock_media_player_entity.entity_id,
|
||||
DOMAIN,
|
||||
{CONF_MAX_VOLUME: 0.5},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Set volume to 100% → device gets 0.5
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_SET,
|
||||
{
|
||||
ATTR_ENTITY_ID: mock_media_player_entity.entity_id,
|
||||
ATTR_MEDIA_VOLUME_LEVEL: 1.0,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_media_player_entity.volume_level == pytest.approx(0.5)
|
||||
|
||||
# Volume down: step in device space = 0.1 * 0.5 = 0.05
|
||||
# New device volume = 0.5 - 0.05 = 0.45
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_DOWN,
|
||||
{ATTR_ENTITY_ID: mock_media_player_entity.entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_media_player_entity.volume_level == pytest.approx(0.45)
|
||||
|
||||
|
||||
async def test_max_volume_overrides_custom_volume_up(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_media_player_custom_vol_entity: MockMediaPlayerVolumeUpDown,
|
||||
) -> None:
|
||||
"""Test that max_volume forces fallback and skips custom volume_up."""
|
||||
entity = mock_media_player_custom_vol_entity
|
||||
|
||||
entity_registry.async_update_entity_options(
|
||||
entity.entity_id,
|
||||
DOMAIN,
|
||||
{CONF_MAX_VOLUME: 0.5},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Set volume to 80% → device gets 0.8 * 0.5 = 0.4
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_SET,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
ATTR_MEDIA_VOLUME_LEVEL: 0.8,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Volume up should use fallback (not custom)
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_UP,
|
||||
{ATTR_ENTITY_ID: entity.entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Custom volume_up should NOT have been called
|
||||
entity.calls_volume_up.assert_not_called()
|
||||
# Device: 0.4 + 0.1 * 0.5 = 0.45
|
||||
assert entity.volume_level == pytest.approx(0.45)
|
||||
|
||||
|
||||
async def test_max_volume_overrides_custom_volume_down(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_media_player_custom_vol_entity: MockMediaPlayerVolumeUpDown,
|
||||
) -> None:
|
||||
"""Test that max_volume forces fallback and skips custom volume_down."""
|
||||
entity = mock_media_player_custom_vol_entity
|
||||
|
||||
entity_registry.async_update_entity_options(
|
||||
entity.entity_id,
|
||||
DOMAIN,
|
||||
{CONF_MAX_VOLUME: 0.5},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Set volume to 80% → device gets 0.8 * 0.5 = 0.4
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_SET,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
ATTR_MEDIA_VOLUME_LEVEL: 0.8,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Volume down should use fallback (not custom)
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_DOWN,
|
||||
{ATTR_ENTITY_ID: entity.entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Custom volume_down should NOT have been called
|
||||
entity.calls_volume_down.assert_not_called()
|
||||
# Device: 0.4 - 0.1 * 0.5 = 0.35
|
||||
assert entity.volume_level == pytest.approx(0.35)
|
||||
|
||||
|
||||
async def test_no_max_volume_uses_custom_volume_up(
|
||||
hass: HomeAssistant,
|
||||
mock_media_player_custom_vol_entity: MockMediaPlayerVolumeUpDown,
|
||||
) -> None:
|
||||
"""Test that without max_volume, custom volume_up is used."""
|
||||
entity = mock_media_player_custom_vol_entity
|
||||
|
||||
# Set volume to 0.5 (no max_volume, so no rescaling)
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_SET,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
ATTR_MEDIA_VOLUME_LEVEL: 0.5,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Volume up should use the custom method
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_UP,
|
||||
{ATTR_ENTITY_ID: entity.entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
entity.calls_volume_up.assert_called_once()
|
||||
assert entity.volume_level == pytest.approx(0.6)
|
||||
|
||||
|
||||
async def test_no_max_volume_uses_custom_volume_down(
|
||||
hass: HomeAssistant,
|
||||
mock_media_player_custom_vol_entity: MockMediaPlayerVolumeUpDown,
|
||||
) -> None:
|
||||
"""Test that without max_volume, custom volume_down is used."""
|
||||
entity = mock_media_player_custom_vol_entity
|
||||
|
||||
# Set volume to 0.5 (no max_volume, so no rescaling)
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_SET,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
ATTR_MEDIA_VOLUME_LEVEL: 0.5,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Volume down should use the custom method
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_DOWN,
|
||||
{ATTR_ENTITY_ID: entity.entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
entity.calls_volume_down.assert_called_once()
|
||||
assert entity.volume_level == pytest.approx(0.4)
|
||||
|
||||
Reference in New Issue
Block a user