Changed API for Ukraine Alarm (#71754)

This commit is contained in:
Paul Annekov
2022-05-13 02:45:39 +03:00
committed by GitHub
parent 7e49ae6410
commit d76ff7d5a2
8 changed files with 99 additions and 181 deletions

View File

@ -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

View File

@ -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"],
},

View File

@ -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"
}

View File

@ -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"
}

View File

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

View File

@ -2349,7 +2349,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

View File

@ -1531,7 +1531,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

View File

@ -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"