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

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

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

55 

56 

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 

60 

61 

62DEFAULT_NOTIFY_ACTION = "notify.send_message" 

63 

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} 

78 

79 

80class AutoArmConfigFlow(ConfigFlow, domain=DOMAIN): 

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

82 

83 VERSION = 1 

84 

85 def __init__(self) -> None: 

86 """Initialize the config flow.""" 

87 self._alarm_panel: str = "" 

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

89 

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

95 

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 ) 

102 

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

108 

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 ) 

117 

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

123 

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 } 

138 

139 return self.async_create_entry( 

140 title="Auto Arm", 

141 data={CONF_ALARM_PANEL: self._alarm_panel}, 

142 options=options, 

143 ) 

144 

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 ) 

153 

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

158 

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

161 

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

165 

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

169 

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) 

173 

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

177 

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 } 

192 

193 return self.async_create_entry( 

194 title="Auto Arm", 

195 data={CONF_ALARM_PANEL: alarm_panel}, 

196 options=options, 

197 ) 

198 

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

203 

204 

205class AutoArmOptionsFlow(OptionsFlow): 

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

207 

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) 

217 

218 options = self.config_entry.options 

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

220 

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 )