mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Merge pull request #71747 from home-assistant/rc
This commit is contained in:
commit
09b49fd7b5
@ -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:
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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",
|
||||
|
@ -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": {
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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",
|
||||
|
@ -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__(
|
||||
|
@ -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.",
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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": [
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"],
|
||||
},
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
5
tests/components/cast/fixtures/rthkaudio2.m3u8
Normal file
5
tests/components/cast/fixtures/rthkaudio2.m3u8
Normal 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
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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},
|
||||
|
85
tests/components/sabnzbd/test_init.py
Normal file
85
tests/components/sabnzbd/test_init.py
Normal 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)
|
||||
}
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user