diff --git a/.core_files.yaml b/.core_files.yaml index 27d51a0ced37a..2a6db0a2943d6 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -91,6 +91,7 @@ components: &components - homeassistant/components/input_number/** - homeassistant/components/input_select/** - homeassistant/components/input_text/** + - homeassistant/components/labs/** - homeassistant/components/logbook/** - homeassistant/components/logger/** - homeassistant/components/lovelace/** diff --git a/homeassistant/components/google_generative_ai_conversation/manifest.json b/homeassistant/components/google_generative_ai_conversation/manifest.json index c23c68982a593..073215bfaf68e 100644 --- a/homeassistant/components/google_generative_ai_conversation/manifest.json +++ b/homeassistant/components/google_generative_ai_conversation/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/google_generative_ai_conversation", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["google-genai==1.56.0"] + "requirements": ["google-genai==1.59.0"] } diff --git a/homeassistant/components/hdfury/button.py b/homeassistant/components/hdfury/button.py index d56864c1f5e43..6b2a292c210f7 100644 --- a/homeassistant/components/hdfury/button.py +++ b/homeassistant/components/hdfury/button.py @@ -19,6 +19,8 @@ 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 93c09362f300b..86e044708c908 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 8a978b1eeb037..02cae0ebd0c72 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 c0849dc5ca9e2..7866b07e0f4f8 100644 --- a/homeassistant/components/hdfury/select.py +++ b/homeassistant/components/hdfury/select.py @@ -20,6 +20,8 @@ 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 4c54511447097..23538c5f0f4dc 100644 --- a/homeassistant/components/hdfury/sensor.py +++ b/homeassistant/components/hdfury/sensor.py @@ -8,6 +8,8 @@ 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 717aa345f02e7..066333b196cf3 100644 --- a/homeassistant/components/hdfury/switch.py +++ b/homeassistant/components/hdfury/switch.py @@ -16,6 +16,8 @@ from .coordinator import HDFuryConfigEntry from .entity import HDFuryEntity +PARALLEL_UPDATES = 1 + @dataclass(kw_only=True, frozen=True) class HDFurySwitchEntityDescription(SwitchEntityDescription): diff --git a/homeassistant/components/proxmoxve/manifest.json b/homeassistant/components/proxmoxve/manifest.json index 220deb07d5f77..35aad8b9b88e8 100644 --- a/homeassistant/components/proxmoxve/manifest.json +++ b/homeassistant/components/proxmoxve/manifest.json @@ -4,6 +4,7 @@ "codeowners": ["@jhollowe", "@Corbeno", "@erwindouna"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/proxmoxve", + "integration_type": "service", "iot_class": "local_polling", "loggers": ["proxmoxer"], "quality_scale": "legacy", diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index b85baa1a6307d..dcaacde9ba9f5 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from typing import Final, cast -from aioshelly.const import RPC_GENERATIONS +from aioshelly.const import MODEL_FLOOD_G4, RPC_GENERATIONS from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR_PLATFORM, @@ -335,6 +335,7 @@ def __init__( device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, supported=lambda status: status.get("alarm") is not None, + models={MODEL_FLOOD_G4}, ), "presence_num_objects": RpcBinarySensorDescription( key="presence", diff --git a/homeassistant/components/xbox/media_player.py b/homeassistant/components/xbox/media_player.py index 36cb73aff5461..66379303bc717 100644 --- a/homeassistant/components/xbox/media_player.py +++ b/homeassistant/components/xbox/media_player.py @@ -157,6 +157,8 @@ async def async_mute_volume(self, mute: bool) -> None: await self.client.smartglass.mute(self._console.id) else: await self.client.smartglass.unmute(self._console.id) + self._attr_is_volume_muted = mute + self.async_write_ha_state() async def async_volume_up(self) -> None: """Turn volume up for media player.""" diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 513c2c72994cf..306fd03662e19 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -25,5 +25,5 @@ "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "integration_type": "device", "iot_class": "local_push", - "requirements": ["xiaomi-ble==1.4.1"] + "requirements": ["xiaomi-ble==1.4.3"] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index a3b2d57ede7aa..2a03d18814727 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5245,7 +5245,7 @@ }, "proxmoxve": { "name": "Proxmox VE", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "local_polling" }, diff --git a/requirements_all.txt b/requirements_all.txt index b8806f54fc46f..2c1b8e34dcb2b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1092,7 +1092,7 @@ google-cloud-speech==2.31.1 google-cloud-texttospeech==2.25.1 # homeassistant.components.google_generative_ai_conversation -google-genai==1.56.0 +google-genai==1.59.0 # homeassistant.components.google_travel_time google-maps-routing==0.6.15 @@ -3215,7 +3215,7 @@ wsdot==0.0.1 wyoming==1.7.2 # homeassistant.components.xiaomi_ble -xiaomi-ble==1.4.1 +xiaomi-ble==1.4.3 # homeassistant.components.knx xknx==3.14.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b17d48e5551e4..f6f6c11972ba3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -968,7 +968,7 @@ google-cloud-speech==2.31.1 google-cloud-texttospeech==2.25.1 # homeassistant.components.google_generative_ai_conversation -google-genai==1.56.0 +google-genai==1.59.0 # homeassistant.components.google_travel_time google-maps-routing==0.6.15 @@ -2691,7 +2691,7 @@ wsdot==0.0.1 wyoming==1.7.2 # homeassistant.components.xiaomi_ble -xiaomi-ble==1.4.1 +xiaomi-ble==1.4.3 # homeassistant.components.knx xknx==3.14.0 diff --git a/tests/components/hdfury/test_button.py b/tests/components/hdfury/test_button.py index 422e73b0d36ee..0845e1832241f 100644 --- a/tests/components/hdfury/test_button.py +++ b/tests/components/hdfury/test_button.py @@ -6,7 +6,8 @@ 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 aabce4c979835..778511d137595 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 eaf9aa42eade2..96579d635b977 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 + ) diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index c79da9d5427fe..9ce7ecd77df80 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -3,7 +3,12 @@ from copy import deepcopy from unittest.mock import Mock -from aioshelly.const import MODEL_BLU_GATEWAY_G3, MODEL_MOTION, MODEL_PLUS_SMOKE +from aioshelly.const import ( + MODEL_BLU_GATEWAY_G3, + MODEL_FLOOD_G4, + MODEL_MOTION, + MODEL_PLUS_SMOKE, +) from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion @@ -612,7 +617,7 @@ async def test_rpc_flood_entities( snapshot: SnapshotAssertion, ) -> None: """Test RPC flood sensor entities.""" - await init_integration(hass, 4) + await init_integration(hass, 4, model=MODEL_FLOOD_G4) for entity in ("flood", "mute", "cable_unplugged"): entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_kitchen_{entity}" @@ -630,7 +635,7 @@ async def test_rpc_flood_cable_unplugged( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC flood cable unplugged entity.""" - await init_integration(hass, 4) + await init_integration(hass, 4, model=MODEL_FLOOD_G4) entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_kitchen_cable_unplugged"