"""Config flow for the Maintenance Supporter integration.""" from __future__ import annotations import logging from typing import Any from uuid import uuid4 import voluptuous as vol from homeassistant.config_entries import ( ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow, ) from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import selector from .config_flow_options_global import validate_notify_service from .config_flow_schedule import ScheduleStepsMixin from .config_flow_trigger import TriggerConfigMixin from .const import ( CONF_DEFAULT_WARNING_DAYS, CONF_NOTIFICATIONS_ENABLED, CONF_NOTIFY_SERVICE, CONF_OBJECT, CONF_OBJECT_AREA, CONF_OBJECT_DOCUMENTATION_URL, CONF_OBJECT_INSTALLATION_DATE, CONF_OBJECT_MANUFACTURER, CONF_OBJECT_MODEL, CONF_OBJECT_NAME, CONF_OBJECT_NOTES, CONF_OBJECT_SERIAL_NUMBER, CONF_OBJECT_WARRANTY_EXPIRY, CONF_TASKS, DEFAULT_WARNING_DAYS, DOMAIN, GLOBAL_UNIQUE_ID, slugify_object_name, ) from .helpers.i18n import normalize_language from .helpers.schedule import normalize_task_storage from .helpers.task_fields import WARNING_DAYS_RANGE from .templates import ( TEMPLATE_CATEGORIES, ObjectTemplate, get_template_by_id, get_templates_by_category, ) _LOGGER = logging.getLogger(__name__) def _localized_template_default_name(template: ObjectTemplate, hass: HomeAssistant) -> str: """Localized prefill for the template-customize name field (v2.21.1).""" from .templates import localize_template_text return localize_template_text(template.name, normalize_language(hass)) or template.name class MaintenanceSupporterConfigFlow(ScheduleStepsMixin, TriggerConfigMixin, ConfigFlow, domain=DOMAIN): """Handle a config flow for Maintenance Supporter.""" VERSION = 1 MINOR_VERSION = 5 def __init__(self) -> None: """Initialize the config flow.""" self._object_data: dict[str, Any] = {} self._tasks: dict[str, dict[str, Any]] = {} self._current_task: dict[str, Any] = {} self._trigger_entity_id: str | None = None self._trigger_entity_state: State | None = None self._template_category: str = "" self._selected_template: ObjectTemplate | None = None async def async_step_user(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the initial step.""" # Check if global entry exists global_exists = any(entry.unique_id == GLOBAL_UNIQUE_ID for entry in self.hass.config_entries.async_entries(DOMAIN)) if not global_exists: return await self.async_step_global_setup() return self.async_show_menu( step_id="user", menu_options=["create_object", "create_from_template"], ) async def async_step_global_setup(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Set up global configuration.""" errors: dict[str, str] = {} if user_input is not None: # Validate notify service format (no hass check — services may not be loaded yet) raw_service = user_input.get(CONF_NOTIFY_SERVICE, "") normalized, error = validate_notify_service(raw_service) if error: errors[CONF_NOTIFY_SERVICE] = error if not errors: await self.async_set_unique_id(GLOBAL_UNIQUE_ID) self._abort_if_unique_id_configured() return self.async_create_entry( title="Maintenance Supporter", data={ CONF_DEFAULT_WARNING_DAYS: user_input.get(CONF_DEFAULT_WARNING_DAYS, DEFAULT_WARNING_DAYS), CONF_NOTIFICATIONS_ENABLED: user_input.get(CONF_NOTIFICATIONS_ENABLED, False), CONF_NOTIFY_SERVICE: normalized, }, ) # Offer notify targets as a dropdown: legacy notify *services* (mobile_app # devices, notify groups) plus notify *entities* (newer model) — many # single devices appear only as an entity. send_message is the generic # action, not a target → excluded. custom_value keeps it free-text (the # format-only validation above never blocks on existence). Matches the # picker in the options flow + panel. notify_targets = { f"notify.{name}" for name in self.hass.services.async_services().get("notify", {}) if name != "send_message" } notify_targets.update(self.hass.states.async_entity_ids("notify")) notify_services = sorted(notify_targets) return self.async_show_form( step_id="global_setup", data_schema=vol.Schema( { vol.Optional(CONF_DEFAULT_WARNING_DAYS, default=DEFAULT_WARNING_DAYS): selector.NumberSelector( selector.NumberSelectorConfig( min=1, max=WARNING_DAYS_RANGE[1], step=1, mode=selector.NumberSelectorMode.BOX ) ), vol.Optional(CONF_NOTIFICATIONS_ENABLED, default=False): selector.BooleanSelector(), vol.Optional(CONF_NOTIFY_SERVICE, default=""): selector.SelectSelector( selector.SelectSelectorConfig( options=notify_services, mode=selector.SelectSelectorMode.DROPDOWN, custom_value=True, ) ), } ), errors=errors, ) async def async_step_import(self, import_data: dict[str, Any] | None = None) -> ConfigFlowResult: """Programmatically (re)create the global entry with default settings. Used by the missing-global-entry repair flow to restore the global "Maintenance Supporter" configuration after it was deleted while object entries remained (which strips the summary sensors + panel). Aborts if a global entry already exists, so it's safe to trigger unconditionally. """ await self.async_set_unique_id(GLOBAL_UNIQUE_ID) self._abort_if_unique_id_configured() return self.async_create_entry( title="Maintenance Supporter", data={ CONF_DEFAULT_WARNING_DAYS: DEFAULT_WARNING_DAYS, CONF_NOTIFICATIONS_ENABLED: False, CONF_NOTIFY_SERVICE: "", }, ) async def async_step_create_from_template(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Step 1: Select a template category.""" if user_input is not None: if user_input.get("go_back"): return await self.async_step_user() self._template_category = user_input["template_category"] return await self.async_step_template_select() lang = normalize_language(self.hass) options = [ selector.SelectOptionDict( value=cat_id, label=cat.get(f"name_{lang}", cat["name_en"]), ) for cat_id, cat in TEMPLATE_CATEGORIES.items() ] return self.async_show_form( step_id="create_from_template", data_schema=vol.Schema( { vol.Required("template_category"): selector.SelectSelector( selector.SelectSelectorConfig( options=options, mode=selector.SelectSelectorMode.LIST, ) ), vol.Optional("go_back", default=False): selector.BooleanSelector(), } ), ) async def async_step_template_select(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Step 2: Select a template from the chosen category.""" if user_input is not None: if user_input.get("go_back"): return await self.async_step_create_from_template() template = get_template_by_id(user_input["template_id"]) if template is None: return self.async_abort(reason="template_not_found") self._selected_template = template return await self.async_step_template_customize() # v2.21: admin-hidden templates stay out of the picker. from .templates import get_disabled_template_ids, localize_template_text lang = normalize_language(self.hass) disabled = get_disabled_template_ids(self.hass) templates = [t for t in get_templates_by_category(self._template_category) if t.id not in disabled] options = [ selector.SelectOptionDict( value=t.id, label=localize_template_text(t.name, lang) or t.name, ) for t in templates ] return self.async_show_form( step_id="template_select", data_schema=vol.Schema( { vol.Required("template_id"): selector.SelectSelector( selector.SelectSelectorConfig( options=options, mode=selector.SelectSelectorMode.LIST, ) ), vol.Optional("go_back", default=False): selector.BooleanSelector(), } ), ) async def async_step_template_customize(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Step 3: Customize the template before creating the entry.""" errors: dict[str, str] = {} template = self._selected_template if template is None: return self.async_abort(reason="unknown") if user_input is not None: if user_input.get("go_back"): return await self.async_step_template_select() name = user_input[CONF_OBJECT_NAME] # Validate unique name (case-insensitive to match slug-based unique_id) existing_names = [ entry.data.get(CONF_OBJECT, {}).get(CONF_OBJECT_NAME, "").lower() for entry in self.hass.config_entries.async_entries(DOMAIN) if entry.unique_id != GLOBAL_UNIQUE_ID ] if name.lower() in existing_names: errors[CONF_OBJECT_NAME] = "name_exists" else: # Build object data self._object_data = { "id": uuid4().hex, CONF_OBJECT_NAME: name, CONF_OBJECT_AREA: user_input.get(CONF_OBJECT_AREA), CONF_OBJECT_MANUFACTURER: user_input.get(CONF_OBJECT_MANUFACTURER), CONF_OBJECT_MODEL: user_input.get(CONF_OBJECT_MODEL), CONF_OBJECT_SERIAL_NUMBER: user_input.get(CONF_OBJECT_SERIAL_NUMBER), } # Build tasks from template from homeassistant.util import dt as dt_util from .helpers.sanitize import cap_object_fields, cap_task_fields today_iso = dt_util.now().date().isoformat() from .templates import localize_template_text create_lang = normalize_language(self.hass) self._tasks = {} for tt in template.tasks: task_id = uuid4().hex task_data = { "id": task_id, "object_id": self._object_data["id"], "name": localize_template_text(tt.name, create_lang), "type": tt.type, "enabled": True, "schedule_type": tt.schedule_type, "warning_days": tt.warning_days, "history": [], "created_at": today_iso, } if tt.interval_days is not None: task_data["interval_days"] = tt.interval_days if tt.notes: task_data["notes"] = localize_template_text(tt.notes, create_lang) cap_task_fields(task_data) self._tasks[task_id] = task_data cap_object_fields(self._object_data) self._object_data["task_ids"] = list(self._tasks.keys()) return await self.async_step_finish() return self.async_show_form( step_id="template_customize", data_schema=vol.Schema( { vol.Required( CONF_OBJECT_NAME, default=_localized_template_default_name(template, self.hass), ): selector.TextSelector(selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)), vol.Optional(CONF_OBJECT_AREA): selector.AreaSelector(), vol.Optional(CONF_OBJECT_MANUFACTURER): selector.TextSelector( selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT) ), vol.Optional(CONF_OBJECT_MODEL): selector.TextSelector( selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT) ), vol.Optional(CONF_OBJECT_SERIAL_NUMBER): selector.TextSelector( selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT) ), vol.Optional("go_back", default=False): selector.BooleanSelector(), } ), errors=errors, description_placeholders={ "template_name": template.name, "task_count": str(len(template.tasks)), "task_list": ", ".join(t.name for t in template.tasks), }, ) async def async_step_reconfigure(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Allow user to reconfigure object settings.""" entry = self._get_reconfigure_entry() obj_data = dict(entry.data.get(CONF_OBJECT, {})) errors: dict[str, str] = {} if user_input is not None: name = user_input[CONF_OBJECT_NAME] # Validate unique name (skip self) for other in self.hass.config_entries.async_entries(DOMAIN): if other.entry_id != entry.entry_id and other.unique_id != GLOBAL_UNIQUE_ID: if other.data.get(CONF_OBJECT, {}).get("name", "").lower() == name.lower(): errors["base"] = "name_exists" break if not errors: # Migrate name-slug-based unique_ids BEFORE overwriting the # name (see helpers.entity_rename.migrate_object_unique_ids). from .helpers.entity_rename import migrate_object_unique_ids migrate_object_unique_ids(self.hass, entry, obj_data.get("name"), name) obj_data["name"] = name obj_data["area_id"] = user_input.get(CONF_OBJECT_AREA) obj_data["manufacturer"] = user_input.get(CONF_OBJECT_MANUFACTURER) obj_data["model"] = user_input.get(CONF_OBJECT_MODEL) obj_data["serial_number"] = user_input.get(CONF_OBJECT_SERIAL_NUMBER) obj_data["installation_date"] = user_input.get(CONF_OBJECT_INSTALLATION_DATE) obj_data["warranty_expiry"] = user_input.get(CONF_OBJECT_WARRANTY_EXPIRY) # v1.4.0 (#43) obj_data["documentation_url"] = user_input.get(CONF_OBJECT_DOCUMENTATION_URL) or None # v1.4.10 (#46) obj_data["notes"] = (user_input.get(CONF_OBJECT_NOTES) or "").strip() or None new_data = dict(entry.data) new_data[CONF_OBJECT] = obj_data return self.async_update_reload_and_abort(entry, data=new_data, title=name) suggested: dict[str, Any] = { CONF_OBJECT_NAME: obj_data.get("name", ""), CONF_OBJECT_MANUFACTURER: obj_data.get("manufacturer", ""), CONF_OBJECT_MODEL: obj_data.get("model", ""), CONF_OBJECT_SERIAL_NUMBER: obj_data.get("serial_number", ""), CONF_OBJECT_DOCUMENTATION_URL: obj_data.get("documentation_url", ""), CONF_OBJECT_NOTES: obj_data.get("notes", ""), } if obj_data.get("area_id"): suggested[CONF_OBJECT_AREA] = obj_data["area_id"] if obj_data.get("installation_date"): suggested[CONF_OBJECT_INSTALLATION_DATE] = obj_data["installation_date"] if obj_data.get("warranty_expiry"): suggested[CONF_OBJECT_WARRANTY_EXPIRY] = obj_data["warranty_expiry"] schema = self.add_suggested_values_to_schema( vol.Schema( { vol.Required(CONF_OBJECT_NAME): str, vol.Optional(CONF_OBJECT_AREA): selector.AreaSelector(), vol.Optional(CONF_OBJECT_MANUFACTURER): str, vol.Optional(CONF_OBJECT_MODEL): str, vol.Optional(CONF_OBJECT_SERIAL_NUMBER): str, vol.Optional( CONF_OBJECT_INSTALLATION_DATE, ): selector.DateSelector(), vol.Optional( CONF_OBJECT_WARRANTY_EXPIRY, ): selector.DateSelector(), # v1.4.0 (#43): place under serial_number per the request vol.Optional(CONF_OBJECT_DOCUMENTATION_URL): selector.TextSelector( selector.TextSelectorConfig(type=selector.TextSelectorType.URL) ), # v1.4.10 (#46): free-form notes (multiline) vol.Optional(CONF_OBJECT_NOTES): selector.TextSelector( selector.TextSelectorConfig( type=selector.TextSelectorType.TEXT, multiline=True, ) ), } ), suggested, ) return self.async_show_form( step_id="reconfigure", data_schema=schema, errors=errors, description_placeholders={"name": entry.title}, ) async def async_step_websocket(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle object creation from the WebSocket API (no UI).""" from homeassistant.util import dt as dt_util from .helpers.sanitize import cap_object_fields, cap_task_fields if user_input is None: return self.async_abort(reason="missing_data") obj_data = dict(user_input.get(CONF_OBJECT, {})) cap_object_fields(obj_data) object_name = obj_data.get(CONF_OBJECT_NAME, "Unknown") object_slug = slugify_object_name(object_name) await self.async_set_unique_id(f"maintenance_supporter_{object_slug}") self._abort_if_unique_id_configured() obj_data.setdefault("task_ids", []) # Stamp `created_at` on imported tasks that lack it so next_due has a # stable anchor (issue #30). Imports from CSV/JSON go through this # chokepoint regardless of format. Cap every task's strings so # imports can't bypass the WS-schema length limits. today_iso = dt_util.now().date().isoformat() tasks = dict(user_input.get(CONF_TASKS, {})) for task_id, td in list(tasks.items()): if not isinstance(td, dict): continue new_td = dict(td) if "created_at" not in new_td: new_td["created_at"] = today_iso cap_task_fields(new_td) # Store recurrence in the canonical nested `schedule` shape — this is # the CSV/JSON import chokepoint (schedule-model v2). tasks[task_id] = normalize_task_storage(new_td) # Spare parts: re-validate each imported definition through the same # normalizer the WS CRUD uses (bad entries are dropped, not fatal). parts_in = user_input.get("parts") parts: dict[str, dict[str, Any]] = {} if isinstance(parts_in, dict): from .helpers.parts import PartValidationError, normalize_part for pid, praw in parts_in.items(): try: part = normalize_part({**praw, "id": pid}) except (PartValidationError, TypeError): continue parts[part["id"]] = part data: dict[str, Any] = { CONF_OBJECT: obj_data, CONF_TASKS: tasks, } if parts: data["parts"] = parts return self.async_create_entry(title=object_name, data=data) async def async_step_create_object(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Create a new maintenance object.""" errors: dict[str, str] = {} if user_input is not None: if user_input.get("go_back"): return await self.async_step_user() name = user_input[CONF_OBJECT_NAME] # Validate unique name (case-insensitive to match slug-based unique_id) existing_names = [ entry.data.get(CONF_OBJECT, {}).get(CONF_OBJECT_NAME, "").lower() for entry in self.hass.config_entries.async_entries(DOMAIN) if entry.unique_id != GLOBAL_UNIQUE_ID ] if name.lower() in existing_names: errors[CONF_OBJECT_NAME] = "name_exists" else: from .helpers.sanitize import cap_object_fields self._object_data = { "id": uuid4().hex, CONF_OBJECT_NAME: name, CONF_OBJECT_AREA: user_input.get(CONF_OBJECT_AREA), CONF_OBJECT_MANUFACTURER: user_input.get(CONF_OBJECT_MANUFACTURER), CONF_OBJECT_MODEL: user_input.get(CONF_OBJECT_MODEL), CONF_OBJECT_SERIAL_NUMBER: user_input.get(CONF_OBJECT_SERIAL_NUMBER), CONF_OBJECT_INSTALLATION_DATE: user_input.get(CONF_OBJECT_INSTALLATION_DATE), CONF_OBJECT_WARRANTY_EXPIRY: user_input.get(CONF_OBJECT_WARRANTY_EXPIRY), # v1.4.0 (#43) CONF_OBJECT_DOCUMENTATION_URL: user_input.get(CONF_OBJECT_DOCUMENTATION_URL) or None, # v1.4.10 (#46) CONF_OBJECT_NOTES: ((user_input.get(CONF_OBJECT_NOTES) or "").strip() or None), } cap_object_fields(self._object_data) self._tasks = {} return await self.async_step_task_menu() return self.async_show_form( step_id="create_object", data_schema=vol.Schema( { vol.Required(CONF_OBJECT_NAME): selector.TextSelector( selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT) ), vol.Optional(CONF_OBJECT_AREA): selector.AreaSelector(), vol.Optional(CONF_OBJECT_MANUFACTURER): selector.TextSelector( selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT) ), vol.Optional(CONF_OBJECT_MODEL): selector.TextSelector( selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT) ), vol.Optional(CONF_OBJECT_SERIAL_NUMBER): selector.TextSelector( selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT) ), vol.Optional(CONF_OBJECT_INSTALLATION_DATE): selector.DateSelector(), vol.Optional(CONF_OBJECT_WARRANTY_EXPIRY): selector.DateSelector(), # v1.4.0 (#43): place under serial_number per the request vol.Optional(CONF_OBJECT_DOCUMENTATION_URL): selector.TextSelector( selector.TextSelectorConfig(type=selector.TextSelectorType.URL) ), # v1.4.10 (#46): free-form notes (multiline) vol.Optional(CONF_OBJECT_NOTES): selector.TextSelector( selector.TextSelectorConfig( type=selector.TextSelectorType.TEXT, multiline=True, ) ), vol.Optional("go_back", default=False): selector.BooleanSelector(), } ), errors=errors, ) async def async_step_task_menu(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Show menu to add tasks or finish.""" return self.async_show_menu( step_id="task_menu", menu_options=["add_task", "finish"], description_placeholders={ "object_name": self._object_data.get(CONF_OBJECT_NAME, ""), "task_count": str(len(self._tasks)), }, ) async def async_step_add_task(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Add a maintenance task.""" return await self._schedule_add_task( user_input, step_id="add_task", on_go_back=self.async_step_task_menu, time_based_step=self.async_step_time_based, calendar_step=self.async_step_calendar, sensor_step=self.async_step_sensor_select, one_time_step=self.async_step_one_time, manual_step=self.async_step_manual, seed_id=True, description_placeholders={ "object_name": self._object_data.get(CONF_OBJECT_NAME, ""), }, ) async def async_step_time_based(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Configure time-based schedule.""" return await self._schedule_time_based( user_input, step_id="time_based", on_go_back=self.async_step_add_task, on_complete=self._save_task_and_return, ) async def async_step_calendar(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Configure a calendar recurrence kind (weekdays / nth_weekday / day_of_month) during initial setup.""" return await self._schedule_calendar( user_input, step_id="calendar", on_go_back=self.async_step_add_task, on_complete=self._save_task_and_return, ) async def async_step_one_time(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Configure a one-time (non-recurring) task.""" return await self._schedule_one_time( user_input, step_id="one_time", on_go_back=self.async_step_add_task, on_complete=self._save_task_and_return, ) # --- Sensor trigger steps (thin wrappers delegating to TriggerConfigMixin) --- async def async_step_sensor_select(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Select sensor entity for trigger.""" self._on_cancel = lambda: self.async_step_add_task() return await self._trigger_sensor_select( user_input, step_id="sensor_select", next_step=self.async_step_sensor_attribute, ) async def async_step_sensor_attribute(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Select attribute to monitor.""" self._on_cancel = lambda: self.async_step_sensor_select() return await self._trigger_sensor_attribute( user_input, step_id="sensor_attribute", next_step=self.async_step_trigger_type, error_step_id="sensor_select", ) async def async_step_trigger_type(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Select trigger type.""" self._on_cancel = lambda: self.async_step_sensor_attribute() return await self._trigger_type_select( user_input, step_id="trigger_type", threshold_step=self.async_step_trigger_threshold, counter_step=self.async_step_trigger_counter, state_change_step=self.async_step_trigger_state_change, runtime_step=self.async_step_trigger_runtime, compound_step=self.async_step_compound_logic, ) async def async_step_trigger_threshold(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Configure threshold trigger.""" self._on_cancel = lambda: self.async_step_trigger_type() return await self._trigger_threshold_config( user_input, step_id="trigger_threshold", on_complete=self._save_task_and_return, ) async def async_step_trigger_counter(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Configure counter trigger.""" self._on_cancel = lambda: self.async_step_trigger_type() return await self._trigger_counter_config( user_input, step_id="trigger_counter", on_complete=self._save_task_and_return, ) async def async_step_trigger_state_change(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Configure state change trigger.""" self._on_cancel = lambda: self.async_step_trigger_type() return await self._trigger_state_change_config( user_input, step_id="trigger_state_change", on_complete=self._save_task_and_return, ) async def async_step_trigger_runtime(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Configure runtime trigger.""" self._on_cancel = lambda: self.async_step_trigger_type() return await self._trigger_runtime_config( user_input, step_id="trigger_runtime", on_complete=self._save_task_and_return, ) # --- Compound Trigger Steps --- async def async_step_compound_logic(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Select compound trigger logic.""" self._on_cancel = lambda: self.async_step_trigger_type() return await self._trigger_compound_logic( user_input, step_id="compound_logic", next_step=self.async_step_compound_condition_entity, ) async def async_step_compound_condition_entity(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Select entity for compound condition.""" if getattr(self, "_compound_conditions", []): self._on_cancel = lambda: self.async_step_compound_review() else: self._on_cancel = lambda: self.async_step_compound_logic() return await self._trigger_compound_condition_entity( user_input, step_id="compound_condition_entity", next_step=self.async_step_compound_condition_type, ) async def async_step_compound_condition_type(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Select trigger type for compound condition.""" self._on_cancel = lambda: self.async_step_compound_condition_entity() return await self._trigger_compound_condition_type( user_input, step_id="compound_condition_type", threshold_step=self.async_step_compound_condition_threshold, counter_step=self.async_step_compound_condition_counter, state_change_step=self.async_step_compound_condition_state_change, runtime_step=self.async_step_compound_condition_runtime, ) async def async_step_compound_condition_threshold(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Configure threshold for compound condition.""" self._on_cancel = lambda: self.async_step_compound_condition_type() return await self._trigger_compound_condition_config( user_input, "threshold", step_id="compound_condition_threshold", on_complete=self.async_step_compound_review, ) async def async_step_compound_condition_counter(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Configure counter for compound condition.""" self._on_cancel = lambda: self.async_step_compound_condition_type() return await self._trigger_compound_condition_config( user_input, "counter", step_id="compound_condition_counter", on_complete=self.async_step_compound_review, ) async def async_step_compound_condition_state_change(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Configure state_change for compound condition.""" self._on_cancel = lambda: self.async_step_compound_condition_type() return await self._trigger_compound_condition_config( user_input, "state_change", step_id="compound_condition_state_change", on_complete=self.async_step_compound_review, ) async def async_step_compound_condition_runtime(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Configure runtime for compound condition.""" self._on_cancel = lambda: self.async_step_compound_condition_type() return await self._trigger_compound_condition_config( user_input, "runtime", step_id="compound_condition_runtime", on_complete=self.async_step_compound_review, ) async def async_step_compound_review(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Review compound trigger conditions.""" self._on_cancel = lambda: self.async_step_compound_logic() return await self._trigger_compound_review( user_input, step_id="compound_review", add_condition_step=self.async_step_compound_condition_entity, on_complete=self._save_task_and_return, ) # --- Manual & Finish --- async def async_step_manual(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Configure manual schedule.""" return await self._schedule_manual( user_input, step_id="manual", on_go_back=self.async_step_add_task, on_complete=self._save_task_and_return, ) async def async_step_finish(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Finish the object setup and create the config entry.""" if not self._tasks: # No tasks defined: go back to task menu with error return self.async_show_menu( step_id="task_menu", menu_options=["add_task", "finish"], description_placeholders={ "object_name": self._object_data.get(CONF_OBJECT_NAME, ""), "task_count": "0", }, ) object_name = self._object_data.get(CONF_OBJECT_NAME, "Unknown") object_slug = slugify_object_name(object_name) await self.async_set_unique_id(f"maintenance_supporter_{object_slug}") self._abort_if_unique_id_configured() # Add task_ids to object self._object_data["task_ids"] = list(self._tasks.keys()) return self.async_create_entry( title=object_name, data={ CONF_OBJECT: self._object_data, # Store recurrence in the canonical nested `schedule` shape. CONF_TASKS: {tid: normalize_task_storage(td) for tid, td in self._tasks.items()}, }, ) def _save_task_and_return(self) -> ConfigFlowResult: """Save the current task and return to task menu.""" from .config_flow_schedule import build_new_task_record task_id = self._current_task.get("id", uuid4().hex) task_data = build_new_task_record( self._current_task, task_id=task_id, object_id=self._object_data.get("id", ""), hass=self.hass, # No entry (and thus no Store) exists yet during setup — history and # a backdated last_performed must ride entry.data. seed_history=True, include_last_performed=True, ) self._tasks[task_id] = task_data self._current_task = {} _LOGGER.debug("Task saved: %s (total: %d)", task_data["name"], len(self._tasks)) # Return to task menu using show_menu (not await) return self.async_show_menu( step_id="task_menu", menu_options=["add_task", "finish"], description_placeholders={ "object_name": self._object_data.get(CONF_OBJECT_NAME, ""), "task_count": str(len(self._tasks)), }, ) @staticmethod @callback def async_get_options_flow( config_entry: ConfigEntry, ) -> OptionsFlow: """Get the options flow for this handler.""" from .config_flow_options_global import GlobalOptionsFlow from .config_flow_options_task import MaintenanceOptionsFlow if config_entry.unique_id == GLOBAL_UNIQUE_ID: return GlobalOptionsFlow() return MaintenanceOptionsFlow()