From adcb0260e0c3cdbcf2ba6662e61a0af0290a6eca Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Dec 2020 22:07:58 +0100 Subject: [PATCH 01/90] Bumped version to 1.0.0b0 --- homeassistant/const.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 469c0fa7fbb..0e779d06c0d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" -MAJOR_VERSION = 0 -MINOR_VERSION = 119 -PATCH_VERSION = "0.dev0" +MAJOR_VERSION = 1 +MINOR_VERSION = 0 +PATCH_VERSION = "0b0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 57687f9d5697838cb956d8cd4a520a706b8b600d Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sat, 28 Nov 2020 09:29:16 +0100 Subject: [PATCH 02/90] Bump pyatmo to v4.2.1 (#43713) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 824b835b01a..aecd119454a 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==4.2.0" + "pyatmo==4.2.1" ], "after_dependencies": [ "cloud", diff --git a/requirements_all.txt b/requirements_all.txt index 937d1671d5f..ef867a64659 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1274,7 +1274,7 @@ pyarlo==0.2.3 pyatag==0.3.4.4 # homeassistant.components.netatmo -pyatmo==4.2.0 +pyatmo==4.2.1 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49f421ba3ba..e23fafb8f17 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -640,7 +640,7 @@ pyarlo==0.2.3 pyatag==0.3.4.4 # homeassistant.components.netatmo -pyatmo==4.2.0 +pyatmo==4.2.1 # homeassistant.components.blackbird pyblackbird==0.5 From b10b80d7b19773cb0c7ccdcf260e6a5f5a847725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 2 Dec 2020 14:34:17 +0100 Subject: [PATCH 03/90] Increase timeout for snapshot upload (#43851) --- homeassistant/components/hassio/http.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 2c1445dd456..2aa05ae6ab4 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -77,6 +77,7 @@ class HassIOView(HomeAssistantView): This method is a coroutine. """ read_timeout = _get_timeout(path) + client_timeout = 10 data = None headers = _init_header(request) if path == "snapshots/new/upload": @@ -89,9 +90,10 @@ class HassIOView(HomeAssistantView): request._client_max_size = ( # pylint: disable=protected-access MAX_UPLOAD_SIZE ) + client_timeout = 300 try: - with async_timeout.timeout(10): + with async_timeout.timeout(client_timeout): data = await request.read() method = getattr(self._websession, request.method.lower()) From 679d1c2b67d00fbb92183404bae5cda704412fc9 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Wed, 2 Dec 2020 22:03:31 +0100 Subject: [PATCH 04/90] Implement new Google TTS API via dedicated library (#43863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- .../components/google_translate/manifest.json | 2 +- .../components/google_translate/tts.py | 176 ++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/google_translate/test_tts.py | 301 +++++++----------- 5 files changed, 175 insertions(+), 308 deletions(-) diff --git a/homeassistant/components/google_translate/manifest.json b/homeassistant/components/google_translate/manifest.json index 6d40b2f7a09..c5b3edc8798 100644 --- a/homeassistant/components/google_translate/manifest.json +++ b/homeassistant/components/google_translate/manifest.json @@ -2,6 +2,6 @@ "domain": "google_translate", "name": "Google Translate Text-to-Speech", "documentation": "https://www.home-assistant.io/integrations/google_translate", - "requirements": ["gTTS-token==1.1.4"], + "requirements": ["gTTS==2.2.1"], "codeowners": [] } diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py index 66c00008046..c9a5eef2c83 100644 --- a/homeassistant/components/google_translate/tts.py +++ b/homeassistant/components/google_translate/tts.py @@ -1,78 +1,95 @@ """Support for the Google speech service.""" -import asyncio +from io import BytesIO import logging -import re -import aiohttp -from aiohttp.hdrs import REFERER, USER_AGENT -import async_timeout -from gtts_token import gtts_token +from gtts import gTTS, gTTSError import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider -from homeassistant.const import HTTP_OK -from homeassistant.helpers.aiohttp_client import async_get_clientsession _LOGGER = logging.getLogger(__name__) -GOOGLE_SPEECH_URL = "https://translate.google.com/translate_tts" -MESSAGE_SIZE = 148 - SUPPORT_LANGUAGES = [ "af", - "sq", "ar", - "hy", "bn", + "bs", "ca", - "zh", - "zh-cn", - "zh-tw", - "zh-yue", - "hr", "cs", + "cy", "da", - "nl", - "en", - "en-au", - "en-uk", - "en-us", - "eo", - "fi", - "fr", "de", "el", + "en", + "eo", + "es", + "et", + "fi", + "fr", + "gu", "hi", + "hr", "hu", - "is", + "hy", "id", + "is", "it", "ja", + "jw", + "km", + "kn", "ko", "la", "lv", "mk", + "ml", + "mr", + "my", + "ne", + "nl", "no", "pl", "pt", - "pt-br", "ro", "ru", - "sr", + "si", "sk", - "es", - "es-es", - "es-mx", - "es-us", - "sw", + "sq", + "sr", + "su", "sv", + "sw", "ta", + "te", "th", + "tl", "tr", - "vi", - "cy", "uk", - "bg-BG", + "ur", + "vi", + # dialects + "zh-CN", + "zh-cn", + "zh-tw", + "en-us", + "en-ca", + "en-uk", + "en-gb", + "en-au", + "en-gh", + "en-in", + "en-ie", + "en-nz", + "en-ng", + "en-ph", + "en-za", + "en-tz", + "fr-ca", + "fr-fr", + "pt-br", + "pt-pt", + "es-es", + "es-us", ] DEFAULT_LANG = "en" @@ -94,14 +111,6 @@ class GoogleProvider(Provider): """Init Google TTS service.""" self.hass = hass self._lang = lang - self.headers = { - REFERER: "http://translate.google.com/", - USER_AGENT: ( - "Mozilla/5.0 (Windows NT 10.0; WOW64) " - "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/47.0.2526.106 Safari/537.36" - ), - } self.name = "Google" @property @@ -114,74 +123,15 @@ class GoogleProvider(Provider): """Return list of supported languages.""" return SUPPORT_LANGUAGES - async def async_get_tts_audio(self, message, language, options=None): + def get_tts_audio(self, message, language, options=None): """Load TTS from google.""" + tts = gTTS(text=message, lang=language) + mp3_data = BytesIO() - token = gtts_token.Token() - websession = async_get_clientsession(self.hass) - message_parts = self._split_message_to_parts(message) + try: + tts.write_to_fp(mp3_data) + except gTTSError as exc: + _LOGGER.exception("Error during processing of TTS request %s", exc) + return None, None - data = b"" - for idx, part in enumerate(message_parts): - try: - part_token = await self.hass.async_add_executor_job( - token.calculate_token, part - ) - except ValueError as err: - # If token seed fetching fails. - _LOGGER.warning(err) - return None, None - - url_param = { - "ie": "UTF-8", - "tl": language, - "q": part, - "tk": part_token, - "total": len(message_parts), - "idx": idx, - "client": "tw-ob", - "textlen": len(part), - } - - try: - with async_timeout.timeout(10): - request = await websession.get( - GOOGLE_SPEECH_URL, params=url_param, headers=self.headers - ) - - if request.status != HTTP_OK: - _LOGGER.error( - "Error %d on load URL %s", request.status, request.url - ) - return None, None - data += await request.read() - - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Timeout for google speech") - return None, None - - return "mp3", data - - @staticmethod - def _split_message_to_parts(message): - """Split message into single parts.""" - if len(message) <= MESSAGE_SIZE: - return [message] - - punc = "!()[]?.,;:" - punc_list = [re.escape(c) for c in punc] - pattern = "|".join(punc_list) - parts = re.split(pattern, message) - - def split_by_space(fullstring): - """Split a string by space.""" - if len(fullstring) > MESSAGE_SIZE: - idx = fullstring.rfind(" ", 0, MESSAGE_SIZE) - return [fullstring[:idx]] + split_by_space(fullstring[idx:]) - return [fullstring] - - msg_parts = [] - for part in parts: - msg_parts += split_by_space(part) - - return [msg for msg in msg_parts if len(msg) > 0] + return "mp3", mp3_data.getvalue() diff --git a/requirements_all.txt b/requirements_all.txt index ef867a64659..82b0ff13c5e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -625,7 +625,7 @@ freesms==0.1.2 fritzconnection==1.3.4 # homeassistant.components.google_translate -gTTS-token==1.1.4 +gTTS==2.2.1 # homeassistant.components.garmin_connect garminconnect==0.1.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e23fafb8f17..79b5a6ec707 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -308,7 +308,7 @@ fnvhash==0.1.0 foobot_async==0.3.2 # homeassistant.components.google_translate -gTTS-token==1.1.4 +gTTS==2.2.1 # homeassistant.components.garmin_connect garminconnect==0.1.16 diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index 8e9ec9b7e1c..79c303fd2ff 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -1,8 +1,10 @@ """The tests for the Google speech platform.""" -import asyncio import os import shutil +from gtts import gTTSError +import pytest + from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP, @@ -10,226 +12,141 @@ from homeassistant.components.media_player.const import ( ) import homeassistant.components.tts as tts from homeassistant.config import async_process_ha_core_config -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component from tests.async_mock import patch -from tests.common import assert_setup_component, get_test_home_assistant, mock_service +from tests.common import async_mock_service from tests.components.tts.test_init import mutagen_mock # noqa: F401 -class TestTTSGooglePlatform: - """Test the Google speech component.""" +@pytest.fixture(autouse=True) +def cleanup_cache(hass): + """Clean up TTS cache.""" + yield + default_tts = hass.config.path(tts.DEFAULT_CACHE_DIR) + if os.path.isdir(default_tts): + shutil.rmtree(default_tts) - def setup_method(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - asyncio.run_coroutine_threadsafe( - async_process_ha_core_config( - self.hass, {"internal_url": "http://example.local:8123"} - ), - self.hass.loop, - ) +@pytest.fixture +async def calls(hass): + """Mock media player calls.""" + return async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - self.url = "https://translate.google.com/translate_tts" - self.url_param = { - "tl": "en", - "q": "90%25%20of%20I%20person%20is%20on%20front%20of%20your%20door.", - "tk": 5, - "client": "tw-ob", - "textlen": 41, - "total": 1, - "idx": 0, - "ie": "UTF-8", - } - def teardown_method(self): - """Stop everything that was started.""" - default_tts = self.hass.config.path(tts.DEFAULT_CACHE_DIR) - if os.path.isdir(default_tts): - shutil.rmtree(default_tts) +@pytest.fixture(autouse=True) +async def setup_internal_url(hass): + """Set up internal url.""" + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"} + ) - self.hass.stop() - def test_setup_component(self): - """Test setup component.""" - config = {tts.DOMAIN: {"platform": "google_translate"}} +@pytest.fixture +def mock_gtts(): + """Mock gtts.""" + with patch("homeassistant.components.google_translate.tts.gTTS") as mock_gtts: + yield mock_gtts - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - @patch("gtts_token.gtts_token.Token.calculate_token", autospec=True, return_value=5) - def test_service_say(self, mock_calculate, aioclient_mock): - """Test service call say.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) +async def test_service_say(hass, mock_gtts, calls): + """Test service call say.""" - aioclient_mock.get(self.url, params=self.url_param, status=200, content=b"test") + await async_setup_component( + hass, tts.DOMAIN, {tts.DOMAIN: {"platform": "google_translate"}} + ) - config = {tts.DOMAIN: {"platform": "google_translate"}} + await hass.services.async_call( + tts.DOMAIN, + "google_translate_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + }, + blocking=True, + ) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + assert len(calls) == 1 + assert len(mock_gtts.mock_calls) == 2 + assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".mp3") != -1 - self.hass.services.call( - tts.DOMAIN, - "google_translate_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "90% of I person is on front of your door.", - }, - ) - self.hass.block_till_done() + assert mock_gtts.mock_calls[0][2] == { + "text": "There is a person at the front door.", + "lang": "en", + } - assert len(calls) == 1 - assert len(aioclient_mock.mock_calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".mp3") != -1 - @patch("gtts_token.gtts_token.Token.calculate_token", autospec=True, return_value=5) - def test_service_say_german_config(self, mock_calculate, aioclient_mock): - """Test service call say with german code in the config.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) +async def test_service_say_german_config(hass, mock_gtts, calls): + """Test service call say with german code in the config.""" - self.url_param["tl"] = "de" - aioclient_mock.get(self.url, params=self.url_param, status=200, content=b"test") + await async_setup_component( + hass, + tts.DOMAIN, + {tts.DOMAIN: {"platform": "google_translate", "language": "de"}}, + ) - config = {tts.DOMAIN: {"platform": "google_translate", "language": "de"}} + await hass.services.async_call( + tts.DOMAIN, + "google_translate_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + }, + blocking=True, + ) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + assert len(calls) == 1 + assert len(mock_gtts.mock_calls) == 2 + assert mock_gtts.mock_calls[0][2] == { + "text": "There is a person at the front door.", + "lang": "de", + } - self.hass.services.call( - tts.DOMAIN, - "google_translate_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "90% of I person is on front of your door.", - }, - ) - self.hass.block_till_done() - assert len(calls) == 1 - assert len(aioclient_mock.mock_calls) == 1 +async def test_service_say_german_service(hass, mock_gtts, calls): + """Test service call say with german code in the service.""" - @patch("gtts_token.gtts_token.Token.calculate_token", autospec=True, return_value=5) - def test_service_say_german_service(self, mock_calculate, aioclient_mock): - """Test service call say with german code in the service.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + config = { + tts.DOMAIN: {"platform": "google_translate", "service_name": "google_say"} + } - self.url_param["tl"] = "de" - aioclient_mock.get(self.url, params=self.url_param, status=200, content=b"test") + await async_setup_component(hass, tts.DOMAIN, config) - config = { - tts.DOMAIN: {"platform": "google_translate", "service_name": "google_say"} - } + await hass.services.async_call( + tts.DOMAIN, + "google_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + tts.ATTR_LANGUAGE: "de", + }, + blocking=True, + ) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + assert len(calls) == 1 + assert len(mock_gtts.mock_calls) == 2 + assert mock_gtts.mock_calls[0][2] == { + "text": "There is a person at the front door.", + "lang": "de", + } - self.hass.services.call( - tts.DOMAIN, - "google_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "90% of I person is on front of your door.", - tts.ATTR_LANGUAGE: "de", - }, - ) - self.hass.block_till_done() - assert len(calls) == 1 - assert len(aioclient_mock.mock_calls) == 1 +async def test_service_say_error(hass, mock_gtts, calls): + """Test service call say with http response 400.""" + mock_gtts.return_value.write_to_fp.side_effect = gTTSError + await async_setup_component( + hass, tts.DOMAIN, {tts.DOMAIN: {"platform": "google_translate"}} + ) - @patch("gtts_token.gtts_token.Token.calculate_token", autospec=True, return_value=5) - def test_service_say_error(self, mock_calculate, aioclient_mock): - """Test service call say with http response 400.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + await hass.services.async_call( + tts.DOMAIN, + "google_translate_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is a person at the front door.", + }, + blocking=True, + ) - aioclient_mock.get(self.url, params=self.url_param, status=400, content=b"test") - - config = {tts.DOMAIN: {"platform": "google_translate"}} - - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - - self.hass.services.call( - tts.DOMAIN, - "google_translate_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "90% of I person is on front of your door.", - }, - ) - self.hass.block_till_done() - - assert len(calls) == 0 - assert len(aioclient_mock.mock_calls) == 1 - - @patch("gtts_token.gtts_token.Token.calculate_token", autospec=True, return_value=5) - def test_service_say_timeout(self, mock_calculate, aioclient_mock): - """Test service call say with http timeout.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - - aioclient_mock.get(self.url, params=self.url_param, exc=asyncio.TimeoutError()) - - config = {tts.DOMAIN: {"platform": "google_translate"}} - - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - - self.hass.services.call( - tts.DOMAIN, - "google_translate_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "90% of I person is on front of your door.", - }, - ) - self.hass.block_till_done() - - assert len(calls) == 0 - assert len(aioclient_mock.mock_calls) == 1 - - @patch("gtts_token.gtts_token.Token.calculate_token", autospec=True, return_value=5) - def test_service_say_long_size(self, mock_calculate, aioclient_mock): - """Test service call say with a lot of text.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - - self.url_param["total"] = 9 - self.url_param["q"] = "I%20person%20is%20on%20front%20of%20your%20door" - self.url_param["textlen"] = 33 - for idx in range(9): - self.url_param["idx"] = idx - aioclient_mock.get( - self.url, params=self.url_param, status=200, content=b"test" - ) - - config = { - tts.DOMAIN: {"platform": "google_translate", "service_name": "google_say"} - } - - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - - self.hass.services.call( - tts.DOMAIN, - "google_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: ( - "I person is on front of your door." - "I person is on front of your door." - "I person is on front of your door." - "I person is on front of your door." - "I person is on front of your door." - "I person is on front of your door." - "I person is on front of your door." - "I person is on front of your door." - "I person is on front of your door." - ), - }, - ) - self.hass.block_till_done() - - assert len(calls) == 1 - assert len(aioclient_mock.mock_calls) == 9 - assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".mp3") != -1 + assert len(calls) == 0 + assert len(mock_gtts.mock_calls) == 2 From c3fed66df6ab677e470610866486ff27f2535d64 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 5 Dec 2020 12:00:40 +0100 Subject: [PATCH 05/90] Bumped version to 0.118.5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 37b495d8c24..a1834fc4fe8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 118 -PATCH_VERSION = "4" +PATCH_VERSION = "5" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From e93b9db55d1d7ea95149325e5cef68f0ddedb456 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 30 Nov 2020 19:38:39 +0100 Subject: [PATCH 06/90] Pin pip < 20.3 (#43771) --- .github/workflows/ci.yaml | 4 ++-- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ad6becee4e2..9320da66dd4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -48,7 +48,7 @@ jobs: run: | python -m venv venv . venv/bin/activate - pip install -U pip setuptools + pip install -U "pip<20.3" setuptools pip install -r requirements.txt -r requirements_test.txt - name: Restore pre-commit environment from cache id: cache-precommit @@ -611,7 +611,7 @@ jobs: run: | python -m venv venv . venv/bin/activate - pip install -U pip setuptools wheel + pip install -U "pip<20.3" setuptools wheel pip install -r requirements_all.txt pip install -r requirements_test.txt pip install -e . diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ad46a8a36f4..33a9466f4dc 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,7 @@ jinja2>=2.11.2 netdisco==2.8.2 paho-mqtt==1.5.1 pillow==7.2.0 -pip>=8.0.3 +pip>=8.0.3,<20.3 python-slugify==4.0.1 pytz>=2020.1 pyyaml==5.3.1 diff --git a/requirements.txt b/requirements.txt index 3a376b6e7cc..ece1877ea75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 PyJWT==1.7.1 cryptography==3.2 -pip>=8.0.3 +pip>=8.0.3,<20.3 python-slugify==4.0.1 pytz>=2020.1 pyyaml==5.3.1 diff --git a/setup.py b/setup.py index 885d6e192d6..d5d133d4a3a 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ REQUIRES = [ "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. "cryptography==3.2", - "pip>=8.0.3", + "pip>=8.0.3,<20.3", "python-slugify==4.0.1", "pytz>=2020.1", "pyyaml==5.3.1", From ec90ebe136dc93b98dad7e245af0297b8229266b Mon Sep 17 00:00:00 2001 From: Emily Mills Date: Wed, 2 Dec 2020 15:28:17 -0600 Subject: [PATCH 07/90] Add Kuler Sky Bluetooth floor lamp integration (#42372) Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 1 + homeassistant/components/kulersky/__init__.py | 44 +++ .../components/kulersky/config_flow.py | 29 ++ homeassistant/components/kulersky/const.py | 2 + homeassistant/components/kulersky/light.py | 210 ++++++++++++ .../components/kulersky/manifest.json | 12 + .../components/kulersky/strings.json | 13 + .../components/kulersky/translations/en.json | 13 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/kulersky/__init__.py | 1 + tests/components/kulersky/test_config_flow.py | 104 ++++++ tests/components/kulersky/test_light.py | 315 ++++++++++++++++++ 14 files changed, 751 insertions(+) create mode 100644 homeassistant/components/kulersky/__init__.py create mode 100644 homeassistant/components/kulersky/config_flow.py create mode 100644 homeassistant/components/kulersky/const.py create mode 100644 homeassistant/components/kulersky/light.py create mode 100644 homeassistant/components/kulersky/manifest.json create mode 100644 homeassistant/components/kulersky/strings.json create mode 100644 homeassistant/components/kulersky/translations/en.json create mode 100644 tests/components/kulersky/__init__.py create mode 100644 tests/components/kulersky/test_config_flow.py create mode 100644 tests/components/kulersky/test_light.py diff --git a/CODEOWNERS b/CODEOWNERS index fe3af4c1ee6..c6deb8e9f8f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -242,6 +242,7 @@ homeassistant/components/keyboard_remote/* @bendavid homeassistant/components/knx/* @Julius2342 @farmio @marvin-w homeassistant/components/kodi/* @OnFreund @cgtobi homeassistant/components/konnected/* @heythisisnate @kit-klein +homeassistant/components/kulersky/* @emlove homeassistant/components/lametric/* @robbiet480 homeassistant/components/launch_library/* @ludeeus homeassistant/components/lcn/* @alengwenus diff --git a/homeassistant/components/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py new file mode 100644 index 00000000000..ff984e2c0d3 --- /dev/null +++ b/homeassistant/components/kulersky/__init__.py @@ -0,0 +1,44 @@ +"""Kuler Sky lights integration.""" +import asyncio + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + +PLATFORMS = ["light"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Kuler Sky component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Kuler Sky from a config entry.""" + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/kulersky/config_flow.py b/homeassistant/components/kulersky/config_flow.py new file mode 100644 index 00000000000..2b22fcdbd31 --- /dev/null +++ b/homeassistant/components/kulersky/config_flow.py @@ -0,0 +1,29 @@ +"""Config flow for Kuler Sky.""" +import logging + +import pykulersky + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def _async_has_devices(hass) -> bool: + """Return if there are devices that can be discovered.""" + # Check if there are any devices that can be discovered in the network. + try: + devices = await hass.async_add_executor_job( + pykulersky.discover_bluetooth_devices + ) + except pykulersky.PykulerskyException as exc: + _LOGGER.error("Unable to discover nearby Kuler Sky devices: %s", exc) + return False + return len(devices) > 0 + + +config_entry_flow.register_discovery_flow( + DOMAIN, "Kuler Sky", _async_has_devices, config_entries.CONN_CLASS_UNKNOWN +) diff --git a/homeassistant/components/kulersky/const.py b/homeassistant/components/kulersky/const.py new file mode 100644 index 00000000000..ae1e7a435dc --- /dev/null +++ b/homeassistant/components/kulersky/const.py @@ -0,0 +1,2 @@ +"""Constants for the Kuler Sky integration.""" +DOMAIN = "kulersky" diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py new file mode 100644 index 00000000000..4c17d1bcba3 --- /dev/null +++ b/homeassistant/components/kulersky/light.py @@ -0,0 +1,210 @@ +"""Kuler Sky light platform.""" +import asyncio +from datetime import timedelta +import logging +from typing import Callable, List + +import pykulersky + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + ATTR_WHITE_VALUE, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_WHITE_VALUE, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import HomeAssistantType +import homeassistant.util.color as color_util + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_KULERSKY = SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE + +DISCOVERY_INTERVAL = timedelta(seconds=60) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up Kuler sky light devices.""" + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + if "devices" not in hass.data[DOMAIN]: + hass.data[DOMAIN]["devices"] = set() + if "discovery" not in hass.data[DOMAIN]: + hass.data[DOMAIN]["discovery"] = asyncio.Lock() + + async def discover(*args): + """Attempt to discover new lights.""" + # Since discovery needs to connect to all discovered bluetooth devices, and + # only rules out devices after a timeout, it can potentially take a long + # time. If there's already a discovery running, just skip this poll. + if hass.data[DOMAIN]["discovery"].locked(): + return + + async with hass.data[DOMAIN]["discovery"]: + bluetooth_devices = await hass.async_add_executor_job( + pykulersky.discover_bluetooth_devices + ) + + # Filter out already connected lights + new_devices = [ + device + for device in bluetooth_devices + if device["address"] not in hass.data[DOMAIN]["devices"] + ] + + for device in new_devices: + light = pykulersky.Light(device["address"], device["name"]) + try: + # Attempt to connect to this light and read the color. If the + # connection fails, either this is not a Kuler Sky light, or + # it's bluetooth connection is currently locked by another + # device. If the vendor's app is connected to the light when + # home assistant tries to connect, this connection will fail. + await hass.async_add_executor_job(light.connect) + await hass.async_add_executor_job(light.get_color) + except pykulersky.PykulerskyException: + continue + # The light has successfully connected + hass.data[DOMAIN]["devices"].add(device["address"]) + async_add_entities([KulerskyLight(light)], update_before_add=True) + + # Start initial discovery + hass.async_add_job(discover) + + # Perform recurring discovery of new devices + async_track_time_interval(hass, discover, DISCOVERY_INTERVAL) + + +class KulerskyLight(LightEntity): + """Representation of an Kuler Sky Light.""" + + def __init__(self, light: pykulersky.Light): + """Initialize a Kuler Sky light.""" + self._light = light + self._hs_color = None + self._brightness = None + self._white_value = None + self._available = True + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + self.async_on_remove( + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.disconnect) + ) + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + await self.hass.async_add_executor_job(self.disconnect) + + def disconnect(self, *args) -> None: + """Disconnect the underlying device.""" + self._light.disconnect() + + @property + def name(self): + """Return the display name of this light.""" + return self._light.name + + @property + def unique_id(self): + """Return the ID of this light.""" + return self._light.address + + @property + def device_info(self): + """Device info for this light.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Brightech", + } + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_KULERSKY + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._brightness + + @property + def hs_color(self): + """Return the hs color.""" + return self._hs_color + + @property + def white_value(self): + """Return the white value of this light between 0..255.""" + return self._white_value + + @property + def is_on(self): + """Return true if light is on.""" + return self._brightness > 0 or self._white_value > 0 + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + default_hs = (0, 0) if self._hs_color is None else self._hs_color + hue_sat = kwargs.get(ATTR_HS_COLOR, default_hs) + + default_brightness = 0 if self._brightness is None else self._brightness + brightness = kwargs.get(ATTR_BRIGHTNESS, default_brightness) + + default_white_value = 255 if self._white_value is None else self._white_value + white_value = kwargs.get(ATTR_WHITE_VALUE, default_white_value) + + if brightness == 0 and white_value == 0 and not kwargs: + # If the light would be off, and no additional parameters were + # passed, just turn the light on full brightness. + brightness = 255 + white_value = 255 + + rgb = color_util.color_hsv_to_RGB(*hue_sat, brightness / 255 * 100) + + self._light.set_color(*rgb, white_value) + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + self._light.set_color(0, 0, 0, 0) + + def update(self): + """Fetch new state data for this light.""" + try: + if not self._light.connected: + self._light.connect() + # pylint: disable=invalid-name + r, g, b, w = self._light.get_color() + except pykulersky.PykulerskyException as exc: + if self._available: + _LOGGER.warning("Unable to connect to %s: %s", self._light.address, exc) + self._available = False + return + if not self._available: + _LOGGER.info("Reconnected to %s", self.entity_id) + self._available = True + + hsv = color_util.color_RGB_to_hsv(r, g, b) + self._hs_color = hsv[:2] + self._brightness = int(round((hsv[2] / 100) * 255)) + self._white_value = w diff --git a/homeassistant/components/kulersky/manifest.json b/homeassistant/components/kulersky/manifest.json new file mode 100644 index 00000000000..4f445e4fc18 --- /dev/null +++ b/homeassistant/components/kulersky/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "kulersky", + "name": "Kuler Sky", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/kulersky", + "requirements": [ + "pykulersky==0.4.0" + ], + "codeowners": [ + "@emlove" + ] +} diff --git a/homeassistant/components/kulersky/strings.json b/homeassistant/components/kulersky/strings.json new file mode 100644 index 00000000000..ad8f0f41ae7 --- /dev/null +++ b/homeassistant/components/kulersky/strings.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + } +} diff --git a/homeassistant/components/kulersky/translations/en.json b/homeassistant/components/kulersky/translations/en.json new file mode 100644 index 00000000000..f05becffed3 --- /dev/null +++ b/homeassistant/components/kulersky/translations/en.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No devices found on the network", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "confirm": { + "description": "Do you want to start set up?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a8e871aa02e..833f11190b6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -108,6 +108,7 @@ FLOWS = [ "juicenet", "kodi", "konnected", + "kulersky", "life360", "lifx", "local_ip", diff --git a/requirements_all.txt b/requirements_all.txt index cb93fc3e7c9..725bc5f0c09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1475,6 +1475,9 @@ pykira==0.1.1 # homeassistant.components.kodi pykodi==0.2.1 +# homeassistant.components.kulersky +pykulersky==0.4.0 + # homeassistant.components.kwb pykwb==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df7cbdafc63..c3f8ed3c3b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -745,6 +745,9 @@ pykira==0.1.1 # homeassistant.components.kodi pykodi==0.2.1 +# homeassistant.components.kulersky +pykulersky==0.4.0 + # homeassistant.components.lastfm pylast==4.0.0 diff --git a/tests/components/kulersky/__init__.py b/tests/components/kulersky/__init__.py new file mode 100644 index 00000000000..2b723b28fbd --- /dev/null +++ b/tests/components/kulersky/__init__.py @@ -0,0 +1 @@ +"""Tests for the Kuler Sky integration.""" diff --git a/tests/components/kulersky/test_config_flow.py b/tests/components/kulersky/test_config_flow.py new file mode 100644 index 00000000000..59e3188fd7e --- /dev/null +++ b/tests/components/kulersky/test_config_flow.py @@ -0,0 +1,104 @@ +"""Test the Kuler Sky config flow.""" +import pykulersky + +from homeassistant import config_entries, setup +from homeassistant.components.kulersky.config_flow import DOMAIN + +from tests.async_mock import patch + + +async def test_flow_success(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.kulersky.config_flow.pykulersky.discover_bluetooth_devices", + return_value=[ + { + "address": "AA:BB:CC:11:22:33", + "name": "Bedroom", + } + ], + ), patch( + "homeassistant.components.kulersky.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.kulersky.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Kuler Sky" + assert result2["data"] == {} + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_flow_no_devices_found(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.kulersky.config_flow.pykulersky.discover_bluetooth_devices", + return_value=[], + ), patch( + "homeassistant.components.kulersky.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.kulersky.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_flow_exceptions_caught(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.kulersky.config_flow.pykulersky.discover_bluetooth_devices", + side_effect=pykulersky.PykulerskyException("TEST"), + ), patch( + "homeassistant.components.kulersky.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.kulersky.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py new file mode 100644 index 00000000000..1b2472d7d7f --- /dev/null +++ b/tests/components/kulersky/test_light.py @@ -0,0 +1,315 @@ +"""Test the Kuler Sky lights.""" +import asyncio + +import pykulersky +import pytest + +from homeassistant import setup +from homeassistant.components.kulersky.light import DOMAIN +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_WHITE_VALUE, + ATTR_XY_COLOR, + SCAN_INTERVAL, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_WHITE_VALUE, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +import homeassistant.util.dt as dt_util + +from tests.async_mock import MagicMock, patch +from tests.common import MockConfigEntry, async_fire_time_changed + + +@pytest.fixture +async def mock_entry(hass): + """Create a mock light entity.""" + return MockConfigEntry(domain=DOMAIN) + + +@pytest.fixture +async def mock_light(hass, mock_entry): + """Create a mock light entity.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + light = MagicMock(spec=pykulersky.Light) + light.address = "AA:BB:CC:11:22:33" + light.name = "Bedroom" + light.connected = False + with patch( + "homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices", + return_value=[ + { + "address": "AA:BB:CC:11:22:33", + "name": "Bedroom", + } + ], + ): + with patch( + "homeassistant.components.kulersky.light.pykulersky.Light" + ) as mockdevice, patch.object(light, "connect") as mock_connect, patch.object( + light, "get_color", return_value=(0, 0, 0, 0) + ): + mockdevice.return_value = light + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert mock_connect.called + light.connected = True + + yield light + + +async def test_init(hass, mock_light): + """Test platform setup.""" + state = hass.states.get("light.bedroom") + assert state.state == STATE_OFF + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS + | SUPPORT_COLOR + | SUPPORT_WHITE_VALUE, + } + + with patch.object(hass.loop, "stop"), patch.object( + mock_light, "disconnect" + ) as mock_disconnect: + await hass.async_stop() + await hass.async_block_till_done() + + assert mock_disconnect.called + + +async def test_discovery_lock(hass, mock_entry): + """Test discovery lock.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + discovery_finished = None + first_discovery_started = asyncio.Event() + + async def mock_discovery(*args): + """Block to simulate multiple discovery calls while one still running.""" + nonlocal discovery_finished + if discovery_finished: + first_discovery_started.set() + await discovery_finished.wait() + return [] + + with patch( + "homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices", + return_value=[], + ), patch( + "homeassistant.components.kulersky.light.async_track_time_interval", + ) as mock_track_time_interval: + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + with patch.object( + hass, "async_add_executor_job", side_effect=mock_discovery + ) as mock_run_discovery: + discovery_coroutine = mock_track_time_interval.call_args[0][1] + + discovery_finished = asyncio.Event() + + # Schedule multiple discoveries + hass.async_create_task(discovery_coroutine()) + hass.async_create_task(discovery_coroutine()) + hass.async_create_task(discovery_coroutine()) + + # Wait until the first discovery call is blocked + await first_discovery_started.wait() + + # Unblock the first discovery + discovery_finished.set() + + # Flush the remaining jobs + await hass.async_block_till_done() + + # The discovery method should only have been called once + mock_run_discovery.assert_called_once() + + +async def test_discovery_connection_error(hass, mock_entry): + """Test that invalid devices are skipped.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + light = MagicMock(spec=pykulersky.Light) + light.address = "AA:BB:CC:11:22:33" + light.name = "Bedroom" + light.connected = False + with patch( + "homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices", + return_value=[ + { + "address": "AA:BB:CC:11:22:33", + "name": "Bedroom", + } + ], + ): + with patch( + "homeassistant.components.kulersky.light.pykulersky.Light" + ) as mockdevice, patch.object( + light, "connect", side_effect=pykulersky.PykulerskyException + ): + mockdevice.return_value = light + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Assert entity was not added + state = hass.states.get("light.bedroom") + assert state is None + + +async def test_remove_entry(hass, mock_light, mock_entry): + """Test platform setup.""" + with patch.object(mock_light, "disconnect") as mock_disconnect: + await hass.config_entries.async_remove(mock_entry.entry_id) + + assert mock_disconnect.called + + +async def test_update_exception(hass, mock_light): + """Test platform setup.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch.object( + mock_light, "get_color", side_effect=pykulersky.PykulerskyException + ): + await hass.helpers.entity_component.async_update_entity("light.bedroom") + state = hass.states.get("light.bedroom") + assert state is not None + assert state.state == STATE_UNAVAILABLE + + +async def test_light_turn_on(hass, mock_light): + """Test KulerSkyLight turn_on.""" + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(255, 255, 255, 255) + ): + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.bedroom"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_color.assert_called_with(255, 255, 255, 255) + + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(50, 50, 50, 255) + ): + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.bedroom", ATTR_BRIGHTNESS: 50}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_color.assert_called_with(50, 50, 50, 255) + + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(50, 45, 25, 255) + ): + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.bedroom", ATTR_HS_COLOR: (50, 50)}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_set_color.assert_called_with(50, 45, 25, 255) + + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(220, 201, 110, 180) + ): + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.bedroom", ATTR_WHITE_VALUE: 180}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_color.assert_called_with(50, 45, 25, 180) + + +async def test_light_turn_off(hass, mock_light): + """Test KulerSkyLight turn_on.""" + with patch.object(mock_light, "set_color") as mock_set_color, patch.object( + mock_light, "get_color", return_value=(0, 0, 0, 0) + ): + await hass.services.async_call( + "light", + "turn_off", + {ATTR_ENTITY_ID: "light.bedroom"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_color.assert_called_with(0, 0, 0, 0) + + +async def test_light_update(hass, mock_light): + """Test KulerSkyLight update.""" + utcnow = dt_util.utcnow() + + state = hass.states.get("light.bedroom") + assert state.state == STATE_OFF + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS + | SUPPORT_COLOR + | SUPPORT_WHITE_VALUE, + } + + # Test an exception during discovery + with patch.object( + mock_light, "get_color", side_effect=pykulersky.PykulerskyException("TEST") + ): + utcnow = utcnow + SCAN_INTERVAL + async_fire_time_changed(hass, utcnow) + await hass.async_block_till_done() + + state = hass.states.get("light.bedroom") + assert state.state == STATE_UNAVAILABLE + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS + | SUPPORT_COLOR + | SUPPORT_WHITE_VALUE, + } + + with patch.object( + mock_light, + "get_color", + return_value=(80, 160, 200, 240), + ): + utcnow = utcnow + SCAN_INTERVAL + async_fire_time_changed(hass, utcnow) + await hass.async_block_till_done() + + state = hass.states.get("light.bedroom") + assert state.state == STATE_ON + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS + | SUPPORT_COLOR + | SUPPORT_WHITE_VALUE, + ATTR_BRIGHTNESS: 200, + ATTR_HS_COLOR: (200, 60), + ATTR_RGB_COLOR: (102, 203, 255), + ATTR_WHITE_VALUE: 240, + ATTR_XY_COLOR: (0.184, 0.261), + } From 15b775a4d374500f234ecbc9268dfd35825a834b Mon Sep 17 00:00:00 2001 From: tehbrd Date: Thu, 3 Dec 2020 10:58:10 +1000 Subject: [PATCH 08/90] Fix intesishome passing coroutine to HassJob (#43837) * Update climate.py Not allowed to pass coroutines to hassjob. * Update climate.py * Lint Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/components/intesishome/climate.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index 57912d7d24d..a41161c7a6e 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -374,9 +374,11 @@ class IntesisAC(ClimateEntity): reconnect_minutes, ) # Schedule reconnection - async_call_later( - self.hass, reconnect_minutes * 60, self._controller.connect() - ) + + async def try_connect(_now): + await self._controller.connect() + + async_call_later(self.hass, reconnect_minutes * 60, try_connect) if self._controller.is_connected and not self._connected: # Connection has been restored From bf4e98d1ae9accb232b02ceb1a1a28bf376a3c66 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 2 Dec 2020 20:45:08 -0700 Subject: [PATCH 09/90] Fix Slack "invalid_blocks_format" bug (#43875) * Fix Slack "invalid_blocks_format" bug * Fix optional params * Fix one more optional param * Update manifest --- CODEOWNERS | 1 + homeassistant/components/slack/manifest.json | 2 +- homeassistant/components/slack/notify.py | 27 ++++++++++++-------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c6deb8e9f8f..27614c3d49d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -404,6 +404,7 @@ homeassistant/components/simplisafe/* @bachya homeassistant/components/sinch/* @bendikrb homeassistant/components/sisyphus/* @jkeljo homeassistant/components/sky_hub/* @rogerselwyn +homeassistant/components/slack/* @bachya homeassistant/components/slide/* @ualex73 homeassistant/components/sma/* @kellerza homeassistant/components/smappee/* @bsmappee diff --git a/homeassistant/components/slack/manifest.json b/homeassistant/components/slack/manifest.json index ad45abbe3c0..e183dd455f1 100644 --- a/homeassistant/components/slack/manifest.json +++ b/homeassistant/components/slack/manifest.json @@ -3,5 +3,5 @@ "name": "Slack", "documentation": "https://www.home-assistant.io/integrations/slack", "requirements": ["slackclient==2.5.0"], - "codeowners": [] + "codeowners": ["@bachya"] } diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index 88317b31585..90caad62a58 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -198,17 +198,21 @@ class SlackNotificationService(BaseNotificationService): _LOGGER.error("Error while uploading file message: %s", err) async def _async_send_text_only_message( - self, targets, message, title, blocks, username, icon + self, + targets, + message, + title, + *, + username=None, + icon=None, + blocks=None, ): """Send a text-only message.""" - message_dict = { - "blocks": blocks, - "link_names": True, - "text": message, - "username": username, - } + message_dict = {"link_names": True, "text": message} + + if username: + message_dict["username"] = username - icon = icon or self._icon if icon: if icon.lower().startswith(("http://", "https://")): icon_type = "url" @@ -217,6 +221,9 @@ class SlackNotificationService(BaseNotificationService): message_dict[f"icon_{icon_type}"] = icon + if blocks: + message_dict["blocks"] = blocks + tasks = { target: self._client.chat_postMessage(**message_dict, channel=target) for target in targets @@ -256,15 +263,15 @@ class SlackNotificationService(BaseNotificationService): elif ATTR_BLOCKS in data: blocks = data[ATTR_BLOCKS] else: - blocks = {} + blocks = None return await self._async_send_text_only_message( targets, message, title, - blocks, username=data.get(ATTR_USERNAME, self._username), icon=data.get(ATTR_ICON, self._icon), + blocks=blocks, ) # Message Type 2: A message that uploads a remote file From 3ee1aed06f71558422b74ac79c9ccfeded58679e Mon Sep 17 00:00:00 2001 From: djtimca <60706061+djtimca@users.noreply.github.com> Date: Thu, 3 Dec 2020 21:57:35 -0500 Subject: [PATCH 10/90] Bump auroranoaa library to 0.0.2 (#43898) --- homeassistant/components/aurora/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aurora/manifest.json b/homeassistant/components/aurora/manifest.json index 20f9e82dcb0..8d7d856e50c 100644 --- a/homeassistant/components/aurora/manifest.json +++ b/homeassistant/components/aurora/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/aurora", "config_flow": true, "codeowners": ["@djtimca"], - "requirements": ["auroranoaa==0.0.1"] + "requirements": ["auroranoaa==0.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 725bc5f0c09..506aabc1a53 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -291,7 +291,7 @@ asyncpysupla==0.0.5 atenpdu==0.3.0 # homeassistant.components.aurora -auroranoaa==0.0.1 +auroranoaa==0.0.2 # homeassistant.components.aurora_abb_powerone aurorapy==0.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c3f8ed3c3b7..1c7fbdae683 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -171,7 +171,7 @@ arcam-fmj==0.5.3 async-upnp-client==0.14.13 # homeassistant.components.aurora -auroranoaa==0.0.1 +auroranoaa==0.0.2 # homeassistant.components.stream av==8.0.2 From 6c5911d37f33db863bd5e87af1888bc787299841 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Dec 2020 16:44:18 +0100 Subject: [PATCH 11/90] Blueprint: descriptions + descriptive errors (#43899) --- .../components/automation/blueprints/motion_light.yaml | 3 ++- .../automation/blueprints/notify_leaving_zone.yaml | 4 +++- homeassistant/components/blueprint/importer.py | 8 ++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/automation/blueprints/motion_light.yaml b/homeassistant/components/automation/blueprints/motion_light.yaml index c10d3691e6b..c11d22d974e 100644 --- a/homeassistant/components/automation/blueprints/motion_light.yaml +++ b/homeassistant/components/automation/blueprints/motion_light.yaml @@ -1,5 +1,6 @@ blueprint: name: Motion-activated Light + description: Turn on a light when motion is detected. domain: automation source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml input: @@ -17,7 +18,7 @@ blueprint: domain: light no_motion_wait: name: Wait time - description: Time to wait until the light should be turned off. + description: Time to leave the light on after last motion is detected. default: 120 selector: number: diff --git a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml index 9b79396f066..d3a70d773ee 100644 --- a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml +++ b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml @@ -1,5 +1,6 @@ blueprint: - name: Send notification when a person leaves a zone + name: Zone Notification + description: Send a notification to a device when a person leaves a specific zone. domain: automation source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml input: @@ -26,6 +27,7 @@ trigger: variables: zone_entity: !input zone_entity + # This is the state of the person when it's in this zone. zone_state: "{{ states[zone_entity].name }}" person_entity: !input person_entity person_name: "{{ states[person_entity].name }}" diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index bc40f76e7c2..524b04293ee 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -124,7 +124,9 @@ def _extract_blueprint_from_community_topic( break if blueprint is None: - raise HomeAssistantError("No valid blueprint found in the topic") + raise HomeAssistantError( + "No valid blueprint found in the topic. Blueprint syntax blocks need to be marked as YAML or no syntax." + ) return ImportedBlueprint( f'{post["username"]}/{topic["slug"]}', block_content, blueprint @@ -204,7 +206,9 @@ async def fetch_blueprint_from_github_gist_url( break if blueprint is None: - raise HomeAssistantError("No valid blueprint found in the gist") + raise HomeAssistantError( + "No valid blueprint found in the gist. The blueprint file needs to end with '.yaml'" + ) return ImportedBlueprint( f"{gist['owner']['login']}/{filename[:-5]}", content, blueprint From b174c1d4eb49561c7eb357bf3ce33a64d29f24ed Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 3 Dec 2020 19:40:33 +0100 Subject: [PATCH 12/90] Unsubscribe ozw stop listener on entry unload (#43900) --- homeassistant/components/ozw/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index c0d50e18abc..1f46e7a17c6 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -279,7 +279,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except asyncio.CancelledError: pass - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt_client) + ozw_data[DATA_UNSUBSCRIBE].append( + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, async_stop_mqtt_client + ) + ) ozw_data[DATA_STOP_MQTT_CLIENT] = async_stop_mqtt_client else: From eb6128b53e0a9846dc18da17b9d2d95932501e14 Mon Sep 17 00:00:00 2001 From: Emily Mills Date: Thu, 3 Dec 2020 11:08:16 -0600 Subject: [PATCH 13/90] Kulersky cleanups (#43901) --- .../components/kulersky/config_flow.py | 2 +- homeassistant/components/kulersky/light.py | 21 ++++++++++++------- tests/components/kulersky/test_light.py | 6 +++--- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/kulersky/config_flow.py b/homeassistant/components/kulersky/config_flow.py index 2b22fcdbd31..04f7719b8e6 100644 --- a/homeassistant/components/kulersky/config_flow.py +++ b/homeassistant/components/kulersky/config_flow.py @@ -25,5 +25,5 @@ async def _async_has_devices(hass) -> bool: config_entry_flow.register_discovery_flow( - DOMAIN, "Kuler Sky", _async_has_devices, config_entries.CONN_CLASS_UNKNOWN + DOMAIN, "Kuler Sky", _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL ) diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index 4c17d1bcba3..71dd4a158ca 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -33,6 +33,12 @@ DISCOVERY_INTERVAL = timedelta(seconds=60) PARALLEL_UPDATES = 0 +def check_light(light: pykulersky.Light): + """Attempt to connect to this light and read the color.""" + light.connect() + light.get_color() + + async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, @@ -69,13 +75,12 @@ async def async_setup_entry( for device in new_devices: light = pykulersky.Light(device["address"], device["name"]) try: - # Attempt to connect to this light and read the color. If the - # connection fails, either this is not a Kuler Sky light, or - # it's bluetooth connection is currently locked by another - # device. If the vendor's app is connected to the light when - # home assistant tries to connect, this connection will fail. - await hass.async_add_executor_job(light.connect) - await hass.async_add_executor_job(light.get_color) + # If the connection fails, either this is not a Kuler Sky + # light, or it's bluetooth connection is currently locked + # by another device. If the vendor's app is connected to + # the light when home assistant tries to connect, this + # connection will fail. + await hass.async_add_executor_job(check_light, light) except pykulersky.PykulerskyException: continue # The light has successfully connected @@ -83,7 +88,7 @@ async def async_setup_entry( async_add_entities([KulerskyLight(light)], update_before_add=True) # Start initial discovery - hass.async_add_job(discover) + hass.async_create_task(discover()) # Perform recurring discovery of new devices async_track_time_interval(hass, discover, DISCOVERY_INTERVAL) diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py index 1b2472d7d7f..5403f7cedde 100644 --- a/tests/components/kulersky/test_light.py +++ b/tests/components/kulersky/test_light.py @@ -56,11 +56,11 @@ async def mock_light(hass, mock_entry): ], ): with patch( - "homeassistant.components.kulersky.light.pykulersky.Light" - ) as mockdevice, patch.object(light, "connect") as mock_connect, patch.object( + "homeassistant.components.kulersky.light.pykulersky.Light", + return_value=light, + ), patch.object(light, "connect") as mock_connect, patch.object( light, "get_color", return_value=(0, 0, 0, 0) ): - mockdevice.return_value = light mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() From 6ae46268a421d1684387f411d5ac4b1868c1c62c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 3 Dec 2020 22:41:02 +0100 Subject: [PATCH 14/90] Updated frontend to 20201203.0 (#43907) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 34cd7edbf26..1bf6cdc580a 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20201202.0"], + "requirements": ["home-assistant-frontend==20201203.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3f85297fc79..65f228f5a0c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.38.0 -home-assistant-frontend==20201202.0 +home-assistant-frontend==20201203.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index 506aabc1a53..4f2c23c8b45 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201202.0 +home-assistant-frontend==20201203.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c7fbdae683..101a47f4123 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201202.0 +home-assistant-frontend==20201203.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From fb005ba3e289fdfd5e56502a612ca872f7f968df Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Dec 2020 03:16:37 +0100 Subject: [PATCH 15/90] Bump hatasmota to 0.1.4 (#43912) --- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_light.py | 205 ++++++++++++++++++ 4 files changed, 208 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index a4d6ec6036f..a4c6f77fc13 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota (beta)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.1.2"], + "requirements": ["hatasmota==0.1.4"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index 4f2c23c8b45..26cc43bc504 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ hass-nabucasa==0.38.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.1.2 +hatasmota==0.1.4 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 101a47f4123..e88443140fc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -376,7 +376,7 @@ hangups==0.4.11 hass-nabucasa==0.38.0 # homeassistant.components.tasmota -hatasmota==0.1.2 +hatasmota==0.1.4 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 627eb5198aa..f09c27da753 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -493,6 +493,191 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): assert state.state == STATE_OFF +async def test_controlling_state_via_mqtt_rgbww_hex(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 5 # 5 channel light (RGBCW) + config["so"]["17"] = 0 # Hex color in state updates + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 127.5 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"FF8000"}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 128, 0) + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"00FF800000"}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (0, 255, 128) + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("white_value") == 127.5 + # Setting white > 0 should clear the color + assert not state.attributes.get("rgb_color") + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("color_temp") == 300 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + # Setting white to 0 should clear the white_value and color_temp + assert not state.attributes.get("white_value") + assert not state.attributes.get("color_temp") + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "Cycle down" + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') + + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + +async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 5 # 5 channel light (RGBCW) + config["ty"] = 1 # Tuya device + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 127.5 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"255,128,0"}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 128, 0) + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("white_value") == 127.5 + # Setting white > 0 should clear the color + assert not state.attributes.get("rgb_color") + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("color_temp") == 300 + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + # Setting white to 0 should clear the white_value and color_temp + assert not state.attributes.get("white_value") + assert not state.attributes.get("color_temp") + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "Cycle down" + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') + + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async def test_sending_mqtt_commands_on_off(hass, mqtt_mock, setup_tasmota): """Test the sending MQTT commands.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -745,6 +930,26 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() + # Dim the light from 0->100: Speed should be capped at 40 + await common.async_turn_on(hass, "light.test", brightness=255, transition=100) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 1;NoDelay;Speed 40;NoDelay;Dimmer 100", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Dim the light from 0->0: Speed should be 1 + await common.async_turn_on(hass, "light.test", brightness=0, transition=100) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 1;NoDelay;Speed 1;NoDelay;Power1 OFF", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + # Dim the light from 0->50: Speed should be 4*2*2=16 await common.async_turn_on(hass, "light.test", brightness=128, transition=4) mqtt_mock.async_publish.assert_called_once_with( From 698ee59f1011ae6293e7d4ed8d2bbbc56ce24c6e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 4 Dec 2020 20:23:20 +0100 Subject: [PATCH 16/90] Always send ozw network key to add-on config (#43938) --- homeassistant/components/ozw/config_flow.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py index 4543bc27984..887560b154e 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -147,9 +147,10 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.network_key = user_input[CONF_NETWORK_KEY] self.usb_path = user_input[CONF_USB_PATH] - new_addon_config = {CONF_ADDON_DEVICE: self.usb_path} - if self.network_key: - new_addon_config[CONF_ADDON_NETWORK_KEY] = self.network_key + new_addon_config = { + CONF_ADDON_DEVICE: self.usb_path, + CONF_ADDON_NETWORK_KEY: self.network_key, + } if new_addon_config != self.addon_config: await self._async_set_addon_config(new_addon_config) From 46b13e20ac6cd5fccde600db1e369817a81b8718 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 4 Dec 2020 20:41:08 +0100 Subject: [PATCH 17/90] Handle stale ozw discovery flow (#43939) --- homeassistant/components/ozw/config_flow.py | 12 ++-- tests/components/ozw/test_config_flow.py | 66 +++++++++++++++++---- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py index 887560b154e..7c7c6e65dfe 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -58,17 +58,14 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(DOMAIN) self._abort_if_unique_id_configured() - addon_config = await self._async_get_addon_config() - self.usb_path = addon_config[CONF_ADDON_DEVICE] - self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "") - return await self.async_step_hassio_confirm() async def async_step_hassio_confirm(self, user_input=None): """Confirm the add-on discovery.""" if user_input is not None: - self.use_addon = True - return self._async_create_entry_from_vars() + return await self.async_step_on_supervisor( + user_input={CONF_USE_ADDON: True} + ) return self.async_show_form(step_id="hassio_confirm") @@ -107,6 +104,9 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.use_addon = True if await self._async_is_addon_running(): + addon_config = await self._async_get_addon_config() + self.usb_path = addon_config[CONF_ADDON_DEVICE] + self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "") return self._async_create_entry_from_vars() if await self._async_is_addon_installed(): diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py index 289b6c7f4cd..e86232adc65 100644 --- a/tests/components/ozw/test_config_flow.py +++ b/tests/components/ozw/test_config_flow.py @@ -159,9 +159,10 @@ async def test_not_addon(hass, supervisor): assert len(mock_setup_entry.mock_calls) == 1 -async def test_addon_running(hass, supervisor, addon_running): +async def test_addon_running(hass, supervisor, addon_running, addon_options): """Test add-on already running on Supervisor.""" - hass.config.components.add("mqtt") + addon_options["device"] = "/test" + addon_options["network_key"] = "abc123" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -182,8 +183,8 @@ async def test_addon_running(hass, supervisor, addon_running): assert result["type"] == "create_entry" assert result["title"] == TITLE assert result["data"] == { - "usb_path": None, - "network_key": None, + "usb_path": "/test", + "network_key": "abc123", "use_addon": True, "integration_created_addon": False, } @@ -193,7 +194,6 @@ async def test_addon_running(hass, supervisor, addon_running): async def test_addon_info_failure(hass, supervisor, addon_info): """Test add-on info failure.""" - hass.config.components.add("mqtt") addon_info.side_effect = HassioAPIError() await setup.async_setup_component(hass, "persistent_notification", {}) @@ -213,7 +213,6 @@ async def test_addon_installed( hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon ): """Test add-on already installed but not running on Supervisor.""" - hass.config.components.add("mqtt") await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -250,7 +249,6 @@ async def test_set_addon_config_failure( hass, supervisor, addon_installed, addon_options, set_addon_options ): """Test add-on set config failure.""" - hass.config.components.add("mqtt") set_addon_options.side_effect = HassioAPIError() await setup.async_setup_component(hass, "persistent_notification", {}) @@ -273,7 +271,6 @@ async def test_start_addon_failure( hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon ): """Test add-on start failure.""" - hass.config.components.add("mqtt") start_addon.side_effect = HassioAPIError() await setup.async_setup_component(hass, "persistent_notification", {}) @@ -302,7 +299,6 @@ async def test_addon_not_installed( start_addon, ): """Test add-on not installed.""" - hass.config.components.add("mqtt") addon_installed.return_value["version"] = None await setup.async_setup_component(hass, "persistent_notification", {}) @@ -348,7 +344,6 @@ async def test_addon_not_installed( async def test_install_addon_failure(hass, supervisor, addon_installed, install_addon): """Test add-on install failure.""" - hass.config.components.add("mqtt") addon_installed.return_value["version"] = None install_addon.side_effect = HassioAPIError() await setup.async_setup_component(hass, "persistent_notification", {}) @@ -488,3 +483,54 @@ async def test_abort_discovery_with_existing_entry( assert result["type"] == "abort" assert result["reason"] == "already_configured" + + +async def test_discovery_addon_not_running( + hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon +): + """Test discovery with add-on already installed but not running.""" + addon_options["device"] = None + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["step_id"] == "hassio_confirm" + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["step_id"] == "start_addon" + assert result["type"] == "form" + + +async def test_discovery_addon_not_installed( + hass, supervisor, addon_installed, install_addon, addon_options +): + """Test discovery with add-on not installed.""" + addon_installed.return_value["version"] = None + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["step_id"] == "hassio_confirm" + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["step_id"] == "install_addon" + assert result["type"] == "progress" + + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "form" + assert result["step_id"] == "start_addon" From bf6bd969a2fbf0a2d8a619430bc10e730e359b56 Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Sat, 5 Dec 2020 05:32:49 -0500 Subject: [PATCH 18/90] Return unique id of Blink binary sensor (#43942) --- homeassistant/components/blink/binary_sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index 1841dbbc438..f69c94f0f5e 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -44,6 +44,11 @@ class BlinkBinarySensor(BinarySensorEntity): """Return the name of the blink sensor.""" return self._name + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return self._unique_id + @property def device_class(self): """Return the class of this device.""" From 4c2dfa54da6f2863f06b193f7fe58abfbf32ffae Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 4 Dec 2020 23:04:31 +0100 Subject: [PATCH 19/90] Updated frontend to 20201204.0 (#43945) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 1bf6cdc580a..65fe745e51d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20201203.0"], + "requirements": ["home-assistant-frontend==20201204.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 65f228f5a0c..d820f5e715b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.38.0 -home-assistant-frontend==20201203.0 +home-assistant-frontend==20201204.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index 26cc43bc504..c063877e817 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201203.0 +home-assistant-frontend==20201204.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e88443140fc..31578ce6b89 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201203.0 +home-assistant-frontend==20201204.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 625f219d6bfe25326be76356cbcfdff51d0a9640 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 5 Dec 2020 11:53:43 +0100 Subject: [PATCH 20/90] Fix device refresh service can always add devices (#43950) --- homeassistant/components/deconz/gateway.py | 6 ++++-- homeassistant/components/deconz/services.py | 8 +++---- tests/components/deconz/test_binary_sensor.py | 21 ++++++++++++++++++- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index dc41bb778ec..881ea883c4c 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -121,9 +121,11 @@ class DeconzGateway: async_dispatcher_send(self.hass, self.signal_reachable, True) @callback - def async_add_device_callback(self, device_type, device=None) -> None: + def async_add_device_callback( + self, device_type, device=None, force: bool = False + ) -> None: """Handle event of new device creation in deCONZ.""" - if not self.option_allow_new_devices: + if not force and not self.option_allow_new_devices: return args = [] diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 2c286fac0a1..d524354ff0b 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -146,10 +146,10 @@ async def async_refresh_devices_service(hass, data): await gateway.api.refresh_state() gateway.ignore_state_updates = False - gateway.async_add_device_callback(NEW_GROUP) - gateway.async_add_device_callback(NEW_LIGHT) - gateway.async_add_device_callback(NEW_SCENE) - gateway.async_add_device_callback(NEW_SENSOR) + gateway.async_add_device_callback(NEW_GROUP, force=True) + gateway.async_add_device_callback(NEW_LIGHT, force=True) + gateway.async_add_device_callback(NEW_SCENE, force=True) + gateway.async_add_device_callback(NEW_SENSOR, force=True) async def async_remove_orphaned_entries_service(hass, data): diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 5038c5bf3f2..78a4f1e937d 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -10,15 +10,19 @@ from homeassistant.components.binary_sensor import ( from homeassistant.components.deconz.const import ( CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_NEW_DEVICES, + CONF_MASTER_GATEWAY, DOMAIN as DECONZ_DOMAIN, ) from homeassistant.components.deconz.gateway import get_gateway_from_config_entry +from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.async_mock import patch + SENSORS = { "1": { "id": "Presence sensor id", @@ -172,7 +176,7 @@ async def test_add_new_binary_sensor_ignored(hass): """Test that adding a new binary sensor is not allowed.""" config_entry = await setup_deconz_integration( hass, - options={CONF_ALLOW_NEW_DEVICES: False}, + options={CONF_MASTER_GATEWAY: True, CONF_ALLOW_NEW_DEVICES: False}, ) gateway = get_gateway_from_config_entry(hass, config_entry) assert len(hass.states.async_all()) == 0 @@ -188,8 +192,23 @@ async def test_add_new_binary_sensor_ignored(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 + assert not hass.states.get("binary_sensor.presence_sensor") entity_registry = await hass.helpers.entity_registry.async_get_registry() assert ( len(async_entries_for_config_entry(entity_registry, config_entry.entry_id)) == 0 ) + + with patch( + "pydeconz.DeconzSession.request", + return_value={ + "groups": {}, + "lights": {}, + "sensors": {"1": deepcopy(SENSORS["1"])}, + }, + ): + await hass.services.async_call(DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + assert hass.states.get("binary_sensor.presence_sensor") From a390b0aca883d9dfbba137c0c45f63a9151480a3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 5 Dec 2020 13:15:45 +0100 Subject: [PATCH 21/90] Bumped version to 1.0.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0e779d06c0d..a34d9d1bff0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 1 MINOR_VERSION = 0 -PATCH_VERSION = "0b0" +PATCH_VERSION = "0b1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 6cb1ce056aeb99d5e182364d835e0daa975a5304 Mon Sep 17 00:00:00 2001 From: treylok Date: Sat, 5 Dec 2020 07:13:46 -0600 Subject: [PATCH 22/90] Fix Ecobee set humidity (#43954) --- homeassistant/components/ecobee/climate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index ccfddca4b03..94396bbf883 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -24,6 +24,7 @@ from homeassistant.components.climate.const import ( SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) @@ -161,6 +162,7 @@ SUPPORT_FLAGS = ( | SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_FAN_MODE + | SUPPORT_TARGET_HUMIDITY ) @@ -651,7 +653,7 @@ class Thermostat(ClimateEntity): def set_humidity(self, humidity): """Set the humidity level.""" - self.data.ecobee.set_humidity(self.thermostat_index, humidity) + self.data.ecobee.set_humidity(self.thermostat_index, int(humidity)) def set_hvac_mode(self, hvac_mode): """Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" From 4780bdd48f226b8ee3af4b3e319a6e2f3c32f7ab Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 7 Dec 2020 10:27:33 +0200 Subject: [PATCH 23/90] Prevent firing Shelly input events at startup (#43986) Co-authored-by: Paulus Schoutsen --- homeassistant/components/shelly/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 3c34da574a6..298c7e111b2 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -158,7 +158,11 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): last_event_count = self._last_input_events_count.get(channel) self._last_input_events_count[channel] = block.inputEventCnt - if last_event_count == block.inputEventCnt or event_type == "": + if ( + last_event_count is None + or last_event_count == block.inputEventCnt + or event_type == "" + ): continue if event_type in INPUTS_EVENTS_DICT: From 591777455a785310f5a81c704fae60fd1c0b268d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 Dec 2020 17:24:32 +0100 Subject: [PATCH 24/90] Update ring to 0.6.2 (#43995) --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index d46f12af511..550da4d38ec 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,7 +2,7 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": ["ring_doorbell==0.6.0"], + "requirements": ["ring_doorbell==0.6.2"], "dependencies": ["ffmpeg"], "codeowners": ["@balloob"], "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index c063877e817..839a5bd0e39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1939,7 +1939,7 @@ rfk101py==0.0.1 rflink==0.0.55 # homeassistant.components.ring -ring_doorbell==0.6.0 +ring_doorbell==0.6.2 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31578ce6b89..1691ccdb263 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -947,7 +947,7 @@ restrictedpython==5.0 rflink==0.0.55 # homeassistant.components.ring -ring_doorbell==0.6.0 +ring_doorbell==0.6.2 # homeassistant.components.roku rokuecp==0.6.0 From c7ec7e7c9845120e5d653ad4320ce5ecb8b28cb7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 Dec 2020 09:11:57 +0000 Subject: [PATCH 25/90] Bumped version to 1.0.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a34d9d1bff0..8aa48b5178c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 1 MINOR_VERSION = 0 -PATCH_VERSION = "0b1" +PATCH_VERSION = "0b2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 154fd4aa8bdac103dcef18263a4ff54b4495747b Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 7 Dec 2020 11:20:22 +0100 Subject: [PATCH 26/90] Fix Solaredge integration in case the data is not complete (#43557) Co-authored-by: Martin Hjelmare --- homeassistant/components/solaredge/sensor.py | 29 ++++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 0cd498b4e3b..e3e59676bf5 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -425,22 +425,21 @@ class SolarEdgeEnergyDetailsService(SolarEdgeDataService): self.data = {} self.attributes = {} self.unit = energy_details["unit"] - meters = energy_details["meters"] - for entity in meters: - for key, data in entity.items(): - if key == "type" and data in [ - "Production", - "SelfConsumption", - "FeedIn", - "Purchased", - "Consumption", - ]: - energy_type = data - if key == "values": - for row in data: - self.data[energy_type] = row["value"] - self.attributes[energy_type] = {"date": row["date"]} + for meter in energy_details["meters"]: + if "type" not in meter or "values" not in meter: + continue + if meter["type"] not in [ + "Production", + "SelfConsumption", + "FeedIn", + "Purchased", + "Consumption", + ]: + continue + if len(meter["values"][0]) == 2: + self.data[meter["type"]] = meter["values"][0]["value"] + self.attributes[meter["type"]] = {"date": meter["values"][0]["date"]} _LOGGER.debug( "Updated SolarEdge energy details: %s, %s", self.data, self.attributes From a1c55374605b7ab4218c5955efa3e68b02f87a63 Mon Sep 17 00:00:00 2001 From: Nigel Rook Date: Mon, 7 Dec 2020 12:14:54 +0000 Subject: [PATCH 27/90] Update generic_thermostat current_temperature on startup (#43951) Co-authored-by: Martin Hjelmare --- .../components/generic_thermostat/climate.py | 10 +++++--- .../generic_thermostat/test_climate.py | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 4072c43bc27..175ee8f1d5b 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -33,7 +33,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import DOMAIN as HA_DOMAIN, callback +from homeassistant.core import DOMAIN as HA_DOMAIN, CoreState, callback from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( @@ -207,7 +207,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): ) @callback - def _async_startup(event): + def _async_startup(*_): """Init on startup.""" sensor_state = self.hass.states.get(self.sensor_entity_id) if sensor_state and sensor_state.state not in ( @@ -215,8 +215,12 @@ class GenericThermostat(ClimateEntity, RestoreEntity): STATE_UNKNOWN, ): self._async_update_temp(sensor_state) + self.async_write_ha_state() - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) + if self.hass.state == CoreState.running: + _async_startup() + else: + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) # Check If we have an old state old_state = await self.async_get_last_state() diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index eaf7c8e5651..71c6f41282b 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -209,6 +209,30 @@ async def test_setup_defaults_to_unknown(hass): assert HVAC_MODE_OFF == hass.states.get(ENTITY).state +async def test_setup_gets_current_temp_from_sensor(hass): + """Test that current temperature is updated on entity addition.""" + hass.config.units = METRIC_SYSTEM + _setup_sensor(hass, 18) + await hass.async_block_till_done() + await async_setup_component( + hass, + DOMAIN, + { + "climate": { + "platform": "generic_thermostat", + "name": "test", + "cold_tolerance": 2, + "hot_tolerance": 4, + "heater": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "away_temp": 16, + } + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY).attributes["current_temperature"] == 18 + + async def test_default_setup_params(hass, setup_comp_2): """Test the setup with default parameters.""" state = hass.states.get(ENTITY) From 4b8e3171a3576e09c448e6df5bf7db841e4e20d5 Mon Sep 17 00:00:00 2001 From: Alex Szlavik Date: Tue, 8 Dec 2020 05:32:48 -0500 Subject: [PATCH 28/90] Retry tuya setup on auth rate limiting (#44001) Co-authored-by: Martin Hjelmare --- homeassistant/components/tuya/__init__.py | 5 +++++ homeassistant/components/tuya/config_flow.py | 14 ++++++++++++-- homeassistant/components/tuya/manifest.json | 2 +- homeassistant/components/tuya/strings.json | 3 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 23 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index bc665baeb86..5876331ea97 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -6,6 +6,7 @@ import logging from tuyaha import TuyaApi from tuyaha.tuyaapi import ( TuyaAPIException, + TuyaAPIRateLimitException, TuyaFrequentlyInvokeException, TuyaNetException, TuyaServerException, @@ -137,6 +138,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) as exc: raise ConfigEntryNotReady() from exc + except TuyaAPIRateLimitException as exc: + _LOGGER.error("Tuya login rate limited") + raise ConfigEntryNotReady() from exc + except TuyaAPIException as exc: _LOGGER.error( "Connection error during integration setup. Error: %s", diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index e2048aaf7bf..5d22a83e03e 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -2,7 +2,12 @@ import logging from tuyaha import TuyaApi -from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException, TuyaServerException +from tuyaha.tuyaapi import ( + TuyaAPIException, + TuyaAPIRateLimitException, + TuyaNetException, + TuyaServerException, +) import voluptuous as vol from homeassistant import config_entries @@ -103,7 +108,7 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): tuya.init( self._username, self._password, self._country_code, self._platform ) - except (TuyaNetException, TuyaServerException): + except (TuyaAPIRateLimitException, TuyaNetException, TuyaServerException): return RESULT_CONN_ERROR except TuyaAPIException: return RESULT_AUTH_FAILED @@ -249,6 +254,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Handle options flow.""" + + if self.config_entry.state != config_entries.ENTRY_STATE_LOADED: + _LOGGER.error("Tuya integration not yet loaded") + return self.async_abort(reason="cannot_connect") + if user_input is not None: dev_ids = user_input.get(CONF_LIST_DEVICES) if dev_ids: diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 642a4dbe5d1..7481e56f00a 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -2,7 +2,7 @@ "domain": "tuya", "name": "Tuya", "documentation": "https://www.home-assistant.io/integrations/tuya", - "requirements": ["tuyaha==0.0.8"], + "requirements": ["tuyaha==0.0.9"], "codeowners": ["@ollo69"], "config_flow": true } diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 84575906010..444ff0b5c21 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, "step": { "init": { "title": "Configure Tuya Options", diff --git a/requirements_all.txt b/requirements_all.txt index 839a5bd0e39..4b204d1f95d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2207,7 +2207,7 @@ tp-connected==0.0.4 transmissionrpc==0.11 # homeassistant.components.tuya -tuyaha==0.0.8 +tuyaha==0.0.9 # homeassistant.components.twentemilieu twentemilieu==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1691ccdb263..0855d6fd616 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1069,7 +1069,7 @@ total_connect_client==0.55 transmissionrpc==0.11 # homeassistant.components.tuya -tuyaha==0.0.8 +tuyaha==0.0.9 # homeassistant.components.twentemilieu twentemilieu==0.3.0 From a9d69bc1b95fb4663c36028651138204c6a829aa Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 7 Dec 2020 10:01:58 -0800 Subject: [PATCH 29/90] Bump pymyq to 2.0.11 (#44003) --- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 0e3d53be081..ee3471725b6 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -2,7 +2,7 @@ "domain": "myq", "name": "MyQ", "documentation": "https://www.home-assistant.io/integrations/myq", - "requirements": ["pymyq==2.0.10"], + "requirements": ["pymyq==2.0.11"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 4b204d1f95d..d9c6096d5a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1542,7 +1542,7 @@ pymsteams==0.1.12 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==2.0.10 +pymyq==2.0.11 # homeassistant.components.mysensors pymysensors==0.18.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0855d6fd616..2edc59996f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -782,7 +782,7 @@ pymodbus==2.3.0 pymonoprice==0.3 # homeassistant.components.myq -pymyq==2.0.10 +pymyq==2.0.11 # homeassistant.components.nut pynut2==2.1.2 From e3b650b2c84a974f3c16471bebdb772d53919a83 Mon Sep 17 00:00:00 2001 From: JJdeVries <43748187+JJdeVries@users.noreply.github.com> Date: Mon, 7 Dec 2020 12:46:53 +0100 Subject: [PATCH 30/90] Fix unit of measurement for asuswrt sensors (#44009) --- homeassistant/components/asuswrt/sensor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 15ca58a525f..aa13bee81d0 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -29,7 +29,7 @@ class _SensorTypes(enum.Enum): UPLOAD_SPEED = "upload_speed" @property - def unit(self) -> Optional[str]: + def unit_of_measurement(self) -> Optional[str]: """Return a string with the unit of the sensortype.""" if self in (_SensorTypes.UPLOAD, _SensorTypes.DOWNLOAD): return DATA_GIGABYTES @@ -161,3 +161,8 @@ class AsuswrtSensor(CoordinatorEntity): def icon(self) -> Optional[str]: """Return the icon to use in the frontend.""" return self._type.icon + + @property + def unit_of_measurement(self) -> Optional[str]: + """Return the unit of measurement of this entity, if any.""" + return self._type.unit_of_measurement From ac82dac4f3d622a6eb24ac55e8b537b95554bf7d Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 7 Dec 2020 12:51:35 +0000 Subject: [PATCH 31/90] Hide HomeKit devices from discovery that are known to be problematic (#44014) --- .../components/homekit_controller/config_flow.py | 12 ++++++++++++ .../homekit_controller/test_config_flow.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 9881ef15dcb..71c8005cbc5 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -21,6 +21,14 @@ HOMEKIT_BRIDGE_DOMAIN = "homekit" HOMEKIT_BRIDGE_SERIAL_NUMBER = "homekit.bridge" HOMEKIT_BRIDGE_MODEL = "Home Assistant HomeKit Bridge" +HOMEKIT_IGNORE = [ + # eufy Indoor Cam 2K Pan & Tilt + # https://github.com/home-assistant/core/issues/42307 + "T8410", + # Hive Hub - vendor does not give user a pairing code + "HHKBridge1,1", +] + PAIRING_FILE = "pairing.json" MDNS_SUFFIX = "._hap._tcp.local." @@ -255,6 +263,10 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): # Devices in HOMEKIT_IGNORE have native local integrations - users # should be encouraged to use native integration and not confused # by alternative HK API. + if model in HOMEKIT_IGNORE: + return self.async_abort(reason="ignored_model") + + # If this is a HomeKit bridge exported by *this* HA instance ignore it. if await self._hkid_is_homekit_bridge(hkid): return self.async_abort(reason="ignored_model") diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index a8eb869abf4..72a8133159d 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -257,6 +257,21 @@ async def test_discovery_ignored_model(hass, controller): """Already paired.""" device = setup_mock_accessory(controller) discovery_info = get_device_discovery_info(device) + discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF" + discovery_info["properties"]["md"] = "HHKBridge1,1" + + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + ) + assert result["type"] == "abort" + assert result["reason"] == "ignored_model" + + +async def test_discovery_ignored_hk_bridge(hass, controller): + """Already paired.""" + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) config_entry = MockConfigEntry(domain=config_flow.HOMEKIT_BRIDGE_DOMAIN, data={}) formatted_mac = device_registry.format_mac("AA:BB:CC:DD:EE:FF") From 7a4dac81838d10af3fe596492aa9657fc40279e1 Mon Sep 17 00:00:00 2001 From: Fuzzy <16689090+FuzzyMistborn@users.noreply.github.com> Date: Tue, 8 Dec 2020 15:10:50 -0500 Subject: [PATCH 32/90] Add T8400 to ignore list (#44017) --- homeassistant/components/homekit_controller/config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 71c8005cbc5..e046a131a6b 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -22,8 +22,9 @@ HOMEKIT_BRIDGE_SERIAL_NUMBER = "homekit.bridge" HOMEKIT_BRIDGE_MODEL = "Home Assistant HomeKit Bridge" HOMEKIT_IGNORE = [ - # eufy Indoor Cam 2K Pan & Tilt + # eufy Indoor Cam 2K and 2K Pan & Tilt # https://github.com/home-assistant/core/issues/42307 + "T8400", "T8410", # Hive Hub - vendor does not give user a pairing code "HHKBridge1,1", From b09a8f0d21974972a7645bb793213a9b6a024ecd Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 8 Dec 2020 17:01:07 +0000 Subject: [PATCH 33/90] Fix how homekit_controller enumerates Hue remote (#44019) --- .../homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../specific_devices/test_hue_bridge.py | 19 +++++++++---------- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 45c493ad864..9580a7ee50d 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "requirements": [ - "aiohomekit==0.2.57" + "aiohomekit==0.2.60" ], "zeroconf": [ "_hap._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index d9c6096d5a6..acb4eae7074 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -172,7 +172,7 @@ aioguardian==1.0.4 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit==0.2.57 +aiohomekit==0.2.60 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2edc59996f9..4d1f812344b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -103,7 +103,7 @@ aioguardian==1.0.4 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit==0.2.57 +aiohomekit==0.2.60 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py index 67b7508eb94..168ae85b228 100644 --- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -51,16 +51,15 @@ async def test_hue_bridge_setup(hass): ] for button in ("button1", "button2", "button3", "button4"): - for subtype in ("single_press", "double_press", "long_press"): - expected.append( - { - "device_id": device.id, - "domain": "homekit_controller", - "platform": "device", - "type": button, - "subtype": subtype, - } - ) + expected.append( + { + "device_id": device.id, + "domain": "homekit_controller", + "platform": "device", + "type": button, + "subtype": "single_press", + } + ) triggers = await async_get_device_automations(hass, "trigger", device.id) assert_lists_same(triggers, expected) From 445fbb2c73dc7990e6318b25aabc518bac855395 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 7 Dec 2020 13:34:47 -0700 Subject: [PATCH 34/90] Bump simplisafe-python to 9.6.1 (#44030) --- 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 0f209b366e8..eeb37b46df9 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,6 +3,6 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==9.6.0"], + "requirements": ["simplisafe-python==9.6.1"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index acb4eae7074..c3725a7256a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2021,7 +2021,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.6.0 +simplisafe-python==9.6.1 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d1f812344b..f65478ad29e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -984,7 +984,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.6.0 +simplisafe-python==9.6.1 # homeassistant.components.slack slackclient==2.5.0 From ec4b23173ca35fe919156263865eb81ed50d6238 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 8 Dec 2020 22:14:55 +0000 Subject: [PATCH 35/90] Update pyarlo to 0.2.4 (#44034) --- homeassistant/components/arlo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/arlo/manifest.json b/homeassistant/components/arlo/manifest.json index 41d4fc40e5f..f046f84f94d 100644 --- a/homeassistant/components/arlo/manifest.json +++ b/homeassistant/components/arlo/manifest.json @@ -2,7 +2,7 @@ "domain": "arlo", "name": "Arlo", "documentation": "https://www.home-assistant.io/integrations/arlo", - "requirements": ["pyarlo==0.2.3"], + "requirements": ["pyarlo==0.2.4"], "dependencies": ["ffmpeg"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index c3725a7256a..b9b9074f975 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1271,7 +1271,7 @@ pyairvisual==5.0.4 pyalmond==0.0.2 # homeassistant.components.arlo -pyarlo==0.2.3 +pyarlo==0.2.4 # homeassistant.components.atag pyatag==0.3.4.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f65478ad29e..02677865bc2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -640,7 +640,7 @@ pyairvisual==5.0.4 pyalmond==0.0.2 # homeassistant.components.arlo -pyarlo==0.2.3 +pyarlo==0.2.4 # homeassistant.components.atag pyatag==0.3.4.4 From 8ad0b9845e9338664ec1d833f6730c31245b65c9 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 8 Dec 2020 08:19:57 +0100 Subject: [PATCH 36/90] Add the missing ATTR_ENABLED attribute to Brother integration list of sensors (#44036) --- homeassistant/components/brother/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index cbb3d2a70cb..5aecde16327 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -136,6 +136,7 @@ SENSOR_TYPES = { ATTR_ICON: "mdi:printer-3d", ATTR_LABEL: ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, + ATTR_ENABLED: True, }, ATTR_BLACK_TONER_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", From 70257d7a211f3de94bad86e7dba63d73233f29de Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 7 Dec 2020 20:06:32 -0500 Subject: [PATCH 37/90] Update ZHA dependencies (#44039) zha-quirks==0.0.48 zigpy==0.28.2 zigpy-znp==0.3.0 --- homeassistant/components/zha/manifest.json | 7 ++++--- requirements_all.txt | 7 ++++--- requirements_test_all.txt | 10 +++++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 780bb5bc999..f1821c9e480 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -6,13 +6,14 @@ "requirements": [ "bellows==0.21.0", "pyserial==3.4", - "zha-quirks==0.0.47", + "pyserial-asyncio==0.4", + "zha-quirks==0.0.48", "zigpy-cc==0.5.2", "zigpy-deconz==0.11.0", - "zigpy==0.28.1", + "zigpy==0.28.2", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", - "zigpy-znp==0.2.2" + "zigpy-znp==0.3.0" ], "codeowners": ["@dmulcahey", "@adminiuga"] } diff --git a/requirements_all.txt b/requirements_all.txt index b9b9074f975..8b1c7d01827 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1661,6 +1661,7 @@ pysdcp==1 pysensibo==1.0.3 # homeassistant.components.serial +# homeassistant.components.zha pyserial-asyncio==0.4 # homeassistant.components.acer_projector @@ -2344,7 +2345,7 @@ zengge==0.2 zeroconf==0.28.6 # homeassistant.components.zha -zha-quirks==0.0.47 +zha-quirks==0.0.48 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2365,10 +2366,10 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.2.2 +zigpy-znp==0.3.0 # homeassistant.components.zha -zigpy==0.28.1 +zigpy==0.28.2 # homeassistant.components.zoneminder zm-py==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02677865bc2..4f2c56e3d3f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -834,6 +834,10 @@ pyrisco==0.3.1 # homeassistant.components.ruckus_unleashed pyruckus==0.12 +# homeassistant.components.serial +# homeassistant.components.zha +pyserial-asyncio==0.4 + # homeassistant.components.acer_projector # homeassistant.components.zha pyserial==3.4 @@ -1140,7 +1144,7 @@ zeep[async]==4.0.0 zeroconf==0.28.6 # homeassistant.components.zha -zha-quirks==0.0.47 +zha-quirks==0.0.48 # homeassistant.components.zha zigpy-cc==0.5.2 @@ -1155,7 +1159,7 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.2.2 +zigpy-znp==0.3.0 # homeassistant.components.zha -zigpy==0.28.1 +zigpy==0.28.2 From 5fee55f8245e9ced7521973ae08d21499d379003 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 7 Dec 2020 19:57:19 -0700 Subject: [PATCH 38/90] Bump simplisafe-python to 9.6.2 (#44040) --- 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 eeb37b46df9..a502a7908f0 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,6 +3,6 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==9.6.1"], + "requirements": ["simplisafe-python==9.6.2"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8b1c7d01827..469e05be593 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2022,7 +2022,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.6.1 +simplisafe-python==9.6.2 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f2c56e3d3f..da4d91f22ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -988,7 +988,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.6.1 +simplisafe-python==9.6.2 # homeassistant.components.slack slackclient==2.5.0 From a2f9cbc9415ad957de43c864a2720f0f33202f33 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Dec 2020 13:06:29 +0100 Subject: [PATCH 39/90] Fix extracting entity and device IDs from scripts (#44048) * Fix extracting entity and device IDs from scripts * Fix extracting from data_template --- homeassistant/helpers/script.py | 66 ++++++++++++------- tests/components/automation/test_init.py | 3 + .../blueprint/test_websocket_api.py | 2 +- tests/helpers/test_script.py | 41 +++++++++++- .../automation/test_event_service.yaml | 1 + 5 files changed, 86 insertions(+), 27 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 645131b60b5..48a662e3a81 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -22,10 +22,10 @@ from async_timeout import timeout import voluptuous as vol from homeassistant import exceptions -import homeassistant.components.device_automation as device_automation +from homeassistant.components import device_automation, scene from homeassistant.components.logger import LOGSEVERITY -import homeassistant.components.scene as scene from homeassistant.const import ( + ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_ALIAS, CONF_CHOOSE, @@ -44,6 +44,7 @@ from homeassistant.const import ( CONF_REPEAT, CONF_SCENE, CONF_SEQUENCE, + CONF_TARGET, CONF_TIMEOUT, CONF_UNTIL, CONF_VARIABLES, @@ -60,13 +61,9 @@ from homeassistant.core import ( HomeAssistant, callback, ) -from homeassistant.helpers import condition, config_validation as cv, template +from homeassistant.helpers import condition, config_validation as cv, service, template from homeassistant.helpers.event import async_call_later, async_track_template from homeassistant.helpers.script_variables import ScriptVariables -from homeassistant.helpers.service import ( - CONF_SERVICE_DATA, - async_prepare_call_from_config, -) from homeassistant.helpers.trigger import ( async_initialize_triggers, async_validate_trigger_config, @@ -429,13 +426,13 @@ class _ScriptRun: self._script.last_action = self._action.get(CONF_ALIAS, "call service") self._log("Executing step %s", self._script.last_action) - domain, service, service_data = async_prepare_call_from_config( + domain, service_name, service_data = service.async_prepare_call_from_config( self._hass, self._action, self._variables ) running_script = ( domain == "automation" - and service == "trigger" + and service_name == "trigger" or domain in ("python_script", "script") ) # If this might start a script then disable the call timeout. @@ -448,7 +445,7 @@ class _ScriptRun: service_task = self._hass.async_create_task( self._hass.services.async_call( domain, - service, + service_name, service_data, blocking=True, context=self._context, @@ -755,6 +752,23 @@ async def _async_stop_scripts_at_shutdown(hass, event): _VarsType = Union[Dict[str, Any], MappingProxyType] +def _referenced_extract_ids(data: Dict, key: str, found: Set[str]) -> None: + """Extract referenced IDs.""" + if not data: + return + + item_ids = data.get(key) + + if item_ids is None or isinstance(item_ids, template.Template): + return + + if isinstance(item_ids, str): + item_ids = [item_ids] + + for item_id in item_ids: + found.add(item_id) + + class Script: """Representation of a script.""" @@ -889,7 +903,16 @@ class Script: for step in self.sequence: action = cv.determine_script_action(step) - if action == cv.SCRIPT_ACTION_CHECK_CONDITION: + if action == cv.SCRIPT_ACTION_CALL_SERVICE: + for data in ( + step, + step.get(CONF_TARGET), + step.get(service.CONF_SERVICE_DATA), + step.get(service.CONF_SERVICE_DATA_TEMPLATE), + ): + _referenced_extract_ids(data, ATTR_DEVICE_ID, referenced) + + elif action == cv.SCRIPT_ACTION_CHECK_CONDITION: referenced |= condition.async_extract_devices(step) elif action == cv.SCRIPT_ACTION_DEVICE_AUTOMATION: @@ -910,20 +933,13 @@ class Script: action = cv.determine_script_action(step) if action == cv.SCRIPT_ACTION_CALL_SERVICE: - data = step.get(CONF_SERVICE_DATA) - if not data: - continue - - entity_ids = data.get(ATTR_ENTITY_ID) - - if entity_ids is None or isinstance(entity_ids, template.Template): - continue - - if isinstance(entity_ids, str): - entity_ids = [entity_ids] - - for entity_id in entity_ids: - referenced.add(entity_id) + for data in ( + step, + step.get(CONF_TARGET), + step.get(service.CONF_SERVICE_DATA), + step.get(service.CONF_SERVICE_DATA_TEMPLATE), + ): + _referenced_extract_ids(data, ATTR_ENTITY_ID, referenced) elif action == cv.SCRIPT_ACTION_CHECK_CONDITION: referenced |= condition.async_extract_entities(step) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 95667d9a690..5f258fc28b7 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1254,3 +1254,6 @@ async def test_blueprint_automation(hass, calls): hass.bus.async_fire("blueprint_event") await hass.async_block_till_done() assert len(calls) == 1 + assert automation.entities_in_automation(hass, "automation.automation_0") == [ + "light.kitchen" + ] diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index c4f39127d93..bb08414b6e8 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -124,7 +124,7 @@ async def test_save_blueprint(hass, aioclient_mock, hass_ws_client): assert msg["success"] assert write_mock.mock_calls assert write_mock.call_args[0] == ( - "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n", + "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n", ) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 8f9e3cec36c..92666335f28 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1338,6 +1338,18 @@ async def test_referenced_entities(hass): "service": "test.script", "data": {"entity_id": "{{ 'light.service_template' }}"}, }, + { + "service": "test.script", + "entity_id": "light.direct_entity_referenced", + }, + { + "service": "test.script", + "target": {"entity_id": "light.entity_in_target"}, + }, + { + "service": "test.script", + "data_template": {"entity_id": "light.entity_in_data_template"}, + }, { "condition": "state", "entity_id": "sensor.condition", @@ -1357,6 +1369,9 @@ async def test_referenced_entities(hass): "light.service_list", "sensor.condition", "scene.hello", + "light.direct_entity_referenced", + "light.entity_in_target", + "light.entity_in_data_template", } # Test we cache results. assert script_obj.referenced_entities is script_obj.referenced_entities @@ -1374,12 +1389,36 @@ async def test_referenced_devices(hass): "device_id": "condition-dev-id", "domain": "switch", }, + { + "service": "test.script", + "data": {"device_id": "data-string-id"}, + }, + { + "service": "test.script", + "data_template": {"device_id": "data-template-string-id"}, + }, + { + "service": "test.script", + "target": {"device_id": "target-string-id"}, + }, + { + "service": "test.script", + "target": {"device_id": ["target-list-id-1", "target-list-id-2"]}, + }, ] ), "Test Name", "test_domain", ) - assert script_obj.referenced_devices == {"script-dev-id", "condition-dev-id"} + assert script_obj.referenced_devices == { + "script-dev-id", + "condition-dev-id", + "data-string-id", + "data-template-string-id", + "target-string-id", + "target-list-id-1", + "target-list-id-2", + } # Test we cache results. assert script_obj.referenced_devices is script_obj.referenced_devices diff --git a/tests/testing_config/blueprints/automation/test_event_service.yaml b/tests/testing_config/blueprints/automation/test_event_service.yaml index eff8b52db16..ab067b004ac 100644 --- a/tests/testing_config/blueprints/automation/test_event_service.yaml +++ b/tests/testing_config/blueprints/automation/test_event_service.yaml @@ -9,3 +9,4 @@ trigger: event_type: !input trigger_event action: service: !input service_to_call + entity_id: light.kitchen From 88941eaa5148d8ca8c0cee02d543cd666191a960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Tue, 8 Dec 2020 14:16:31 +0100 Subject: [PATCH 40/90] Bump pyatv to 0.7.5 (#44051) --- homeassistant/components/apple_tv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index ccd5da49547..21b2df308d3 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", "requirements": [ - "pyatv==0.7.3" + "pyatv==0.7.5" ], "zeroconf": [ "_mediaremotetv._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 469e05be593..e2d17cfe39a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1283,7 +1283,7 @@ pyatmo==4.2.1 pyatome==0.1.1 # homeassistant.components.apple_tv -pyatv==0.7.3 +pyatv==0.7.5 # homeassistant.components.bbox pybbox==0.0.5-alpha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da4d91f22ae..b086eb6c882 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -649,7 +649,7 @@ pyatag==0.3.4.4 pyatmo==4.2.1 # homeassistant.components.apple_tv -pyatv==0.7.3 +pyatv==0.7.5 # homeassistant.components.blackbird pyblackbird==0.5 From 9cfeb44b565fe6f331cdaa58fe9647766b8faf06 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 8 Dec 2020 14:34:26 -0500 Subject: [PATCH 41/90] Exclude coordinator when looking up group members entity IDs (#44058) --- homeassistant/components/zha/core/group.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index 8edb1da8f68..59277a394b3 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -194,6 +194,8 @@ class ZHAGroup(LogMixin): """Return entity ids from the entity domain for this group.""" domain_entity_ids: List[str] = [] for member in self.members: + if member.device.is_coordinator: + continue entities = async_entries_for_device( self._zha_gateway.ha_entity_registry, member.device.device_id, From 0e871c3390492c49d86523b659688f02d3baba13 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 9 Dec 2020 19:02:44 +0100 Subject: [PATCH 42/90] Some lights only support hs, like the lidl christmas lights (#44059) --- homeassistant/components/deconz/light.py | 16 ++++++-- tests/components/deconz/test_light.py | 52 ++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index be967a76fea..6d759ccaf48 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -114,7 +114,9 @@ class DeconzBaseLight(DeconzDevice, LightEntity): if self._device.ct is not None: self._features |= SUPPORT_COLOR_TEMP - if self._device.xy is not None: + if self._device.xy is not None or ( + self._device.hue is not None and self._device.sat is not None + ): self._features |= SUPPORT_COLOR if self._device.effect is not None: @@ -141,8 +143,10 @@ class DeconzBaseLight(DeconzDevice, LightEntity): @property def hs_color(self): """Return the hs color value.""" - if self._device.colormode in ("xy", "hs") and self._device.xy: - return color_util.color_xy_to_hs(*self._device.xy) + if self._device.colormode in ("xy", "hs"): + if self._device.xy: + return color_util.color_xy_to_hs(*self._device.xy) + return (self._device.hue / 65535 * 360, self._device.sat / 255 * 100) return None @property @@ -163,7 +167,11 @@ class DeconzBaseLight(DeconzDevice, LightEntity): data["ct"] = kwargs[ATTR_COLOR_TEMP] if ATTR_HS_COLOR in kwargs: - data["xy"] = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + if self._device.xy is not None: + data["xy"] = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + else: + data["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) + data["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) if ATTR_BRIGHTNESS in kwargs: data["bri"] = kwargs[ATTR_BRIGHTNESS] diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index b971de28d43..18a135a5e05 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -353,3 +353,55 @@ async def test_configuration_tool(hass): await setup_deconz_integration(hass, get_state_response=data) assert len(hass.states.async_all()) == 0 + + +async def test_lidl_christmas_light(hass): + """Test that lights or groups entities are created.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["lights"] = { + "0": { + "etag": "87a89542bf9b9d0aa8134919056844f8", + "hascolor": True, + "lastannounced": None, + "lastseen": "2020-12-05T22:57Z", + "manufacturername": "_TZE200_s8gkrkxk", + "modelid": "TS0601", + "name": "xmas light", + "state": { + "bri": 25, + "colormode": "hs", + "effect": "none", + "hue": 53691, + "on": True, + "reachable": True, + "sat": 141, + }, + "swversion": None, + "type": "Color dimmable light", + "uniqueid": "58:8e:81:ff:fe:db:7b:be-01", + } + } + config_entry = await setup_deconz_integration(hass, get_state_response=data) + gateway = get_gateway_from_config_entry(hass, config_entry) + xmas_light_device = gateway.api.lights["0"] + + assert len(hass.states.async_all()) == 1 + + with patch.object(xmas_light_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.xmas_light", + ATTR_HS_COLOR: (20, 30), + }, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with( + "put", + "/lights/0/state", + json={"on": True, "hue": 3640, "sat": 76}, + ) + + assert hass.states.get("light.xmas_light") From 53b6a971d2330e42c945186c007ce240f7b2ae21 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 9 Dec 2020 17:48:16 +0100 Subject: [PATCH 43/90] Fix ignored Axis config entries doesn't break set up of new entries (#44062) --- homeassistant/components/axis/config_flow.py | 3 ++- tests/components/axis/test_config_flow.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index ea1db54855b..8d52b7f8d9f 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -5,6 +5,7 @@ from ipaddress import ip_address import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import SOURCE_IGNORE from homeassistant.const import ( CONF_HOST, CONF_MAC, @@ -122,7 +123,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): same_model = [ entry.data[CONF_NAME] for entry in self.hass.config_entries.async_entries(AXIS_DOMAIN) - if entry.data[CONF_MODEL] == model + if entry.source != SOURCE_IGNORE and entry.data[CONF_MODEL] == model ] name = model diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 24f888ea6ef..b9dceec7477 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components.axis.const import ( DEFAULT_STREAM_PROFILE, DOMAIN as AXIS_DOMAIN, ) -from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import ( CONF_HOST, CONF_MAC, @@ -31,6 +31,8 @@ from tests.common import MockConfigEntry async def test_flow_manual_configuration(hass): """Test that config flow works.""" + MockConfigEntry(domain=AXIS_DOMAIN, source=SOURCE_IGNORE).add_to_hass(hass) + result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, context={"source": SOURCE_USER} ) From f3eb21ba59da0cbe31f2b87fb601294bdc3cd246 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 9 Dec 2020 19:08:29 +0100 Subject: [PATCH 44/90] Bumped version to 1.0.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8aa48b5178c..22e3a5f1813 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 1 MINOR_VERSION = 0 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0b3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 29e3fbe568f880f10ba06b60965e36d741c94a99 Mon Sep 17 00:00:00 2001 From: zewelor Date: Thu, 10 Dec 2020 09:54:10 +0100 Subject: [PATCH 45/90] Fix yeelight unavailbility (#44061) --- homeassistant/components/yeelight/__init__.py | 66 ++++++++++++------- tests/components/yeelight/__init__.py | 3 +- tests/components/yeelight/test_init.py | 46 ++++++++++++- 3 files changed, 88 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index ae9d75de54f..324999c7124 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -17,7 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "yeelight" DATA_YEELIGHT = DOMAIN DATA_UPDATED = "yeelight_{}_data_updated" -DEVICE_INITIALIZED = f"{DOMAIN}_device_initialized" +DEVICE_INITIALIZED = "yeelight_{}_device_initialized" DEFAULT_NAME = "Yeelight" DEFAULT_TRANSITION = 350 @@ -181,8 +181,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Yeelight from a config entry.""" async def _initialize(host: str, capabilities: Optional[dict] = None) -> None: - device = await _async_setup_device(hass, host, entry, capabilities) + async_dispatcher_connect( + hass, + DEVICE_INITIALIZED.format(host), + _load_platforms, + ) + + device = await _async_get_device(hass, host, entry, capabilities) hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id][DATA_DEVICE] = device + + await device.async_setup() + + async def _load_platforms(): + for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) @@ -249,28 +260,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -async def _async_setup_device( - hass: HomeAssistant, - host: str, - entry: ConfigEntry, - capabilities: Optional[dict], -) -> None: - # Get model from config and capabilities - model = entry.options.get(CONF_MODEL) - if not model and capabilities is not None: - model = capabilities.get("model") - - # Set up device - bulb = Bulb(host, model=model or None) - if capabilities is None: - capabilities = await hass.async_add_executor_job(bulb.get_capabilities) - - device = YeelightDevice(hass, host, entry.options, bulb, capabilities) - await hass.async_add_executor_job(device.update) - await device.async_setup() - return device - - @callback def _async_unique_name(capabilities: dict) -> str: """Generate name from capabilities.""" @@ -374,6 +363,7 @@ class YeelightDevice: self._device_type = None self._available = False self._remove_time_tracker = None + self._initialized = False self._name = host # Default name is host if capabilities: @@ -495,6 +485,8 @@ class YeelightDevice: try: self.bulb.get_properties(UPDATE_REQUEST_PROPERTIES) self._available = True + if not self._initialized: + self._initialize_device() except BulbException as ex: if self._available: # just inform once _LOGGER.error( @@ -522,6 +514,11 @@ class YeelightDevice: ex, ) + def _initialize_device(self): + self._get_capabilities() + self._initialized = True + dispatcher_send(self._hass, DEVICE_INITIALIZED.format(self._host)) + def update(self): """Update device properties and send data updated signal.""" self._update_properties() @@ -584,3 +581,22 @@ class YeelightEntity(Entity): def update(self) -> None: """Update the entity.""" self._device.update() + + +async def _async_get_device( + hass: HomeAssistant, + host: str, + entry: ConfigEntry, + capabilities: Optional[dict], +) -> YeelightDevice: + # Get model from config and capabilities + model = entry.options.get(CONF_MODEL) + if not model and capabilities is not None: + model = capabilities.get("model") + + # Set up device + bulb = Bulb(host, model=model or None) + if capabilities is None: + capabilities = await hass.async_add_executor_job(bulb.get_capabilities) + + return YeelightDevice(hass, host, entry.options, bulb, capabilities) diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index 9f811586a77..5405b69490b 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -55,7 +55,8 @@ PROPERTIES = { "current_brightness": "30", } -ENTITY_BINARY_SENSOR = f"binary_sensor.{UNIQUE_NAME}_nightlight" +ENTITY_BINARY_SENSOR_TEMPLATE = "binary_sensor.{}_nightlight" +ENTITY_BINARY_SENSOR = ENTITY_BINARY_SENSOR_TEMPLATE.format(UNIQUE_NAME) ENTITY_LIGHT = f"light.{UNIQUE_NAME}" ENTITY_NIGHTLIGHT = f"light.{UNIQUE_NAME}_nightlight" ENTITY_AMBILIGHT = f"light.{UNIQUE_NAME}_ambilight" diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index d9c23cfa1a7..882f9944ca1 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -1,21 +1,27 @@ """Test Yeelight.""" +from unittest.mock import MagicMock + from yeelight import BulbType from homeassistant.components.yeelight import ( CONF_NIGHTLIGHT_SWITCH, CONF_NIGHTLIGHT_SWITCH_TYPE, + DATA_CONFIG_ENTRIES, + DATA_DEVICE, DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, ) -from homeassistant.const import CONF_DEVICES, CONF_NAME +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component from . import ( + CAPABILITIES, CONFIG_ENTRY_DATA, ENTITY_AMBILIGHT, ENTITY_BINARY_SENSOR, + ENTITY_BINARY_SENSOR_TEMPLATE, ENTITY_LIGHT, ENTITY_NIGHTLIGHT, ID, @@ -115,6 +121,7 @@ async def test_unique_ids_entry(hass: HomeAssistant): mocked_bulb = _mocked_bulb() mocked_bulb.bulb_type = BulbType.WhiteTempMood + with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -132,3 +139,40 @@ async def test_unique_ids_entry(hass: HomeAssistant): assert ( er.async_get(ENTITY_AMBILIGHT).unique_id == f"{config_entry.entry_id}-ambilight" ) + + +async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant): + """Test Yeelight off while adding to ha, for example on HA start.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + **CONFIG_ENTRY_DATA, + CONF_HOST: IP_ADDRESS, + }, + unique_id=ID, + ) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb(True) + mocked_bulb.bulb_type = BulbType.WhiteTempMood + + with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( + f"{MODULE}.config_flow.yeelight.Bulb", return_value=mocked_bulb + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( + IP_ADDRESS.replace(".", "_") + ) + er = await entity_registry.async_get_registry(hass) + assert er.async_get(binary_sensor_entity_id) is None + + type(mocked_bulb).get_capabilities = MagicMock(CAPABILITIES) + type(mocked_bulb).get_properties = MagicMock(None) + + hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE].update() + await hass.async_block_till_done() + + er = await entity_registry.async_get_registry(hass) + assert er.async_get(binary_sensor_entity_id) is not None From 6ea3c671e9b85d9779d5f394c5e6de551c1b501e Mon Sep 17 00:00:00 2001 From: "J.P. Hutchins" <34154542+JPHutchins@users.noreply.github.com> Date: Thu, 10 Dec 2020 01:09:08 -0800 Subject: [PATCH 46/90] Fix transmission torrent filtering and sorting (#44069) --- homeassistant/components/transmission/sensor.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index ef1e68e2d0a..ea62de71e8d 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -145,12 +145,10 @@ class TransmissionTorrentsSensor(TransmissionSensor): @property def device_state_attributes(self): """Return the state attributes, if any.""" - limit = self._tm_client.config_entry.options[CONF_LIMIT] - order = self._tm_client.config_entry.options[CONF_ORDER] - torrents = self._tm_client.api.torrents[0:limit] info = _torrents_info( - torrents, - order=order, + torrents=self._tm_client.api.torrents, + order=self._tm_client.config_entry.options[CONF_ORDER], + limit=self._tm_client.config_entry.options[CONF_LIMIT], statuses=self.SUBTYPE_MODES[self._sub_type], ) return { @@ -173,11 +171,11 @@ def _filter_torrents(torrents, statuses=None): ] -def _torrents_info(torrents, order, statuses=None): +def _torrents_info(torrents, order, limit, statuses=None): infos = {} torrents = _filter_torrents(torrents, statuses) torrents = SUPPORTED_ORDER_MODES[order](torrents) - for torrent in _filter_torrents(torrents, statuses): + for torrent in torrents[:limit]: info = infos[torrent.name] = { "added_date": torrent.addedDate, "percent_done": f"{torrent.percentDone * 100:.2f}", From e68544cd7dfcc88bd6e9c41d7591ecc941be48fa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Dec 2020 10:10:38 +0100 Subject: [PATCH 47/90] Bump hass-nabucasa to 0.39.0 (#44097) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 3f65ed2ba46..03bf2761857 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.38.0"], + "requirements": ["hass-nabucasa==0.39.0"], "dependencies": ["http", "webhook", "alexa"], "after_dependencies": ["google_assistant"], "codeowners": ["@home-assistant/cloud"] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d820f5e715b..61d8d4a35a6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==3.2 defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 -hass-nabucasa==0.38.0 +hass-nabucasa==0.39.0 home-assistant-frontend==20201204.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' diff --git a/requirements_all.txt b/requirements_all.txt index e2d17cfe39a..d020d105625 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -732,7 +732,7 @@ habitipy==0.2.0 hangups==0.4.11 # homeassistant.components.cloud -hass-nabucasa==0.38.0 +hass-nabucasa==0.39.0 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b086eb6c882..7cc159f9212 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -373,7 +373,7 @@ ha-ffmpeg==3.0.2 hangups==0.4.11 # homeassistant.components.cloud -hass-nabucasa==0.38.0 +hass-nabucasa==0.39.0 # homeassistant.components.tasmota hatasmota==0.1.4 From cff4f7bbbfd068f550cc7808149d80c83b1e5744 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Dec 2020 09:12:20 +0000 Subject: [PATCH 48/90] Bumped version to 1.0.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 22e3a5f1813..72f422ed8fb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 1 MINOR_VERSION = 0 -PATCH_VERSION = "0b3" +PATCH_VERSION = "0b4" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From a01cf72d673d558bd8dcd42a8df5d4a33fc03fd5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Dec 2020 11:11:49 +0100 Subject: [PATCH 49/90] Fix importing blueprints from forums with HTML entities (#44098) --- .../components/blueprint/importer.py | 3 +- tests/components/blueprint/test_importer.py | 79 +- tests/fixtures/blueprint/community_post.json | 749 ++++++++---------- 3 files changed, 391 insertions(+), 440 deletions(-) diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index 524b04293ee..f0230aba1b7 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -1,5 +1,6 @@ """Import logic for blueprint.""" from dataclasses import dataclass +import html import re from typing import Optional @@ -110,7 +111,7 @@ def _extract_blueprint_from_community_topic( block_content = block_content.strip() try: - data = yaml.parse_yaml(block_content) + data = yaml.parse_yaml(html.unescape(block_content)) except HomeAssistantError: if block_syntax == "yaml": raise diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index bb8903459c9..8e674e3a9de 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -16,6 +16,70 @@ def community_post(): return load_fixture("blueprint/community_post.json") +COMMUNITY_POST_INPUTS = { + "remote": { + "name": "Remote", + "description": "IKEA remote to use", + "selector": { + "device": { + "integration": "zha", + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI remote control", + } + }, + }, + "light": { + "name": "Light(s)", + "description": "The light(s) to control", + "selector": {"target": {"entity": {"domain": "light"}}}, + }, + "force_brightness": { + "name": "Force turn on brightness", + "description": 'Force the brightness to the set level below, when the "on" button on the remote is pushed and lights turn on.\n', + "default": False, + "selector": {"boolean": {}}, + }, + "brightness": { + "name": "Brightness", + "description": "Brightness of the light(s) when turning on", + "default": 50, + "selector": { + "number": { + "min": 0.0, + "max": 100.0, + "mode": "slider", + "step": 1.0, + "unit_of_measurement": "%", + } + }, + }, + "button_left_short": { + "name": "Left button - short press", + "description": "Action to run on short left button press", + "default": [], + "selector": {"action": {}}, + }, + "button_left_long": { + "name": "Left button - long press", + "description": "Action to run on long left button press", + "default": [], + "selector": {"action": {}}, + }, + "button_right_short": { + "name": "Right button - short press", + "description": "Action to run on short right button press", + "default": [], + "selector": {"action": {}}, + }, + "button_right_long": { + "name": "Right button - long press", + "description": "Action to run on long right button press", + "default": [], + "selector": {"action": {}}, + }, +} + + def test_get_community_post_import_url(): """Test variations of generating import forum url.""" assert ( @@ -57,10 +121,7 @@ def test_extract_blueprint_from_community_topic(community_post): ) assert imported_blueprint is not None assert imported_blueprint.blueprint.domain == "automation" - assert imported_blueprint.blueprint.inputs == { - "service_to_call": None, - "trigger_event": None, - } + assert imported_blueprint.blueprint.inputs == COMMUNITY_POST_INPUTS def test_extract_blueprint_from_community_topic_invalid_yaml(): @@ -103,11 +164,11 @@ async def test_fetch_blueprint_from_community_url(hass, aioclient_mock, communit ) assert isinstance(imported_blueprint, importer.ImportedBlueprint) assert imported_blueprint.blueprint.domain == "automation" - assert imported_blueprint.blueprint.inputs == { - "service_to_call": None, - "trigger_event": None, - } - assert imported_blueprint.suggested_filename == "balloob/test-topic" + assert imported_blueprint.blueprint.inputs == COMMUNITY_POST_INPUTS + assert ( + imported_blueprint.suggested_filename + == "frenck/zha-ikea-five-button-remote-for-lights" + ) assert ( imported_blueprint.blueprint.metadata["source_url"] == "https://community.home-assistant.io/t/test-topic/123/2" diff --git a/tests/fixtures/blueprint/community_post.json b/tests/fixtures/blueprint/community_post.json index 5b9a3dcb9c7..121d53ad94e 100644 --- a/tests/fixtures/blueprint/community_post.json +++ b/tests/fixtures/blueprint/community_post.json @@ -2,39 +2,58 @@ "post_stream": { "posts": [ { - "id": 1144853, - "name": "Paulus Schoutsen", - "username": "balloob", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", - "created_at": "2020-10-16T12:20:12.688Z", - "cooked": "\u003cp\u003ehere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003c/p\u003e\n\u003ch1\u003eBlock without syntax\u003c/h1\u003e\n\u003cpre\u003e\u003ccode class=\"lang-auto\"\u003eblueprint:\n domain: automation\n name: Example Blueprint from post\n input:\n trigger_event:\n service_to_call:\ntrigger:\n platform: event\n event_type: !input trigger_event\naction:\n service: !input service_to_call\n\u003c/code\u003e\u003c/pre\u003e", + "id": 1216212, + "name": "Franck Nijhof", + "username": "frenck", + "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png", + "created_at": "2020-12-10T09:20:58.974Z", + "cooked": "\u003cp\u003eThis is a blueprint for the IKEA five-button remotes (the round ones), specifically for use with ZHA.\u003c/p\u003e\n\u003cp\u003e\u003cdiv class=\"lightbox-wrapper\"\u003e\u003ca class=\"lightbox\" href=\"https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg\" data-download-href=\"/uploads/short-url/8SdGCUtkzOTNpMjggpBvSFs4WQ.jpeg?dl=1\" title=\"image\"\u003e\u003cimg src=\"https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_500x500.jpeg\" alt=\"image\" data-base62-sha1=\"8SdGCUtkzOTNpMjggpBvSFs4WQ\" width=\"500\" height=\"500\" srcset=\"https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_500x500.jpeg, https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_750x750.jpeg 1.5x, https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_1000x1000.jpeg 2x\" data-small-upload=\"https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_10x10.png\"\u003e\u003cdiv class=\"meta\"\u003e\u003csvg class=\"fa d-icon d-icon-far-image svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#far-image\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003cspan class=\"filename\"\u003eimage\u003c/span\u003e\u003cspan class=\"informations\"\u003e1400×1400 150 KB\u003c/span\u003e\u003csvg class=\"fa d-icon d-icon-discourse-expand svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#discourse-expand\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/div\u003e\u003c/a\u003e\u003c/div\u003e\u003c/p\u003e\n\u003cp\u003eIt was specially created for use with (any) light(s). As the basic light controls are already mapped in this blueprint.\u003c/p\u003e\n\u003cp\u003eThe middle “on” button, toggle the lights on/off to the last set brightness (unless the force brightness is toggled on in the blueprint). Dim up/down buttons will change the brightness smoothly and can be pressed and hold until the brightness is satisfactory.\u003c/p\u003e\n\u003cp\u003eThe “left” and “right” buttons can be assigned to a short and long button press action. This allows you to assign, e.g., a scene or anything else.\u003c/p\u003e\n\u003cp\u003eThis is what the Blueprint looks like from the UI:\u003c/p\u003e\n\u003cp\u003e\u003cdiv class=\"lightbox-wrapper\"\u003e\u003ca class=\"lightbox\" href=\"https://community-assets.home-assistant.io/original/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83.png\" data-download-href=\"/uploads/short-url/mf5vhlKYe6yeuFayUzlCTBfveKf.png?dl=1\" title=\"image\"\u003e\u003cimg src=\"https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_610x500.png\" alt=\"image\" data-base62-sha1=\"mf5vhlKYe6yeuFayUzlCTBfveKf\" width=\"610\" height=\"500\" srcset=\"https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_610x500.png, https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_915x750.png 1.5x, https://community-assets.home-assistant.io/original/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83.png 2x\" data-small-upload=\"https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_10x10.png\"\u003e\u003cdiv class=\"meta\"\u003e\u003csvg class=\"fa d-icon d-icon-far-image svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#far-image\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003cspan class=\"filename\"\u003eimage\u003c/span\u003e\u003cspan class=\"informations\"\u003e975×799 64.1 KB\u003c/span\u003e\u003csvg class=\"fa d-icon d-icon-discourse-expand svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#discourse-expand\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/div\u003e\u003c/a\u003e\u003c/div\u003e\u003c/p\u003e\n\u003cp\u003eBlueprint, which you can import by using this forum topic URL:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-yaml\"\u003eblueprint:\n name: ZHA - IKEA five button remote for lights\n description: |\n Control lights with an IKEA five button remote (the round ones).\n\n The middle \"on\" button, toggle the lights on/off to the last set brightness\n (unless the force brightness is toggled on in the blueprint).\n\n Dim up/down buttons will change the brightness smoothly and can be pressed\n and hold until the brightness is satisfactory.\n\n The \"left\" and \"right\" buttons can be assigned to a short and long button\n press action. This allows you to assign, e.g., a scene or anything else.\n\n domain: automation\n input:\n remote:\n name: Remote\n description: IKEA remote to use\n selector:\n device:\n integration: zha\n manufacturer: IKEA of Sweden\n model: TRADFRI remote control\n light:\n name: Light(s)\n description: The light(s) to control\n selector:\n target:\n entity:\n domain: light\n force_brightness:\n name: Force turn on brightness\n description: \u0026gt;\n Force the brightness to the set level below, when the \"on\" button on\n the remote is pushed and lights turn on.\n default: false\n selector:\n boolean:\n brightness:\n name: Brightness\n description: Brightness of the light(s) when turning on\n default: 50\n selector:\n number:\n min: 0\n max: 100\n mode: slider\n step: 1\n unit_of_measurement: \"%\"\n button_left_short:\n name: Left button - short press\n description: Action to run on short left button press\n default: []\n selector:\n action:\n button_left_long:\n name: Left button - long press\n description: Action to run on long left button press\n default: []\n selector:\n action:\n button_right_short:\n name: Right button - short press\n description: Action to run on short right button press\n default: []\n selector:\n action:\n button_right_long:\n name: Right button - long press\n description: Action to run on long right button press\n default: []\n selector:\n action:\n\nmode: restart\nmax_exceeded: silent\n\nvariables:\n force_brightness: !input force_brightness\n\ntrigger:\n - platform: event\n event_type: zha_event\n event_data:\n device_id: !input remote\n\naction:\n - variables:\n command: \"{{ trigger.event.data.command }}\"\n cluster_id: \"{{ trigger.event.data.cluster_id }}\"\n endpoint_id: \"{{ trigger.event.data.endpoint_id }}\"\n args: \"{{ trigger.event.data.args }}\"\n - choose:\n - conditions:\n - \"{{ command == 'toggle' }}\"\n - \"{{ cluster_id == 6 }}\"\n - \"{{ endpoint_id == 1 }}\"\n sequence:\n - choose:\n - conditions: \"{{ force_brightness }}\"\n sequence:\n - service: light.toggle\n target: !input light\n data:\n transition: 1\n brightness_pct: !input brightness\n default:\n - service: light.toggle\n target: !input light\n data:\n transition: 1\n\n - conditions:\n - \"{{ command == 'step_with_on_off' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [0, 43, 5] }}\"\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: 10\n transition: 1\n\n - conditions:\n - \"{{ command == 'move_with_on_off' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [0, 84] }}\"\n sequence:\n - repeat:\n count: 10\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: 10\n transition: 1\n - delay: 1\n\n - conditions:\n - \"{{ command == 'step' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [1, 43, 5] }}\"\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: -10\n transition: 1\n\n - conditions:\n - \"{{ command == 'move' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [1, 84] }}\"\n sequence:\n - repeat:\n count: 10\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: -10\n transition: 1\n - delay: 1\n\n - conditions:\n - \"{{ command == 'press' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [257, 13, 0] }}\"\n sequence: !input button_left_short\n\n - conditions:\n - \"{{ command == 'hold' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [3329, 0] }}\"\n sequence: !input button_left_long\n\n - conditions:\n - \"{{ command == 'press' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [256, 13, 0] }}\"\n sequence: !input button_right_short\n\n - conditions:\n - \"{{ command == 'hold' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [3328, 0] }}\"\n sequence: !input button_right_long\n\u003c/code\u003e\u003c/pre\u003e", "post_number": 1, "post_type": 1, - "updated_at": "2020-10-20T08:24:14.189Z", + "updated_at": "2020-12-10T09:22:08.993Z", "reply_count": 0, "reply_to_post_number": null, "quote_count": 0, "incoming_link_count": 0, - "reads": 2, - "readers_count": 1, - "score": 0.4, - "yours": true, - "topic_id": 236133, - "topic_slug": "test-topic", - "display_username": "Paulus Schoutsen", + "reads": 3, + "readers_count": 2, + "score": 0.6, + "yours": false, + "topic_id": 253804, + "topic_slug": "zha-ikea-five-button-remote-for-lights", + "display_username": "Franck Nijhof", "primary_group_name": null, "primary_group_flair_url": null, "primary_group_flair_bg_color": null, "primary_group_flair_color": null, - "version": 2, + "version": 1, "can_edit": true, "can_delete": false, "can_recover": false, "can_wiki": true, + "link_counts": [ + { + "url": "https://community-assets.home-assistant.io/original/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83.png", + "internal": false, + "reflection": false, + "title": "9be4788b5358284d138c4304fb0b8068c18a2b83.png", + "clicks": 0 + }, + { + "url": "https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg", + "internal": false, + "reflection": false, + "title": "0100d04d2debf34eb11abdfee0707624f3961f80.jpeg", + "clicks": 0 + } + ], "read": true, - "user_title": "Founder of Home Assistant", - "title_is_group": false, + "user_title": null, "actions_summary": [ + { + "id": 2, + "can_act": true + }, { "id": 3, "can_act": true @@ -48,75 +67,7 @@ "can_act": true }, { - "id": 7, - "can_act": true - } - ], - "moderator": true, - "admin": true, - "staff": true, - "user_id": 3, - "hidden": false, - "trust_level": 2, - "deleted_at": null, - "user_deleted": false, - "edit_reason": null, - "can_view_edit_history": true, - "wiki": false, - "reviewable_id": 0, - "reviewable_score_count": 0, - "reviewable_score_pending_count": 0, - "user_created_at": "2016-03-30T07:50:25.541Z", - "user_date_of_birth": null, - "user_signature": null, - "can_accept_answer": false, - "can_unaccept_answer": false, - "accepted_answer": false - }, - { - "id": 1144854, - "name": "Paulus Schoutsen", - "username": "balloob", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", - "created_at": "2020-10-16T12:20:17.535Z", - "cooked": "", - "post_number": 2, - "post_type": 3, - "updated_at": "2020-10-16T12:20:17.535Z", - "reply_count": 0, - "reply_to_post_number": null, - "quote_count": 0, - "incoming_link_count": 1, - "reads": 2, - "readers_count": 1, - "score": 5.4, - "yours": true, - "topic_id": 236133, - "topic_slug": "test-topic", - "display_username": "Paulus Schoutsen", - "primary_group_name": null, - "primary_group_flair_url": null, - "primary_group_flair_bg_color": null, - "primary_group_flair_color": null, - "version": 1, - "can_edit": true, - "can_delete": true, - "can_recover": false, - "can_wiki": true, - "read": true, - "user_title": "Founder of Home Assistant", - "title_is_group": false, - "actions_summary": [ - { - "id": 3, - "can_act": true - }, - { - "id": 4, - "can_act": true - }, - { - "id": 8, + "id": 6, "can_act": true }, { @@ -127,82 +78,9 @@ "moderator": true, "admin": true, "staff": true, - "user_id": 3, + "user_id": 10250, "hidden": false, - "trust_level": 2, - "deleted_at": null, - "user_deleted": false, - "edit_reason": null, - "can_view_edit_history": true, - "wiki": false, - "action_code": "visible.disabled", - "reviewable_id": 0, - "reviewable_score_count": 0, - "reviewable_score_pending_count": 0, - "user_created_at": "2016-03-30T07:50:25.541Z", - "user_date_of_birth": null, - "user_signature": null, - "can_accept_answer": false, - "can_unaccept_answer": false, - "accepted_answer": false - }, - { - "id": 1144872, - "name": "Paulus Schoutsen", - "username": "balloob", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", - "created_at": "2020-10-16T12:27:53.926Z", - "cooked": "\u003cp\u003eTest reply!\u003c/p\u003e", - "post_number": 3, - "post_type": 1, - "updated_at": "2020-10-16T12:27:53.926Z", - "reply_count": 0, - "reply_to_post_number": null, - "quote_count": 0, - "incoming_link_count": 0, - "reads": 2, - "readers_count": 1, - "score": 0.4, - "yours": true, - "topic_id": 236133, - "topic_slug": "test-topic", - "display_username": "Paulus Schoutsen", - "primary_group_name": null, - "primary_group_flair_url": null, - "primary_group_flair_bg_color": null, - "primary_group_flair_color": null, - "version": 1, - "can_edit": true, - "can_delete": true, - "can_recover": false, - "can_wiki": true, - "read": true, - "user_title": "Founder of Home Assistant", - "title_is_group": false, - "actions_summary": [ - { - "id": 3, - "can_act": true - }, - { - "id": 4, - "can_act": true - }, - { - "id": 8, - "can_act": true - }, - { - "id": 7, - "can_act": true - } - ], - "moderator": true, - "admin": true, - "staff": true, - "user_id": 3, - "hidden": false, - "trust_level": 2, + "trust_level": 4, "deleted_at": null, "user_deleted": false, "edit_reason": null, @@ -211,7 +89,7 @@ "reviewable_id": 0, "reviewable_score_count": 0, "reviewable_score_pending_count": 0, - "user_created_at": "2016-03-30T07:50:25.541Z", + "user_created_at": "2017-08-12T12:46:55.467Z", "user_date_of_birth": null, "user_signature": null, "can_accept_answer": false, @@ -220,36 +98,34 @@ } ], "stream": [ - 1144853, - 1144854, - 1144872 + 1216212 ] }, "timeline_lookup": [ [ 1, - 3 + 0 ] ], "suggested_topics": [ { - "id": 17750, - "title": "Tutorial: Creating your first add-on", - "fancy_title": "Tutorial: Creating your first add-on", - "slug": "tutorial-creating-your-first-add-on", - "posts_count": 26, - "reply_count": 14, - "highest_post_number": 27, - "image_url": null, - "created_at": "2017-05-14T07:51:33.946Z", - "last_posted_at": "2020-07-28T11:29:27.892Z", + "id": 168593, + "title": "Dwains Dashboard - 1 CLICK install Lovelace Dashboard for desktop, tablet and mobile. v2.0.0", + "fancy_title": "Dwains Dashboard - 1 CLICK install Lovelace Dashboard for desktop, tablet and mobile. v2.0.0", + "slug": "dwains-dashboard-1-click-install-lovelace-dashboard-for-desktop-tablet-and-mobile-v2-0-0", + "posts_count": 1162, + "reply_count": 785, + "highest_post_number": 1185, + "image_url": "//community-assets.home-assistant.io/original/3X/a/0/a051e5940117bebcb70e8d8545ad4b65f63bd175.jpeg", + "created_at": "2020-02-03T13:15:24.364Z", + "last_posted_at": "2020-12-10T07:57:47.304Z", "bumped": true, - "bumped_at": "2020-07-28T11:29:27.892Z", + "bumped_at": "2020-12-10T07:57:47.304Z", "archetype": "regular", "unseen": false, - "last_read_post_number": 18, - "unread": 7, - "new_posts": 2, + "last_read_post_number": 81, + "unread": 0, + "new_posts": 1109, "pinned": false, "unpinned": null, "visible": true, @@ -258,11 +134,19 @@ "notification_level": 2, "bookmarked": false, "liked": false, - "thumbnails": null, + "thumbnails": [ + { + "max_width": null, + "max_height": null, + "width": 296, + "height": 50, + "url": "//community-assets.home-assistant.io/original/3X/a/0/a051e5940117bebcb70e8d8545ad4b65f63bd175.jpeg" + } + ], "tags": [], - "like_count": 9, - "views": 4355, - "category_id": 25, + "like_count": 1214, + "views": 71580, + "category_id": 34, "featured_link": null, "has_accepted_answer": false, "posters": [ @@ -270,50 +154,50 @@ "extras": null, "description": "Original Poster", "user": { - "id": 3, - "username": "balloob", - "name": "Paulus Schoutsen", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png" + "id": 36674, + "username": "dwains", + "name": "Dwain Scheeren", + "avatar_template": "/user_avatar/community.home-assistant.io/dwains/{size}/100261_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 9852, - "username": "JSCSJSCS", + "id": 16514, + "username": "jimpower", "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/jscsjscs/{size}/38256_2.png" + "avatar_template": "/user_avatar/community.home-assistant.io/jimpower/{size}/66909_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 11494, - "username": "so3n", - "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/so3n/{size}/46007_2.png" + "id": 1473, + "username": "thundergreen", + "name": "Thundergreen", + "avatar_template": "/user_avatar/community.home-assistant.io/thundergreen/{size}/18379_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 9094, - "username": "IoTnerd", - "name": "Balázs Suhajda", - "avatar_template": "/user_avatar/community.home-assistant.io/iotnerd/{size}/33526_2.png" + "id": 64369, + "username": "MRobi", + "name": "Mike", + "avatar_template": "/user_avatar/community.home-assistant.io/mrobi/{size}/113127_2.png" } }, { "extras": "latest", "description": "Most Recent Poster", "user": { - "id": 73134, - "username": "diord", - "name": "", - "avatar_template": "/letter_avatar/diord/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + "id": 9646, + "username": "Freshhat", + "name": "Freshhat", + "avatar_template": "/user_avatar/community.home-assistant.io/freshhat/{size}/24797_2.png" } } ] @@ -323,19 +207,19 @@ "title": "Lovelace: Button card", "fancy_title": "Lovelace: Button card", "slug": "lovelace-button-card", - "posts_count": 4608, - "reply_count": 3522, - "highest_post_number": 4691, + "posts_count": 4775, + "reply_count": 3635, + "highest_post_number": 4858, "image_url": null, "created_at": "2018-08-28T00:18:19.312Z", - "last_posted_at": "2020-10-20T07:33:29.523Z", + "last_posted_at": "2020-12-10T04:42:58.851Z", "bumped": true, - "bumped_at": "2020-10-20T07:33:29.523Z", + "bumped_at": "2020-12-10T04:42:58.851Z", "archetype": "regular", "unseen": false, "last_read_post_number": 1938, "unread": 369, - "new_posts": 2384, + "new_posts": 2551, "pinned": false, "unpinned": null, "visible": true, @@ -346,8 +230,8 @@ "liked": false, "thumbnails": null, "tags": [], - "like_count": 1700, - "views": 184752, + "like_count": 1740, + "views": 199965, "category_id": 34, "featured_link": null, "has_accepted_answer": false, @@ -366,20 +250,20 @@ "extras": null, "description": "Frequent Poster", "user": { - "id": 2019, - "username": "iantrich", - "name": "Ian", - "avatar_template": "/user_avatar/community.home-assistant.io/iantrich/{size}/154042_2.png" + "id": 33228, + "username": "jimz011", + "name": "Jim", + "avatar_template": "/user_avatar/community.home-assistant.io/jimz011/{size}/62413_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 33228, - "username": "jimz011", - "name": "Jim", - "avatar_template": "/user_avatar/community.home-assistant.io/jimz011/{size}/62413_2.png" + "id": 12475, + "username": "Mariusthvdb", + "name": "Marius", + "avatar_template": "/user_avatar/community.home-assistant.io/mariusthvdb/{size}/49008_2.png" } }, { @@ -396,32 +280,32 @@ "extras": "latest", "description": "Most Recent Poster", "user": { - "id": 26227, - "username": "RomRider", - "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/romrider/{size}/41384_2.png" + "id": 52090, + "username": "parautenbach", + "name": "Pieter Rautenbach", + "avatar_template": "/user_avatar/community.home-assistant.io/parautenbach/{size}/89345_2.png" } } ] }, { - "id": 10564, - "title": "Professional/Commercial Use?", - "fancy_title": "Professional/Commercial Use?", - "slug": "professional-commercial-use", - "posts_count": 54, - "reply_count": 37, - "highest_post_number": 54, + "id": 58639, + "title": "Echo Devices (Alexa) as Media Player - Testers Needed", + "fancy_title": "Echo Devices (Alexa) as Media Player - Testers Needed", + "slug": "echo-devices-alexa-as-media-player-testers-needed", + "posts_count": 4429, + "reply_count": 3009, + "highest_post_number": 4517, "image_url": null, - "created_at": "2017-01-27T05:01:57.453Z", - "last_posted_at": "2020-10-20T07:03:57.895Z", + "created_at": "2018-07-04T03:36:22.187Z", + "last_posted_at": "2020-12-10T04:26:11.298Z", "bumped": true, - "bumped_at": "2020-10-20T07:03:57.895Z", + "bumped_at": "2020-12-10T04:26:11.298Z", "archetype": "regular", "unseen": false, - "last_read_post_number": 7, + "last_read_post_number": 3219, "unread": 0, - "new_posts": 47, + "new_posts": 1298, "pinned": false, "unpinned": null, "visible": true, @@ -431,104 +315,12 @@ "bookmarked": false, "liked": false, "thumbnails": null, - "tags": [], - "like_count": 21, - "views": 10695, - "category_id": 17, - "featured_link": null, - "has_accepted_answer": false, - "posters": [ - { - "extras": null, - "description": "Original Poster", - "user": { - "id": 4758, - "username": "oobie11", - "name": "Bryan", - "avatar_template": "/user_avatar/community.home-assistant.io/oobie11/{size}/37858_2.png" - } - }, - { - "extras": null, - "description": "Frequent Poster", - "user": { - "id": 18386, - "username": "pitp2", - "name": "", - "avatar_template": "/letter_avatar/pitp2/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" - } - }, - { - "extras": null, - "description": "Frequent Poster", - "user": { - "id": 23116, - "username": "jortegamx", - "name": "Jake", - "avatar_template": "/user_avatar/community.home-assistant.io/jortegamx/{size}/45515_2.png" - } - }, - { - "extras": null, - "description": "Frequent Poster", - "user": { - "id": 39038, - "username": "orif73", - "name": "orif73", - "avatar_template": "/letter_avatar/orif73/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" - } - }, - { - "extras": "latest", - "description": "Most Recent Poster", - "user": { - "id": 41040, - "username": "devastator", - "name": "", - "avatar_template": "/letter_avatar/devastator/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" - } - } - ] - }, - { - "id": 219480, - "title": "What the heck is with the 'latest state change' not being kept after restart?", - "fancy_title": "What the heck is with the \u0026lsquo;latest state change\u0026rsquo; not being kept after restart?", - "slug": "what-the-heck-is-with-the-latest-state-change-not-being-kept-after-restart", - "posts_count": 37, - "reply_count": 13, - "highest_post_number": 38, - "image_url": "https://community-assets.home-assistant.io/original/3X/3/4/349d096b209d40d5f424b64e970bcf360332cc7f.png", - "created_at": "2020-08-18T13:10:09.367Z", - "last_posted_at": "2020-10-20T00:32:07.312Z", - "bumped": true, - "bumped_at": "2020-10-20T00:32:07.312Z", - "archetype": "regular", - "unseen": false, - "last_read_post_number": 8, - "unread": 0, - "new_posts": 30, - "pinned": false, - "unpinned": null, - "visible": true, - "closed": false, - "archived": false, - "notification_level": 2, - "bookmarked": false, - "liked": false, - "thumbnails": [ - { - "max_width": null, - "max_height": null, - "width": 469, - "height": 59, - "url": "https://community-assets.home-assistant.io/original/3X/3/4/349d096b209d40d5f424b64e970bcf360332cc7f.png" - } + "tags": [ + "alexa" ], - "tags": [], - "like_count": 26, - "views": 1722, - "category_id": 52, + "like_count": 1092, + "views": 179580, + "category_id": 47, "featured_link": null, "has_accepted_answer": false, "posters": [ @@ -536,72 +328,72 @@ "extras": null, "description": "Original Poster", "user": { - "id": 3124, - "username": "andriej", + "id": 1084, + "username": "keatontaylor", + "name": "Keatontaylor", + "avatar_template": "/letter_avatar/keatontaylor/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + } + }, + { + "extras": null, + "description": "Frequent Poster", + "user": { + "id": 24884, + "username": "h4nc", "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/andriej/{size}/24457_2.png" + "avatar_template": "/user_avatar/community.home-assistant.io/h4nc/{size}/68244_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 15052, - "username": "Misiu", + "id": 9191, + "username": "finity", "name": "", - "avatar_template": "/user_avatar/community.home-assistant.io/misiu/{size}/20752_2.png" + "avatar_template": "/letter_avatar/finity/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 4629, - "username": "lolouk44", - "name": "lolouk44", - "avatar_template": "/user_avatar/community.home-assistant.io/lolouk44/{size}/119845_2.png" - } - }, - { - "extras": null, - "description": "Frequent Poster", - "user": { - "id": 51736, - "username": "hmoffatt", - "name": "Hamish Moffatt", - "avatar_template": "/user_avatar/community.home-assistant.io/hmoffatt/{size}/88700_2.png" + "id": 1269, + "username": "ReneTode", + "name": "", + "avatar_template": "/user_avatar/community.home-assistant.io/renetode/{size}/1533_2.png" } }, { "extras": "latest", "description": "Most Recent Poster", "user": { - "id": 78711, - "username": "Astrosteve", - "name": "Steve", - "avatar_template": "/letter_avatar/astrosteve/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + "id": 46136, + "username": "chirad", + "name": "Dinoj", + "avatar_template": "/letter_avatar/chirad/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" } } ] }, { - "id": 162594, - "title": "A different take on designing a Lovelace UI", - "fancy_title": "A different take on designing a Lovelace UI", - "slug": "a-different-take-on-designing-a-lovelace-ui", - "posts_count": 641, - "reply_count": 425, - "highest_post_number": 654, + "id": 252336, + "title": "Unhealthy state", + "fancy_title": "Unhealthy state", + "slug": "unhealthy-state", + "posts_count": 89, + "reply_count": 69, + "highest_post_number": 91, "image_url": null, - "created_at": "2020-01-11T23:09:25.207Z", - "last_posted_at": "2020-10-19T23:32:15.555Z", + "created_at": "2020-12-05T20:32:00.864Z", + "last_posted_at": "2020-12-09T22:41:30.212Z", "bumped": true, - "bumped_at": "2020-10-19T23:32:15.555Z", + "bumped_at": "2020-12-09T22:41:30.212Z", "archetype": "regular", "unseen": false, - "last_read_post_number": 7, - "unread": 32, - "new_posts": 615, + "last_read_post_number": 75, + "unread": 0, + "new_posts": 16, "pinned": false, "unpinned": null, "visible": true, @@ -609,12 +401,12 @@ "archived": false, "notification_level": 2, "bookmarked": false, - "liked": false, + "liked": true, "thumbnails": null, "tags": [], - "like_count": 453, - "views": 68547, - "category_id": 9, + "like_count": 33, + "views": 946, + "category_id": 11, "featured_link": null, "has_accepted_answer": false, "posters": [ @@ -622,90 +414,179 @@ "extras": null, "description": "Original Poster", "user": { - "id": 11256, - "username": "Mattias_Persson", - "name": "Mattias Persson", - "avatar_template": "/user_avatar/community.home-assistant.io/mattias_persson/{size}/14773_2.png" + "id": 26121, + "username": "helgemor", + "name": "Helge", + "avatar_template": "/user_avatar/community.home-assistant.io/helgemor/{size}/42574_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 27634, - "username": "Jason_hill", - "name": "Jason Hill", - "avatar_template": "/user_avatar/community.home-assistant.io/jason_hill/{size}/93218_2.png" + "id": 3204, + "username": "nickrout", + "name": "Nick Rout", + "avatar_template": "/user_avatar/community.home-assistant.io/nickrout/{size}/27020_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 46782, - "username": "Martin_Pejstrup", - "name": "mpejstrup", - "avatar_template": "/user_avatar/community.home-assistant.io/martin_pejstrup/{size}/78412_2.png" + "id": 28146, + "username": "123", + "name": "Taras", + "avatar_template": "/user_avatar/community.home-assistant.io/123/{size}/44349_2.png" } }, { "extras": null, "description": "Frequent Poster", "user": { - "id": 46841, - "username": "spudje", - "name": "", - "avatar_template": "/letter_avatar/spudje/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + "id": 8361, + "username": "kanga_who", + "name": "Jason", + "avatar_template": "/user_avatar/community.home-assistant.io/kanga_who/{size}/46427_2.png" } }, { "extras": "latest", "description": "Most Recent Poster", "user": { - "id": 20924, - "username": "Diego_Santos", - "name": "Diego Santos", - "avatar_template": "/user_avatar/community.home-assistant.io/diego_santos/{size}/29096_2.png" + "id": 44704, + "username": "joselito1", + "name": "jose litomans", + "avatar_template": "/user_avatar/community.home-assistant.io/joselito1/{size}/75914_2.png" + } + } + ] + }, + { + "id": 130280, + "title": "Home Assistant Cast", + "fancy_title": "Home Assistant Cast", + "slug": "home-assistant-cast", + "posts_count": 282, + "reply_count": 206, + "highest_post_number": 289, + "image_url": null, + "created_at": "2019-08-06T15:59:00.183Z", + "last_posted_at": "2020-12-09T16:48:51.132Z", + "bumped": true, + "bumped_at": "2020-12-09T16:48:51.132Z", + "archetype": "regular", + "unseen": false, + "last_read_post_number": 88, + "unread": 0, + "new_posts": 201, + "pinned": false, + "unpinned": null, + "visible": true, + "closed": false, + "archived": false, + "notification_level": 3, + "bookmarked": false, + "liked": false, + "thumbnails": null, + "tags": [], + "like_count": 94, + "views": 29308, + "category_id": 30, + "featured_link": null, + "has_accepted_answer": false, + "posters": [ + { + "extras": null, + "description": "Original Poster", + "user": { + "id": -1, + "username": "system", + "name": "system", + "avatar_template": "/user_avatar/community.home-assistant.io/system/{size}/13_2.png" + } + }, + { + "extras": null, + "description": "Frequent Poster", + "user": { + "id": 11649, + "username": "DavidFW1960", + "name": "David", + "avatar_template": "/user_avatar/community.home-assistant.io/davidfw1960/{size}/66886_2.png" + } + }, + { + "extras": null, + "description": "Frequent Poster", + "user": { + "id": 26084, + "username": "Yoinkz", + "name": "", + "avatar_template": "/letter_avatar/yoinkz/{size}/5_70a404e2c8e633b245e797a566d32dc7.png" + } + }, + { + "extras": null, + "description": "Frequent Poster", + "user": { + "id": 3204, + "username": "nickrout", + "name": "Nick Rout", + "avatar_template": "/user_avatar/community.home-assistant.io/nickrout/{size}/27020_2.png" + } + }, + { + "extras": "latest", + "description": "Most Recent Poster", + "user": { + "id": 45396, + "username": "Wetzel402", + "name": "Cody", + "avatar_template": "/user_avatar/community.home-assistant.io/wetzel402/{size}/76694_2.png" } } ] } ], - "tags": [], - "id": 236133, - "title": "Test Topic", - "fancy_title": "Test Topic", - "posts_count": 3, - "created_at": "2020-10-16T12:20:12.580Z", - "views": 13, + "tags": [ + "blueprint", + "zha" + ], + "id": 253804, + "title": "ZHA - IKEA five button remote for lights", + "fancy_title": "ZHA - IKEA five button remote for lights", + "posts_count": 1, + "created_at": "2020-12-10T09:20:58.681Z", + "views": 4, "reply_count": 0, "like_count": 0, - "last_posted_at": "2020-10-16T12:27:53.926Z", - "visible": false, + "last_posted_at": "2020-12-10T09:20:58.974Z", + "visible": true, "closed": false, "archived": false, "has_summary": false, "archetype": "regular", - "slug": "test-topic", - "category_id": 1, - "word_count": 37, + "slug": "zha-ikea-five-button-remote-for-lights", + "category_id": 53, + "word_count": 633, "deleted_at": null, - "user_id": 3, + "user_id": 10250, "featured_link": null, "pinned_globally": false, "pinned_at": null, "pinned_until": null, - "image_url": null, + "image_url": "https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg", "draft": null, - "draft_key": "topic_236133", - "draft_sequence": 8, - "posted": true, + "draft_key": "topic_253804", + "draft_sequence": 0, + "posted": false, "unpinned": null, "pinned": false, "current_post_number": 1, - "highest_post_number": 3, - "last_read_post_number": 3, - "last_read_post_id": 1144872, + "highest_post_number": 1, + "last_read_post_number": 1, + "last_read_post_id": 1216212, "deleted_by": null, "has_deleted": false, "actions_summary": [ @@ -732,16 +613,24 @@ "bookmarked": false, "topic_timer": null, "private_topic_timer": null, - "message_bus_last_id": 5, + "message_bus_last_id": 4, "participant_count": 1, "show_read_indicator": false, - "thumbnails": null, + "thumbnails": [ + { + "max_width": null, + "max_height": null, + "width": 1400, + "height": 1400, + "url": "https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg" + } + ], "can_vote": false, "vote_count": null, "user_voted": false, "details": { - "notification_level": 3, - "notifications_reason_id": 1, + "notification_level": 1, + "notifications_reason_id": null, "can_move_posts": true, "can_edit": true, "can_delete": true, @@ -756,11 +645,11 @@ "can_remove_self_id": 3, "participants": [ { - "id": 3, - "username": "balloob", - "name": "Paulus Schoutsen", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png", - "post_count": 3, + "id": 10250, + "username": "frenck", + "name": "Franck Nijhof", + "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png", + "post_count": 1, "primary_group_name": null, "primary_group_flair_url": null, "primary_group_flair_color": null, @@ -768,16 +657,16 @@ } ], "created_by": { - "id": 3, - "username": "balloob", - "name": "Paulus Schoutsen", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png" + "id": 10250, + "username": "frenck", + "name": "Franck Nijhof", + "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png" }, "last_poster": { - "id": 3, - "username": "balloob", - "name": "Paulus Schoutsen", - "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png" + "id": 10250, + "username": "frenck", + "name": "Franck Nijhof", + "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png" } } } From 83b7439a0819d4464b4c08243c33eae728f074bf Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 10 Dec 2020 16:45:57 +0100 Subject: [PATCH 50/90] Fix importing blueprint from community (#44104) --- homeassistant/components/blueprint/importer.py | 4 ++-- tests/components/blueprint/test_importer.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index f0230aba1b7..217851df980 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -108,10 +108,10 @@ def _extract_blueprint_from_community_topic( if block_syntax not in ("auto", "yaml"): continue - block_content = block_content.strip() + block_content = html.unescape(block_content.strip()) try: - data = yaml.parse_yaml(html.unescape(block_content)) + data = yaml.parse_yaml(block_content) except HomeAssistantError: if block_syntax == "yaml": raise diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 8e674e3a9de..382363aa560 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -173,6 +173,7 @@ async def test_fetch_blueprint_from_community_url(hass, aioclient_mock, communit imported_blueprint.blueprint.metadata["source_url"] == "https://community.home-assistant.io/t/test-topic/123/2" ) + assert "gt;" not in imported_blueprint.raw_data @pytest.mark.parametrize( From 42d1644762f34e85a97415abcabdb087b6eb13bb Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 10 Dec 2020 17:54:55 +0100 Subject: [PATCH 51/90] Update frontend to 20201210.0 (#44105) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 65fe745e51d..cb52afd4b65 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20201204.0"], + "requirements": ["home-assistant-frontend==20201210.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 61d8d4a35a6..a08b3904b72 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.39.0 -home-assistant-frontend==20201204.0 +home-assistant-frontend==20201210.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index d020d105625..90f5c6267a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201204.0 +home-assistant-frontend==20201210.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7cc159f9212..766965f52f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201204.0 +home-assistant-frontend==20201210.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 3986df63dc3cfd118d0d2d82694829303aab08b8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 10 Dec 2020 21:25:50 +0100 Subject: [PATCH 52/90] Support more errors to better do retries in UniFi (#44108) --- homeassistant/components/unifi/controller.py | 14 ++++++++++++-- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifi/test_controller.py | 16 ++++++++++++++++ 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index a4435ccfecf..30b82c65c85 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -397,7 +397,12 @@ class UniFiController: await self.api.login() self.api.start_websocket() - except (asyncio.TimeoutError, aiounifi.AiounifiException): + except ( + asyncio.TimeoutError, + aiounifi.BadGateway, + aiounifi.ServiceUnavailable, + aiounifi.AiounifiException, + ): self.hass.loop.call_later(RETRY_TIMER, self.reconnect) @callback @@ -464,7 +469,12 @@ async def get_controller( LOGGER.warning("Connected to UniFi at %s but not registered.", host) raise AuthenticationRequired from err - except (asyncio.TimeoutError, aiounifi.RequestError) as err: + except ( + asyncio.TimeoutError, + aiounifi.BadGateway, + aiounifi.ServiceUnavailable, + aiounifi.RequestError, + ) as err: LOGGER.error("Error connecting to the UniFi controller at %s", host) raise CannotConnect from err diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 48c080f82f7..94b1c90f4f3 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "Ubiquiti UniFi", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==25"], + "requirements": ["aiounifi==26"], "codeowners": ["@Kane610"], "quality_scale": "platinum" } diff --git a/requirements_all.txt b/requirements_all.txt index 90f5c6267a2..2d7b7788db5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -224,7 +224,7 @@ aioshelly==0.5.1 aioswitcher==1.2.1 # homeassistant.components.unifi -aiounifi==25 +aiounifi==26 # homeassistant.components.yandex_transport aioymaps==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 766965f52f3..dd704b759b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aioshelly==0.5.1 aioswitcher==1.2.1 # homeassistant.components.unifi -aiounifi==25 +aiounifi==26 # homeassistant.components.yandex_transport aioymaps==1.1.0 diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 83732601cd6..8d5cb85bf9f 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -295,6 +295,22 @@ async def test_get_controller_login_failed(hass): await get_controller(hass, **CONTROLLER_DATA) +async def test_get_controller_controller_bad_gateway(hass): + """Check that get_controller can handle controller being unavailable.""" + with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( + "aiounifi.Controller.login", side_effect=aiounifi.BadGateway + ), pytest.raises(CannotConnect): + await get_controller(hass, **CONTROLLER_DATA) + + +async def test_get_controller_controller_service_unavailable(hass): + """Check that get_controller can handle controller being unavailable.""" + with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( + "aiounifi.Controller.login", side_effect=aiounifi.ServiceUnavailable + ), pytest.raises(CannotConnect): + await get_controller(hass, **CONTROLLER_DATA) + + async def test_get_controller_controller_unavailable(hass): """Check that get_controller can handle controller being unavailable.""" with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( From 9c6ff9af91a85fb9d6f7d21494db2a63c22b1b63 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Dec 2020 21:34:52 +0100 Subject: [PATCH 53/90] Bumped version to 1.0.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 72f422ed8fb..b1a196fe232 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 1 MINOR_VERSION = 0 -PATCH_VERSION = "0b4" +PATCH_VERSION = "0b5" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 8c6636994ffb3365325d60a7c988c67f0bf1f9ca Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Fri, 11 Dec 2020 10:53:21 -0500 Subject: [PATCH 54/90] Fix Met.no forecast precipitation (#44106) --- homeassistant/components/met/weather.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 73b9134415d..c0c8c11c644 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -21,8 +21,10 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, + LENGTH_INCHES, LENGTH_KILOMETERS, LENGTH_MILES, + LENGTH_MILLIMETERS, PRESSURE_HPA, PRESSURE_INHG, TEMP_CELSIUS, @@ -32,7 +34,14 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure -from .const import ATTR_MAP, CONDITIONS_MAP, CONF_TRACK_HOME, DOMAIN, FORECAST_MAP +from .const import ( + ATTR_FORECAST_PRECIPITATION, + ATTR_MAP, + CONDITIONS_MAP, + CONF_TRACK_HOME, + DOMAIN, + FORECAST_MAP, +) _LOGGER = logging.getLogger(__name__) @@ -221,6 +230,14 @@ class MetWeather(CoordinatorEntity, WeatherEntity): for k, v in FORECAST_MAP.items() if met_item.get(v) is not None } + if not self._is_metric: + if ATTR_FORECAST_PRECIPITATION in ha_item: + precip_inches = convert_distance( + ha_item[ATTR_FORECAST_PRECIPITATION], + LENGTH_MILLIMETERS, + LENGTH_INCHES, + ) + ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2) if ha_item.get(ATTR_FORECAST_CONDITION): ha_item[ATTR_FORECAST_CONDITION] = format_condition( ha_item[ATTR_FORECAST_CONDITION] From 8507b620168ae90bc3c6edf70544cd1ead76da51 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 12 Dec 2020 02:43:38 -0700 Subject: [PATCH 55/90] Fix inability to erase SimpliSafe code (#44137) --- .../components/simplisafe/config_flow.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index c34255bc62a..f17a2ce2e4c 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -15,6 +15,15 @@ from homeassistant.helpers import aiohttp_client from . import async_get_client_id from .const import DOMAIN, LOGGER # pylint: disable=unused-import +FULL_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_CODE): str, + } +) +PASSWORD_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) + class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a SimpliSafe config flow.""" @@ -24,15 +33,6 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the config flow.""" - self.full_data_schema = vol.Schema( - { - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_CODE): str, - } - ) - self.password_data_schema = vol.Schema({vol.Required(CONF_PASSWORD): str}) - self._code = None self._password = None self._username = None @@ -125,21 +125,19 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle re-auth completion.""" if not user_input: return self.async_show_form( - step_id="reauth_confirm", data_schema=self.password_data_schema + step_id="reauth_confirm", data_schema=PASSWORD_DATA_SCHEMA ) self._password = user_input[CONF_PASSWORD] return await self._async_login_during_step( - step_id="reauth_confirm", form_schema=self.password_data_schema + step_id="reauth_confirm", form_schema=PASSWORD_DATA_SCHEMA ) async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" if not user_input: - return self.async_show_form( - step_id="user", data_schema=self.full_data_schema - ) + return self.async_show_form(step_id="user", data_schema=FULL_DATA_SCHEMA) await self.async_set_unique_id(user_input[CONF_USERNAME]) self._abort_if_unique_id_configured() @@ -149,7 +147,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._username = user_input[CONF_USERNAME] return await self._async_login_during_step( - step_id="user", form_schema=self.full_data_schema + step_id="user", form_schema=FULL_DATA_SCHEMA ) @@ -171,7 +169,9 @@ class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow): { vol.Optional( CONF_CODE, - default=self.config_entry.options.get(CONF_CODE), + description={ + "suggested_value": self.config_entry.options.get(CONF_CODE) + }, ): str } ), From f858d6f9ec97a4871594f6388e15fccf929c7875 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sat, 12 Dec 2020 19:47:46 +0100 Subject: [PATCH 56/90] Fix upnp first discovered device is used (#44151) Co-authored-by: Martin Hjelmare --- homeassistant/components/upnp/__init__.py | 14 ++++++++++++-- homeassistant/components/upnp/config_flow.py | 1 + homeassistant/components/upnp/device.py | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 773a872f33f..c9f96a0e9d7 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -43,6 +43,8 @@ async def async_discover_and_construct( ) -> Device: """Discovery devices and construct a Device for one.""" # pylint: disable=invalid-name + _LOGGER.debug("Constructing device: %s::%s", udn, st) + discovery_infos = await Device.async_discover(hass) _LOGGER.debug("Discovered devices: %s", discovery_infos) if not discovery_infos: @@ -53,7 +55,7 @@ async def async_discover_and_construct( # Get the discovery info with specified UDN/ST. filtered = [di for di in discovery_infos if di[DISCOVERY_UDN] == udn] if st: - filtered = [di for di in discovery_infos if di[DISCOVERY_ST] == st] + filtered = [di for di in filtered if di[DISCOVERY_ST] == st] if not filtered: _LOGGER.warning( 'Wanted UPnP/IGD device with UDN/ST "%s"/"%s" not found, aborting', @@ -74,6 +76,7 @@ async def async_discover_and_construct( ) _LOGGER.info("Detected multiple UPnP/IGD devices, using: %s", device_name) + _LOGGER.debug("Constructing from discovery_info: %s", discovery_info) location = discovery_info[DISCOVERY_LOCATION] return await Device.async_create_device(hass, location) @@ -104,7 +107,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: """Set up UPnP/IGD device from a config entry.""" - _LOGGER.debug("async_setup_entry, config_entry: %s", config_entry.data) + _LOGGER.debug("Setting up config entry: %s", config_entry.unique_id) # Discover and construct. udn = config_entry.data.get(CONFIG_ENTRY_UDN) @@ -123,6 +126,11 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) # Ensure entry has a unique_id. if not config_entry.unique_id: + _LOGGER.debug( + "Setting unique_id: %s, for config_entry: %s", + device.unique_id, + config_entry, + ) hass.config_entries.async_update_entry( entry=config_entry, unique_id=device.unique_id, @@ -152,6 +160,8 @@ async def async_unload_entry( hass: HomeAssistantType, config_entry: ConfigEntry ) -> bool: """Unload a UPnP/IGD device from a config entry.""" + _LOGGER.debug("Unloading config entry: %s", config_entry.unique_id) + udn = config_entry.data.get(CONFIG_ENTRY_UDN) if udn in hass.data[DOMAIN][DOMAIN_DEVICES]: del hass.data[DOMAIN][DOMAIN_DEVICES][udn] diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 72efc4ffd55..7b20c7709a0 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -154,6 +154,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() # Store discovery. + _LOGGER.debug("New discovery, continuing") name = discovery_info.get("friendlyName", "") discovery = { DISCOVERY_UDN: udn, diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 5f29043a1fe..6bc497170ca 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -109,7 +109,7 @@ class Device: def __str__(self) -> str: """Get string representation.""" - return f"IGD Device: {self.name}/{self.udn}" + return f"IGD Device: {self.name}/{self.udn}::{self.device_type}" async def async_get_traffic_data(self) -> Mapping[str, any]: """ From 7c2a2b08c703f82ca44de60802469d75bfc9a3e7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 12 Dec 2020 21:34:16 +0100 Subject: [PATCH 57/90] Updated frontend to 20201212.0 (#44154) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index cb52afd4b65..caf309e6718 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20201210.0"], + "requirements": ["home-assistant-frontend==20201212.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a08b3904b72..eeb104ff84c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.39.0 -home-assistant-frontend==20201210.0 +home-assistant-frontend==20201212.0 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index 2d7b7788db5..9a48f2623b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201210.0 +home-assistant-frontend==20201212.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd704b759b5..3dff4609143 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20201210.0 +home-assistant-frontend==20201212.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From c02be99ab767fdf7d304f0dff5b91283ac9dcd2c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Dec 2020 20:35:58 +0000 Subject: [PATCH 58/90] Bumped version to 1.0.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b1a196fe232..4335c93f86a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 1 MINOR_VERSION = 0 -PATCH_VERSION = "0b5" +PATCH_VERSION = "0b6" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 17cd00ba836e5058b2a77bbfb9c18e04a9e82eca Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Dec 2020 22:24:16 +0100 Subject: [PATCH 59/90] Remove invalidation_version from deprecated (#44156) * Remove invalidation_version from deprecated. We don't follow up and just hurts releases * Revert change to ZHA --- .../components/arcam_fmj/__init__.py | 2 +- homeassistant/components/automation/config.py | 2 +- homeassistant/components/canary/camera.py | 2 +- .../components/cloudflare/__init__.py | 8 +- homeassistant/components/daikin/__init__.py | 2 +- homeassistant/components/directv/__init__.py | 2 +- .../components/flunearyou/__init__.py | 2 +- homeassistant/components/hyperion/light.py | 6 +- homeassistant/components/local_ip/__init__.py | 2 +- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/notion/__init__.py | 2 +- .../components/rainmachine/__init__.py | 2 +- homeassistant/components/roku/__init__.py | 2 +- homeassistant/components/sentry/__init__.py | 2 +- .../components/simplisafe/__init__.py | 2 +- homeassistant/components/withings/__init__.py | 2 +- homeassistant/components/zha/core/device.py | 2 + homeassistant/helpers/config_validation.py | 39 +--- tests/helpers/test_config_validation.py | 174 ------------------ tests/test_config.py | 2 +- 20 files changed, 25 insertions(+), 234 deletions(-) diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index 0875e094352..0175dfd6586 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -23,7 +23,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.115") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def _await_cancel(task): diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 8a13334bc6d..9c26f3552aa 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -32,7 +32,7 @@ from .helpers import async_get_blueprints _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA]) PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_HIDE_ENTITY, invalidation_version="0.110"), + cv.deprecated(CONF_HIDE_ENTITY), script.make_script_schema( { # str on purpose diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index c3fd2a6ff00..fd2f08c1488 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -30,7 +30,7 @@ from .coordinator import CanaryDataUpdateCoordinator MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90) PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_FFMPEG_ARGUMENTS, invalidation_version="0.118"), + cv.deprecated(CONF_FFMPEG_ARGUMENTS), PLATFORM_SCHEMA.extend( { vol.Optional( diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index 3ebb919393a..446890887c1 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -33,10 +33,10 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - cv.deprecated(CONF_EMAIL, invalidation_version="0.119"), - cv.deprecated(CONF_API_KEY, invalidation_version="0.119"), - cv.deprecated(CONF_ZONE, invalidation_version="0.119"), - cv.deprecated(CONF_RECORDS, invalidation_version="0.119"), + cv.deprecated(CONF_EMAIL), + cv.deprecated(CONF_API_KEY), + cv.deprecated(CONF_ZONE), + cv.deprecated(CONF_RECORDS), vol.Schema( { vol.Optional(CONF_EMAIL): cv.string, diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 7b9c1ded673..b4950b8b05b 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -30,7 +30,7 @@ COMPONENT_TYPES = ["climate", "sensor", "switch"] CONFIG_SCHEMA = vol.Schema( vol.All( - cv.deprecated(DOMAIN, invalidation_version="0.113.0"), + cv.deprecated(DOMAIN), { DOMAIN: vol.Schema( { diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index 59682178d40..22a97b9e82e 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -22,7 +22,7 @@ from .const import ( DOMAIN, ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.120") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["media_player", "remote"] SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 7399dd3847d..46442f112b6 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -20,7 +20,7 @@ from .const import ( DEFAULT_UPDATE_INTERVAL = timedelta(minutes=30) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["sensor"] diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index b8e9040f7ce..5aa087c0515 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -80,13 +80,13 @@ SUPPORT_HYPERION = SUPPORT_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT # Usage of YAML for configuration of the Hyperion component is deprecated. PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_HDMI_PRIORITY, invalidation_version="0.118"), + cv.deprecated(CONF_HDMI_PRIORITY), cv.deprecated(CONF_HOST), cv.deprecated(CONF_PORT), - cv.deprecated(CONF_DEFAULT_COLOR, invalidation_version="0.118"), + cv.deprecated(CONF_DEFAULT_COLOR), cv.deprecated(CONF_NAME), cv.deprecated(CONF_PRIORITY), - cv.deprecated(CONF_EFFECT_LIST, invalidation_version="0.118"), + cv.deprecated(CONF_EFFECT_LIST), PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py index f787c028762..637520aa30c 100644 --- a/homeassistant/components/local_ip/__init__.py +++ b/homeassistant/components/local_ip/__init__.py @@ -11,7 +11,7 @@ from .const import DOMAIN, PLATFORM CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - cv.deprecated(CONF_NAME, invalidation_version="0.110"), + cv.deprecated(CONF_NAME), vol.Schema({vol.Optional(CONF_NAME, default=DOMAIN): cv.string}), ) }, diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 73caf023ef6..cced3670cca 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -191,7 +191,7 @@ def embedded_broker_deprecated(value): CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - cv.deprecated(CONF_TLS_VERSION, invalidation_version="0.115"), + cv.deprecated(CONF_TLS_VERSION), vol.Schema( { vol.Optional(CONF_CLIENT_ID): cv.string, diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 561f3edf896..88da19f5ab2 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -30,7 +30,7 @@ ATTR_SYSTEM_NAME = "system_name" DEFAULT_ATTRIBUTION = "Data provided by Notion" DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup(hass: HomeAssistant, config: dict) -> bool: diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index a520772ff77..41c56e38db6 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -74,7 +74,7 @@ SERVICE_STOP_PROGRAM_SCHEMA = vol.Schema( SERVICE_STOP_ZONE_SCHEMA = vol.Schema({vol.Required(CONF_ZONE_ID): cv.positive_int}) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["binary_sensor", "sensor", "switch"] diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 739e345a637..af2e0ee946f 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -30,7 +30,7 @@ from .const import ( DOMAIN, ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.120") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN] SCAN_INTERVAL = timedelta(seconds=15) diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index eecac0281e6..6be02b9ba5e 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -33,7 +33,7 @@ from .const import ( ENTITY_COMPONENTS, ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.117") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) LOGGER_INFO_REGEX = re.compile(r"^(\w+)\.?(\w+)?\.?(\w+)?\.?(\w+)?(?:\..*)?$") diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 2430aad43cf..89f5c40b1ff 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -138,7 +138,7 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( } ) -CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119") +CONFIG_SCHEMA = cv.deprecated(DOMAIN) @callback diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 94795a10c83..c6f420d172a 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -40,7 +40,7 @@ DOMAIN = const.DOMAIN CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - cv.deprecated(const.CONF_PROFILES, invalidation_version="0.114"), + cv.deprecated(const.CONF_PROFILES), vol.Schema( { vol.Required(CONF_CLIENT_ID): vol.All(cv.string, vol.Length(min=1)), diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 81b522308ff..cd3b1bd93ce 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -254,8 +254,10 @@ class ZHADevice(LogMixin): "device_event_type": "device_offline" } } + if hasattr(self._zigpy_device, "device_automation_triggers"): triggers.update(self._zigpy_device.device_automation_triggers) + return triggers @property diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index dea8deec715..0513c5c6e7e 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -28,7 +28,6 @@ from typing import ( from urllib.parse import urlparse from uuid import UUID -from pkg_resources import parse_version import voluptuous as vol import voluptuous_serialize @@ -80,7 +79,6 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, WEEKDAYS, - __version__, ) from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError @@ -712,7 +710,6 @@ class multi_select: def deprecated( key: str, replacement_key: Optional[str] = None, - invalidation_version: Optional[str] = None, default: Optional[Any] = None, ) -> Callable[[Dict], Dict]: """ @@ -725,8 +722,6 @@ def deprecated( - No warning if only replacement_key provided - No warning if neither key nor replacement_key are provided - Adds replacement_key with default value in this case - - Once the invalidation_version is crossed, raises vol.Invalid if key - is detected """ module = inspect.getmodule(inspect.stack()[1][0]) if module is not None: @@ -737,56 +732,24 @@ def deprecated( # https://github.com/home-assistant/core/issues/24982 module_name = __name__ - if replacement_key and invalidation_version: - warning = ( - "The '{key}' option is deprecated," - " please replace it with '{replacement_key}'." - " This option {invalidation_status} invalid in version" - " {invalidation_version}" - ) - elif replacement_key: + if replacement_key: warning = ( "The '{key}' option is deprecated," " please replace it with '{replacement_key}'" ) - elif invalidation_version: - warning = ( - "The '{key}' option is deprecated," - " please remove it from your configuration." - " This option {invalidation_status} invalid in version" - " {invalidation_version}" - ) else: warning = ( "The '{key}' option is deprecated," " please remove it from your configuration" ) - def check_for_invalid_version() -> None: - """Raise error if current version has reached invalidation.""" - if not invalidation_version: - return - - if parse_version(__version__) >= parse_version(invalidation_version): - raise vol.Invalid( - warning.format( - key=key, - replacement_key=replacement_key, - invalidation_status="became", - invalidation_version=invalidation_version, - ) - ) - def validator(config: Dict) -> Dict: """Check if key is in config and log warning.""" if key in config: - check_for_invalid_version() KeywordStyleAdapter(logging.getLogger(module_name)).warning( warning, key=key, replacement_key=replacement_key, - invalidation_status="will become", - invalidation_version=invalidation_version, ) value = config[key] diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 480b2280afe..5d907408b61 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -698,116 +698,6 @@ def test_deprecated_with_replacement_key(caplog, schema): assert test_data == output -def test_deprecated_with_invalidation_version(caplog, schema, version): - """ - Test deprecation behaves correctly with only an invalidation_version. - - Expected behavior: - - Outputs the appropriate deprecation warning if key is detected - - Processes schema without changing any values - - No warning or difference in output if key is not provided - - Once the invalidation_version is crossed, raises vol.Invalid if key - is detected - """ - deprecated_schema = vol.All( - cv.deprecated("mars", invalidation_version="9999.99.9"), schema - ) - - message = ( - "The 'mars' option is deprecated, " - "please remove it from your configuration. " - "This option will become invalid in version 9999.99.9" - ) - - test_data = {"mars": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 1 - assert message in caplog.text - assert test_data == output - - caplog.clear() - assert len(caplog.records) == 0 - - test_data = {"venus": False} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert test_data == output - - invalidated_schema = vol.All( - cv.deprecated("mars", invalidation_version="0.1.0"), schema - ) - test_data = {"mars": True} - with pytest.raises(vol.MultipleInvalid) as exc_info: - invalidated_schema(test_data) - assert str(exc_info.value) == ( - "The 'mars' option is deprecated, " - "please remove it from your configuration. This option became " - "invalid in version 0.1.0" - ) - - -def test_deprecated_with_replacement_key_and_invalidation_version( - caplog, schema, version -): - """ - Test deprecation behaves with a replacement key & invalidation_version. - - Expected behavior: - - Outputs the appropriate deprecation warning if key is detected - - Processes schema moving the value from key to replacement_key - - Processes schema changing nothing if only replacement_key provided - - No warning if only replacement_key provided - - No warning or difference in output if neither key nor - replacement_key are provided - - Once the invalidation_version is crossed, raises vol.Invalid if key - is detected - """ - deprecated_schema = vol.All( - cv.deprecated( - "mars", replacement_key="jupiter", invalidation_version="9999.99.9" - ), - schema, - ) - - warning = ( - "The 'mars' option is deprecated, " - "please replace it with 'jupiter'. This option will become " - "invalid in version 9999.99.9" - ) - - test_data = {"mars": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 1 - assert warning in caplog.text - assert {"jupiter": True} == output - - caplog.clear() - assert len(caplog.records) == 0 - - test_data = {"jupiter": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert test_data == output - - test_data = {"venus": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert test_data == output - - invalidated_schema = vol.All( - cv.deprecated("mars", replacement_key="jupiter", invalidation_version="0.1.0"), - schema, - ) - test_data = {"mars": True} - with pytest.raises(vol.MultipleInvalid) as exc_info: - invalidated_schema(test_data) - assert str(exc_info.value) == ( - "The 'mars' option is deprecated, " - "please replace it with 'jupiter'. This option became " - "invalid in version 0.1.0" - ) - - def test_deprecated_with_default(caplog, schema): """ Test deprecation behaves correctly with a default value. @@ -894,69 +784,6 @@ def test_deprecated_with_replacement_key_and_default(caplog, schema): assert {"jupiter": True} == output -def test_deprecated_with_replacement_key_invalidation_version_default( - caplog, schema, version -): - """ - Test deprecation with a replacement key, invalidation_version & default. - - Expected behavior: - - Outputs the appropriate deprecation warning if key is detected - - Processes schema moving the value from key to replacement_key - - Processes schema changing nothing if only replacement_key provided - - No warning if only replacement_key provided - - No warning if neither key nor replacement_key are provided - - Adds replacement_key with default value in this case - - Once the invalidation_version is crossed, raises vol.Invalid if key - is detected - """ - deprecated_schema = vol.All( - cv.deprecated( - "mars", - replacement_key="jupiter", - invalidation_version="9999.99.9", - default=False, - ), - schema, - ) - - test_data = {"mars": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 1 - assert ( - "The 'mars' option is deprecated, " - "please replace it with 'jupiter'. This option will become " - "invalid in version 9999.99.9" - ) in caplog.text - assert {"jupiter": True} == output - - caplog.clear() - assert len(caplog.records) == 0 - - test_data = {"jupiter": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert test_data == output - - test_data = {"venus": True} - output = deprecated_schema(test_data.copy()) - assert len(caplog.records) == 0 - assert {"venus": True, "jupiter": False} == output - - invalidated_schema = vol.All( - cv.deprecated("mars", replacement_key="jupiter", invalidation_version="0.1.0"), - schema, - ) - test_data = {"mars": True} - with pytest.raises(vol.MultipleInvalid) as exc_info: - invalidated_schema(test_data) - assert str(exc_info.value) == ( - "The 'mars' option is deprecated, " - "please replace it with 'jupiter'. This option became " - "invalid in version 0.1.0" - ) - - def test_deprecated_cant_find_module(): """Test if the current module cannot be inspected.""" with patch("inspect.getmodule", return_value=None): @@ -964,7 +791,6 @@ def test_deprecated_cant_find_module(): cv.deprecated( "mars", replacement_key="jupiter", - invalidation_version="1.0.0", default=False, ) diff --git a/tests/test_config.py b/tests/test_config.py index bfda156f2b7..931b672d01b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1116,7 +1116,7 @@ async def test_component_config_exceptions(hass, caplog): ("non_existing", vol.Schema({"zone": int}), None), ("zone", vol.Schema({}), None), ("plex", vol.Schema(vol.All({"plex": {"host": str}})), "dict"), - ("openuv", cv.deprecated("openuv", invalidation_version="0.115"), None), + ("openuv", cv.deprecated("openuv"), None), ], ) def test_identify_config_schema(domain, schema, expected): From 9c45874206be5a7af35eb7ac248450c8c81b9bf0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Dec 2020 05:36:17 -0600 Subject: [PATCH 60/90] Bump zeroconf to 0.28.7 to fix thread safety (#44160) Service registration was not thread safe --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 1848c890573..753ac2a2441 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.28.6"], + "requirements": ["zeroconf==0.28.7"], "dependencies": ["api"], "codeowners": ["@bdraco"], "quality_scale": "internal" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index eeb104ff84c..7aa59bd5836 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ sqlalchemy==1.3.20 voluptuous-serialize==2.4.0 voluptuous==0.12.0 yarl==1.4.2 -zeroconf==0.28.6 +zeroconf==0.28.7 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 9a48f2623b6..0a0af294a85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2342,7 +2342,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.28.6 +zeroconf==0.28.7 # homeassistant.components.zha zha-quirks==0.0.48 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3dff4609143..4a416873fba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1141,7 +1141,7 @@ yeelight==0.5.4 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.28.6 +zeroconf==0.28.7 # homeassistant.components.zha zha-quirks==0.0.48 From 3981592ccd9c5fc6db708ac8f60dd94ec83fe531 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 13 Dec 2020 08:43:33 -0500 Subject: [PATCH 61/90] Bump the ZHA quirks lib to 0.0.49 (#44173) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index f1821c9e480..bcaa4038de1 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.21.0", "pyserial==3.4", "pyserial-asyncio==0.4", - "zha-quirks==0.0.48", + "zha-quirks==0.0.49", "zigpy-cc==0.5.2", "zigpy-deconz==0.11.0", "zigpy==0.28.2", diff --git a/requirements_all.txt b/requirements_all.txt index 0a0af294a85..858b747b2ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2345,7 +2345,7 @@ zengge==0.2 zeroconf==0.28.7 # homeassistant.components.zha -zha-quirks==0.0.48 +zha-quirks==0.0.49 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a416873fba..9dc1bd8d860 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1144,7 +1144,7 @@ zeep[async]==4.0.0 zeroconf==0.28.7 # homeassistant.components.zha -zha-quirks==0.0.48 +zha-quirks==0.0.49 # homeassistant.components.zha zigpy-cc==0.5.2 From 1ac5ecbe694fe1c4dacf8c6d11cd3de526754423 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Dec 2020 15:04:00 +0100 Subject: [PATCH 62/90] Bumped version to 1.0.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4335c93f86a..a5ffbff4cd8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 1 MINOR_VERSION = 0 -PATCH_VERSION = "0b6" +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 623826261bd74918815692f326bbd79d6f24d97e Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Thu, 10 Dec 2020 21:50:51 +0100 Subject: [PATCH 63/90] Remove deprecated CONF_ALLOW_UNLOCK, CONF_API_KEY from Google Assistant (#44087) * Remove deprecated CONF_ALLOW_UNLOCK, CONF_API_KEY * Use vol.Remove() to prevent setup fail * Keep constants --- .../components/google_assistant/__init__.py | 14 +++++----- .../components/google_assistant/const.py | 2 -- .../components/google_assistant/http.py | 26 +------------------ 3 files changed, 8 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 0afd66c10aa..8f4ee3b51c4 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -11,8 +11,6 @@ from homeassistant.helpers import config_validation as cv from .const import ( CONF_ALIASES, - CONF_ALLOW_UNLOCK, - CONF_API_KEY, CONF_CLIENT_EMAIL, CONF_ENTITY_CONFIG, CONF_EXPOSE, @@ -36,6 +34,9 @@ from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401, is _LOGGER = logging.getLogger(__name__) +CONF_ALLOW_UNLOCK = "allow_unlock" +CONF_API_KEY = "api_key" + ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME): cv.string, @@ -61,8 +62,6 @@ def _check_report_state(data): GOOGLE_ASSISTANT_SCHEMA = vol.All( - cv.deprecated(CONF_ALLOW_UNLOCK, invalidation_version="0.95"), - cv.deprecated(CONF_API_KEY, invalidation_version="0.105"), vol.Schema( { vol.Required(CONF_PROJECT_ID): cv.string, @@ -72,13 +71,14 @@ GOOGLE_ASSISTANT_SCHEMA = vol.All( vol.Optional( CONF_EXPOSED_DOMAINS, default=DEFAULT_EXPOSED_DOMAINS ): cv.ensure_list, - vol.Optional(CONF_API_KEY): cv.string, vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA}, - vol.Optional(CONF_ALLOW_UNLOCK): cv.boolean, # str on purpose, makes sure it is configured correctly. vol.Optional(CONF_SECURE_DEVICES_PIN): str, vol.Optional(CONF_REPORT_STATE, default=False): cv.boolean, vol.Optional(CONF_SERVICE_ACCOUNT): GOOGLE_SERVICE_ACCOUNT, + # deprecated configuration options + vol.Remove(CONF_ALLOW_UNLOCK): cv.boolean, + vol.Remove(CONF_API_KEY): cv.string, }, extra=vol.PREVENT_EXTRA, ), @@ -113,7 +113,7 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): await google_config.async_sync_entities(agent_user_id) # Register service only if key is provided - if CONF_API_KEY in config or CONF_SERVICE_ACCOUNT in config: + if CONF_SERVICE_ACCOUNT in config: hass.services.async_register( DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler ) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 47ceabb20e8..d6badf2e7ba 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -30,9 +30,7 @@ CONF_EXPOSE_BY_DEFAULT = "expose_by_default" CONF_EXPOSED_DOMAINS = "exposed_domains" CONF_PROJECT_ID = "project_id" CONF_ALIASES = "aliases" -CONF_API_KEY = "api_key" CONF_ROOM_HINT = "room" -CONF_ALLOW_UNLOCK = "allow_unlock" CONF_SECURE_DEVICES_PIN = "secure_devices_pin" CONF_REPORT_STATE = "report_state" CONF_SERVICE_ACCOUNT = "service_account" diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 4bf0df8b933..5cf1cb14379 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -19,7 +19,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util from .const import ( - CONF_API_KEY, CONF_CLIENT_EMAIL, CONF_ENTITY_CONFIG, CONF_EXPOSE, @@ -135,11 +134,7 @@ class GoogleConfig(AbstractConfig): return True async def _async_request_sync_devices(self, agent_user_id: str): - if CONF_API_KEY in self._config: - await self.async_call_homegraph_api_key( - REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} - ) - elif CONF_SERVICE_ACCOUNT in self._config: + if CONF_SERVICE_ACCOUNT in self._config: await self.async_call_homegraph_api( REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} ) @@ -164,25 +159,6 @@ class GoogleConfig(AbstractConfig): self._access_token = token["access_token"] self._access_token_renew = now + timedelta(seconds=token["expires_in"]) - async def async_call_homegraph_api_key(self, url, data): - """Call a homegraph api with api key authentication.""" - websession = async_get_clientsession(self.hass) - try: - res = await websession.post( - url, params={"key": self._config.get(CONF_API_KEY)}, json=data - ) - _LOGGER.debug( - "Response on %s with data %s was %s", url, data, await res.text() - ) - res.raise_for_status() - return res.status - except ClientResponseError as error: - _LOGGER.error("Request for %s failed: %d", url, error.status) - return error.status - except (asyncio.TimeoutError, ClientError): - _LOGGER.error("Could not contact %s", url) - return HTTP_INTERNAL_SERVER_ERROR - async def async_call_homegraph_api(self, url, data): """Call a homegraph api with authentication.""" session = async_get_clientsession(self.hass) From 277b916fcdfd7eab4c0507d7136449d287250715 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Dec 2020 22:13:58 +0100 Subject: [PATCH 64/90] 2020.12.0 --- homeassistant/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a5ffbff4cd8..1e7d243b9ad 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" -MAJOR_VERSION = 1 -MINOR_VERSION = 0 +MAJOR_VERSION = 2020 +MINOR_VERSION = 12 PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" From 70bd998d7cfe2f963c35b463756580441d0dd61c Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 14 Dec 2020 13:51:28 -0800 Subject: [PATCH 65/90] Bump envoy_reader version to 0.17.3 (#44205) * Bump envoy_reader version to 0.17.0rc0 * Fixing version number --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index e6ab8dbf6a9..b339013a69f 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -2,7 +2,7 @@ "domain": "enphase_envoy", "name": "Enphase Envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", - "requirements": ["envoy_reader==0.17.0"], + "requirements": ["envoy_reader==0.17.3"], "codeowners": [ "@gtdiehl" ] diff --git a/requirements_all.txt b/requirements_all.txt index 858b747b2ae..1f417c7a402 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -559,7 +559,7 @@ env_canada==0.2.4 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.17.0 +envoy_reader==0.17.3 # homeassistant.components.season ephem==3.7.7.0 From 150ce05e2667aaabbf6eae6b41965412bc03eb4e Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Tue, 15 Dec 2020 04:45:24 +0100 Subject: [PATCH 66/90] Bump dsmr-parser to 0.25 (#44223) * Bump version in manifest * Update requirements_all.txt * Update requirements_test_all.txt --- homeassistant/components/dsmr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index fdbba4212f6..c3f6aa4dea3 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dsmr", "name": "DSMR Slimme Meter", "documentation": "https://www.home-assistant.io/integrations/dsmr", - "requirements": ["dsmr_parser==0.23"], + "requirements": ["dsmr_parser==0.25"], "codeowners": ["@Robbie1221"], "config_flow": false } diff --git a/requirements_all.txt b/requirements_all.txt index 1f417c7a402..7e37bc0a8e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -508,7 +508,7 @@ doorbirdpy==2.1.0 dovado==0.4.1 # homeassistant.components.dsmr -dsmr_parser==0.23 +dsmr_parser==0.25 # homeassistant.components.dwd_weather_warnings dwdwfsapi==1.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9dc1bd8d860..fd6dc5c612a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -269,7 +269,7 @@ distro==1.5.0 doorbirdpy==2.1.0 # homeassistant.components.dsmr -dsmr_parser==0.23 +dsmr_parser==0.25 # homeassistant.components.dynalite dynalite_devices==0.1.46 From 43ef5bab182b96a7f04a9768f77504a725030c39 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 14 Dec 2020 13:03:25 -0700 Subject: [PATCH 67/90] Fix unhandled KeyError in Recollect Waste (#44224) --- homeassistant/components/recollect_waste/manifest.json | 2 +- homeassistant/components/recollect_waste/sensor.py | 6 ++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json index 4e6b71d59b7..dc8a85ce2aa 100644 --- a/homeassistant/components/recollect_waste/manifest.json +++ b/homeassistant/components/recollect_waste/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/recollect_waste", "requirements": [ - "aiorecollect==0.2.2" + "aiorecollect==1.0.1" ], "codeowners": [ "@bachya" diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 7ce75b1e3fa..53304c93218 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -120,9 +120,11 @@ class RecollectWasteSensor(CoordinatorEntity): self._state = pickup_event.date self._attributes.update( { - ATTR_PICKUP_TYPES: pickup_event.pickup_types, + ATTR_PICKUP_TYPES: [t.name for t in pickup_event.pickup_types], ATTR_AREA_NAME: pickup_event.area_name, - ATTR_NEXT_PICKUP_TYPES: next_pickup_event.pickup_types, + ATTR_NEXT_PICKUP_TYPES: [ + t.name for t in next_pickup_event.pickup_types + ], ATTR_NEXT_PICKUP_DATE: next_date, } ) diff --git a/requirements_all.txt b/requirements_all.txt index 7e37bc0a8e6..10746f93d63 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -215,7 +215,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.recollect_waste -aiorecollect==0.2.2 +aiorecollect==1.0.1 # homeassistant.components.shelly aioshelly==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd6dc5c612a..830f2cd67d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.recollect_waste -aiorecollect==0.2.2 +aiorecollect==1.0.1 # homeassistant.components.shelly aioshelly==0.5.1 From 07df3b95654711883b273ce649b9cb6bd20f7c1a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Dec 2020 17:23:00 +0100 Subject: [PATCH 68/90] Bump hatasmota to 0.1.6 (#44226) --- homeassistant/components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index a4c6f77fc13..5b298d44ce0 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota (beta)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.1.4"], + "requirements": ["hatasmota==0.1.6"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index 10746f93d63..3b1fa3f76e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ hass-nabucasa==0.39.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.1.4 +hatasmota==0.1.6 # homeassistant.components.jewish_calendar hdate==0.9.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 830f2cd67d4..e8d3dfedfc8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -376,7 +376,7 @@ hangups==0.4.11 hass-nabucasa==0.39.0 # homeassistant.components.tasmota -hatasmota==0.1.4 +hatasmota==0.1.6 # homeassistant.components.jewish_calendar hdate==0.9.12 From 3752fa3123e755320608bd2ae9c41a8e30515d76 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Dec 2020 11:00:22 +0100 Subject: [PATCH 69/90] Remove Home Assistant Cast user when removing entry (#44228) * Remove Home Assistant Cast user when removing entry * Fix test * Fix test --- homeassistant/components/cast/__init__.py | 5 ++++ .../components/cast/home_assistant_cast.py | 11 +++++++ .../cast/test_home_assistant_cast.py | 30 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 4dfb58ef3b7..49cec207764 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -29,3 +29,8 @@ async def async_setup_entry(hass, entry: config_entries.ConfigEntry): hass.config_entries.async_forward_entry_setup(entry, "media_player") ) return True + + +async def async_remove_entry(hass, entry): + """Remove Home Assistant Cast user.""" + await home_assistant_cast.async_remove_user(hass, entry) diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index 28672ef409c..3edc1ce2cde 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -72,3 +72,14 @@ async def async_setup_ha_cast( } ), ) + + +async def async_remove_user( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +): + """Remove Home Assistant Cast user.""" + user_id: Optional[str] = entry.data.get("user_id") + + if user_id is not None: + user = await hass.auth.async_get_user(user_id) + await hass.auth.async_remove_user(user) diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index 2fff760bb70..8ddb6e82eda 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -1,5 +1,6 @@ """Test Home Assistant Cast.""" +from homeassistant import config_entries from homeassistant.components.cast import home_assistant_cast from homeassistant.config import async_process_ha_core_config @@ -86,3 +87,32 @@ async def test_use_cloud_url(hass, mock_zeroconf): assert len(calls) == 1 controller = calls[0][0] assert controller.hass_url == "https://something.nabu.casa" + + +async def test_remove_entry(hass, mock_zeroconf): + """Test removing config entry removes user.""" + entry = MockConfigEntry( + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + data={}, + domain="cast", + title="Google Cast", + ) + + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.cast.media_player._async_setup_platform" + ), patch( + "pychromecast.discovery.discover_chromecasts", return_value=(True, None) + ), patch( + "pychromecast.discovery.stop_discovery" + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert "cast" in hass.config.components + + user_id = entry.data.get("user_id") + assert await hass.auth.async_get_user(user_id) + + assert await hass.config_entries.async_remove(entry.entry_id) + assert not await hass.auth.async_get_user(user_id) From 3ab0b63540e0aebf836947ab5196b1cf6b0aaf32 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 15 Dec 2020 10:44:28 -0500 Subject: [PATCH 70/90] Default smartenergy multiplier and divisor (#44257) --- homeassistant/components/zha/core/channels/smartenergy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 792b9413294..87c22b160f4 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -89,12 +89,12 @@ class Metering(ZigbeeChannel): @property def divisor(self) -> int: """Return divisor for the value.""" - return self.cluster.get("divisor") + return self.cluster.get("divisor") or 1 @property def multiplier(self) -> int: """Return multiplier for the value.""" - return self.cluster.get("multiplier") + return self.cluster.get("multiplier") or 1 async def async_configure(self) -> None: """Configure channel.""" From 3a41878a8c0f758d22fa364d7535fbea53b57a31 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Dec 2020 11:18:50 +0100 Subject: [PATCH 71/90] Fix setting timestamp on input_datetime (#44274) --- .../components/input_datetime/__init__.py | 8 ++---- tests/components/input_datetime/test_init.py | 28 +++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index aa1f0b8814a..195e4c2242e 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -365,9 +365,7 @@ class InputDatetime(RestoreEntity): def async_set_datetime(self, date=None, time=None, datetime=None, timestamp=None): """Set a new date / time.""" if timestamp: - datetime = dt_util.as_local(dt_util.utc_from_timestamp(timestamp)).replace( - tzinfo=None - ) + datetime = dt_util.as_local(dt_util.utc_from_timestamp(timestamp)) if datetime: date = datetime.date() @@ -388,8 +386,8 @@ class InputDatetime(RestoreEntity): if not time: time = self._current_datetime.time() - self._current_datetime = py_datetime.datetime.combine(date, time).replace( - tzinfo=dt_util.DEFAULT_TIME_ZONE + self._current_datetime = dt_util.DEFAULT_TIME_ZONE.localize( + py_datetime.datetime.combine(date, time) ) self.async_write_ha_state() diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index d40a88e3f43..a336ef82363 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -697,6 +697,15 @@ async def test_timestamp(hass): ).strftime(FMT_DATETIME) == "2020-12-13 10:00:00" ) + # Use datetime.datetime.fromtimestamp + assert ( + dt_util.as_local( + datetime.datetime.fromtimestamp( + state_without_tz.attributes[ATTR_TIMESTAMP] + ) + ).strftime(FMT_DATETIME) + == "2020-12-13 10:00:00" + ) # Test initial time sets timestamp correctly. state_time = hass.states.get("input_datetime.test_time_initial") @@ -704,5 +713,24 @@ async def test_timestamp(hass): assert state_time.state == "10:00:00" assert state_time.attributes[ATTR_TIMESTAMP] == 10 * 60 * 60 + # Test that setting the timestamp of an entity works. + await hass.services.async_call( + DOMAIN, + "set_datetime", + { + ATTR_ENTITY_ID: "input_datetime.test_datetime_initial_with_tz", + ATTR_TIMESTAMP: state_without_tz.attributes[ATTR_TIMESTAMP], + }, + blocking=True, + ) + state_with_tz_updated = hass.states.get( + "input_datetime.test_datetime_initial_with_tz" + ) + assert state_with_tz_updated.state == "2020-12-13 10:00:00" + assert ( + state_with_tz_updated.attributes[ATTR_TIMESTAMP] + == state_without_tz.attributes[ATTR_TIMESTAMP] + ) + finally: dt_util.set_default_time_zone(ORIG_TIMEZONE) From 3a82aecd38ab1d86fb2ade556b6db3cd894a7980 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 16 Dec 2020 22:28:59 +0200 Subject: [PATCH 72/90] Fix Shelly devices missing properties field (#44279) --- .../components/shelly/config_flow.py | 8 +---- tests/components/shelly/test_config_flow.py | 30 +------------------ 2 files changed, 2 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 88e01f04bc5..e23e9561c77 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -27,12 +27,6 @@ HOST_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) HTTP_CONNECT_ERRORS = (asyncio.TimeoutError, aiohttp.ClientError) -def _remove_prefix(shelly_str): - if shelly_str.startswith("shellyswitch"): - return shelly_str[6:] - return shelly_str - - async def validate_input(hass: core.HomeAssistant, host, data): """Validate the user input allows us to connect. @@ -159,7 +153,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.host = zeroconf_info["host"] # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { - "name": _remove_prefix(zeroconf_info["properties"]["id"]) + "name": zeroconf_info.get("name", "").split(".")[0] } return await self.async_step_confirm_discovery() diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 1796847bd74..fe6a567e32e 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -20,11 +20,6 @@ DISCOVERY_INFO = { "name": "shelly1pm-12345", "properties": {"id": "shelly1pm-12345"}, } -SWITCH25_DISCOVERY_INFO = { - "host": "1.1.1.1", - "name": "shellyswitch25-12345", - "properties": {"id": "shellyswitch25-12345"}, -} async def test_form(hass): @@ -67,7 +62,7 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_title_without_name_and_prefix(hass): +async def test_title_without_name(hass): """Test we set the title to the hostname when the device doesn't have a name.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -330,29 +325,6 @@ async def test_zeroconf(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_zeroconf_with_switch_prefix(hass): - """Test we get remove shelly from the prefix.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - with patch( - "aioshelly.get_info", - return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - data=SWITCH25_DISCOVERY_INFO, - context={"source": config_entries.SOURCE_ZEROCONF}, - ) - assert result["type"] == "form" - assert result["errors"] == {} - context = next( - flow["context"] - for flow in hass.config_entries.flow.async_progress() - if flow["flow_id"] == result["flow_id"] - ) - assert context["title_placeholders"]["name"] == "switch25-12345" - - @pytest.mark.parametrize( "error", [(asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown")] ) From ff9e1ddeed8d6248db16d8fa45993ac5ad3a0310 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Dec 2020 20:29:55 +0000 Subject: [PATCH 73/90] Bumped version to 2020.12.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1e7d243b9ad..582648e8d32 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2020 MINOR_VERSION = 12 -PATCH_VERSION = "0" +PATCH_VERSION = "1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From ac62028c196bff5665510d13c2b1dbddb5fc8eaa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Dec 2020 20:55:22 +0000 Subject: [PATCH 74/90] Fix account link test --- tests/components/cloud/test_account_link.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index ce310001b35..1580969b0a5 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -44,7 +44,7 @@ async def test_setup_provide_implementation(hass): "homeassistant.components.cloud.account_link._get_services", return_value=[ {"service": "test", "min_version": "0.1.0"}, - {"service": "too_new", "min_version": "100.0.0"}, + {"service": "too_new", "min_version": "1000000.0.0"}, ], ): assert ( From 217e3c44d6aff05fb187ae8db574eff5546cbb52 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Dec 2020 13:00:53 -0600 Subject: [PATCH 75/90] Bump HAP-python to 3.1.0 (#44176) Fixes many spec compliance issues, unavailable cases following an unexpected exception, and a thread safety race condition. --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 486e9f1643c..d188dd270ab 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ - "HAP-python==3.0.0", + "HAP-python==3.1.0", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1", diff --git a/requirements_all.txt b/requirements_all.txt index 3b1fa3f76e6..c8c91f98414 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -17,7 +17,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==3.0.0 +HAP-python==3.1.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8d3dfedfc8..cf6042a8cc9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.homekit -HAP-python==3.0.0 +HAP-python==3.1.0 # homeassistant.components.flick_electric PyFlick==0.0.2 From c8bb6eb3bf1671a60f16918b05dae6fc845a5df9 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 14 Dec 2020 10:04:41 +0100 Subject: [PATCH 76/90] Update denonavr to 0.9.8 (#44194) --- homeassistant/components/denonavr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index c8341a3ec2c..31085292fbb 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -3,7 +3,7 @@ "name": "Denon AVR Network Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/denonavr", - "requirements": ["denonavr==0.9.7", "getmac==0.8.2"], + "requirements": ["denonavr==0.9.8", "getmac==0.8.2"], "codeowners": ["@scarface-4711", "@starkillerOG"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index c8c91f98414..d3471c1856e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -481,7 +481,7 @@ defusedxml==0.6.0 deluge-client==1.7.1 # homeassistant.components.denonavr -denonavr==0.9.7 +denonavr==0.9.8 # homeassistant.components.devolo_home_control devolo-home-control-api==0.16.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf6042a8cc9..0c5fb601f5a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -254,7 +254,7 @@ debugpy==1.2.0 defusedxml==0.6.0 # homeassistant.components.denonavr -denonavr==0.9.7 +denonavr==0.9.8 # homeassistant.components.devolo_home_control devolo-home-control-api==0.16.0 From 839468549326bbec8b40b9eff40781fde8b481bf Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Wed, 16 Dec 2020 23:01:39 +0000 Subject: [PATCH 77/90] Bump pyroon to 0.0.28 (#44302) --- homeassistant/components/roon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index 4f5601a7f30..4bd5903253a 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", "requirements": [ - "roonapi==0.0.25" + "roonapi==0.0.28" ], "codeowners": [ "@pavoni" diff --git a/requirements_all.txt b/requirements_all.txt index d3471c1856e..1d827f2a163 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1958,7 +1958,7 @@ rokuecp==0.6.0 roombapy==1.6.2 # homeassistant.components.roon -roonapi==0.0.25 +roonapi==0.0.28 # homeassistant.components.rova rova==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c5fb601f5a..6fd738050e2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -960,7 +960,7 @@ rokuecp==0.6.0 roombapy==1.6.2 # homeassistant.components.roon -roonapi==0.0.25 +roonapi==0.0.28 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 From 2be5547c571df26236059ceb08e5abfe1776b7d4 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 17 Dec 2020 16:12:06 +0000 Subject: [PATCH 78/90] Fix velux homekit covers not enumerated correctly (#44318) --- .../components/homekit_controller/cover.py | 1 + .../specific_devices/test_velux_gateway.py | 79 ++++ .../homekit_controller/velux_gateway.json | 380 ++++++++++++++++++ 3 files changed, 460 insertions(+) create mode 100644 tests/components/homekit_controller/specific_devices/test_velux_gateway.py create mode 100644 tests/fixtures/homekit_controller/velux_gateway.json diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index fdf48ebba5d..6c945c81115 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -248,4 +248,5 @@ class HomeKitWindowCover(HomeKitEntity, CoverEntity): ENTITY_TYPES = { ServicesTypes.GARAGE_DOOR_OPENER: HomeKitGarageDoorCover, ServicesTypes.WINDOW_COVERING: HomeKitWindowCover, + ServicesTypes.WINDOW: HomeKitWindowCover, } diff --git a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py new file mode 100644 index 00000000000..033b4aa7b4d --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py @@ -0,0 +1,79 @@ +""" +Test against characteristics captured from a Velux Gateway. + +https://github.com/home-assistant/core/issues/44314 +""" + +from homeassistant.components.cover import ( + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, +) + +from tests.components.homekit_controller.common import ( + Helper, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_simpleconnect_cover_setup(hass): + """Test that a velux gateway can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "velux_gateway.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + # Check that the cover is correctly found and set up + cover_id = "cover.velux_window" + cover = entity_registry.async_get(cover_id) + assert cover.unique_id == "homekit-1111111a114a111a-8" + + cover_helper = Helper( + hass, + cover_id, + pairing, + accessories[0], + config_entry, + ) + + cover_state = await cover_helper.poll_and_get_state() + assert cover_state.attributes["friendly_name"] == "VELUX Window" + assert cover_state.state == "closed" + assert cover_state.attributes["supported_features"] == ( + SUPPORT_CLOSE | SUPPORT_SET_POSITION | SUPPORT_OPEN + ) + + # Check that one of the sensors is correctly found and set up + sensor_id = "sensor.velux_sensor_temperature" + sensor = entity_registry.async_get(sensor_id) + assert sensor.unique_id == "homekit-a11b111-8" + + sensor_helper = Helper( + hass, + sensor_id, + pairing, + accessories[0], + config_entry, + ) + + sensor_state = await sensor_helper.poll_and_get_state() + assert sensor_state.attributes["friendly_name"] == "VELUX Sensor Temperature" + assert sensor_state.state == "18.9" + + # The cover and sensor are different devices (accessories) attached to the same bridge + assert cover.device_id != sensor.device_id + + device_registry = await hass.helpers.device_registry.async_get_registry() + + device = device_registry.async_get(cover.device_id) + assert device.manufacturer == "VELUX" + assert device.name == "VELUX Window" + assert device.model == "VELUX Window" + assert device.sw_version == "48" + + bridge = device_registry.async_get(device.via_device_id) + assert bridge.manufacturer == "VELUX" + assert bridge.name == "VELUX Gateway" + assert bridge.model == "VELUX Gateway" + assert bridge.sw_version == "70" diff --git a/tests/fixtures/homekit_controller/velux_gateway.json b/tests/fixtures/homekit_controller/velux_gateway.json new file mode 100644 index 00000000000..1a6f60537b3 --- /dev/null +++ b/tests/fixtures/homekit_controller/velux_gateway.json @@ -0,0 +1,380 @@ +[ + { + "aid": 1, + "services": [ + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "iid": 1, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX Gateway" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX" + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX Gateway" + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": [ + "pr" + ], + "format": "string", + "value": "a1a11a1" + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": [ + "pw" + ], + "format": "bool" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": [ + "pr" + ], + "format": "string", + "value": "70" + } + ], + "hidden": false, + "primary": false + }, + { + "type": "000000A2-0000-1000-8000-0026BB765291", + "iid": 8, + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 9, + "perms": [ + "pr" + ], + "format": "string", + "value": "1.1.0" + } + ], + "hidden": false, + "primary": false + } + ] + }, + { + "aid": 2, + "services": [ + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "iid": 1, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX Sensor" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX" + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX Sensor" + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": [ + "pr" + ], + "format": "string", + "value": "a11b111" + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": [ + "pw" + ], + "format": "bool" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": [ + "pr" + ], + "format": "string", + "value": "16" + } + ], + "hidden": false, + "primary": false + }, + { + "type": "0000008A-0000-1000-8000-0026BB765291", + "iid": 8, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 9, + "perms": [ + "pr" + ], + "format": "string", + "value": "Temperature sensor" + }, + { + "type": "00000011-0000-1000-8000-0026BB765291", + "iid": 10, + "perms": [ + "pr", + "ev" + ], + "format": "float", + "value": 18.9, + "minValue": 0, + "maxValue": 50, + "minStep": 0.1, + "unit": "celsius" + } + ], + "hidden": false, + "primary": true + }, + { + "type": "00000082-0000-1000-8000-0026BB765291", + "iid": 11, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 12, + "perms": [ + "pr" + ], + "format": "string", + "value": "Humidity sensor" + }, + { + "type": "00000010-0000-1000-8000-0026BB765291", + "iid": 13, + "perms": [ + "pr", + "ev" + ], + "format": "float", + "value": 58, + "minValue": 0, + "maxValue": 100, + "minStep": 1, + "unit": "percentage" + } + ], + "hidden": false, + "primary": false + }, + { + "type": "00000097-0000-1000-8000-0026BB765291", + "iid": 14, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 15, + "perms": [ + "pr" + ], + "format": "string", + "value": "Carbon Dioxide sensor" + }, + { + "type": "00000092-0000-1000-8000-0026BB765291", + "iid": 16, + "perms": [ + "pr", + "ev" + ], + "format": "uint8", + "value": 0, + "maxValue": 1, + "minValue": 0, + "minStep": 1 + }, + { + "type": "00000093-0000-1000-8000-0026BB765291", + "iid": 17, + "perms": [ + "pr", + "ev" + ], + "format": "float", + "value": 400, + "minValue": 0, + "maxValue": 5000 + } + ], + "hidden": false, + "primary": false + } + ] + }, + { + "aid": 3, + "services": [ + { + "type": "0000003E-0000-1000-8000-0026BB765291", + "iid": 1, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX Window" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX" + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": [ + "pr" + ], + "format": "string", + "value": "VELUX Window" + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": [ + "pr" + ], + "format": "string", + "value": "1111111a114a111a" + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": [ + "pw" + ], + "format": "bool" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": [ + "pr" + ], + "format": "string", + "value": "48" + } + ], + "hidden": false, + "primary": false + }, + { + "type": "0000008B-0000-1000-8000-0026BB765291", + "iid": 8, + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 9, + "perms": [ + "pr" + ], + "format": "string", + "value": "Roof Window" + }, + { + "type": "0000007C-0000-1000-8000-0026BB765291", + "iid": 11, + "perms": [ + "pr", + "pw", + "ev" + ], + "format": "uint8", + "value": 0, + "maxValue": 100, + "minValue": 0, + "unit": "percentage", + "minStep": 1 + }, + { + "type": "0000006D-0000-1000-8000-0026BB765291", + "iid": 10, + "perms": [ + "pr", + "ev" + ], + "format": "uint8", + "value": 0, + "maxValue": 100, + "minValue": 0, + "unit": "percentage", + "minStep": 1 + }, + { + "type": "00000072-0000-1000-8000-0026BB765291", + "iid": 12, + "perms": [ + "pr", + "ev" + ], + "format": "uint8", + "value": 2, + "maxValue": 2, + "minValue": 0, + "minStep": 1 + } + ], + "hidden": false, + "primary": true + } + ] + } +] \ No newline at end of file From 9c5dba6a2405f3c252c3596fc9987f63279d17d5 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Thu, 17 Dec 2020 11:08:19 -0700 Subject: [PATCH 79/90] Bump pyMyQ to version 2.0.12 (#44328) --- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index ee3471725b6..5f863ad7f34 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -2,7 +2,7 @@ "domain": "myq", "name": "MyQ", "documentation": "https://www.home-assistant.io/integrations/myq", - "requirements": ["pymyq==2.0.11"], + "requirements": ["pymyq==2.0.12"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 1d827f2a163..328e2183892 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1542,7 +1542,7 @@ pymsteams==0.1.12 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==2.0.11 +pymyq==2.0.12 # homeassistant.components.mysensors pymysensors==0.18.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6fd738050e2..e6d54280d07 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -782,7 +782,7 @@ pymodbus==2.3.0 pymonoprice==0.3 # homeassistant.components.myq -pymyq==2.0.11 +pymyq==2.0.12 # homeassistant.components.nut pynut2==2.1.2 From 89168dd3720e9f0f30eaaebd02f4b642901a9917 Mon Sep 17 00:00:00 2001 From: rubenbe Date: Fri, 18 Dec 2020 08:37:32 +0100 Subject: [PATCH 80/90] Update pytradfri to 7.0.5 (#44347) --- homeassistant/components/tradfri/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 7ffa8ed24bf..57f58f05993 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,7 +3,7 @@ "name": "IKEA TRÅDFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==7.0.4"], + "requirements": ["pytradfri[async]==7.0.5"], "homekit": { "models": ["TRADFRI"] }, diff --git a/requirements_all.txt b/requirements_all.txt index 328e2183892..86d05943729 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1858,7 +1858,7 @@ pytraccar==0.9.0 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==7.0.4 +pytradfri[async]==7.0.5 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6d54280d07..579a659950b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -915,7 +915,7 @@ pytile==4.0.0 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==7.0.4 +pytradfri[async]==7.0.5 # homeassistant.components.vera pyvera==0.3.11 From 0cac5b7cb343b1afdaa6cbe0b8f0e77818dc81fc Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 18 Dec 2020 13:12:16 -0700 Subject: [PATCH 81/90] Bump pyiqvia to 0.3.1 (#44358) --- homeassistant/components/iqvia/__init__.py | 2 +- homeassistant/components/iqvia/config_flow.py | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index db0df3a073b..bf1725a036a 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -52,7 +52,7 @@ async def async_setup_entry(hass, entry): ) websession = aiohttp_client.async_get_clientsession(hass) - client = Client(entry.data[CONF_ZIP_CODE], websession) + client = Client(entry.data[CONF_ZIP_CODE], session=websession) async def async_get_data_from_api(api_coro): """Get data from a particular API coroutine.""" diff --git a/homeassistant/components/iqvia/config_flow.py b/homeassistant/components/iqvia/config_flow.py index e43c61985d6..ecd1e3c3c4b 100644 --- a/homeassistant/components/iqvia/config_flow.py +++ b/homeassistant/components/iqvia/config_flow.py @@ -30,7 +30,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): websession = aiohttp_client.async_get_clientsession(self.hass) try: - Client(user_input[CONF_ZIP_CODE], websession) + Client(user_input[CONF_ZIP_CODE], session=websession) except InvalidZipError: return self.async_show_form( step_id="user", diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 5ab331df44e..6445b4ad91f 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,6 +3,6 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.19.2", "pyiqvia==0.2.1"], + "requirements": ["numpy==1.19.2", "pyiqvia==0.3.1"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 86d05943729..f55280326cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1455,7 +1455,7 @@ pyipma==2.0.5 pyipp==0.11.0 # homeassistant.components.iqvia -pyiqvia==0.2.1 +pyiqvia==0.3.1 # homeassistant.components.irish_rail_transport pyirishrail==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 579a659950b..da18893a910 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -734,7 +734,7 @@ pyipma==2.0.5 pyipp==0.11.0 # homeassistant.components.iqvia -pyiqvia==0.2.1 +pyiqvia==0.3.1 # homeassistant.components.isy994 pyisy==2.1.0 From f76211d58aee0c1c074cb2bcdefff2c86bda5867 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 18 Dec 2020 12:28:18 -0700 Subject: [PATCH 82/90] Fix bug in unloading RainMachine options listener (#44359) * Fix bug in unloading RainMachine options listener * Order --- homeassistant/components/rainmachine/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 41c56e38db6..c8697adbcd4 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -275,7 +275,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ]: hass.services.async_register(DOMAIN, service, method, schema=schema) - hass.data[DOMAIN][DATA_LISTENER] = entry.add_update_listener(async_reload_entry) + hass.data[DOMAIN][DATA_LISTENER][entry.entry_id] = entry.add_update_listener( + async_reload_entry + ) return True From a58219bbc7314b4479db4de380bb3d338c79e88b Mon Sep 17 00:00:00 2001 From: On Freund Date: Tue, 22 Dec 2020 13:32:56 +0200 Subject: [PATCH 83/90] Fix Volumio pause with missing track type (#44447) --- homeassistant/components/volumio/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 89fb17affc8..69790e71732 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -204,7 +204,7 @@ class Volumio(MediaPlayerEntity): async def async_media_pause(self): """Send media_pause command to media player.""" - if self._state["trackType"] == "webradio": + if self._state.get("trackType") == "webradio": await self._volumio.stop() else: await self._volumio.pause() From 78f81b6e3736ad60c93dee549ce5484d33d8acab Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Sat, 26 Dec 2020 07:53:34 -0500 Subject: [PATCH 84/90] Fix falsey comparisons in NWS weather (#44486) --- homeassistant/components/nws/weather.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 69dac297b1b..34c2909188f 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -161,7 +161,7 @@ class NWSWeather(WeatherEntity): temp_c = None if self.observation: temp_c = self.observation.get("temperature") - if temp_c: + if temp_c is not None: return convert_temperature(temp_c, TEMP_CELSIUS, TEMP_FAHRENHEIT) return None @@ -273,7 +273,7 @@ class NWSWeather(WeatherEntity): data[ATTR_FORECAST_WIND_BEARING] = forecast_entry.get("windBearing") wind_speed = forecast_entry.get("windSpeedAvg") - if wind_speed: + if wind_speed is not None: if self.is_metric: data[ATTR_FORECAST_WIND_SPEED] = round( convert_distance(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS) From 59c20686c1aa429af78cf9683046f5aa33f914ab Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 26 Dec 2020 22:24:05 +0100 Subject: [PATCH 85/90] Bump pydeconz to version 77 (#44514) --- 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 c2846f8c57f..22711b84b9d 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==76"], + "requirements": ["pydeconz==77"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" diff --git a/requirements_all.txt b/requirements_all.txt index f55280326cf..63d574c3922 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1337,7 +1337,7 @@ pydaikin==2.3.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==76 +pydeconz==77 # homeassistant.components.delijn pydelijn==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da18893a910..bd9819319f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -673,7 +673,7 @@ pycountry==19.8.18 pydaikin==2.3.1 # homeassistant.components.deconz -pydeconz==76 +pydeconz==77 # homeassistant.components.dexcom pydexcom==0.2.0 From 99d7c8391747e0cfb954eb014be0e8cc19ee2e7a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Dec 2020 14:41:39 +0100 Subject: [PATCH 86/90] Fix Tasmota device triggers (#44574) --- .../components/tasmota/device_trigger.py | 4 +- .../components/tasmota/test_device_trigger.py | 147 ++++++++++++++++-- 2 files changed, 139 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py index e7dad0885a0..f06d815e5c5 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -56,7 +56,7 @@ class TriggerInstance: event_trigger.CONF_EVENT_TYPE: TASMOTA_EVENT, event_trigger.CONF_EVENT_DATA: { "mac": self.trigger.tasmota_trigger.cfg.mac, - "source": self.trigger.tasmota_trigger.cfg.source, + "source": self.trigger.tasmota_trigger.cfg.subtype, "event": self.trigger.tasmota_trigger.cfg.event, }, } @@ -126,7 +126,7 @@ class Trigger: def _on_trigger(): data = { "mac": self.tasmota_trigger.cfg.mac, - "source": self.tasmota_trigger.cfg.source, + "source": self.tasmota_trigger.cfg.subtype, "event": self.tasmota_trigger.cfg.event, } self.hass.bus.async_fire( diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py index 42fc5dc7a49..2c88533f30d 100644 --- a/tests/components/tasmota/test_device_trigger.py +++ b/tests/components/tasmota/test_device_trigger.py @@ -21,7 +21,42 @@ from tests.common import ( from tests.components.blueprint.conftest import stub_blueprint_populate # noqa -async def test_get_triggers(hass, device_reg, entity_reg, mqtt_mock, setup_tasmota): +async def test_get_triggers_btn(hass, device_reg, entity_reg, mqtt_mock, setup_tasmota): + """Test we get the expected triggers from a discovered mqtt device.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["btn"][0] = 1 + config["btn"][1] = 1 + config["so"]["13"] = 1 + config["so"]["73"] = 1 + mac = config["mac"] + + async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) + await hass.async_block_till_done() + + device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "00000049A3BC_button_1_SINGLE", + "type": "button_short_press", + "subtype": "button_1", + }, + { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "00000049A3BC_button_2_SINGLE", + "type": "button_short_press", + "subtype": "button_2", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_get_triggers_swc(hass, device_reg, entity_reg, mqtt_mock, setup_tasmota): """Test we get the expected triggers from a discovered mqtt device.""" config = copy.deepcopy(DEFAULT_CONFIG) config["swc"][0] = 0 @@ -239,13 +274,83 @@ async def test_update_remove_triggers( assert triggers == [] -async def test_if_fires_on_mqtt_message( +async def test_if_fires_on_mqtt_message_btn( hass, device_reg, calls, mqtt_mock, setup_tasmota ): - """Test triggers firing.""" + """Test button triggers firing.""" + # Discover a device with 2 device triggers + config = copy.deepcopy(DEFAULT_CONFIG) + config["btn"][0] = 1 + config["btn"][2] = 1 + config["so"]["73"] = 1 + mac = config["mac"] + + async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) + await hass.async_block_till_done() + device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "00000049A3BC_button_1_SINGLE", + "type": "button_short_press", + "subtype": "button_1", + }, + "action": { + "service": "test.automation", + "data_template": {"some": ("short_press_1")}, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "00000049A3BC_button_3_SINGLE", + "subtype": "button_3", + "type": "button_short_press", + }, + "action": { + "service": "test.automation", + "data_template": {"some": ("short_press_3")}, + }, + }, + ] + }, + ) + + # Fake button 1 single press. + async_fire_mqtt_message( + hass, "tasmota_49A3BC/stat/RESULT", '{"Button1":{"Action":"SINGLE"}}' + ) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "short_press_1" + + # Fake button 3 single press. + async_fire_mqtt_message( + hass, "tasmota_49A3BC/stat/RESULT", '{"Button3":{"Action":"SINGLE"}}' + ) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "short_press_3" + + +async def test_if_fires_on_mqtt_message_swc( + hass, device_reg, calls, mqtt_mock, setup_tasmota +): + """Test switch triggers firing.""" # Discover a device with 2 device triggers config = copy.deepcopy(DEFAULT_CONFIG) config["swc"][0] = 0 + config["swc"][1] = 0 config["swc"][2] = 9 config["swn"][2] = "custom_switch" mac = config["mac"] @@ -270,7 +375,21 @@ async def test_if_fires_on_mqtt_message( }, "action": { "service": "test.automation", - "data_template": {"some": ("short_press")}, + "data_template": {"some": ("short_press_1")}, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "00000049A3BC_switch_2_TOGGLE", + "type": "button_short_press", + "subtype": "switch_2", + }, + "action": { + "service": "test.automation", + "data_template": {"some": ("short_press_2")}, }, }, { @@ -284,28 +403,36 @@ async def test_if_fires_on_mqtt_message( }, "action": { "service": "test.automation", - "data_template": {"some": ("long_press")}, + "data_template": {"some": ("long_press_3")}, }, }, ] }, ) - # Fake short press. + # Fake switch 1 short press. async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"Switch1":{"Action":"TOGGLE"}}' ) await hass.async_block_till_done() assert len(calls) == 1 - assert calls[0].data["some"] == "short_press" + assert calls[0].data["some"] == "short_press_1" - # Fake long press. + # Fake switch 2 short press. + async_fire_mqtt_message( + hass, "tasmota_49A3BC/stat/RESULT", '{"Switch2":{"Action":"TOGGLE"}}' + ) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "short_press_2" + + # Fake switch 3 long press. async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"custom_switch":{"Action":"HOLD"}}' ) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "long_press" + assert len(calls) == 3 + assert calls[2].data["some"] == "long_press_3" async def test_if_fires_on_mqtt_message_late_discover( From 62f8d6cc04dc3e65af64070e5483afe9be51341d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 29 Dec 2020 12:16:39 -1000 Subject: [PATCH 87/90] Fix template triggers from time events (#44603) Co-authored-by: Paulus Schoutsen --- homeassistant/components/template/trigger.py | 47 ++++++------ tests/components/template/test_trigger.py | 77 +++++++++++++++++++- 2 files changed, 99 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 5d748edb841..80ad585486b 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -52,25 +52,33 @@ async def async_attach_trigger( if not result_as_boolean(result): return - entity_id = event.data.get("entity_id") - from_s = event.data.get("old_state") - to_s = event.data.get("new_state") + entity_id = event and event.data.get("entity_id") + from_s = event and event.data.get("old_state") + to_s = event and event.data.get("new_state") + + if entity_id is not None: + description = f"{entity_id} via template" + else: + description = "time change or manual update via template" + + template_variables = { + "platform": platform_type, + "entity_id": entity_id, + "from_state": from_s, + "to_state": to_s, + } + trigger_variables = { + "for": time_delta, + "description": description, + } @callback def call_action(*_): """Call action with right context.""" + nonlocal trigger_variables hass.async_run_hass_job( job, - { - "trigger": { - "platform": "template", - "entity_id": entity_id, - "from_state": from_s, - "to_state": to_s, - "for": time_delta if not time_delta else period, - "description": f"{entity_id} via template", - } - }, + {"trigger": {**template_variables, **trigger_variables}}, (to_s.context if to_s else None), ) @@ -78,18 +86,9 @@ async def async_attach_trigger( call_action() return - variables = { - "trigger": { - "platform": platform_type, - "entity_id": entity_id, - "from_state": from_s, - "to_state": to_s, - } - } - try: period = cv.positive_time_period( - template.render_complex(time_delta, variables) + template.render_complex(time_delta, {"trigger": template_variables}) ) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( @@ -97,6 +96,8 @@ async def async_attach_trigger( ) return + trigger_variables["for"] = period + delay_cancel = async_call_later(hass, period.seconds, call_action) info = async_track_template_result( diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py index 828cf1fb7b4..822a274bf23 100644 --- a/tests/components/template/test_trigger.py +++ b/tests/components/template/test_trigger.py @@ -11,6 +11,7 @@ from homeassistant.core import Context, callback from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, @@ -626,6 +627,7 @@ async def test_if_fires_on_change_with_for_0_advanced(hass, calls): async def test_if_fires_on_change_with_for_2(hass, calls): """Test for firing on change with for.""" + context = Context() assert await async_setup_component( hass, automation.DOMAIN, @@ -636,17 +638,33 @@ async def test_if_fires_on_change_with_for_2(hass, calls): "value_template": "{{ is_state('test.entity', 'world') }}", "for": 5, }, - "action": {"service": "test.automation"}, + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, } }, ) - hass.states.async_set("test.entity", "world") + hass.states.async_set("test.entity", "world", context=context) await hass.async_block_till_done() assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() assert len(calls) == 1 + assert calls[0].context.parent_id == context.id + assert calls[0].data["some"] == "template - test.entity - hello - world - 0:00:05" async def test_if_not_fires_on_change_with_for(hass, calls): @@ -811,3 +829,58 @@ async def test_invalid_for_template_1(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() assert mock_logger.error.called + + +async def test_if_fires_on_time_change(hass, calls): + """Test for firing on time changes.""" + start_time = dt_util.utcnow() + timedelta(hours=24) + time_that_will_not_match_right_away = start_time.replace(minute=1, second=0) + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "template", + "value_template": "{{ utcnow().minute % 2 == 0 }}", + }, + "action": {"service": "test.automation"}, + } + }, + ) + await hass.async_block_till_done() + assert len(calls) == 0 + + # Trigger once (match template) + first_time = start_time.replace(minute=2, second=0) + with patch("homeassistant.util.dt.utcnow", return_value=first_time): + async_fire_time_changed(hass, first_time) + await hass.async_block_till_done() + assert len(calls) == 1 + + # Trigger again (match template) + second_time = start_time.replace(minute=4, second=0) + with patch("homeassistant.util.dt.utcnow", return_value=second_time): + async_fire_time_changed(hass, second_time) + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(calls) == 1 + + # Trigger again (do not match template) + third_time = start_time.replace(minute=5, second=0) + with patch("homeassistant.util.dt.utcnow", return_value=third_time): + async_fire_time_changed(hass, third_time) + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(calls) == 1 + + # Trigger again (match template) + forth_time = start_time.replace(minute=8, second=0) + with patch("homeassistant.util.dt.utcnow", return_value=forth_time): + async_fire_time_changed(hass, forth_time) + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(calls) == 2 From 19531b90a30294b391095184bb75dd01d87cfb81 Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Tue, 29 Dec 2020 20:54:04 -0500 Subject: [PATCH 88/90] Bump env_canada to 0.2.5 (#44631) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 2a51c6ffd83..02a60049f07 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,6 +2,6 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.2.4"], + "requirements": ["env_canada==0.2.5"], "codeowners": ["@michaeldavie"] } diff --git a/requirements_all.txt b/requirements_all.txt index 63d574c3922..de7e05feca6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -553,7 +553,7 @@ enocean==0.50 enturclient==0.2.1 # homeassistant.components.environment_canada -env_canada==0.2.4 +env_canada==0.2.5 # homeassistant.components.envirophat # envirophat==0.0.6 From af4849c18342a2a40f28f6b45b617a991b0b9458 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Wed, 30 Dec 2020 08:44:44 +0000 Subject: [PATCH 89/90] Bump pycarwings2 to 2.10 (#44634) --- homeassistant/components/nissan_leaf/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index 339b5750036..db78e5ce0e9 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -2,6 +2,6 @@ "domain": "nissan_leaf", "name": "Nissan Leaf", "documentation": "https://www.home-assistant.io/integrations/nissan_leaf", - "requirements": ["pycarwings2==2.9"], + "requirements": ["pycarwings2==2.10"], "codeowners": ["@filcole"] } diff --git a/requirements_all.txt b/requirements_all.txt index de7e05feca6..f1426cebca8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1298,7 +1298,7 @@ pyblackbird==0.5 pybotvac==0.0.17 # homeassistant.components.nissan_leaf -pycarwings2==2.9 +pycarwings2==2.10 # homeassistant.components.cloudflare pycfdns==1.2.1 From d1ad474a8d5c66341c074688b52553b6a5eef795 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 30 Dec 2020 10:17:56 +0100 Subject: [PATCH 90/90] Bumped version to 2020.12.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 582648e8d32..ac4f07a7086 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2020 MINOR_VERSION = 12 -PATCH_VERSION = "1" +PATCH_VERSION = "2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1)