Address late review of nina (#61915)

This commit is contained in:
Maximilian 2021-12-17 15:14:59 +00:00 committed by GitHub
parent ed1ce7d9f9
commit 703b689183
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 79 additions and 81 deletions

View File

@ -1,17 +1,18 @@
"""The Nina integration.""" """The Nina integration."""
from __future__ import annotations from __future__ import annotations
from datetime import timedelta import datetime as dt
from typing import Any from typing import Any
from async_timeout import timeout from async_timeout import timeout
from pynina import ApiError, Nina, Warning as NinaWarning from pynina import ApiError, Nina
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
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
from homeassistant.util import dt as dt_util
from .const import ( from .const import (
_LOGGER, _LOGGER,
@ -59,33 +60,26 @@ class NINADataUpdateCoordinator(DataUpdateCoordinator):
self.warnings: dict[str, Any] = {} self.warnings: dict[str, Any] = {}
self.corona_filter: bool = corona_filter self.corona_filter: bool = corona_filter
for region in regions.keys(): for region in regions:
self._nina.addRegion(region) self._nina.addRegion(region)
update_interval: timedelta = SCAN_INTERVAL super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_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.""" """Update data."""
try:
async with timeout(10): async with timeout(10):
try:
await self._nina.update() await self._nina.update()
return self._parse_data()
except ApiError as err: except ApiError as err:
raise UpdateFailed(err) from err raise UpdateFailed(err) from err
return self._parse_data()
def _parse_data(self) -> dict[str, Any]: def _parse_data(self) -> dict[str, Any]:
"""Parse warning data.""" """Parse warning data."""
return_data: dict[str, Any] = {} return_data: dict[str, Any] = {}
for ( for region_id, raw_warnings in self._nina.warnings.items():
region_id
) in self._nina.warnings: # pylint: disable=consider-using-dict-items
raw_warnings: list[NinaWarning] = self._nina.warnings[region_id]
warnings_for_regions: list[Any] = [] warnings_for_regions: list[Any] = []
for raw_warn in raw_warnings: for raw_warn in raw_warnings:
@ -95,12 +89,22 @@ class NINADataUpdateCoordinator(DataUpdateCoordinator):
warn_obj: dict[str, Any] = { warn_obj: dict[str, Any] = {
ATTR_ID: raw_warn.id, ATTR_ID: raw_warn.id,
ATTR_HEADLINE: raw_warn.headline, ATTR_HEADLINE: raw_warn.headline,
ATTR_SENT: raw_warn.sent or "", ATTR_SENT: self._to_utc(raw_warn.sent),
ATTR_START: raw_warn.start or "", ATTR_START: self._to_utc(raw_warn.start),
ATTR_EXPIRES: raw_warn.expires or "", ATTR_EXPIRES: self._to_utc(raw_warn.expires),
} }
warnings_for_regions.append(warn_obj) warnings_for_regions.append(warn_obj)
return_data[region_id] = warnings_for_regions return_data[region_id] = warnings_for_regions
return return_data return return_data
@staticmethod
def _to_utc(input_time: str) -> str | None:
if input_time:
return (
dt.datetime.fromisoformat(input_time)
.astimezone(dt_util.UTC)
.isoformat()
)
return None

View File

@ -53,37 +53,33 @@ class NINAMessage(CoordinatorEntity, BinarySensorEntity):
self, self,
coordinator: NINADataUpdateCoordinator, coordinator: NINADataUpdateCoordinator,
region: str, region: str,
regionName: str, region_name: str,
slotID: int, slot_id: int,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
self._region: str = region self._region: str = region
self._region_name: str = regionName self._warning_index: int = slot_id - 1
self._slot_id: int = slotID
self._warning_index: int = slotID - 1
self._coordinator: NINADataUpdateCoordinator = coordinator self._attr_name: str = f"Warning: {region_name} {slot_id}"
self._attr_unique_id: str = f"{region}-{slot_id}"
self._attr_name: str = f"Warning: {self._region_name} {self._slot_id}"
self._attr_unique_id: str = f"{self._region}-{self._slot_id}"
self._attr_device_class: str = BinarySensorDeviceClass.SAFETY self._attr_device_class: str = BinarySensorDeviceClass.SAFETY
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return the state of the sensor.""" """Return the state of the sensor."""
return len(self._coordinator.data[self._region]) > self._warning_index return len(self.coordinator.data[self._region]) > self._warning_index
@property @property
def extra_state_attributes(self) -> dict[str, Any]: def extra_state_attributes(self) -> dict[str, Any]:
"""Return extra attributes of the sensor.""" """Return extra attributes of the sensor."""
if ( if (
not len(self._coordinator.data[self._region]) > self._warning_index not len(self.coordinator.data[self._region]) > self._warning_index
) or not self.is_on: ) or not self.is_on:
return {} return {}
data: dict[str, Any] = self._coordinator.data[self._region][self._warning_index] data: dict[str, Any] = self.coordinator.data[self._region][self._warning_index]
return { return {
ATTR_HEADLINE: data[ATTR_HEADLINE], ATTR_HEADLINE: data[ATTR_HEADLINE],

View File

@ -8,6 +8,7 @@ import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from .const import ( from .const import (
@ -45,53 +46,46 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if self._async_current_entries(): if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed") return self.async_abort(reason="single_instance_allowed")
has_error: bool = False if not self._all_region_codes_sorted:
nina: Nina = Nina(async_get_clientsession(self.hass))
if len(self._all_region_codes_sorted) == 0:
try: try:
nina: Nina = Nina()
self._all_region_codes_sorted = self.swap_key_value( self._all_region_codes_sorted = self.swap_key_value(
await nina.getAllRegionalCodes() await nina.getAllRegionalCodes()
) )
except ApiError:
errors["base"] = "cannot_connect"
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception: %s", err)
return self.async_abort(reason="unknown")
self.split_regions() self.split_regions()
except ApiError as err: if user_input is not None and not errors:
_LOGGER.warning("NINA setup error: %s", err) user_input[CONF_REGIONS] = []
errors["base"] = "cannot_connect"
has_error = True
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception: %s", err)
errors["base"] = "unknown"
return self.async_abort(reason="unknown")
if user_input is not None and not has_error:
config: dict[str, Any] = user_input
config[CONF_REGIONS] = []
for group in CONST_REGIONS: for group in CONST_REGIONS:
if group_input := user_input.get(group): if group_input := user_input.get(group):
config[CONF_REGIONS] += group_input user_input[CONF_REGIONS] += group_input
if len(config[CONF_REGIONS]) > 0: if user_input[CONF_REGIONS]:
tmp: dict[str, Any] = {} tmp: dict[str, Any] = {}
for reg in config[CONF_REGIONS]: for reg in user_input[CONF_REGIONS]:
tmp[self._all_region_codes_sorted[reg]] = reg.split("_", 1)[0] tmp[self._all_region_codes_sorted[reg]] = reg.split("_", 1)[0]
compact: dict[str, Any] = {} compact: dict[str, Any] = {}
for key, val in tmp.items(): for key, val in tmp.items():
if val in compact: if val in compact:
# Abenberg, St + Abenberger Wald
compact[val] = f"{compact[val]} + {key}" compact[val] = f"{compact[val]} + {key}"
break break
compact[val] = key compact[val] = key
config[CONF_REGIONS] = compact user_input[CONF_REGIONS] = compact
return self.async_create_entry(title="NINA", data=config) return self.async_create_entry(title="NINA", data=user_input)
errors["base"] = "no_selection" errors["base"] = "no_selection"
@ -122,7 +116,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
all_region_codes_swaped[value] = key all_region_codes_swaped[value] = key
else: else:
for i in range(len(dict_to_sort)): for i in range(len(dict_to_sort)):
tmp_value: str = value + "_" + str(i) tmp_value: str = f"{value}_{i}"
if tmp_value not in all_region_codes_swaped: if tmp_value not in all_region_codes_swaped:
all_region_codes_swaped[tmp_value] = key all_region_codes_swaped[tmp_value] = key
break break

View File

@ -3,8 +3,6 @@ import json
from typing import Any, Dict from typing import Any, Dict
from unittest.mock import patch from unittest.mock import patch
from pynina import ApiError
from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.nina.const import ( from homeassistant.components.nina.const import (
ATTR_EXPIRES, ATTR_EXPIRES,
@ -64,9 +62,9 @@ async def test_sensors(hass: HomeAssistant) -> None:
assert state_w1.state == STATE_ON assert state_w1.state == STATE_ON
assert state_w1.attributes.get(ATTR_HEADLINE) == "Ausfall Notruf 112" assert state_w1.attributes.get(ATTR_HEADLINE) == "Ausfall Notruf 112"
assert state_w1.attributes.get(ATTR_ID) == "mow.DE-NW-BN-SE030-20201014-30-000" assert state_w1.attributes.get(ATTR_ID) == "mow.DE-NW-BN-SE030-20201014-30-000"
assert state_w1.attributes.get(ATTR_SENT) == "2021-10-11T05:20:00+01:00" assert state_w1.attributes.get(ATTR_SENT) == "2021-10-11T04:20:00+00:00"
assert state_w1.attributes.get(ATTR_START) == "2021-11-01T05:20:00+01:00" assert state_w1.attributes.get(ATTR_START) == "2021-11-01T04:20:00+00:00"
assert state_w1.attributes.get(ATTR_EXPIRES) == "3021-11-22T05:19:00+01:00" assert state_w1.attributes.get(ATTR_EXPIRES) == "3021-11-22T04:19:00+00:00"
assert entry_w1.unique_id == "083350000000-1" assert entry_w1.unique_id == "083350000000-1"
assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
@ -157,9 +155,9 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None:
== "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen" == "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen"
) )
assert state_w1.attributes.get(ATTR_ID) == "mow.DE-BW-S-SE018-20211102-18-001" assert state_w1.attributes.get(ATTR_ID) == "mow.DE-BW-S-SE018-20211102-18-001"
assert state_w1.attributes.get(ATTR_SENT) == "2021-11-02T20:07:16+01:00" assert state_w1.attributes.get(ATTR_SENT) == "2021-11-02T19:07:16+00:00"
assert state_w1.attributes.get(ATTR_START) == "" assert state_w1.attributes.get(ATTR_START) is None
assert state_w1.attributes.get(ATTR_EXPIRES) == "" assert state_w1.attributes.get(ATTR_EXPIRES) is None
assert entry_w1.unique_id == "083350000000-1" assert entry_w1.unique_id == "083350000000-1"
assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
@ -170,9 +168,9 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None:
assert state_w2.state == STATE_ON assert state_w2.state == STATE_ON
assert state_w2.attributes.get(ATTR_HEADLINE) == "Ausfall Notruf 112" assert state_w2.attributes.get(ATTR_HEADLINE) == "Ausfall Notruf 112"
assert state_w2.attributes.get(ATTR_ID) == "mow.DE-NW-BN-SE030-20201014-30-000" assert state_w2.attributes.get(ATTR_ID) == "mow.DE-NW-BN-SE030-20201014-30-000"
assert state_w2.attributes.get(ATTR_SENT) == "2021-10-11T05:20:00+01:00" assert state_w2.attributes.get(ATTR_SENT) == "2021-10-11T04:20:00+00:00"
assert state_w2.attributes.get(ATTR_START) == "2021-11-01T05:20:00+01:00" assert state_w2.attributes.get(ATTR_START) == "2021-11-01T04:20:00+00:00"
assert state_w2.attributes.get(ATTR_EXPIRES) == "3021-11-22T05:19:00+01:00" assert state_w2.attributes.get(ATTR_EXPIRES) == "3021-11-22T04:19:00+00:00"
assert entry_w2.unique_id == "083350000000-2" assert entry_w2.unique_id == "083350000000-2"
assert state_w2.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY assert state_w2.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
@ -215,21 +213,3 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None:
assert entry_w5.unique_id == "083350000000-5" assert entry_w5.unique_id == "083350000000-5"
assert state_w5.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY assert state_w5.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
async def test_sensors_connection_error(hass: HomeAssistant) -> None:
"""Test the creation and values of the NINA sensors with no connected."""
with patch(
"pynina.baseApi.BaseAPI._makeRequest",
side_effect=ApiError("Could not connect to Api"),
):
conf_entry: MockConfigEntry = MockConfigEntry(
domain=DOMAIN, title="NINA", data=ENTRY_DATA
)
conf_entry.add_to_hass(hass)
await hass.config_entries.async_setup(conf_entry.entry_id)
await hass.async_block_till_done()
assert conf_entry.state == ConfigEntryState.SETUP_RETRY

View File

@ -87,6 +87,9 @@ async def test_step_user(hass: HomeAssistant) -> None:
with patch( with patch(
"pynina.baseApi.BaseAPI._makeRequest", "pynina.baseApi.BaseAPI._makeRequest",
return_value=DUMMY_RESPONSE, return_value=DUMMY_RESPONSE,
), patch(
"homeassistant.components.nina.async_setup_entry",
return_value=True,
): ):
result: dict[str, Any] = await hass.config_entries.flow.async_init( result: dict[str, Any] = await hass.config_entries.flow.async_init(
@ -128,3 +131,4 @@ async def test_step_user_already_configured(hass: HomeAssistant) -> None:
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "single_instance_allowed"

View File

@ -3,6 +3,8 @@ import json
from typing import Any, Dict from typing import Any, Dict
from unittest.mock import patch from unittest.mock import patch
from pynina import ApiError
from homeassistant.components.nina.const import DOMAIN from homeassistant.components.nina.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -44,3 +46,21 @@ async def test_config_entry_not_ready(hass: HomeAssistant) -> None:
entry: MockConfigEntry = await init_integration(hass) entry: MockConfigEntry = await init_integration(hass)
assert entry.state == ConfigEntryState.LOADED assert entry.state == ConfigEntryState.LOADED
async def test_sensors_connection_error(hass: HomeAssistant) -> None:
"""Test the creation and values of the NINA sensors with no connected."""
with patch(
"pynina.baseApi.BaseAPI._makeRequest",
side_effect=ApiError("Could not connect to Api"),
):
conf_entry: MockConfigEntry = MockConfigEntry(
domain=DOMAIN, title="NINA", data=ENTRY_DATA
)
conf_entry.add_to_hass(hass)
await hass.config_entries.async_setup(conf_entry.entry_id)
await hass.async_block_till_done()
assert conf_entry.state == ConfigEntryState.SETUP_RETRY