diff --git a/homeassistant/components/ukraine_alarm/__init__.py b/homeassistant/components/ukraine_alarm/__init__.py index b2b2ff4162f..587854a3a7e 100644 --- a/homeassistant/components/ukraine_alarm/__init__.py +++ b/homeassistant/components/ukraine_alarm/__init__.py @@ -7,10 +7,10 @@ from typing import Any import aiohttp from aiohttp import ClientSession -from ukrainealarm.client import Client +from uasiren.client import Client from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_REGION +from homeassistant.const import CONF_REGION from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -24,14 +24,11 @@ UPDATE_INTERVAL = timedelta(seconds=10) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ukraine Alarm as config entry.""" - api_key = entry.data[CONF_API_KEY] region_id = entry.data[CONF_REGION] websession = async_get_clientsession(hass) - coordinator = UkraineAlarmDataUpdateCoordinator( - hass, websession, api_key, region_id - ) + coordinator = UkraineAlarmDataUpdateCoordinator(hass, websession, region_id) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator @@ -56,19 +53,18 @@ class UkraineAlarmDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): self, hass: HomeAssistant, session: ClientSession, - api_key: str, region_id: str, ) -> None: """Initialize.""" self.region_id = region_id - self.ukrainealarm = Client(session, api_key) + self.uasiren = Client(session) super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" try: - res = await self.ukrainealarm.get_alerts(self.region_id) + res = await self.uasiren.get_alerts(self.region_id) except aiohttp.ClientError as error: raise UpdateFailed(f"Error fetching alerts from API: {error}") from error diff --git a/homeassistant/components/ukraine_alarm/config_flow.py b/homeassistant/components/ukraine_alarm/config_flow.py index dcf41658dfb..4f1e1c5cf23 100644 --- a/homeassistant/components/ukraine_alarm/config_flow.py +++ b/homeassistant/components/ukraine_alarm/config_flow.py @@ -2,17 +2,20 @@ from __future__ import annotations import asyncio +import logging import aiohttp -from ukrainealarm.client import Client +from uasiren.client import Client import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_REGION +from homeassistant.const import CONF_NAME, CONF_REGION from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) + class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for Ukraine Alarm.""" @@ -21,54 +24,47 @@ class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize a new UkraineAlarmConfigFlow.""" - self.api_key = None self.states = None self.selected_region = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" - errors = {} - if user_input is not None: + if len(self._async_current_entries()) == 5: + return self.async_abort(reason="max_regions") + + if not self.states: websession = async_get_clientsession(self.hass) + reason = None + unknown_err_msg = None try: - regions = await Client( - websession, user_input[CONF_API_KEY] - ).get_regions() + regions = await Client(websession).get_regions() except aiohttp.ClientResponseError as ex: - errors["base"] = "invalid_api_key" if ex.status == 401 else "unknown" + if ex.status == 429: + reason = "rate_limit" + else: + reason = "unknown" + unknown_err_msg = str(ex) except aiohttp.ClientConnectionError: - errors["base"] = "cannot_connect" - except aiohttp.ClientError: - errors["base"] = "unknown" + reason = "cannot_connect" + except aiohttp.ClientError as ex: + reason = "unknown" + unknown_err_msg = str(ex) except asyncio.TimeoutError: - errors["base"] = "timeout" + reason = "timeout" - if not errors and not regions: - errors["base"] = "unknown" + if not reason and not regions: + reason = "unknown" + unknown_err_msg = "no regions returned" - if not errors: - self.api_key = user_input[CONF_API_KEY] - self.states = regions["states"] - return await self.async_step_state() + if unknown_err_msg: + _LOGGER.error("Failed to connect to the service: %s", unknown_err_msg) - schema = vol.Schema( - { - vol.Required(CONF_API_KEY): str, - } - ) + if reason: + return self.async_abort(reason=reason) + self.states = regions["states"] - return self.async_show_form( - step_id="user", - data_schema=schema, - description_placeholders={"api_url": "https://api.ukrainealarm.com/"}, - errors=errors, - last_step=False, - ) - - async def async_step_state(self, user_input=None): - """Handle user-chosen state.""" - return await self._handle_pick_region("state", "district", user_input) + return await self._handle_pick_region("user", "district", user_input) async def async_step_district(self, user_input=None): """Handle user-chosen district.""" @@ -126,7 +122,6 @@ class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=self.selected_region["regionName"], data={ - CONF_API_KEY: self.api_key, CONF_REGION: self.selected_region["regionId"], CONF_NAME: self.selected_region["regionName"], }, diff --git a/homeassistant/components/ukraine_alarm/manifest.json b/homeassistant/components/ukraine_alarm/manifest.json index 08dad9960b5..5592ac774a4 100644 --- a/homeassistant/components/ukraine_alarm/manifest.json +++ b/homeassistant/components/ukraine_alarm/manifest.json @@ -3,7 +3,7 @@ "name": "Ukraine Alarm", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ukraine_alarm", - "requirements": ["ukrainealarm==0.0.1"], + "requirements": ["uasiren==0.0.1"], "codeowners": ["@PaulAnnekov"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ukraine_alarm/strings.json b/homeassistant/components/ukraine_alarm/strings.json index 79f81e71b08..6831d66adb3 100644 --- a/homeassistant/components/ukraine_alarm/strings.json +++ b/homeassistant/components/ukraine_alarm/strings.json @@ -1,22 +1,15 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" - }, - "error": { - "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "max_regions": "Max 5 regions can be configured", + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]", + "rate_limit": "Too much requests", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]", "timeout": "[%key:common::config_flow::error::timeout_connect%]" }, "step": { "user": { - "data": { - "api_key": "[%key:common::config_flow::data::api_key%]" - }, - "description": "Set up the Ukraine Alarm integration. To generate an API key go to {api_url}" - }, - "state": { "data": { "region": "Region" }, @@ -24,13 +17,13 @@ }, "district": { "data": { - "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" + "region": "[%key:component::ukraine_alarm::config::step::user::data::region%]" }, "description": "If you want to monitor not only state, choose its specific district" }, "community": { "data": { - "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" + "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" } diff --git a/homeassistant/components/ukraine_alarm/translations/en.json b/homeassistant/components/ukraine_alarm/translations/en.json index 2c39945cb87..857311ea3e7 100644 --- a/homeassistant/components/ukraine_alarm/translations/en.json +++ b/homeassistant/components/ukraine_alarm/translations/en.json @@ -1,15 +1,19 @@ { "config": { + "abort": { + "already_configured": "Location is already configured", + "cannot_connect": "Failed to connect", + "max_regions": "Max 5 regions can be configured", + "rate_limit": "Too much requests", + "timeout": "Timeout establishing connection", + "unknown": "Unexpected error" + }, "step": { - "user": { - "description": "Set up the Ukraine Alarm integration. To generate an API key go to {api_url}", - "title": "Ukraine Alarm" - }, - "state": { + "community": { "data": { "region": "Region" }, - "description": "Choose state to monitor" + "description": "If you want to monitor not only state and district, choose its specific community" }, "district": { "data": { @@ -17,12 +21,12 @@ }, "description": "If you want to monitor not only state, choose its specific district" }, - "community": { + "user": { "data": { "region": "Region" }, - "description": "If you want to monitor not only state and district, choose its specific community" + "description": "Choose state to monitor" } } } -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 0cb659a32d0..ef48c8117f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2343,7 +2343,7 @@ twitchAPI==2.5.2 uEagle==0.0.2 # homeassistant.components.ukraine_alarm -ukrainealarm==0.0.1 +uasiren==0.0.1 # homeassistant.components.unifiprotect unifi-discovery==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a905e42fae..cb683ae0bab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1525,7 +1525,7 @@ twitchAPI==2.5.2 uEagle==0.0.2 # homeassistant.components.ukraine_alarm -ukrainealarm==0.0.1 +uasiren==0.0.1 # homeassistant.components.unifiprotect unifi-discovery==1.1.2 diff --git a/tests/components/ukraine_alarm/test_config_flow.py b/tests/components/ukraine_alarm/test_config_flow.py index 3832e6a9fb6..7369816fdc7 100644 --- a/tests/components/ukraine_alarm/test_config_flow.py +++ b/tests/components/ukraine_alarm/test_config_flow.py @@ -3,15 +3,20 @@ import asyncio from collections.abc import Generator from unittest.mock import AsyncMock, patch -from aiohttp import ClientConnectionError, ClientError, ClientResponseError +from aiohttp import ClientConnectionError, ClientError, ClientResponseError, RequestInfo import pytest +from yarl import URL from homeassistant import config_entries from homeassistant.components.ukraine_alarm.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) -MOCK_API_KEY = "mock-api-key" +from tests.common import MockConfigEntry def _region(rid, recurse=0, depth=0): @@ -57,12 +62,7 @@ async def test_state(hass: HomeAssistant) -> None: ) assert result["type"] == RESULT_TYPE_FORM - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result2["type"] == RESULT_TYPE_FORM with patch( @@ -80,7 +80,6 @@ async def test_state(hass: HomeAssistant) -> None: assert result3["type"] == RESULT_TYPE_CREATE_ENTRY assert result3["title"] == "State 1" assert result3["data"] == { - "api_key": MOCK_API_KEY, "region": "1", "name": result3["title"], } @@ -94,12 +93,7 @@ async def test_state_district(hass: HomeAssistant) -> None: ) assert result["type"] == RESULT_TYPE_FORM - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result2["type"] == RESULT_TYPE_FORM result3 = await hass.config_entries.flow.async_configure( @@ -125,7 +119,6 @@ async def test_state_district(hass: HomeAssistant) -> None: assert result4["type"] == RESULT_TYPE_CREATE_ENTRY assert result4["title"] == "District 2.2" assert result4["data"] == { - "api_key": MOCK_API_KEY, "region": "2.2", "name": result4["title"], } @@ -139,12 +132,7 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: ) assert result["type"] == RESULT_TYPE_FORM - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result2["type"] == RESULT_TYPE_FORM result3 = await hass.config_entries.flow.async_configure( @@ -170,7 +158,6 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: assert result4["type"] == RESULT_TYPE_CREATE_ENTRY assert result4["title"] == "State 2" assert result4["data"] == { - "api_key": MOCK_API_KEY, "region": "2", "name": result4["title"], } @@ -186,9 +173,6 @@ async def test_state_district_community(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, ) assert result2["type"] == RESULT_TYPE_FORM @@ -223,132 +207,89 @@ async def test_state_district_community(hass: HomeAssistant) -> None: assert result5["type"] == RESULT_TYPE_CREATE_ENTRY assert result5["title"] == "Community 3.2.1" assert result5["data"] == { - "api_key": MOCK_API_KEY, "region": "3.2.1", "name": result5["title"], } assert len(mock_setup_entry.mock_calls) == 1 -async def test_invalid_api(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: - """Test we can create entry for just region.""" +async def test_max_regions(hass: HomeAssistant) -> None: + """Test max regions config.""" + for i in range(5): + MockConfigEntry( + domain=DOMAIN, + unique_id=i, + ).add_to_hass(hass) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - mock_get_regions.side_effect = ClientResponseError(None, None, status=401) + assert result["type"] == "abort" + assert result["reason"] == "max_regions" - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, + +async def test_rate_limit(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: + """Test rate limit error.""" + mock_get_regions.side_effect = ClientResponseError(None, None, status=429) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "invalid_api_key"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "rate_limit" async def test_server_error(hass: HomeAssistant, mock_get_regions) -> None: - """Test we can create entry for just region.""" + """Test server error.""" + mock_get_regions.side_effect = ClientResponseError( + RequestInfo(None, None, None, real_url=URL("/regions")), None, status=500 + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - - mock_get_regions.side_effect = ClientResponseError(None, None, status=500) - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "unknown"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" async def test_cannot_connect(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: - """Test we can create entry for just region.""" + """Test connection error.""" + mock_get_regions.side_effect = ClientConnectionError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - - mock_get_regions.side_effect = ClientConnectionError - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "cannot_connect"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" async def test_unknown_client_error( hass: HomeAssistant, mock_get_regions: AsyncMock ) -> None: - """Test we can create entry for just region.""" + """Test client error.""" + mock_get_regions.side_effect = ClientError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - - mock_get_regions.side_effect = ClientError - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "unknown"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" async def test_timeout_error(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: - """Test we can create entry for just region.""" + """Test timeout error.""" + mock_get_regions.side_effect = asyncio.TimeoutError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - - mock_get_regions.side_effect = asyncio.TimeoutError - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "timeout"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "timeout" async def test_no_regions_returned( hass: HomeAssistant, mock_get_regions: AsyncMock ) -> None: - """Test we can create entry for just region.""" + """Test regions not returned.""" + mock_get_regions.return_value = {} result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - - mock_get_regions.return_value = {} - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "unknown"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown"