From 0e23eb9ebd5636c97a80d5e589cf5e407eb86709 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Wed, 10 Sep 2025 23:37:12 +0200 Subject: [PATCH 1/6] =?UTF-8?q?Update=20Sleep=20as=20Android=20quality=20s?= =?UTF-8?q?cale=20to=20platinum=20=F0=9F=8F=86=EF=B8=8F=20=20(#150449)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/sleep_as_android/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sleep_as_android/manifest.json b/homeassistant/components/sleep_as_android/manifest.json index fbac134ffa154a..f2b38d2089b163 100644 --- a/homeassistant/components/sleep_as_android/manifest.json +++ b/homeassistant/components/sleep_as_android/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["webhook"], "documentation": "https://www.home-assistant.io/integrations/sleep_as_android", "iot_class": "local_push", - "quality_scale": "silver" + "quality_scale": "platinum" } From ac154c020c3269dee91d63e9636c656f03997912 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Wed, 10 Sep 2025 14:52:50 -0700 Subject: [PATCH 2/6] Use a state selector for history_stats (#150445) Co-authored-by: Joost Lekkerkerker --- .../components/history_stats/config_flow.py | 94 +++++++++++++------ .../components/history_stats/strings.json | 10 ++ .../history_stats/test_config_flow.py | 39 +++++++- 3 files changed, 111 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/history_stats/config_flow.py b/homeassistant/components/history_stats/config_flow.py index 750180bf3f6b02..e8c3be8aef5668 100644 --- a/homeassistant/components/history_stats/config_flow.py +++ b/homeassistant/components/history_stats/config_flow.py @@ -26,9 +26,10 @@ SelectSelector, SelectSelectorConfig, SelectSelectorMode, + StateSelector, + StateSelectorConfig, TemplateSelector, TextSelector, - TextSelectorConfig, ) from homeassistant.helpers.template import Template @@ -67,7 +68,6 @@ async def validate_options( { vol.Required(CONF_NAME, default=DEFAULT_NAME): TextSelector(), vol.Required(CONF_ENTITY_ID): EntitySelector(), - vol.Required(CONF_STATE): TextSelector(TextSelectorConfig(multiple=True)), vol.Required(CONF_TYPE, default=CONF_TYPE_TIME): SelectSelector( SelectSelectorConfig( options=CONF_TYPE_KEYS, @@ -77,44 +77,78 @@ async def validate_options( ), } ) -DATA_SCHEMA_OPTIONS = vol.Schema( - { - vol.Optional(CONF_ENTITY_ID): EntitySelector( - EntitySelectorConfig(read_only=True) - ), - vol.Optional(CONF_STATE): TextSelector( - TextSelectorConfig(multiple=True, read_only=True) - ), - vol.Optional(CONF_TYPE): SelectSelector( - SelectSelectorConfig( - options=CONF_TYPE_KEYS, - mode=SelectSelectorMode.DROPDOWN, - translation_key=CONF_TYPE, - read_only=True, - ) - ), - vol.Optional(CONF_START): TemplateSelector(), - vol.Optional(CONF_END): TemplateSelector(), - vol.Optional(CONF_DURATION): DurationSelector( - DurationSelectorConfig(enable_day=True, allow_negative=False) - ), - } -) + + +async def get_state_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: + """Return schema for state step.""" + entity_id = handler.options[CONF_ENTITY_ID] + + return vol.Schema( + { + vol.Optional(CONF_ENTITY_ID): EntitySelector( + EntitySelectorConfig(read_only=True) + ), + vol.Required(CONF_STATE): StateSelector( + StateSelectorConfig( + multiple=True, + entity_id=entity_id, + ) + ), + } + ) + + +async def get_options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: + """Return schema for options step.""" + entity_id = handler.options[CONF_ENTITY_ID] + return _get_options_schema_with_entity_id(entity_id) + + +def _get_options_schema_with_entity_id(entity_id: str) -> vol.Schema: + return vol.Schema( + { + vol.Optional(CONF_ENTITY_ID): EntitySelector( + EntitySelectorConfig(read_only=True) + ), + vol.Optional(CONF_STATE): StateSelector( + StateSelectorConfig( + multiple=True, + entity_id=entity_id, + read_only=True, + ) + ), + vol.Optional(CONF_TYPE): SelectSelector( + SelectSelectorConfig( + options=CONF_TYPE_KEYS, + mode=SelectSelectorMode.DROPDOWN, + translation_key=CONF_TYPE, + read_only=True, + ) + ), + vol.Optional(CONF_START): TemplateSelector(), + vol.Optional(CONF_END): TemplateSelector(), + vol.Optional(CONF_DURATION): DurationSelector( + DurationSelectorConfig(enable_day=True, allow_negative=False) + ), + } + ) + CONFIG_FLOW = { "user": SchemaFlowFormStep( schema=DATA_SCHEMA_SETUP, - next_step="options", + next_step="state", ), + "state": SchemaFlowFormStep(schema=get_state_schema, next_step="options"), "options": SchemaFlowFormStep( - schema=DATA_SCHEMA_OPTIONS, + schema=get_options_schema, validate_user_input=validate_options, preview="history_stats", ), } OPTIONS_FLOW = { "init": SchemaFlowFormStep( - DATA_SCHEMA_OPTIONS, + schema=get_options_schema, validate_user_input=validate_options, preview="history_stats", ), @@ -198,7 +232,9 @@ def async_preview_updated( validated_data: Any = None try: - validated_data = DATA_SCHEMA_OPTIONS(msg["user_input"]) + validated_data = (_get_options_schema_with_entity_id(entity_id))( + msg["user_input"] + ) except vol.Invalid as ex: connection.send_error(msg["id"], "invalid_schema", str(ex)) return diff --git a/homeassistant/components/history_stats/strings.json b/homeassistant/components/history_stats/strings.json index 7a33099cf9928e..7c4a1cfa677c42 100644 --- a/homeassistant/components/history_stats/strings.json +++ b/homeassistant/components/history_stats/strings.json @@ -23,6 +23,16 @@ "type": "The type of sensor, one of 'time', 'ratio' or 'count'" } }, + "state": { + "data": { + "entity_id": "[%key:component::history_stats::config::step::user::data::entity_id%]", + "state": "[%key:component::history_stats::config::step::user::data::state%]" + }, + "data_description": { + "entity_id": "[%key:component::history_stats::config::step::user::data_description::entity_id%]", + "state": "[%key:component::history_stats::config::step::user::data_description::state%]" + } + }, "options": { "description": "Read the documentation for further details on how to configure the history stats sensor using these options.", "data": { diff --git a/tests/components/history_stats/test_config_flow.py b/tests/components/history_stats/test_config_flow.py index 08dbefe7465331..5b0756f6c61dfd 100644 --- a/tests/components/history_stats/test_config_flow.py +++ b/tests/components/history_stats/test_config_flow.py @@ -42,11 +42,17 @@ async def test_form( { CONF_NAME: DEFAULT_NAME, CONF_ENTITY_ID: "binary_sensor.test_monitored", - CONF_STATE: ["on"], CONF_TYPE: "count", }, ) await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_STATE: ["on"], + }, + ) + await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -124,11 +130,25 @@ async def test_validation_options( { CONF_NAME: DEFAULT_NAME, CONF_ENTITY_ID: "binary_sensor.test_monitored", - CONF_STATE: ["on"], CONF_TYPE: "count", }, ) await hass.async_block_till_done() + + assert result["step_id"] == "state" + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_STATE: ["on"], + }, + ) + await hass.async_block_till_done() + + assert result["step_id"] == "options" + assert result["type"] is FlowResultType.FORM + result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -182,12 +202,19 @@ async def test_entry_already_exist( { CONF_NAME: DEFAULT_NAME, CONF_ENTITY_ID: "binary_sensor.test_monitored", - CONF_STATE: ["on"], CONF_TYPE: "count", }, ) await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_STATE: ["on"], + }, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -256,6 +283,12 @@ def _fake_states(*args, **kwargs): CONF_NAME: DEFAULT_NAME, CONF_ENTITY_ID: monitored_entity, CONF_TYPE: CONF_TYPE_COUNT, + }, + ) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { CONF_STATE: ["on"], }, ) From c1eb4926167e0759a12efa669620850b26c27c0a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Sep 2025 17:19:15 -0500 Subject: [PATCH 3/6] Fix Bluetooth mock to prevent degraded mode repair issues in tests (#152081) --- tests/components/bluetooth/test_init.py | 4 +--- tests/conftest.py | 5 +++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index de299c58b93e54..d896cd83e76982 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -4,7 +4,7 @@ from datetime import timedelta import time from typing import Any -from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch from bleak import BleakError from bleak.backends.scanner import AdvertisementData, BLEDevice @@ -140,7 +140,6 @@ def register_detection_callback(self, *args, **kwargs): "adapter": "hci0", "bluez": scanner.PASSIVE_SCANNER_ARGS, # pylint: disable=c-extension-no-member "scanning_mode": "passive", - "detection_callback": ANY, } @@ -190,7 +189,6 @@ def register_detection_callback(self, *args, **kwargs): assert init_kwargs == { "adapter": "hci0", "scanning_mode": "active", - "detection_callback": ANY, } diff --git a/tests/conftest.py b/tests/conftest.py index a07e659378aa90..05714d71a22a93 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1858,9 +1858,10 @@ def mock_bleak_scanner_start() -> Generator[MagicMock]: # pylint: disable-next=c-extension-no-member bluetooth_scanner.OriginalBleakScanner.stop = AsyncMock() # type: ignore[assignment] - # Mock BlueZ management controller + # Mock BlueZ management controller to successfully setup + # This prevents the manager from operating in degraded mode mock_mgmt_bluetooth_ctl = Mock() - mock_mgmt_bluetooth_ctl.setup = AsyncMock(side_effect=OSError("Mocked error")) + mock_mgmt_bluetooth_ctl.setup = AsyncMock(return_value=None) with ( patch.object( From 86750ae5c31e56635142dbe4e592b18013348245 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Sep 2025 19:45:22 -0500 Subject: [PATCH 4/6] Fix HomeKit Controller stale values at startup (#152086) Co-authored-by: TheJulianJES --- .../homekit_controller/connection.py | 54 ++++++++++-- .../homekit_controller/test_connection.py | 87 +++++++++++++++++++ 2 files changed, 134 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 139ceef48adf0b..ce8dc498d6d512 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -20,7 +20,12 @@ EncryptionError, ) from aiohomekit.model import Accessories, Accessory, Transport -from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes +from aiohomekit.model.characteristics import ( + EVENT_CHARACTERISTICS, + Characteristic, + CharacteristicPermissions, + CharacteristicsTypes, +) from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.thread import async_get_preferred_dataset @@ -179,6 +184,21 @@ def remove_pollable_characteristics( for aid_iid in characteristics: self.pollable_characteristics.discard(aid_iid) + def get_all_pollable_characteristics(self) -> set[tuple[int, int]]: + """Get all characteristics that can be polled. + + This is used during startup to poll all readable characteristics + before entities have registered what they care about. + """ + return { + (accessory.aid, char.iid) + for accessory in self.entity_map.accessories + for service in accessory.services + for char in service.characteristics + if CharacteristicPermissions.paired_read in char.perms + and char.type not in EVENT_CHARACTERISTICS + } + def add_watchable_characteristics( self, characteristics: list[tuple[int, int]] ) -> None: @@ -309,9 +329,13 @@ async def async_setup(self) -> None: await self.async_process_entity_map() if transport != Transport.BLE: - # Do a single poll to make sure the chars are - # up to date so we don't restore old data. - await self.async_update() + # When Home Assistant starts, we restore the accessory map from storage + # which contains characteristic values from when HA was last running. + # These values are stale and may be incorrect (e.g., Ecobee thermostats + # report 100°C when restarting). We need to poll for fresh values before + # creating entities. Use poll_all=True since entities haven't registered + # their characteristics yet. + await self.async_update(poll_all=True) self._async_start_polling() # If everything is up to date, we can create the entities @@ -863,9 +887,25 @@ async def async_request_update(self, now: datetime | None = None) -> None: """Request an debounced update from the accessory.""" await self._debounced_update.async_call() - async def async_update(self, now: datetime | None = None) -> None: - """Poll state of all entities attached to this bridge/accessory.""" - to_poll = self.pollable_characteristics + async def async_update( + self, now: datetime | None = None, *, poll_all: bool = False + ) -> None: + """Poll state of all entities attached to this bridge/accessory. + + Args: + now: The current time (used by time interval callbacks). + poll_all: If True, poll all readable characteristics instead + of just the registered ones. + This is useful during initial setup before entities have + registered their characteristics. + """ + if poll_all: + # Poll all readable characteristics during initial startup + # excluding device trigger characteristics (buttons, doorbell, etc.) + to_poll = self.get_all_pollable_characteristics() + else: + to_poll = self.pollable_characteristics + if not to_poll: self.async_update_available_state() _LOGGER.debug( diff --git a/tests/components/homekit_controller/test_connection.py b/tests/components/homekit_controller/test_connection.py index 00c7bb16259916..99203d400fea5b 100644 --- a/tests/components/homekit_controller/test_connection.py +++ b/tests/components/homekit_controller/test_connection.py @@ -2,6 +2,7 @@ from collections.abc import Callable import dataclasses +from typing import Any from unittest import mock from aiohomekit.controller import TransportType @@ -11,6 +12,7 @@ from aiohomekit.testing import FakeController import pytest +from homeassistant.components.climate import ATTR_CURRENT_TEMPERATURE from homeassistant.components.homekit_controller.const import ( DEBOUNCE_COOLDOWN, DOMAIN, @@ -439,3 +441,88 @@ def _create_accessory(accessory: Accessory) -> Service: await time_changed(hass, DEBOUNCE_COOLDOWN) await hass.async_block_till_done() assert len(mock_get_characteristics.call_args_list[0][0][0]) > 1 + + +async def test_poll_all_on_startup_refreshes_stale_values( + hass: HomeAssistant, hass_storage: dict[str, Any] +) -> None: + """Test that entities get fresh values on startup instead of stale stored values.""" + # Load actual Ecobee accessory fixture + accessories = await setup_accessories_from_file(hass, "ecobee3.json") + + # Pre-populate storage with the accessories data (already has stale values) + hass_storage["homekit_controller-entity-map"] = { + "version": 1, + "minor_version": 1, + "key": "homekit_controller-entity-map", + "data": { + "pairings": { + "00:00:00:00:00:00": { + "config_num": 1, + "accessories": [ + a.to_accessory_and_service_list() for a in accessories + ], + } + } + }, + } + + # Track what gets polled during setup + polled_chars: list[tuple[int, int]] = [] + + # Set up the test accessories + fake_controller = await setup_platform(hass) + + # Mock get_characteristics to track polling and return fresh temperature + async def mock_get_characteristics( + chars: set[tuple[int, int]], **kwargs: Any + ) -> dict[tuple[int, int], dict[str, Any]]: + """Return fresh temperature value when polled.""" + polled_chars.extend(chars) + # Return fresh values for all characteristics + result: dict[tuple[int, int], dict[str, Any]] = {} + for aid, iid in chars: + # Find the characteristic and return appropriate value + for accessory in accessories: + if accessory.aid != aid: + continue + for service in accessory.services: + for char in service.characteristics: + if char.iid != iid: + continue + # Return fresh temperature instead of stale fixture value + if char.type == CharacteristicsTypes.TEMPERATURE_CURRENT: + result[(aid, iid)] = {"value": 22.5} # Fresh value + else: + result[(aid, iid)] = {"value": char.value} + break + return result + + # Add the paired device with our mock + await fake_controller.add_paired_device(accessories, "00:00:00:00:00:00") + config_entry = MockConfigEntry( + version=1, + domain="homekit_controller", + entry_id="TestData", + data={"AccessoryPairingID": "00:00:00:00:00:00"}, + title="test", + ) + config_entry.add_to_hass(hass) + + # Get the pairing and patch its get_characteristics + pairing = fake_controller.pairings["00:00:00:00:00:00"] + + with mock.patch.object(pairing, "get_characteristics", mock_get_characteristics): + # Set up the config entry (this should trigger poll_all=True) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Verify that polling happened during setup (poll_all=True was used) + assert ( + len(polled_chars) == 79 + ) # The Ecobee fixture has exactly 79 readable characteristics + + # Check that the climate entity has the fresh temperature (22.5°C) not the stale fixture value (21.8°C) + state = hass.states.get("climate.homew") + assert state is not None + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5 From 2cdf0b74d5ed8b955e156e7ad39d455c40ae29d4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 10 Sep 2025 22:49:10 -0400 Subject: [PATCH 5/6] Gemini: Reuse attachment mime type if known (#152094) --- .../google_generative_ai_conversation/__init__.py | 2 +- .../google_generative_ai_conversation/ai_task.py | 2 +- .../google_generative_ai_conversation/entity.py | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/__init__.py b/homeassistant/components/google_generative_ai_conversation/__init__.py index 86966937057f9c..c6a07a93331078 100644 --- a/homeassistant/components/google_generative_ai_conversation/__init__.py +++ b/homeassistant/components/google_generative_ai_conversation/__init__.py @@ -92,7 +92,7 @@ async def generate_content(call: ServiceCall) -> ServiceResponse: prompt_parts.extend( await async_prepare_files_for_prompt( - hass, client, [Path(filename) for filename in files] + hass, client, [(Path(filename), None) for filename in files] ) ) diff --git a/homeassistant/components/google_generative_ai_conversation/ai_task.py b/homeassistant/components/google_generative_ai_conversation/ai_task.py index 003ca09947bf9b..1703aab1678135 100644 --- a/homeassistant/components/google_generative_ai_conversation/ai_task.py +++ b/homeassistant/components/google_generative_ai_conversation/ai_task.py @@ -122,7 +122,7 @@ async def _async_generate_image( await async_prepare_files_for_prompt( self.hass, self._genai_client, - [a.path for a in user_message.attachments], + [(a.path, a.mime_type) for a in user_message.attachments], ) ) diff --git a/homeassistant/components/google_generative_ai_conversation/entity.py b/homeassistant/components/google_generative_ai_conversation/entity.py index c9364603b7906b..cbb493e29b8830 100644 --- a/homeassistant/components/google_generative_ai_conversation/entity.py +++ b/homeassistant/components/google_generative_ai_conversation/entity.py @@ -452,7 +452,7 @@ async def _async_handle_chat_log( await async_prepare_files_for_prompt( self.hass, self._genai_client, - [a.path for a in user_message.attachments], + [(a.path, a.mime_type) for a in user_message.attachments], ) ) @@ -526,7 +526,7 @@ def create_generate_content_config(self) -> GenerateContentConfig: async def async_prepare_files_for_prompt( - hass: HomeAssistant, client: Client, files: list[Path] + hass: HomeAssistant, client: Client, files: list[tuple[Path, str | None]] ) -> list[File]: """Upload files so they can be attached to a prompt. @@ -535,10 +535,11 @@ async def async_prepare_files_for_prompt( def upload_files() -> list[File]: prompt_parts: list[File] = [] - for filename in files: + for filename, mimetype in files: if not filename.exists(): raise HomeAssistantError(f"`{filename}` does not exist") - mimetype = mimetypes.guess_type(filename)[0] + if mimetype is None: + mimetype = mimetypes.guess_type(filename)[0] prompt_parts.append( client.files.upload( file=filename, From f91e4090f96e914dfc48141725246cfcfe4591d8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 10 Sep 2025 22:55:59 -0400 Subject: [PATCH 6/6] Add path to resolved media in image_upload (#152093) --- homeassistant/components/image_upload/__init__.py | 4 ++-- homeassistant/components/image_upload/const.py | 1 + .../components/image_upload/media_source.py | 15 +++++++++++++-- .../components/image_upload/test_media_source.py | 2 ++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/image_upload/__init__.py b/homeassistant/components/image_upload/__init__.py index 2bf28d13fd2ef8..ff86d4441e49b5 100644 --- a/homeassistant/components/image_upload/__init__.py +++ b/homeassistant/components/image_upload/__init__.py @@ -24,7 +24,7 @@ from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.util import dt as dt_util -from .const import DOMAIN +from .const import DOMAIN, FOLDER_IMAGE _LOGGER = logging.getLogger(__name__) STORAGE_KEY = "image" @@ -45,7 +45,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Image integration.""" - image_dir = pathlib.Path(hass.config.path("image")) + image_dir = pathlib.Path(hass.config.path(FOLDER_IMAGE)) hass.data[DOMAIN] = storage_collection = ImageStorageCollection(hass, image_dir) await storage_collection.async_load() ImageUploadStorageCollectionWebsocket( diff --git a/homeassistant/components/image_upload/const.py b/homeassistant/components/image_upload/const.py index f7607f745c7909..89981b9dc302fd 100644 --- a/homeassistant/components/image_upload/const.py +++ b/homeassistant/components/image_upload/const.py @@ -1,3 +1,4 @@ """Constants for the Image Upload integration.""" DOMAIN = "image_upload" +FOLDER_IMAGE = "image" diff --git a/homeassistant/components/image_upload/media_source.py b/homeassistant/components/image_upload/media_source.py index ee9511e2c36999..d1fc978c27839f 100644 --- a/homeassistant/components/image_upload/media_source.py +++ b/homeassistant/components/image_upload/media_source.py @@ -2,6 +2,10 @@ from __future__ import annotations +import pathlib + +from propcache.api import cached_property + from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_source import ( BrowseMediaSource, @@ -12,7 +16,7 @@ ) from homeassistant.core import HomeAssistant -from .const import DOMAIN +from .const import DOMAIN, FOLDER_IMAGE async def async_get_media_source(hass: HomeAssistant) -> ImageUploadMediaSource: @@ -30,6 +34,11 @@ def __init__(self, hass: HomeAssistant) -> None: super().__init__(DOMAIN) self.hass = hass + @cached_property + def image_folder(self) -> pathlib.Path: + """Return the image folder path.""" + return pathlib.Path(self.hass.config.path(FOLDER_IMAGE)) + async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" image = self.hass.data[DOMAIN].data.get(item.identifier) @@ -38,7 +47,9 @@ async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: raise Unresolvable(f"Could not resolve media item: {item.identifier}") return PlayMedia( - f"/api/image/serve/{image['id']}/original", image["content_type"] + f"/api/image/serve/{image['id']}/original", + image["content_type"], + path=self.image_folder / item.identifier / "original", ) async def async_browse_media( diff --git a/tests/components/image_upload/test_media_source.py b/tests/components/image_upload/test_media_source.py index d66e099bdc9d95..3545abcb79929d 100644 --- a/tests/components/image_upload/test_media_source.py +++ b/tests/components/image_upload/test_media_source.py @@ -1,5 +1,6 @@ """Test image_upload media source.""" +from pathlib import Path import tempfile from unittest.mock import patch @@ -79,6 +80,7 @@ async def test_resolving( assert item is not None assert item.url == f"/api/image/serve/{image_id}/original" assert item.mime_type == "image/png" + assert item.path == Path(hass.config.path("image")) / image_id / "original" invalid_id = "aabbccddeeff" with pytest.raises(