Skip to content

Commit fcd61f1

Browse files
committed
Move potential file I/O into executor
1 parent 505c745 commit fcd61f1

File tree

2 files changed

+29
-15
lines changed

2 files changed

+29
-15
lines changed

custom_components/composite/config.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ def _entities(entities: list[str | dict]) -> list[dict]:
6464
def _entity_picture(entity_picture: str) -> str:
6565
"""Validate entity picture.
6666
67+
Must be run in an executor since it might do file I/O.
68+
6769
Can be an URL or a file in "/local".
6870
6971
file can be "/local/file" or just "file"
@@ -204,4 +206,6 @@ async def async_validate_config(
204206
hass: HomeAssistant, config: ConfigType
205207
) -> ConfigType | None:
206208
"""Validate configuration."""
207-
return cast(ConfigType, _CONFIG_SCHEMA(config))
209+
# Perform _CONFIG_SCHEMA validation in executor since it may indirectly invoke
210+
# _entity_picture which must be run in an executor because it might do file I/O.
211+
return cast(ConfigType, await hass.async_add_executor_job(_CONFIG_SCHEMA, config))

custom_components/composite/config_flow.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,11 @@ def _uploaded_dir(self) -> Path:
104104
"""Return real path to "/local/uploaded" directory."""
105105
return self._local_dir / "uploaded"
106106

107-
@cached_property
108107
def _local_files(self) -> list[str]:
109-
"""Return a list of files in "/local" and subdirectories."""
108+
"""Return a list of files in "/local" and subdirectories.
109+
110+
Must be called in an executor since it does file I/O.
111+
"""
110112
if not (local_dir := self._local_dir).is_dir():
111113
_LOGGER.debug("/local directory (%s) does not exist", local_dir)
112114
return []
@@ -172,7 +174,7 @@ def _set_entity_picture(
172174
def _save_uploaded_file(self, uploaded_file_id: str) -> str:
173175
"""Save uploaded file.
174176
175-
Must be called in an executor.
177+
Must be called in an executor since it does file I/O.
176178
177179
Returns name of file relative to "/local".
178180
"""
@@ -284,7 +286,7 @@ async def async_step_ep_menu(
284286
cur_source = entity_id
285287

286288
menu_options = ["all_states", "ep_upload_file", "ep_input_entity"]
287-
if self._local_files:
289+
if await self.hass.async_add_executor_job(self._local_files):
288290
menu_options.insert(1, "ep_local_file")
289291
if cur_source:
290292
menu_options.append("ep_none")
@@ -332,7 +334,7 @@ async def async_step_ep_local_file(
332334
self._set_entity_picture(local_file=user_input.get(CONF_ENTITY_PICTURE))
333335
return await self.async_step_all_states()
334336

335-
local_files = self._local_files
337+
local_files = await self.hass.async_add_executor_job(self._local_files)
336338
_, local_file = self._cur_entity_picture
337339
if local_file and local_file not in local_files:
338340
local_files.append(local_file)
@@ -363,14 +365,24 @@ async def async_step_ep_upload_file(
363365
self._set_entity_picture()
364366
return await self.async_step_all_states()
365367

366-
local_dir_exists = self._local_dir.is_dir()
367-
local_file = await self.hass.async_add_executor_job(
368-
self._save_uploaded_file, uploaded_file_id
368+
def save_uploaded_file() -> tuple[bool, str]:
369+
"""Save uploaded file.
370+
371+
Must be called in an executor since it does file I/O.
372+
373+
Returns if local directory existed beforehand and name of uploaded file.
374+
"""
375+
local_dir_exists = self._local_dir.is_dir()
376+
local_file = self._save_uploaded_file(uploaded_file_id)
377+
return local_dir_exists, local_file
378+
379+
local_dir_exists, local_file = await self.hass.async_add_executor_job(
380+
save_uploaded_file
369381
)
370382
self._set_entity_picture(local_file=local_file)
371-
if local_dir_exists:
372-
return await self.async_step_all_states()
373-
return await self.async_step_ep_warn()
383+
if not local_dir_exists:
384+
return await self.async_step_ep_warn()
385+
return await self.async_step_all_states()
374386

375387
accept = ", ".join(f".{ext}" for ext in PICTURE_SUFFIXES)
376388
data_schema = vol.Schema(
@@ -464,9 +476,7 @@ def async_get_options_flow(config_entry: ConfigEntry) -> CompositeOptionsFlow:
464476
@callback
465477
def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool:
466478
"""Return options flow support for this handler."""
467-
if config_entry.source == SOURCE_IMPORT:
468-
return False
469-
return True
479+
return config_entry.source != SOURCE_IMPORT
470480

471481
@property
472482
def options(self) -> dict[str, Any]:

0 commit comments

Comments
 (0)