Coverage for custom_components/autoarm/config_flow.py: 100%
79 statements
« prev ^ index » next coverage.py v7.10.6, created at 2026-02-17 01:14 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2026-02-17 01:14 +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 ALARM_STATES,
24 CONF_ALARM_PANEL,
25 CONF_CALENDAR_CONTROL,
26 CONF_CALENDAR_NO_EVENT,
27 CONF_CALENDARS,
28 CONF_DAY,
29 CONF_DIURNAL,
30 CONF_EARLIEST,
31 CONF_LATEST,
32 CONF_NIGHT,
33 CONF_NOTIFY,
34 CONF_OCCUPANCY,
35 CONF_OCCUPANCY_DEFAULT,
36 CONF_SUNRISE,
37 CONF_SUNSET,
38 DOMAIN,
39 NO_CAL_EVENT_OPTIONS,
40 NOTIFY_COMMON,
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_NOTIFY_ACTION = "notify_action"
49CONF_NOTIFY_TARGETS = "notify_targets"
50CONF_NOTIFY_ENABLED = "notify_enabled"
51CONF_SUNRISE_EARLIEST = "sunrise_earliest"
52CONF_SUNRISE_LATEST = "sunrise_latest"
53CONF_SUNSET_EARLIEST = "sunset_earliest"
54CONF_SUNSET_LATEST = "sunset_latest"
57def _time_to_str(t: dt.time | None) -> str | None:
58 """Convert a datetime.time to HH:MM:SS string for ConfigEntry storage."""
59 return t.isoformat() if t else None
62DEFAULT_NOTIFY_ACTION = "notify.send_message"
64DEFAULT_OPTIONS: dict[str, Any] = {
65 CONF_CALENDAR_ENTITIES: [],
66 CONF_PERSON_ENTITIES: [],
67 CONF_OCCUPANCY_DEFAULT_DAY: "armed_home",
68 CONF_OCCUPANCY_DEFAULT_NIGHT: None,
69 CONF_NO_EVENT_MODE: "auto",
70 CONF_NOTIFY_ENABLED: True,
71 CONF_NOTIFY_ACTION: DEFAULT_NOTIFY_ACTION,
72 CONF_NOTIFY_TARGETS: [],
73 CONF_SUNRISE_EARLIEST: None,
74 CONF_SUNRISE_LATEST: None,
75 CONF_SUNSET_EARLIEST: None,
76 CONF_SUNSET_LATEST: None,
77}
80class AutoArmConfigFlow(ConfigFlow, domain=DOMAIN):
81 """Handle a config flow for Auto Arm."""
83 VERSION = 1
85 def __init__(self) -> None:
86 """Initialize the config flow."""
87 self._alarm_panel: str = ""
88 self._calendar_entities: list[str] = []
90 async def async_step_user(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
91 """Handle the alarm panel selection step."""
92 if user_input is not None:
93 self._alarm_panel = user_input[CONF_ALARM_PANEL]
94 return await self.async_step_calendars()
96 return self.async_show_form(
97 step_id="user",
98 data_schema=vol.Schema({
99 vol.Required(CONF_ALARM_PANEL): EntitySelector(EntitySelectorConfig(domain="alarm_control_panel")),
100 }),
101 )
103 async def async_step_calendars(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
104 """Handle the calendar entity selection step."""
105 if user_input is not None:
106 self._calendar_entities = user_input.get(CONF_CALENDAR_ENTITIES, [])
107 return await self.async_step_persons()
109 return self.async_show_form(
110 step_id="calendars",
111 data_schema=vol.Schema({
112 vol.Optional(CONF_CALENDAR_ENTITIES, default=[]): EntitySelector(
113 EntitySelectorConfig(domain="calendar", multiple=True)
114 ),
115 }),
116 )
118 async def async_step_persons(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
119 """Handle the person entity selection step."""
120 if user_input is not None:
121 await self.async_set_unique_id(DOMAIN)
122 self._abort_if_unique_id_configured()
124 options = {
125 CONF_CALENDAR_ENTITIES: self._calendar_entities,
126 CONF_PERSON_ENTITIES: user_input.get(CONF_PERSON_ENTITIES, []),
127 CONF_OCCUPANCY_DEFAULT_DAY: DEFAULT_OPTIONS[CONF_OCCUPANCY_DEFAULT_DAY],
128 CONF_OCCUPANCY_DEFAULT_NIGHT: DEFAULT_OPTIONS[CONF_OCCUPANCY_DEFAULT_NIGHT],
129 CONF_NO_EVENT_MODE: DEFAULT_OPTIONS[CONF_NO_EVENT_MODE],
130 CONF_NOTIFY_ACTION: DEFAULT_NOTIFY_ACTION,
131 CONF_NOTIFY_ENABLED: True,
132 CONF_NOTIFY_TARGETS: [],
133 CONF_SUNRISE_EARLIEST: None,
134 CONF_SUNRISE_LATEST: None,
135 CONF_SUNSET_EARLIEST: None,
136 CONF_SUNSET_LATEST: None,
137 }
139 return self.async_create_entry(
140 title="Auto Arm",
141 data={CONF_ALARM_PANEL: self._alarm_panel},
142 options=options,
143 )
145 return self.async_show_form(
146 step_id="persons",
147 data_schema=vol.Schema({
148 vol.Optional(CONF_PERSON_ENTITIES, default=[]): EntitySelector(
149 EntitySelectorConfig(domain="person", multiple=True)
150 ),
151 }),
152 )
154 async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
155 """Handle import from YAML configuration."""
156 await self.async_set_unique_id(DOMAIN)
157 self._abort_if_unique_id_configured()
159 alarm_panel_config = import_data.get(CONF_ALARM_PANEL, {})
160 alarm_panel = alarm_panel_config.get(CONF_ENTITY_ID, "") if isinstance(alarm_panel_config, dict) else ""
162 occupancy_config = import_data.get(CONF_OCCUPANCY, {})
163 person_entities = occupancy_config.get(CONF_ENTITY_ID, [])
164 occupancy_defaults = occupancy_config.get(CONF_OCCUPANCY_DEFAULT, {})
166 calendar_config = import_data.get(CONF_CALENDAR_CONTROL, {})
167 calendar_entities = [cal[CONF_ENTITY_ID] for cal in calendar_config.get(CONF_CALENDARS, []) if CONF_ENTITY_ID in cal]
168 no_event_mode = calendar_config.get(CONF_CALENDAR_NO_EVENT, DEFAULT_OPTIONS[CONF_NO_EVENT_MODE])
170 notify_config = import_data.get(CONF_NOTIFY, {})
171 notify_action = notify_config.get(NOTIFY_COMMON, {}).get(CONF_SERVICE, DEFAULT_NOTIFY_ACTION)
172 notify_enabled: bool = notify_config.get(NOTIFY_COMMON, {}).get(CONF_ENABLED, True)
174 diurnal_config = import_data.get(CONF_DIURNAL, {})
175 sunrise_config = diurnal_config.get(CONF_SUNRISE, {}) if diurnal_config else {}
176 sunset_config = diurnal_config.get(CONF_SUNSET, {}) if diurnal_config else {}
178 options = {
179 CONF_CALENDAR_ENTITIES: calendar_entities,
180 CONF_PERSON_ENTITIES: person_entities,
181 CONF_OCCUPANCY_DEFAULT_DAY: occupancy_defaults.get(CONF_DAY, DEFAULT_OPTIONS[CONF_OCCUPANCY_DEFAULT_DAY]),
182 CONF_OCCUPANCY_DEFAULT_NIGHT: occupancy_defaults.get(CONF_NIGHT),
183 CONF_NO_EVENT_MODE: no_event_mode,
184 CONF_NOTIFY_ENABLED: notify_enabled,
185 CONF_NOTIFY_ACTION: notify_action,
186 CONF_NOTIFY_TARGETS: [],
187 CONF_SUNRISE_EARLIEST: _time_to_str(sunrise_config.get(CONF_EARLIEST)),
188 CONF_SUNRISE_LATEST: _time_to_str(sunrise_config.get(CONF_LATEST)),
189 CONF_SUNSET_EARLIEST: _time_to_str(sunset_config.get(CONF_EARLIEST)),
190 CONF_SUNSET_LATEST: _time_to_str(sunset_config.get(CONF_LATEST)),
191 }
193 return self.async_create_entry(
194 title="Auto Arm",
195 data={CONF_ALARM_PANEL: alarm_panel},
196 options=options,
197 )
199 @staticmethod
200 def async_get_options_flow(config_entry: ConfigEntry) -> "AutoArmOptionsFlow": # noqa: ARG004
201 """Get the options flow for this handler."""
202 return AutoArmOptionsFlow()
205class AutoArmOptionsFlow(OptionsFlow):
206 """Handle options flow for Auto Arm."""
208 async def async_step_init(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
209 """Manage the options."""
210 if user_input is not None:
211 # Flatten section dicts into top-level options
212 data = {k: v for k, v in user_input.items() if not isinstance(v, dict)}
213 for v in user_input.values():
214 if isinstance(v, dict):
215 data.update(v)
216 return self.async_create_entry(title="", data=data)
218 options = self.config_entry.options
219 notify_services = sorted(f"notify.{service}" for service in self.hass.services.async_services().get("notify", {}))
221 return self.async_show_form(
222 step_id="init",
223 data_schema=vol.Schema({
224 vol.Optional(
225 CONF_CALENDAR_ENTITIES,
226 default=options.get(CONF_CALENDAR_ENTITIES, []),
227 ): EntitySelector(EntitySelectorConfig(domain="calendar", multiple=True)),
228 vol.Optional(
229 CONF_PERSON_ENTITIES,
230 default=options.get(CONF_PERSON_ENTITIES, []),
231 ): EntitySelector(EntitySelectorConfig(domain="person", multiple=True)),
232 vol.Optional(
233 CONF_OCCUPANCY_DEFAULT_DAY,
234 default=options.get(CONF_OCCUPANCY_DEFAULT_DAY, "armed_home"),
235 ): SelectSelector(
236 SelectSelectorConfig(
237 options=ALARM_STATES,
238 mode=SelectSelectorMode.DROPDOWN,
239 )
240 ),
241 vol.Optional(
242 CONF_OCCUPANCY_DEFAULT_NIGHT,
243 description={"suggested_value": options.get(CONF_OCCUPANCY_DEFAULT_NIGHT, "armed_night")},
244 ): SelectSelector(
245 SelectSelectorConfig(
246 options=ALARM_STATES,
247 mode=SelectSelectorMode.DROPDOWN,
248 )
249 ),
250 vol.Optional(
251 CONF_NO_EVENT_MODE,
252 default=options.get(CONF_NO_EVENT_MODE, "auto"),
253 ): SelectSelector(
254 SelectSelectorConfig(
255 options=NO_CAL_EVENT_OPTIONS,
256 mode=SelectSelectorMode.DROPDOWN,
257 )
258 ),
259 vol.Required("notify_options"): section(
260 vol.Schema({
261 vol.Required(
262 CONF_NOTIFY_ENABLED,
263 default=options.get(CONF_NOTIFY_ENABLED, True),
264 ): BooleanSelector(),
265 vol.Optional(
266 CONF_NOTIFY_ACTION,
267 default=options.get(CONF_NOTIFY_ACTION, DEFAULT_NOTIFY_ACTION),
268 ): SelectSelector(
269 SelectSelectorConfig(
270 options=notify_services,
271 multiple=False,
272 mode=SelectSelectorMode.DROPDOWN,
273 )
274 ),
275 vol.Optional(
276 CONF_NOTIFY_TARGETS,
277 default=options.get(CONF_NOTIFY_TARGETS, []),
278 ): TextSelector(TextSelectorConfig(multiple=True)),
279 }),
280 {"collapsed": True},
281 ),
282 vol.Required("sunrise_options"): section(
283 vol.Schema({
284 vol.Optional(
285 CONF_SUNRISE_EARLIEST,
286 description={"suggested_value": options.get(CONF_SUNRISE_EARLIEST)},
287 ): TimeSelector(),
288 vol.Optional(
289 CONF_SUNRISE_LATEST,
290 description={"suggested_value": options.get(CONF_SUNRISE_LATEST)},
291 ): TimeSelector(),
292 }),
293 {"collapsed": True},
294 ),
295 vol.Required("sunset_options"): section(
296 vol.Schema({
297 vol.Optional(
298 CONF_SUNSET_EARLIEST,
299 description={"suggested_value": options.get(CONF_SUNSET_EARLIEST)},
300 ): TimeSelector(),
301 vol.Optional(
302 CONF_SUNSET_LATEST,
303 description={"suggested_value": options.get(CONF_SUNSET_LATEST)},
304 ): TimeSelector(),
305 }),
306 {"collapsed": True},
307 ),
308 }),
309 )