mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 09:47:52 +00:00
Add nina integration (#56647)
* Added nina integration * Improvements implemented * Fixed lint errors * Added tests * Improvements implemented * Use client session from HA * Added custom coordinator * Fixed tests * Fix pylint errors * Library updated to 0.1.4 * Optimization of static attributes * Removed unused code * Switched to BinarySensorDeviceClass * Switched to Platform Enum * Improve repetition * Improve repetition * Fix corona filter * Removed intermediate variable Co-authored-by: Marvin Wichmann <marvin@fam-wichmann.de> * Fix black formatting Co-authored-by: Marvin Wichmann <marvin@fam-wichmann.de>
This commit is contained in:
parent
cf371ea8dd
commit
9f7b8d3009
@ -353,6 +353,7 @@ homeassistant/components/nextcloud/* @meichthys
|
||||
homeassistant/components/nfandroidtv/* @tkdrob
|
||||
homeassistant/components/nightscout/* @marciogranzotto
|
||||
homeassistant/components/nilu/* @hfurubotten
|
||||
homeassistant/components/nina/* @DeerMaximum
|
||||
homeassistant/components/nissan_leaf/* @filcole
|
||||
homeassistant/components/nmap_tracker/* @bdraco
|
||||
homeassistant/components/nmbs/* @thibmaek
|
||||
|
106
homeassistant/components/nina/__init__.py
Normal file
106
homeassistant/components/nina/__init__.py
Normal file
@ -0,0 +1,106 @@
|
||||
"""The Nina integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from async_timeout import timeout
|
||||
from pynina import ApiError, Nina, Warning as NinaWarning
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
_LOGGER,
|
||||
ATTR_EXPIRES,
|
||||
ATTR_HEADLINE,
|
||||
ATTR_ID,
|
||||
ATTR_SENT,
|
||||
ATTR_START,
|
||||
CONF_FILTER_CORONA,
|
||||
CONF_REGIONS,
|
||||
DOMAIN,
|
||||
SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
PLATFORMS: list[str] = [Platform.BINARY_SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up platform from a ConfigEntry."""
|
||||
|
||||
regions: dict[str, str] = entry.data[CONF_REGIONS]
|
||||
|
||||
coordinator = NINADataUpdateCoordinator(
|
||||
hass, regions, entry.data[CONF_FILTER_CORONA]
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class NINADataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to manage fetching NINA data API."""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, regions: dict[str, str], corona_filter: bool
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
self._regions: dict[str, str] = regions
|
||||
self._nina: Nina = Nina(async_get_clientsession(hass))
|
||||
self.warnings: dict[str, Any] = {}
|
||||
self.corona_filter: bool = corona_filter
|
||||
|
||||
for region in regions.keys():
|
||||
self._nina.addRegion(region)
|
||||
|
||||
update_interval: timedelta = SCAN_INTERVAL
|
||||
|
||||
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update data."""
|
||||
|
||||
try:
|
||||
async with timeout(10):
|
||||
await self._nina.update()
|
||||
return self._parse_data()
|
||||
except ApiError as err:
|
||||
raise UpdateFailed(err) from err
|
||||
|
||||
def _parse_data(self) -> dict[str, Any]:
|
||||
"""Parse warning data."""
|
||||
|
||||
return_data: dict[str, Any] = {}
|
||||
|
||||
for (
|
||||
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] = []
|
||||
|
||||
for raw_warn in raw_warnings:
|
||||
if "corona" in raw_warn.headline.lower() and self.corona_filter:
|
||||
continue
|
||||
|
||||
warn_obj: dict[str, Any] = {
|
||||
ATTR_ID: raw_warn.id,
|
||||
ATTR_HEADLINE: raw_warn.headline,
|
||||
ATTR_SENT: raw_warn.sent or "",
|
||||
ATTR_START: raw_warn.start or "",
|
||||
ATTR_EXPIRES: raw_warn.expires or "",
|
||||
}
|
||||
warnings_for_regions.append(warn_obj)
|
||||
|
||||
return_data[region_id] = warnings_for_regions
|
||||
|
||||
return return_data
|
94
homeassistant/components/nina/binary_sensor.py
Normal file
94
homeassistant/components/nina/binary_sensor.py
Normal file
@ -0,0 +1,94 @@
|
||||
"""NINA sensor platform."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import NINADataUpdateCoordinator
|
||||
from .const import (
|
||||
ATTR_EXPIRES,
|
||||
ATTR_HEADLINE,
|
||||
ATTR_ID,
|
||||
ATTR_SENT,
|
||||
ATTR_START,
|
||||
CONF_MESSAGE_SLOTS,
|
||||
CONF_REGIONS,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up entries."""
|
||||
|
||||
coordinator: NINADataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
regions: dict[str, str] = config_entry.data[CONF_REGIONS]
|
||||
message_slots: int = config_entry.data[CONF_MESSAGE_SLOTS]
|
||||
|
||||
entities: list[NINAMessage] = []
|
||||
|
||||
for ent in coordinator.data:
|
||||
for i in range(0, message_slots):
|
||||
entities.append(NINAMessage(coordinator, ent, regions[ent], i + 1))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class NINAMessage(CoordinatorEntity, BinarySensorEntity):
|
||||
"""Representation of an NINA warning."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: NINADataUpdateCoordinator,
|
||||
region: str,
|
||||
regionName: str,
|
||||
slotID: int,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._region: str = region
|
||||
self._region_name: str = regionName
|
||||
self._slot_id: int = slotID
|
||||
self._warning_index: int = slotID - 1
|
||||
|
||||
self._coordinator: NINADataUpdateCoordinator = coordinator
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the state of the sensor."""
|
||||
return len(self._coordinator.data[self._region]) > self._warning_index
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return extra attributes of the sensor."""
|
||||
if (
|
||||
not len(self._coordinator.data[self._region]) > self._warning_index
|
||||
) or not self.is_on:
|
||||
return {}
|
||||
|
||||
data: dict[str, Any] = self._coordinator.data[self._region][self._warning_index]
|
||||
|
||||
return {
|
||||
ATTR_HEADLINE: data[ATTR_HEADLINE],
|
||||
ATTR_ID: data[ATTR_ID],
|
||||
ATTR_SENT: data[ATTR_SENT],
|
||||
ATTR_START: data[ATTR_START],
|
||||
ATTR_EXPIRES: data[ATTR_EXPIRES],
|
||||
}
|
138
homeassistant/components/nina/config_flow.py
Normal file
138
homeassistant/components/nina/config_flow.py
Normal file
@ -0,0 +1,138 @@
|
||||
"""Config flow for Nina integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pynina import ApiError, Nina
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import (
|
||||
_LOGGER,
|
||||
CONF_FILTER_CORONA,
|
||||
CONF_MESSAGE_SLOTS,
|
||||
CONF_REGIONS,
|
||||
CONST_REGION_MAPPING,
|
||||
CONST_REGIONS,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for NINA."""
|
||||
|
||||
VERSION: int = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__()
|
||||
self._all_region_codes_sorted: dict[str, str] = {}
|
||||
self.regions: dict[str, dict[str, Any]] = {}
|
||||
|
||||
for name in CONST_REGIONS:
|
||||
self.regions[name] = {}
|
||||
|
||||
async def async_step_user(
|
||||
self: ConfigFlow,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, Any] = {}
|
||||
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
has_error: bool = False
|
||||
|
||||
if len(self._all_region_codes_sorted) == 0:
|
||||
try:
|
||||
nina: Nina = Nina()
|
||||
|
||||
self._all_region_codes_sorted = self.swap_key_value(
|
||||
await nina.getAllRegionalCodes()
|
||||
)
|
||||
|
||||
self.split_regions()
|
||||
|
||||
except ApiError as err:
|
||||
_LOGGER.warning("NINA setup error: %s", err)
|
||||
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:
|
||||
if group_input := user_input.get(group):
|
||||
config[CONF_REGIONS] += group_input
|
||||
|
||||
if len(config[CONF_REGIONS]) > 0:
|
||||
tmp: dict[str, Any] = {}
|
||||
|
||||
for reg in config[CONF_REGIONS]:
|
||||
tmp[self._all_region_codes_sorted[reg]] = reg.split("_", 1)[0]
|
||||
|
||||
compact: dict[str, Any] = {}
|
||||
|
||||
for key, val in tmp.items():
|
||||
if val in compact:
|
||||
compact[val] = f"{compact[val]} + {key}"
|
||||
break
|
||||
compact[val] = key
|
||||
|
||||
config[CONF_REGIONS] = compact
|
||||
|
||||
return self.async_create_entry(title="NINA", data=config)
|
||||
|
||||
errors["base"] = "no_selection"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
**{
|
||||
vol.Optional(region): cv.multi_select(self.regions[region])
|
||||
for region in CONST_REGIONS
|
||||
},
|
||||
vol.Required(CONF_MESSAGE_SLOTS, default=5): vol.All(
|
||||
int, vol.Range(min=1, max=20)
|
||||
),
|
||||
vol.Required(CONF_FILTER_CORONA, default=True): cv.boolean,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def swap_key_value(dict_to_sort: dict[str, str]) -> dict[str, str]:
|
||||
"""Swap keys and values in dict."""
|
||||
all_region_codes_swaped: dict[str, str] = {}
|
||||
|
||||
for key, value in dict_to_sort.items():
|
||||
if value not in all_region_codes_swaped:
|
||||
all_region_codes_swaped[value] = key
|
||||
else:
|
||||
for i in range(len(dict_to_sort)):
|
||||
tmp_value: str = value + "_" + str(i)
|
||||
if tmp_value not in all_region_codes_swaped:
|
||||
all_region_codes_swaped[tmp_value] = key
|
||||
break
|
||||
|
||||
return dict(sorted(all_region_codes_swaped.items(), key=lambda ele: ele[1]))
|
||||
|
||||
def split_regions(self) -> None:
|
||||
"""Split regions alphabetical."""
|
||||
for index, name in self._all_region_codes_sorted.items():
|
||||
for region_name, grouping_letters in CONST_REGION_MAPPING.items():
|
||||
if name[0] in grouping_letters:
|
||||
self.regions[region_name][index] = name
|
||||
break
|
53
homeassistant/components/nina/const.py
Normal file
53
homeassistant/components/nina/const.py
Normal file
@ -0,0 +1,53 @@
|
||||
"""Constants for the Nina integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from logging import Logger, getLogger
|
||||
from typing import Final
|
||||
|
||||
_LOGGER: Logger = getLogger(__package__)
|
||||
|
||||
SCAN_INTERVAL: timedelta = timedelta(minutes=5)
|
||||
|
||||
DOMAIN: str = "nina"
|
||||
|
||||
CONF_REGIONS: str = "regions"
|
||||
CONF_MESSAGE_SLOTS: str = "slots"
|
||||
CONF_FILTER_CORONA: str = "corona_filter"
|
||||
|
||||
ATTR_HEADLINE: str = "Headline"
|
||||
ATTR_ID: str = "ID"
|
||||
ATTR_SENT: str = "Sent"
|
||||
ATTR_START: str = "Start"
|
||||
ATTR_EXPIRES: str = "Expires"
|
||||
|
||||
CONST_LIST_A_TO_D: list[str] = ["A", "Ä", "B", "C", "D"]
|
||||
CONST_LIST_E_TO_H: list[str] = ["E", "F", "G", "H"]
|
||||
CONST_LIST_I_TO_L: list[str] = ["I", "J", "K", "L"]
|
||||
CONST_LIST_M_TO_Q: list[str] = ["M", "N", "O", "Ö", "P", "Q"]
|
||||
CONST_LIST_R_TO_U: list[str] = ["R", "S", "T", "U", "Ü"]
|
||||
CONST_LIST_V_TO_Z: list[str] = ["V", "W", "X", "Y"]
|
||||
|
||||
CONST_REGION_A_TO_D: Final = "_a_to_d"
|
||||
CONST_REGION_E_TO_H: Final = "_e_to_h"
|
||||
CONST_REGION_I_TO_L: Final = "_i_to_l"
|
||||
CONST_REGION_M_TO_Q: Final = "_m_to_q"
|
||||
CONST_REGION_R_TO_U: Final = "_r_to_u"
|
||||
CONST_REGION_V_TO_Z: Final = "_v_to_z"
|
||||
CONST_REGIONS: Final = [
|
||||
CONST_REGION_A_TO_D,
|
||||
CONST_REGION_E_TO_H,
|
||||
CONST_REGION_I_TO_L,
|
||||
CONST_REGION_M_TO_Q,
|
||||
CONST_REGION_R_TO_U,
|
||||
CONST_REGION_V_TO_Z,
|
||||
]
|
||||
|
||||
CONST_REGION_MAPPING: dict[str, list[str]] = {
|
||||
CONST_REGION_A_TO_D: CONST_LIST_A_TO_D,
|
||||
CONST_REGION_E_TO_H: CONST_LIST_E_TO_H,
|
||||
CONST_REGION_I_TO_L: CONST_LIST_I_TO_L,
|
||||
CONST_REGION_M_TO_Q: CONST_LIST_M_TO_Q,
|
||||
CONST_REGION_R_TO_U: CONST_LIST_R_TO_U,
|
||||
CONST_REGION_V_TO_Z: CONST_LIST_V_TO_Z,
|
||||
}
|
14
homeassistant/components/nina/manifest.json
Normal file
14
homeassistant/components/nina/manifest.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"domain": "nina",
|
||||
"name": "NINA",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/nina",
|
||||
"requirements": [
|
||||
"pynina==0.1.4"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@DeerMaximum"
|
||||
],
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
27
homeassistant/components/nina/strings.json
Normal file
27
homeassistant/components/nina/strings.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"config": {
|
||||
"step":{
|
||||
"user": {
|
||||
"title": "Select city/county",
|
||||
"data" : {
|
||||
"_a_to_d": "City/county (A-D)",
|
||||
"_e_to_h": "City/county (E-H)",
|
||||
"_i_to_l": "City/county (I-L)",
|
||||
"_m_to_q": "City/county (M-Q)",
|
||||
"_r_to_u": "City/county (R-U)",
|
||||
"_v_to_z": "City/county (V-Z)",
|
||||
"slots": "Maximum warnings per city/county",
|
||||
"corona_filter": "Remove Corona Warnings"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
},
|
||||
"error": {
|
||||
"no_selection": "Please select at least one city/county",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
}
|
||||
}
|
27
homeassistant/components/nina/translations/en.json
Normal file
27
homeassistant/components/nina/translations/en.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Already configured. Only a single configuration possible."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"no_selection": "Please select at least one city/county",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"_a_to_d": "City/county (A-D)",
|
||||
"_e_to_h": "City/county (E-H)",
|
||||
"_i_to_l": "City/county (I-L)",
|
||||
"_m_to_q": "City/county (M-Q)",
|
||||
"_r_to_u": "City/county (R-U)",
|
||||
"_v_to_z": "City/county (V-Z)",
|
||||
"corona_filter": "Remove Corona Warnings",
|
||||
"slots": "Maximum warnings per city/county"
|
||||
},
|
||||
"title": "Select city/county"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -202,6 +202,7 @@ FLOWS = [
|
||||
"nexia",
|
||||
"nfandroidtv",
|
||||
"nightscout",
|
||||
"nina",
|
||||
"nmap_tracker",
|
||||
"notion",
|
||||
"nuheat",
|
||||
|
@ -1669,6 +1669,9 @@ pynetgear==0.7.0
|
||||
# homeassistant.components.netio
|
||||
pynetio==0.1.9.1
|
||||
|
||||
# homeassistant.components.nina
|
||||
pynina==0.1.4
|
||||
|
||||
# homeassistant.components.nuki
|
||||
pynuki==1.4.1
|
||||
|
||||
|
@ -1021,6 +1021,9 @@ pymysensors==0.22.1
|
||||
# homeassistant.components.netgear
|
||||
pynetgear==0.7.0
|
||||
|
||||
# homeassistant.components.nina
|
||||
pynina==0.1.4
|
||||
|
||||
# homeassistant.components.nuki
|
||||
pynuki==1.4.1
|
||||
|
||||
|
1
tests/components/nina/__init__.py
Normal file
1
tests/components/nina/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Nina integration."""
|
1
tests/components/nina/fixtures/sample_regions.json
Normal file
1
tests/components/nina/fixtures/sample_regions.json
Normal file
File diff suppressed because one or more lines are too long
44
tests/components/nina/fixtures/sample_warnings.json
Normal file
44
tests/components/nina/fixtures/sample_warnings.json
Normal file
@ -0,0 +1,44 @@
|
||||
[
|
||||
{
|
||||
"id": "mow.DE-BW-S-SE018-20211102-18-001",
|
||||
"payload": {
|
||||
"version": 1,
|
||||
"type": "ALERT",
|
||||
"id": "mow.DE-BW-S-SE018-20211102-18-001",
|
||||
"hash": "cae97b1c11bde900017305f681904ad5a6e8fd1c841241ced524b83eaa3522f4",
|
||||
"data": {
|
||||
"headline": "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen",
|
||||
"provider": "MOWAS",
|
||||
"severity": "Minor",
|
||||
"msgType": "Update",
|
||||
"transKeys": {"event": "BBK-EVC-040"},
|
||||
"area": {"type": "ZGEM", "data": "9956+1102,100001"}
|
||||
}
|
||||
},
|
||||
"i18nTitle": {
|
||||
"de": "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen"
|
||||
},
|
||||
"sent": "2021-11-02T20:07:16+01:00"
|
||||
},
|
||||
{
|
||||
"id": "mow.DE-NW-BN-SE030-20201014-30-000",
|
||||
"payload": {
|
||||
"version": 1,
|
||||
"type": "ALERT",
|
||||
"id": "mow.DE-NW-BN-SE030-20201014-30-000",
|
||||
"hash": "551db820a43be7e4f39283e1dfb71b212cd520c3ee478d44f43519e9c48fde4c",
|
||||
"data": {
|
||||
"headline": "Ausfall Notruf 112",
|
||||
"provider": "MOWAS",
|
||||
"severity": "Minor",
|
||||
"msgType": "Update",
|
||||
"transKeys": {"event": "BBK-EVC-040"},
|
||||
"area": {"type": "ZGEM", "data": "1+11057,100001"}
|
||||
}
|
||||
},
|
||||
"i18nTitle": {"de": "Ausfall Notruf 112"},
|
||||
"start": "2021-11-01T05:20:00+01:00",
|
||||
"sent": "2021-10-11T05:20:00+01:00",
|
||||
"expires": "3021-11-22T05:19:00+01:00"
|
||||
}
|
||||
]
|
235
tests/components/nina/test_binary_sensor.py
Normal file
235
tests/components/nina/test_binary_sensor.py
Normal file
@ -0,0 +1,235 @@
|
||||
"""Test the Nina binary sensor."""
|
||||
import json
|
||||
from typing import Any, Dict
|
||||
from unittest.mock import patch
|
||||
|
||||
from pynina import ApiError
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.components.nina.const import (
|
||||
ATTR_EXPIRES,
|
||||
ATTR_HEADLINE,
|
||||
ATTR_ID,
|
||||
ATTR_SENT,
|
||||
ATTR_START,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
ENTRY_DATA: Dict[str, Any] = {
|
||||
"slots": 5,
|
||||
"corona_filter": True,
|
||||
"regions": {"083350000000": "Aach, Stadt"},
|
||||
}
|
||||
|
||||
ENTRY_DATA_NO_CORONA: Dict[str, Any] = {
|
||||
"slots": 5,
|
||||
"corona_filter": False,
|
||||
"regions": {"083350000000": "Aach, Stadt"},
|
||||
}
|
||||
|
||||
|
||||
async def test_sensors(hass: HomeAssistant) -> None:
|
||||
"""Test the creation and values of the NINA sensors."""
|
||||
|
||||
dummy_response: Dict[str, Any] = json.loads(
|
||||
load_fixture("sample_warnings.json", "nina")
|
||||
)
|
||||
|
||||
with patch(
|
||||
"pynina.baseApi.BaseAPI._makeRequest",
|
||||
return_value=dummy_response,
|
||||
):
|
||||
|
||||
conf_entry: MockConfigEntry = MockConfigEntry(
|
||||
domain=DOMAIN, title="NINA", data=ENTRY_DATA
|
||||
)
|
||||
|
||||
entity_registry: er = er.async_get(hass)
|
||||
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.LOADED
|
||||
|
||||
state_w1 = hass.states.get("binary_sensor.warning_aach_stadt_1")
|
||||
entry_w1 = entity_registry.async_get("binary_sensor.warning_aach_stadt_1")
|
||||
|
||||
assert state_w1.state == STATE_ON
|
||||
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_SENT) == "2021-10-11T05:20:00+01:00"
|
||||
assert state_w1.attributes.get(ATTR_START) == "2021-11-01T05:20:00+01:00"
|
||||
assert state_w1.attributes.get(ATTR_EXPIRES) == "3021-11-22T05:19:00+01:00"
|
||||
|
||||
assert entry_w1.unique_id == "083350000000-1"
|
||||
assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
|
||||
|
||||
state_w2 = hass.states.get("binary_sensor.warning_aach_stadt_2")
|
||||
entry_w2 = entity_registry.async_get("binary_sensor.warning_aach_stadt_2")
|
||||
|
||||
assert state_w2.state == STATE_OFF
|
||||
assert state_w2.attributes.get(ATTR_HEADLINE) is None
|
||||
assert state_w2.attributes.get(ATTR_ID) is None
|
||||
assert state_w2.attributes.get(ATTR_SENT) is None
|
||||
assert state_w2.attributes.get(ATTR_START) is None
|
||||
assert state_w2.attributes.get(ATTR_EXPIRES) is None
|
||||
|
||||
assert entry_w2.unique_id == "083350000000-2"
|
||||
assert state_w2.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
|
||||
|
||||
state_w3 = hass.states.get("binary_sensor.warning_aach_stadt_3")
|
||||
entry_w3 = entity_registry.async_get("binary_sensor.warning_aach_stadt_3")
|
||||
|
||||
assert state_w3.state == STATE_OFF
|
||||
assert state_w3.attributes.get(ATTR_HEADLINE) is None
|
||||
assert state_w3.attributes.get(ATTR_ID) is None
|
||||
assert state_w3.attributes.get(ATTR_SENT) is None
|
||||
assert state_w3.attributes.get(ATTR_START) is None
|
||||
assert state_w3.attributes.get(ATTR_EXPIRES) is None
|
||||
|
||||
assert entry_w3.unique_id == "083350000000-3"
|
||||
assert state_w3.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
|
||||
|
||||
state_w4 = hass.states.get("binary_sensor.warning_aach_stadt_4")
|
||||
entry_w4 = entity_registry.async_get("binary_sensor.warning_aach_stadt_4")
|
||||
|
||||
assert state_w4.state == STATE_OFF
|
||||
assert state_w4.attributes.get(ATTR_HEADLINE) is None
|
||||
assert state_w4.attributes.get(ATTR_ID) is None
|
||||
assert state_w4.attributes.get(ATTR_SENT) is None
|
||||
assert state_w4.attributes.get(ATTR_START) is None
|
||||
assert state_w4.attributes.get(ATTR_EXPIRES) is None
|
||||
|
||||
assert entry_w4.unique_id == "083350000000-4"
|
||||
assert state_w4.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
|
||||
|
||||
state_w5 = hass.states.get("binary_sensor.warning_aach_stadt_5")
|
||||
entry_w5 = entity_registry.async_get("binary_sensor.warning_aach_stadt_5")
|
||||
|
||||
assert state_w5.state == STATE_OFF
|
||||
assert state_w5.attributes.get(ATTR_HEADLINE) is None
|
||||
assert state_w5.attributes.get(ATTR_ID) is None
|
||||
assert state_w5.attributes.get(ATTR_SENT) is None
|
||||
assert state_w5.attributes.get(ATTR_START) is None
|
||||
assert state_w5.attributes.get(ATTR_EXPIRES) is None
|
||||
|
||||
assert entry_w5.unique_id == "083350000000-5"
|
||||
assert state_w5.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
|
||||
|
||||
|
||||
async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None:
|
||||
"""Test the creation and values of the NINA sensors without the corona filter."""
|
||||
|
||||
dummy_response: Dict[str, Any] = json.loads(
|
||||
load_fixture("nina/sample_warnings.json")
|
||||
)
|
||||
|
||||
with patch(
|
||||
"pynina.baseApi.BaseAPI._makeRequest",
|
||||
return_value=dummy_response,
|
||||
):
|
||||
|
||||
conf_entry: MockConfigEntry = MockConfigEntry(
|
||||
domain=DOMAIN, title="NINA", data=ENTRY_DATA_NO_CORONA
|
||||
)
|
||||
|
||||
entity_registry: er = er.async_get(hass)
|
||||
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.LOADED
|
||||
|
||||
state_w1 = hass.states.get("binary_sensor.warning_aach_stadt_1")
|
||||
entry_w1 = entity_registry.async_get("binary_sensor.warning_aach_stadt_1")
|
||||
|
||||
assert state_w1.state == STATE_ON
|
||||
assert (
|
||||
state_w1.attributes.get(ATTR_HEADLINE)
|
||||
== "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_SENT) == "2021-11-02T20:07:16+01:00"
|
||||
assert state_w1.attributes.get(ATTR_START) == ""
|
||||
assert state_w1.attributes.get(ATTR_EXPIRES) == ""
|
||||
|
||||
assert entry_w1.unique_id == "083350000000-1"
|
||||
assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
|
||||
|
||||
state_w2 = hass.states.get("binary_sensor.warning_aach_stadt_2")
|
||||
entry_w2 = entity_registry.async_get("binary_sensor.warning_aach_stadt_2")
|
||||
|
||||
assert state_w2.state == STATE_ON
|
||||
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_SENT) == "2021-10-11T05:20:00+01:00"
|
||||
assert state_w2.attributes.get(ATTR_START) == "2021-11-01T05:20:00+01:00"
|
||||
assert state_w2.attributes.get(ATTR_EXPIRES) == "3021-11-22T05:19:00+01:00"
|
||||
|
||||
assert entry_w2.unique_id == "083350000000-2"
|
||||
assert state_w2.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
|
||||
|
||||
state_w3 = hass.states.get("binary_sensor.warning_aach_stadt_3")
|
||||
entry_w3 = entity_registry.async_get("binary_sensor.warning_aach_stadt_3")
|
||||
|
||||
assert state_w3.state == STATE_OFF
|
||||
assert state_w3.attributes.get(ATTR_HEADLINE) is None
|
||||
assert state_w3.attributes.get(ATTR_ID) is None
|
||||
assert state_w3.attributes.get(ATTR_SENT) is None
|
||||
assert state_w3.attributes.get(ATTR_START) is None
|
||||
assert state_w3.attributes.get(ATTR_EXPIRES) is None
|
||||
|
||||
assert entry_w3.unique_id == "083350000000-3"
|
||||
assert state_w3.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
|
||||
|
||||
state_w4 = hass.states.get("binary_sensor.warning_aach_stadt_4")
|
||||
entry_w4 = entity_registry.async_get("binary_sensor.warning_aach_stadt_4")
|
||||
|
||||
assert state_w4.state == STATE_OFF
|
||||
assert state_w4.attributes.get(ATTR_HEADLINE) is None
|
||||
assert state_w4.attributes.get(ATTR_ID) is None
|
||||
assert state_w4.attributes.get(ATTR_SENT) is None
|
||||
assert state_w4.attributes.get(ATTR_START) is None
|
||||
assert state_w4.attributes.get(ATTR_EXPIRES) is None
|
||||
|
||||
assert entry_w4.unique_id == "083350000000-4"
|
||||
assert state_w4.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
|
||||
|
||||
state_w5 = hass.states.get("binary_sensor.warning_aach_stadt_5")
|
||||
entry_w5 = entity_registry.async_get("binary_sensor.warning_aach_stadt_5")
|
||||
|
||||
assert state_w5.state == STATE_OFF
|
||||
assert state_w5.attributes.get(ATTR_HEADLINE) is None
|
||||
assert state_w5.attributes.get(ATTR_ID) is None
|
||||
assert state_w5.attributes.get(ATTR_SENT) is None
|
||||
assert state_w5.attributes.get(ATTR_START) is None
|
||||
assert state_w5.attributes.get(ATTR_EXPIRES) is None
|
||||
|
||||
assert entry_w5.unique_id == "083350000000-5"
|
||||
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
|
130
tests/components/nina/test_config_flow.py
Normal file
130
tests/components/nina/test_config_flow.py
Normal file
@ -0,0 +1,130 @@
|
||||
"""Test the Nina config flow."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from pynina import ApiError
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.nina.const import (
|
||||
CONF_FILTER_CORONA,
|
||||
CONF_MESSAGE_SLOTS,
|
||||
CONST_REGION_A_TO_D,
|
||||
CONST_REGION_E_TO_H,
|
||||
CONST_REGION_I_TO_L,
|
||||
CONST_REGION_M_TO_Q,
|
||||
CONST_REGION_R_TO_U,
|
||||
CONST_REGION_V_TO_Z,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import load_fixture
|
||||
|
||||
DUMMY_DATA: dict[str, Any] = {
|
||||
CONF_MESSAGE_SLOTS: 5,
|
||||
CONST_REGION_A_TO_D: ["095760000000_0", "095760000000_1"],
|
||||
CONST_REGION_E_TO_H: ["010610000000_0", "010610000000_1"],
|
||||
CONST_REGION_I_TO_L: ["071320000000_0", "071320000000_1"],
|
||||
CONST_REGION_M_TO_Q: ["071380000000_0", "071380000000_1"],
|
||||
CONST_REGION_R_TO_U: ["072320000000_0", "072320000000_1"],
|
||||
CONST_REGION_V_TO_Z: ["081270000000_0", "081270000000_1"],
|
||||
CONF_FILTER_CORONA: True,
|
||||
}
|
||||
|
||||
DUMMY_RESPONSE: dict[str, Any] = json.loads(load_fixture("sample_regions.json", "nina"))
|
||||
|
||||
|
||||
async def test_show_set_form(hass: HomeAssistant) -> None:
|
||||
"""Test that the setup form is served."""
|
||||
with patch(
|
||||
"pynina.baseApi.BaseAPI._makeRequest",
|
||||
return_value=DUMMY_RESPONSE,
|
||||
):
|
||||
|
||||
result: dict[str, Any] = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
|
||||
async def test_step_user_connection_error(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow by user but no connection."""
|
||||
with patch(
|
||||
"pynina.baseApi.BaseAPI._makeRequest",
|
||||
side_effect=ApiError("Could not connect to Api"),
|
||||
):
|
||||
|
||||
result: dict[str, Any] = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_step_user_unexpected_exception(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow by user but with an unexpected exception."""
|
||||
with patch(
|
||||
"pynina.baseApi.BaseAPI._makeRequest",
|
||||
side_effect=Exception("DUMMY"),
|
||||
):
|
||||
|
||||
result: dict[str, Any] = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
|
||||
|
||||
async def test_step_user(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow by user with valid values."""
|
||||
with patch(
|
||||
"pynina.baseApi.BaseAPI._makeRequest",
|
||||
return_value=DUMMY_RESPONSE,
|
||||
):
|
||||
|
||||
result: dict[str, Any] = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "NINA"
|
||||
|
||||
|
||||
async def test_step_user_no_selection(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow by user with no selection."""
|
||||
with patch(
|
||||
"pynina.baseApi.BaseAPI._makeRequest",
|
||||
return_value=DUMMY_RESPONSE,
|
||||
):
|
||||
|
||||
result: dict[str, Any] = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data={}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "no_selection"}
|
||||
|
||||
|
||||
async def test_step_user_already_configured(hass: HomeAssistant) -> None:
|
||||
"""Test starting a flow by user but it was already configured."""
|
||||
with patch(
|
||||
"pynina.baseApi.BaseAPI._makeRequest",
|
||||
return_value=DUMMY_RESPONSE,
|
||||
):
|
||||
result: dict[str, Any] = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
46
tests/components/nina/test_init.py
Normal file
46
tests/components/nina/test_init.py
Normal file
@ -0,0 +1,46 @@
|
||||
"""Test the Nina init file."""
|
||||
import json
|
||||
from typing import Any, Dict
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.nina.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
ENTRY_DATA: Dict[str, Any] = {
|
||||
"slots": 5,
|
||||
"corona_filter": True,
|
||||
"regions": {"083350000000": "Aach, Stadt"},
|
||||
}
|
||||
|
||||
|
||||
async def init_integration(hass) -> MockConfigEntry:
|
||||
"""Set up the NINA integration in Home Assistant."""
|
||||
|
||||
dummy_response: Dict[str, Any] = json.loads(
|
||||
load_fixture("sample_warnings.json", "nina")
|
||||
)
|
||||
|
||||
with patch(
|
||||
"pynina.baseApi.BaseAPI._makeRequest",
|
||||
return_value=dummy_response,
|
||||
):
|
||||
|
||||
entry: MockConfigEntry = MockConfigEntry(
|
||||
domain=DOMAIN, title="NINA", data=ENTRY_DATA
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
return entry
|
||||
|
||||
|
||||
async def test_config_entry_not_ready(hass: HomeAssistant) -> None:
|
||||
"""Test the configuration entry."""
|
||||
entry: MockConfigEntry = await init_integration(hass)
|
||||
|
||||
assert entry.state == ConfigEntryState.LOADED
|
Loading…
x
Reference in New Issue
Block a user