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

1"""Config flow for Auto Arm integration.""" 

2 

3import datetime as dt 

4from typing import Any 

5 

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) 

21 

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) 

42 

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" 

56 

57DEFAULT_CALENDAR_OCCUPANCY_OVERRIDE_STATES: list[str] = ["disarmed", "armed_home", "armed_night", "armed_away"] 

58 

59 

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 

63 

64 

65DEFAULT_NOTIFY_ACTION = "notify.send_message" 

66 

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} 

82 

83 

84class AutoArmConfigFlow(ConfigFlow, domain=DOMAIN): 

85 """Handle a config flow for Auto Arm.""" 

86 

87 VERSION = 1 

88 

89 def __init__(self) -> None: 

90 """Initialize the config flow.""" 

91 self._alarm_panel: str = "" 

92 self._calendar_entities: list[str] = [] 

93 

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() 

99 

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 ) 

106 

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() 

112 

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 ) 

121 

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() 

127 

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 } 

143 

144 return self.async_create_entry( 

145 title="Auto Arm", 

146 data={CONF_ALARM_PANEL: self._alarm_panel}, 

147 options=options, 

148 ) 

149 

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 ) 

158 

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() 

163 

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 "" 

166 

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, {}) 

170 

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]) 

174 

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) 

178 

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 {} 

182 

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 } 

197 

198 return self.async_create_entry( 

199 title="Auto Arm", 

200 data={CONF_ALARM_PANEL: alarm_panel}, 

201 options=options, 

202 ) 

203 

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() 

208 

209 

210class AutoArmOptionsFlow(OptionsFlow): 

211 """Handle options flow for Auto Arm.""" 

212 

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) 

222 

223 options = self.config_entry.options 

224 notify_services = sorted(f"notify.{service}" for service in self.hass.services.async_services().get("notify", {})) 

225 

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 )