Changed API for Ukraine Alarm (#71754)

This commit is contained in:
Paul Annekov 2022-05-13 02:45:39 +03:00 committed by Paulus Schoutsen
parent 2500cc6132
commit f65eca9c19
8 changed files with 108 additions and 179 deletions

View File

@ -7,10 +7,10 @@ from typing import Any
import aiohttp import aiohttp
from aiohttp import ClientSession from aiohttp import ClientSession
from ukrainealarm.client import Client from uasiren.client import Client
from homeassistant.config_entries import ConfigEntry 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.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 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: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Ukraine Alarm as config entry.""" """Set up Ukraine Alarm as config entry."""
api_key = entry.data[CONF_API_KEY]
region_id = entry.data[CONF_REGION] region_id = entry.data[CONF_REGION]
websession = async_get_clientsession(hass) websession = async_get_clientsession(hass)
coordinator = UkraineAlarmDataUpdateCoordinator( coordinator = UkraineAlarmDataUpdateCoordinator(hass, websession, region_id)
hass, websession, api_key, region_id
)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
@ -56,19 +53,18 @@ class UkraineAlarmDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self, self,
hass: HomeAssistant, hass: HomeAssistant,
session: ClientSession, session: ClientSession,
api_key: str,
region_id: str, region_id: str,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
self.region_id = region_id 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) super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL)
async def _async_update_data(self) -> dict[str, Any]: async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library.""" """Update data via library."""
try: try:
res = await self.ukrainealarm.get_alerts(self.region_id) res = await self.uasiren.get_alerts(self.region_id)
except aiohttp.ClientError as error: except aiohttp.ClientError as error:
raise UpdateFailed(f"Error fetching alerts from API: {error}") from error raise UpdateFailed(f"Error fetching alerts from API: {error}") from error

View File

@ -2,17 +2,20 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import logging
import aiohttp import aiohttp
from ukrainealarm.client import Client from uasiren.client import Client
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries 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 homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for Ukraine Alarm.""" """Config flow for Ukraine Alarm."""
@ -21,54 +24,47 @@ class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self): def __init__(self):
"""Initialize a new UkraineAlarmConfigFlow.""" """Initialize a new UkraineAlarmConfigFlow."""
self.api_key = None
self.states = None self.states = None
self.selected_region = None self.selected_region = None
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user.""" """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) websession = async_get_clientsession(self.hass)
reason = None
unknown_err_msg = None
try: try:
regions = await Client( regions = await Client(websession).get_regions()
websession, user_input[CONF_API_KEY]
).get_regions()
except aiohttp.ClientResponseError as ex: 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: except aiohttp.ClientConnectionError:
errors["base"] = "cannot_connect" reason = "cannot_connect"
except aiohttp.ClientError: except aiohttp.ClientError as ex:
errors["base"] = "unknown" reason = "unknown"
unknown_err_msg = str(ex)
except asyncio.TimeoutError: except asyncio.TimeoutError:
errors["base"] = "timeout" reason = "timeout"
if not errors and not regions: if not reason and not regions:
errors["base"] = "unknown" reason = "unknown"
unknown_err_msg = "no regions returned"
if not errors: if unknown_err_msg:
self.api_key = user_input[CONF_API_KEY] _LOGGER.error("Failed to connect to the service: %s", unknown_err_msg)
if reason:
return self.async_abort(reason=reason)
self.states = regions["states"] self.states = regions["states"]
return await self.async_step_state()
schema = vol.Schema( return await self._handle_pick_region("user", "district", user_input)
{
vol.Required(CONF_API_KEY): str,
}
)
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)
async def async_step_district(self, user_input=None): async def async_step_district(self, user_input=None):
"""Handle user-chosen district.""" """Handle user-chosen district."""
@ -126,7 +122,6 @@ class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry( return self.async_create_entry(
title=self.selected_region["regionName"], title=self.selected_region["regionName"],
data={ data={
CONF_API_KEY: self.api_key,
CONF_REGION: self.selected_region["regionId"], CONF_REGION: self.selected_region["regionId"],
CONF_NAME: self.selected_region["regionName"], CONF_NAME: self.selected_region["regionName"],
}, },

View File

@ -3,7 +3,7 @@
"name": "Ukraine Alarm", "name": "Ukraine Alarm",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ukraine_alarm", "documentation": "https://www.home-assistant.io/integrations/ukraine_alarm",
"requirements": ["ukrainealarm==0.0.1"], "requirements": ["uasiren==0.0.1"],
"codeowners": ["@PaulAnnekov"], "codeowners": ["@PaulAnnekov"],
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
} }

View File

@ -1,22 +1,15 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]" "max_regions": "Max 5 regions can be configured",
}, "already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
"error": { "rate_limit": "Too much requests",
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]", "unknown": "[%key:common::config_flow::error::unknown%]",
"timeout": "[%key:common::config_flow::error::timeout_connect%]" "timeout": "[%key:common::config_flow::error::timeout_connect%]"
}, },
"step": { "step": {
"user": { "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": { "data": {
"region": "Region" "region": "Region"
}, },
@ -24,13 +17,13 @@
}, },
"district": { "district": {
"data": { "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" "description": "If you want to monitor not only state, choose its specific district"
}, },
"community": { "community": {
"data": { "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" "description": "If you want to monitor not only state and district, choose its specific community"
} }

View File

@ -1,15 +1,19 @@
{ {
"config": { "config": {
"step": { "abort": {
"user": { "already_configured": "Location is already configured",
"description": "Set up the Ukraine Alarm integration. To generate an API key go to {api_url}", "cannot_connect": "Failed to connect",
"title": "Ukraine Alarm" "max_regions": "Max 5 regions can be configured",
"rate_limit": "Too much requests",
"timeout": "Timeout establishing connection",
"unknown": "Unexpected error"
}, },
"state": { "step": {
"community": {
"data": { "data": {
"region": "Region" "region": "Region"
}, },
"description": "Choose state to monitor" "description": "If you want to monitor not only state and district, choose its specific community"
}, },
"district": { "district": {
"data": { "data": {
@ -17,11 +21,11 @@
}, },
"description": "If you want to monitor not only state, choose its specific district" "description": "If you want to monitor not only state, choose its specific district"
}, },
"community": { "user": {
"data": { "data": {
"region": "Region" "region": "Region"
}, },
"description": "If you want to monitor not only state and district, choose its specific community" "description": "Choose state to monitor"
} }
} }
} }

View File

@ -2343,7 +2343,7 @@ twitchAPI==2.5.2
uEagle==0.0.2 uEagle==0.0.2
# homeassistant.components.ukraine_alarm # homeassistant.components.ukraine_alarm
ukrainealarm==0.0.1 uasiren==0.0.1
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
unifi-discovery==1.1.2 unifi-discovery==1.1.2

View File

@ -1525,7 +1525,7 @@ twitchAPI==2.5.2
uEagle==0.0.2 uEagle==0.0.2
# homeassistant.components.ukraine_alarm # homeassistant.components.ukraine_alarm
ukrainealarm==0.0.1 uasiren==0.0.1
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
unifi-discovery==1.1.2 unifi-discovery==1.1.2

View File

@ -3,15 +3,20 @@ import asyncio
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from aiohttp import ClientConnectionError, ClientError, ClientResponseError from aiohttp import ClientConnectionError, ClientError, ClientResponseError, RequestInfo
import pytest import pytest
from yarl import URL
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.ukraine_alarm.const import DOMAIN from homeassistant.components.ukraine_alarm.const import DOMAIN
from homeassistant.core import HomeAssistant 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): def _region(rid, recurse=0, depth=0):
@ -57,12 +62,7 @@ async def test_state(hass: HomeAssistant) -> None:
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_FORM
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(result["flow_id"])
result["flow_id"],
{
"api_key": MOCK_API_KEY,
},
)
assert result2["type"] == RESULT_TYPE_FORM assert result2["type"] == RESULT_TYPE_FORM
with patch( with patch(
@ -80,7 +80,6 @@ async def test_state(hass: HomeAssistant) -> None:
assert result3["type"] == RESULT_TYPE_CREATE_ENTRY assert result3["type"] == RESULT_TYPE_CREATE_ENTRY
assert result3["title"] == "State 1" assert result3["title"] == "State 1"
assert result3["data"] == { assert result3["data"] == {
"api_key": MOCK_API_KEY,
"region": "1", "region": "1",
"name": result3["title"], "name": result3["title"],
} }
@ -94,12 +93,7 @@ async def test_state_district(hass: HomeAssistant) -> None:
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_FORM
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(result["flow_id"])
result["flow_id"],
{
"api_key": MOCK_API_KEY,
},
)
assert result2["type"] == RESULT_TYPE_FORM assert result2["type"] == RESULT_TYPE_FORM
result3 = await hass.config_entries.flow.async_configure( 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["type"] == RESULT_TYPE_CREATE_ENTRY
assert result4["title"] == "District 2.2" assert result4["title"] == "District 2.2"
assert result4["data"] == { assert result4["data"] == {
"api_key": MOCK_API_KEY,
"region": "2.2", "region": "2.2",
"name": result4["title"], "name": result4["title"],
} }
@ -139,12 +132,7 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None:
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_FORM
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(result["flow_id"])
result["flow_id"],
{
"api_key": MOCK_API_KEY,
},
)
assert result2["type"] == RESULT_TYPE_FORM assert result2["type"] == RESULT_TYPE_FORM
result3 = await hass.config_entries.flow.async_configure( 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["type"] == RESULT_TYPE_CREATE_ENTRY
assert result4["title"] == "State 2" assert result4["title"] == "State 2"
assert result4["data"] == { assert result4["data"] == {
"api_key": MOCK_API_KEY,
"region": "2", "region": "2",
"name": result4["title"], "name": result4["title"],
} }
@ -186,9 +173,6 @@ async def test_state_district_community(hass: HomeAssistant) -> None:
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{
"api_key": MOCK_API_KEY,
},
) )
assert result2["type"] == RESULT_TYPE_FORM 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["type"] == RESULT_TYPE_CREATE_ENTRY
assert result5["title"] == "Community 3.2.1" assert result5["title"] == "Community 3.2.1"
assert result5["data"] == { assert result5["data"] == {
"api_key": MOCK_API_KEY,
"region": "3.2.1", "region": "3.2.1",
"name": result5["title"], "name": result5["title"],
} }
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
async def test_invalid_api(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: async def test_max_regions(hass: HomeAssistant) -> None:
"""Test we can create entry for just region.""" """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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} 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"], async def test_rate_limit(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None:
{ """Test rate limit error."""
"api_key": MOCK_API_KEY, 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 result["type"] == RESULT_TYPE_ABORT
assert result2["step_id"] == "user" assert result["reason"] == "rate_limit"
assert result2["errors"] == {"base": "invalid_api_key"}
async def test_server_error(hass: HomeAssistant, mock_get_regions) -> None: 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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "unknown"
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"}
async def test_cannot_connect(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: 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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "cannot_connect"
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"}
async def test_unknown_client_error( async def test_unknown_client_error(
hass: HomeAssistant, mock_get_regions: AsyncMock hass: HomeAssistant, mock_get_regions: AsyncMock
) -> None: ) -> 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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "unknown"
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"}
async def test_timeout_error(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: 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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "timeout"
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"}
async def test_no_regions_returned( async def test_no_regions_returned(
hass: HomeAssistant, mock_get_regions: AsyncMock hass: HomeAssistant, mock_get_regions: AsyncMock
) -> None: ) -> 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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "unknown"
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"}