Coverage for custom_components/autoarm/config_flow.py: 100%
81 statements
« prev ^ index » next coverage.py v7.10.6, created at 2026-04-27 16:32 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2026-04-27 16:32 +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": # noqa: ARG004
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)
221 return self.async_create_entry(title="", data=data)
223 options = self.config_entry.options
224 notify_services = sorted(f"notify.{service}" for service in self.hass.services.async_services().get("notify", {}))
226 return self.async_show_form(
227 step_id="init",
228 data_schema=vol.Schema({
229 vol.Optional(
230 CONF_CALENDAR_ENTITIES,
231 default=options.get(CONF_CALENDAR_ENTITIES, []),
232 ): EntitySelector(EntitySelectorConfig(domain="calendar", multiple=True)),
233 vol.Optional(
234 CONF_PERSON_ENTITIES,
235 default=options.get(CONF_PERSON_ENTITIES, []),
236 ): EntitySelector(EntitySelectorConfig(domain="person", multiple=True)),
237 vol.Optional(
238 CONF_OCCUPANCY_DEFAULT_DAY,
239 default=options.get(CONF_OCCUPANCY_DEFAULT_DAY, "armed_home"),
240 ): SelectSelector(
241 SelectSelectorConfig(
242 options=PUBLIC_ALARM_STATES,
243 mode=SelectSelectorMode.DROPDOWN,
244 )
245 ),
246 vol.Optional(
247 CONF_OCCUPANCY_DEFAULT_NIGHT,
248 description={"suggested_value": options.get(CONF_OCCUPANCY_DEFAULT_NIGHT, "armed_night")},
249 ): SelectSelector(
250 SelectSelectorConfig(
251 options=PUBLIC_ALARM_STATES,
252 mode=SelectSelectorMode.DROPDOWN,
253 )
254 ),
255 vol.Optional(
256 CONF_NO_EVENT_MODE,
257 default=options.get(CONF_NO_EVENT_MODE, "auto"),
258 ): SelectSelector(
259 SelectSelectorConfig(
260 options=NO_CAL_EVENT_OPTIONS,
261 mode=SelectSelectorMode.DROPDOWN,
262 )
263 ),
264 vol.Required("calendar_options"): section(
265 vol.Schema({
266 vol.Optional(
267 CONF_CALENDAR_OCCUPANCY_OVERRIDE_STATES,
268 default=options.get(
269 CONF_CALENDAR_OCCUPANCY_OVERRIDE_STATES, DEFAULT_CALENDAR_OCCUPANCY_OVERRIDE_STATES
270 ),
271 ): SelectSelector(
272 SelectSelectorConfig(
273 options=PUBLIC_ALARM_STATES,
274 multiple=True,
275 mode=SelectSelectorMode.LIST,
276 )
277 ),
278 }),
279 {"collapsed": True},
280 ),
281 vol.Required("notify_options"): section(
282 vol.Schema({
283 vol.Required(
284 CONF_NOTIFY_ENABLED,
285 default=options.get(CONF_NOTIFY_ENABLED, True),
286 ): BooleanSelector(),
287 vol.Optional(
288 CONF_NOTIFY_ACTION,
289 default=options.get(CONF_NOTIFY_ACTION, DEFAULT_NOTIFY_ACTION),
290 ): SelectSelector(
291 SelectSelectorConfig(
292 options=notify_services,
293 multiple=False,
294 mode=SelectSelectorMode.DROPDOWN,
295 )
296 ),
297 vol.Optional(
298 CONF_NOTIFY_TARGETS,
299 default=options.get(CONF_NOTIFY_TARGETS, []),
300 ): TextSelector(TextSelectorConfig(multiple=True)),
301 }),
302 {"collapsed": True},
303 ),
304 vol.Required("sunrise_options"): section(
305 vol.Schema({
306 vol.Optional(
307 CONF_SUNRISE_EARLIEST,
308 description={"suggested_value": options.get(CONF_SUNRISE_EARLIEST)},
309 ): TimeSelector(),
310 vol.Optional(
311 CONF_SUNRISE_LATEST,
312 description={"suggested_value": options.get(CONF_SUNRISE_LATEST)},
313 ): TimeSelector(),
314 }),
315 {"collapsed": True},
316 ),
317 vol.Required("sunset_options"): section(
318 vol.Schema({
319 vol.Optional(
320 CONF_SUNSET_EARLIEST,
321 description={"suggested_value": options.get(CONF_SUNSET_EARLIEST)},
322 ): TimeSelector(),
323 vol.Optional(
324 CONF_SUNSET_LATEST,
325 description={"suggested_value": options.get(CONF_SUNSET_LATEST)},
326 ): TimeSelector(),
327 }),
328 {"collapsed": True},
329 ),
330 }),
331 )