1 """Config flow for PurpleAir integration."""
3 from __future__
import annotations
6 from collections.abc
import Mapping
7 from copy
import deepcopy
8 from dataclasses
import dataclass, field
9 from typing
import Any, cast
11 from aiopurpleair
import API
12 from aiopurpleair.endpoints.sensors
import NearbySensorResult
13 from aiopurpleair.errors
import InvalidApiKeyError, PurpleAirError
14 import voluptuous
as vol
31 config_validation
as cv,
32 device_registry
as dr,
33 entity_registry
as er,
43 from .const
import CONF_SENSOR_INDICES, DOMAIN, LOGGER
45 CONF_DISTANCE =
"distance"
46 CONF_NEARBY_SENSOR_OPTIONS =
"nearby_sensor_options"
47 CONF_SENSOR_DEVICE_ID =
"sensor_device_id"
48 CONF_SENSOR_INDEX =
"sensor_index"
52 API_KEY_SCHEMA = vol.Schema(
54 vol.Required(CONF_API_KEY): cv.string,
61 """Get an aiopurpleair API object."""
62 session = aiohttp_client.async_get_clientsession(hass)
63 return API(api_key, session=session)
68 """Define a schema for searching for sensors near a coordinate pair."""
72 CONF_LATITUDE,
"coords", default=hass.config.latitude
75 CONF_LONGITUDE,
"coords", default=hass.config.longitude
77 vol.Optional(CONF_DISTANCE, default=DEFAULT_DISTANCE): cv.positive_int,
84 nearby_sensor_results: list[NearbySensorResult],
85 ) -> list[SelectOptionDict]:
86 """Return a set of nearby sensors as SelectOptionDict objects."""
89 value=
str(result.sensor.sensor_index), label=cast(str, result.sensor.name)
91 for result
in nearby_sensor_results
97 """Define a schema for selecting a sensor from a list."""
109 hass: HomeAssistant, config_entry: ConfigEntry
110 ) -> list[SelectOptionDict]:
111 """Return a set of already-configured sensors as SelectOptionDict objects."""
112 device_registry = dr.async_get(hass)
115 for device_entry
in device_registry.devices.get_devices_for_config_entry_id(
116 config_entry.entry_id
123 """Define a schema removing a sensor."""
135 """Define a validation result."""
138 errors: dict[str, Any] = field(default_factory=dict)
142 """Validate an API key.
144 This method returns a dictionary of errors (if appropriate).
150 await api.async_check_api_key()
151 except InvalidApiKeyError:
152 errors[
"base"] =
"invalid_api_key"
153 except PurpleAirError
as err:
154 LOGGER.error(
"PurpleAir error while checking API key: %s", err)
155 errors[
"base"] =
"unknown"
156 except Exception
as err:
157 LOGGER.exception(
"Unexpected exception while checking API key: %s", err)
158 errors[
"base"] =
"unknown"
172 ) -> ValidationResult:
173 """Validate coordinates."""
178 nearby_sensor_results = await api.sensors.async_get_nearby_sensors(
179 [
"name"], latitude, longitude, distance, limit_results=5
181 except PurpleAirError
as err:
182 LOGGER.error(
"PurpleAir error while getting nearby sensors: %s", err)
183 errors[
"base"] =
"unknown"
184 except Exception
as err:
185 LOGGER.exception(
"Unexpected exception while getting nearby sensors: %s", err)
186 errors[
"base"] =
"unknown"
188 if not nearby_sensor_results:
189 errors[
"base"] =
"no_sensors_near_coordinates"
198 """Handle a config flow for PurpleAir."""
204 self.
_flow_data_flow_data: dict[str, Any] = {}
209 config_entry: ConfigEntry,
210 ) -> PurpleAirOptionsFlowHandler:
211 """Define the config flow to handle options."""
215 self, user_input: dict[str, Any] |
None =
None
216 ) -> ConfigFlowResult:
217 """Handle the discovery of sensors near a latitude/longitude."""
218 if user_input
is None:
220 step_id=
"by_coordinates",
227 user_input[CONF_LATITUDE],
228 user_input[CONF_LONGITUDE],
229 user_input[CONF_DISTANCE],
231 if validation.errors:
233 step_id=
"by_coordinates",
235 errors=validation.errors,
245 self, user_input: dict[str, Any] |
None =
None
246 ) -> ConfigFlowResult:
247 """Handle the selection of a sensor."""
248 if user_input
is None:
249 options = self.
_flow_data_flow_data.pop(CONF_NEARBY_SENSOR_OPTIONS)
251 step_id=
"choose_sensor",
256 title=self.
_flow_data_flow_data[CONF_API_KEY][:5],
260 options={CONF_SENSOR_INDICES: [
int(user_input[CONF_SENSOR_INDEX])]},
264 self, entry_data: Mapping[str, Any]
265 ) -> ConfigFlowResult:
266 """Handle configuration by re-auth."""
270 self, user_input: dict[str, Any] |
None =
None
271 ) -> ConfigFlowResult:
272 """Handle the re-auth step."""
273 if user_input
is None:
275 step_id=
"reauth_confirm", data_schema=API_KEY_SCHEMA
278 api_key = user_input[CONF_API_KEY]
281 if validation.errors:
283 step_id=
"reauth_confirm",
284 data_schema=API_KEY_SCHEMA,
285 errors=validation.errors,
293 self, user_input: dict[str, Any] |
None =
None
294 ) -> ConfigFlowResult:
295 """Handle the initial step."""
296 if user_input
is None:
299 api_key = user_input[CONF_API_KEY]
304 if validation.errors:
307 data_schema=API_KEY_SCHEMA,
308 errors=validation.errors,
316 """Handle a PurpleAir options flow."""
320 self._flow_data: dict[str, Any] = {}
324 """Return the settings schema."""
339 self, user_input: dict[str, Any] |
None =
None
340 ) -> ConfigFlowResult:
342 if user_input
is None:
344 step_id=
"add_sensor",
351 user_input[CONF_LATITUDE],
352 user_input[CONF_LONGITUDE],
353 user_input[CONF_DISTANCE],
356 if validation.errors:
358 step_id=
"add_sensor",
360 errors=validation.errors,
370 self, user_input: dict[str, Any] |
None =
None
371 ) -> ConfigFlowResult:
372 """Choose a sensor."""
373 if user_input
is None:
374 options = self._flow_data.pop(CONF_NEARBY_SENSOR_OPTIONS)
376 step_id=
"choose_sensor",
380 sensor_index =
int(user_input[CONF_SENSOR_INDEX])
383 return self.
async_abortasync_abort(reason=
"already_configured")
386 options[CONF_SENSOR_INDICES].append(sensor_index)
390 self, user_input: dict[str, Any] |
None =
None
391 ) -> ConfigFlowResult:
392 """Manage the options."""
395 menu_options=[
"add_sensor",
"remove_sensor",
"settings"],
399 self, user_input: dict[str, Any] |
None =
None
400 ) -> ConfigFlowResult:
401 """Remove a sensor."""
402 if user_input
is None:
404 step_id=
"remove_sensor",
410 device_registry = dr.async_get(self.hass)
411 entity_registry = er.async_get(self.hass)
413 device_id = user_input[CONF_SENSOR_DEVICE_ID]
414 device_entry = cast(dr.DeviceEntry, device_registry.async_get(device_id))
417 entity_entries = er.async_entries_for_device(
418 entity_registry, device_id, include_disabled_entities=
True
421 device_entities_removed_event = asyncio.Event()
424 def async_device_entity_state_changed(
425 _: Event[EventStateChangedData],
427 """Listen and respond when all device entities are removed."""
429 self.hass.states.get(entity_entry.entity_id)
is None
430 for entity_entry
in entity_entries
432 device_entities_removed_event.set()
438 [entity_entry.entity_id
for entity_entry
in entity_entries],
439 async_device_entity_state_changed,
441 device_registry.async_update_device(
444 await device_entities_removed_event.wait()
450 removed_sensor_index = next(
453 if (DOMAIN,
str(sensor_index))
in device_entry.identifiers
456 options[CONF_SENSOR_INDICES].
remove(removed_sensor_index)
461 self, user_input: dict[str, Any] |
None =
None
462 ) -> ConfigFlowResult:
463 """Manage settings."""
464 if user_input
is None:
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
PurpleAirOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_choose_sensor(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_by_coordinates(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_add_sensor(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
vol.Schema settings_schema(self)
ConfigFlowResult async_step_remove_sensor(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_settings(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_choose_sensor(self, dict[str, Any]|None user_input=None)
ConfigEntry _get_reauth_entry(self)
ConfigFlowResult async_create_entry(self, *str title, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None, Mapping[str, Any]|None options=None)
ConfigFlowResult async_update_reload_and_abort(self, ConfigEntry entry, *str|None|UndefinedType unique_id=UNDEFINED, str|UndefinedType title=UNDEFINED, Mapping[str, Any]|UndefinedType data=UNDEFINED, Mapping[str, Any]|UndefinedType data_updates=UNDEFINED, Mapping[str, Any]|UndefinedType options=UNDEFINED, str|UndefinedType reason=UNDEFINED, bool reload_even_if_entry_is_unchanged=True)
None _async_abort_entries_match(self, dict[str, Any]|None match_dict=None)
ConfigFlowResult async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
ConfigEntry config_entry(self)
None config_entry(self, ConfigEntry value)
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_show_menu(self, *str|None step_id=None, Container[str] menu_options, Mapping[str, str]|None description_placeholders=None)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
bool remove(self, _T matcher)
vol.Schema async_get_remove_sensor_schema(list[SelectOptionDict] sensors)
ValidationResult async_validate_coordinates(HomeAssistant hass, str api_key, float latitude, float longitude, float distance)
list[SelectOptionDict] async_get_remove_sensor_options(HomeAssistant hass, ConfigEntry config_entry)
vol.Schema async_get_coordinates_schema(HomeAssistant hass)
ValidationResult async_validate_api_key(HomeAssistant hass, str api_key)
list[SelectOptionDict] async_get_nearby_sensors_options(list[NearbySensorResult] nearby_sensor_results)
API async_get_api(HomeAssistant hass, str api_key)
vol.Schema async_get_nearby_sensors_schema(list[SelectOptionDict] options)
CALLBACK_TYPE async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)