Merge pull request #71747 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2022-05-12 17:34:47 -07:00 committed by GitHub
commit 09b49fd7b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 569 additions and 249 deletions

View File

@ -237,12 +237,14 @@ def _is_url(url):
return all([result.scheme, result.netloc])
async def _fetch_playlist(hass, url):
async def _fetch_playlist(hass, url, supported_content_types):
"""Fetch a playlist from the given url."""
try:
session = aiohttp_client.async_get_clientsession(hass, verify_ssl=False)
async with session.get(url, timeout=5) as resp:
charset = resp.charset or "utf-8"
if resp.content_type in supported_content_types:
raise PlaylistSupported
try:
playlist_data = (await resp.content.read(64 * 1024)).decode(charset)
except ValueError as err:
@ -260,7 +262,16 @@ async def parse_m3u(hass, url):
Based on https://github.com/dvndrsn/M3uParser/blob/master/m3uparser.py
"""
m3u_data = await _fetch_playlist(hass, url)
# From Mozilla gecko source: https://github.com/mozilla/gecko-dev/blob/c4c1adbae87bf2d128c39832d72498550ee1b4b8/dom/media/DecoderTraits.cpp#L47-L52
hls_content_types = (
# https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-10
"application/vnd.apple.mpegurl",
# Some sites serve these as the informal HLS m3u type.
"application/x-mpegurl",
"audio/mpegurl",
"audio/x-mpegurl",
)
m3u_data = await _fetch_playlist(hass, url, hls_content_types)
m3u_lines = m3u_data.splitlines()
playlist = []
@ -301,7 +312,7 @@ async def parse_pls(hass, url):
Based on https://github.com/mariob/plsparser/blob/master/src/plsparser.py
"""
pls_data = await _fetch_playlist(hass, url)
pls_data = await _fetch_playlist(hass, url, ())
pls_parser = configparser.ConfigParser()
try:

View File

@ -3,7 +3,7 @@
"name": "Google Cast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast",
"requirements": ["pychromecast==12.1.1"],
"requirements": ["pychromecast==12.1.2"],
"after_dependencies": [
"cloud",
"http",

View File

@ -3,7 +3,7 @@
"name": "deCONZ",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/deconz",
"requirements": ["pydeconz==91"],
"requirements": ["pydeconz==92"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",

View File

@ -112,6 +112,7 @@ class FibaroLight(FibaroDevice, LightEntity):
if ATTR_BRIGHTNESS in kwargs:
self._attr_brightness = kwargs[ATTR_BRIGHTNESS]
self.set_level(scaleto99(self._attr_brightness))
return
if ATTR_RGB_COLOR in kwargs:
# Update based on parameters

View File

@ -37,6 +37,7 @@ from .const import (
CONF_RTSP_TRANSPORT,
CONF_STILL_IMAGE_URL,
CONF_STREAM_SOURCE,
CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
DEFAULT_NAME,
FFMPEG_OPTION_MAP,
GET_IMAGE_TIMEOUT,
@ -160,6 +161,10 @@ class GenericCamera(Camera):
CONF_RTSP_TRANSPORT
]
self._auth = generate_auth(device_info)
if device_info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS):
self.stream_options[
FFMPEG_OPTION_MAP[CONF_USE_WALLCLOCK_AS_TIMESTAMPS]
] = "1"
self._last_url = None
self._last_image = None

View File

@ -41,6 +41,7 @@ from .const import (
CONF_RTSP_TRANSPORT,
CONF_STILL_IMAGE_URL,
CONF_STREAM_SOURCE,
CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
DEFAULT_NAME,
DOMAIN,
FFMPEG_OPTION_MAP,
@ -64,6 +65,7 @@ SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml", "webp"}
def build_schema(
user_input: dict[str, Any] | MappingProxyType[str, Any],
is_options_flow: bool = False,
show_advanced_options=False,
):
"""Create schema for camera config setup."""
spec = {
@ -106,6 +108,13 @@ def build_schema(
default=user_input.get(CONF_LIMIT_REFETCH_TO_URL_CHANGE, False),
)
] = bool
if show_advanced_options:
spec[
vol.Required(
CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
default=user_input.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS, False),
)
] = bool
return vol.Schema(spec)
@ -199,6 +208,8 @@ async def async_test_stream(hass, info) -> dict[str, str]:
}
if rtsp_transport := info.get(CONF_RTSP_TRANSPORT):
stream_options[FFMPEG_OPTION_MAP[CONF_RTSP_TRANSPORT]] = rtsp_transport
if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS):
stream_options[FFMPEG_OPTION_MAP[CONF_USE_WALLCLOCK_AS_TIMESTAMPS]] = "1"
_LOGGER.debug("Attempting to open stream %s", stream_source)
container = await hass.async_add_executor_job(
partial(
@ -356,6 +367,9 @@ class GenericOptionsFlowHandler(OptionsFlow):
],
CONF_FRAMERATE: user_input[CONF_FRAMERATE],
CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL],
CONF_USE_WALLCLOCK_AS_TIMESTAMPS: user_input.get(
CONF_USE_WALLCLOCK_AS_TIMESTAMPS
),
}
return self.async_create_entry(
title=title,
@ -363,6 +377,10 @@ class GenericOptionsFlowHandler(OptionsFlow):
)
return self.async_show_form(
step_id="init",
data_schema=build_schema(user_input or self.config_entry.options, True),
data_schema=build_schema(
user_input or self.config_entry.options,
True,
self.show_advanced_options,
),
errors=errors,
)

View File

@ -8,7 +8,11 @@ CONF_STILL_IMAGE_URL = "still_image_url"
CONF_STREAM_SOURCE = "stream_source"
CONF_FRAMERATE = "framerate"
CONF_RTSP_TRANSPORT = "rtsp_transport"
FFMPEG_OPTION_MAP = {CONF_RTSP_TRANSPORT: "rtsp_transport"}
CONF_USE_WALLCLOCK_AS_TIMESTAMPS = "use_wallclock_as_timestamps"
FFMPEG_OPTION_MAP = {
CONF_RTSP_TRANSPORT: "rtsp_transport",
CONF_USE_WALLCLOCK_AS_TIMESTAMPS: "use_wallclock_as_timestamps",
}
RTSP_TRANSPORTS = {
"tcp": "TCP",
"udp": "UDP",

View File

@ -55,9 +55,13 @@
"authentication": "[%key:component::generic::config::step::user::data::authentication%]",
"limit_refetch_to_url_change": "[%key:component::generic::config::step::user::data::limit_refetch_to_url_change%]",
"password": "[%key:common::config_flow::data::password%]",
"use_wallclock_as_timestamps": "Use wallclock as timestamps",
"username": "[%key:common::config_flow::data::username%]",
"framerate": "[%key:component::generic::config::step::user::data::framerate%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"use_wallclock_as_timestamps": "This option may correct segmenting or crashing issues arising from buggy timestamp implementations on some cameras"
}
},
"content_type": {

View File

@ -32,7 +32,6 @@
"user": {
"data": {
"authentication": "Authentication",
"content_type": "Content Type",
"framerate": "Frame Rate (Hz)",
"limit_refetch_to_url_change": "Limit refetch to url change",
"password": "Password",
@ -72,15 +71,18 @@
"init": {
"data": {
"authentication": "Authentication",
"content_type": "Content Type",
"framerate": "Frame Rate (Hz)",
"limit_refetch_to_url_change": "Limit refetch to url change",
"password": "Password",
"rtsp_transport": "RTSP transport protocol",
"still_image_url": "Still Image URL (e.g. http://...)",
"stream_source": "Stream Source URL (e.g. rtsp://...)",
"use_wallclock_as_timestamps": "Use wallclock as timestamps",
"username": "Username",
"verify_ssl": "Verify SSL certificate"
},
"data_description": {
"use_wallclock_as_timestamps": "This option may correct segmenting or crashing issues arising from buggy timestamp implementations on some cameras"
}
}
}

View File

@ -96,7 +96,11 @@ class HistoryStats:
new_data = False
if event and event.data["new_state"] is not None:
new_state: State = event.data["new_state"]
if current_period_start <= new_state.last_changed <= current_period_end:
if (
current_period_start_timestamp
<= floored_timestamp(new_state.last_changed)
<= current_period_end_timestamp
):
self._history_current_period.append(new_state)
new_data = True
if not new_data and current_period_end_timestamp < now_timestamp:

View File

@ -153,10 +153,9 @@ class InsteonEntity(Entity):
def get_device_property(self, name: str):
"""Get a single Insteon device property value (raw)."""
value = None
if (prop := self._insteon_device.properties.get(name)) is not None:
value = prop.value if prop.new_value is None else prop.new_value
return value
return prop.value
return None
def _get_label(self):
"""Get the device label for grouped devices."""

View File

@ -58,9 +58,9 @@ class InsteonDimmerEntity(InsteonEntity, LightEntity):
"""Turn light on."""
if ATTR_BRIGHTNESS in kwargs:
brightness = int(kwargs[ATTR_BRIGHTNESS])
else:
elif self._insteon_device_group.group == 1:
brightness = self.get_device_property(ON_LEVEL)
if brightness is not None:
if brightness:
await self._insteon_device.async_on(
on_level=brightness, group=self._insteon_device_group.group
)

View File

@ -3,7 +3,7 @@
"name": "Logi Circle",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/logi_circle",
"requirements": ["logi_circle==0.2.2"],
"requirements": ["logi_circle==0.2.3"],
"dependencies": ["ffmpeg", "http"],
"codeowners": ["@evanjd"],
"iot_class": "cloud_polling",

View File

@ -176,8 +176,6 @@ class MeaterProbeTemperature(
):
"""Meater Temperature Sensor Entity."""
_attr_device_class = SensorDeviceClass.TEMPERATURE
_attr_native_unit_of_measurement = TEMP_CELSIUS
entity_description: MeaterSensorEntityDescription
def __init__(

View File

@ -3,7 +3,7 @@
"name": "Nettigo Air Monitor",
"documentation": "https://www.home-assistant.io/integrations/nam",
"codeowners": ["@bieniu"],
"requirements": ["nettigo-air-monitor==1.2.3"],
"requirements": ["nettigo-air-monitor==1.2.4"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View File

@ -165,17 +165,13 @@ class ONVIFDevice:
)
return
tzone = dt_util.DEFAULT_TIME_ZONE
cdate = device_time.LocalDateTime
if device_time.UTCDateTime:
tzone = dt_util.UTC
cdate = device_time.UTCDateTime
else:
tzone = (
dt_util.get_time_zone(
device_time.TimeZone or str(dt_util.DEFAULT_TIME_ZONE)
)
or dt_util.DEFAULT_TIME_ZONE
)
cdate = device_time.LocalDateTime
elif device_time.TimeZone:
tzone = dt_util.get_time_zone(device_time.TimeZone.TZ) or tzone
if cdate is None:
LOGGER.warning("Could not retrieve date/time on this camera")

View File

@ -1,4 +1,6 @@
"""Support for monitoring an SABnzbd NZB client."""
from __future__ import annotations
from collections.abc import Callable
import logging
@ -14,12 +16,13 @@ from homeassistant.const import (
CONF_PORT,
CONF_SENSORS,
CONF_SSL,
CONF_URL,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import async_get
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType
@ -42,7 +45,7 @@ from .const import (
UPDATE_INTERVAL,
)
from .sab import get_client
from .sensor import SENSOR_KEYS
from .sensor import OLD_SENSOR_KEYS
PLATFORMS = ["sensor"]
_LOGGER = logging.getLogger(__name__)
@ -76,12 +79,11 @@ CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_API_KEY): str,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
vol.Required(CONF_URL): str,
vol.Optional(CONF_PATH): str,
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_SENSORS): vol.All(
cv.ensure_list, [vol.In(SENSOR_KEYS)]
cv.ensure_list, [vol.In(OLD_SENSOR_KEYS)]
),
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
},
@ -123,8 +125,56 @@ def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall)
raise ValueError(f"No api for API key: {call_data_api_key}")
def update_device_identifiers(hass: HomeAssistant, entry: ConfigEntry):
"""Update device identifiers to new identifiers."""
device_registry = async_get(hass)
device_entry = device_registry.async_get_device({(DOMAIN, DOMAIN)})
if device_entry and entry.entry_id in device_entry.config_entries:
new_identifiers = {(DOMAIN, entry.entry_id)}
_LOGGER.debug(
"Updating device id <%s> with new identifiers <%s>",
device_entry.id,
new_identifiers,
)
device_registry.async_update_device(
device_entry.id, new_identifiers=new_identifiers
)
async def migrate_unique_id(hass: HomeAssistant, entry: ConfigEntry):
"""Migrate entities to new unique ids (with entry_id)."""
@callback
def async_migrate_callback(entity_entry: RegistryEntry) -> dict | None:
"""
Define a callback to migrate appropriate SabnzbdSensor entities to new unique IDs.
Old: description.key
New: {entry_id}_description.key
"""
entry_id = entity_entry.config_entry_id
if entry_id is None:
return None
if entity_entry.unique_id.startswith(entry_id):
return None
new_unique_id = f"{entry_id}_{entity_entry.unique_id}"
_LOGGER.debug(
"Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
entity_entry.entity_id,
entity_entry.unique_id,
new_unique_id,
)
return {"new_unique_id": new_unique_id}
await async_migrate_entries(hass, entry.entry_id, async_migrate_callback)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the SabNzbd Component."""
sab_api = await get_client(hass, entry.data)
if not sab_api:
raise ConfigEntryNotReady
@ -137,6 +187,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
KEY_NAME: entry.data[CONF_NAME],
}
await migrate_unique_id(hass, entry)
update_device_identifiers(hass, entry)
@callback
def extract_api(func: Callable) -> Callable:
"""Define a decorator to get the correct api for a service call."""
@ -188,6 +241,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.error(err)
async_track_time_interval(hass, async_update_sabnzbd, UPDATE_INTERVAL)
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True

View File

@ -103,7 +103,19 @@ SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = (
),
)
SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
OLD_SENSOR_KEYS = [
"current_status",
"speed",
"queue_size",
"queue_remaining",
"disk_size",
"disk_free",
"queue_count",
"day_size",
"week_size",
"month_size",
"total_size",
]
async def async_setup_entry(
@ -113,11 +125,16 @@ async def async_setup_entry(
) -> None:
"""Set up a Sabnzbd sensor entry."""
sab_api_data = hass.data[DOMAIN][config_entry.entry_id][KEY_API_DATA]
client_name = hass.data[DOMAIN][config_entry.entry_id][KEY_NAME]
entry_id = config_entry.entry_id
sab_api_data = hass.data[DOMAIN][entry_id][KEY_API_DATA]
client_name = hass.data[DOMAIN][entry_id][KEY_NAME]
async_add_entities(
[SabnzbdSensor(sab_api_data, client_name, sensor) for sensor in SENSOR_TYPES]
[
SabnzbdSensor(sab_api_data, client_name, sensor, entry_id)
for sensor in SENSOR_TYPES
]
)
@ -128,17 +145,21 @@ class SabnzbdSensor(SensorEntity):
_attr_should_poll = False
def __init__(
self, sabnzbd_api_data, client_name, description: SabnzbdSensorEntityDescription
self,
sabnzbd_api_data,
client_name,
description: SabnzbdSensorEntityDescription,
entry_id,
):
"""Initialize the sensor."""
unique_id = description.key
self._attr_unique_id = unique_id
self._attr_unique_id = f"{entry_id}_{description.key}"
self.entity_description = description
self._sabnzbd_api = sabnzbd_api_data
self._attr_name = f"{client_name} {description.name}"
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, DOMAIN)},
identifiers={(DOMAIN, entry_id)},
name=DEFAULT_NAME,
)
@ -156,9 +177,11 @@ class SabnzbdSensor(SensorEntity):
self.entity_description.key
)
if self.entity_description.key == SPEED_KEY:
self._attr_native_value = round(float(self._attr_native_value) / 1024, 1)
elif "size" in self.entity_description.key:
self._attr_native_value = round(float(self._attr_native_value), 2)
if self._attr_native_value is not None:
if self.entity_description.key == SPEED_KEY:
self._attr_native_value = round(
float(self._attr_native_value) / 1024, 1
)
elif "size" in self.entity_description.key:
self._attr_native_value = round(float(self._attr_native_value), 2)
self.schedule_update_ha_state()

View File

@ -3,7 +3,7 @@
"name": "SimpliSafe",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
"requirements": ["simplisafe-python==2022.05.0"],
"requirements": ["simplisafe-python==2022.05.1"],
"codeowners": ["@bachya"],
"iot_class": "cloud_polling",
"dhcp": [

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,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,11 +21,11 @@
},
"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"
}
}
}

View File

@ -3,7 +3,7 @@
"name": "UniFi Protect",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/unifiprotect",
"requirements": ["pyunifiprotect==3.4.0", "unifi-discovery==1.1.2"],
"requirements": ["pyunifiprotect==3.4.1", "unifi-discovery==1.1.2"],
"dependencies": ["http"],
"codeowners": ["@briis", "@AngellusMortis", "@bdraco"],
"quality_scale": "platinum",

View File

@ -8,6 +8,12 @@ from zwave_js_server.const import ConfigurationValueType
from zwave_js_server.model.node import Node
from zwave_js_server.model.value import ConfigurationValue
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from .const import DOMAIN
NODE_STATUSES = ["asleep", "awake", "dead", "alive"]
CONF_SUBTYPE = "subtype"
@ -41,3 +47,21 @@ def generate_config_parameter_subtype(config_value: ConfigurationValue) -> str:
parameter = f"{parameter}[{hex(config_value.property_key)}]"
return f"{parameter} ({config_value.property_name})"
@callback
def async_bypass_dynamic_config_validation(hass: HomeAssistant, device_id: str) -> bool:
"""Return whether device's config entries are not loaded."""
dev_reg = dr.async_get(hass)
if (device := dev_reg.async_get(device_id)) is None:
raise ValueError(f"Device {device_id} not found")
entry = next(
(
config_entry
for config_entry in hass.config_entries.async_entries(DOMAIN)
if config_entry.entry_id in device.config_entries
and config_entry.state == ConfigEntryState.LOADED
),
None,
)
return not entry

View File

@ -29,12 +29,12 @@ from .device_automation_helpers import (
CONF_SUBTYPE,
CONF_VALUE_ID,
NODE_STATUSES,
async_bypass_dynamic_config_validation,
generate_config_parameter_subtype,
get_config_parameter_value_schema,
)
from .helpers import (
async_get_node_from_device_id,
async_is_device_config_entry_not_loaded,
check_type_schema_map,
get_zwave_value_from_config,
remove_keys_with_empty_values,
@ -101,7 +101,7 @@ async def async_validate_condition_config(
# We return early if the config entry for this device is not ready because we can't
# validate the value without knowing the state of the device
try:
device_config_entry_not_loaded = async_is_device_config_entry_not_loaded(
bypass_dynamic_config_validation = async_bypass_dynamic_config_validation(
hass, config[CONF_DEVICE_ID]
)
except ValueError as err:
@ -109,7 +109,7 @@ async def async_validate_condition_config(
f"Device {config[CONF_DEVICE_ID]} not found"
) from err
if device_config_entry_not_loaded:
if bypass_dynamic_config_validation:
return config
if config[CONF_TYPE] == VALUE_TYPE:

View File

@ -53,12 +53,12 @@ from .const import (
from .device_automation_helpers import (
CONF_SUBTYPE,
NODE_STATUSES,
async_bypass_dynamic_config_validation,
generate_config_parameter_subtype,
)
from .helpers import (
async_get_node_from_device_id,
async_get_node_status_sensor_entity_id,
async_is_device_config_entry_not_loaded,
check_type_schema_map,
copy_available_params,
get_value_state_schema,
@ -215,7 +215,7 @@ async def async_validate_trigger_config(
# We return early if the config entry for this device is not ready because we can't
# validate the value without knowing the state of the device
try:
device_config_entry_not_loaded = async_is_device_config_entry_not_loaded(
bypass_dynamic_config_validation = async_bypass_dynamic_config_validation(
hass, config[CONF_DEVICE_ID]
)
except ValueError as err:
@ -223,7 +223,7 @@ async def async_validate_trigger_config(
f"Device {config[CONF_DEVICE_ID]} not found"
) from err
if device_config_entry_not_loaded:
if bypass_dynamic_config_validation:
return config
trigger_type = config[CONF_TYPE]

View File

@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 5
PATCH_VERSION: Final = "3"
PATCH_VERSION: Final = "4"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@ -957,7 +957,7 @@ lmnotify==0.0.4
locationsharinglib==4.1.5
# homeassistant.components.logi_circle
logi_circle==0.2.2
logi_circle==0.2.3
# homeassistant.components.london_underground
london-tube-status==0.2
@ -1065,7 +1065,7 @@ netdisco==3.0.0
netmap==0.7.0.2
# homeassistant.components.nam
nettigo-air-monitor==1.2.3
nettigo-air-monitor==1.2.4
# homeassistant.components.neurio_energy
neurio==0.3.1
@ -1399,7 +1399,7 @@ pycfdns==1.2.2
pychannels==1.0.0
# homeassistant.components.cast
pychromecast==12.1.1
pychromecast==12.1.2
# homeassistant.components.pocketcasts
pycketcasts==1.0.0
@ -1432,7 +1432,7 @@ pydaikin==2.7.0
pydanfossair==0.1.0
# homeassistant.components.deconz
pydeconz==91
pydeconz==92
# homeassistant.components.delijn
pydelijn==1.0.0
@ -1981,7 +1981,7 @@ pytrafikverket==0.1.6.2
pyudev==0.22.0
# homeassistant.components.unifiprotect
pyunifiprotect==3.4.0
pyunifiprotect==3.4.1
# homeassistant.components.uptimerobot
pyuptimerobot==22.2.0
@ -2153,7 +2153,7 @@ simplehound==0.3
simplepush==1.1.4
# homeassistant.components.simplisafe
simplisafe-python==2022.05.0
simplisafe-python==2022.05.1
# homeassistant.components.sisyphus
sisyphus-control==3.1.2
@ -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

View File

@ -655,7 +655,7 @@ librouteros==3.2.0
libsoundtouch==0.8
# homeassistant.components.logi_circle
logi_circle==0.2.2
logi_circle==0.2.3
# homeassistant.components.recorder
lru-dict==1.1.7
@ -727,7 +727,7 @@ netdisco==3.0.0
netmap==0.7.0.2
# homeassistant.components.nam
nettigo-air-monitor==1.2.3
nettigo-air-monitor==1.2.4
# homeassistant.components.nexia
nexia==0.9.13
@ -938,7 +938,7 @@ pybotvac==0.0.23
pycfdns==1.2.2
# homeassistant.components.cast
pychromecast==12.1.1
pychromecast==12.1.2
# homeassistant.components.climacell
pyclimacell==0.18.2
@ -953,7 +953,7 @@ pycoolmasternet-async==0.1.2
pydaikin==2.7.0
# homeassistant.components.deconz
pydeconz==91
pydeconz==92
# homeassistant.components.dexcom
pydexcom==0.2.3
@ -1304,7 +1304,7 @@ pytrafikverket==0.1.6.2
pyudev==0.22.0
# homeassistant.components.unifiprotect
pyunifiprotect==3.4.0
pyunifiprotect==3.4.1
# homeassistant.components.uptimerobot
pyuptimerobot==22.2.0
@ -1404,7 +1404,7 @@ sharkiq==0.0.1
simplehound==0.3
# homeassistant.components.simplisafe
simplisafe-python==2022.05.0
simplisafe-python==2022.05.1
# homeassistant.components.slack
slackclient==2.5.0
@ -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

View File

@ -1,6 +1,6 @@
[metadata]
name = homeassistant
version = 2022.5.3
version = 2022.5.4
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0

View File

@ -0,0 +1,5 @@
#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=54000,CODECS="mp4a.40.2"
https://rthkaudio2-lh.akamaihd.net/i/radio2_1@355865/index_56_a-p.m3u8?sd=10&rebase=on
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=54000,CODECS="mp4a.40.2"
https://rthkaudio2-lh.akamaihd.net/i/radio2_1@355865/index_56_a-b.m3u8?sd=10&rebase=on

View File

@ -14,10 +14,25 @@ from homeassistant.components.cast.helpers import (
from tests.common import load_fixture
async def test_hls_playlist_supported(hass, aioclient_mock):
@pytest.mark.parametrize(
"url,fixture,content_type",
(
(
"http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_low/ak/bbc_radio_fourfm.m3u8",
"bbc_radio_fourfm.m3u8",
None,
),
(
"https://rthkaudio2-lh.akamaihd.net/i/radio2_1@355865/master.m3u8",
"rthkaudio2.m3u8",
"application/vnd.apple.mpegurl",
),
),
)
async def test_hls_playlist_supported(hass, aioclient_mock, url, fixture, content_type):
"""Test playlist parsing of HLS playlist."""
url = "http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_low/ak/bbc_radio_fourfm.m3u8"
aioclient_mock.get(url, text=load_fixture("bbc_radio_fourfm.m3u8", "cast"))
headers = {"content-type": content_type}
aioclient_mock.get(url, text=load_fixture(fixture, "cast"), headers=headers)
with pytest.raises(PlaylistSupported):
await parse_playlist(hass, url)

View File

@ -18,6 +18,7 @@ from homeassistant.components.generic.const import (
CONF_RTSP_TRANSPORT,
CONF_STILL_IMAGE_URL,
CONF_STREAM_SOURCE,
CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
DOMAIN,
)
from homeassistant.const import (
@ -653,3 +654,32 @@ async def test_migrate_existing_ids(hass) -> None:
entity_entry = registry.async_get(entity_id)
assert entity_entry.unique_id == new_unique_id
@respx.mock
async def test_use_wallclock_as_timestamps_option(hass, fakeimg_png, mock_av_open):
"""Test the use_wallclock_as_timestamps option flow."""
mock_entry = MockConfigEntry(
title="Test Camera",
domain=DOMAIN,
data={},
options=TESTDATA,
)
with mock_av_open:
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(
mock_entry.entry_id, context={"show_advanced_options": True}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY

View File

@ -1387,3 +1387,117 @@ async def test_measure_cet(hass, recorder_mock):
assert hass.states.get("sensor.sensor2").state == "0.83"
assert hass.states.get("sensor.sensor3").state == "1"
assert hass.states.get("sensor.sensor4").state == "83.3"
@pytest.mark.parametrize("time_zone", ["Europe/Berlin", "America/Chicago", "US/Hawaii"])
async def test_end_time_with_microseconds_zeroed(time_zone, hass, recorder_mock):
"""Test the history statistics sensor that has the end time microseconds zeroed out."""
hass.config.set_time_zone(time_zone)
start_of_today = dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0)
start_time = start_of_today + timedelta(minutes=60)
t0 = start_time + timedelta(minutes=20)
t1 = t0 + timedelta(minutes=10)
t2 = t1 + timedelta(minutes=10)
time_200 = start_of_today + timedelta(hours=2)
def _fake_states(*args, **kwargs):
return {
"binary_sensor.heatpump_compressor_state": [
ha.State(
"binary_sensor.heatpump_compressor_state", "on", last_changed=t0
),
ha.State(
"binary_sensor.heatpump_compressor_state",
"off",
last_changed=t1,
),
ha.State(
"binary_sensor.heatpump_compressor_state", "on", last_changed=t2
),
]
}
with freeze_time(time_200), patch(
"homeassistant.components.recorder.history.state_changes_during_period",
_fake_states,
):
await async_setup_component(
hass,
"sensor",
{
"sensor": [
{
"platform": "history_stats",
"entity_id": "binary_sensor.heatpump_compressor_state",
"name": "heatpump_compressor_today",
"state": "on",
"start": "{{ now().replace(hour=0, minute=0, second=0, microsecond=0) }}",
"end": "{{ now().replace(microsecond=0) }}",
"type": "time",
},
]
},
)
await hass.async_block_till_done()
await async_update_entity(hass, "sensor.heatpump_compressor_today")
await hass.async_block_till_done()
assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83"
async_fire_time_changed(hass, time_200)
await hass.async_block_till_done()
assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83"
hass.states.async_set("binary_sensor.heatpump_compressor_state", "off")
await hass.async_block_till_done()
time_400 = start_of_today + timedelta(hours=4)
with freeze_time(time_400):
async_fire_time_changed(hass, time_400)
await hass.async_block_till_done()
assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83"
hass.states.async_set("binary_sensor.heatpump_compressor_state", "on")
await hass.async_block_till_done()
time_600 = start_of_today + timedelta(hours=6)
with freeze_time(time_600):
async_fire_time_changed(hass, time_600)
await hass.async_block_till_done()
assert hass.states.get("sensor.heatpump_compressor_today").state == "3.83"
rolled_to_next_day = start_of_today + timedelta(days=1)
assert rolled_to_next_day.hour == 0
assert rolled_to_next_day.minute == 0
assert rolled_to_next_day.second == 0
assert rolled_to_next_day.microsecond == 0
with freeze_time(rolled_to_next_day):
async_fire_time_changed(hass, rolled_to_next_day)
await hass.async_block_till_done()
assert hass.states.get("sensor.heatpump_compressor_today").state == "0.0"
rolled_to_next_day_plus_12 = start_of_today + timedelta(
days=1, hours=12, microseconds=0
)
with freeze_time(rolled_to_next_day_plus_12):
async_fire_time_changed(hass, rolled_to_next_day_plus_12)
await hass.async_block_till_done()
assert hass.states.get("sensor.heatpump_compressor_today").state == "12.0"
rolled_to_next_day_plus_14 = start_of_today + timedelta(
days=1, hours=14, microseconds=0
)
with freeze_time(rolled_to_next_day_plus_14):
async_fire_time_changed(hass, rolled_to_next_day_plus_14)
await hass.async_block_till_done()
assert hass.states.get("sensor.heatpump_compressor_today").state == "14.0"
rolled_to_next_day_plus_16_860000 = start_of_today + timedelta(
days=1, hours=16, microseconds=860000
)
with freeze_time(rolled_to_next_day_plus_16_860000):
hass.states.async_set("binary_sensor.heatpump_compressor_state", "off")
async_fire_time_changed(hass, rolled_to_next_day_plus_16_860000)
await hass.async_block_till_done()
rolled_to_next_day_plus_18 = start_of_today + timedelta(days=1, hours=18)
with freeze_time(rolled_to_next_day_plus_18):
async_fire_time_changed(hass, rolled_to_next_day_plus_18)
await hass.async_block_till_done()
assert hass.states.get("sensor.heatpump_compressor_today").state == "16.0"

View File

@ -87,7 +87,6 @@ async def test_import_flow(hass) -> None:
"homeassistant.components.sabnzbd.sab.SabnzbdApi.check_available",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},

View File

@ -0,0 +1,85 @@
"""Tests for the SABnzbd Integration."""
from unittest.mock import patch
import pytest
from homeassistant.components.sabnzbd import DEFAULT_NAME, DOMAIN, OLD_SENSOR_KEYS
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_URL
from homeassistant.helpers.device_registry import DeviceEntryType
from tests.common import MockConfigEntry, mock_device_registry, mock_registry
MOCK_ENTRY_ID = "mock_entry_id"
MOCK_UNIQUE_ID = "someuniqueid"
MOCK_DEVICE_ID = "somedeviceid"
MOCK_DATA_VERSION_1 = {
CONF_API_KEY: "api_key",
CONF_URL: "http://127.0.0.1:8080",
CONF_NAME: "name",
}
MOCK_ENTRY_VERSION_1 = MockConfigEntry(
domain=DOMAIN, data=MOCK_DATA_VERSION_1, entry_id=MOCK_ENTRY_ID, version=1
)
@pytest.fixture
def device_registry(hass):
"""Return an empty, loaded, registry."""
return mock_device_registry(hass)
@pytest.fixture
def entity_registry(hass):
"""Return an empty, loaded, registry."""
return mock_registry(hass)
async def test_unique_id_migrate(hass, device_registry, entity_registry):
"""Test that config flow entry is migrated correctly."""
# Start with the config entry at Version 1.
mock_entry = MOCK_ENTRY_VERSION_1
mock_entry.add_to_hass(hass)
mock_d_entry = device_registry.async_get_or_create(
config_entry_id=mock_entry.entry_id,
identifiers={(DOMAIN, DOMAIN)},
name=DEFAULT_NAME,
entry_type=DeviceEntryType.SERVICE,
)
entity_id_sensor_key = []
for sensor_key in OLD_SENSOR_KEYS:
mock_entity_id = f"{SENSOR_DOMAIN}.{DOMAIN}_{sensor_key}"
entity_registry.async_get_or_create(
SENSOR_DOMAIN,
DOMAIN,
unique_id=sensor_key,
config_entry=mock_entry,
device_id=mock_d_entry.id,
)
entity = entity_registry.async_get(mock_entity_id)
assert entity.entity_id == mock_entity_id
assert entity.unique_id == sensor_key
entity_id_sensor_key.append((mock_entity_id, sensor_key))
with patch(
"homeassistant.components.sabnzbd.sab.SabnzbdApi.check_available",
return_value=True,
):
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
for mock_entity_id, sensor_key in entity_id_sensor_key:
entity = entity_registry.async_get(mock_entity_id)
assert entity.unique_id == f"{MOCK_ENTRY_ID}_{sensor_key}"
assert device_registry.async_get(mock_d_entry.id).identifiers == {
(DOMAIN, MOCK_ENTRY_ID)
}

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"