From 0d20c2f5e569f545dcb32a24fd4d877c9df02ccd Mon Sep 17 00:00:00 2001 From: kernelpanic85 <80057019+kernelpanic85@users.noreply.github.com> Date: Wed, 30 Sep 2026 05:28:59 -0400 Subject: [PATCH] Fix JVC Projector diagnostic entities showing unavailable (#183526) --- .../components/jvc_projector/coordinator.py | 19 +++++++-- tests/components/jvc_projector/test_select.py | 35 +++++++++++++++- tests/components/jvc_projector/test_sensor.py | 42 ++++++++++++++++++- 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/jvc_projector/coordinator.py b/homeassistant/components/jvc_projector/coordinator.py index 616b519ad245..92843d4661ce 100644 --- a/homeassistant/components/jvc_projector/coordinator.py +++ b/homeassistant/components/jvc_projector/coordinator.py @@ -156,7 +156,9 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]): # Command has dependencies so defer until below deferred_commands.append(command) else: - await self._update_command_state(command, new_state) + await self._update_command_state( + command, new_state, ignore_timeout=True + ) # Deferred commands should have had dependencies met above for command in deferred_commands: @@ -167,7 +169,9 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]): elif depend_command in self.state: value = self.state[depend_command] if value and value in depend_values: - await self._update_command_state(command, new_state) + await self._update_command_state( + command, new_state, ignore_timeout=True + ) elif self.state.get(cmd.Signal) != cmd.Signal.NONE: new_state[cmd.Signal] = cmd.Signal.NONE @@ -183,11 +187,20 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]): return new_state async def _update_command_state( - self, command: type[Command], new_state: dict[type[Command], str] + self, + command: type[Command], + new_state: dict[type[Command], str], + *, + ignore_timeout: bool = False, ) -> str | None: """Update state with the current value of a command.""" try: value = await self.device.get(command) + except JvcProjectorTimeoutError: + if not ignore_timeout: + raise + _LOGGER.debug("Command %s timed out; retaining cached value", command.name) + return self.state.get(command) except JvcProjectorCommandError as err: _LOGGER.warning("Command %s failed: %s", command.name, err) cached = self.state.get(command) diff --git a/tests/components/jvc_projector/test_select.py b/tests/components/jvc_projector/test_select.py index a3fd45a41bc0..893b3249095b 100644 --- a/tests/components/jvc_projector/test_select.py +++ b/tests/components/jvc_projector/test_select.py @@ -4,7 +4,8 @@ from datetime import timedelta from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory -from jvcprojector import command as cmd +from jvcprojector import JvcProjectorTimeoutError, command as cmd +import pytest from homeassistant.components.jvc_projector.coordinator import INTERVAL_FAST from homeassistant.components.select import ( @@ -12,7 +13,12 @@ from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_OPTION +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_OPTION, + STATE_UNKNOWN, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -21,6 +27,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed INPUT_ENTITY_ID = "select.jvc_projector_input" HDR_PROCESSING_ENTITY_ID = "select.jvc_projector_hdr_processing" HDR_SENSOR_ENTITY_ID = "sensor.jvc_projector_hdr" +MOTION_ENHANCE_ENTITY_ID = "select.jvc_projector_motion_enhance" async def test_input_select( @@ -47,6 +54,30 @@ async def test_input_select( mock_device.set.assert_called_once_with(cmd.Input, cmd.Input.HDMI2) +@pytest.mark.parametrize( + "mock_device", + [{"fixture_override": {cmd.MotionEnhance: JvcProjectorTimeoutError}}], + indirect=True, +) +async def test_motion_enhance_timeout_is_unknown( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test a motion enhance timeout does not make the select unavailable.""" + entity_registry.async_update_entity(MOTION_ENHANCE_ENTITY_ID, disabled_by=None) + + await hass.config_entries.async_reload(mock_integration.entry_id) + await hass.async_block_till_done() + await mock_integration.runtime_data.async_refresh() + await hass.async_block_till_done() + + state = hass.states.get(MOTION_ENHANCE_ENTITY_ID) + assert state is not None + assert state.state == STATE_UNKNOWN + + async def test_enable_hdr_processing_select( hass: HomeAssistant, entity_registry: er.EntityRegistry, diff --git a/tests/components/jvc_projector/test_sensor.py b/tests/components/jvc_projector/test_sensor.py index 5fe304198297..a1fbd82a0dd5 100644 --- a/tests/components/jvc_projector/test_sensor.py +++ b/tests/components/jvc_projector/test_sensor.py @@ -2,9 +2,10 @@ from unittest.mock import MagicMock -from jvcprojector import Command, command as cmd +from jvcprojector import Command, JvcProjectorTimeoutError, command as cmd import pytest +from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -46,6 +47,45 @@ async def test_diagnostic_sensor_state( assert state.state == expected_state +@pytest.mark.parametrize( + "mock_device", + [ + { + "fixture_override": { + cmd.Source: JvcProjectorTimeoutError, + cmd.Colorimetry: JvcProjectorTimeoutError, + cmd.LinkRate: JvcProjectorTimeoutError, + } + } + ], + indirect=True, +) +async def test_diagnostic_sensor_timeout_is_unknown( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test optional diagnostic sensor timeouts do not make entities unavailable.""" + entity_ids = ( + "sensor.jvc_projector_resolution", + "sensor.jvc_projector_colorimetry", + "sensor.jvc_projector_link_rate", + ) + for entity_id in entity_ids: + entity_registry.async_update_entity(entity_id, disabled_by=None) + + await hass.config_entries.async_reload(mock_integration.entry_id) + await hass.async_block_till_done() + await mock_integration.runtime_data.async_refresh() + await hass.async_block_till_done() + + for entity_id in entity_ids: + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_UNKNOWN + + async def test_entity_state( hass: HomeAssistant, entity_registry: er.EntityRegistry,