From 22930457c5512c7a8320267ba724b6f506e2b82a Mon Sep 17 00:00:00 2001 From: Marvin ROGER Date: Thu, 12 May 2022 16:23:18 +0200 Subject: [PATCH 01/20] Fix timezone issue on onvif integration (#70473) --- homeassistant/components/onvif/device.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index f45362c6e6c..d376b7fe258 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -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") From 4a544254266292e8cb892b80595b05b7ebb395ef Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Mon, 9 May 2022 14:20:45 -0400 Subject: [PATCH 02/20] Fix Insteon issue with dimmer default on level (#71426) --- homeassistant/components/insteon/insteon_entity.py | 5 ++--- homeassistant/components/insteon/light.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index 60935f3f951..67d30ba8cad 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -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.""" diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py index 05ad9794042..bf8b693b103 100644 --- a/homeassistant/components/insteon/light.py +++ b/homeassistant/components/insteon/light.py @@ -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 ) From b213f221b5eaeb7f105350556bd65af2aa589301 Mon Sep 17 00:00:00 2001 From: Shai Ungar Date: Mon, 9 May 2022 10:27:23 +0300 Subject: [PATCH 03/20] Migrate sabnzbd sensors unique ids (#71455) * Migrate sensors unique ids 1. migrate sensors to have unique id constructed also from entry_id 2. add migration flow in init 3. bump config flow to version 2 4. add tests for migration * move migrate to async_setup_entry * 1. Use the entity registry api in tests 2. Set up the config entry and not use integration directly 3. remove patch for entity registry * fix too many lines * Update tests/components/sabnzbd/test_init.py Co-authored-by: Martin Hjelmare * Update tests/components/sabnzbd/test_init.py Co-authored-by: Martin Hjelmare * Update tests/components/sabnzbd/test_init.py Co-authored-by: Martin Hjelmare * Update tests/components/sabnzbd/test_init.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/sabnzbd/__init__.py | 56 +++++++++++++ homeassistant/components/sabnzbd/sensor.py | 35 +++++--- tests/components/sabnzbd/test_config_flow.py | 1 - tests/components/sabnzbd/test_init.py | 85 ++++++++++++++++++++ 4 files changed, 164 insertions(+), 13 deletions(-) create mode 100644 tests/components/sabnzbd/test_init.py diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index aca50e404a2..3fa5054a5f0 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -1,4 +1,6 @@ """Support for monitoring an SABnzbd NZB client.""" +from __future__ import annotations + from collections.abc import Callable import logging @@ -19,7 +21,9 @@ from homeassistant.const import ( 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 @@ -123,8 +127,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 +189,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 +243,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 diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index 539eaa4f097..ebdb9190ed9 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -113,11 +113,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 +133,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 +165,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() diff --git a/tests/components/sabnzbd/test_config_flow.py b/tests/components/sabnzbd/test_config_flow.py index d04c5b18ab1..bc72dff2535 100644 --- a/tests/components/sabnzbd/test_config_flow.py +++ b/tests/components/sabnzbd/test_config_flow.py @@ -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}, diff --git a/tests/components/sabnzbd/test_init.py b/tests/components/sabnzbd/test_init.py new file mode 100644 index 00000000000..9bdef4119d0 --- /dev/null +++ b/tests/components/sabnzbd/test_init.py @@ -0,0 +1,85 @@ +"""Tests for the SABnzbd Integration.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.sabnzbd import DEFAULT_NAME, DOMAIN, 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 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) + } From 6c70a518ebdbeef49ffd8da930c308e6dc513d73 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 8 May 2022 15:21:18 -0600 Subject: [PATCH 04/20] Bump simplisafe-python to 2022.05.1 (#71545) * Bump simplisafe-python to 2022.05.1 * Trigger Build --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 804523b3390..cb1b02e37ae 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -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": [ diff --git a/requirements_all.txt b/requirements_all.txt index 15ce8f1c8ce..d869142ce86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eeb9f18fbfa..7ab7fa4a5ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From 18d440cc6f92c530d2955e8d0b5a83639dc9661f Mon Sep 17 00:00:00 2001 From: Shai Ungar Date: Mon, 9 May 2022 13:34:16 +0300 Subject: [PATCH 05/20] Fix SABnzbd config check (#71549) --- homeassistant/components/sabnzbd/__init__.py | 6 ++---- homeassistant/components/sabnzbd/sensor.py | 14 +++++++++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index 3fa5054a5f0..c03d27fefe5 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -16,7 +16,6 @@ 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 @@ -46,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__) @@ -80,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, }, diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index ebdb9190ed9..043a344ec7b 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -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( From 2caa92b1ebefb1e72ec42a4a2901af9f4f392c93 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sun, 8 May 2022 22:07:12 -0400 Subject: [PATCH 06/20] Fix typer/click incompatibilty for unifiprotect (#71555) Co-authored-by: J. Nick Koston --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index d5dbb51ffc1..3c3b461ed4f 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -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", diff --git a/requirements_all.txt b/requirements_all.txt index d869142ce86..e72c54c7129 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ab7fa4a5ed..1ec8844dad7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From 4b28d522a8d7aa4eeb7234c1f65dccbeacd0d821 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 May 2022 13:57:32 +0200 Subject: [PATCH 07/20] Improve Google Cast detection of HLS playlists (#71564) --- homeassistant/components/cast/helpers.py | 17 ++++++++++++++--- tests/components/cast/test_helpers.py | 21 ++++++++++++++++++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index dd98a2bc051..dfeb9fce25b 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -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: diff --git a/tests/components/cast/test_helpers.py b/tests/components/cast/test_helpers.py index 0d7a3b1ff14..d729d36a225 100644 --- a/tests/components/cast/test_helpers.py +++ b/tests/components/cast/test_helpers.py @@ -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) From 0099560a8d22102cba703dd8e9e5a098c757b44a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 May 2022 11:03:32 +0200 Subject: [PATCH 08/20] Correct device class for meater cook sensors (#71565) --- homeassistant/components/meater/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index 8c719d588d8..fecde148a5e 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -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__( From 8f5677f523bba7b35a3da4dbc82153635bae460d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 May 2022 13:59:13 +0200 Subject: [PATCH 09/20] Bump pychromecast to 12.1.2 (#71567) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index cee46913937..10edc81e0fc 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -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", diff --git a/requirements_all.txt b/requirements_all.txt index e72c54c7129..b8f7a452499 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ec8844dad7..f678c7ff2ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From 596039ff4a47b35ec1d23a7a0e155ee612e669e2 Mon Sep 17 00:00:00 2001 From: Evan Bruhn Date: Mon, 9 May 2022 21:58:26 +1000 Subject: [PATCH 10/20] Bump logi_circle to 0.2.3 (#71578) --- homeassistant/components/logi_circle/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/logi_circle/manifest.json b/homeassistant/components/logi_circle/manifest.json index 94c040f3b75..2d8495df576 100644 --- a/homeassistant/components/logi_circle/manifest.json +++ b/homeassistant/components/logi_circle/manifest.json @@ -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", diff --git a/requirements_all.txt b/requirements_all.txt index b8f7a452499..fc87c3b3601 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f678c7ff2ed..973a42c0628 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From cd69b5708f984f36484f41d43a4deb50e840c470 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 10 May 2022 00:11:50 +0200 Subject: [PATCH 11/20] Bump nam backend library to version 1.2.4 (#71584) --- homeassistant/components/nam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index 16231ef0b88..a842af46f84 100644 --- a/homeassistant/components/nam/manifest.json +++ b/homeassistant/components/nam/manifest.json @@ -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.", diff --git a/requirements_all.txt b/requirements_all.txt index fc87c3b3601..74f7b42a900 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 973a42c0628..e1b17dd97ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From fabea6aacb5fe9f42809eecb3db98d7b9d344c54 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 10 May 2022 00:03:03 +0200 Subject: [PATCH 12/20] Bump pydeconz to v92 (#71613) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 1ce3477db70..2a4a5ccf253 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -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", diff --git a/requirements_all.txt b/requirements_all.txt index 74f7b42a900..0cb659a32d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1b17dd97ca..9a905e42fae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From aa3012243414ee89f669d66988c856e04cf6735b Mon Sep 17 00:00:00 2001 From: rappenze Date: Tue, 10 May 2022 22:33:40 +0200 Subject: [PATCH 13/20] Fix wrong brightness level change visible in UI (#71655) --- homeassistant/components/fibaro/light.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index 9d0309bc4ee..08a9e651668 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -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 From 1f53786e1b230b279fe399ef01d210b43f659ec6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 May 2022 22:44:35 -0500 Subject: [PATCH 14/20] Prevent history_stats from rejecting states when microseconds differ (#71704) --- .../components/history_stats/data.py | 6 +- tests/components/history_stats/test_sensor.py | 114 ++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 3f22f4cc32b..3d21cca6b6d 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -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: diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index bfa0c8f415e..4f56edaa291 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -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" From a19a88db639344b4201ce9025b79979fdafc6a2f Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 11 May 2022 22:55:12 -0400 Subject: [PATCH 15/20] Fix zwave_js device automation bug (#71715) --- .../zwave_js/device_automation_helpers.py | 24 +++++++++++++++++++ .../components/zwave_js/device_condition.py | 6 ++--- .../components/zwave_js/device_trigger.py | 6 ++--- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zwave_js/device_automation_helpers.py b/homeassistant/components/zwave_js/device_automation_helpers.py index 906efb2c4f9..f17ddccf03c 100644 --- a/homeassistant/components/zwave_js/device_automation_helpers.py +++ b/homeassistant/components/zwave_js/device_automation_helpers.py @@ -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 diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index c70371d6f8a..549319d23f4 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -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: diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index 89379f9a953..0b6369654fe 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -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] From c9543d8665ec0ed7f72bca1286bda039ef9a343a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 12 May 2022 07:34:25 -0700 Subject: [PATCH 16/20] Bumped version to 2022.5.4 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 569c121f909..5eb59e819da 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -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) diff --git a/setup.cfg b/setup.cfg index 2ec80dd2855..b492bd0a240 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 From 316bfc89f27aca3f175abfbdb8510368fb73a784 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 May 2022 12:58:42 -0500 Subject: [PATCH 17/20] Fix merge conflict with master to dev in sabnzbd (CI fix) (#71605) --- tests/components/sabnzbd/test_init.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/sabnzbd/test_init.py b/tests/components/sabnzbd/test_init.py index 9bdef4119d0..f140c332778 100644 --- a/tests/components/sabnzbd/test_init.py +++ b/tests/components/sabnzbd/test_init.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from homeassistant.components.sabnzbd import DEFAULT_NAME, DOMAIN, SENSOR_KEYS +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 @@ -54,7 +54,7 @@ async def test_unique_id_migrate(hass, device_registry, entity_registry): entity_id_sensor_key = [] - for sensor_key in SENSOR_KEYS: + for sensor_key in OLD_SENSOR_KEYS: mock_entity_id = f"{SENSOR_DOMAIN}.{DOMAIN}_{sensor_key}" entity_registry.async_get_or_create( SENSOR_DOMAIN, From 2500cc6132d7e5a2eb09cface512b92faba0f88d Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 13 May 2022 07:43:24 +0800 Subject: [PATCH 18/20] Add use_wallclock_as_timestamps option to generic (#71245) Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/components/generic/camera.py | 5 ++++ .../components/generic/config_flow.py | 20 ++++++++++++- homeassistant/components/generic/const.py | 6 +++- homeassistant/components/generic/strings.json | 4 +++ .../components/generic/translations/en.json | 6 ++-- tests/components/generic/test_config_flow.py | 30 +++++++++++++++++++ 6 files changed, 67 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index d20032e2607..197890efad3 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -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 diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 086262aa0a1..0a49393d9cc 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -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, ) diff --git a/homeassistant/components/generic/const.py b/homeassistant/components/generic/const.py index 60b4cec61a6..8ae5f16c4c4 100644 --- a/homeassistant/components/generic/const.py +++ b/homeassistant/components/generic/const.py @@ -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", diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index 01b1fe48a82..0954656f71d 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -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": { diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index b158488f178..b552c780d29 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -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" } } } diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 457cac26aa5..dd53cb8548e 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -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 From f65eca9c195f340751c2092b9ea719bbb6fb8f65 Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Fri, 13 May 2022 02:45:39 +0300 Subject: [PATCH 19/20] Changed API for Ukraine Alarm (#71754) --- .../components/ukraine_alarm/__init__.py | 14 +- .../components/ukraine_alarm/config_flow.py | 67 ++++---- .../components/ukraine_alarm/manifest.json | 2 +- .../components/ukraine_alarm/strings.json | 17 +- .../ukraine_alarm/translations/en.json | 22 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../ukraine_alarm/test_config_flow.py | 161 ++++++------------ 8 files changed, 108 insertions(+), 179 deletions(-) diff --git a/homeassistant/components/ukraine_alarm/__init__.py b/homeassistant/components/ukraine_alarm/__init__.py index b2b2ff4162f..587854a3a7e 100644 --- a/homeassistant/components/ukraine_alarm/__init__.py +++ b/homeassistant/components/ukraine_alarm/__init__.py @@ -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 diff --git a/homeassistant/components/ukraine_alarm/config_flow.py b/homeassistant/components/ukraine_alarm/config_flow.py index dcf41658dfb..4f1e1c5cf23 100644 --- a/homeassistant/components/ukraine_alarm/config_flow.py +++ b/homeassistant/components/ukraine_alarm/config_flow.py @@ -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"], }, diff --git a/homeassistant/components/ukraine_alarm/manifest.json b/homeassistant/components/ukraine_alarm/manifest.json index 08dad9960b5..5592ac774a4 100644 --- a/homeassistant/components/ukraine_alarm/manifest.json +++ b/homeassistant/components/ukraine_alarm/manifest.json @@ -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" } diff --git a/homeassistant/components/ukraine_alarm/strings.json b/homeassistant/components/ukraine_alarm/strings.json index 79f81e71b08..6831d66adb3 100644 --- a/homeassistant/components/ukraine_alarm/strings.json +++ b/homeassistant/components/ukraine_alarm/strings.json @@ -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" } diff --git a/homeassistant/components/ukraine_alarm/translations/en.json b/homeassistant/components/ukraine_alarm/translations/en.json index 2c39945cb87..857311ea3e7 100644 --- a/homeassistant/components/ukraine_alarm/translations/en.json +++ b/homeassistant/components/ukraine_alarm/translations/en.json @@ -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,12 +21,12 @@ }, "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" } } } -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 0cb659a32d0..ef48c8117f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a905e42fae..cb683ae0bab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/ukraine_alarm/test_config_flow.py b/tests/components/ukraine_alarm/test_config_flow.py index 3832e6a9fb6..7369816fdc7 100644 --- a/tests/components/ukraine_alarm/test_config_flow.py +++ b/tests/components/ukraine_alarm/test_config_flow.py @@ -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" From 99c5070591128542c1f9ea05cfd69a4fe8a8ea23 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 May 2022 18:41:25 +0200 Subject: [PATCH 20/20] Add missing cast test fixture (#71595) --- tests/components/cast/fixtures/rthkaudio2.m3u8 | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tests/components/cast/fixtures/rthkaudio2.m3u8 diff --git a/tests/components/cast/fixtures/rthkaudio2.m3u8 b/tests/components/cast/fixtures/rthkaudio2.m3u8 new file mode 100644 index 00000000000..388c115635f --- /dev/null +++ b/tests/components/cast/fixtures/rthkaudio2.m3u8 @@ -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