diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index b1c8adc9f49b..c65e96298e01 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -43,7 +43,6 @@ PLATFORMS = [ Platform.BUTTON, Platform.CLIMATE, Platform.FAN, - Platform.IMAGE, Platform.LIGHT, Platform.NUMBER, Platform.SELECT, diff --git a/homeassistant/components/home_connect/common.py b/homeassistant/components/home_connect/common.py index 4cd0f6734240..e0f26a54a0ec 100644 --- a/homeassistant/components/home_connect/common.py +++ b/homeassistant/components/home_connect/common.py @@ -1,7 +1,7 @@ """Common callbacks for all Home Connect platforms.""" from collections import defaultdict -from collections.abc import Callable, Sequence +from collections.abc import Callable from functools import partial from typing import cast @@ -10,7 +10,7 @@ from aiohomeconnect.model import EventKey from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity import Entity, EntityDescription +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN @@ -48,7 +48,7 @@ def _create_option_entities( known_entity_unique_ids: dict[str, str], get_option_entities_for_appliance: Callable[ [HomeConnectApplianceCoordinator, er.EntityRegistry], - Sequence[HomeConnectEntity], + list[HomeConnectEntity], ], async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: @@ -74,11 +74,11 @@ def _handle_paired_or_connected_appliance( entry: HomeConnectConfigEntry, known_entity_unique_ids: dict[str, str], get_entities_for_appliance: Callable[ - [HomeConnectApplianceCoordinator], Sequence[Entity] + [HomeConnectApplianceCoordinator], list[HomeConnectEntity] ], get_option_entities_for_appliance: Callable[ [HomeConnectApplianceCoordinator, er.EntityRegistry], - Sequence[HomeConnectEntity], + list[HomeConnectEntity], ] | None, changed_options_listener_remove_callbacks: dict[str, list[Callable[[], None]]], @@ -91,7 +91,7 @@ def _handle_paired_or_connected_appliance( when they are turned off, so we need to check if the entities have been added already or it is the first time we see them when the appliance is connected. """ - entities: list[Entity] = [] + entities: list[HomeConnectEntity] = [] entity_registry = er.async_get(hass) for appliance_coordinator in entry.runtime_data.appliance_coordinators.values(): appliance_ha_id = appliance_coordinator.data.info.ha_id @@ -161,12 +161,12 @@ def setup_home_connect_entry( hass: HomeAssistant, entry: HomeConnectConfigEntry, get_entities_for_appliance: Callable[ - [HomeConnectApplianceCoordinator], Sequence[Entity] + [HomeConnectApplianceCoordinator], list[HomeConnectEntity] ], async_add_entities: AddConfigEntryEntitiesCallback, get_option_entities_for_appliance: Callable[ [HomeConnectApplianceCoordinator, er.EntityRegistry], - Sequence[HomeConnectEntity], + list[HomeConnectEntity], ] | None = None, ) -> None: diff --git a/homeassistant/components/home_connect/config_flow.py b/homeassistant/components/home_connect/config_flow.py index 4c852654ebf2..897416b156b6 100644 --- a/homeassistant/components/home_connect/config_flow.py +++ b/homeassistant/components/home_connect/config_flow.py @@ -2,7 +2,7 @@ from collections.abc import Mapping import logging -from typing import Any, Final, override +from typing import Any, override import jwt import voluptuous as vol @@ -13,8 +13,6 @@ from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .const import DOMAIN -INPUT_IMAGES_SCOPE: Final = "images_scope" - class OAuth2FlowHandler( config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN @@ -25,49 +23,12 @@ class OAuth2FlowHandler( MINOR_VERSION = 3 - images_scope: bool | None = None - @property @override def logger(self) -> logging.Logger: """Return logger.""" return logging.getLogger(__name__) - @property - @override - def extra_authorize_data(self) -> dict[str, str]: - return { - "scope": ( - "Control Monitor Settings" - f" IdentifyAppliance{' Images' if self.images_scope else ''}" - ), - } - - @override - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle a flow start.""" - return await self.async_step_scopes(user_input) - - async def async_step_scopes( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Ask for the scopes to use.""" - if user_input is not None: - self.images_scope = user_input[INPUT_IMAGES_SCOPE] - if self.images_scope is not None: - return await self.async_step_pick_implementation(None) - - return self.async_show_form( - step_id="scopes", - data_schema=vol.Schema( - { - vol.Required(INPUT_IMAGES_SCOPE): bool, - } - ), - ) - async def async_step_reauth( self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: diff --git a/homeassistant/components/home_connect/coordinator.py b/homeassistant/components/home_connect/coordinator.py index 496631ba91cd..7e85e2957db7 100644 --- a/homeassistant/components/home_connect/coordinator.py +++ b/homeassistant/components/home_connect/coordinator.py @@ -29,7 +29,6 @@ from aiohomeconnect.model.error import ( TooManyRequestsError, UnauthorizedError, ) -from aiohomeconnect.model.image import Image from aiohomeconnect.model.program import EnumerateProgram, ProgramDefinitionOption from homeassistant.config_entries import ConfigEntry @@ -66,7 +65,6 @@ class HomeConnectApplianceData: programs: list[EnumerateProgram] settings: dict[SettingKey, GetSetting] status: dict[StatusKey, Status] - images: dict[str, Image] def update(self, other: HomeConnectApplianceData) -> None: """Update data with data from other instance.""" @@ -80,7 +78,6 @@ class HomeConnectApplianceData: self.programs.extend(other.programs) self.settings.update(other.settings) self.status.update(other.status) - self.images.update(other.images) @classmethod def empty(cls, appliance: HomeAppliance) -> HomeConnectApplianceData: @@ -93,7 +90,6 @@ class HomeConnectApplianceData: programs=[], settings={}, status={}, - images={}, ) @@ -272,10 +268,6 @@ class HomeConnectApplianceCoordinator(DataUpdateCoordinator[HomeConnectAppliance self.global_listeners = global_listeners self.data = HomeConnectApplianceData.empty(appliance) self._execution_tracker: list[float] = [] - self.should_fetch_images = "Images" in self._config_entry.data["token"].get( - "scope", "" - ) - self._image_listeners: dict[str, list[CALLBACK_TYPE]] = {} def _get_listeners_for_event_key(self, event_key: EventKey) -> list[CALLBACK_TYPE]: return [ @@ -575,18 +567,6 @@ class HomeConnectApplianceCoordinator(DataUpdateCoordinator[HomeConnectAppliance except HomeConnectError: commands = set() - try: - images = await self.get_latest_images() if self.should_fetch_images else {} - except TooManyRequestsError: - raise - except HomeConnectError as error: - _LOGGER.debug( - "Error fetching images for %s: %s", - appliance.ha_id, - error, - ) - images = {} - self.data.update( HomeConnectApplianceData( commands=commands, @@ -596,7 +576,6 @@ class HomeConnectApplianceCoordinator(DataUpdateCoordinator[HomeConnectAppliance programs=programs, settings=settings, status=status, - images=images, ) ) @@ -667,59 +646,6 @@ class HomeConnectApplianceCoordinator(DataUpdateCoordinator[HomeConnectAppliance for listener in self._get_listeners_for_event_key(EventKey(option_key)): listener() - async def get_latest_images(self) -> dict[str, Image]: - """Get the latest images for the appliance.""" - try: - new_images = await self.client.get_images(self.data.info.ha_id) - except TooManyRequestsError: - raise - except HomeConnectError as error: - _LOGGER.debug( - "Error fetching images for %s: %s", - self.data.info.ha_id, - error, - ) - return {} - - latest_images: dict[str, Image] = {} - for image in new_images.images: - if ( - existing_image := latest_images.get(image.key) - ) is None or image.timestamp > existing_image.timestamp: - latest_images[image.key] = image - - return latest_images - - async def update_images(self) -> None: - """Update images for appliance.""" - old_images = self.data.images.copy() - self.data.images.update(await self.get_latest_images()) - for image_key, image in self.data.images.items(): - if image.image_key != old_images[image_key].image_key: - for listener in self._image_listeners.get(image_key, []): - listener() - - def add_image_listener( - self, image_key: str, update_callback: CALLBACK_TYPE - ) -> Callable[[], None]: - """Listen for image updates. - - These listeners will not be called on refresh. - """ - - @callback - def remove_listener() -> None: - """Remove update listener.""" - self._image_listeners[image_key].remove(update_callback) - if not self._image_listeners[image_key]: - del self._image_listeners[image_key] - - if image_key not in self._image_listeners: - self._image_listeners[image_key] = [] - self._image_listeners[image_key].append(update_callback) - - return remove_listener - def refreshed_too_often_recently(self) -> bool: """Check if the appliance data hasn't been refreshed too often recently.""" diff --git a/homeassistant/components/home_connect/image.py b/homeassistant/components/home_connect/image.py deleted file mode 100644 index 078c681ac158..000000000000 --- a/homeassistant/components/home_connect/image.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Image entity for Home Connect.""" - -from typing import override - -from aiohomeconnect.model.error import HomeConnectError - -from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription -from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.util import dt as dt_util - -from .common import setup_home_connect_entry -from .const import DOMAIN -from .coordinator import HomeConnectApplianceCoordinator, HomeConnectConfigEntry -from .utils import get_dict_from_home_connect_error - -PARALLEL_UPDATES = 1 - -IMAGES = ( - ImageEntityDescription( - key="Refrigeration.Common.EnumType.Compartment.Type.InteriorRightRC", - translation_key="interior_right_camera", - ), - ImageEntityDescription( - key="Refrigeration.Common.EnumType.Compartment.Type.DoorRightRC", - translation_key="door_right_camera", - ), -) - - -def _get_entities_for_appliance( - appliance_coordinator: HomeConnectApplianceCoordinator, -) -> list[HomeConnectImageEntity]: - """Get all entities for an appliance.""" - return [ - HomeConnectImageEntity(appliance_coordinator, desc) - for desc in IMAGES - if desc.key in appliance_coordinator.data.images - ] - - -async def async_setup_entry( - hass: HomeAssistant, - entry: HomeConnectConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Set up the Home Connect sensor.""" - setup_home_connect_entry( - hass, - entry, - _get_entities_for_appliance, - async_add_entities, - ) - - -class HomeConnectImageEntity(ImageEntity): - """Image class for Home Connect.""" - - _attr_has_entity_name = True - _last_image_key_fetched: str | None = None - - def __init__( - self, - appliance_coordinator: HomeConnectApplianceCoordinator, - desc: ImageEntityDescription, - ) -> None: - """Initialize the entity.""" - appliance_ha_id = appliance_coordinator.data.info.ha_id - super().__init__(appliance_coordinator.hass) - self.appliance = appliance_coordinator.data - self.entity_description = desc - self.appliance_coordinator = appliance_coordinator - self._attr_unique_id = f"{appliance_ha_id}-{desc.key}" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, appliance_ha_id)}, - ) - - @override - async def async_added_to_hass(self) -> None: - """When entity is added to hass.""" - await super().async_added_to_hass() - await self.async_fetch_image() - self.async_on_remove( - self.appliance_coordinator.add_image_listener( - self.entity_description.key, self._handle_coordinator_update - ) - ) - - async def async_update(self) -> None: - """Set the value of the image based on the given value.""" - await self.appliance_coordinator.update_images() - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self.hass.async_create_task(self.async_fetch_image()) - - async def async_fetch_image(self) -> None: - """Fetch the image from the Home Connect API if it has changed since the last fetch.""" - image_info = self.appliance_coordinator.data.images[self.entity_description.key] - if image_info.image_key == self._last_image_key_fetched: - return - self._last_image_key_fetched = image_info.image_key - self._attr_image_last_updated = dt_util.utc_from_timestamp( - # It is not specified whether the timestamp is in seconds or milliseconds, - # so we check if it is larger than 10^10 (which would indicate milliseconds) - # and convert it to seconds if necessary. - image_info.timestamp / 1000 - if image_info.timestamp > 10_000_000_000 - else image_info.timestamp - ) - try: - image_data = await self.appliance_coordinator.client.get_image( - self.appliance.info.ha_id, image_key=image_info.image_key - ) - except HomeConnectError as err: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="fetch_image_error", - translation_placeholders=get_dict_from_home_connect_error(err), - ) from err - - self._cached_image = Image( - content_type="image/jpeg", - content=image_data, - ) - self.async_write_ha_state() diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index ff70c5c8afc7..a6d266a9a1ec 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -32,16 +32,6 @@ "reauth_confirm": { "description": "The Home Connect integration needs to re-authenticate your account", "title": "[%key:common::config_flow::title::reauth%]" - }, - "scopes": { - "data": { - "images_scope": "Images scope" - }, - "data_description": { - "images_scope": "Allows Home Assistant to access images from your Home Connect devices." - }, - "description": "Select the optional scopes you want to enable for Home Connect authentication.", - "title": "Scopes" } } }, @@ -152,14 +142,6 @@ } } }, - "image": { - "door_right_camera": { - "name": "Door right camera" - }, - "interior_right_camera": { - "name": "Interior right camera" - } - }, "light": { "ambient_light": { "name": "Ambient light" @@ -1601,9 +1583,6 @@ "fetch_api_error": { "message": "Error obtaining data from the API: {error}" }, - "fetch_image_error": { - "message": "Error obtaining image from the API: {error}" - }, "fetch_program_error": { "message": "Error obtaining the selected or active program: {error}" }, diff --git a/tests/components/home_connect/conftest.py b/tests/components/home_connect/conftest.py index bcbc607fca74..97b71d205adb 100644 --- a/tests/components/home_connect/conftest.py +++ b/tests/components/home_connect/conftest.py @@ -12,7 +12,6 @@ from aiohomeconnect.model import ( ArrayOfCommands, ArrayOfEvents, ArrayOfHomeAppliances, - ArrayOfImages, ArrayOfOptions, ArrayOfPrograms, ArrayOfSettings, @@ -79,7 +78,6 @@ def mock_token_entry(token_expiration_time: float) -> dict[str, Any]: "access_token": FAKE_ACCESS_TOKEN, "type": "Bearer", "expires_at": token_expiration_time, - "scope": "Control Monitor Images Settings IdentifyAppliance", } @@ -123,22 +121,6 @@ def mock_config_entry_v1_2(token_entry: dict[str, Any]) -> MockConfigEntry: ) -@pytest.fixture(name="config_entry_no_images_scope") -def mock_config_entry_no_image_scope(token_entry: dict[str, Any]) -> MockConfigEntry: - """Fixture for a config entry.""" - _token_entry = token_entry.copy() - _token_entry["scope"] = "Control Monitor Settings IdentifyAppliance" - return MockConfigEntry( - domain=DOMAIN, - data={ - "auth_implementation": FAKE_AUTH_IMPL, - "token": _token_entry, - }, - minor_version=3, - unique_id="1234567890", - ) - - @pytest.fixture(autouse=True) async def setup_credentials(hass: HomeAssistant) -> None: """Fixture to setup credentials.""" @@ -442,7 +424,6 @@ def mock_client( mock.get_settings = AsyncMock(side_effect=_get_settings_side_effect) mock.get_setting = AsyncMock(side_effect=_get_setting_side_effect) mock.get_status = AsyncMock(return_value=copy.deepcopy(MOCK_STATUS)) - mock.get_images = AsyncMock(return_value=ArrayOfImages([])) mock.get_all_programs = AsyncMock(side_effect=_get_all_programs_side_effect) mock.get_available_commands = AsyncMock( side_effect=_get_available_commands_side_effect @@ -506,7 +487,6 @@ def mock_client_with_exception( mock.get_settings = AsyncMock(side_effect=exception) mock.get_setting = AsyncMock(side_effect=exception) mock.get_status = AsyncMock(side_effect=exception) - mock.get_images = AsyncMock(side_effect=exception) mock.get_all_programs = AsyncMock(side_effect=exception) mock.get_available_commands = AsyncMock(side_effect=exception) mock.put_command = AsyncMock(side_effect=exception) diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index 1464be1b580c..29afa27bea06 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -3,7 +3,6 @@ from collections.abc import Awaitable, Callable from http import HTTPStatus from unittest.mock import MagicMock, patch -from urllib.parse import parse_qsl, urlsplit from aiohomeconnect.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN from aiohomeconnect.model import HomeAppliance @@ -26,23 +25,6 @@ from tests.typing import ClientSessionGenerator CLIENT_ID = "1234" CLIENT_SECRET = "5678" - -def assert_authorize_url(url: str, state: str, images_scope: bool | None) -> None: - """Assert the generated OAuth authorize URL.""" - split_url = urlsplit(url) - - assert ( - f"{split_url.scheme}://{split_url.netloc}{split_url.path}" == OAUTH2_AUTHORIZE - ) - assert dict(parse_qsl(split_url.query)) == { - "response_type": "code", - "client_id": CLIENT_ID, - "redirect_uri": "https://example.com/auth/external/callback", - "state": state, - "scope": f"Control Monitor Settings IdentifyAppliance{' Images' if images_scope else ''}", - } - - DHCP_DISCOVERY = ( DhcpServiceInfo( ip="1.1.1.1", @@ -113,14 +95,10 @@ DHCP_DISCOVERY = ( @pytest.mark.usefixtures("current_request_with_host") -@pytest.mark.parametrize( - "images_scope", [True, False], ids=["images_scope", "no_images_scope"] -) async def test_full_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - images_scope: bool, ) -> None: """Check full flow.""" assert await setup.async_setup_component(hass, "home_connect", {}) @@ -128,13 +106,6 @@ async def test_full_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context=ConfigFlowContext(source=config_entries.SOURCE_USER) ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "scopes" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"images_scope": images_scope} - ) state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -144,7 +115,11 @@ async def test_full_flow( ) assert result["type"] is FlowResultType.EXTERNAL_STEP - assert_authorize_url(result["url"], state, images_scope) + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") @@ -186,13 +161,6 @@ async def test_prevent_reconfiguring_same_account( result = await hass.config_entries.flow.async_init( DOMAIN, context=ConfigFlowContext(source=config_entries.SOURCE_USER) ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "scopes" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"images_scope": True} - ) state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -202,7 +170,11 @@ async def test_prevent_reconfiguring_same_account( ) assert result["type"] is FlowResultType.EXTERNAL_STEP - assert_authorize_url(result["url"], state, True) + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") @@ -242,13 +214,6 @@ async def test_reauth_flow( assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "scopes" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"images_scope": False} - ) state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -303,13 +268,6 @@ async def test_reauth_flow_with_different_account( assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "scopes" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"images_scope": True} - ) state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -365,13 +323,6 @@ async def test_zeroconf_flow( result["flow_id"], {}, ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "scopes" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"images_scope": True} - ) state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -381,7 +332,11 @@ async def test_zeroconf_flow( ) assert result["type"] is FlowResultType.EXTERNAL_STEP - assert_authorize_url(result["url"], state, True) + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") @@ -451,13 +406,6 @@ async def test_dhcp_flow( result["flow_id"], {}, ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "scopes" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"images_scope": True} - ) state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -466,7 +414,11 @@ async def test_dhcp_flow( }, ) assert result["type"] is FlowResultType.EXTERNAL_STEP - assert_authorize_url(result["url"], state, True) + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") diff --git a/tests/components/home_connect/test_coordinator.py b/tests/components/home_connect/test_coordinator.py index 1ec6c03f0170..e57ab2239290 100644 --- a/tests/components/home_connect/test_coordinator.py +++ b/tests/components/home_connect/test_coordinator.py @@ -77,7 +77,6 @@ INITIAL_FETCH_CLIENT_METHODS = [ "get_all_programs", "get_available_commands", "get_available_program", - "get_images", ] @@ -1100,27 +1099,3 @@ async def test_option_values_kept_after_changing_program( await hass.async_block_till_done() assert hass.states.is_state(entity_id, "on") - - -async def test_images_not_fetched_if_no_images_scope( - hass: HomeAssistant, - client: MagicMock, - config_entry_no_images_scope: MockConfigEntry, - platforms: list[str], -) -> None: - """Test that images are not fetched if the images scope is not granted.""" - config_entry_no_images_scope.add_to_hass(hass) - assert config_entry_no_images_scope.state is ConfigEntryState.NOT_LOADED - with ( - patch("homeassistant.components.home_connect.PLATFORMS", platforms), - patch("homeassistant.components.home_connect.HomeConnectClient") as client_mock, - ): - client_mock.return_value = client - assert await hass.config_entries.async_setup( - config_entry_no_images_scope.entry_id - ) - await hass.async_block_till_done() - - assert config_entry_no_images_scope.state is ConfigEntryState.LOADED - - client.get_images.assert_not_awaited() diff --git a/tests/components/home_connect/test_image.py b/tests/components/home_connect/test_image.py deleted file mode 100644 index 156a9002af66..000000000000 --- a/tests/components/home_connect/test_image.py +++ /dev/null @@ -1,227 +0,0 @@ -"""Tests for home_connect image entities.""" - -from collections.abc import Awaitable, Callable -from unittest.mock import AsyncMock, MagicMock - -from aiohomeconnect.model import ArrayOfEvents, EventMessage, EventType, HomeAppliance -from aiohomeconnect.model.image import ArrayOfImages, Image -import pytest - -from homeassistant.components.home_connect.const import DOMAIN -from homeassistant.components.homeassistant import ( - DOMAIN as HA_DOMAIN, - SERVICE_UPDATE_ENTITY, -) -from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ATTR_ENTITY_ID, EntityStateAttribute, Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.setup import async_setup_component - -from tests.common import MockConfigEntry -from tests.typing import ClientSessionGenerator - - -@pytest.fixture -def platforms() -> list[Platform]: - """Fixture to specify platforms to test.""" - return [Platform.IMAGE] - - -@pytest.fixture(autouse=True) -def images(client: MagicMock) -> list[Image]: - """Fixture to inject and return the default image entities.""" - images = [ - Image( - key="Refrigeration.Common.EnumType.Compartment.Type.InteriorRightRC", - image_key="image_key_1", - preview_image_key="preview_image_key_1", - timestamp=1785974400000, - quality="good", - ), - Image( - key="Refrigeration.Common.EnumType.Compartment.Type.DoorRightRC", - image_key="image_key_2", - preview_image_key="preview_image_key_2", - timestamp=1785974400000, - quality="good", - ), - ] - client.get_images = AsyncMock(return_value=ArrayOfImages(images)) - return images - - -@pytest.mark.parametrize("appliance", ["FridgeFreezer"], indirect=True) -async def test_paired_depaired_devices_flow( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, - client: MagicMock, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - appliance: HomeAppliance, -) -> None: - """Test device removal and re-addition on API events.""" - assert await integration_setup(client) - assert config_entry.state is ConfigEntryState.LOADED - - device = device_registry.async_get_device_by_identifier( - (DOMAIN, appliance.ha_id), config_entry.entry_id - ) - assert device - entity_entries = entity_registry.entities.get_entries_for_device_id(device.id) - assert entity_entries - - await client.add_events( - [ - EventMessage( - appliance.ha_id, - EventType.DEPAIRED, - data=ArrayOfEvents([]), - ) - ] - ) - await hass.async_block_till_done() - - device = device_registry.async_get_device_by_identifier( - (DOMAIN, appliance.ha_id), config_entry.entry_id - ) - assert not device - for entity_entry in entity_entries: - assert not entity_registry.async_get(entity_entry.entity_id) - - # Now that all everything related to the device is removed, pair it again - await client.add_events( - [ - EventMessage( - appliance.ha_id, - EventType.PAIRED, - data=ArrayOfEvents([]), - ) - ] - ) - await hass.async_block_till_done() - - assert device_registry.async_get_device_by_identifier( - (DOMAIN, appliance.ha_id), config_entry.entry_id - ) - for entity_entry in entity_entries: - assert entity_registry.async_get(entity_entry.entity_id) - - -@pytest.mark.parametrize("appliance", ["FridgeFreezer"], indirect=True) -async def test_image_functionality( - hass: HomeAssistant, - client: MagicMock, - hass_client: ClientSessionGenerator, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - appliance: HomeAppliance, -) -> None: - """Test that the image entities use the correct image data.""" - image_data_dict = { - "image_key_1": b"image_data_1", - "image_key_2": b"image_data_2", - } - - async def mock_get_image(_: str, *, image_key: str) -> bytes: - return image_data_dict[image_key] - - client.get_image = AsyncMock(wraps=mock_get_image) - - assert await integration_setup(client) - assert config_entry.state is ConfigEntryState.LOADED - - client.get_images.assert_awaited_once_with(appliance.ha_id) - assert client.get_image.await_count == 2 - for image_key in image_data_dict: - client.get_image.assert_any_call(appliance.ha_id, image_key=image_key) - - _client = await hass_client() - for entity_id, expected_image_data in ( - ("image.fridgefreezer_interior_right_camera", b"image_data_1"), - ("image.fridgefreezer_door_right_camera", b"image_data_2"), - ): - state = hass.states.get(entity_id) - assert state - assert state.state == "2026-08-06T00:00:00+00:00" - - resp = await _client.get(state.attributes[EntityStateAttribute.ENTITY_PICTURE]) - assert resp.status == 200 - assert await resp.read() == expected_image_data - - -@pytest.mark.parametrize("appliance", ["FridgeFreezer"], indirect=True) -async def test_update_image_entity_functionality( - hass: HomeAssistant, - client: MagicMock, - hass_client: ClientSessionGenerator, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - appliance: HomeAppliance, -) -> None: - """Test that the image update correctly. - - Whenever an entity does update, the integration update any other from the same - appliance y there's an update available. - """ - image_data_dict = { - "image_key_1": b"image_data_1", - "image_key_2": b"image_data_2", - "image_key_3": b"image_data_3", - "image_key_4": b"image_data_4", - } - entity_id = "image.fridgefreezer_interior_right_camera" - - async def mock_get_image(_: str, *, image_key: str) -> bytes: - return image_data_dict[image_key] - - client.get_image = AsyncMock(wraps=mock_get_image) - - await async_setup_component(hass, HA_DOMAIN, {}) - assert await integration_setup(client) - assert config_entry.state is ConfigEntryState.LOADED - - client.get_images.assert_awaited_once_with(appliance.ha_id) - client.get_images.reset_mock() - client.get_image.reset_mock() - - client.get_images.return_value.images.extend( - [ - Image( - key="Refrigeration.Common.EnumType.Compartment.Type.InteriorRightRC", - image_key="image_key_3", - preview_image_key="preview_image_key_3", - timestamp=1785978000000, - quality="good", - ), - Image( - key="Refrigeration.Common.EnumType.Compartment.Type.DoorRightRC", - image_key="image_key_4", - preview_image_key="preview_image_key_4", - timestamp=1785978000000, - quality="good", - ), - ] - ) - await hass.services.async_call( - HA_DOMAIN, SERVICE_UPDATE_ENTITY, {ATTR_ENTITY_ID: entity_id}, blocking=True - ) - - client.get_images.assert_awaited_once_with(appliance.ha_id) - assert client.get_image.await_count == 2 - client.get_image.assert_any_await(appliance.ha_id, image_key="image_key_3") - client.get_image.assert_any_await(appliance.ha_id, image_key="image_key_4") - - for entity_id, expected_image_data in ( - ("image.fridgefreezer_interior_right_camera", b"image_data_3"), - ("image.fridgefreezer_door_right_camera", b"image_data_4"), - ): - state = hass.states.get(entity_id) - assert state - assert state.state == "2026-08-06T01:00:00+00:00" - - _client = await hass_client() - resp = await _client.get(state.attributes[EntityStateAttribute.ENTITY_PICTURE]) - assert resp.status == 200 - assert await resp.read() == expected_image_data