From 22930457c5512c7a8320267ba724b6f506e2b82a Mon Sep 17 00:00:00 2001 From: Marvin ROGER Date: Thu, 12 May 2022 16:23:18 +0200 Subject: [PATCH 01/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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 From 5deb78a0dd26e663f54723c320c7452ba4d3ada0 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 18 May 2022 19:59:35 +0200 Subject: [PATCH 21/35] Refresh camera stream source of Synology DSM connected cameras (#70938) Co-authored-by: Paulus Schoutsen --- .../components/synology_dsm/__init__.py | 24 +++++++++++++--- .../components/synology_dsm/camera.py | 28 ++++++++++++++++++- .../components/synology_dsm/const.py | 3 ++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 1151bf128cc..0881d5a85e9 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -28,6 +28,7 @@ from homeassistant.helpers.device_registry import ( DeviceEntry, async_get_registry as get_dev_reg, ) +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .common import SynoApi @@ -41,6 +42,7 @@ from .const import ( EXCEPTION_DETAILS, EXCEPTION_UNKNOWN, PLATFORMS, + SIGNAL_CAMERA_SOURCE_CHANGED, SYNO_API, SYSTEM_LOADED, UNDO_UPDATE_LISTENER, @@ -128,6 +130,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return None surveillance_station = api.surveillance_station + current_data: dict[str, SynoCamera] = { + camera.id: camera for camera in surveillance_station.get_all_cameras() + } try: async with async_timeout.timeout(30): @@ -135,12 +140,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SynologyDSMAPIErrorException as err: raise UpdateFailed(f"Error communicating with API: {err}") from err - return { - "cameras": { - camera.id: camera for camera in surveillance_station.get_all_cameras() - } + new_data: dict[str, SynoCamera] = { + camera.id: camera for camera in surveillance_station.get_all_cameras() } + for cam_id, cam_data_new in new_data.items(): + if ( + (cam_data_current := current_data.get(cam_id)) is not None + and cam_data_current.live_view.rtsp != cam_data_new.live_view.rtsp + ): + async_dispatcher_send( + hass, + f"{SIGNAL_CAMERA_SOURCE_CHANGED}_{entry.entry_id}_{cam_id}", + cam_data_new.live_view.rtsp, + ) + + return {"cameras": new_data} + async def async_coordinator_update_data_central() -> None: """Fetch all device and sensor data from api.""" try: diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index c6d44d8883d..cab2536187c 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -16,7 +16,8 @@ from homeassistant.components.camera import ( CameraEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -27,6 +28,7 @@ from .const import ( COORDINATOR_CAMERAS, DEFAULT_SNAPSHOT_QUALITY, DOMAIN, + SIGNAL_CAMERA_SOURCE_CHANGED, SYNO_API, ) from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription @@ -130,6 +132,29 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): """Return the camera motion detection status.""" return self.camera_data.is_motion_detection_enabled # type: ignore[no-any-return] + def _listen_source_updates(self) -> None: + """Listen for camera source changed events.""" + + @callback + def _handle_signal(url: str) -> None: + if self.stream: + _LOGGER.debug("Update stream URL for camera %s", self.camera_data.name) + self.stream.update_source(url) + + assert self.platform + assert self.platform.config_entry + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SIGNAL_CAMERA_SOURCE_CHANGED}_{self.platform.config_entry.entry_id}_{self.camera_data.id}", + _handle_signal, + ) + ) + + async def async_added_to_hass(self) -> None: + """Subscribe to signal.""" + self._listen_source_updates() + def camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: @@ -162,6 +187,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): ) if not self.available: return None + return self.camera_data.live_view.rtsp # type: ignore[no-any-return] def enable_motion_detection(self) -> None: diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 1b4e5f0bb36..f716130a5e4 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -43,6 +43,9 @@ DEFAULT_SNAPSHOT_QUALITY = SNAPSHOT_PROFILE_BALANCED ENTITY_UNIT_LOAD = "load" +# Signals +SIGNAL_CAMERA_SOURCE_CHANGED = "synology_dsm.camera_stream_source_changed" + # Services SERVICE_REBOOT = "reboot" SERVICE_SHUTDOWN = "shutdown" From a3bd911ce396956e2cee038b2ea8d39011e12db0 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 18 May 2022 19:29:02 +0200 Subject: [PATCH 22/35] Warn user if "model" key is missing from Shelly firmware (#71612) Co-authored-by: Paulus Schoutsen --- .../components/shelly/config_flow.py | 33 ++++++++++++------- homeassistant/components/shelly/strings.json | 3 +- .../components/shelly/translations/en.json | 1 + tests/components/shelly/test_config_flow.py | 23 +++++++++++++ 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 9311be1a49e..abcfe689e93 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -119,6 +119,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" + except KeyError: + errors["base"] = "firmware_not_fully_provisioned" except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -160,6 +162,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except aioshelly.exceptions.JSONRPCError: errors["base"] = "cannot_connect" + except KeyError: + errors["base"] = "firmware_not_fully_provisioned" except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -219,6 +223,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: self.device_info = await validate_input(self.hass, self.host, self.info, {}) + except KeyError: + LOGGER.debug("Shelly host %s firmware not fully provisioned", self.host) except HTTP_CONNECT_ERRORS: return self.async_abort(reason="cannot_connect") @@ -229,18 +235,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle discovery confirm.""" errors: dict[str, str] = {} - if user_input is not None: - return self.async_create_entry( - title=self.device_info["title"], - data={ - "host": self.host, - CONF_SLEEP_PERIOD: self.device_info[CONF_SLEEP_PERIOD], - "model": self.device_info["model"], - "gen": self.device_info["gen"], - }, - ) - - self._set_confirm_only() + try: + if user_input is not None: + return self.async_create_entry( + title=self.device_info["title"], + data={ + "host": self.host, + CONF_SLEEP_PERIOD: self.device_info[CONF_SLEEP_PERIOD], + "model": self.device_info["model"], + "gen": self.device_info["gen"], + }, + ) + except KeyError: + errors["base"] = "firmware_not_fully_provisioned" + else: + self._set_confirm_only() return self.async_show_form( step_id="confirm_discovery", diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 209ae6682b8..db1c6043187 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -21,7 +21,8 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "firmware_not_fully_provisioned": "Device not fully provisioned. Please contact Shelly support" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json index c23eb13840c..f3e882c3016 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Failed to connect", + "firmware_not_fully_provisioned": "Device not fully provisioned. Please contact Shelly support", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 1293fe92760..713999de36f 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -225,6 +225,29 @@ async def test_form_errors_get_info(hass, error): assert result2["errors"] == {"base": base_error} +@pytest.mark.parametrize("error", [(KeyError, "firmware_not_fully_provisioned")]) +async def test_form_missing_key_get_info(hass, error): + """Test we handle missing key.""" + exc, base_error = error + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch( + "aioshelly.common.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False, "gen": "2"}, + ), patch( + "homeassistant.components.shelly.config_flow.validate_input", + side_effect=KeyError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": base_error} + + @pytest.mark.parametrize( "error", [(asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown")] ) From 5fbc4b8dbaab4f41e8fd030f8286e1912a0f4586 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 13 May 2022 21:30:44 +1000 Subject: [PATCH 23/35] Remove LIFX bulb discovery from the inflight list if it fails to connect (#71673) Remove the bulb discovery from the inflight list if it fails to connect Signed-off-by: Avi Miller --- homeassistant/components/lifx/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 428ae8745e0..c4df24a0cb0 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -419,6 +419,8 @@ class LIFXManager: if color_resp is None or version_resp is None: _LOGGER.error("Failed to connect to %s", bulb.ip_addr) bulb.registered = False + if bulb.mac_addr in self.discoveries_inflight: + self.discoveries_inflight.pop(bulb.mac_addr) else: bulb.timeout = MESSAGE_TIMEOUT bulb.retry_count = MESSAGE_RETRIES From 107615ebefdb0b4db4985c560077276da7a1dd8c Mon Sep 17 00:00:00 2001 From: rappenze Date: Sun, 15 May 2022 15:02:05 +0200 Subject: [PATCH 24/35] Limit parallel requests in fibaro light (#71762) --- homeassistant/components/fibaro/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index 08a9e651668..0115e0301c3 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -23,6 +23,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import FIBARO_DEVICES, FibaroDevice from .const import DOMAIN +PARALLEL_UPDATES = 2 + def scaleto255(value: int | None) -> int: """Scale the input value from 0-100 to 0-255.""" From 2448661371246bb0192fda88c6b11376686c91c2 Mon Sep 17 00:00:00 2001 From: Ethan Madden Date: Mon, 16 May 2022 01:30:49 -0700 Subject: [PATCH 25/35] Fix VeSync air_quality fan attribute (#71771) * Refactor attribute inclusion for VeSync fans. A recent change to pyvesync (introduced in 2.2) changed `air_quality` to refer to air quality as an integer representation of perceived air quality rather than a direct reading of the PM2.5 sensor. With 2.3 the PM2.5 sensor access was restored as `air_quality_value`. Unfortunately, `air_quality_value` was not added as an attribute on the fan object, and rather only exists in the `details` dictionary on the fan object. * Update homeassistant/components/vesync/fan.py Co-authored-by: Martin Hjelmare * Rename `air_quality_value` attribute to `pm25` This should make it more clear what the attribute actually represents * `air_quality` attribute reports `air_quality_value` This restores previous behavior for this integration to what it was before the `pyvesync==2.02` upgrade, using the `air_quality` attribute to report pm2.5 concentrations (formerly `air_quality`) rather the vague measurement now reported by `air_quality`. Co-authored-by: Martin Hjelmare --- homeassistant/components/vesync/fan.py | 4 ++-- homeassistant/components/vesync/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index e37a6c8893e..f16a785ee1e 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -171,8 +171,8 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): if hasattr(self.smartfan, "night_light"): attr["night_light"] = self.smartfan.night_light - if hasattr(self.smartfan, "air_quality"): - attr["air_quality"] = self.smartfan.air_quality + if self.smartfan.details.get("air_quality_value") is not None: + attr["air_quality"] = self.smartfan.details["air_quality_value"] if hasattr(self.smartfan, "mode"): attr["mode"] = self.smartfan.mode diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index c93e070a484..49be473b748 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -3,7 +3,7 @@ "name": "VeSync", "documentation": "https://www.home-assistant.io/integrations/vesync", "codeowners": ["@markperdue", "@webdjoe", "@thegardenmonkey"], - "requirements": ["pyvesync==2.0.2"], + "requirements": ["pyvesync==2.0.3"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["pyvesync"] diff --git a/requirements_all.txt b/requirements_all.txt index ef48c8117f5..011e759b481 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1996,7 +1996,7 @@ pyvera==0.3.13 pyversasense==0.0.6 # homeassistant.components.vesync -pyvesync==2.0.2 +pyvesync==2.0.3 # homeassistant.components.vizio pyvizio==0.1.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb683ae0bab..49bbec71c35 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1313,7 +1313,7 @@ pyuptimerobot==22.2.0 pyvera==0.3.13 # homeassistant.components.vesync -pyvesync==2.0.2 +pyvesync==2.0.3 # homeassistant.components.vizio pyvizio==0.1.57 From 5f3c7f11d8bd572d7e1525c72353c59bd472b0f2 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Fri, 13 May 2022 18:42:33 -0400 Subject: [PATCH 26/35] Fix handling package detection for latest UniFi Protect beta (#71821) Co-authored-by: J. Nick Koston --- homeassistant/components/unifiprotect/config_flow.py | 8 ++++++-- homeassistant/components/unifiprotect/manifest.json | 2 +- homeassistant/components/unifiprotect/select.py | 2 +- homeassistant/components/unifiprotect/switch.py | 9 +++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifiprotect/test_switch.py | 10 +++++++--- 7 files changed, 26 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 0cfce44a6ca..27108d24eaa 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -143,7 +143,9 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_VERIFY_SSL] = False nvr_data, errors = await self._async_get_nvr_data(user_input) if nvr_data and not errors: - return self._async_create_entry(nvr_data.name, user_input) + return self._async_create_entry( + nvr_data.name or nvr_data.type, user_input + ) placeholders = { "name": discovery_info["hostname"] @@ -289,7 +291,9 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(nvr_data.mac) self._abort_if_unique_id_configured() - return self._async_create_entry(nvr_data.name, user_input) + return self._async_create_entry( + nvr_data.name or nvr_data.type, user_input + ) user_input = user_input or {} return self.async_show_form( diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 3c3b461ed4f..3efad51db31 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.1", "unifi-discovery==1.1.2"], + "requirements": ["pyunifiprotect==3.5.1", "unifi-discovery==1.1.2"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index b7b53ff81c8..f0500ea54e5 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -140,7 +140,7 @@ def _get_doorbell_options(api: ProtectApiClient) -> list[dict[str, Any]]: def _get_paired_camera_options(api: ProtectApiClient) -> list[dict[str, Any]]: options = [{"id": TYPE_EMPTY_VALUE, "name": "Not Paired"}] for camera in api.bootstrap.cameras.values(): - options.append({"id": camera.id, "name": camera.name}) + options.append({"id": camera.id, "name": camera.name or camera.type}) return options diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 2257f399f75..85a089994f8 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -159,6 +159,15 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_value="is_face_detection_on", ufp_set_method="set_face_detection", ), + ProtectSwitchEntityDescription( + key="smart_package", + name="Detections: Package", + icon="mdi:package-variant-closed", + entity_category=EntityCategory.CONFIG, + ufp_required_field="can_detect_package", + ufp_value="is_package_detection_on", + ufp_set_method="set_package_detection", + ), ) SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( diff --git a/requirements_all.txt b/requirements_all.txt index 011e759b481..a7cef9f1b43 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.1 +pyunifiprotect==3.5.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49bbec71c35..eb2be965ee7 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.1 +pyunifiprotect==3.5.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 0a3ac92076e..c54d04a8cb7 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -26,9 +26,13 @@ from .conftest import ( ids_from_device_description, ) -CAMERA_SWITCHES_NO_FACE = [d for d in CAMERA_SWITCHES if d.name != "Detections: Face"] +CAMERA_SWITCHES_BASIC = [ + d + for d in CAMERA_SWITCHES + if d.name != "Detections: Face" and d.name != "Detections: Package" +] CAMERA_SWITCHES_NO_EXTRA = [ - d for d in CAMERA_SWITCHES_NO_FACE if d.name not in ("High FPS", "Privacy Mode") + d for d in CAMERA_SWITCHES_BASIC if d.name not in ("High FPS", "Privacy Mode") ] @@ -253,7 +257,7 @@ async def test_switch_setup_camera_all( entity_registry = er.async_get(hass) - for description in CAMERA_SWITCHES_NO_FACE: + for description in CAMERA_SWITCHES_BASIC: unique_id, entity_id = ids_from_device_description( Platform.SWITCH, camera, description ) From 81f9cc40dd8eaad557a58829fc488c1624b20083 Mon Sep 17 00:00:00 2001 From: RadekHvizdos <10856567+RadekHvizdos@users.noreply.github.com> Date: Sun, 15 May 2022 11:29:35 +0200 Subject: [PATCH 27/35] Add missing Shelly Cover sensors bugfix (#71831) Switching Shelly Plus 2PM from switch to cover mode results in missing sensors for Power, Voltage, Energy and Temperature. These parameters are still available in the API, but need to be accessed via "cover" key instead of "switch" key. This change adds the missing sensors. --- homeassistant/components/shelly/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index df5a75a7ed9..77c09283fbf 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -297,6 +297,9 @@ def get_rpc_key_instances(keys_dict: dict[str, Any], key: str) -> list[str]: if key in keys_dict: return [key] + if key == "switch" and "cover:0" in keys_dict: + key = "cover" + keys_list: list[str] = [] for i in range(MAX_RPC_KEY_INSTANCES): key_inst = f"{key}:{i}" From ce39461810194d8f2f4b2fb1e351c0fa24efc8ae Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 15 May 2022 20:36:57 +0200 Subject: [PATCH 28/35] Revert changing `pysnmp` to `pysnmplib` (#71901) --- homeassistant/components/brother/manifest.json | 2 +- homeassistant/components/snmp/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index c230373ea36..aaf1af72db9 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -3,7 +3,7 @@ "name": "Brother Printer", "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], - "requirements": ["brother==1.2.0"], + "requirements": ["brother==1.1.0"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index ef0213e82dc..76df9e18606 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -2,7 +2,7 @@ "domain": "snmp", "name": "SNMP", "documentation": "https://www.home-assistant.io/integrations/snmp", - "requirements": ["pysnmplib==5.0.10"], + "requirements": ["pysnmp==4.4.12"], "codeowners": [], "iot_class": "local_polling", "loggers": ["pyasn1", "pysmi", "pysnmp"] diff --git a/requirements_all.txt b/requirements_all.txt index a7cef9f1b43..b851ba6b9d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -436,7 +436,7 @@ bravia-tv==1.0.11 broadlink==0.18.1 # homeassistant.components.brother -brother==1.2.0 +brother==1.1.0 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 @@ -1829,7 +1829,7 @@ pysmarty==0.8 pysml==0.0.7 # homeassistant.components.snmp -pysnmplib==5.0.10 +pysnmp==4.4.12 # homeassistant.components.soma pysoma==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb2be965ee7..192a5db2d40 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -327,7 +327,7 @@ bravia-tv==1.0.11 broadlink==0.18.1 # homeassistant.components.brother -brother==1.2.0 +brother==1.1.0 # homeassistant.components.brunt brunt==1.2.0 From a0d1c5d1e6eb8a2f27add6bcfac7483a976b7909 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 19:01:56 +0200 Subject: [PATCH 29/35] Suppress Upnp error in SamsungTV resubscribe (#71925) * Suppress Upnp error in SamsungTV resubscribe * Supress UpnpCommunicationError instead * Log resubscribe errors * Add tests * Add exc_info --- .../components/samsungtv/media_player.py | 5 +- .../components/samsungtv/test_media_player.py | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index a0b70af2db5..e064b016dc6 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -12,6 +12,7 @@ from async_upnp_client.client import UpnpDevice, UpnpService, UpnpStateVariable from async_upnp_client.client_factory import UpnpFactory from async_upnp_client.exceptions import ( UpnpActionResponseError, + UpnpCommunicationError, UpnpConnectionError, UpnpError, UpnpResponseError, @@ -307,8 +308,10 @@ class SamsungTVDevice(MediaPlayerEntity): async def _async_resubscribe_dmr(self) -> None: assert self._dmr_device - with contextlib.suppress(UpnpConnectionError): + try: await self._dmr_device.async_subscribe_services(auto_resubscribe=True) + except UpnpCommunicationError as err: + LOGGER.debug("Device rejected re-subscription: %r", err, exc_info=True) async def _async_shutdown_dmr(self) -> None: """Handle removal.""" diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index e8407a86a3e..1686b8d6a95 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -6,6 +6,8 @@ from unittest.mock import DEFAULT as DEFAULT_MOCK, AsyncMock, Mock, call, patch from async_upnp_client.exceptions import ( UpnpActionResponseError, + UpnpCommunicationError, + UpnpConnectionError, UpnpError, UpnpResponseError, ) @@ -1505,3 +1507,49 @@ async def test_upnp_re_subscribe_events( assert state.state == STATE_ON assert dmr_device.async_subscribe_services.call_count == 2 assert dmr_device.async_unsubscribe_services.call_count == 1 + + +@pytest.mark.usefixtures("rest_api", "upnp_notify_server") +@pytest.mark.parametrize( + "error", + {UpnpConnectionError(), UpnpCommunicationError(), UpnpResponseError(status=400)}, +) +async def test_upnp_failed_re_subscribe_events( + hass: HomeAssistant, + remotews: Mock, + dmr_device: Mock, + mock_now: datetime, + caplog: pytest.LogCaptureFixture, + error: Exception, +) -> None: + """Test for Upnp event feedback.""" + await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + assert dmr_device.async_subscribe_services.call_count == 1 + assert dmr_device.async_unsubscribe_services.call_count == 0 + + with patch.object( + remotews, "start_listening", side_effect=WebSocketException("Boom") + ), patch.object(remotews, "is_alive", return_value=False): + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + assert dmr_device.async_subscribe_services.call_count == 1 + assert dmr_device.async_unsubscribe_services.call_count == 1 + + next_update = mock_now + timedelta(minutes=10) + with patch("homeassistant.util.dt.utcnow", return_value=next_update), patch.object( + dmr_device, "async_subscribe_services", side_effect=error + ): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + assert "Device rejected re-subscription" in caplog.text From 6155a642226d5e40f79b59d00de199678af1ca02 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 16 May 2022 12:50:03 +0200 Subject: [PATCH 30/35] Properly handle Shelly gen2 device disconnect (#71937) --- homeassistant/components/shelly/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index d29584c4e83..41a9e68fbdd 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -812,7 +812,7 @@ class RpcPollingWrapper(update_coordinator.DataUpdateCoordinator): LOGGER.debug("Polling Shelly RPC Device - %s", self.name) async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): await self.device.update_status() - except OSError as err: + except (OSError, aioshelly.exceptions.RPCTimeout) as err: raise update_coordinator.UpdateFailed("Device disconnected") from err @property From d34d3baa07ab7a17a5644e049a3075e87f50872e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 May 2022 12:28:04 -0500 Subject: [PATCH 31/35] Include initial state in history_stats count (#71952) --- .../components/history_stats/data.py | 14 +++++----- .../components/history_stats/sensor.py | 2 +- tests/components/history_stats/test_sensor.py | 26 +++++++++---------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 3d21cca6b6d..8153557422d 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -19,7 +19,7 @@ class HistoryStatsState: """The current stats of the history stats.""" hours_matched: float | None - changes_to_match_state: int | None + match_count: int | None period: tuple[datetime.datetime, datetime.datetime] @@ -121,14 +121,12 @@ class HistoryStats: self._state = HistoryStatsState(None, None, self._period) return self._state - hours_matched, changes_to_match_state = self._async_compute_hours_and_changes( + hours_matched, match_count = self._async_compute_hours_and_changes( now_timestamp, current_period_start_timestamp, current_period_end_timestamp, ) - self._state = HistoryStatsState( - hours_matched, changes_to_match_state, self._period - ) + self._state = HistoryStatsState(hours_matched, match_count, self._period) return self._state def _update_from_database( @@ -156,7 +154,7 @@ class HistoryStats: ) last_state_change_timestamp = start_timestamp elapsed = 0.0 - changes_to_match_state = 0 + match_count = 1 if previous_state_matches else 0 # Make calculations for item in self._history_current_period: @@ -166,7 +164,7 @@ class HistoryStats: if previous_state_matches: elapsed += state_change_timestamp - last_state_change_timestamp elif current_state_matches: - changes_to_match_state += 1 + match_count += 1 previous_state_matches = current_state_matches last_state_change_timestamp = state_change_timestamp @@ -178,4 +176,4 @@ class HistoryStats: # Save value in hours hours_matched = elapsed / 3600 - return hours_matched, changes_to_match_state + return hours_matched, match_count diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index b3e64106d9f..b0ce1a8fca5 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -166,4 +166,4 @@ class HistoryStatsSensor(HistoryStatsSensorBase): elif self._type == CONF_TYPE_RATIO: self._attr_native_value = pretty_ratio(state.hours_matched, state.period) elif self._type == CONF_TYPE_COUNT: - self._attr_native_value = state.changes_to_match_state + self._attr_native_value = state.match_count diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 4f56edaa291..b375a8f63c4 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -438,7 +438,7 @@ async def test_measure(hass, recorder_mock): assert hass.states.get("sensor.sensor1").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 == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -519,7 +519,7 @@ async def test_async_on_entire_period(hass, recorder_mock): assert hass.states.get("sensor.on_sensor1").state == "1.0" assert hass.states.get("sensor.on_sensor2").state == "1.0" - assert hass.states.get("sensor.on_sensor3").state == "0" + assert hass.states.get("sensor.on_sensor3").state == "1" assert hass.states.get("sensor.on_sensor4").state == "100.0" @@ -886,7 +886,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "0.0" assert hass.states.get("sensor.sensor2").state == "0.0" - assert hass.states.get("sensor.sensor3").state == "0" + assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "0.0" one_hour_in = start_time + timedelta(minutes=60) @@ -896,7 +896,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.0" assert hass.states.get("sensor.sensor2").state == "1.0" - assert hass.states.get("sensor.sensor3").state == "0" + assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "50.0" turn_off_time = start_time + timedelta(minutes=90) @@ -908,7 +908,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.5" assert hass.states.get("sensor.sensor2").state == "1.5" - assert hass.states.get("sensor.sensor3").state == "0" + assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "75.0" turn_back_on_time = start_time + timedelta(minutes=105) @@ -918,7 +918,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.5" assert hass.states.get("sensor.sensor2").state == "1.5" - assert hass.states.get("sensor.sensor3").state == "0" + assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "75.0" with freeze_time(turn_back_on_time): @@ -927,7 +927,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.5" assert hass.states.get("sensor.sensor2").state == "1.5" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "75.0" end_time = start_time + timedelta(minutes=120) @@ -937,7 +937,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.75" assert hass.states.get("sensor.sensor2").state == "1.75" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "87.5" @@ -1198,7 +1198,7 @@ async def test_measure_sliding_window(hass, recorder_mock): assert hass.states.get("sensor.sensor1").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 == "2" assert hass.states.get("sensor.sensor4").state == "41.7" past_next_update = start_time + timedelta(minutes=30) @@ -1211,7 +1211,7 @@ async def test_measure_sliding_window(hass, recorder_mock): assert hass.states.get("sensor.sensor1").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 == "2" assert hass.states.get("sensor.sensor4").state == "41.7" @@ -1291,7 +1291,7 @@ async def test_measure_from_end_going_backwards(hass, recorder_mock): assert hass.states.get("sensor.sensor1").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 == "2" assert hass.states.get("sensor.sensor4").state == "83.3" past_next_update = start_time + timedelta(minutes=30) @@ -1304,7 +1304,7 @@ async def test_measure_from_end_going_backwards(hass, recorder_mock): assert hass.states.get("sensor.sensor1").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 == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -1385,7 +1385,7 @@ async def test_measure_cet(hass, recorder_mock): assert hass.states.get("sensor.sensor1").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 == "2" assert hass.states.get("sensor.sensor4").state == "83.3" From 6b0c7a2dd4cd52837cdf27f5b7aa80e673850589 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 18 May 2022 10:00:46 +0300 Subject: [PATCH 32/35] Fix filesize doing IO in event loop (#72038) --- homeassistant/components/filesize/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 22b8cd60d79..52bb869827a 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -132,7 +132,7 @@ class FileSizeCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict[str, float | int | datetime]: """Fetch file information.""" try: - statinfo = os.stat(self._path) + statinfo = await self.hass.async_add_executor_job(os.stat, self._path) except OSError as error: raise UpdateFailed(f"Can not retrieve file statistics {error}") from error From a1df9c33aabc37e280b8241baf0db2b1939035e0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 09:36:20 +0200 Subject: [PATCH 33/35] Ignore UpnpXmlContentError in SamsungTV (#72056) --- homeassistant/components/samsungtv/media_player.py | 6 ++++-- tests/components/samsungtv/test_media_player.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index e064b016dc6..423a778011d 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -16,6 +16,7 @@ from async_upnp_client.exceptions import ( UpnpConnectionError, UpnpError, UpnpResponseError, + UpnpXmlContentError, ) from async_upnp_client.profiles.dlna import DmrDevice from async_upnp_client.utils import async_get_local_ip @@ -271,11 +272,12 @@ class SamsungTVDevice(MediaPlayerEntity): # NETWORK,NONE upnp_factory = UpnpFactory(upnp_requester, non_strict=True) upnp_device: UpnpDevice | None = None - with contextlib.suppress(UpnpConnectionError, UpnpResponseError): + try: upnp_device = await upnp_factory.async_create_device( self._ssdp_rendering_control_location ) - if not upnp_device: + except (UpnpConnectionError, UpnpResponseError, UpnpXmlContentError) as err: + LOGGER.debug("Unable to create Upnp DMR device: %r", err, exc_info=True) return _, event_ip = await async_get_local_ip( self._ssdp_rendering_control_location, self.hass.loop diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 1686b8d6a95..e548822f1d0 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -1370,6 +1370,7 @@ async def test_upnp_not_available( ) -> None: """Test for volume control when Upnp is not available.""" await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + assert "Unable to create Upnp DMR device" in caplog.text # Upnp action fails assert await hass.services.async_call( @@ -1387,6 +1388,7 @@ async def test_upnp_missing_service( ) -> None: """Test for volume control when Upnp is not available.""" await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + assert "Unable to create Upnp DMR device" in caplog.text # Upnp action fails assert await hass.services.async_call( From 996633553be1b472c73dcb4de0c27d989718fbf7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 20:04:42 +0200 Subject: [PATCH 34/35] Cleanup unused import in SamsungTV (#72102) --- homeassistant/components/samsungtv/media_player.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 423a778011d..6a884c59a87 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from collections.abc import Coroutine, Sequence -import contextlib from datetime import datetime, timedelta from typing import Any From 1b107f6845833592d9b1214ec27e8eb4997828ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 May 2022 12:14:42 -0700 Subject: [PATCH 35/35] Bumped version to 2022.5.5 --- 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 5eb59e819da..e41189fda07 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 = "4" +PATCH_VERSION: Final = "5" __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 b492bd0a240..ab8af3d941b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.5.4 +version = 2022.5.5 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0