22from __future__ import annotations
33
44from abc import abstractmethod
5- from functools import cached_property
5+ from functools import cached_property # pylint: disable=hass-deprecated-import
66import logging
77from pathlib import Path
88import shutil
1717from homeassistant .config_entries import (
1818 SOURCE_IMPORT ,
1919 ConfigEntry ,
20+ ConfigEntryBaseFlow ,
2021 ConfigFlow ,
22+ ConfigFlowResult ,
2123 OptionsFlowWithConfigEntry ,
2224)
2325from homeassistant .const import (
3032 UnitOfSpeed ,
3133)
3234from homeassistant .core import State , callback
33- from homeassistant .data_entry_flow import FlowHandler , FlowResult
3435from homeassistant .helpers .selector import (
3536 BooleanSelector ,
3637 EntitySelector ,
@@ -85,7 +86,7 @@ def split_conf(conf: dict[str, Any]) -> dict[str, dict[str, Any]]:
8586 }
8687
8788
88- class CompositeFlow (FlowHandler ):
89+ class CompositeFlow (ConfigEntryBaseFlow ):
8990 """Composite flow mixin."""
9091
9192 @cached_property
@@ -103,9 +104,11 @@ def _uploaded_dir(self) -> Path:
103104 """Return real path to "/local/uploaded" directory."""
104105 return self ._local_dir / "uploaded"
105106
106- @cached_property
107107 def _local_files (self ) -> list [str ]:
108- """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+ """
109112 if not (local_dir := self ._local_dir ).is_dir ():
110113 _LOGGER .debug ("/local directory (%s) does not exist" , local_dir )
111114 return []
@@ -171,14 +174,14 @@ def _set_entity_picture(
171174 def _save_uploaded_file (self , uploaded_file_id : str ) -> str :
172175 """Save uploaded file.
173176
174- Must be called in an executor.
177+ Must be called in an executor since it does file I/O .
175178
176179 Returns name of file relative to "/local".
177180 """
178181 with process_uploaded_file (self .hass , uploaded_file_id ) as uf_path :
179182 ud = self ._uploaded_dir
180183 ud .mkdir (parents = True , exist_ok = True )
181- suffix = MIME_TO_SUFFIX [filetype .guess_mime (uf_path )]
184+ suffix = MIME_TO_SUFFIX [cast ( str , filetype .guess_mime (uf_path ) )]
182185 fn = ud / f"x.{ suffix } "
183186 idx = 0
184187 while (uf := fn .with_stem (f"image{ idx :03d} " )).exists ():
@@ -188,7 +191,7 @@ def _save_uploaded_file(self, uploaded_file_id: str) -> str:
188191
189192 async def async_step_options (
190193 self , user_input : dict [str , Any ] | None = None
191- ) -> FlowResult :
194+ ) -> ConfigFlowResult :
192195 """Get config options."""
193196 errors = {}
194197
@@ -271,7 +274,9 @@ def entity_filter(state: State) -> bool:
271274 step_id = "options" , data_schema = data_schema , errors = errors , last_step = False
272275 )
273276
274- async def async_step_ep_menu (self , _ : dict [str , Any ] | None = None ) -> FlowResult :
277+ async def async_step_ep_menu (
278+ self , _ : dict [str , Any ] | None = None
279+ ) -> ConfigFlowResult :
275280 """Specify where to get composite's picture from."""
276281 entity_id , local_file = self ._cur_entity_picture
277282 cur_source : Path | str | None
@@ -281,7 +286,7 @@ async def async_step_ep_menu(self, _: dict[str, Any] | None = None) -> FlowResul
281286 cur_source = entity_id
282287
283288 menu_options = ["all_states" , "ep_upload_file" , "ep_input_entity" ]
284- if self ._local_files :
289+ if await self .hass . async_add_executor_job ( self . _local_files ) :
285290 menu_options .insert (1 , "ep_local_file" )
286291 if cur_source :
287292 menu_options .append ("ep_none" )
@@ -294,7 +299,7 @@ async def async_step_ep_menu(self, _: dict[str, Any] | None = None) -> FlowResul
294299
295300 async def async_step_ep_input_entity (
296301 self , user_input : dict [str , Any ] | None = None
297- ) -> FlowResult :
302+ ) -> ConfigFlowResult :
298303 """Specify which input to get composite's picture from."""
299304 if user_input is not None :
300305 self ._set_entity_picture (entity_id = user_input .get (CONF_ENTITY ))
@@ -323,13 +328,13 @@ async def async_step_ep_input_entity(
323328
324329 async def async_step_ep_local_file (
325330 self , user_input : dict [str , Any ] | None = None
326- ) -> FlowResult :
331+ ) -> ConfigFlowResult :
327332 """Specify a local file for composite's picture."""
328333 if user_input is not None :
329334 self ._set_entity_picture (local_file = user_input .get (CONF_ENTITY_PICTURE ))
330335 return await self .async_step_all_states ()
331336
332- local_files = self ._local_files
337+ local_files = await self .hass . async_add_executor_job ( self . _local_files )
333338 _ , local_file = self ._cur_entity_picture
334339 if local_file and local_file not in local_files :
335340 local_files .append (local_file )
@@ -353,21 +358,31 @@ async def async_step_ep_local_file(
353358
354359 async def async_step_ep_upload_file (
355360 self , user_input : dict [str , Any ] | None = None
356- ) -> FlowResult :
361+ ) -> ConfigFlowResult :
357362 """Upload a file for composite's picture."""
358363 if user_input is not None :
359364 if (uploaded_file_id := user_input .get (CONF_ENTITY_PICTURE )) is None :
360365 self ._set_entity_picture ()
361366 return await self .async_step_all_states ()
362367
363- local_dir_exists = self ._local_dir .is_dir ()
364- local_file = await self .hass .async_add_executor_job (
365- 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
366381 )
367382 self ._set_entity_picture (local_file = local_file )
368- if local_dir_exists :
369- return await self .async_step_all_states ()
370- 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 ()
371386
372387 accept = ", " .join (f".{ ext } " for ext in PICTURE_SUFFIXES )
373388 data_schema = vol .Schema (
@@ -383,7 +398,7 @@ async def async_step_ep_upload_file(
383398
384399 async def async_step_ep_warn (
385400 self , user_input : dict [str , Any ] | None = None
386- ) -> FlowResult :
401+ ) -> ConfigFlowResult :
387402 """Warn that since "/local" was created system might need to be restarted."""
388403 if user_input is not None :
389404 return await self .async_step_all_states ()
@@ -394,14 +409,16 @@ async def async_step_ep_warn(
394409 last_step = False ,
395410 )
396411
397- async def async_step_ep_none (self , _ : dict [str , Any ] | None = None ) -> FlowResult :
412+ async def async_step_ep_none (
413+ self , _ : dict [str , Any ] | None = None
414+ ) -> ConfigFlowResult :
398415 """Set composite's entity picture to none."""
399416 self ._set_entity_picture ()
400417 return await self .async_step_all_states ()
401418
402419 async def async_step_all_states (
403420 self , user_input : dict [str , Any ] | None = None
404- ) -> FlowResult :
421+ ) -> ConfigFlowResult :
405422 """Specify if all states should be used for appropriate entities."""
406423 if user_input is not None :
407424 entity_ids = user_input .get (CONF_ENTITY , [])
@@ -430,7 +447,9 @@ async def async_step_all_states(
430447 return self .async_show_form (step_id = "all_states" , data_schema = data_schema )
431448
432449 @abstractmethod
433- async def async_step_done (self , _ : dict [str , Any ] | None = None ) -> FlowResult :
450+ async def async_step_done (
451+ self , _ : dict [str , Any ] | None = None
452+ ) -> ConfigFlowResult :
434453 """Finish the flow."""
435454
436455
@@ -457,16 +476,14 @@ def async_get_options_flow(config_entry: ConfigEntry) -> CompositeOptionsFlow:
457476 @callback
458477 def async_supports_options_flow (cls , config_entry : ConfigEntry ) -> bool :
459478 """Return options flow support for this handler."""
460- if config_entry .source == SOURCE_IMPORT :
461- return False
462- return True
479+ return config_entry .source != SOURCE_IMPORT
463480
464481 @property
465482 def options (self ) -> dict [str , Any ]:
466483 """Return mutable copy of options."""
467484 return self ._options
468485
469- async def async_step_import (self , data : dict [str , Any ]) -> FlowResult :
486+ async def async_step_import (self , data : dict [str , Any ]) -> ConfigFlowResult :
470487 """Import config entry from configuration."""
471488 if (driving_speed := data .get (CONF_DRIVING_SPEED )) is not None :
472489 data [CONF_DRIVING_SPEED ] = SpeedConverter .convert (
@@ -483,7 +500,9 @@ async def async_step_import(self, data: dict[str, Any]) -> FlowResult:
483500 ** split_conf (data ), # type: ignore[arg-type]
484501 )
485502
486- async def async_step_user (self , _ : dict [str , Any ] | None = None ) -> FlowResult :
503+ async def async_step_user (
504+ self , _ : dict [str , Any ] | None = None
505+ ) -> ConfigFlowResult :
487506 """Start user config flow."""
488507 return await self .async_step_name ()
489508
@@ -499,7 +518,7 @@ def _name_used(self, name: str) -> bool:
499518
500519 async def async_step_name (
501520 self , user_input : dict [str , Any ] | None = None
502- ) -> FlowResult :
521+ ) -> ConfigFlowResult :
503522 """Get name."""
504523 errors = {}
505524
@@ -517,14 +536,18 @@ async def async_step_name(
517536 step_id = "name" , data_schema = data_schema , errors = errors , last_step = False
518537 )
519538
520- async def async_step_done (self , _ : dict [str , Any ] | None = None ) -> FlowResult :
539+ async def async_step_done (
540+ self , _ : dict [str , Any ] | None = None
541+ ) -> ConfigFlowResult :
521542 """Finish the flow."""
522543 return self .async_create_entry (title = self ._name , data = {}, options = self .options )
523544
524545
525546class CompositeOptionsFlow (OptionsFlowWithConfigEntry , CompositeFlow ):
526547 """Composite integration options flow."""
527548
528- async def async_step_done (self , _ : dict [str , Any ] | None = None ) -> FlowResult :
549+ async def async_step_done (
550+ self , _ : dict [str , Any ] | None = None
551+ ) -> ConfigFlowResult :
529552 """Finish the flow."""
530553 return self .async_create_entry (title = "" , data = self .options )
0 commit comments