Report Vizio volume and mute when absent from the audio settings collection (#179433)

This commit is contained in:
Raman Gupta
2026-08-24 21:55:51 +02:00
committed by GitHub
parent 0b07a0e1aa
commit 4e24e2038d
4 changed files with 145 additions and 20 deletions
+25 -1
View File
@@ -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,
+19 -13
View File
@@ -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
+16
View File
@@ -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,
+85 -6
View File
@@ -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