Skip to content

Commit dcec6c3

Browse files
authored
Forbid to choose state in Ukraine Alarm integration (home-assistant#156183)
1 parent c0e59c4 commit dcec6c3

File tree

6 files changed

+172
-100
lines changed

6 files changed

+172
-100
lines changed

homeassistant/components/ukraine_alarm/__init__.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,23 @@
22

33
from __future__ import annotations
44

5+
import logging
6+
from typing import TYPE_CHECKING
7+
8+
import aiohttp
9+
from uasiren.client import Client
10+
511
from homeassistant.config_entries import ConfigEntry
12+
from homeassistant.const import CONF_NAME, CONF_REGION
613
from homeassistant.core import HomeAssistant
14+
from homeassistant.helpers import issue_registry as ir
715
from homeassistant.helpers.aiohttp_client import async_get_clientsession
816

917
from .const import DOMAIN, PLATFORMS
1018
from .coordinator import UkraineAlarmDataUpdateCoordinator
1119

20+
_LOGGER = logging.getLogger(__name__)
21+
1222

1323
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
1424
"""Set up Ukraine Alarm as config entry."""
@@ -30,3 +40,56 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
3040
hass.data[DOMAIN].pop(entry.entry_id)
3141

3242
return unload_ok
43+
44+
45+
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
46+
"""Migrate old entry."""
47+
_LOGGER.debug("Migrating from version %s", config_entry.version)
48+
49+
if config_entry.version == 1:
50+
# Version 1 had states as first-class selections
51+
# Version 2 only allows states w/o districts, districts and communities
52+
region_id = config_entry.data[CONF_REGION]
53+
54+
websession = async_get_clientsession(hass)
55+
try:
56+
regions_data = await Client(websession).get_regions()
57+
except (aiohttp.ClientError, TimeoutError) as err:
58+
_LOGGER.warning(
59+
"Could not migrate config entry %s: failed to fetch current regions: %s",
60+
config_entry.entry_id,
61+
err,
62+
)
63+
return False
64+
65+
if TYPE_CHECKING:
66+
assert isinstance(regions_data, dict)
67+
68+
state_with_districts = None
69+
for state in regions_data["states"]:
70+
if state["regionId"] == region_id and state.get("regionChildIds"):
71+
state_with_districts = state
72+
break
73+
74+
if state_with_districts:
75+
ir.async_create_issue(
76+
hass,
77+
DOMAIN,
78+
f"deprecated_state_region_{config_entry.entry_id}",
79+
is_fixable=False,
80+
issue_domain=DOMAIN,
81+
severity=ir.IssueSeverity.WARNING,
82+
translation_key="deprecated_state_region",
83+
translation_placeholders={
84+
"region_name": config_entry.data.get(CONF_NAME, region_id),
85+
},
86+
)
87+
88+
return False
89+
90+
hass.config_entries.async_update_entry(config_entry, version=2)
91+
_LOGGER.info("Migration to version %s successful", 2)
92+
return True
93+
94+
_LOGGER.error("Unknown version %s", config_entry.version)
95+
return False

homeassistant/components/ukraine_alarm/config_flow.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
class UkraineAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
2222
"""Config flow for Ukraine Alarm."""
2323

24-
VERSION = 1
24+
VERSION = 2
2525

2626
def __init__(self) -> None:
2727
"""Initialize a new UkraineAlarmConfigFlow."""
@@ -112,7 +112,7 @@ async def _handle_pick_region(
112112
return await self._async_finish_flow()
113113

114114
regions = {}
115-
if self.selected_region:
115+
if self.selected_region and step_id != "district":
116116
regions[self.selected_region["regionId"]] = self.selected_region[
117117
"regionName"
118118
]

homeassistant/components/ukraine_alarm/strings.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,19 @@
1313
"data": {
1414
"region": "[%key:component::ukraine_alarm::config::step::user::data::region%]"
1515
},
16-
"description": "If you want to monitor not only state and district, choose its specific community"
16+
"description": "Choose the district you selected above or select a specific community within that district"
1717
},
1818
"district": {
1919
"data": {
2020
"region": "[%key:component::ukraine_alarm::config::step::user::data::region%]"
2121
},
22-
"description": "If you want to monitor not only state, choose its specific district"
22+
"description": "Choose a district to monitor within the selected state"
2323
},
2424
"user": {
2525
"data": {
2626
"region": "Region"
2727
},
28-
"description": "Choose state to monitor"
28+
"description": "Choose a state"
2929
}
3030
}
3131
},
@@ -50,5 +50,11 @@
5050
"name": "Urban fights"
5151
}
5252
}
53+
},
54+
"issues": {
55+
"deprecated_state_region": {
56+
"description": "The region `{region_name}` is a state-level region, which is no longer supported. Please remove this integration entry and add it again, selecting a district or community instead of the entire state.",
57+
"title": "State-level region monitoring is no longer supported"
58+
}
5359
}
5460
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,27 @@
11
"""Tests for the Ukraine Alarm integration."""
2+
3+
4+
def _region(rid, recurse=0, depth=0):
5+
"""Create a test region with optional nested structure."""
6+
if depth == 0:
7+
name_prefix = "State"
8+
elif depth == 1:
9+
name_prefix = "District"
10+
else:
11+
name_prefix = "Community"
12+
13+
name = f"{name_prefix} {rid}"
14+
region = {"regionId": rid, "regionName": name, "regionChildIds": []}
15+
16+
if not recurse:
17+
return region
18+
19+
for i in range(1, 4):
20+
region["regionChildIds"].append(_region(f"{rid}.{i}", recurse - 1, depth + 1))
21+
22+
return region
23+
24+
25+
REGIONS = {
26+
"states": [_region(f"{i}", i - 1) for i in range(1, 4)],
27+
}

tests/components/ukraine_alarm/test_config_flow.py

Lines changed: 2 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,9 @@
1212
from homeassistant.core import HomeAssistant
1313
from homeassistant.data_entry_flow import FlowResultType
1414

15-
from tests.common import MockConfigEntry
16-
17-
18-
def _region(rid, recurse=0, depth=0):
19-
if depth == 0:
20-
name_prefix = "State"
21-
elif depth == 1:
22-
name_prefix = "District"
23-
else:
24-
name_prefix = "Community"
25-
26-
name = f"{name_prefix} {rid}"
27-
region = {"regionId": rid, "regionName": name, "regionChildIds": []}
28-
29-
if not recurse:
30-
return region
15+
from . import REGIONS
3116

32-
for i in range(1, 4):
33-
region["regionChildIds"].append(_region(f"{rid}.{i}", recurse - 1, depth + 1))
34-
35-
return region
36-
37-
38-
REGIONS = {
39-
"states": [_region(f"{i}", i - 1) for i in range(1, 4)],
40-
}
17+
from tests.common import MockConfigEntry
4118

4219

4320
@pytest.fixture(autouse=True)
@@ -51,37 +28,6 @@ def mock_get_regions() -> Generator[AsyncMock]:
5128
yield mock_get
5229

5330

54-
async def test_state(hass: HomeAssistant) -> None:
55-
"""Test we can create entry for state."""
56-
result = await hass.config_entries.flow.async_init(
57-
DOMAIN, context={"source": config_entries.SOURCE_USER}
58-
)
59-
assert result["type"] is FlowResultType.FORM
60-
61-
result2 = await hass.config_entries.flow.async_configure(result["flow_id"])
62-
assert result2["type"] is FlowResultType.FORM
63-
64-
with patch(
65-
"homeassistant.components.ukraine_alarm.async_setup_entry",
66-
return_value=True,
67-
) as mock_setup_entry:
68-
result3 = await hass.config_entries.flow.async_configure(
69-
result["flow_id"],
70-
{
71-
"region": "1",
72-
},
73-
)
74-
await hass.async_block_till_done()
75-
76-
assert result3["type"] is FlowResultType.CREATE_ENTRY
77-
assert result3["title"] == "State 1"
78-
assert result3["data"] == {
79-
"region": "1",
80-
"name": result3["title"],
81-
}
82-
assert len(mock_setup_entry.mock_calls) == 1
83-
84-
8531
async def test_state_district(hass: HomeAssistant) -> None:
8632
"""Test we can create entry for state + district."""
8733
result = await hass.config_entries.flow.async_init(
@@ -121,45 +67,6 @@ async def test_state_district(hass: HomeAssistant) -> None:
12167
assert len(mock_setup_entry.mock_calls) == 1
12268

12369

124-
async def test_state_district_pick_region(hass: HomeAssistant) -> None:
125-
"""Test we can create entry for region which has districts."""
126-
result = await hass.config_entries.flow.async_init(
127-
DOMAIN, context={"source": config_entries.SOURCE_USER}
128-
)
129-
assert result["type"] is FlowResultType.FORM
130-
131-
result2 = await hass.config_entries.flow.async_configure(result["flow_id"])
132-
assert result2["type"] is FlowResultType.FORM
133-
134-
result3 = await hass.config_entries.flow.async_configure(
135-
result["flow_id"],
136-
{
137-
"region": "2",
138-
},
139-
)
140-
assert result3["type"] is FlowResultType.FORM
141-
142-
with patch(
143-
"homeassistant.components.ukraine_alarm.async_setup_entry",
144-
return_value=True,
145-
) as mock_setup_entry:
146-
result4 = await hass.config_entries.flow.async_configure(
147-
result["flow_id"],
148-
{
149-
"region": "2",
150-
},
151-
)
152-
await hass.async_block_till_done()
153-
154-
assert result4["type"] is FlowResultType.CREATE_ENTRY
155-
assert result4["title"] == "State 2"
156-
assert result4["data"] == {
157-
"region": "2",
158-
"name": result4["title"],
159-
}
160-
assert len(mock_setup_entry.mock_calls) == 1
161-
162-
16370
async def test_state_district_community(hass: HomeAssistant) -> None:
16471
"""Test we can create entry for state + district + community."""
16572
result = await hass.config_entries.flow.async_init(
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Test the Ukraine Alarm integration initialization."""
2+
3+
from unittest.mock import patch
4+
5+
from homeassistant.components.ukraine_alarm.const import DOMAIN
6+
from homeassistant.core import HomeAssistant
7+
from homeassistant.helpers import issue_registry as ir
8+
9+
from . import REGIONS
10+
11+
from tests.common import MockConfigEntry
12+
13+
14+
async def test_migration_v1_to_v2_state_without_districts(
15+
hass: HomeAssistant,
16+
issue_registry: ir.IssueRegistry,
17+
) -> None:
18+
"""Test migration allows states without districts."""
19+
entry = MockConfigEntry(
20+
domain=DOMAIN,
21+
version=1,
22+
data={"region": "1", "name": "State 1"},
23+
unique_id="1",
24+
)
25+
entry.add_to_hass(hass)
26+
27+
with (
28+
patch(
29+
"homeassistant.components.ukraine_alarm.Client.get_regions",
30+
return_value=REGIONS,
31+
),
32+
patch(
33+
"homeassistant.components.ukraine_alarm.Client.get_alerts",
34+
return_value=[{"activeAlerts": []}],
35+
),
36+
):
37+
result = await hass.config_entries.async_setup(entry.entry_id)
38+
assert result is True
39+
assert entry.version == 2
40+
41+
assert (
42+
DOMAIN,
43+
f"deprecated_state_region_{entry.entry_id}",
44+
) not in issue_registry.issues
45+
46+
47+
async def test_migration_v1_to_v2_state_with_districts_fails(
48+
hass: HomeAssistant,
49+
issue_registry: ir.IssueRegistry,
50+
) -> None:
51+
"""Test migration rejects states with districts."""
52+
entry = MockConfigEntry(
53+
domain=DOMAIN,
54+
version=1,
55+
data={"region": "2", "name": "State 2"},
56+
unique_id="2",
57+
)
58+
entry.add_to_hass(hass)
59+
60+
with patch(
61+
"homeassistant.components.ukraine_alarm.Client.get_regions",
62+
return_value=REGIONS,
63+
):
64+
result = await hass.config_entries.async_setup(entry.entry_id)
65+
assert result is False
66+
67+
assert (
68+
DOMAIN,
69+
f"deprecated_state_region_{entry.entry_id}",
70+
) in issue_registry.issues

0 commit comments

Comments
 (0)