mirror of
https://github.com/home-assistant/core.git
synced 2026-09-10 07:31:41 +01:00
Report Vizio volume and mute when absent from the audio settings collection (#179433)
This commit is contained in:
@@ -32,7 +32,13 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, VIZIO_AUDIO_SETTINGS, VIZIO_SOUND_MODE
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
VIZIO_AUDIO_SETTINGS,
|
||||
VIZIO_MUTE,
|
||||
VIZIO_SOUND_MODE,
|
||||
VIZIO_VOLUME,
|
||||
)
|
||||
|
||||
type VizioConfigEntry = ConfigEntry[VizioRuntimeData]
|
||||
|
||||
@@ -96,6 +102,11 @@ class VizioDeviceData:
|
||||
# Audio settings from get_settings("audio")
|
||||
audio_settings: dict[str, SettingInfo] | None = None
|
||||
|
||||
# Volume and mute, read individually when the audio settings
|
||||
# collection does not carry them.
|
||||
volume: int | None = None
|
||||
is_muted: bool | None = None
|
||||
|
||||
# Sound mode options from get_setting("audio", "eq")
|
||||
sound_mode_list: list[str] | None = None
|
||||
|
||||
@@ -193,6 +204,17 @@ class VizioDeviceCoordinator(DataUpdateCoordinator[VizioDeviceData]):
|
||||
# Device is on - fetch all data
|
||||
audio_settings = await _optional(self.device.get_settings(VIZIO_AUDIO_SETTINGS))
|
||||
|
||||
# Some firmware omits volume and mute from the audio settings
|
||||
# collection even though the individual settings still work, and
|
||||
# there is no way to tell in advance. Read them directly only when
|
||||
# they are missing, so unaffected devices cost nothing extra.
|
||||
volume: int | None = None
|
||||
is_muted: bool | None = None
|
||||
if audio_settings is None or VIZIO_VOLUME not in audio_settings:
|
||||
volume = await _optional(self.device.get_volume())
|
||||
if audio_settings is None or VIZIO_MUTE not in audio_settings:
|
||||
is_muted = await _optional(self.device.is_muted())
|
||||
|
||||
sound_mode_list = None
|
||||
if audio_settings and VIZIO_SOUND_MODE in audio_settings:
|
||||
sound_mode = await _optional(
|
||||
@@ -231,6 +253,8 @@ class VizioDeviceCoordinator(DataUpdateCoordinator[VizioDeviceData]):
|
||||
return VizioDeviceData(
|
||||
is_on=True,
|
||||
audio_settings=audio_settings,
|
||||
volume=volume,
|
||||
is_muted=is_muted,
|
||||
sound_mode_list=sound_mode_list,
|
||||
current_input=current_input,
|
||||
input_list=input_list,
|
||||
|
||||
@@ -152,20 +152,26 @@ class VizioDevice(VizioEntity, MediaPlayerEntity):
|
||||
# Device is on - apply coordinator data
|
||||
self._attr_state = MediaPlayerState.ON
|
||||
|
||||
# Audio settings
|
||||
# Audio settings. Volume and mute come from the collection when it
|
||||
# carries them, and from the coordinator's individual reads when it
|
||||
# does not - some firmware omits them from the collection.
|
||||
volume: int | None
|
||||
if data.audio_settings and VIZIO_VOLUME in data.audio_settings:
|
||||
volume = int(data.audio_settings[VIZIO_VOLUME].value)
|
||||
else:
|
||||
volume = data.volume
|
||||
self._attr_volume_level = (
|
||||
None if volume is None else float(volume) / self._max_volume
|
||||
)
|
||||
|
||||
if data.audio_settings and VIZIO_MUTE in data.audio_settings:
|
||||
self._attr_is_volume_muted = (
|
||||
str(data.audio_settings[VIZIO_MUTE].value).lower() == VIZIO_MUTE_ON
|
||||
)
|
||||
else:
|
||||
self._attr_is_volume_muted = data.is_muted
|
||||
|
||||
if data.audio_settings:
|
||||
if VIZIO_VOLUME in data.audio_settings:
|
||||
self._attr_volume_level = (
|
||||
float(data.audio_settings[VIZIO_VOLUME].value) / self._max_volume
|
||||
)
|
||||
else:
|
||||
self._attr_volume_level = None
|
||||
if VIZIO_MUTE in data.audio_settings:
|
||||
self._attr_is_volume_muted = (
|
||||
str(data.audio_settings[VIZIO_MUTE].value).lower() == VIZIO_MUTE_ON
|
||||
)
|
||||
else:
|
||||
self._attr_is_volume_muted = None
|
||||
if VIZIO_SOUND_MODE in data.audio_settings:
|
||||
self._attr_supported_features |= (
|
||||
MediaPlayerEntityFeature.SELECT_SOUND_MODE
|
||||
|
||||
@@ -254,6 +254,14 @@ def vizio_bypass_update_fixture() -> Generator[None]:
|
||||
"homeassistant.components.vizio.Vizio.get_settings",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.vizio.Vizio.get_volume",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.vizio.Vizio.is_muted",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.vizio.Vizio.get_current_input",
|
||||
return_value=None,
|
||||
@@ -397,6 +405,14 @@ def vizio_update_fixture() -> Generator[None]:
|
||||
options=tuple(EQ_LIST),
|
||||
),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.vizio.Vizio.get_volume",
|
||||
return_value=int(SOUNDBAR_PROFILE.max_volume / 2),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.vizio.Vizio.is_muted",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.vizio.Vizio.get_current_input",
|
||||
return_value=CURRENT_INPUT,
|
||||
|
||||
@@ -9,7 +9,7 @@ from unittest.mock import call, patch
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from vizaio import AppConfig, RemoteKey, VizioConnectionError
|
||||
from vizaio import AppConfig, RemoteKey, VizioConnectionError, VizioNotFoundError
|
||||
from vizaio.apps import BUNDLED_APPS, UNKNOWN_APP, is_app_input
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
@@ -170,6 +170,18 @@ async def _cm_for_test_setup_without_apps(
|
||||
"homeassistant.components.vizio.Vizio.get_power_state",
|
||||
return_value=vizio_power_state,
|
||||
),
|
||||
# The coordinator falls back to these when the audio settings
|
||||
# collection omits volume or mute. Default them to unsupported so
|
||||
# a test opting out of a setting really gets no value for it;
|
||||
# tests exercising the fallback patch over these.
|
||||
patch(
|
||||
"homeassistant.components.vizio.Vizio.get_volume",
|
||||
side_effect=VizioNotFoundError("not supported"),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.vizio.Vizio.is_muted",
|
||||
side_effect=VizioNotFoundError("not supported"),
|
||||
),
|
||||
):
|
||||
yield
|
||||
|
||||
@@ -293,12 +305,12 @@ async def _test_service(
|
||||
async def test_tv_without_volume_in_audio_settings(
|
||||
hass: HomeAssistant, mock_tv_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test a TV whose audio settings omit volume.
|
||||
"""Test a TV that exposes volume and mute nowhere at all.
|
||||
|
||||
Some firmware does not list `volume` (or `mute`) in the `audio`
|
||||
settings collection even though the individual settings still work.
|
||||
The entity must still load, with the unavailable attributes reported
|
||||
as None instead of raising on every coordinator update.
|
||||
Some firmware does not list them in the `audio` settings collection;
|
||||
when the individual settings are unavailable too, there is nothing
|
||||
left to read. The entity must still load with both attributes unset,
|
||||
rather than raising on every coordinator update.
|
||||
"""
|
||||
async with _cm_for_test_setup_without_apps({"eq": CURRENT_EQ}, True):
|
||||
await setup_integration(hass, mock_tv_config_entry)
|
||||
@@ -310,6 +322,73 @@ async def test_tv_without_volume_in_audio_settings(
|
||||
assert attr[ATTR_SOUND_MODE] == CURRENT_EQ
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
|
||||
async def test_tv_volume_and_mute_read_individually(
|
||||
hass: HomeAssistant, mock_tv_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test a TV whose audio settings omit volume and mute.
|
||||
|
||||
Some firmware does not list them in the `audio` collection even
|
||||
though the individual settings still work, so the coordinator reads
|
||||
them directly and the entity still reports both.
|
||||
"""
|
||||
volume = int(MAX_VOLUME[MediaPlayerDeviceClass.TV] / 2)
|
||||
async with _cm_for_test_setup_without_apps({"eq": CURRENT_EQ}, True):
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.vizio.Vizio.get_volume",
|
||||
return_value=volume,
|
||||
) as get_volume,
|
||||
patch(
|
||||
"homeassistant.components.vizio.Vizio.is_muted",
|
||||
return_value=True,
|
||||
) as is_muted,
|
||||
):
|
||||
await setup_integration(hass, mock_tv_config_entry)
|
||||
|
||||
attr = _get_attr_and_assert_base_attr(
|
||||
hass, MediaPlayerDeviceClass.TV, STATE_ON
|
||||
)
|
||||
assert (
|
||||
attr["volume_level"]
|
||||
== float(volume) / MAX_VOLUME[MediaPlayerDeviceClass.TV]
|
||||
)
|
||||
assert attr["is_volume_muted"] is True
|
||||
assert get_volume.called
|
||||
assert is_muted.called
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
|
||||
async def test_tv_volume_and_mute_not_read_when_present(
|
||||
hass: HomeAssistant, mock_tv_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test that the individual reads are skipped when not needed.
|
||||
|
||||
Devices that do list volume and mute in the collection must not pay
|
||||
an extra round trip for each on every coordinator update.
|
||||
"""
|
||||
volume = int(MAX_VOLUME[MediaPlayerDeviceClass.TV] / 2)
|
||||
async with _cm_for_test_setup_without_apps(
|
||||
{"volume": volume, "mute": "Off", "eq": CURRENT_EQ}, True
|
||||
):
|
||||
with (
|
||||
patch("homeassistant.components.vizio.Vizio.get_volume") as get_volume,
|
||||
patch("homeassistant.components.vizio.Vizio.is_muted") as is_muted,
|
||||
):
|
||||
await setup_integration(hass, mock_tv_config_entry)
|
||||
|
||||
attr = _get_attr_and_assert_base_attr(
|
||||
hass, MediaPlayerDeviceClass.TV, STATE_ON
|
||||
)
|
||||
assert (
|
||||
attr["volume_level"]
|
||||
== float(volume) / MAX_VOLUME[MediaPlayerDeviceClass.TV]
|
||||
)
|
||||
assert attr["is_volume_muted"] is False
|
||||
assert not get_volume.called
|
||||
assert not is_muted.called
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
|
||||
async def test_speaker_on(
|
||||
hass: HomeAssistant, mock_speaker_config_entry: MockConfigEntry
|
||||
|
||||
Reference in New Issue
Block a user