Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions homeassistant/components/ukraine_alarm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,76 @@

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

try:
websession = async_get_clientsession(hass)
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


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Ukraine Alarm as config entry."""
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/ukraine_alarm/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
class UkraineAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
"""Config flow for Ukraine Alarm."""

VERSION = 1
VERSION = 2

def __init__(self) -> None:
"""Initialize a new UkraineAlarmConfigFlow."""
Expand Down Expand Up @@ -112,7 +112,7 @@ async def _handle_pick_region(
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"
]
Expand Down
12 changes: 9 additions & 3 deletions homeassistant/components/ukraine_alarm/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
},
Expand All @@ -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"
}
}
}
26 changes: 26 additions & 0 deletions tests/components/ukraine_alarm/__init__.py
Original file line number Diff line number Diff line change
@@ -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)],
}
97 changes: 2 additions & 95 deletions tests/components/ukraine_alarm/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,9 @@
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType

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
from . import REGIONS

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)],
}
from tests.common import MockConfigEntry


@pytest.fixture(autouse=True)
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
63 changes: 63 additions & 0 deletions tests/components/ukraine_alarm/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Test the Ukraine Alarm integration initialization."""

from unittest.mock import patch

from homeassistant.components.ukraine_alarm import async_migrate_entry
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) -> 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,
):
result = await async_migrate_entry(hass, entry)
assert result is True
assert entry.version == 2

issue_registry = ir.async_get(hass)
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,
) -> 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 async_migrate_entry(hass, entry)
assert result is False

issue_registry = ir.async_get(hass)
assert (
DOMAIN,
f"deprecated_state_region_{entry.entry_id}",
) in issue_registry.issues
Loading