Forbid to choose state in Ukraine Alarm integration (#156183)

This commit is contained in:
Paul Annekov
2025-11-11 15:05:14 +02:00
committed by GitHub
parent c0e59c4508
commit dcec6c3dc8
6 changed files with 172 additions and 100 deletions

View File

@@ -2,13 +2,23 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
import aiohttp
from uasiren.client import Client
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CONF_REGION
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, PLATFORMS
from .coordinator import UkraineAlarmDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Ukraine Alarm as config entry."""
@@ -30,3 +40,56 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s", config_entry.version)
if config_entry.version == 1:
# Version 1 had states as first-class selections
# Version 2 only allows states w/o districts, districts and communities
region_id = config_entry.data[CONF_REGION]
websession = async_get_clientsession(hass)
try:
regions_data = await Client(websession).get_regions()
except (aiohttp.ClientError, TimeoutError) as err:
_LOGGER.warning(
"Could not migrate config entry %s: failed to fetch current regions: %s",
config_entry.entry_id,
err,
)
return False
if TYPE_CHECKING:
assert isinstance(regions_data, dict)
state_with_districts = None
for state in regions_data["states"]:
if state["regionId"] == region_id and state.get("regionChildIds"):
state_with_districts = state
break
if state_with_districts:
ir.async_create_issue(
hass,
DOMAIN,
f"deprecated_state_region_{config_entry.entry_id}",
is_fixable=False,
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key="deprecated_state_region",
translation_placeholders={
"region_name": config_entry.data.get(CONF_NAME, region_id),
},
)
return False
hass.config_entries.async_update_entry(config_entry, version=2)
_LOGGER.info("Migration to version %s successful", 2)
return True
_LOGGER.error("Unknown version %s", config_entry.version)
return False

View File

@@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
class UkraineAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
"""Config flow for Ukraine Alarm."""
VERSION = 1
VERSION = 2
def __init__(self) -> None:
"""Initialize a new UkraineAlarmConfigFlow."""
@@ -112,7 +112,7 @@ class UkraineAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
return await self._async_finish_flow()
regions = {}
if self.selected_region:
if self.selected_region and step_id != "district":
regions[self.selected_region["regionId"]] = self.selected_region[
"regionName"
]

View File

@@ -13,19 +13,19 @@
"data": {
"region": "[%key:component::ukraine_alarm::config::step::user::data::region%]"
},
"description": "If you want to monitor not only state and district, choose its specific community"
"description": "Choose the district you selected above or select a specific community within that district"
},
"district": {
"data": {
"region": "[%key:component::ukraine_alarm::config::step::user::data::region%]"
},
"description": "If you want to monitor not only state, choose its specific district"
"description": "Choose a district to monitor within the selected state"
},
"user": {
"data": {
"region": "Region"
},
"description": "Choose state to monitor"
"description": "Choose a state"
}
}
},
@@ -50,5 +50,11 @@
"name": "Urban fights"
}
}
},
"issues": {
"deprecated_state_region": {
"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.",
"title": "State-level region monitoring is no longer supported"
}
}
}

View File

@@ -1 +1,27 @@
"""Tests for the Ukraine Alarm integration."""
def _region(rid, recurse=0, depth=0):
"""Create a test region with optional nested structure."""
if depth == 0:
name_prefix = "State"
elif depth == 1:
name_prefix = "District"
else:
name_prefix = "Community"
name = f"{name_prefix} {rid}"
region = {"regionId": rid, "regionName": name, "regionChildIds": []}
if not recurse:
return region
for i in range(1, 4):
region["regionChildIds"].append(_region(f"{rid}.{i}", recurse - 1, depth + 1))
return region
REGIONS = {
"states": [_region(f"{i}", i - 1) for i in range(1, 4)],
}

View File

@@ -12,34 +12,11 @@ from homeassistant.components.ukraine_alarm.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from . import REGIONS
from tests.common import MockConfigEntry
def _region(rid, recurse=0, depth=0):
if depth == 0:
name_prefix = "State"
elif depth == 1:
name_prefix = "District"
else:
name_prefix = "Community"
name = f"{name_prefix} {rid}"
region = {"regionId": rid, "regionName": name, "regionChildIds": []}
if not recurse:
return region
for i in range(1, 4):
region["regionChildIds"].append(_region(f"{rid}.{i}", recurse - 1, depth + 1))
return region
REGIONS = {
"states": [_region(f"{i}", i - 1) for i in range(1, 4)],
}
@pytest.fixture(autouse=True)
def mock_get_regions() -> Generator[AsyncMock]:
"""Mock the get_regions method."""
@@ -51,37 +28,6 @@ def mock_get_regions() -> Generator[AsyncMock]:
yield mock_get
async def test_state(hass: HomeAssistant) -> None:
"""Test we can create entry for state."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
result2 = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result2["type"] is FlowResultType.FORM
with patch(
"homeassistant.components.ukraine_alarm.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"region": "1",
},
)
await hass.async_block_till_done()
assert result3["type"] is FlowResultType.CREATE_ENTRY
assert result3["title"] == "State 1"
assert result3["data"] == {
"region": "1",
"name": result3["title"],
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_state_district(hass: HomeAssistant) -> None:
"""Test we can create entry for state + district."""
result = await hass.config_entries.flow.async_init(
@@ -121,45 +67,6 @@ async def test_state_district(hass: HomeAssistant) -> None:
assert len(mock_setup_entry.mock_calls) == 1
async def test_state_district_pick_region(hass: HomeAssistant) -> None:
"""Test we can create entry for region which has districts."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
result2 = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result2["type"] is FlowResultType.FORM
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"region": "2",
},
)
assert result3["type"] is FlowResultType.FORM
with patch(
"homeassistant.components.ukraine_alarm.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result4 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"region": "2",
},
)
await hass.async_block_till_done()
assert result4["type"] is FlowResultType.CREATE_ENTRY
assert result4["title"] == "State 2"
assert result4["data"] == {
"region": "2",
"name": result4["title"],
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_state_district_community(hass: HomeAssistant) -> None:
"""Test we can create entry for state + district + community."""
result = await hass.config_entries.flow.async_init(

View File

@@ -0,0 +1,70 @@
"""Test the Ukraine Alarm integration initialization."""
from unittest.mock import patch
from homeassistant.components.ukraine_alarm.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from . import REGIONS
from tests.common import MockConfigEntry
async def test_migration_v1_to_v2_state_without_districts(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test migration allows states without districts."""
entry = MockConfigEntry(
domain=DOMAIN,
version=1,
data={"region": "1", "name": "State 1"},
unique_id="1",
)
entry.add_to_hass(hass)
with (
patch(
"homeassistant.components.ukraine_alarm.Client.get_regions",
return_value=REGIONS,
),
patch(
"homeassistant.components.ukraine_alarm.Client.get_alerts",
return_value=[{"activeAlerts": []}],
),
):
result = await hass.config_entries.async_setup(entry.entry_id)
assert result is True
assert entry.version == 2
assert (
DOMAIN,
f"deprecated_state_region_{entry.entry_id}",
) not in issue_registry.issues
async def test_migration_v1_to_v2_state_with_districts_fails(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test migration rejects states with districts."""
entry = MockConfigEntry(
domain=DOMAIN,
version=1,
data={"region": "2", "name": "State 2"},
unique_id="2",
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.ukraine_alarm.Client.get_regions",
return_value=REGIONS,
):
result = await hass.config_entries.async_setup(entry.entry_id)
assert result is False
assert (
DOMAIN,
f"deprecated_state_region_{entry.entry_id}",
) in issue_registry.issues