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]) 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.""" """Fetch a playlist from the given url."""
try: try:
session = aiohttp_client.async_get_clientsession(hass, verify_ssl=False) session = aiohttp_client.async_get_clientsession(hass, verify_ssl=False)
async with session.get(url, timeout=5) as resp: async with session.get(url, timeout=5) as resp:
charset = resp.charset or "utf-8" charset = resp.charset or "utf-8"
if resp.content_type in supported_content_types:
raise PlaylistSupported
try: try:
playlist_data = (await resp.content.read(64 * 1024)).decode(charset) playlist_data = (await resp.content.read(64 * 1024)).decode(charset)
except ValueError as err: 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 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() m3u_lines = m3u_data.splitlines()
playlist = [] playlist = []
@ -301,7 +312,7 @@ async def parse_pls(hass, url):
Based on https://github.com/mariob/plsparser/blob/master/src/plsparser.py 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() pls_parser = configparser.ConfigParser()
try: try:

View File

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

View File

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

View File

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

View File

@ -37,6 +37,7 @@ from .const import (
CONF_RTSP_TRANSPORT, CONF_RTSP_TRANSPORT,
CONF_STILL_IMAGE_URL, CONF_STILL_IMAGE_URL,
CONF_STREAM_SOURCE, CONF_STREAM_SOURCE,
CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
DEFAULT_NAME, DEFAULT_NAME,
FFMPEG_OPTION_MAP, FFMPEG_OPTION_MAP,
GET_IMAGE_TIMEOUT, GET_IMAGE_TIMEOUT,
@ -160,6 +161,10 @@ class GenericCamera(Camera):
CONF_RTSP_TRANSPORT CONF_RTSP_TRANSPORT
] ]
self._auth = generate_auth(device_info) 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_url = None
self._last_image = None self._last_image = None

View File

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

View File

@ -8,7 +8,11 @@ CONF_STILL_IMAGE_URL = "still_image_url"
CONF_STREAM_SOURCE = "stream_source" CONF_STREAM_SOURCE = "stream_source"
CONF_FRAMERATE = "framerate" CONF_FRAMERATE = "framerate"
CONF_RTSP_TRANSPORT = "rtsp_transport" 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 = { RTSP_TRANSPORTS = {
"tcp": "TCP", "tcp": "TCP",
"udp": "UDP", "udp": "UDP",

View File

@ -55,9 +55,13 @@
"authentication": "[%key:component::generic::config::step::user::data::authentication%]", "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%]", "limit_refetch_to_url_change": "[%key:component::generic::config::step::user::data::limit_refetch_to_url_change%]",
"password": "[%key:common::config_flow::data::password%]", "password": "[%key:common::config_flow::data::password%]",
"use_wallclock_as_timestamps": "Use wallclock as timestamps",
"username": "[%key:common::config_flow::data::username%]", "username": "[%key:common::config_flow::data::username%]",
"framerate": "[%key:component::generic::config::step::user::data::framerate%]", "framerate": "[%key:component::generic::config::step::user::data::framerate%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" "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": { "content_type": {

View File

@ -32,7 +32,6 @@
"user": { "user": {
"data": { "data": {
"authentication": "Authentication", "authentication": "Authentication",
"content_type": "Content Type",
"framerate": "Frame Rate (Hz)", "framerate": "Frame Rate (Hz)",
"limit_refetch_to_url_change": "Limit refetch to url change", "limit_refetch_to_url_change": "Limit refetch to url change",
"password": "Password", "password": "Password",
@ -72,15 +71,18 @@
"init": { "init": {
"data": { "data": {
"authentication": "Authentication", "authentication": "Authentication",
"content_type": "Content Type",
"framerate": "Frame Rate (Hz)", "framerate": "Frame Rate (Hz)",
"limit_refetch_to_url_change": "Limit refetch to url change", "limit_refetch_to_url_change": "Limit refetch to url change",
"password": "Password", "password": "Password",
"rtsp_transport": "RTSP transport protocol", "rtsp_transport": "RTSP transport protocol",
"still_image_url": "Still Image URL (e.g. http://...)", "still_image_url": "Still Image URL (e.g. http://...)",
"stream_source": "Stream Source URL (e.g. rtsp://...)", "stream_source": "Stream Source URL (e.g. rtsp://...)",
"use_wallclock_as_timestamps": "Use wallclock as timestamps",
"username": "Username", "username": "Username",
"verify_ssl": "Verify SSL certificate" "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 new_data = False
if event and event.data["new_state"] is not None: if event and event.data["new_state"] is not None:
new_state: State = event.data["new_state"] 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) self._history_current_period.append(new_state)
new_data = True new_data = True
if not new_data and current_period_end_timestamp < now_timestamp: 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): def get_device_property(self, name: str):
"""Get a single Insteon device property value (raw).""" """Get a single Insteon device property value (raw)."""
value = None
if (prop := self._insteon_device.properties.get(name)) is not 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 prop.value
return value return None
def _get_label(self): def _get_label(self):
"""Get the device label for grouped devices.""" """Get the device label for grouped devices."""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,6 @@
"""Support for monitoring an SABnzbd NZB client.""" """Support for monitoring an SABnzbd NZB client."""
from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
import logging import logging
@ -14,12 +16,13 @@ from homeassistant.const import (
CONF_PORT, CONF_PORT,
CONF_SENSORS, CONF_SENSORS,
CONF_SSL, CONF_SSL,
CONF_URL,
) )
from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
import homeassistant.helpers.config_validation as cv 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.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.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@ -42,7 +45,7 @@ from .const import (
UPDATE_INTERVAL, UPDATE_INTERVAL,
) )
from .sab import get_client from .sab import get_client
from .sensor import SENSOR_KEYS from .sensor import OLD_SENSOR_KEYS
PLATFORMS = ["sensor"] PLATFORMS = ["sensor"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -76,12 +79,11 @@ CONFIG_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_API_KEY): str, vol.Required(CONF_API_KEY): str,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
vol.Required(CONF_URL): str,
vol.Optional(CONF_PATH): str, vol.Optional(CONF_PATH): str,
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_SENSORS): vol.All( 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, 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}") 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: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the SabNzbd Component.""" """Set up the SabNzbd Component."""
sab_api = await get_client(hass, entry.data) sab_api = await get_client(hass, entry.data)
if not sab_api: if not sab_api:
raise ConfigEntryNotReady raise ConfigEntryNotReady
@ -137,6 +187,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
KEY_NAME: entry.data[CONF_NAME], KEY_NAME: entry.data[CONF_NAME],
} }
await migrate_unique_id(hass, entry)
update_device_identifiers(hass, entry)
@callback @callback
def extract_api(func: Callable) -> Callable: def extract_api(func: Callable) -> Callable:
"""Define a decorator to get the correct api for a service call.""" """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) _LOGGER.error(err)
async_track_time_interval(hass, async_update_sabnzbd, UPDATE_INTERVAL) async_track_time_interval(hass, async_update_sabnzbd, UPDATE_INTERVAL)
hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True 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( async def async_setup_entry(
@ -113,11 +125,16 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up a Sabnzbd sensor entry.""" """Set up a Sabnzbd sensor entry."""
sab_api_data = hass.data[DOMAIN][config_entry.entry_id][KEY_API_DATA] entry_id = config_entry.entry_id
client_name = hass.data[DOMAIN][config_entry.entry_id][KEY_NAME]
sab_api_data = hass.data[DOMAIN][entry_id][KEY_API_DATA]
client_name = hass.data[DOMAIN][entry_id][KEY_NAME]
async_add_entities( 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 _attr_should_poll = False
def __init__( def __init__(
self, sabnzbd_api_data, client_name, description: SabnzbdSensorEntityDescription self,
sabnzbd_api_data,
client_name,
description: SabnzbdSensorEntityDescription,
entry_id,
): ):
"""Initialize the sensor.""" """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.entity_description = description
self._sabnzbd_api = sabnzbd_api_data self._sabnzbd_api = sabnzbd_api_data
self._attr_name = f"{client_name} {description.name}" self._attr_name = f"{client_name} {description.name}"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE, entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, DOMAIN)}, identifiers={(DOMAIN, entry_id)},
name=DEFAULT_NAME, name=DEFAULT_NAME,
) )
@ -156,9 +177,11 @@ class SabnzbdSensor(SensorEntity):
self.entity_description.key self.entity_description.key
) )
if self._attr_native_value is not None:
if self.entity_description.key == SPEED_KEY: if self.entity_description.key == SPEED_KEY:
self._attr_native_value = round(float(self._attr_native_value) / 1024, 1) self._attr_native_value = round(
float(self._attr_native_value) / 1024, 1
)
elif "size" in self.entity_description.key: elif "size" in self.entity_description.key:
self._attr_native_value = round(float(self._attr_native_value), 2) self._attr_native_value = round(float(self._attr_native_value), 2)
self.schedule_update_ha_state() self.schedule_update_ha_state()

View File

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

View File

@ -7,10 +7,10 @@ from typing import Any
import aiohttp import aiohttp
from aiohttp import ClientSession from aiohttp import ClientSession
from ukrainealarm.client import Client from uasiren.client import Client
from homeassistant.config_entries import ConfigEntry 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.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -24,14 +24,11 @@ UPDATE_INTERVAL = timedelta(seconds=10)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Ukraine Alarm as config entry.""" """Set up Ukraine Alarm as config entry."""
api_key = entry.data[CONF_API_KEY]
region_id = entry.data[CONF_REGION] region_id = entry.data[CONF_REGION]
websession = async_get_clientsession(hass) websession = async_get_clientsession(hass)
coordinator = UkraineAlarmDataUpdateCoordinator( coordinator = UkraineAlarmDataUpdateCoordinator(hass, websession, region_id)
hass, websession, api_key, region_id
)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
@ -56,19 +53,18 @@ class UkraineAlarmDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self, self,
hass: HomeAssistant, hass: HomeAssistant,
session: ClientSession, session: ClientSession,
api_key: str,
region_id: str, region_id: str,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
self.region_id = region_id 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) super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL)
async def _async_update_data(self) -> dict[str, Any]: async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library.""" """Update data via library."""
try: try:
res = await self.ukrainealarm.get_alerts(self.region_id) res = await self.uasiren.get_alerts(self.region_id)
except aiohttp.ClientError as error: except aiohttp.ClientError as error:
raise UpdateFailed(f"Error fetching alerts from API: {error}") from error raise UpdateFailed(f"Error fetching alerts from API: {error}") from error

View File

@ -2,17 +2,20 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import logging
import aiohttp import aiohttp
from ukrainealarm.client import Client from uasiren.client import Client
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries 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 homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for Ukraine Alarm.""" """Config flow for Ukraine Alarm."""
@ -21,54 +24,47 @@ class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self): def __init__(self):
"""Initialize a new UkraineAlarmConfigFlow.""" """Initialize a new UkraineAlarmConfigFlow."""
self.api_key = None
self.states = None self.states = None
self.selected_region = None self.selected_region = None
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user.""" """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) websession = async_get_clientsession(self.hass)
reason = None
unknown_err_msg = None
try: try:
regions = await Client( regions = await Client(websession).get_regions()
websession, user_input[CONF_API_KEY]
).get_regions()
except aiohttp.ClientResponseError as ex: 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: except aiohttp.ClientConnectionError:
errors["base"] = "cannot_connect" reason = "cannot_connect"
except aiohttp.ClientError: except aiohttp.ClientError as ex:
errors["base"] = "unknown" reason = "unknown"
unknown_err_msg = str(ex)
except asyncio.TimeoutError: except asyncio.TimeoutError:
errors["base"] = "timeout" reason = "timeout"
if not errors and not regions: if not reason and not regions:
errors["base"] = "unknown" reason = "unknown"
unknown_err_msg = "no regions returned"
if not errors: if unknown_err_msg:
self.api_key = user_input[CONF_API_KEY] _LOGGER.error("Failed to connect to the service: %s", unknown_err_msg)
if reason:
return self.async_abort(reason=reason)
self.states = regions["states"] self.states = regions["states"]
return await self.async_step_state()
schema = vol.Schema( return await self._handle_pick_region("user", "district", user_input)
{
vol.Required(CONF_API_KEY): str,
}
)
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)
async def async_step_district(self, user_input=None): async def async_step_district(self, user_input=None):
"""Handle user-chosen district.""" """Handle user-chosen district."""
@ -126,7 +122,6 @@ class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry( return self.async_create_entry(
title=self.selected_region["regionName"], title=self.selected_region["regionName"],
data={ data={
CONF_API_KEY: self.api_key,
CONF_REGION: self.selected_region["regionId"], CONF_REGION: self.selected_region["regionId"],
CONF_NAME: self.selected_region["regionName"], CONF_NAME: self.selected_region["regionName"],
}, },

View File

@ -3,7 +3,7 @@
"name": "Ukraine Alarm", "name": "Ukraine Alarm",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ukraine_alarm", "documentation": "https://www.home-assistant.io/integrations/ukraine_alarm",
"requirements": ["ukrainealarm==0.0.1"], "requirements": ["uasiren==0.0.1"],
"codeowners": ["@PaulAnnekov"], "codeowners": ["@PaulAnnekov"],
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
} }

View File

@ -1,22 +1,15 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]" "max_regions": "Max 5 regions can be configured",
}, "already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
"error": { "rate_limit": "Too much requests",
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]", "unknown": "[%key:common::config_flow::error::unknown%]",
"timeout": "[%key:common::config_flow::error::timeout_connect%]" "timeout": "[%key:common::config_flow::error::timeout_connect%]"
}, },
"step": { "step": {
"user": { "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": { "data": {
"region": "Region" "region": "Region"
}, },
@ -24,13 +17,13 @@
}, },
"district": { "district": {
"data": { "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" "description": "If you want to monitor not only state, choose its specific district"
}, },
"community": { "community": {
"data": { "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" "description": "If you want to monitor not only state and district, choose its specific community"
} }

View File

@ -1,15 +1,19 @@
{ {
"config": { "config": {
"step": { "abort": {
"user": { "already_configured": "Location is already configured",
"description": "Set up the Ukraine Alarm integration. To generate an API key go to {api_url}", "cannot_connect": "Failed to connect",
"title": "Ukraine Alarm" "max_regions": "Max 5 regions can be configured",
"rate_limit": "Too much requests",
"timeout": "Timeout establishing connection",
"unknown": "Unexpected error"
}, },
"state": { "step": {
"community": {
"data": { "data": {
"region": "Region" "region": "Region"
}, },
"description": "Choose state to monitor" "description": "If you want to monitor not only state and district, choose its specific community"
}, },
"district": { "district": {
"data": { "data": {
@ -17,11 +21,11 @@
}, },
"description": "If you want to monitor not only state, choose its specific district" "description": "If you want to monitor not only state, choose its specific district"
}, },
"community": { "user": {
"data": { "data": {
"region": "Region" "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", "name": "UniFi Protect",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/unifiprotect", "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"], "dependencies": ["http"],
"codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"],
"quality_scale": "platinum", "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.node import Node
from zwave_js_server.model.value import ConfigurationValue 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"] NODE_STATUSES = ["asleep", "awake", "dead", "alive"]
CONF_SUBTYPE = "subtype" CONF_SUBTYPE = "subtype"
@ -41,3 +47,21 @@ def generate_config_parameter_subtype(config_value: ConfigurationValue) -> str:
parameter = f"{parameter}[{hex(config_value.property_key)}]" parameter = f"{parameter}[{hex(config_value.property_key)}]"
return f"{parameter} ({config_value.property_name})" 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_SUBTYPE,
CONF_VALUE_ID, CONF_VALUE_ID,
NODE_STATUSES, NODE_STATUSES,
async_bypass_dynamic_config_validation,
generate_config_parameter_subtype, generate_config_parameter_subtype,
get_config_parameter_value_schema, get_config_parameter_value_schema,
) )
from .helpers import ( from .helpers import (
async_get_node_from_device_id, async_get_node_from_device_id,
async_is_device_config_entry_not_loaded,
check_type_schema_map, check_type_schema_map,
get_zwave_value_from_config, get_zwave_value_from_config,
remove_keys_with_empty_values, 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 # 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 # validate the value without knowing the state of the device
try: 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] hass, config[CONF_DEVICE_ID]
) )
except ValueError as err: except ValueError as err:
@ -109,7 +109,7 @@ async def async_validate_condition_config(
f"Device {config[CONF_DEVICE_ID]} not found" f"Device {config[CONF_DEVICE_ID]} not found"
) from err ) from err
if device_config_entry_not_loaded: if bypass_dynamic_config_validation:
return config return config
if config[CONF_TYPE] == VALUE_TYPE: if config[CONF_TYPE] == VALUE_TYPE:

View File

@ -53,12 +53,12 @@ from .const import (
from .device_automation_helpers import ( from .device_automation_helpers import (
CONF_SUBTYPE, CONF_SUBTYPE,
NODE_STATUSES, NODE_STATUSES,
async_bypass_dynamic_config_validation,
generate_config_parameter_subtype, generate_config_parameter_subtype,
) )
from .helpers import ( from .helpers import (
async_get_node_from_device_id, async_get_node_from_device_id,
async_get_node_status_sensor_entity_id, async_get_node_status_sensor_entity_id,
async_is_device_config_entry_not_loaded,
check_type_schema_map, check_type_schema_map,
copy_available_params, copy_available_params,
get_value_state_schema, 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 # 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 # validate the value without knowing the state of the device
try: 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] hass, config[CONF_DEVICE_ID]
) )
except ValueError as err: except ValueError as err:
@ -223,7 +223,7 @@ async def async_validate_trigger_config(
f"Device {config[CONF_DEVICE_ID]} not found" f"Device {config[CONF_DEVICE_ID]} not found"
) from err ) from err
if device_config_entry_not_loaded: if bypass_dynamic_config_validation:
return config return config
trigger_type = config[CONF_TYPE] trigger_type = config[CONF_TYPE]

View File

@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022 MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 5 MINOR_VERSION: Final = 5
PATCH_VERSION: Final = "3" PATCH_VERSION: Final = "4"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) 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 locationsharinglib==4.1.5
# homeassistant.components.logi_circle # homeassistant.components.logi_circle
logi_circle==0.2.2 logi_circle==0.2.3
# homeassistant.components.london_underground # homeassistant.components.london_underground
london-tube-status==0.2 london-tube-status==0.2
@ -1065,7 +1065,7 @@ netdisco==3.0.0
netmap==0.7.0.2 netmap==0.7.0.2
# homeassistant.components.nam # homeassistant.components.nam
nettigo-air-monitor==1.2.3 nettigo-air-monitor==1.2.4
# homeassistant.components.neurio_energy # homeassistant.components.neurio_energy
neurio==0.3.1 neurio==0.3.1
@ -1399,7 +1399,7 @@ pycfdns==1.2.2
pychannels==1.0.0 pychannels==1.0.0
# homeassistant.components.cast # homeassistant.components.cast
pychromecast==12.1.1 pychromecast==12.1.2
# homeassistant.components.pocketcasts # homeassistant.components.pocketcasts
pycketcasts==1.0.0 pycketcasts==1.0.0
@ -1432,7 +1432,7 @@ pydaikin==2.7.0
pydanfossair==0.1.0 pydanfossair==0.1.0
# homeassistant.components.deconz # homeassistant.components.deconz
pydeconz==91 pydeconz==92
# homeassistant.components.delijn # homeassistant.components.delijn
pydelijn==1.0.0 pydelijn==1.0.0
@ -1981,7 +1981,7 @@ pytrafikverket==0.1.6.2
pyudev==0.22.0 pyudev==0.22.0
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
pyunifiprotect==3.4.0 pyunifiprotect==3.4.1
# homeassistant.components.uptimerobot # homeassistant.components.uptimerobot
pyuptimerobot==22.2.0 pyuptimerobot==22.2.0
@ -2153,7 +2153,7 @@ simplehound==0.3
simplepush==1.1.4 simplepush==1.1.4
# homeassistant.components.simplisafe # homeassistant.components.simplisafe
simplisafe-python==2022.05.0 simplisafe-python==2022.05.1
# homeassistant.components.sisyphus # homeassistant.components.sisyphus
sisyphus-control==3.1.2 sisyphus-control==3.1.2
@ -2343,7 +2343,7 @@ twitchAPI==2.5.2
uEagle==0.0.2 uEagle==0.0.2
# homeassistant.components.ukraine_alarm # homeassistant.components.ukraine_alarm
ukrainealarm==0.0.1 uasiren==0.0.1
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
unifi-discovery==1.1.2 unifi-discovery==1.1.2

View File

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

View File

@ -1,6 +1,6 @@
[metadata] [metadata]
name = homeassistant name = homeassistant
version = 2022.5.3 version = 2022.5.4
author = The Home Assistant Authors author = The Home Assistant Authors
author_email = hello@home-assistant.io author_email = hello@home-assistant.io
license = Apache-2.0 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 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.""" """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" headers = {"content-type": content_type}
aioclient_mock.get(url, text=load_fixture("bbc_radio_fourfm.m3u8", "cast")) aioclient_mock.get(url, text=load_fixture(fixture, "cast"), headers=headers)
with pytest.raises(PlaylistSupported): with pytest.raises(PlaylistSupported):
await parse_playlist(hass, url) await parse_playlist(hass, url)

View File

@ -18,6 +18,7 @@ from homeassistant.components.generic.const import (
CONF_RTSP_TRANSPORT, CONF_RTSP_TRANSPORT,
CONF_STILL_IMAGE_URL, CONF_STILL_IMAGE_URL,
CONF_STREAM_SOURCE, CONF_STREAM_SOURCE,
CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
DOMAIN, DOMAIN,
) )
from homeassistant.const import ( from homeassistant.const import (
@ -653,3 +654,32 @@ async def test_migrate_existing_ids(hass) -> None:
entity_entry = registry.async_get(entity_id) entity_entry = registry.async_get(entity_id)
assert entity_entry.unique_id == new_unique_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.sensor2").state == "0.83"
assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor3").state == "1"
assert hass.states.get("sensor.sensor4").state == "83.3" 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", "homeassistant.components.sabnzbd.sab.SabnzbdApi.check_available",
return_value=True, return_value=True,
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": SOURCE_IMPORT}, 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 collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from aiohttp import ClientConnectionError, ClientError, ClientResponseError from aiohttp import ClientConnectionError, ClientError, ClientResponseError, RequestInfo
import pytest import pytest
from yarl import URL
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.ukraine_alarm.const import DOMAIN from homeassistant.components.ukraine_alarm.const import DOMAIN
from homeassistant.core import HomeAssistant 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): def _region(rid, recurse=0, depth=0):
@ -57,12 +62,7 @@ async def test_state(hass: HomeAssistant) -> None:
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_FORM
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(result["flow_id"])
result["flow_id"],
{
"api_key": MOCK_API_KEY,
},
)
assert result2["type"] == RESULT_TYPE_FORM assert result2["type"] == RESULT_TYPE_FORM
with patch( with patch(
@ -80,7 +80,6 @@ async def test_state(hass: HomeAssistant) -> None:
assert result3["type"] == RESULT_TYPE_CREATE_ENTRY assert result3["type"] == RESULT_TYPE_CREATE_ENTRY
assert result3["title"] == "State 1" assert result3["title"] == "State 1"
assert result3["data"] == { assert result3["data"] == {
"api_key": MOCK_API_KEY,
"region": "1", "region": "1",
"name": result3["title"], "name": result3["title"],
} }
@ -94,12 +93,7 @@ async def test_state_district(hass: HomeAssistant) -> None:
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_FORM
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(result["flow_id"])
result["flow_id"],
{
"api_key": MOCK_API_KEY,
},
)
assert result2["type"] == RESULT_TYPE_FORM assert result2["type"] == RESULT_TYPE_FORM
result3 = await hass.config_entries.flow.async_configure( 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["type"] == RESULT_TYPE_CREATE_ENTRY
assert result4["title"] == "District 2.2" assert result4["title"] == "District 2.2"
assert result4["data"] == { assert result4["data"] == {
"api_key": MOCK_API_KEY,
"region": "2.2", "region": "2.2",
"name": result4["title"], "name": result4["title"],
} }
@ -139,12 +132,7 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None:
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_FORM
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(result["flow_id"])
result["flow_id"],
{
"api_key": MOCK_API_KEY,
},
)
assert result2["type"] == RESULT_TYPE_FORM assert result2["type"] == RESULT_TYPE_FORM
result3 = await hass.config_entries.flow.async_configure( 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["type"] == RESULT_TYPE_CREATE_ENTRY
assert result4["title"] == "State 2" assert result4["title"] == "State 2"
assert result4["data"] == { assert result4["data"] == {
"api_key": MOCK_API_KEY,
"region": "2", "region": "2",
"name": result4["title"], "name": result4["title"],
} }
@ -186,9 +173,6 @@ async def test_state_district_community(hass: HomeAssistant) -> None:
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{
"api_key": MOCK_API_KEY,
},
) )
assert result2["type"] == RESULT_TYPE_FORM 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["type"] == RESULT_TYPE_CREATE_ENTRY
assert result5["title"] == "Community 3.2.1" assert result5["title"] == "Community 3.2.1"
assert result5["data"] == { assert result5["data"] == {
"api_key": MOCK_API_KEY,
"region": "3.2.1", "region": "3.2.1",
"name": result5["title"], "name": result5["title"],
} }
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
async def test_invalid_api(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: async def test_max_regions(hass: HomeAssistant) -> None:
"""Test we can create entry for just region.""" """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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} 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"], async def test_rate_limit(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None:
{ """Test rate limit error."""
"api_key": MOCK_API_KEY, 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 result["type"] == RESULT_TYPE_ABORT
assert result2["step_id"] == "user" assert result["reason"] == "rate_limit"
assert result2["errors"] == {"base": "invalid_api_key"}
async def test_server_error(hass: HomeAssistant, mock_get_regions) -> None: 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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "unknown"
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"}
async def test_cannot_connect(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: 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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "cannot_connect"
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"}
async def test_unknown_client_error( async def test_unknown_client_error(
hass: HomeAssistant, mock_get_regions: AsyncMock hass: HomeAssistant, mock_get_regions: AsyncMock
) -> None: ) -> 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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "unknown"
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"}
async def test_timeout_error(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: 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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "timeout"
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"}
async def test_no_regions_returned( async def test_no_regions_returned(
hass: HomeAssistant, mock_get_regions: AsyncMock hass: HomeAssistant, mock_get_regions: AsyncMock
) -> None: ) -> 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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "unknown"
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"}