Coverage for custom_components/autoarm/config_flow.py: 100%
84 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-09-09 06:20 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-09-09 06:20 +0000
1"""Config flow for Auto Arm integration."""
3import datetime as dt
4from typing import Any
6import voluptuous as vol
7from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow
8from homeassistant.const import CONF_ENABLED, CONF_ENTITY_ID, CONF_SERVICE
9from homeassistant.data_entry_flow import section
10from homeassistant.helpers.selector import (
11 BooleanSelector,
12 EntitySelector,
13 EntitySelectorConfig,
14 SelectSelector,
15 SelectSelectorConfig,
16 SelectSelectorMode,
17 TextSelector,
18 TextSelectorConfig,
19 TimeSelector,
20)
22from .const import (
23 CONF_ALARM_PANEL,
24 CONF_CALENDAR_CONTROL,
25 CONF_CALENDAR_NO_EVENT,
26 CONF_CALENDARS,
27 CONF_DAY,
28 CONF_DIURNAL,
29 CONF_EARLIEST,
30 CONF_LATEST,
31 CONF_NIGHT,
32 CONF_NOTIFY,
33 CONF_OCCUPANCY,
34 CONF_OCCUPANCY_DEFAULT,
35 CONF_SUNRISE,
36 CONF_SUNSET,
37 DOMAIN,
38 NO_CAL_EVENT_OPTIONS,
39 NOTIFY_COMMON,
40 PUBLIC_ALARM_STATES,
41)
43CONF_CALENDAR_ENTITIES = "calendar_entities"
44CONF_PERSON_ENTITIES = "person_entities"
45CONF_OCCUPANCY_DEFAULT_DAY = "occupancy_default_day"
46CONF_OCCUPANCY_DEFAULT_NIGHT = "occupancy_default_night"
47CONF_NO_EVENT_MODE = "no_event_mode"
48CONF_CALENDAR_OCCUPANCY_OVERRIDE_STATES = "calendar_occupancy_override_states"
49CONF_NOTIFY_ACTION = "notify_action"
50CONF_NOTIFY_TARGETS = "notify_targets"
51CONF_NOTIFY_ENABLED = "notify_enabled"
52CONF_SUNRISE_EARLIEST = "sunrise_earliest"
53CONF_SUNRISE_LATEST = "sunrise_latest"
54CONF_SUNSET_EARLIEST = "sunset_earliest"
55CONF_SUNSET_LATEST = "sunset_latest"
57DEFAULT_CALENDAR_OCCUPANCY_OVERRIDE_STATES: list[str] = ["disarmed", "armed_home", "armed_night", "armed_away"]
60def _time_to_str(t: dt.time | None) -> str | None:
61 """Convert a datetime.time to HH:MM:SS string for ConfigEntry storage."""
62 return t.isoformat() if t else None
65DEFAULT_NOTIFY_ACTION = "notify.send_message"
67DEFAULT_OPTIONS: dict[str, Any] = {
68 CONF_CALENDAR_ENTITIES: [],
69 CONF_PERSON_ENTITIES: [],
70 CONF_OCCUPANCY_DEFAULT_DAY: "armed_home",
71 CONF_OCCUPANCY_DEFAULT_NIGHT: None,
72 CONF_NO_EVENT_MODE: "auto",
73 CONF_CALENDAR_OCCUPANCY_OVERRIDE_STATES: DEFAULT_CALENDAR_OCCUPANCY_OVERRIDE_STATES,
74 CONF_NOTIFY_ENABLED: True,
75 CONF_NOTIFY_ACTION: DEFAULT_NOTIFY_ACTION,
76 CONF_NOTIFY_TARGETS: [],
77 CONF_SUNRISE_EARLIEST: None,
78 CONF_SUNRISE_LATEST: None,
79 CONF_SUNSET_EARLIEST: None,
80 CONF_SUNSET_LATEST: None,
81}
84class AutoArmConfigFlow(ConfigFlow, domain=DOMAIN):
85 """Handle a config flow for Auto Arm."""
87 VERSION = 1
89 def __init__(self) -> None:
90 """Initialize the config flow."""
91 self._alarm_panel: str = ""
92 self._calendar_entities: list[str] = []
94 async def async_step_user(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
95 """Handle the alarm panel selection step."""
96 if user_input is not None:
97 self._alarm_panel = user_input[CONF_ALARM_PANEL]
98 return await self.async_step_calendars()
100 return self.async_show_form(
101 step_id="user",
102 data_schema=vol.Schema({
103 vol.Required(CONF_ALARM_PANEL): EntitySelector(EntitySelectorConfig(domain="alarm_control_panel")),
104 }),
105 )
107 async def async_step_calendars(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
108 """Handle the calendar entity selection step."""
109 if user_input is not None:
110 self._calendar_entities = user_input.get(CONF_CALENDAR_ENTITIES, [])
111 return await self.async_step_persons()
113 return self.async_show_form(
114 step_id="calendars",
115 data_schema=vol.Schema({
116 vol.Optional(CONF_CALENDAR_ENTITIES, default=[]): EntitySelector(
117 EntitySelectorConfig(domain="calendar", multiple=True)
118 ),
119 }),
120 )
122 async def async_step_persons(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
123 """Handle the person entity selection step."""
124 if user_input is not None:
125 await self.async_set_unique_id(DOMAIN)
126 self._abort_if_unique_id_configured()
128 options = {
129 CONF_CALENDAR_ENTITIES: self._calendar_entities,
130 CONF_PERSON_ENTITIES: user_input.get(CONF_PERSON_ENTITIES, []),
131 CONF_OCCUPANCY_DEFAULT_DAY: DEFAULT_OPTIONS[CONF_OCCUPANCY_DEFAULT_DAY],
132 CONF_OCCUPANCY_DEFAULT_NIGHT: DEFAULT_OPTIONS[CONF_OCCUPANCY_DEFAULT_NIGHT],
133 CONF_NO_EVENT_MODE: DEFAULT_OPTIONS[CONF_NO_EVENT_MODE],
134 CONF_CALENDAR_OCCUPANCY_OVERRIDE_STATES: DEFAULT_CALENDAR_OCCUPANCY_OVERRIDE_STATES,
135 CONF_NOTIFY_ACTION: DEFAULT_NOTIFY_ACTION,
136 CONF_NOTIFY_ENABLED: True,
137 CONF_NOTIFY_TARGETS: [],
138 CONF_SUNRISE_EARLIEST: None,
139 CONF_SUNRISE_LATEST: None,
140 CONF_SUNSET_EARLIEST: None,
141 CONF_SUNSET_LATEST: None,
142 }
144 return self.async_create_entry(
145 title="Auto Arm",
146 data={CONF_ALARM_PANEL: self._alarm_panel},
147 options=options,
148 )
150 return self.async_show_form(
151 step_id="persons",
152 data_schema=vol.Schema({
153 vol.Optional(CONF_PERSON_ENTITIES, default=[]): EntitySelector(
154 EntitySelectorConfig(domain="person", multiple=True)
155 ),
156 }),
157 )
159 async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
160 """Handle import from YAML configuration."""
161 await self.async_set_unique_id(DOMAIN)
162 self._abort_if_unique_id_configured()
164 alarm_panel_config = import_data.get(CONF_ALARM_PANEL, {})
165 alarm_panel = alarm_panel_config.get(CONF_ENTITY_ID, "") if isinstance(alarm_panel_config, dict) else ""
167 occupancy_config = import_data.get(CONF_OCCUPANCY, {})
168 person_entities = occupancy_config.get(CONF_ENTITY_ID, [])
169 occupancy_defaults = occupancy_config.get(CONF_OCCUPANCY_DEFAULT, {})
171 calendar_config = import_data.get(CONF_CALENDAR_CONTROL, {})
172 calendar_entities = [cal[CONF_ENTITY_ID] for cal in calendar_config.get(CONF_CALENDARS, []) if CONF_ENTITY_ID in cal]
173 no_event_mode = calendar_config.get(CONF_CALENDAR_NO_EVENT, DEFAULT_OPTIONS[CONF_NO_EVENT_MODE])
175 notify_config = import_data.get(CONF_NOTIFY, {})
176 notify_action = notify_config.get(NOTIFY_COMMON, {}).get(CONF_SERVICE, DEFAULT_NOTIFY_ACTION)
177 notify_enabled: bool = notify_config.get(NOTIFY_COMMON, {}).get(CONF_ENABLED, True)
179 diurnal_config = import_data.get(CONF_DIURNAL, {})
180 sunrise_config = diurnal_config.get(CONF_SUNRISE, {}) if diurnal_config else {}
181 sunset_config = diurnal_config.get(CONF_SUNSET, {}) if diurnal_config else {}
183 options = {
184 CONF_CALENDAR_ENTITIES: calendar_entities,
185 CONF_PERSON_ENTITIES: person_entities,
186 CONF_OCCUPANCY_DEFAULT_DAY: occupancy_defaults.get(CONF_DAY, DEFAULT_OPTIONS[CONF_OCCUPANCY_DEFAULT_DAY]),
187 CONF_OCCUPANCY_DEFAULT_NIGHT: occupancy_defaults.get(CONF_NIGHT),
188 CONF_NO_EVENT_MODE: no_event_mode,
189 CONF_NOTIFY_ENABLED: notify_enabled,
190 CONF_NOTIFY_ACTION: notify_action,
191 CONF_NOTIFY_TARGETS: [],
192 CONF_SUNRISE_EARLIEST: _time_to_str(sunrise_config.get(CONF_EARLIEST)),
193 CONF_SUNRISE_LATEST: _time_to_str(sunrise_config.get(CONF_LATEST)),
194 CONF_SUNSET_EARLIEST: _time_to_str(sunset_config.get(CONF_EARLIEST)),
195 CONF_SUNSET_LATEST: _time_to_str(sunset_config.get(CONF_LATEST)),
196 }
198 return self.async_create_entry(
199 title="Auto Arm",
200 data={CONF_ALARM_PANEL: alarm_panel},
201 options=options,
202 )
204 @staticmethod
205 def async_get_options_flow(config_entry: ConfigEntry) -> "AutoArmOptionsFlow":
206 """Get the options flow for this handler."""
207 return AutoArmOptionsFlow()
210class AutoArmOptionsFlow(OptionsFlow):
211 """Handle options flow for Auto Arm."""
213 async def async_step_init(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
214 """Manage the options."""
215 if user_input is not None:
216 # Flatten section dicts into top-level options
217 data = {k: v for k, v in user_input.items() if not isinstance(v, dict)}
218 for v in user_input.values():
219 if isinstance(v, dict):
220 data.update(v)
222 # The alarm panel entity lives in config_entry.data, not options.
223 alarm_panel = data.pop(CONF_ALARM_PANEL)
224 if alarm_panel != self.config_entry.data.get(CONF_ALARM_PANEL):
225 self.hass.config_entries.async_update_entry(
226 self.config_entry,
227 data={**self.config_entry.data, CONF_ALARM_PANEL: alarm_panel},
228 )
230 return self.async_create_entry(title="", data=data)
232 options = self.config_entry.options
233 notify_services = sorted(f"notify.{service}" for service in self.hass.services.async_services().get("notify", {}))
235 return self.async_show_form(
236 step_id="init",
237 data_schema=vol.Schema({
238 vol.Required(
239 CONF_ALARM_PANEL,
240 default=self.config_entry.data.get(CONF_ALARM_PANEL, ""),
241 ): EntitySelector(EntitySelectorConfig(domain="alarm_control_panel")),
242 vol.Optional(
243 CONF_CALENDAR_ENTITIES,
244 default=options.get(CONF_CALENDAR_ENTITIES, []),
245 ): EntitySelector(EntitySelectorConfig(domain="calendar", multiple=True)),
246 vol.Optional(
247 CONF_PERSON_ENTITIES,
248 default=options.get(CONF_PERSON_ENTITIES, []),
249 ): EntitySelector(EntitySelectorConfig(domain="person", multiple=True)),
250 vol.Optional(
251 CONF_OCCUPANCY_DEFAULT_DAY,
252 default=options.get(CONF_OCCUPANCY_DEFAULT_DAY, "armed_home"),
253 ): SelectSelector(
254 SelectSelectorConfig(
255 options=PUBLIC_ALARM_STATES,
256 mode=SelectSelectorMode.DROPDOWN,
257 )
258 ),
259 vol.Optional(
260 CONF_OCCUPANCY_DEFAULT_NIGHT,
261 description={"suggested_value": options.get(CONF_OCCUPANCY_DEFAULT_NIGHT, "armed_night")},
262 ): SelectSelector(
263 SelectSelectorConfig(
264 options=PUBLIC_ALARM_STATES,
265 mode=SelectSelectorMode.DROPDOWN,
266 )
267 ),
268 vol.Optional(
269 CONF_NO_EVENT_MODE,
270 default=options.get(CONF_NO_EVENT_MODE, "auto"),
271 ): SelectSelector(
272 SelectSelectorConfig(
273 options=NO_CAL_EVENT_OPTIONS,
274 mode=SelectSelectorMode.DROPDOWN,
275 )
276 ),
277 vol.Required("calendar_options"): section(
278 vol.Schema({
279 vol.Optional(
280 CONF_CALENDAR_OCCUPANCY_OVERRIDE_STATES,
281 default=options.get(
282 CONF_CALENDAR_OCCUPANCY_OVERRIDE_STATES, DEFAULT_CALENDAR_OCCUPANCY_OVERRIDE_STATES
283 ),
284 ): SelectSelector(
285 SelectSelectorConfig(
286 options=PUBLIC_ALARM_STATES,
287 multiple=True,
288 mode=SelectSelectorMode.LIST,
289 )
290 ),
291 }),
292 {"collapsed": True},
293 ),
294 vol.Required("notify_options"): section(
295 vol.Schema({
296 vol.Required(
297 CONF_NOTIFY_ENABLED,
298 default=options.get(CONF_NOTIFY_ENABLED, True),
299 ): BooleanSelector(),
300 vol.Optional(
301 CONF_NOTIFY_ACTION,
302 default=options.get(CONF_NOTIFY_ACTION, DEFAULT_NOTIFY_ACTION),
303 ): SelectSelector(
304 SelectSelectorConfig(
305 options=notify_services,
306 multiple=False,
307 mode=SelectSelectorMode.DROPDOWN,
308 )
309 ),
310 vol.Optional(
311 CONF_NOTIFY_TARGETS,
312 default=options.get(CONF_NOTIFY_TARGETS, []),
313 ): TextSelector(TextSelectorConfig(multiple=True)),
314 }),
315 {"collapsed": True},
316 ),
317 vol.Required("sunrise_options"): section(
318 vol.Schema({
319 vol.Optional(
320 CONF_SUNRISE_EARLIEST,
321 description={"suggested_value": options.get(CONF_SUNRISE_EARLIEST)},
322 ): TimeSelector(),
323 vol.Optional(
324 CONF_SUNRISE_LATEST,
325 description={"suggested_value": options.get(CONF_SUNRISE_LATEST)},
326 ): TimeSelector(),
327 }),
328 {"collapsed": True},
329 ),
330 vol.Required("sunset_options"): section(
331 vol.Schema({
332 vol.Optional(
333 CONF_SUNSET_EARLIEST,
334 description={"suggested_value": options.get(CONF_SUNSET_EARLIEST)},
335 ): TimeSelector(),
336 vol.Optional(
337 CONF_SUNSET_LATEST,
338 description={"suggested_value": options.get(CONF_SUNSET_LATEST)},
339 ): TimeSelector(),
340 }),
341 {"collapsed": True},
342 ),
343 }),
344 )