From fa29d8180fe1f16eb68f89f0672c513725397ff7 Mon Sep 17 00:00:00 2001 From: Glenn de Haan Date: Sat, 17 Jan 2026 16:57:25 +0100 Subject: [PATCH] Improve quality scale to silver HDFury integration (#161077) --- homeassistant/components/hdfury/button.py | 2 + homeassistant/components/hdfury/manifest.json | 2 +- .../components/hdfury/quality_scale.yaml | 4 +- homeassistant/components/hdfury/select.py | 14 +- homeassistant/components/hdfury/sensor.py | 2 + homeassistant/components/hdfury/switch.py | 2 + tests/components/hdfury/test_button.py | 20 ++- tests/components/hdfury/test_select.py | 145 +++++++++++++++++- tests/components/hdfury/test_switch.py | 55 +++++-- 9 files changed, 214 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/hdfury/button.py b/homeassistant/components/hdfury/button.py index d56864c1f5e..6b2a292c210 100644 --- a/homeassistant/components/hdfury/button.py +++ b/homeassistant/components/hdfury/button.py @@ -19,6 +19,8 @@ from .const import DOMAIN from .coordinator import HDFuryConfigEntry from .entity import HDFuryEntity +PARALLEL_UPDATES = 1 + @dataclass(kw_only=True, frozen=True) class HDFuryButtonEntityDescription(ButtonEntityDescription): diff --git a/homeassistant/components/hdfury/manifest.json b/homeassistant/components/hdfury/manifest.json index 93c09362f30..86e044708c9 100644 --- a/homeassistant/components/hdfury/manifest.json +++ b/homeassistant/components/hdfury/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/hdfury", "integration_type": "device", "iot_class": "local_polling", - "quality_scale": "bronze", + "quality_scale": "silver", "requirements": ["hdfury==1.3.1"] } diff --git a/homeassistant/components/hdfury/quality_scale.yaml b/homeassistant/components/hdfury/quality_scale.yaml index 8a978b1eeb0..02cae0ebd0c 100644 --- a/homeassistant/components/hdfury/quality_scale.yaml +++ b/homeassistant/components/hdfury/quality_scale.yaml @@ -35,11 +35,11 @@ rules: entity-unavailable: done integration-owner: done log-when-unavailable: done - parallel-updates: todo + parallel-updates: done reauthentication-flow: status: exempt comment: Integration has no authentication flow. - test-coverage: todo + test-coverage: done # Gold devices: done diff --git a/homeassistant/components/hdfury/select.py b/homeassistant/components/hdfury/select.py index c0849dc5ca9..7866b07e0f4 100644 --- a/homeassistant/components/hdfury/select.py +++ b/homeassistant/components/hdfury/select.py @@ -20,6 +20,8 @@ from .const import DOMAIN from .coordinator import HDFuryConfigEntry, HDFuryCoordinator from .entity import HDFuryEntity +PARALLEL_UPDATES = 1 + @dataclass(kw_only=True, frozen=True) class HDFurySelectEntityDescription(SelectEntityDescription): @@ -77,13 +79,11 @@ async def async_setup_entry( coordinator = entry.runtime_data - entities: list[HDFuryEntity] = [] - - for description in SELECT_PORTS: - if description.key not in coordinator.data.info: - continue - - entities.append(HDFurySelect(coordinator, description)) + entities: list[HDFuryEntity] = [ + HDFurySelect(coordinator, description) + for description in SELECT_PORTS + if description.key in coordinator.data.info + ] # Add OPMODE select if present if "opmode" in coordinator.data.info: diff --git a/homeassistant/components/hdfury/sensor.py b/homeassistant/components/hdfury/sensor.py index 4c545114470..23538c5f0f4 100644 --- a/homeassistant/components/hdfury/sensor.py +++ b/homeassistant/components/hdfury/sensor.py @@ -8,6 +8,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import HDFuryConfigEntry from .entity import HDFuryEntity +PARALLEL_UPDATES = 0 + SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="RX0", diff --git a/homeassistant/components/hdfury/switch.py b/homeassistant/components/hdfury/switch.py index 717aa345f02..066333b196c 100644 --- a/homeassistant/components/hdfury/switch.py +++ b/homeassistant/components/hdfury/switch.py @@ -16,6 +16,8 @@ from .const import DOMAIN from .coordinator import HDFuryConfigEntry from .entity import HDFuryEntity +PARALLEL_UPDATES = 1 + @dataclass(kw_only=True, frozen=True) class HDFurySwitchEntityDescription(SwitchEntityDescription): diff --git a/tests/components/hdfury/test_button.py b/tests/components/hdfury/test_button.py index 422e73b0d36..0845e183224 100644 --- a/tests/components/hdfury/test_button.py +++ b/tests/components/hdfury/test_button.py @@ -6,7 +6,8 @@ from hdfury import HDFuryError import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.const import Platform +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.entity_registry as er @@ -47,9 +48,9 @@ async def test_button_presses( await setup_integration(hass, mock_config_entry, [Platform.BUTTON]) await hass.services.async_call( - "button", - "press", - {"entity_id": entity_id}, + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) @@ -67,10 +68,13 @@ async def test_button_press_error( await setup_integration(hass, mock_config_entry, [Platform.BUTTON]) - with pytest.raises(HomeAssistantError): + with pytest.raises( + HomeAssistantError, + match="An error occurred while communicating with HDFury device", + ): await hass.services.async_call( - "button", - "press", - {"entity_id": "button.hdfury_vrroom_02_restart"}, + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.hdfury_vrroom_02_restart"}, blocking=True, ) diff --git a/tests/components/hdfury/test_select.py b/tests/components/hdfury/test_select.py index aabce4c9798..778511d1375 100644 --- a/tests/components/hdfury/test_select.py +++ b/tests/components/hdfury/test_select.py @@ -1,14 +1,25 @@ """Tests for the HDFury select platform.""" +from datetime import timedelta +from unittest.mock import AsyncMock + +from freezegun.api import FrozenDateTimeFactory +from hdfury import HDFuryError +import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.const import Platform +from homeassistant.components.select import ( + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform async def test_select_entities( @@ -21,3 +32,133 @@ async def test_select_entities( await setup_integration(hass, mock_config_entry, [Platform.SELECT]) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_select_operation_mode( + hass: HomeAssistant, + mock_hdfury_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test selecting operation mode.""" + + await setup_integration(hass, mock_config_entry, [Platform.SELECT]) + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.hdfury_vrroom_02_operation_mode", + ATTR_OPTION: "1", + }, + blocking=True, + ) + + mock_hdfury_client.set_operation_mode.assert_awaited_once_with("1") + + +@pytest.mark.parametrize( + ("entity_id"), + [ + ("select.hdfury_vrroom_02_port_select_tx0"), + ("select.hdfury_vrroom_02_port_select_tx1"), + ], +) +async def test_select_tx_ports( + hass: HomeAssistant, + mock_hdfury_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_id: str, +) -> None: + """Test selecting TX ports.""" + + await setup_integration(hass, mock_config_entry, [Platform.SELECT]) + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: entity_id, + ATTR_OPTION: "1", + }, + blocking=True, + ) + + mock_hdfury_client.set_port_selection.assert_awaited() + + +async def test_select_operation_mode_error( + hass: HomeAssistant, + mock_hdfury_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test operation mode select raises HomeAssistantError.""" + + mock_hdfury_client.set_operation_mode.side_effect = HDFuryError() + + await setup_integration(hass, mock_config_entry, [Platform.SELECT]) + + with pytest.raises( + HomeAssistantError, + match="An error occurred while communicating with HDFury device", + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.hdfury_vrroom_02_operation_mode", + ATTR_OPTION: "1", + }, + blocking=True, + ) + + +async def test_select_ports_missing_state( + hass: HomeAssistant, + mock_hdfury_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test TX port selection fails when TX state is incomplete.""" + + mock_hdfury_client.get_info.return_value = { + "portseltx0": "0", + "portseltx1": None, + "opmode": "0", + } + + await setup_integration(hass, mock_config_entry, [Platform.SELECT]) + + with pytest.raises( + HomeAssistantError, + match="An error occurred while validating TX states", + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.hdfury_vrroom_02_port_select_tx0", + ATTR_OPTION: "0", + }, + blocking=True, + ) + + +async def test_select_entities_unavailable_on_error( + hass: HomeAssistant, + mock_hdfury_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test API error causes entities to become unavailable.""" + + await setup_integration(hass, mock_config_entry, [Platform.SELECT]) + + mock_hdfury_client.get_info.side_effect = HDFuryError() + + freezer.tick(timedelta(seconds=61)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert ( + hass.states.get("select.hdfury_vrroom_02_port_select_tx0").state + == STATE_UNAVAILABLE + ) diff --git a/tests/components/hdfury/test_switch.py b/tests/components/hdfury/test_switch.py index eaf9aa42ead..96579d635b9 100644 --- a/tests/components/hdfury/test_switch.py +++ b/tests/components/hdfury/test_switch.py @@ -1,19 +1,28 @@ """Tests for the HDFury switch platform.""" +from datetime import timedelta from unittest.mock import AsyncMock +from freezegun.api import FrozenDateTimeFactory from hdfury import HDFuryError import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.const import Platform +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_UNAVAILABLE, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform async def test_switch_entities( @@ -34,15 +43,15 @@ async def test_switch_entities( ( "switch.hdfury_vrroom_02_auto_switch_inputs", "set_auto_switch_inputs", - "turn_on", + SERVICE_TURN_ON, ), ( "switch.hdfury_vrroom_02_auto_switch_inputs", "set_auto_switch_inputs", - "turn_off", + SERVICE_TURN_OFF, ), - ("switch.hdfury_vrroom_02_oled_display", "set_oled", "turn_on"), - ("switch.hdfury_vrroom_02_oled_display", "set_oled", "turn_off"), + ("switch.hdfury_vrroom_02_oled_display", "set_oled", SERVICE_TURN_ON), + ("switch.hdfury_vrroom_02_oled_display", "set_oled", SERVICE_TURN_OFF), ], ) async def test_switch_turn_on_off( @@ -58,9 +67,9 @@ async def test_switch_turn_on_off( await setup_integration(hass, mock_config_entry, [Platform.SWITCH]) await hass.services.async_call( - "switch", + SWITCH_DOMAIN, service, - {"entity_id": entity_id}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) @@ -70,8 +79,8 @@ async def test_switch_turn_on_off( @pytest.mark.parametrize( ("service", "method"), [ - ("turn_on", "set_auto_switch_inputs"), - ("turn_off", "set_auto_switch_inputs"), + (SERVICE_TURN_ON, "set_auto_switch_inputs"), + (SERVICE_TURN_OFF, "set_auto_switch_inputs"), ], ) async def test_switch_turn_error( @@ -92,8 +101,30 @@ async def test_switch_turn_error( match="An error occurred while communicating with HDFury device", ): await hass.services.async_call( - "switch", + SWITCH_DOMAIN, service, - {"entity_id": "switch.hdfury_vrroom_02_auto_switch_inputs"}, + {ATTR_ENTITY_ID: "switch.hdfury_vrroom_02_auto_switch_inputs"}, blocking=True, ) + + +async def test_switch_entities_unavailable_on_error( + hass: HomeAssistant, + mock_hdfury_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test API error causes entities to become unavailable.""" + + await setup_integration(hass, mock_config_entry, [Platform.SWITCH]) + + mock_hdfury_client.get_info.side_effect = HDFuryError() + + freezer.tick(timedelta(seconds=61)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert ( + hass.states.get("switch.hdfury_vrroom_02_auto_switch_inputs").state + == STATE_UNAVAILABLE + )