Skip to content

Commit ae84c7e

Browse files
joostlekabmantis
andauthored
Add subentries to WAQI (home-assistant#148966)
Co-authored-by: Abílio Costa <[email protected]>
1 parent 415c8b4 commit ae84c7e

File tree

9 files changed

+843
-189
lines changed

9 files changed

+843
-189
lines changed

homeassistant/components/waqi/__init__.py

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,169 @@
22

33
from __future__ import annotations
44

5+
from types import MappingProxyType
6+
from typing import TYPE_CHECKING
7+
58
from aiowaqi import WAQIClient
69

10+
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
711
from homeassistant.const import CONF_API_KEY, Platform
812
from homeassistant.core import HomeAssistant
13+
from homeassistant.helpers import (
14+
config_validation as cv,
15+
device_registry as dr,
16+
entity_registry as er,
17+
)
918
from homeassistant.helpers.aiohttp_client import async_get_clientsession
19+
from homeassistant.helpers.typing import ConfigType
1020

21+
from .const import CONF_STATION_NUMBER, DOMAIN, SUBENTRY_TYPE_STATION
1122
from .coordinator import WAQIConfigEntry, WAQIDataUpdateCoordinator
1223

24+
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
1325
PLATFORMS: list[Platform] = [Platform.SENSOR]
1426

1527

28+
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
29+
"""Set up WAQI."""
30+
31+
await async_migrate_integration(hass)
32+
return True
33+
34+
1635
async def async_setup_entry(hass: HomeAssistant, entry: WAQIConfigEntry) -> bool:
1736
"""Set up World Air Quality Index (WAQI) from a config entry."""
1837

1938
client = WAQIClient(session=async_get_clientsession(hass))
2039
client.authenticate(entry.data[CONF_API_KEY])
2140

22-
waqi_coordinator = WAQIDataUpdateCoordinator(hass, entry, client)
23-
await waqi_coordinator.async_config_entry_first_refresh()
24-
entry.runtime_data = waqi_coordinator
41+
entry.runtime_data = {}
42+
43+
for subentry in entry.subentries.values():
44+
if subentry.subentry_type != SUBENTRY_TYPE_STATION:
45+
continue
46+
47+
# Create a coordinator for each station subentry
48+
coordinator = WAQIDataUpdateCoordinator(hass, entry, subentry, client)
49+
await coordinator.async_config_entry_first_refresh()
50+
entry.runtime_data[subentry.subentry_id] = coordinator
51+
52+
entry.async_on_unload(entry.add_update_listener(async_update_entry))
2553

2654
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
2755

2856
return True
2957

3058

59+
async def async_update_entry(hass: HomeAssistant, entry: WAQIConfigEntry) -> None:
60+
"""Update entry."""
61+
await hass.config_entries.async_reload(entry.entry_id)
62+
63+
3164
async def async_unload_entry(hass: HomeAssistant, entry: WAQIConfigEntry) -> bool:
3265
"""Unload a config entry."""
3366
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
67+
68+
69+
async def async_migrate_integration(hass: HomeAssistant) -> None:
70+
"""Migrate integration entry structure to subentries."""
71+
72+
# Make sure we get enabled config entries first
73+
entries = sorted(
74+
hass.config_entries.async_entries(DOMAIN),
75+
key=lambda e: e.disabled_by is not None,
76+
)
77+
if not any(entry.version == 1 for entry in entries):
78+
return
79+
80+
api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {}
81+
entity_registry = er.async_get(hass)
82+
device_registry = dr.async_get(hass)
83+
84+
for entry in entries:
85+
subentry = ConfigSubentry(
86+
data=MappingProxyType(
87+
{CONF_STATION_NUMBER: entry.data[CONF_STATION_NUMBER]}
88+
),
89+
subentry_type="station",
90+
title=entry.title,
91+
unique_id=entry.unique_id,
92+
)
93+
if entry.data[CONF_API_KEY] not in api_keys_entries:
94+
all_disabled = all(
95+
e.disabled_by is not None
96+
for e in entries
97+
if e.data[CONF_API_KEY] == entry.data[CONF_API_KEY]
98+
)
99+
api_keys_entries[entry.data[CONF_API_KEY]] = (entry, all_disabled)
100+
101+
parent_entry, all_disabled = api_keys_entries[entry.data[CONF_API_KEY]]
102+
103+
hass.config_entries.async_add_subentry(parent_entry, subentry)
104+
105+
entities = er.async_entries_for_config_entry(entity_registry, entry.entry_id)
106+
if TYPE_CHECKING:
107+
assert entry.unique_id is not None
108+
device = device_registry.async_get_device(
109+
identifiers={(DOMAIN, entry.unique_id)}
110+
)
111+
112+
for entity_entry in entities:
113+
entity_disabled_by = entity_entry.disabled_by
114+
if (
115+
entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY
116+
and not all_disabled
117+
):
118+
# Device and entity registries don't update the disabled_by flag
119+
# when moving a device or entity from one config entry to another,
120+
# so we need to do it manually.
121+
entity_disabled_by = (
122+
er.RegistryEntryDisabler.DEVICE
123+
if device
124+
else er.RegistryEntryDisabler.USER
125+
)
126+
entity_registry.async_update_entity(
127+
entity_entry.entity_id,
128+
config_entry_id=parent_entry.entry_id,
129+
config_subentry_id=subentry.subentry_id,
130+
disabled_by=entity_disabled_by,
131+
)
132+
133+
if device is not None:
134+
# Device and entity registries don't update the disabled_by flag when
135+
# moving a device or entity from one config entry to another, so we
136+
# need to do it manually.
137+
device_disabled_by = device.disabled_by
138+
if (
139+
device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY
140+
and not all_disabled
141+
):
142+
device_disabled_by = dr.DeviceEntryDisabler.USER
143+
device_registry.async_update_device(
144+
device.id,
145+
disabled_by=device_disabled_by,
146+
add_config_subentry_id=subentry.subentry_id,
147+
add_config_entry_id=parent_entry.entry_id,
148+
)
149+
if parent_entry.entry_id != entry.entry_id:
150+
device_registry.async_update_device(
151+
device.id,
152+
remove_config_entry_id=entry.entry_id,
153+
)
154+
else:
155+
device_registry.async_update_device(
156+
device.id,
157+
remove_config_entry_id=entry.entry_id,
158+
remove_config_subentry_id=None,
159+
)
160+
161+
if parent_entry.entry_id != entry.entry_id:
162+
await hass.config_entries.async_remove(entry.entry_id)
163+
else:
164+
hass.config_entries.async_update_entry(
165+
entry,
166+
title="WAQI",
167+
version=2,
168+
data={CONF_API_KEY: entry.data[CONF_API_KEY]},
169+
unique_id=None,
170+
)

homeassistant/components/waqi/config_flow.py

Lines changed: 52 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,24 @@
1313
)
1414
import voluptuous as vol
1515

16-
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
16+
from homeassistant.config_entries import (
17+
ConfigEntry,
18+
ConfigFlow,
19+
ConfigFlowResult,
20+
ConfigSubentryFlow,
21+
SubentryFlowResult,
22+
)
1723
from homeassistant.const import (
1824
CONF_API_KEY,
1925
CONF_LATITUDE,
2026
CONF_LOCATION,
2127
CONF_LONGITUDE,
22-
CONF_METHOD,
2328
)
29+
from homeassistant.core import callback
2430
from homeassistant.helpers.aiohttp_client import async_get_clientsession
25-
from homeassistant.helpers.selector import (
26-
LocationSelector,
27-
SelectSelector,
28-
SelectSelectorConfig,
29-
)
31+
from homeassistant.helpers.selector import LocationSelector
3032

31-
from .const import CONF_STATION_NUMBER, DOMAIN
33+
from .const import CONF_STATION_NUMBER, DOMAIN, SUBENTRY_TYPE_STATION
3234

3335
_LOGGER = logging.getLogger(__name__)
3436

@@ -54,18 +56,23 @@ async def get_by_station_number(
5456
class WAQIConfigFlow(ConfigFlow, domain=DOMAIN):
5557
"""Handle a config flow for World Air Quality Index (WAQI)."""
5658

57-
VERSION = 1
59+
VERSION = 2
5860

59-
def __init__(self) -> None:
60-
"""Initialize config flow."""
61-
self.data: dict[str, Any] = {}
61+
@classmethod
62+
@callback
63+
def async_get_supported_subentry_types(
64+
cls, config_entry: ConfigEntry
65+
) -> dict[str, type[ConfigSubentryFlow]]:
66+
"""Return subentries supported by this handler."""
67+
return {SUBENTRY_TYPE_STATION: StationFlowHandler}
6268

6369
async def async_step_user(
6470
self, user_input: dict[str, Any] | None = None
6571
) -> ConfigFlowResult:
6672
"""Handle the initial step."""
6773
errors: dict[str, str] = {}
6874
if user_input is not None:
75+
self._async_abort_entries_match({CONF_API_KEY: user_input[CONF_API_KEY]})
6976
client = WAQIClient(session=async_get_clientsession(self.hass))
7077
client.authenticate(user_input[CONF_API_KEY])
7178
try:
@@ -78,35 +85,40 @@ async def async_step_user(
7885
_LOGGER.exception("Unexpected exception")
7986
errors["base"] = "unknown"
8087
else:
81-
self.data = user_input
82-
if user_input[CONF_METHOD] == CONF_MAP:
83-
return await self.async_step_map()
84-
return await self.async_step_station_number()
88+
return self.async_create_entry(
89+
title="World Air Quality Index",
90+
data={
91+
CONF_API_KEY: user_input[CONF_API_KEY],
92+
},
93+
)
8594

8695
return self.async_show_form(
8796
step_id="user",
88-
data_schema=vol.Schema(
89-
{
90-
vol.Required(CONF_API_KEY): str,
91-
vol.Required(CONF_METHOD): SelectSelector(
92-
SelectSelectorConfig(
93-
options=[CONF_MAP, CONF_STATION_NUMBER],
94-
translation_key="method",
95-
)
96-
),
97-
}
98-
),
97+
data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}),
9998
errors=errors,
10099
)
101100

101+
102+
class StationFlowHandler(ConfigSubentryFlow):
103+
"""Handle subentry flow."""
104+
105+
async def async_step_user(
106+
self, user_input: dict[str, Any] | None = None
107+
) -> SubentryFlowResult:
108+
"""User flow to create a sensor subentry."""
109+
return self.async_show_menu(
110+
step_id="user",
111+
menu_options=["map", "station_number"],
112+
)
113+
102114
async def async_step_map(
103115
self, user_input: dict[str, Any] | None = None
104-
) -> ConfigFlowResult:
116+
) -> SubentryFlowResult:
105117
"""Add measuring station via map."""
106118
errors: dict[str, str] = {}
107119
if user_input is not None:
108120
client = WAQIClient(session=async_get_clientsession(self.hass))
109-
client.authenticate(self.data[CONF_API_KEY])
121+
client.authenticate(self._get_entry().data[CONF_API_KEY])
110122
try:
111123
measuring_station = await client.get_by_coordinates(
112124
user_input[CONF_LOCATION][CONF_LATITUDE],
@@ -124,9 +136,7 @@ async def async_step_map(
124136
data_schema=self.add_suggested_values_to_schema(
125137
vol.Schema(
126138
{
127-
vol.Required(
128-
CONF_LOCATION,
129-
): LocationSelector(),
139+
vol.Required(CONF_LOCATION): LocationSelector(),
130140
}
131141
),
132142
{
@@ -141,12 +151,12 @@ async def async_step_map(
141151

142152
async def async_step_station_number(
143153
self, user_input: dict[str, Any] | None = None
144-
) -> ConfigFlowResult:
154+
) -> SubentryFlowResult:
145155
"""Add measuring station via station number."""
146156
errors: dict[str, str] = {}
147157
if user_input is not None:
148158
client = WAQIClient(session=async_get_clientsession(self.hass))
149-
client.authenticate(self.data[CONF_API_KEY])
159+
client.authenticate(self._get_entry().data[CONF_API_KEY])
150160
station_number = user_input[CONF_STATION_NUMBER]
151161
measuring_station, errors = await get_by_station_number(
152162
client, abs(station_number)
@@ -160,25 +170,22 @@ async def async_step_station_number(
160170
return await self._async_create_entry(measuring_station)
161171
return self.async_show_form(
162172
step_id=CONF_STATION_NUMBER,
163-
data_schema=vol.Schema(
164-
{
165-
vol.Required(
166-
CONF_STATION_NUMBER,
167-
): int,
168-
}
169-
),
173+
data_schema=vol.Schema({vol.Required(CONF_STATION_NUMBER): int}),
170174
errors=errors,
171175
)
172176

173177
async def _async_create_entry(
174178
self, measuring_station: WAQIAirQuality
175-
) -> ConfigFlowResult:
176-
await self.async_set_unique_id(str(measuring_station.station_id))
177-
self._abort_if_unique_id_configured()
179+
) -> SubentryFlowResult:
180+
station_id = str(measuring_station.station_id)
181+
for entry in self.hass.config_entries.async_entries(DOMAIN):
182+
for subentry in entry.subentries.values():
183+
if subentry.unique_id == station_id:
184+
return self.async_abort(reason="already_configured")
178185
return self.async_create_entry(
179186
title=measuring_station.city.name,
180187
data={
181-
CONF_API_KEY: self.data[CONF_API_KEY],
182188
CONF_STATION_NUMBER: measuring_station.station_id,
183189
},
190+
unique_id=station_id,
184191
)

homeassistant/components/waqi/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88

99
CONF_STATION_NUMBER = "station_number"
1010

11-
ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=waqi"}
11+
SUBENTRY_TYPE_STATION = "station"

0 commit comments

Comments
 (0)