From 7756038bee15a6c447317445421bd2bf7d4348de Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 7 Sep 2020 23:01:27 +0200 Subject: [PATCH 001/185] Bumped version to 0.115.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ea5c203ee02..cb926177ba5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0.dev0" +PATCH_VERSION = "0b0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 214fc044738961c9b42251d3c2aad23922897689 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 8 Sep 2020 10:35:01 +0200 Subject: [PATCH 002/185] Support shelly cover(roller) mode (#39711) Co-authored-by: Paulus Schoutsen --- .coveragerc | 2 + homeassistant/components/shelly/__init__.py | 2 +- homeassistant/components/shelly/cover.py | 104 ++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/shelly/cover.py diff --git a/.coveragerc b/.coveragerc index 56a52b4f4e5..fe7a8d74cbd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -760,7 +760,9 @@ omit = homeassistant/components/shodan/sensor.py homeassistant/components/shelly/__init__.py homeassistant/components/shelly/binary_sensor.py + homeassistant/components/shelly/cover.py homeassistant/components/shelly/entity.py + homeassistant/components/shelly/cover.py homeassistant/components/shelly/light.py homeassistant/components/shelly/sensor.py homeassistant/components/shelly/switch.py diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index cfc566fdb3e..39fc34d6d56 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers import aiohttp_client, device_registry, update_coordi from .const import DOMAIN -PLATFORMS = ["binary_sensor", "light", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "light", "sensor", "switch", "cover"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py new file mode 100644 index 00000000000..ec1933ba420 --- /dev/null +++ b/homeassistant/components/shelly/cover.py @@ -0,0 +1,104 @@ +"""Cover for Shelly.""" +from aioshelly import Block + +from homeassistant.components.cover import ( + ATTR_POSITION, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_STOP, + CoverEntity, +) +from homeassistant.core import callback + +from . import ShellyDeviceWrapper +from .const import DOMAIN +from .entity import ShellyBlockEntity + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up cover for device.""" + wrapper = hass.data[DOMAIN][config_entry.entry_id] + blocks = [block for block in wrapper.device.blocks if block.type == "roller"] + + if not blocks: + return + + async_add_entities(ShellyCover(wrapper, block) for block in blocks) + + +class ShellyCover(ShellyBlockEntity, CoverEntity): + """Switch that controls a cover block on Shelly devices.""" + + def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None: + """Initialize light.""" + super().__init__(wrapper, block) + self.control_result = None + self._supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP + if self.wrapper.device.settings["rollers"][0]["positioning"]: + self._supported_features |= SUPPORT_SET_POSITION + + @property + def is_closed(self): + """If cover is closed.""" + if self.control_result: + return self.control_result["current_pos"] == 0 + + return self.block.rollerPos == 0 + + @property + def current_cover_position(self): + """Position of the cover.""" + if self.control_result: + return self.control_result["current_pos"] + + return self.block.rollerPos + + @property + def is_closing(self): + """Return if the cover is closing.""" + if self.control_result: + return self.control_result["state"] == "close" + + return self.block.roller == "close" + + @property + def is_opening(self): + """Return if the cover is opening.""" + if self.control_result: + return self.control_result["state"] == "open" + + return self.block.roller == "open" + + @property + def supported_features(self): + """Flag supported features.""" + return self._supported_features + + async def async_close_cover(self, **kwargs): + """Close cover.""" + self.control_result = await self.block.set_state(go="close") + self.async_write_ha_state() + + async def async_open_cover(self, **kwargs): + """Open cover.""" + self.control_result = await self.block.set_state(go="open") + self.async_write_ha_state() + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + self.control_result = await self.block.set_state( + go="to_pos", roller_pos=kwargs[ATTR_POSITION] + ) + self.async_write_ha_state() + + async def async_stop_cover(self, **_kwargs): + """Stop the cover.""" + self.control_result = await self.block.set_state(go="stop") + self.async_write_ha_state() + + @callback + def _update_callback(self): + """When device updates, clear control result that overrides state.""" + self.control_result = None + super()._update_callback() From 71c25574055ae8c85f0f771c22225a8695693dbb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 15:23:29 +0200 Subject: [PATCH 003/185] Guard for spotify items without type (#39795) Co-authored-by: Bram Kragten --- .../components/spotify/media_player.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 6f9791f1409..cc62cb9e276 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -437,16 +437,16 @@ def build_item_response(spotify, user, payload): items = media.get("artists", {}).get("items", []) elif media_content_type == "current_user_saved_albums": media = spotify.current_user_saved_albums(limit=BROWSE_LIMIT) - items = media.get("items", []) + items = [item["album"] for item in media.get("items", [])] elif media_content_type == "current_user_saved_tracks": media = spotify.current_user_saved_tracks(limit=BROWSE_LIMIT) - items = media.get("items", []) + items = [item["track"] for item in media.get("items", [])] elif media_content_type == "current_user_saved_shows": media = spotify.current_user_saved_shows(limit=BROWSE_LIMIT) - items = media.get("items", []) + items = [item["show"] for item in media.get("items", [])] elif media_content_type == "current_user_recently_played": media = spotify.current_user_recently_played(limit=BROWSE_LIMIT) - items = media.get("items", []) + items = [item["track"] for item in media.get("items", [])] elif media_content_type == "current_user_top_artists": media = spotify.current_user_top_artists(limit=BROWSE_LIMIT) items = media.get("items", []) @@ -474,7 +474,7 @@ def build_item_response(spotify, user, payload): items = media.get("albums", {}).get("items", []) elif media_content_type == MEDIA_TYPE_PLAYLIST: media = spotify.playlist(media_content_id) - items = media.get("tracks", {}).get("items", []) + items = [item["track"] for item in media.get("tracks", {}).get("items", [])] elif media_content_type == MEDIA_TYPE_ALBUM: media = spotify.album(media_content_id) items = media.get("tracks", {}).get("items", []) @@ -546,14 +546,6 @@ def item_payload(item): Used by async_browse_media. """ - if MEDIA_TYPE_TRACK in item: - item = item[MEDIA_TYPE_TRACK] - elif MEDIA_TYPE_SHOW in item: - item = item[MEDIA_TYPE_SHOW] - elif MEDIA_TYPE_ARTIST in item: - item = item[MEDIA_TYPE_ARTIST] - elif MEDIA_TYPE_ALBUM in item and item["type"] != MEDIA_TYPE_TRACK: - item = item[MEDIA_TYPE_ALBUM] can_expand = item["type"] not in [ MEDIA_TYPE_TRACK, From 2a879afc7ab70a91259ed3eb078884e48e240fa7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 8 Sep 2020 16:42:01 +0200 Subject: [PATCH 004/185] Add media class browse media attribute (#39770) --- .../components/arcam_fmj/media_player.py | 4 + homeassistant/components/kodi/browse_media.py | 21 +++++ .../components/media_player/__init__.py | 4 + .../components/media_player/const.py | 23 ++++++ .../components/media_source/local_source.py | 2 + .../components/media_source/models.py | 4 + .../components/netatmo/media_source.py | 6 +- .../components/philips_js/media_player.py | 4 + .../components/plex/media_browser.py | 65 ++++++++++++++-- homeassistant/components/roku/media_player.py | 12 +++ .../components/sonos/media_player.py | 30 ++++++++ .../components/spotify/media_player.py | 77 ++++++++++++++++--- tests/components/media_source/test_init.py | 2 + tests/components/media_source/test_models.py | 12 ++- 14 files changed, 248 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 7e6c34a8324..1f0f564c59b 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -7,6 +7,8 @@ from arcam.fmj.state import State from homeassistant import config_entries from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.const import ( + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_MUSIC, MEDIA_TYPE_MUSIC, SUPPORT_BROWSE_MEDIA, SUPPORT_PLAY_MEDIA, @@ -255,6 +257,7 @@ class ArcamFmj(MediaPlayerEntity): radio = [ BrowseMedia( title=preset.name, + media_class=MEDIA_CLASS_MUSIC, media_content_id=f"preset:{preset.index}", media_content_type=MEDIA_TYPE_MUSIC, can_play=True, @@ -265,6 +268,7 @@ class ArcamFmj(MediaPlayerEntity): root = BrowseMedia( title="Root", + media_class=MEDIA_CLASS_DIRECTORY, media_content_id="root", media_content_type="library", can_play=False, diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index d308b48101b..1b1576e82da 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -2,6 +2,14 @@ from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.const import ( + MEDIA_CLASS_ALBUM, + MEDIA_CLASS_ARTIST, + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_MOVIE, + MEDIA_CLASS_MUSIC, + MEDIA_CLASS_PLAYLIST, + MEDIA_CLASS_SEASON, + MEDIA_CLASS_TV_SHOW, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, MEDIA_TYPE_EPISODE, @@ -26,6 +34,16 @@ EXPANDABLE_MEDIA_TYPES = [ MEDIA_TYPE_SEASON, ] +CONTENT_TYPE_MEDIA_CLASS = { + "library_music": MEDIA_CLASS_MUSIC, + MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON, + MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, + MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, + MEDIA_TYPE_MOVIE: MEDIA_CLASS_MOVIE, + MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, + MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW, +} + async def build_item_response(media_library, payload): """Create response payload for the provided media query.""" @@ -124,6 +142,7 @@ async def build_item_response(media_library, payload): return return BrowseMedia( + media_class=CONTENT_TYPE_MEDIA_CLASS[search_type], media_content_id=payload["search_id"], media_content_type=search_type, title=title, @@ -177,6 +196,7 @@ def item_payload(item, media_library): return BrowseMedia( title=title, + media_class=CONTENT_TYPE_MEDIA_CLASS[item["type"]], media_content_type=media_content_type, media_content_id=media_content_id, can_play=can_play, @@ -192,6 +212,7 @@ def library_payload(media_library): Used by async_browse_media. """ library_info = BrowseMedia( + media_class=MEDIA_CLASS_DIRECTORY, media_content_id="library", media_content_type="library", title="Media Library", diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 16cabe5edb9..718011b4a76 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -822,6 +822,7 @@ class MediaPlayerEntity(Entity): Payload should follow this format: { "title": str - Title of the item + "media_class": str - Media class "media_content_type": str - see below "media_content_id": str - see below - Can be passed back in to browse further @@ -1046,6 +1047,7 @@ class BrowseMedia: def __init__( self, *, + media_class: str, media_content_id: str, media_content_type: str, title: str, @@ -1055,6 +1057,7 @@ class BrowseMedia: thumbnail: Optional[str] = None, ): """Initialize browse media item.""" + self.media_class = media_class self.media_content_id = media_content_id self.media_content_type = media_content_type self.title = title @@ -1067,6 +1070,7 @@ class BrowseMedia: """Convert Media class to browse media dictionary.""" response = { "title": self.title, + "media_class": self.media_class, "media_content_type": self.media_content_type, "media_content_id": self.media_content_id, "can_play": self.can_play, diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 74c65fcd780..6714d03c19e 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -29,6 +29,29 @@ ATTR_SOUND_MODE_LIST = "sound_mode_list" DOMAIN = "media_player" +MEDIA_CLASS_ALBUM = "album" +MEDIA_CLASS_APP = "app" +MEDIA_CLASS_APPS = "apps" +MEDIA_CLASS_ARTIST = "artist" +MEDIA_CLASS_CHANNEL = "channel" +MEDIA_CLASS_CHANNELS = "channels" +MEDIA_CLASS_COMPOSER = "composer" +MEDIA_CLASS_CONTRIBUTING_ARTIST = "contributing_artist" +MEDIA_CLASS_DIRECTORY = "directory" +MEDIA_CLASS_EPISODE = "episode" +MEDIA_CLASS_GAME = "game" +MEDIA_CLASS_GENRE = "genre" +MEDIA_CLASS_IMAGE = "image" +MEDIA_CLASS_MOVIE = "movie" +MEDIA_CLASS_MUSIC = "music" +MEDIA_CLASS_PLAYLIST = "playlist" +MEDIA_CLASS_PODCAST = "podcast" +MEDIA_CLASS_SEASON = "season" +MEDIA_CLASS_TRACK = "track" +MEDIA_CLASS_TV_SHOW = "tv_show" +MEDIA_CLASS_URL = "url" +MEDIA_CLASS_VIDEO = "video" + MEDIA_TYPE_ALBUM = "album" MEDIA_TYPE_APP = "app" MEDIA_TYPE_APPS = "apps" diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index d670cee1676..774ee64d852 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -6,6 +6,7 @@ from typing import Tuple from aiohttp import web from homeassistant.components.http import HomeAssistantView +from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source.error import Unresolvable from homeassistant.core import HomeAssistant, callback @@ -114,6 +115,7 @@ class LocalSource(MediaSource): media = BrowseMediaSource( domain=DOMAIN, identifier=f"{source_dir_id}/{path.relative_to(self.hass.config.path('media'))}", + media_class=MEDIA_CLASS_DIRECTORY, media_content_type="directory", title=title, can_play=is_file, diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index cd8e44f4a24..5d768fd79d8 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -5,6 +5,8 @@ from typing import List, Optional, Tuple from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.const import ( + MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_CHANNELS, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNELS, ) @@ -52,6 +54,7 @@ class MediaSourceItem: base = BrowseMediaSource( domain=None, identifier=None, + media_class=MEDIA_CLASS_CHANNELS, media_content_type=MEDIA_TYPE_CHANNELS, title="Media Sources", can_play=False, @@ -61,6 +64,7 @@ class MediaSourceItem: BrowseMediaSource( domain=source.domain, identifier=None, + media_class=MEDIA_CLASS_CHANNEL, media_content_type=MEDIA_TYPE_CHANNEL, title=source.name, can_play=False, diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index 09c34ec41af..02ffd608472 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -4,7 +4,10 @@ import logging import re from typing import Optional, Tuple -from homeassistant.components.media_player.const import MEDIA_TYPE_VIDEO +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_VIDEO, + MEDIA_TYPE_VIDEO, +) from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source.const import MEDIA_MIME_TYPES from homeassistant.components.media_source.error import MediaSourceError, Unresolvable @@ -91,6 +94,7 @@ class NetatmoSource(MediaSource): media = BrowseMediaSource( domain=DOMAIN, identifier=path, + media_class=MEDIA_CLASS_VIDEO, media_content_type=MEDIA_TYPE_VIDEO, title=title, can_play=bool( diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 0e2d3f97c49..a780fe6b635 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -11,6 +11,8 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, ) from homeassistant.components.media_player.const import ( + MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_CHANNELS, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNELS, SUPPORT_BROWSE_MEDIA, @@ -288,6 +290,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): return BrowseMedia( title="Channels", + media_class=MEDIA_CLASS_CHANNELS, media_content_id="", media_content_type=MEDIA_TYPE_CHANNELS, can_play=False, @@ -295,6 +298,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): children=[ BrowseMedia( title=channel, + media_class=MEDIA_CLASS_CHANNEL, media_content_id=channel, media_content_type=MEDIA_TYPE_CHANNEL, can_play=True, diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index 1d3f3616450..9d5572a4faa 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -2,13 +2,31 @@ import logging from homeassistant.components.media_player import BrowseMedia +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_ALBUM, + MEDIA_CLASS_ARTIST, + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_EPISODE, + MEDIA_CLASS_MOVIE, + MEDIA_CLASS_PLAYLIST, + MEDIA_CLASS_SEASON, + MEDIA_CLASS_TRACK, + MEDIA_CLASS_TV_SHOW, + MEDIA_CLASS_VIDEO, +) from homeassistant.components.media_player.errors import BrowseError from .const import DOMAIN + +class UnknownMediaType(BrowseError): + """Unknown media type.""" + + EXPANDABLES = ["album", "artist", "playlist", "season", "show"] PLAYLISTS_BROWSE_PAYLOAD = { "title": "Playlists", + "media_class": MEDIA_CLASS_PLAYLIST, "media_content_id": "all", "media_content_type": "playlists", "can_play": False, @@ -19,6 +37,18 @@ SPECIAL_METHODS = { "Recently Added": "recentlyAdded", } +ITEM_TYPE_MEDIA_CLASS = { + "album": MEDIA_CLASS_ALBUM, + "artist": MEDIA_CLASS_ARTIST, + "episode": MEDIA_CLASS_EPISODE, + "movie": MEDIA_CLASS_MOVIE, + "playlist": MEDIA_CLASS_PLAYLIST, + "season": MEDIA_CLASS_SEASON, + "show": MEDIA_CLASS_TV_SHOW, + "track": MEDIA_CLASS_TRACK, + "video": MEDIA_CLASS_VIDEO, +} + _LOGGER = logging.getLogger(__name__) @@ -34,11 +64,17 @@ def browse_media( if media is None: return None - media_info = item_payload(media) + try: + media_info = item_payload(media) + except UnknownMediaType: + return None if media_info.can_expand: media_info.children = [] for item in media: - media_info.children.append(item_payload(item)) + try: + media_info.children.append(item_payload(item)) + except UnknownMediaType: + continue return media_info if media_content_id and ":" in media_content_id: @@ -65,6 +101,7 @@ def browse_media( payload = { "title": title, + "media_class": MEDIA_CLASS_DIRECTORY, "media_content_id": f"{media_content_id}:{special_folder}", "media_content_type": media_content_type, "can_play": False, @@ -75,7 +112,10 @@ def browse_media( method = SPECIAL_METHODS[special_folder] items = getattr(library_or_section, method)() for item in items: - payload["children"].append(item_payload(item)) + try: + payload["children"].append(item_payload(item)) + except UnknownMediaType: + continue return BrowseMedia(**payload) if media_content_type in ["server", None]: @@ -99,8 +139,14 @@ def browse_media( def item_payload(item): """Create response payload for a single media item.""" + try: + media_class = ITEM_TYPE_MEDIA_CLASS[item.type] + except KeyError as err: + _LOGGER.debug("Unknown type received: %s", item.type) + raise UnknownMediaType from err payload = { "title": item.title, + "media_class": media_class, "media_content_id": str(item.ratingKey), "media_content_type": item.type, "can_play": True, @@ -116,6 +162,7 @@ def library_section_payload(section): """Create response payload for a single library section.""" return BrowseMedia( title=section.title, + media_class=MEDIA_CLASS_DIRECTORY, media_content_id=section.key, media_content_type="library", can_play=False, @@ -128,6 +175,7 @@ def special_library_payload(parent_payload, special_type): title = f"{special_type} ({parent_payload.title})" return BrowseMedia( title=title, + media_class=parent_payload.media_class, media_content_id=f"{parent_payload.media_content_id}:{special_type}", media_content_type=parent_payload.media_content_type, can_play=False, @@ -139,6 +187,7 @@ def server_payload(plex_server): """Create response payload to describe libraries of the Plex server.""" server_info = BrowseMedia( title=plex_server.friendly_name, + media_class=MEDIA_CLASS_DIRECTORY, media_content_id=plex_server.machine_identifier, media_content_type="server", can_play=False, @@ -165,7 +214,10 @@ def library_payload(plex_server, library_id): special_library_payload(library_info, "Recently Added") ) for item in library.all(): - library_info.children.append(item_payload(item)) + try: + library_info.children.append(item_payload(item)) + except UnknownMediaType: + continue return library_info @@ -173,5 +225,8 @@ def playlists_payload(plex_server): """Create response payload for all available playlists.""" playlists_info = {**PLAYLISTS_BROWSE_PAYLOAD, "children": []} for playlist in plex_server.playlists(): - playlists_info["children"].append(item_payload(playlist)) + try: + playlists_info["children"].append(item_payload(playlist)) + except UnknownMediaType: + continue return BrowseMedia(**playlists_info) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index c8f1b6d999d..7935206f114 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -11,6 +11,11 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, ) from homeassistant.components.media_player.const import ( + MEDIA_CLASS_APP, + MEDIA_CLASS_APPS, + MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_CHANNELS, + MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_APP, MEDIA_TYPE_APPS, MEDIA_TYPE_CHANNEL, @@ -79,6 +84,7 @@ def browse_media_library(channels: bool = False) -> BrowseMedia: """Create response payload to describe contents of a specific library.""" library_info = BrowseMedia( title="Media Library", + media_class=MEDIA_CLASS_DIRECTORY, media_content_id="library", media_content_type="library", can_play=False, @@ -89,6 +95,7 @@ def browse_media_library(channels: bool = False) -> BrowseMedia: library_info.children.append( BrowseMedia( title="Apps", + media_class=MEDIA_CLASS_APPS, media_content_id="apps", media_content_type=MEDIA_TYPE_APPS, can_expand=True, @@ -100,6 +107,7 @@ def browse_media_library(channels: bool = False) -> BrowseMedia: library_info.children.append( BrowseMedia( title="Channels", + media_class=MEDIA_CLASS_CHANNELS, media_content_id="channels", media_content_type=MEDIA_TYPE_CHANNELS, can_expand=True, @@ -286,6 +294,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): if media_content_type == MEDIA_TYPE_APPS: response = BrowseMedia( title="Apps", + media_class=MEDIA_CLASS_APPS, media_content_id="apps", media_content_type=MEDIA_TYPE_APPS, can_expand=True, @@ -294,6 +303,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): BrowseMedia( title=app.name, thumbnail=self.coordinator.roku.app_icon_url(app.app_id), + media_class=MEDIA_CLASS_APP, media_content_id=app.app_id, media_content_type=MEDIA_TYPE_APP, can_play=True, @@ -306,6 +316,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): if media_content_type == MEDIA_TYPE_CHANNELS: response = BrowseMedia( title="Channels", + media_class=MEDIA_CLASS_CHANNELS, media_content_id="channels", media_content_type=MEDIA_TYPE_CHANNELS, can_expand=True, @@ -313,6 +324,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): children=[ BrowseMedia( title=channel.name, + media_class=MEDIA_CLASS_CHANNEL, media_content_id=channel.number, media_content_type=MEDIA_TYPE_CHANNEL, can_play=True, diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 47610f242eb..9a245f8d4d7 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -17,6 +17,14 @@ import voluptuous as vol from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, + MEDIA_CLASS_ALBUM, + MEDIA_CLASS_ARTIST, + MEDIA_CLASS_COMPOSER, + MEDIA_CLASS_CONTRIBUTING_ARTIST, + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_GENRE, + MEDIA_CLASS_PLAYLIST, + MEDIA_CLASS_TRACK, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, MEDIA_TYPE_COMPOSER, @@ -103,6 +111,23 @@ EXPANDABLE_MEDIA_TYPES = [ SONOS_PLAYLISTS, ] +SONOS_TO_MEDIA_CLASSES = { + SONOS_ALBUM: MEDIA_CLASS_ALBUM, + SONOS_ALBUM_ARTIST: MEDIA_CLASS_ARTIST, + SONOS_ARTIST: MEDIA_CLASS_CONTRIBUTING_ARTIST, + SONOS_COMPOSER: MEDIA_CLASS_COMPOSER, + SONOS_GENRE: MEDIA_CLASS_GENRE, + SONOS_PLAYLISTS: MEDIA_CLASS_PLAYLIST, + SONOS_TRACKS: MEDIA_CLASS_TRACK, + "object.container.album.musicAlbum": MEDIA_CLASS_ALBUM, + "object.container.genre.musicGenre": MEDIA_CLASS_PLAYLIST, + "object.container.person.composer": MEDIA_CLASS_PLAYLIST, + "object.container.person.musicArtist": MEDIA_CLASS_ARTIST, + "object.container.playlistContainer.sameArtist": MEDIA_CLASS_ARTIST, + "object.container.playlistContainer": MEDIA_CLASS_PLAYLIST, + "object.item.audioItem.musicTrack": MEDIA_CLASS_TRACK, +} + SONOS_TO_MEDIA_TYPES = { SONOS_ALBUM: MEDIA_TYPE_ALBUM, SONOS_ALBUM_ARTIST: MEDIA_TYPE_ARTIST, @@ -1462,9 +1487,12 @@ def build_item_response(media_library, payload): except IndexError: title = LIBRARY_TITLES_MAPPING[payload["idstring"]] + media_class = SONOS_TO_MEDIA_CLASSES[MEDIA_TYPES_TO_SONOS[payload["search_type"]]] + return BrowseMedia( title=title, thumbnail=thumbnail, + media_class=media_class, media_content_id=payload["idstring"], media_content_type=payload["search_type"], children=[item_payload(item) for item in media], @@ -1482,6 +1510,7 @@ def item_payload(item): return BrowseMedia( title=item.title, thumbnail=getattr(item, "album_art_uri", None), + media_class=SONOS_TO_MEDIA_CLASSES[get_media_type(item)], media_content_id=get_content_id(item), media_content_type=SONOS_TO_MEDIA_TYPES[get_media_type(item)], can_play=can_play(item.item_class), @@ -1497,6 +1526,7 @@ def library_payload(media_library): """ return BrowseMedia( title="Music Library", + media_class=MEDIA_CLASS_DIRECTORY, media_content_id="library", media_content_type="library", can_play=False, diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index cc62cb9e276..91e0c85aa3c 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -11,6 +11,12 @@ from yarl import URL from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.const import ( + MEDIA_CLASS_ALBUM, + MEDIA_CLASS_ARTIST, + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_PLAYLIST, + MEDIA_CLASS_PODCAST, + MEDIA_CLASS_TRACK, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, MEDIA_TYPE_EPISODE, @@ -96,6 +102,29 @@ LIBRARY_MAP = { "new_releases": "New Releases", } +CONTENT_TYPE_MEDIA_CLASS = { + "current_user_playlists": MEDIA_CLASS_PLAYLIST, + "current_user_followed_artists": MEDIA_CLASS_ARTIST, + "current_user_saved_albums": MEDIA_CLASS_ALBUM, + "current_user_saved_tracks": MEDIA_CLASS_TRACK, + "current_user_saved_shows": MEDIA_CLASS_PODCAST, + "current_user_recently_played": MEDIA_CLASS_TRACK, + "current_user_top_artists": MEDIA_CLASS_ARTIST, + "current_user_top_tracks": MEDIA_CLASS_TRACK, + "featured_playlists": MEDIA_CLASS_PLAYLIST, + "categories": MEDIA_CLASS_DIRECTORY, + "category_playlists": MEDIA_CLASS_PLAYLIST, + "new_releases": MEDIA_CLASS_ALBUM, + MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, + MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, + MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, + MEDIA_TYPE_SHOW: MEDIA_CLASS_PODCAST, +} + + +class MissingMediaInformation(BrowseError): + """Missing media required information.""" + async def async_setup_entry( hass: HomeAssistant, @@ -498,24 +527,32 @@ def build_item_response(spotify, user, payload): return None if media_content_type == "categories": - return BrowseMedia( + media_item = BrowseMedia( title=LIBRARY_MAP.get(media_content_id), + media_class=CONTENT_TYPE_MEDIA_CLASS[media_content_type], media_content_id=media_content_id, media_content_type=media_content_type, can_play=False, can_expand=True, - children=[ + children=[], + ) + for item in items: + try: + item_id = item["id"] + except KeyError: + _LOGGER.debug("Missing id for media item: %s", item) + continue + media_item.children.append( BrowseMedia( title=item.get("name"), - media_content_id=item["id"], + media_class=MEDIA_CLASS_PLAYLIST, + media_content_id=item_id, media_content_type="category_playlists", thumbnail=fetch_image_url(item, key="icons"), can_play=False, can_expand=True, ) - for item in items - ], - ) + ) if title is None: if "name" in media: @@ -525,12 +562,18 @@ def build_item_response(spotify, user, payload): response = { "title": title, + "media_class": CONTENT_TYPE_MEDIA_CLASS[media_content_type], "media_content_id": media_content_id, "media_content_type": media_content_type, "can_play": media_content_type in PLAYABLE_MEDIA_TYPES, - "children": [item_payload(item) for item in items], + "children": [], "can_expand": True, } + for item in items: + try: + response["children"].append(item_payload(item)) + except MissingMediaInformation: + continue if "images" in media: response["thumbnail"] = fetch_image_url(media) @@ -546,20 +589,31 @@ def item_payload(item): Used by async_browse_media. """ + try: + media_type = item["type"] + media_id = item["uri"] + except KeyError as err: + _LOGGER.debug("Missing type or uri for media item: %s", item) + raise MissingMediaInformation from err - can_expand = item["type"] not in [ + can_expand = media_type not in [ MEDIA_TYPE_TRACK, MEDIA_TYPE_EPISODE, ] payload = { "title": item.get("name"), - "media_content_id": item["uri"], - "media_content_type": item["type"], - "can_play": item["type"] in PLAYABLE_MEDIA_TYPES, + "media_content_id": media_id, + "media_content_type": media_type, + "can_play": media_type in PLAYABLE_MEDIA_TYPES, "can_expand": can_expand, } + payload = { + **payload, + "media_class": CONTENT_TYPE_MEDIA_CLASS[media_type], + } + if "images" in item: payload["thumbnail"] = fetch_image_url(item) elif MEDIA_TYPE_ALBUM in item: @@ -576,6 +630,7 @@ def library_payload(): """ library_info = { "title": "Media Library", + "media_class": MEDIA_CLASS_DIRECTORY, "media_content_id": "library", "media_content_type": "library", "can_play": False, diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index eb387fcc6a3..68e0fcda1d8 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -2,6 +2,7 @@ import pytest from homeassistant.components import media_source +from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source import const from homeassistant.setup import async_setup_component @@ -77,6 +78,7 @@ async def test_websocket_browse_media(hass, hass_ws_client): domain=const.DOMAIN, identifier="/media", title="Local Media", + media_class=MEDIA_CLASS_DIRECTORY, media_content_type="listing", can_play=False, can_expand=True, diff --git a/tests/components/media_source/test_models.py b/tests/components/media_source/test_models.py index f951fcfb0c0..3d19edd722d 100644 --- a/tests/components/media_source/test_models.py +++ b/tests/components/media_source/test_models.py @@ -1,5 +1,9 @@ """Test Media Source model methods.""" -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_MUSIC, + MEDIA_TYPE_MUSIC, +) from homeassistant.components.media_source import const, models @@ -8,6 +12,7 @@ async def test_browse_media_as_dict(): base = models.BrowseMediaSource( domain=const.DOMAIN, identifier="media", + media_class=MEDIA_CLASS_DIRECTORY, media_content_type="folder", title="media/", can_play=False, @@ -17,6 +22,7 @@ async def test_browse_media_as_dict(): models.BrowseMediaSource( domain=const.DOMAIN, identifier="media/test.mp3", + media_class=MEDIA_CLASS_MUSIC, media_content_type=MEDIA_TYPE_MUSIC, title="test.mp3", can_play=True, @@ -26,12 +32,14 @@ async def test_browse_media_as_dict(): item = base.as_dict() assert item["title"] == "media/" + assert item["media_class"] == MEDIA_CLASS_DIRECTORY assert item["media_content_type"] == "folder" assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media" assert not item["can_play"] assert item["can_expand"] assert len(item["children"]) == 1 assert item["children"][0]["title"] == "test.mp3" + assert item["children"][0]["media_class"] == MEDIA_CLASS_MUSIC async def test_browse_media_parent_no_children(): @@ -39,6 +47,7 @@ async def test_browse_media_parent_no_children(): base = models.BrowseMediaSource( domain=const.DOMAIN, identifier="media", + media_class=MEDIA_CLASS_DIRECTORY, media_content_type="folder", title="media/", can_play=False, @@ -47,6 +56,7 @@ async def test_browse_media_parent_no_children(): item = base.as_dict() assert item["title"] == "media/" + assert item["media_class"] == MEDIA_CLASS_DIRECTORY assert item["media_content_type"] == "folder" assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media" assert not item["can_play"] From c9ec533aa5595ff9a616bc51a14c91b9dc67f3da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2020 02:41:17 -0500 Subject: [PATCH 005/185] Add the ability to reload bayesian platforms from yaml (#39771) --- homeassistant/components/bayesian/__init__.py | 3 + .../components/bayesian/binary_sensor.py | 5 ++ .../components/bayesian/services.yaml | 2 + .../components/bayesian/test_binary_sensor.py | 59 ++++++++++++++++++- tests/fixtures/bayesian/configuration.yaml | 10 ++++ 5 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/bayesian/services.yaml create mode 100644 tests/fixtures/bayesian/configuration.yaml diff --git a/homeassistant/components/bayesian/__init__.py b/homeassistant/components/bayesian/__init__.py index 971ff8427ac..485592dc5e4 100644 --- a/homeassistant/components/bayesian/__init__.py +++ b/homeassistant/components/bayesian/__init__.py @@ -1 +1,4 @@ """The bayesian component.""" + +DOMAIN = "bayesian" +PLATFORMS = ["binary_sensor"] diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 8d4dab62263..90540e456c5 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -25,8 +25,11 @@ from homeassistant.helpers.event import ( async_track_state_change_event, async_track_template_result, ) +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.template import result_as_boolean +from . import DOMAIN, PLATFORMS + ATTR_OBSERVATIONS = "observations" ATTR_OCCURRED_OBSERVATION_ENTITIES = "occurred_observation_entities" ATTR_PROBABILITY = "probability" @@ -106,6 +109,8 @@ def update_probability(prior, prob_given_true, prob_given_false): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Bayesian Binary sensor.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + name = config[CONF_NAME] observations = config[CONF_OBSERVATIONS] prior = config[CONF_PRIOR] diff --git a/homeassistant/components/bayesian/services.yaml b/homeassistant/components/bayesian/services.yaml new file mode 100644 index 00000000000..ec7313a8630 --- /dev/null +++ b/homeassistant/components/bayesian/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload all bayesian entities. diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 9e4983ab4d5..57c7f404c7f 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -1,15 +1,18 @@ """The test for the bayesian sensor platform.""" import json +from os import path import unittest -from homeassistant.components.bayesian import binary_sensor as bayesian +from homeassistant import config as hass_config +from homeassistant.components.bayesian import DOMAIN, binary_sensor as bayesian from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_RELOAD, STATE_UNKNOWN from homeassistant.setup import async_setup_component, setup_component +from tests.async_mock import patch from tests.common import get_test_home_assistant @@ -631,3 +634,55 @@ async def test_monitored_sensor_goes_away(hass): await hass.async_block_till_done() assert hass.states.get("binary_sensor.test_binary").state == "on" + + +async def test_reload(hass): + """Verify we can reload bayesian sensors.""" + + config = { + "binary_sensor": { + "name": "test", + "platform": "bayesian", + "observations": [ + { + "platform": "state", + "entity_id": "sensor.test_monitored", + "to_state": "on", + "prob_given_true": 0.9, + "prob_given_false": 0.4, + }, + ], + "prior": 0.2, + "probability_threshold": 0.32, + } + } + + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + assert hass.states.get("binary_sensor.test") + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "bayesian/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + assert hass.states.get("binary_sensor.test") is None + assert hass.states.get("binary_sensor.test2") + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/bayesian/configuration.yaml b/tests/fixtures/bayesian/configuration.yaml new file mode 100644 index 00000000000..56a490d4aec --- /dev/null +++ b/tests/fixtures/bayesian/configuration.yaml @@ -0,0 +1,10 @@ +binary_sensor: + - platform: bayesian + prior: 0.1 + observations: + - entity_id: 'switch.kitchen_lights' + prob_given_true: 0.6 + prob_given_false: 0.2 + platform: 'state' + to_state: 'on' + name: test2 From f34e831650a0a5cce65774f50fd3f6fe6cf47eea Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 8 Sep 2020 11:59:39 +0200 Subject: [PATCH 006/185] Remove invalidation version from panel_custom (#39782) --- homeassistant/components/panel_custom/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/panel_custom/__init__.py b/homeassistant/components/panel_custom/__init__.py index e663665675c..a2175253ce1 100644 --- a/homeassistant/components/panel_custom/__init__.py +++ b/homeassistant/components/panel_custom/__init__.py @@ -50,7 +50,7 @@ CONFIG_SCHEMA = vol.Schema( cv.ensure_list, [ vol.All( - cv.deprecated(CONF_WEBCOMPONENT_PATH, invalidation_version="0.115"), + cv.deprecated(CONF_WEBCOMPONENT_PATH), vol.Schema( { vol.Required(CONF_COMPONENT_NAME): cv.string, From f41d28335447c75dc0a548e234bd1217ebe71318 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Sep 2020 13:50:53 +0200 Subject: [PATCH 007/185] Restore miflora now that v0.7.0 is out (#39787) * add miflora again, reverts part of github.com/home-assistant/core/pull/37707 * edit CODEOWNERS --- CODEOWNERS | 2 +- homeassistant/components/miflora/manifest.json | 5 ++--- requirements_all.txt | 4 ++++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 6230904de7b..70d89c5e45e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -253,7 +253,7 @@ homeassistant/components/met/* @danielhiversen @thimic homeassistant/components/meteo_france/* @hacf-fr @oncleben31 @Quentame homeassistant/components/meteoalarm/* @rolfberkenbosch homeassistant/components/metoffice/* @MrHarcombe -homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel +homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel @basnijholt homeassistant/components/mikrotik/* @engrbm87 homeassistant/components/mill/* @danielhiversen homeassistant/components/min_max/* @fabaff diff --git a/homeassistant/components/miflora/manifest.json b/homeassistant/components/miflora/manifest.json index 96558c82fec..7795e8fb6f8 100644 --- a/homeassistant/components/miflora/manifest.json +++ b/homeassistant/components/miflora/manifest.json @@ -1,8 +1,7 @@ { - "disabled": "Dependency contains code that breaks Home Assistant.", "domain": "miflora", "name": "Mi Flora", "documentation": "https://www.home-assistant.io/integrations/miflora", - "requirements": ["bluepy==1.3.0", "miflora==0.6.0"], - "codeowners": ["@danielhiversen", "@ChristianKuehnel"] + "requirements": ["bluepy==1.3.0", "miflora==0.7.0"], + "codeowners": ["@danielhiversen", "@ChristianKuehnel", "@basnijholt"] } diff --git a/requirements_all.txt b/requirements_all.txt index f0198702f88..272ef7bcc10 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -363,6 +363,7 @@ blinkstick==1.1.8 blockchain==1.4.4 # homeassistant.components.decora +# homeassistant.components.miflora # bluepy==1.3.0 # homeassistant.components.bme680 @@ -917,6 +918,9 @@ meteofrance-api==0.1.1 # homeassistant.components.mfi mficlient==0.3.0 +# homeassistant.components.miflora +miflora==0.7.0 + # homeassistant.components.mill millheater==0.3.4 From 02600bf1902309e945997d49dffffbb342993257 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 8 Sep 2020 14:13:48 +0200 Subject: [PATCH 008/185] Fix Sonos issue (#39790) --- homeassistant/components/sonos/media_player.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 9a245f8d4d7..51287f9f288 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -1595,6 +1595,7 @@ def get_media(media_library, item_id, search_type): search_type, "/".join(item_id.split("/")[:-1]), full_album_art_uri=True, + max_items=0, ): if item.item_id == item_id: return item From 7f801faed107dc22a1b469a6cd88cddaa16179b4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 15:23:38 +0200 Subject: [PATCH 009/185] Copy instead of deepcopy the variables in a wait for trigger (#39796) --- homeassistant/helpers/script.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 604102e6af3..74660f8b391 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1,6 +1,5 @@ """Helpers to execute scripts.""" import asyncio -from copy import deepcopy from datetime import datetime, timedelta from functools import partial import itertools @@ -572,7 +571,7 @@ class _ScriptRun: "" if delay is None else f" (timeout: {timedelta(seconds=delay)})", ) - variables = deepcopy(self._variables) + variables = {**self._variables} self._variables["wait"] = {"remaining": delay, "trigger": None} async def async_done(variables, context=None): From 2a68952334386f021cae4b933bbfc30425c94f76 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 15:12:20 +0200 Subject: [PATCH 010/185] Some shelly fixes (#39798) --- .coveragerc | 1 - homeassistant/components/shelly/__init__.py | 2 +- homeassistant/components/shelly/manifest.json | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index fe7a8d74cbd..1a46607e567 100644 --- a/.coveragerc +++ b/.coveragerc @@ -762,7 +762,6 @@ omit = homeassistant/components/shelly/binary_sensor.py homeassistant/components/shelly/cover.py homeassistant/components/shelly/entity.py - homeassistant/components/shelly/cover.py homeassistant/components/shelly/light.py homeassistant/components/shelly/sensor.py homeassistant/components/shelly/switch.py diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 39fc34d6d56..83d5d7b9f3a 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers import aiohttp_client, device_registry, update_coordi from .const import DOMAIN -PLATFORMS = ["binary_sensor", "light", "sensor", "switch", "cover"] +PLATFORMS = ["binary_sensor", "cover", "light", "sensor", "switch"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 010ecf16a56..357f2c10fda 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -2,7 +2,7 @@ "domain": "shelly", "name": "Shelly", "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/shelly2", + "documentation": "https://www.home-assistant.io/integrations/shelly", "requirements": ["aioshelly==0.3.0"], "zeroconf": ["_http._tcp.local."], "codeowners": ["@balloob", "@bieniu"] From c6a7350db1f274d542961c5c28ddffabc62d50f2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 15:42:50 +0200 Subject: [PATCH 011/185] Remove HTML support from frontend (#39799) --- homeassistant/components/frontend/__init__.py | 67 ++++------- .../components/panel_custom/__init__.py | 106 ++++-------------- tests/components/frontend/test_init.py | 8 -- tests/components/panel_custom/test_init.py | 71 ------------ 4 files changed, 48 insertions(+), 204 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 2a741afcfbc..73eb24d08cd 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -70,8 +70,6 @@ MANIFEST_JSON = { DATA_PANELS = "frontend_panels" DATA_JS_VERSION = "frontend_js_version" -DATA_EXTRA_HTML_URL = "frontend_extra_html_url" -DATA_EXTRA_HTML_URL_ES5 = "frontend_extra_html_url_es5" DATA_EXTRA_MODULE_URL = "frontend_extra_module_url" DATA_EXTRA_JS_URL_ES5 = "frontend_extra_js_url_es5" @@ -91,29 +89,23 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.All( - cv.deprecated(CONF_EXTRA_HTML_URL, invalidation_version="0.115"), - cv.deprecated(CONF_EXTRA_HTML_URL_ES5, invalidation_version="0.115"), - vol.Schema( - { - vol.Optional(CONF_FRONTEND_REPO): cv.isdir, - vol.Optional(CONF_THEMES): vol.Schema( - {cv.string: {cv.string: cv.string}} - ), - vol.Optional(CONF_EXTRA_HTML_URL): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_EXTRA_MODULE_URL): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_EXTRA_JS_URL_ES5): vol.All( - cv.ensure_list, [cv.string] - ), - # We no longer use these options. - vol.Optional(CONF_EXTRA_HTML_URL_ES5): cv.match_all, - vol.Optional(CONF_JS_VERSION): cv.match_all, - }, - ), + DOMAIN: vol.Schema( + { + vol.Optional(CONF_FRONTEND_REPO): cv.isdir, + vol.Optional(CONF_THEMES): vol.Schema( + {cv.string: {cv.string: cv.string}} + ), + vol.Optional(CONF_EXTRA_MODULE_URL): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_EXTRA_JS_URL_ES5): vol.All( + cv.ensure_list, [cv.string] + ), + # We no longer use these options. + vol.Optional(CONF_EXTRA_HTML_URL): cv.match_all, + vol.Optional(CONF_EXTRA_HTML_URL_ES5): cv.match_all, + vol.Optional(CONF_JS_VERSION): cv.match_all, + }, ) }, extra=vol.ALLOW_EXTRA, @@ -220,17 +212,6 @@ def async_remove_panel(hass, frontend_url_path): hass.bus.async_fire(EVENT_PANELS_UPDATED) -@bind_hass -@callback -def add_extra_html_url(hass, url, es5=False): - """Register extra html url to load.""" - key = DATA_EXTRA_HTML_URL_ES5 if es5 else DATA_EXTRA_HTML_URL - url_set = hass.data.get(key) - if url_set is None: - url_set = hass.data[key] = set() - url_set.add(url) - - def add_extra_js_url(hass, url, es5=False): """Register extra js or module url to load.""" key = DATA_EXTRA_JS_URL_ES5 if es5 else DATA_EXTRA_MODULE_URL @@ -267,6 +248,13 @@ async def async_setup(hass, config): conf = config.get(DOMAIN, {}) + for key in (CONF_EXTRA_HTML_URL, CONF_EXTRA_HTML_URL_ES5, CONF_JS_VERSION): + if key in conf: + _LOGGER.error( + "Please remove %s from your frontend config. It is no longer supported", + key, + ) + repo_path = conf.get(CONF_FRONTEND_REPO) is_dev = repo_path is not None root_path = _frontend_root(repo_path) @@ -315,12 +303,6 @@ async def async_setup(hass, config): sidebar_icon="hass:hammer", ) - if DATA_EXTRA_HTML_URL not in hass.data: - hass.data[DATA_EXTRA_HTML_URL] = set() - - for url in conf.get(CONF_EXTRA_HTML_URL, []): - add_extra_html_url(hass, url, False) - if DATA_EXTRA_MODULE_URL not in hass.data: hass.data[DATA_EXTRA_MODULE_URL] = set() @@ -522,7 +504,6 @@ class IndexView(web_urldispatcher.AbstractResource): return web.Response( text=template.render( theme_color=MANIFEST_JSON["theme_color"], - extra_urls=hass.data[DATA_EXTRA_HTML_URL], extra_modules=hass.data[DATA_EXTRA_MODULE_URL], extra_js_es5=hass.data[DATA_EXTRA_JS_URL_ES5], ), diff --git a/homeassistant/components/panel_custom/__init__.py b/homeassistant/components/panel_custom/__init__.py index a2175253ce1..8bab4d019e6 100644 --- a/homeassistant/components/panel_custom/__init__.py +++ b/homeassistant/components/panel_custom/__init__.py @@ -1,6 +1,5 @@ """Register a custom front end panel.""" import logging -import os import voluptuous as vol @@ -15,7 +14,6 @@ CONF_SIDEBAR_TITLE = "sidebar_title" CONF_SIDEBAR_ICON = "sidebar_icon" CONF_URL_PATH = "url_path" CONF_CONFIG = "config" -CONF_WEBCOMPONENT_PATH = "webcomponent_path" CONF_JS_URL = "js_url" CONF_MODULE_URL = "module_url" CONF_EMBED_IFRAME = "embed_iframe" @@ -32,55 +30,34 @@ LEGACY_URL = "/api/panel_custom/{}" PANEL_DIR = "panels" -def url_validator(value): - """Validate required urls are specified.""" - has_js_url = CONF_JS_URL in value - has_html_url = CONF_WEBCOMPONENT_PATH in value - has_module_url = CONF_MODULE_URL in value - - if has_html_url and (has_js_url or has_module_url): - raise vol.Invalid("You cannot specify other urls besides a webcomponent path") - - return value - - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( cv.ensure_list, [ - vol.All( - cv.deprecated(CONF_WEBCOMPONENT_PATH), - vol.Schema( - { - vol.Required(CONF_COMPONENT_NAME): cv.string, - vol.Optional(CONF_SIDEBAR_TITLE): cv.string, - vol.Optional( - CONF_SIDEBAR_ICON, default=DEFAULT_ICON - ): cv.icon, - vol.Optional(CONF_URL_PATH): cv.string, - vol.Optional(CONF_CONFIG): dict, - vol.Optional( - CONF_WEBCOMPONENT_PATH, - ): cv.string, - vol.Optional( - CONF_JS_URL, - ): cv.string, - vol.Optional( - CONF_MODULE_URL, - ): cv.string, - vol.Optional( - CONF_EMBED_IFRAME, default=DEFAULT_EMBED_IFRAME - ): cv.boolean, - vol.Optional( - CONF_TRUST_EXTERNAL_SCRIPT, - default=DEFAULT_TRUST_EXTERNAL, - ): cv.boolean, - vol.Optional(CONF_REQUIRE_ADMIN, default=False): cv.boolean, - } - ), - url_validator, - ) + vol.Schema( + { + vol.Required(CONF_COMPONENT_NAME): cv.string, + vol.Optional(CONF_SIDEBAR_TITLE): cv.string, + vol.Optional(CONF_SIDEBAR_ICON, default=DEFAULT_ICON): cv.icon, + vol.Optional(CONF_URL_PATH): cv.string, + vol.Optional(CONF_CONFIG): dict, + vol.Optional( + CONF_JS_URL, + ): cv.string, + vol.Optional( + CONF_MODULE_URL, + ): cv.string, + vol.Optional( + CONF_EMBED_IFRAME, default=DEFAULT_EMBED_IFRAME + ): cv.boolean, + vol.Optional( + CONF_TRUST_EXTERNAL_SCRIPT, + default=DEFAULT_TRUST_EXTERNAL, + ): cv.boolean, + vol.Optional(CONF_REQUIRE_ADMIN, default=False): cv.boolean, + } + ), ], ) }, @@ -98,8 +75,6 @@ async def async_register_panel( # Title/icon for sidebar sidebar_title=None, sidebar_icon=None, - # HTML source of your panel - html_url=None, # JS source of your panel js_url=None, # JS module of your panel @@ -114,16 +89,11 @@ async def async_register_panel( require_admin=False, ): """Register a new custom panel.""" - if js_url is None and html_url is None and module_url is None: + if js_url is None and module_url is None: raise ValueError("Either js_url, module_url or html_url is required.") - if html_url and (js_url or module_url): - raise ValueError("You cannot specify other paths with an HTML url") if config is not None and not isinstance(config, dict): raise ValueError("Config needs to be a dictionary.") - if html_url: - _LOGGER.warning("HTML custom panels have been deprecated") - custom_panel_config = { "name": webcomponent_name, "embed_iframe": embed_iframe, @@ -136,9 +106,6 @@ async def async_register_panel( if module_url is not None: custom_panel_config["module_url"] = module_url - if html_url is not None: - custom_panel_config["html_url"] = html_url - if config is not None: # Make copy because we're mutating it config = dict(config) @@ -162,8 +129,6 @@ async def async_setup(hass, config): if DOMAIN not in config: return True - seen = set() - for panel in config[DOMAIN]: name = panel[CONF_COMPONENT_NAME] @@ -184,29 +149,6 @@ async def async_setup(hass, config): if CONF_MODULE_URL in panel: kwargs["module_url"] = panel[CONF_MODULE_URL] - if CONF_MODULE_URL not in panel and CONF_JS_URL not in panel: - if name in seen: - _LOGGER.warning( - "Got HTML panel with duplicate name %s. Not registering", name - ) - continue - - seen.add(name) - panel_path = panel.get(CONF_WEBCOMPONENT_PATH) - - if panel_path is None: - panel_path = hass.config.path(PANEL_DIR, f"{name}.html") - - if not await hass.async_add_executor_job(os.path.isfile, panel_path): - _LOGGER.error( - "Unable to find webcomponent for %s: %s", name, panel_path - ) - continue - - url = LEGACY_URL.format(name) - hass.http.register_static_path(url, panel_path) - kwargs["html_url"] = url - try: await async_register_panel(hass, **kwargs) except ValueError as err: diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index a7ecbb0e5fe..7298ad753d2 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -334,14 +334,6 @@ async def test_missing_themes(hass, hass_ws_client): assert msg["result"]["themes"] == {} -async def test_extra_urls(mock_http_client_with_urls, mock_onboarded): - """Test that extra urls are loaded.""" - resp = await mock_http_client_with_urls.get("/lovelace?latest") - assert resp.status == 200 - text = await resp.text() - assert text.find('href="https://domain.com/my_extra_url.html"') >= 0 - - async def test_get_panels(hass, hass_ws_client, mock_http_client): """Test get_panels command.""" events = async_capture_events(hass, EVENT_PANELS_UPDATED) diff --git a/tests/components/panel_custom/test_init.py b/tests/components/panel_custom/test_init.py index caa55749c50..ddcb4079ef7 100644 --- a/tests/components/panel_custom/test_init.py +++ b/tests/components/panel_custom/test_init.py @@ -30,52 +30,6 @@ async def test_webcomponent_custom_path_not_found(hass): assert "nice_url" not in panels -async def test_webcomponent_custom_path(hass, caplog): - """Test if a web component is found in config panels dir.""" - filename = "mock.file" - - config = { - "panel_custom": [ - { - "name": "todo-mvc", - "webcomponent_path": filename, - "sidebar_title": "Sidebar Title", - "sidebar_icon": "mdi:iconicon", - "url_path": "nice_url", - "config": {"hello": "world"}, - }, - {"name": "todo-mvc"}, - ] - } - - with patch("os.path.isfile", Mock(return_value=True)): - with patch("os.access", Mock(return_value=True)): - result = await setup.async_setup_component(hass, "panel_custom", config) - assert result - - panels = hass.data.get(frontend.DATA_PANELS, []) - - assert panels - assert "nice_url" in panels - - panel = panels["nice_url"] - - assert panel.config == { - "hello": "world", - "_panel_custom": { - "html_url": "/api/panel_custom/todo-mvc", - "name": "todo-mvc", - "embed_iframe": False, - "trust_external": False, - }, - } - assert panel.frontend_url_path == "nice_url" - assert panel.sidebar_icon == "mdi:iconicon" - assert panel.sidebar_title == "Sidebar Title" - - assert "Got HTML panel with duplicate name todo-mvc. Not registering" in caplog.text - - async def test_js_webcomponent(hass): """Test if a web component is found in config panels dir.""" config = { @@ -188,31 +142,6 @@ async def test_latest_and_es5_build(hass): assert panel.frontend_url_path == "nice_url" -async def test_url_option_conflict(hass): - """Test config with multiple url options.""" - to_try = [ - { - "panel_custom": { - "name": "todo-mvc", - "webcomponent_path": "/local/bla.html", - "js_url": "/local/bla.js", - } - }, - { - "panel_custom": { - "name": "todo-mvc", - "webcomponent_path": "/local/bla.html", - "module_url": "/local/bla.js", - "js_url": "/local/bla.js", - } - }, - ] - - for config in to_try: - result = await setup.async_setup_component(hass, "panel_custom", config) - assert not result - - async def test_url_path_conflict(hass): """Test config with overlapping url path.""" assert await setup.async_setup_component( From 8dee5f4cf8155bdd4326ca286222f407753ee494 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 8 Sep 2020 15:52:04 +0200 Subject: [PATCH 012/185] Remove deprecated Hue configuration (#39800) --- homeassistant/components/hue/__init__.py | 98 +----------------- tests/components/hue/test_init.py | 124 ----------------------- 2 files changed, 1 insertion(+), 221 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index a99f9dd8a2a..fb277ee7a67 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -1,14 +1,11 @@ """Support for the Philips Hue system.""" -import ipaddress import logging from aiohue.util import normalize_bridge_id -import voluptuous as vol from homeassistant import config_entries, core from homeassistant.components import persistent_notification -from homeassistant.const import CONF_HOST -from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers import device_registry as dr from .bridge import HueBridge from .const import ( @@ -21,80 +18,10 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -CONF_BRIDGES = "bridges" - -DATA_CONFIGS = "hue_configs" - -PHUE_CONFIG_FILE = "phue.conf" - -BRIDGE_CONFIG_SCHEMA = vol.Schema( - { - # Validate as IP address and then convert back to a string. - vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string), - vol.Optional(CONF_ALLOW_UNREACHABLE): cv.boolean, - vol.Optional(CONF_ALLOW_HUE_GROUPS): cv.boolean, - vol.Optional("filename"): str, - } -) - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN, invalidation_version="0.115.0"), - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_BRIDGES): vol.All( - cv.ensure_list, - [BRIDGE_CONFIG_SCHEMA], - ) - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - async def async_setup(hass, config): """Set up the Hue platform.""" - conf = config.get(DOMAIN) - if conf is None: - conf = {} - hass.data[DOMAIN] = {} - hass.data[DATA_CONFIGS] = {} - - # User has not configured bridges - if CONF_BRIDGES not in conf: - return True - - bridges = conf[CONF_BRIDGES] - - configured_hosts = { - entry.data.get("host") for entry in hass.config_entries.async_entries(DOMAIN) - } - - for bridge_conf in bridges: - host = bridge_conf[CONF_HOST] - - # Store config in hass.data so the config entry can find it - hass.data[DATA_CONFIGS][host] = bridge_conf - - if host in configured_hosts: - continue - - # No existing config entry found, trigger link config flow. Because we're - # inside the setup of this component we'll have to use hass.async_add_job - # to avoid a deadlock: creating a config entry will set up the component - # but the setup would block till the entry is created! - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={"host": bridge_conf[CONF_HOST]}, - ) - ) - return True @@ -102,8 +29,6 @@ async def async_setup_entry( hass: core.HomeAssistant, entry: config_entries.ConfigEntry ): """Set up a bridge from a config entry.""" - host = entry.data["host"] - config = hass.data[DATA_CONFIGS].get(host) # Migrate allow_unreachable from config entry data to config entry options if ( @@ -133,27 +58,6 @@ async def async_setup_entry( data.pop(CONF_ALLOW_HUE_GROUPS) hass.config_entries.async_update_entry(entry, data=data, options=options) - # Overwrite from YAML configuration - if config is not None: - options = {} - if CONF_ALLOW_HUE_GROUPS in config and ( - CONF_ALLOW_HUE_GROUPS not in entry.options - or config[CONF_ALLOW_HUE_GROUPS] != entry.options[CONF_ALLOW_HUE_GROUPS] - ): - options[CONF_ALLOW_HUE_GROUPS] = config[CONF_ALLOW_HUE_GROUPS] - - if CONF_ALLOW_UNREACHABLE in config and ( - CONF_ALLOW_UNREACHABLE not in entry.options - or config[CONF_ALLOW_UNREACHABLE] != entry.options[CONF_ALLOW_UNREACHABLE] - ): - options[CONF_ALLOW_UNREACHABLE] = config[CONF_ALLOW_UNREACHABLE] - - if options: - hass.config_entries.async_update_entry( - entry, - options={**entry.options, **options}, - ) - bridge = HueBridge(hass, entry) if not await bridge.async_setup(): diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 1d6db3498f1..70a6c3b8756 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -31,130 +31,6 @@ async def test_setup_with_no_config(hass): assert hass.data[hue.DOMAIN] == {} -async def test_setup_defined_hosts_known_auth(hass): - """Test we don't initiate a config entry if config bridge is known.""" - MockConfigEntry(domain="hue", data={"host": "0.0.0.0"}).add_to_hass(hass) - - with patch.object(hue, "async_setup_entry", return_value=True): - assert ( - await async_setup_component( - hass, - hue.DOMAIN, - { - hue.DOMAIN: { - hue.CONF_BRIDGES: [ - { - hue.CONF_HOST: "0.0.0.0", - hue.CONF_ALLOW_HUE_GROUPS: False, - hue.CONF_ALLOW_UNREACHABLE: True, - }, - {hue.CONF_HOST: "1.1.1.1"}, - ] - } - }, - ) - is True - ) - - # Flow started for discovered bridge - assert len(hass.config_entries.flow.async_progress()) == 1 - - # Config stored for domain. - assert hass.data[hue.DATA_CONFIGS] == { - "0.0.0.0": { - hue.CONF_HOST: "0.0.0.0", - hue.CONF_ALLOW_HUE_GROUPS: False, - hue.CONF_ALLOW_UNREACHABLE: True, - }, - "1.1.1.1": {hue.CONF_HOST: "1.1.1.1"}, - } - - -async def test_setup_defined_hosts_no_known_auth(hass): - """Test we initiate config entry if config bridge is not known.""" - assert ( - await async_setup_component( - hass, - hue.DOMAIN, - { - hue.DOMAIN: { - hue.CONF_BRIDGES: { - hue.CONF_HOST: "0.0.0.0", - hue.CONF_ALLOW_HUE_GROUPS: False, - hue.CONF_ALLOW_UNREACHABLE: True, - } - } - }, - ) - is True - ) - - # Flow started for discovered bridge - assert len(hass.config_entries.flow.async_progress()) == 1 - - # Config stored for domain. - assert hass.data[hue.DATA_CONFIGS] == { - "0.0.0.0": { - hue.CONF_HOST: "0.0.0.0", - hue.CONF_ALLOW_HUE_GROUPS: False, - hue.CONF_ALLOW_UNREACHABLE: True, - } - } - - -async def test_config_passed_to_config_entry(hass): - """Test that configured options for a host are loaded via config entry.""" - entry = MockConfigEntry(domain=hue.DOMAIN, data={"host": "0.0.0.0"}) - entry.add_to_hass(hass) - mock_registry = Mock() - with patch.object(hue, "HueBridge") as mock_bridge, patch( - "homeassistant.helpers.device_registry.async_get_registry", - return_value=mock_registry, - ): - mock_bridge.return_value.async_setup = AsyncMock(return_value=True) - mock_bridge.return_value.api.config = Mock( - mac="mock-mac", - bridgeid="mock-bridgeid", - modelid="mock-modelid", - swversion="mock-swversion", - ) - # Can't set name via kwargs - mock_bridge.return_value.api.config.name = "mock-name" - assert ( - await async_setup_component( - hass, - hue.DOMAIN, - { - hue.DOMAIN: { - hue.CONF_BRIDGES: { - hue.CONF_HOST: "0.0.0.0", - hue.CONF_ALLOW_HUE_GROUPS: False, - hue.CONF_ALLOW_UNREACHABLE: True, - } - } - }, - ) - is True - ) - - assert len(mock_bridge.mock_calls) == 2 - p_hass, p_entry = mock_bridge.mock_calls[0][1] - - assert p_hass is hass - assert p_entry is entry - - assert len(mock_registry.mock_calls) == 1 - assert mock_registry.mock_calls[0][2] == { - "config_entry_id": entry.entry_id, - "connections": {("mac", "mock-mac")}, - "identifiers": {("hue", "mock-bridgeid")}, - "manufacturer": "Signify", - "name": "mock-name", - "model": "mock-modelid", - "sw_version": "mock-swversion", - } - - async def test_unload_entry(hass, mock_bridge_setup): """Test being able to unload an entry.""" entry = MockConfigEntry(domain=hue.DOMAIN, data={"host": "0.0.0.0"}) From 0d27e10d7779f2254014d191faf6704164ea58a7 Mon Sep 17 00:00:00 2001 From: Emilv2 Date: Tue, 8 Sep 2020 16:18:34 +0200 Subject: [PATCH 013/185] Bump pydelijn to 0.6.1 (#39802) --- homeassistant/components/delijn/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/delijn/manifest.json b/homeassistant/components/delijn/manifest.json index e3ab4f29512..1de62e8df0f 100644 --- a/homeassistant/components/delijn/manifest.json +++ b/homeassistant/components/delijn/manifest.json @@ -3,5 +3,5 @@ "name": "De Lijn", "documentation": "https://www.home-assistant.io/integrations/delijn", "codeowners": ["@bollewolle", "@Emilv2"], - "requirements": ["pydelijn==0.6.0"] + "requirements": ["pydelijn==0.6.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 272ef7bcc10..1a3afbfbba0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1298,7 +1298,7 @@ pydanfossair==0.1.0 pydeconz==72 # homeassistant.components.delijn -pydelijn==0.6.0 +pydelijn==0.6.1 # homeassistant.components.dexcom pydexcom==0.2.0 From fa0778700756c648cd25beefc11642ee0d3b7953 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2020 10:08:31 -0500 Subject: [PATCH 014/185] Fix cover template entities honoring titlecase True/False (#39803) --- homeassistant/components/template/cover.py | 7 ++-- homeassistant/components/template/light.py | 2 +- tests/components/template/test_cover.py | 41 ++++++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index e0a1b2bc33c..dc9e5ead1d0 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -249,15 +249,16 @@ class CoverTemplate(TemplateEntity, CoverEntity): self._position = None return - if result in _VALID_STATES: - if result in ("true", STATE_OPEN): + state = result.lower() + if state in _VALID_STATES: + if state in ("true", STATE_OPEN): self._position = 100 else: self._position = 0 else: _LOGGER.error( "Received invalid cover is_on state: %s. Expected: %s", - result, + state, ", ".join(_VALID_STATES), ) self._position = None diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 5f87301cce8..2b79846986c 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -412,7 +412,7 @@ class LightTemplate(TemplateEntity, LightEntity): self._available = True return - state = str(result).lower() + state = result.lower() if state in _VALID_STATES: self._state = state in ("true", STATE_ON) else: diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index d51d71648bf..5deb540782c 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -1079,3 +1079,44 @@ async def test_unique_id(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 + + +async def test_state_gets_lowercased(hass): + """Test True/False is lowercased.""" + + hass.states.async_set("binary_sensor.garage_door_sensor", "off") + + await setup.async_setup_component( + hass, + "cover", + { + "cover": { + "platform": "template", + "covers": { + "garage_door": { + "friendly_name": "Garage Door", + "value_template": "{{ is_state('binary_sensor.garage_door_sensor', 'off') }}", + "open_cover": { + "service": "cover.open_cover", + "entity_id": "cover.test_state", + }, + "close_cover": { + "service": "cover.close_cover", + "entity_id": "cover.test_state", + }, + }, + }, + }, + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 2 + + assert hass.states.get("cover.garage_door").state == STATE_OPEN + hass.states.async_set("binary_sensor.garage_door_sensor", "on") + await hass.async_block_till_done() + assert hass.states.get("cover.garage_door").state == STATE_CLOSED From f1de903fb5e884d5b1b040663daa28dcfa3ce1aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2020 10:12:23 -0500 Subject: [PATCH 015/185] Restore missing device_class to template binary_sensor (#39805) --- homeassistant/components/template/binary_sensor.py | 5 +++++ tests/components/template/test_binary_sensor.py | 11 +++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index bd6cb55b880..2e50448c037 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -194,3 +194,8 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): def is_on(self): """Return true if sensor is on.""" return self._state + + @property + def device_class(self): + """Return the sensor class of the binary sensor.""" + return self._device_class diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 7003b55c7ed..f9db8be37c6 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -6,6 +6,7 @@ from unittest import mock from homeassistant import setup from homeassistant.const import ( + ATTR_DEVICE_CLASS, EVENT_HOMEASSISTANT_START, STATE_OFF, STATE_ON, @@ -411,7 +412,10 @@ async def test_available_without_availability_template(hass): await hass.async_start() await hass.async_block_till_done() - assert hass.states.get("binary_sensor.test").state != STATE_UNAVAILABLE + state = hass.states.get("binary_sensor.test") + + assert state.state != STATE_UNAVAILABLE + assert state.attributes[ATTR_DEVICE_CLASS] == "motion" async def test_availability_template(hass): @@ -443,7 +447,10 @@ async def test_availability_template(hass): hass.states.async_set("sensor.test_state", STATE_ON) await hass.async_block_till_done() - assert hass.states.get("binary_sensor.test").state != STATE_UNAVAILABLE + state = hass.states.get("binary_sensor.test") + + assert state.state != STATE_UNAVAILABLE + assert state.attributes[ATTR_DEVICE_CLASS] == "motion" async def test_invalid_attribute_template(hass, caplog): From a5dec53e1bb881b652e10f3dc612680b45f44ccd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2020 10:31:08 -0500 Subject: [PATCH 016/185] Fix isy994 send_node_command (#39806) --- .../components/isy994/binary_sensor.py | 2 - homeassistant/components/isy994/climate.py | 2 - homeassistant/components/isy994/cover.py | 2 - homeassistant/components/isy994/fan.py | 2 - homeassistant/components/isy994/light.py | 3 +- homeassistant/components/isy994/lock.py | 2 - homeassistant/components/isy994/sensor.py | 2 - homeassistant/components/isy994/services.py | 46 +++++++++++-------- homeassistant/components/isy994/switch.py | 2 - homeassistant/helpers/entity_platform.py | 16 +++++++ homeassistant/helpers/reload.py | 10 +--- 11 files changed, 47 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index dc32fcef230..6355a9bcece 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -49,7 +49,6 @@ from .const import ( ) from .entity import ISYNodeEntity, ISYProgramEntity from .helpers import migrate_old_unique_ids -from .services import async_setup_device_services DEVICE_PARENT_REQUIRED = [ DEVICE_CLASS_OPENING, @@ -172,7 +171,6 @@ async def async_setup_entry( await migrate_old_unique_ids(hass, BINARY_SENSOR, devices) async_add_entities(devices) - async_setup_device_services(hass) def _detect_device_type_and_class(node: Union[Group, Node]) -> (str, str): diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 7dfd9a083d3..bb98c3d31bf 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -52,7 +52,6 @@ from .const import ( ) from .entity import ISYNodeEntity from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids -from .services import async_setup_device_services ISY_SUPPORTED_FEATURES = ( SUPPORT_FAN_MODE | SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE @@ -73,7 +72,6 @@ async def async_setup_entry( await migrate_old_unique_ids(hass, CLIMATE, entities) async_add_entities(entities) - async_setup_device_services(hass) class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index 41273f61f01..51a26585778 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -24,7 +24,6 @@ from .const import ( ) from .entity import ISYNodeEntity, ISYProgramEntity from .helpers import migrate_old_unique_ids -from .services import async_setup_device_services async def async_setup_entry( @@ -43,7 +42,6 @@ async def async_setup_entry( await migrate_old_unique_ids(hass, COVER, devices) async_add_entities(devices) - async_setup_device_services(hass) class ISYCoverEntity(ISYNodeEntity, CoverEntity): diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 96aa2144b1c..384ff22403a 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -18,7 +18,6 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS from .entity import ISYNodeEntity, ISYProgramEntity from .helpers import migrate_old_unique_ids -from .services import async_setup_device_services VALUE_TO_STATE = { 0: SPEED_OFF, @@ -51,7 +50,6 @@ async def async_setup_entry( await migrate_old_unique_ids(hass, FAN, devices) async_add_entities(devices) - async_setup_device_services(hass) class ISYFanEntity(ISYNodeEntity, FanEntity): diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 8d58a7f5796..7f94c68714d 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -20,7 +20,7 @@ from .const import ( ) from .entity import ISYNodeEntity from .helpers import migrate_old_unique_ids -from .services import async_setup_device_services, async_setup_light_services +from .services import async_setup_light_services ATTR_LAST_BRIGHTNESS = "last_brightness" @@ -41,7 +41,6 @@ async def async_setup_entry( await migrate_old_unique_ids(hass, LIGHT, devices) async_add_entities(devices) - async_setup_device_services(hass) async_setup_light_services(hass) diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index da50e4e704a..ceb26f3044c 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -10,7 +10,6 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS from .entity import ISYNodeEntity, ISYProgramEntity from .helpers import migrate_old_unique_ids -from .services import async_setup_device_services VALUE_TO_STATE = {0: False, 100: True} @@ -31,7 +30,6 @@ async def async_setup_entry( await migrate_old_unique_ids(hass, LOCK, devices) async_add_entities(devices) - async_setup_device_services(hass) class ISYLockEntity(ISYNodeEntity, LockEntity): diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 8ae646b2791..b4eba10fd34 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -19,7 +19,6 @@ from .const import ( ) from .entity import ISYEntity, ISYNodeEntity from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids -from .services import async_setup_device_services async def async_setup_entry( @@ -40,7 +39,6 @@ async def async_setup_entry( await migrate_old_unique_ids(hass, SENSOR, devices) async_add_entities(devices) - async_setup_device_services(hass) class ISYSensorEntity(ISYNodeEntity): diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index f59db1f5716..ee12cedbca4 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -13,9 +13,10 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, SERVICE_RELOAD, ) -from homeassistant.core import callback +from homeassistant.core import ServiceCall, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import async_get_platforms import homeassistant.helpers.entity_registry as er from homeassistant.helpers.typing import HomeAssistantType @@ -353,6 +354,30 @@ def async_setup_services(hass: HomeAssistantType): domain=DOMAIN, service=SERVICE_RELOAD, service_func=async_reload_config_entries ) + async def _async_send_raw_node_command(call: ServiceCall): + await hass.helpers.service.entity_service_call( + async_get_platforms(hass, DOMAIN), SERVICE_SEND_RAW_NODE_COMMAND, call + ) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_SEND_RAW_NODE_COMMAND, + schema=cv.make_entity_service_schema(SERVICE_SEND_RAW_NODE_COMMAND_SCHEMA), + service_func=_async_send_raw_node_command, + ) + + async def _async_send_node_command(call: ServiceCall): + await hass.helpers.service.entity_service_call( + async_get_platforms(hass, DOMAIN), SERVICE_SEND_NODE_COMMAND, call + ) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_SEND_NODE_COMMAND, + schema=cv.make_entity_service_schema(SERVICE_SEND_NODE_COMMAND_SCHEMA), + service_func=_async_send_node_command, + ) + @callback def async_unload_services(hass: HomeAssistantType): @@ -374,23 +399,8 @@ def async_unload_services(hass: HomeAssistantType): hass.services.async_remove(domain=DOMAIN, service=SERVICE_SET_VARIABLE) hass.services.async_remove(domain=DOMAIN, service=SERVICE_CLEANUP) hass.services.async_remove(domain=DOMAIN, service=SERVICE_RELOAD) - - -@callback -def async_setup_device_services(hass: HomeAssistantType): - """Create device-specific services for the ISY Integration.""" - platform = entity_platform.current_platform.get() - - platform.async_register_entity_service( - SERVICE_SEND_RAW_NODE_COMMAND, - SERVICE_SEND_RAW_NODE_COMMAND_SCHEMA, - SERVICE_SEND_RAW_NODE_COMMAND, - ) - platform.async_register_entity_service( - SERVICE_SEND_NODE_COMMAND, - SERVICE_SEND_NODE_COMMAND_SCHEMA, - SERVICE_SEND_NODE_COMMAND, - ) + hass.services.async_remove(domain=DOMAIN, service=SERVICE_SEND_RAW_NODE_COMMAND) + hass.services.async_remove(domain=DOMAIN, service=SERVICE_SEND_NODE_COMMAND) @callback diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index 68a3bdeecd2..28d2264f283 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -10,7 +10,6 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS from .entity import ISYNodeEntity, ISYProgramEntity from .helpers import migrate_old_unique_ids -from .services import async_setup_device_services async def async_setup_entry( @@ -29,7 +28,6 @@ async def async_setup_entry( await migrate_old_unique_ids(hass, SWITCH, devices) async_add_entities(devices) - async_setup_device_services(hass) class ISYSwitchEntity(ISYNodeEntity, SwitchEntity): diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 32ef88d361f..da1a3635d72 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -595,3 +595,19 @@ class EntityPlatform: current_platform: ContextVar[Optional[EntityPlatform]] = ContextVar( "current_platform", default=None ) + + +@callback +def async_get_platforms( + hass: HomeAssistantType, integration_name: str +) -> List[EntityPlatform]: + """Find existing platforms.""" + if ( + DATA_ENTITY_PLATFORM not in hass.data + or integration_name not in hass.data[DATA_ENTITY_PLATFORM] + ): + return [] + + platforms: List[EntityPlatform] = hass.data[DATA_ENTITY_PLATFORM][integration_name] + + return platforms diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 89fcf45c29a..1c11afdb46b 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -9,7 +9,7 @@ from homeassistant.const import SERVICE_RELOAD from homeassistant.core import Event, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform -from homeassistant.helpers.entity_platform import DATA_ENTITY_PLATFORM, EntityPlatform +from homeassistant.helpers.entity_platform import EntityPlatform, async_get_platforms from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component @@ -141,13 +141,7 @@ def async_get_platform( hass: HomeAssistantType, integration_name: str, integration_platform_name: str ) -> Optional[EntityPlatform]: """Find an existing platform.""" - if ( - DATA_ENTITY_PLATFORM not in hass.data - or integration_name not in hass.data[DATA_ENTITY_PLATFORM] - ): - return None - - for integration_platform in hass.data[DATA_ENTITY_PLATFORM][integration_name]: + for integration_platform in async_get_platforms(hass, integration_name): if integration_platform.domain == integration_platform_name: platform: EntityPlatform = integration_platform return platform From 9ca7efbe4cde30e04a8a69d7234a4514de9151d2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 15:35:41 +0000 Subject: [PATCH 017/185] Bumped version to 0.115.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index cb926177ba5..e18296d679f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -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 c2f16cf21d06a44cc7d8f7cd5c9631e6b936128a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Sep 2020 23:11:42 +0200 Subject: [PATCH 018/185] Fix MQTT light value template (#39820) --- .../components/mqtt/light/schema_basic.py | 4 +++ tests/components/mqtt/test_light.py | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 54af71b3e05..e19fcbf0e40 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -35,6 +35,7 @@ from homeassistant.const import ( CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_UNIQUE_ID, + CONF_VALUE_TEMPLATE, STATE_ON, ) from homeassistant.core import callback @@ -157,6 +158,9 @@ async def async_setup_entity_basic( hass, config, async_add_entities, config_entry, discovery_data=None ): """Set up a MQTT Light.""" + if CONF_STATE_VALUE_TEMPLATE not in config and CONF_VALUE_TEMPLATE in config: + config[CONF_STATE_VALUE_TEMPLATE] = config[CONF_VALUE_TEMPLATE] + async_add_entities([MqttLight(hass, config, config_entry, discovery_data)]) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 56a5b4012c8..da4d90ad5ec 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -648,6 +648,35 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): assert state.attributes.get("xy_color") == (0.14, 0.131) +async def test_controlling_state_via_topic_with_value_template(hass, mqtt_mock): + """Test the setting of the state with undocumented value_template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + "value_template": "{{ value_json.hello }}", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "test_light_rgb/status", '{"hello": "ON"}') + + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "test_light_rgb/status", '{"hello": "OFF"}') + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): """Test the sending of command in optimistic mode.""" config = { From 807bfb71dff88ba08ac2729d56c4155072f04864 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 8 Sep 2020 23:17:30 +0200 Subject: [PATCH 019/185] Update frontend to 20200908.0 (#39824) --- 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 65d0c6d3a21..379e08d30a7 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==20200907.0"], + "requirements": ["home-assistant-frontend==20200908.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5382e95b5b8..99654fafb68 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.36.1 -home-assistant-frontend==20200907.0 +home-assistant-frontend==20200908.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 1a3afbfbba0..22688907370 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200907.0 +home-assistant-frontend==20200908.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d9a40e0284c..b3b94a5c1da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200907.0 +home-assistant-frontend==20200908.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From d32e3dc31a6d9b552037dd916094c17c43acef76 Mon Sep 17 00:00:00 2001 From: Franchie <3265760+Franchie@users.noreply.github.com> Date: Tue, 8 Sep 2020 22:00:38 +0100 Subject: [PATCH 020/185] Avoid failing when hub does not provide cover position information (#39826) The powerview hub, seemingly randomly, will occasionally not provide data for cover positions. Some requests will return the desired response, but minutes later the same request might not. It appears this issue is being experienced by a number of people: https://community.home-assistant.io/t/hunter-douglas-powerview-component-expanding-this-api/88635/48 While an unfortunate bug with the hub, crashing the integration as a result of this missing data appears somewhat excessive. This patch adds a simple check to ensure the 'position' key has been returned by the hub before attempting to access its data. --- homeassistant/components/hunterdouglas_powerview/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 402451da26e..48b5f86cd02 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -215,7 +215,7 @@ class PowerViewShade(ShadeEntity, CoverEntity): def _async_update_current_cover_position(self): """Update the current cover position from the data.""" _LOGGER.debug("Raw data update: %s", self._shade.raw_data) - position_data = self._shade.raw_data[ATTR_POSITION_DATA] + position_data = self._shade.raw_data.get(ATTR_POSITION_DATA, {}) if ATTR_POSITION1 in position_data: self._current_cover_position = position_data[ATTR_POSITION1] self._is_opening = False From 6cadc5b1574c4929a523ba8375764fc6bb5d243e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 21:18:08 +0000 Subject: [PATCH 021/185] Bumped version to 0.115.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e18296d679f..33be5ab0c0a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -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 5165d746aac748d70166f275c9e3ec5e0bc8498d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 8 Sep 2020 23:22:44 +0200 Subject: [PATCH 022/185] Add missing sensors after reworking sensor platform in Shelly integration (#39765) --- .../components/shelly/binary_sensor.py | 15 +++++++- homeassistant/components/shelly/sensor.py | 37 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index c1c241a32ef..c9a13249aa8 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -3,6 +3,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_OPENING, + DEVICE_CLASS_PROBLEM, DEVICE_CLASS_SMOKE, DEVICE_CLASS_VIBRATION, BinarySensorEntity, @@ -15,8 +16,18 @@ from .entity import ( ) SENSORS = { - ("device", "overtemp"): BlockAttributeDescription(name="overtemp"), - ("relay", "overpower"): BlockAttributeDescription(name="overpower"), + ("device", "overtemp"): BlockAttributeDescription( + name="Overheating", device_class=DEVICE_CLASS_PROBLEM + ), + ("device", "overpower"): BlockAttributeDescription( + name="Over Power", device_class=DEVICE_CLASS_PROBLEM + ), + ("light", "overpower"): BlockAttributeDescription( + name="Over Power", device_class=DEVICE_CLASS_PROBLEM + ), + ("relay", "overpower"): BlockAttributeDescription( + name="Over Power", device_class=DEVICE_CLASS_PROBLEM + ), ("sensor", "dwIsOpened"): BlockAttributeDescription( name="Door", device_class=DEVICE_CLASS_OPENING ), diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 8e648a1a269..8a24a6380ed 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -40,6 +40,43 @@ SENSORS = { device_class=sensor.DEVICE_CLASS_POWER, default_enabled=False, ), + ("device", "power"): BlockAttributeDescription( + name="Power", + unit=POWER_WATT, + value=lambda value: round(value, 1), + device_class=sensor.DEVICE_CLASS_POWER, + ), + ("emeter", "power"): BlockAttributeDescription( + name="Power", + unit=POWER_WATT, + value=lambda value: round(value, 1), + device_class=sensor.DEVICE_CLASS_POWER, + ), + ("relay", "power"): BlockAttributeDescription( + name="Power", + unit=POWER_WATT, + value=lambda value: round(value, 1), + device_class=sensor.DEVICE_CLASS_POWER, + ), + ("device", "energy"): BlockAttributeDescription( + name="Energy", + unit=ENERGY_KILO_WATT_HOUR, + value=lambda value: round(value / 60 / 1000, 2), + device_class=sensor.DEVICE_CLASS_ENERGY, + ), + ("emeter", "energy"): BlockAttributeDescription( + name="Energy", + unit=ENERGY_KILO_WATT_HOUR, + value=lambda value: round(value / 1000, 2), + device_class=sensor.DEVICE_CLASS_ENERGY, + ), + ("light", "energy"): BlockAttributeDescription( + name="Energy", + unit=ENERGY_KILO_WATT_HOUR, + value=lambda value: round(value / 60 / 1000, 2), + device_class=sensor.DEVICE_CLASS_ENERGY, + default_enabled=False, + ), ("relay", "energy"): BlockAttributeDescription( name="Energy", unit=ENERGY_KILO_WATT_HOUR, From c91c9f2b409158996c3fa2474b37bb060cb5eed1 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 8 Sep 2020 23:42:45 +0200 Subject: [PATCH 023/185] Fix Kodi media browser (#39829) Co-authored-by: Paulus Schoutsen --- homeassistant/components/kodi/browse_media.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index 1b1576e82da..cae982078db 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -5,8 +5,8 @@ from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, MEDIA_CLASS_ARTIST, MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_EPISODE, MEDIA_CLASS_MOVIE, - MEDIA_CLASS_MUSIC, MEDIA_CLASS_PLAYLIST, MEDIA_CLASS_SEASON, MEDIA_CLASS_TV_SHOW, @@ -32,16 +32,18 @@ EXPANDABLE_MEDIA_TYPES = [ MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_SEASON, + "library_music", ] CONTENT_TYPE_MEDIA_CLASS = { - "library_music": MEDIA_CLASS_MUSIC, + "library_music": MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON, MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, MEDIA_TYPE_MOVIE: MEDIA_CLASS_MOVIE, MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW, + MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE, } @@ -187,16 +189,22 @@ def item_payload(item, media_library): media_content_id = "" title = item["label"] - can_play = media_content_type in PLAYABLE_MEDIA_TYPES and bool(media_content_id) + can_play = media_content_type in PLAYABLE_MEDIA_TYPES and media_content_id can_expand = media_content_type in EXPANDABLE_MEDIA_TYPES thumbnail = item.get("thumbnail") if thumbnail: thumbnail = media_library.thumbnail_url(thumbnail) + if media_content_type == MEDIA_TYPE_MOVIE and not media_content_id: + media_class = MEDIA_CLASS_DIRECTORY + can_expand = True + else: + media_class = CONTENT_TYPE_MEDIA_CLASS[media_content_type] + return BrowseMedia( title=title, - media_class=CONTENT_TYPE_MEDIA_CLASS[item["type"]], + media_class=media_class, media_content_type=media_content_type, media_content_id=media_content_id, can_play=can_play, From 0458b5e3a6bb12979fad6c81d940a8cfafe3ad9d Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 9 Sep 2020 07:56:40 -0500 Subject: [PATCH 024/185] Fix nzbget sensors (#39833) Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen --- .coveragerc | 1 - homeassistant/components/nzbget/sensor.py | 4 +- tests/components/nzbget/__init__.py | 20 ++++----- tests/components/nzbget/test_sensor.py | 54 +++++++++++++++++++++++ 4 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 tests/components/nzbget/test_sensor.py diff --git a/.coveragerc b/.coveragerc index 1a46607e567..a86b6312a77 100644 --- a/.coveragerc +++ b/.coveragerc @@ -589,7 +589,6 @@ omit = homeassistant/components/nut/sensor.py homeassistant/components/nx584/alarm_control_panel.py homeassistant/components/nzbget/coordinator.py - homeassistant/components/nzbget/sensor.py homeassistant/components/obihai/* homeassistant/components/octoprint/* homeassistant/components/oem/climate.py diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index c9437195826..ddbc73ca10a 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -61,7 +61,7 @@ async def async_setup_entry( ) ) - async_add_entities(sensors, True) + async_add_entities(sensors) class NZBGetSensor(NZBGetEntity, Entity): @@ -108,7 +108,7 @@ class NZBGetSensor(NZBGetEntity, Entity): @property def state(self): """Return the state of the sensor.""" - value = self.coordinator.data.status.get(self._sensor_type) + value = self.coordinator.data["status"].get(self._sensor_type) if value is None: _LOGGER.warning("Unable to locate value for %s", self._sensor_type) diff --git a/tests/components/nzbget/__init__.py b/tests/components/nzbget/__init__.py index 8da67e2a0a2..8a36b299d87 100644 --- a/tests/components/nzbget/__init__.py +++ b/tests/components/nzbget/__init__.py @@ -48,16 +48,16 @@ YAML_CONFIG = { MOCK_VERSION = "21.0" MOCK_STATUS = { - "ArticleCacheMB": "64", - "AverageDownloadRate": "512", - "DownloadPaused": "4", - "DownloadRate": "1000", - "DownloadedSizeMB": "256", - "FreeDiskSpaceMB": "1024", - "PostJobCount": "2", - "PostPaused": "4", - "RemainingSizeMB": "512", - "UpTimeSec": "600", + "ArticleCacheMB": 64, + "AverageDownloadRate": 1250000, + "DownloadPaused": 4, + "DownloadRate": 2500000, + "DownloadedSizeMB": 256, + "FreeDiskSpaceMB": 1024, + "PostJobCount": 2, + "PostPaused": 4, + "RemainingSizeMB": 512, + "UpTimeSec": 600, } MOCK_HISTORY = [ diff --git a/tests/components/nzbget/test_sensor.py b/tests/components/nzbget/test_sensor.py new file mode 100644 index 00000000000..43803384740 --- /dev/null +++ b/tests/components/nzbget/test_sensor.py @@ -0,0 +1,54 @@ +"""Test the NZBGet sensors.""" +from datetime import timedelta + +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + DATA_MEGABYTES, + DATA_RATE_MEGABYTES_PER_SECOND, + DEVICE_CLASS_TIMESTAMP, +) +from homeassistant.util import dt as dt_util + +from . import init_integration + +from tests.async_mock import patch + + +async def test_sensors(hass) -> None: + """Test the creation and values of the sensors.""" + now = dt_util.utcnow().replace(microsecond=0) + with patch("homeassistant.util.dt.utcnow", return_value=now): + entry = await init_integration(hass) + + registry = await hass.helpers.entity_registry.async_get_registry() + + uptime = now - timedelta(seconds=600) + + sensors = { + "article_cache": ("ArticleCacheMB", "64", DATA_MEGABYTES, None), + "average_speed": ( + "AverageDownloadRate", + "1.19", + DATA_RATE_MEGABYTES_PER_SECOND, + None, + ), + "download_paused": ("DownloadPaused", "4", None, None), + "speed": ("DownloadRate", "2.38", DATA_RATE_MEGABYTES_PER_SECOND, None), + "size": ("DownloadedSizeMB", "256", DATA_MEGABYTES, None), + "disk_free": ("FreeDiskSpaceMB", "1024", DATA_MEGABYTES, None), + "post_processing_jobs": ("PostJobCount", "2", "Jobs", None), + "post_processing_paused": ("PostPaused", "4", None, None), + "queue_size": ("RemainingSizeMB", "512", DATA_MEGABYTES, None), + "uptime": ("UpTimeSec", uptime.isoformat(), None, DEVICE_CLASS_TIMESTAMP), + } + + for (sensor_id, data) in sensors.items(): + entity_entry = registry.async_get(f"sensor.nzbgettest_{sensor_id}") + assert entity_entry + assert entity_entry.device_class == data[3] + assert entity_entry.unique_id == f"{entry.entry_id}_{data[0]}" + + state = hass.states.get(f"sensor.nzbgettest_{sensor_id}") + assert state + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == data[2] + assert state.state == data[1] From 139a0ca00836bc94ca76ef20b2cb388b7d4d1d4e Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 9 Sep 2020 14:12:11 +0200 Subject: [PATCH 025/185] Fix Kodi media browser (#39840) * Refactor * Make linter happy * Only return at the end * Handle exception --- homeassistant/components/kodi/browse_media.py | 112 +++++++++++------- 1 file changed, 67 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index cae982078db..3fc3b40cd38 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -1,14 +1,17 @@ """Support for media browsing.""" +import logging -from homeassistant.components.media_player import BrowseMedia +from homeassistant.components.media_player import BrowseError, BrowseMedia from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, MEDIA_CLASS_ARTIST, MEDIA_CLASS_DIRECTORY, MEDIA_CLASS_EPISODE, MEDIA_CLASS_MOVIE, + MEDIA_CLASS_MUSIC, MEDIA_CLASS_PLAYLIST, MEDIA_CLASS_SEASON, + MEDIA_CLASS_TRACK, MEDIA_CLASS_TV_SHOW, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, @@ -26,26 +29,24 @@ PLAYABLE_MEDIA_TYPES = [ MEDIA_TYPE_TRACK, ] -EXPANDABLE_MEDIA_TYPES = [ - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_TVSHOW, - MEDIA_TYPE_SEASON, - "library_music", -] - CONTENT_TYPE_MEDIA_CLASS = { - "library_music": MEDIA_CLASS_DIRECTORY, + "library_music": MEDIA_CLASS_MUSIC, MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON, MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, MEDIA_TYPE_MOVIE: MEDIA_CLASS_MOVIE, MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, + MEDIA_TYPE_TRACK: MEDIA_CLASS_TRACK, MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW, MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE, } +_LOGGER = logging.getLogger(__name__) + + +class UnknownMediaType(BrowseError): + """Unknown media type.""" + async def build_item_response(media_library, payload): """Create response payload for the provided media query.""" @@ -141,16 +142,23 @@ async def build_item_response(media_library, payload): title = season["seasondetails"]["label"] if media is None: - return + return None + + children = [] + for item in media: + try: + children.append(item_payload(item, media_library)) + except UnknownMediaType: + pass return BrowseMedia( media_class=CONTENT_TYPE_MEDIA_CLASS[search_type], - media_content_id=payload["search_id"], + media_content_id=search_id, media_content_type=search_type, title=title, can_play=search_type in PLAYABLE_MEDIA_TYPES and search_id, can_expand=True, - children=[item_payload(item, media_library) for item in media], + children=children, thumbnail=thumbnail, ) @@ -161,46 +169,60 @@ def item_payload(item, media_library): Used by async_browse_media. """ - if "songid" in item: - media_content_type = MEDIA_TYPE_TRACK - media_content_id = f"{item['songid']}" - elif "albumid" in item: - media_content_type = MEDIA_TYPE_ALBUM - media_content_id = f"{item['albumid']}" - elif "artistid" in item: - media_content_type = MEDIA_TYPE_ARTIST - media_content_id = f"{item['artistid']}" - elif "movieid" in item: - media_content_type = MEDIA_TYPE_MOVIE - media_content_id = f"{item['movieid']}" - elif "episodeid" in item: - media_content_type = MEDIA_TYPE_EPISODE - media_content_id = f"{item['episodeid']}" - elif "seasonid" in item: - media_content_type = MEDIA_TYPE_SEASON - media_content_id = f"{item['tvshowid']}/{item['season']}" - elif "tvshowid" in item: - media_content_type = MEDIA_TYPE_TVSHOW - media_content_id = f"{item['tvshowid']}" - else: - # this case is for the top folder of each type - # possible content types: album, artist, movie, library_music, tvshow - media_content_type = item.get("type") - media_content_id = "" - title = item["label"] - can_play = media_content_type in PLAYABLE_MEDIA_TYPES and media_content_id - can_expand = media_content_type in EXPANDABLE_MEDIA_TYPES thumbnail = item.get("thumbnail") if thumbnail: thumbnail = media_library.thumbnail_url(thumbnail) - if media_content_type == MEDIA_TYPE_MOVIE and not media_content_id: - media_class = MEDIA_CLASS_DIRECTORY + if "songid" in item: + media_content_type = MEDIA_TYPE_TRACK + media_content_id = f"{item['songid']}" + can_play = True + can_expand = False + elif "albumid" in item: + media_content_type = MEDIA_TYPE_ALBUM + media_content_id = f"{item['albumid']}" + can_play = True + can_expand = True + elif "artistid" in item: + media_content_type = MEDIA_TYPE_ARTIST + media_content_id = f"{item['artistid']}" + can_play = True + can_expand = True + elif "movieid" in item: + media_content_type = MEDIA_TYPE_MOVIE + media_content_id = f"{item['movieid']}" + can_play = True + can_expand = False + elif "episodeid" in item: + media_content_type = MEDIA_TYPE_EPISODE + media_content_id = f"{item['episodeid']}" + can_play = True + can_expand = False + elif "seasonid" in item: + media_content_type = MEDIA_TYPE_SEASON + media_content_id = f"{item['tvshowid']}/{item['season']}" + can_play = False + can_expand = True + elif "tvshowid" in item: + media_content_type = MEDIA_TYPE_TVSHOW + media_content_id = f"{item['tvshowid']}" + can_play = False can_expand = True else: + # this case is for the top folder of each type + # possible content types: album, artist, movie, library_music, tvshow + media_content_type = item["type"] + media_content_id = "" + can_play = False + can_expand = True + + try: media_class = CONTENT_TYPE_MEDIA_CLASS[media_content_type] + except KeyError as err: + _LOGGER.debug("Unknown media type received: %s", media_content_type) + raise UnknownMediaType from err return BrowseMedia( title=title, From b572c0df7fda1eb8b0de574229b15c6e80b85df1 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 9 Sep 2020 14:48:28 +0200 Subject: [PATCH 026/185] Make spotify media class lookup more robust (#39841) --- .../components/spotify/media_player.py | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 91e0c85aa3c..eb05fcc4ddc 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -14,6 +14,7 @@ from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, MEDIA_CLASS_ARTIST, MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_EPISODE, MEDIA_CLASS_PLAYLIST, MEDIA_CLASS_PODCAST, MEDIA_CLASS_TRACK, @@ -118,7 +119,9 @@ CONTENT_TYPE_MEDIA_CLASS = { MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, + MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE, MEDIA_TYPE_SHOW: MEDIA_CLASS_PODCAST, + MEDIA_TYPE_TRACK: MEDIA_CLASS_TRACK, } @@ -126,6 +129,10 @@ class MissingMediaInformation(BrowseError): """Missing media required information.""" +class UnknownMediaType(BrowseError): + """Unknown media type.""" + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -526,10 +533,16 @@ def build_item_response(spotify, user, payload): if media is None: return None + try: + media_class = CONTENT_TYPE_MEDIA_CLASS[media_content_type] + except KeyError: + _LOGGER.debug("Unknown media type received: %s", media_content_type) + return None + if media_content_type == "categories": media_item = BrowseMedia( title=LIBRARY_MAP.get(media_content_id), - media_class=CONTENT_TYPE_MEDIA_CLASS[media_content_type], + media_class=media_class, media_content_id=media_content_id, media_content_type=media_content_type, can_play=False, @@ -562,7 +575,7 @@ def build_item_response(spotify, user, payload): response = { "title": title, - "media_class": CONTENT_TYPE_MEDIA_CLASS[media_content_type], + "media_class": media_class, "media_content_id": media_content_id, "media_content_type": media_content_type, "can_play": media_content_type in PLAYABLE_MEDIA_TYPES, @@ -572,7 +585,7 @@ def build_item_response(spotify, user, payload): for item in items: try: response["children"].append(item_payload(item)) - except MissingMediaInformation: + except (MissingMediaInformation, UnknownMediaType): continue if "images" in media: @@ -596,6 +609,12 @@ def item_payload(item): _LOGGER.debug("Missing type or uri for media item: %s", item) raise MissingMediaInformation from err + try: + media_class = CONTENT_TYPE_MEDIA_CLASS[media_type] + except KeyError as err: + _LOGGER.debug("Unknown media type received: %s", media_type) + raise UnknownMediaType from err + can_expand = media_type not in [ MEDIA_TYPE_TRACK, MEDIA_TYPE_EPISODE, @@ -611,7 +630,7 @@ def item_payload(item): payload = { **payload, - "media_class": CONTENT_TYPE_MEDIA_CLASS[media_type], + "media_class": media_class, } if "images" in item: From 1333e23c23a9a9558ebb1a4fab92d12486a70057 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 9 Sep 2020 13:15:53 +0000 Subject: [PATCH 027/185] Bumped version to 0.115.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 33be5ab0c0a..c038d06bce2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -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 4af6804c50c35e13872e4db07a5cf96e98e0b755 Mon Sep 17 00:00:00 2001 From: Colin Frei Date: Wed, 9 Sep 2020 20:19:37 +0200 Subject: [PATCH 028/185] Use correct URL for Fitbit callbacks (#39823) --- homeassistant/components/fitbit/sensor.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 56afae1e9a7..f0914ab35f0 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -185,7 +185,9 @@ def request_app_setup(hass, config, add_entities, config_path, discovery_info=No else: setup_platform(hass, config, add_entities, discovery_info) - start_url = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}" + start_url = ( + f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}" + ) description = f"""Please create a Fitbit developer app at https://dev.fitbit.com/apps/new. @@ -220,7 +222,7 @@ def request_oauth_completion(hass): def fitbit_configuration_callback(callback_data): """Handle configuration updates.""" - start_url = f"{get_url(hass)}{FITBIT_AUTH_START}" + start_url = f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_START}" description = f"Please authorize Fitbit by visiting {start_url}" @@ -312,7 +314,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): config_file.get(CONF_CLIENT_ID), config_file.get(CONF_CLIENT_SECRET) ) - redirect_uri = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}" + redirect_uri = ( + f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}" + ) fitbit_auth_start_url, _ = oauth.authorize_token_url( redirect_uri=redirect_uri, @@ -357,7 +361,7 @@ class FitbitAuthCallbackView(HomeAssistantView): result = None if data.get("code") is not None: - redirect_uri = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}" + redirect_uri = f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}" try: result = self.oauth.fetch_access_token(data.get("code"), redirect_uri) From 8e3e2d436e40910e8467ce9b00daf59d7eca4eaf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Sep 2020 15:19:14 -0500 Subject: [PATCH 029/185] Use a unique id for each icmplib ping to avoid mixing unrelated responses (#39830) --- homeassistant/components/ping/__init__.py | 24 +++++++++++++++++++ .../components/ping/binary_sensor.py | 23 ++++++++++++------ .../components/ping/device_tracker.py | 13 ++++++++-- homeassistant/components/ping/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 54 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ping/__init__.py b/homeassistant/components/ping/__init__.py index d5ec35276cf..19207ada1b7 100644 --- a/homeassistant/components/ping/__init__.py +++ b/homeassistant/components/ping/__init__.py @@ -1,4 +1,28 @@ """The ping component.""" +from homeassistant.core import callback + DOMAIN = "ping" PLATFORMS = ["binary_sensor"] + +PING_ID = "ping_id" +DEFAULT_START_ID = 129 +MAX_PING_ID = 65534 + + +@callback +def async_get_next_ping_id(hass): + """Find the next id to use in the outbound ping. + + Must be called in async + """ + current_id = hass.data.setdefault(DOMAIN, {}).get(PING_ID, DEFAULT_START_ID) + + if current_id == MAX_PING_ID: + next_id = DEFAULT_START_ID + else: + next_id = current_id + 1 + + hass.data[DOMAIN][PING_ID] = next_id + + return next_id diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index cb0d025f65d..5befe0b7f3a 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -1,6 +1,7 @@ """Tracks the latency of a host by sending ICMP echo requests (ping).""" import asyncio from datetime import timedelta +from functools import partial import logging import re import sys @@ -14,7 +15,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import setup_reload_service -from . import DOMAIN, PLATFORMS +from . import DOMAIN, PLATFORMS, async_get_next_ping_id from .const import PING_TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -131,20 +132,28 @@ class PingData: class PingDataICMPLib(PingData): """The Class for handling the data retrieval using icmplib.""" - def ping(self): - """Send ICMP echo request and return details.""" - return icmp_ping(self._ip_address, count=self._count) - async def async_update(self) -> None: """Retrieve the latest details from the host.""" - data = await self.hass.async_add_executor_job(self.ping) + _LOGGER.warning("ping address: %s", self._ip_address) + data = await self.hass.async_add_executor_job( + partial( + icmp_ping, + self._ip_address, + count=self._count, + id=async_get_next_ping_id(self.hass), + ) + ) + self.available = data.is_alive + if not self.available: + self.data = False + return + self.data = { "min": data.min_rtt, "max": data.max_rtt, "avg": data.avg_rtt, "mdev": "", } - self.available = data.is_alive class PingDataSubProcess(PingData): diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index cbbce13171b..ea2d7a526e3 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -15,8 +15,10 @@ from homeassistant.components.device_tracker.const import ( SOURCE_TYPE_ROUTER, ) import homeassistant.helpers.config_validation as cv +from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.process import kill_subprocess +from . import async_get_next_ping_id from .const import PING_ATTEMPTS_COUNT, PING_TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -76,15 +78,22 @@ class HostSubProcess: class HostICMPLib: """Host object with ping detection.""" - def __init__(self, ip_address, dev_id, _, config): + def __init__(self, ip_address, dev_id, hass, config): """Initialize the Host pinger.""" + self.hass = hass self.ip_address = ip_address self.dev_id = dev_id self._count = config[CONF_PING_COUNT] def ping(self): """Send an ICMP echo request and return True if success.""" - return icmp_ping(self.ip_address, count=PING_ATTEMPTS_COUNT).is_alive + next_id = run_callback_threadsafe( + self.hass.loop, async_get_next_ping_id, self.hass + ).result() + + return icmp_ping( + self.ip_address, count=PING_ATTEMPTS_COUNT, id=next_id + ).is_alive def update(self, see): """Update device state by sending one or more ping messages.""" diff --git a/homeassistant/components/ping/manifest.json b/homeassistant/components/ping/manifest.json index f23697808a2..675ee1a9586 100644 --- a/homeassistant/components/ping/manifest.json +++ b/homeassistant/components/ping/manifest.json @@ -3,6 +3,6 @@ "name": "Ping (ICMP)", "documentation": "https://www.home-assistant.io/integrations/ping", "codeowners": [], - "requirements": ["icmplib==1.1.1"], + "requirements": ["icmplib==1.1.3"], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index 22688907370..5d2a8e708df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -789,7 +789,7 @@ ibm-watson==4.0.1 ibmiotf==0.3.4 # homeassistant.components.ping -icmplib==1.1.1 +icmplib==1.1.3 # homeassistant.components.iglo iglo==1.2.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3b94a5c1da..876f391688c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -392,7 +392,7 @@ huawei-lte-api==1.4.12 iaqualink==0.3.4 # homeassistant.components.ping -icmplib==1.1.1 +icmplib==1.1.3 # homeassistant.components.influxdb influxdb-client==1.8.0 From 5ae0844f35b51a6270c4d20739472fd32c80d3ff Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 9 Sep 2020 22:30:40 +0200 Subject: [PATCH 030/185] Updated warning_device_warn (#39851) duty_cycle: spec says in inrements of 10 duration: its a 16 bit field --- homeassistant/components/zha/services.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index 971321fbfd2..257d1026f7f 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -132,11 +132,11 @@ warning_device_warn: example: "00:0d:6f:00:05:7d:2d:34" mode: description: >- - The Warning Mode field is used as an 4-bit enumeration, can have one of the values defined below in table 8-20 of the ZCL spec. The exact behavior of the WD device in each mode is according to the relevant security standards. + The Warning Mode field is used as an 4-bit enumeration, can have one of the values 0-6 defined below in table 8-20 of the ZCL spec. The exact behavior of the WD device in each mode is according to the relevant security standards. example: 1 strobe: description: >- - The Strobe field is used as a 2-bit enumeration, and determines if the visual indication is required in addition to the audible siren, as indicated in Table 8-21 of the ZCL spec. If the strobe field is “1” and the Warning Mode is “0” (“Stop”) then only the strobe is activated. + The Strobe field is used as a 2-bit enumeration, and determines if the visual indication is required in addition to the audible siren, as indicated in Table 8-21 of the ZCL spec. "0" means no strobe, "1" means strobe. If the strobe field is “1” and the Warning Mode is “0” (“Stop”) then only the strobe is activated. example: 1 level: description: >- @@ -144,12 +144,12 @@ warning_device_warn: example: 2 duration: description: >- - Requested duration of warning, in seconds. If both Strobe and Warning Mode are "0" this field SHALL be ignored. + Requested duration of warning, in seconds (16 bit). If both Strobe and Warning Mode are "0" this field SHALL be ignored. example: 2 duty_cycle: description: >- Indicates the length of the flash cycle. This provides a means of varying the flash duration for different alarm types (e.g., fire, police, burglar). Valid range is 0-100 in increments of 10. All other values SHALL be rounded to the nearest valid value. Strobe SHALL calculate duty cycle over a duration of one second. The ON state SHALL precede the OFF state. For example, if Strobe Duty Cycle Field specifies “40,” then the strobe SHALL flash ON for 4/10ths of a second and then turn OFF for 6/10ths of a second. - example: 2 + example: 50 intensity: description: >- Indicates the intensity of the strobe as shown in Table 8-23 of the ZCL spec. This attribute is designed to vary the output of the strobe (i.e., brightness) and not its frequency, which is detailed in section 8.4.2.3.1.6 of the ZCL spec. From 578c1b283a6673a328ba9b4316a6fde667af92e0 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Wed, 9 Sep 2020 16:19:30 -0400 Subject: [PATCH 031/185] Sort Local Media Source and fix media class (#39858) --- homeassistant/components/media_source/const.py | 11 +++++++++++ .../components/media_source/local_source.py | 13 ++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_source/const.py b/homeassistant/components/media_source/const.py index d50a8b1c404..68a8244c3ce 100644 --- a/homeassistant/components/media_source/const.py +++ b/homeassistant/components/media_source/const.py @@ -1,7 +1,18 @@ """Constants for the media_source integration.""" import re +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_IMAGE, + MEDIA_CLASS_MUSIC, + MEDIA_CLASS_VIDEO, +) + DOMAIN = "media_source" MEDIA_MIME_TYPES = ("audio", "video", "image") +MEDIA_CLASS_MAP = { + "audio": MEDIA_CLASS_MUSIC, + "video": MEDIA_CLASS_VIDEO, + "image": MEDIA_CLASS_IMAGE, +} URI_SCHEME = "media-source://" URI_SCHEME_REGEX = re.compile(r"^media-source://(?P[^/]+)?(?P.+)?") diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index 774ee64d852..a558de775f8 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -12,7 +12,7 @@ from homeassistant.components.media_source.error import Unresolvable from homeassistant.core import HomeAssistant, callback from homeassistant.util import sanitize_path -from .const import DOMAIN, MEDIA_MIME_TYPES +from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia @@ -112,11 +112,15 @@ class LocalSource(MediaSource): if is_dir: title += "/" + media_class = MEDIA_CLASS_MAP.get( + mime_type and mime_type.split("/")[0], MEDIA_CLASS_DIRECTORY + ) + media = BrowseMediaSource( domain=DOMAIN, identifier=f"{source_dir_id}/{path.relative_to(self.hass.config.path('media'))}", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type="directory", + media_class=media_class, + media_content_type=mime_type or "", title=title, can_play=is_file, can_expand=is_dir, @@ -132,6 +136,9 @@ class LocalSource(MediaSource): if child: media.children.append(child) + # Sort children showing directories first, then by name + media.children.sort(key=lambda child: (child.can_play, child.title)) + return media From f79ce7bd042e963ed637aa4132bc725742b987c0 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 9 Sep 2020 15:08:55 -0400 Subject: [PATCH 032/185] Update ZHA dependency (#39862) --- 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 1fd8bb71920..4bde073a933 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zha-quirks==0.0.44", "zigpy-cc==0.5.2", "zigpy-deconz==0.9.2", - "zigpy==0.23.1", + "zigpy==0.23.2", "zigpy-xbee==0.13.0", "zigpy-zigate==0.6.2", "zigpy-znp==0.1.1" diff --git a/requirements_all.txt b/requirements_all.txt index 5d2a8e708df..2490e6b366d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2314,7 +2314,7 @@ zigpy-zigate==0.6.2 zigpy-znp==0.1.1 # homeassistant.components.zha -zigpy==0.23.1 +zigpy==0.23.2 # homeassistant.components.zoneminder zm-py==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 876f391688c..618facba40a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1074,4 +1074,4 @@ zigpy-zigate==0.6.2 zigpy-znp==0.1.1 # homeassistant.components.zha -zigpy==0.23.1 +zigpy==0.23.2 From c5cf95c14bd9ba69ade22c9562943c16ebc36e8f Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 9 Sep 2020 22:19:52 +0200 Subject: [PATCH 033/185] Remove media class apps and channels (#39864) --- homeassistant/components/media_player/const.py | 2 -- homeassistant/components/media_source/models.py | 3 +-- homeassistant/components/philips_js/media_player.py | 3 +-- homeassistant/components/roku/media_player.py | 10 ++++------ 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 6714d03c19e..0035fc9f4d2 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -31,10 +31,8 @@ DOMAIN = "media_player" MEDIA_CLASS_ALBUM = "album" MEDIA_CLASS_APP = "app" -MEDIA_CLASS_APPS = "apps" MEDIA_CLASS_ARTIST = "artist" MEDIA_CLASS_CHANNEL = "channel" -MEDIA_CLASS_CHANNELS = "channels" MEDIA_CLASS_COMPOSER = "composer" MEDIA_CLASS_CONTRIBUTING_ARTIST = "contributing_artist" MEDIA_CLASS_DIRECTORY = "directory" diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index 5d768fd79d8..3b2044d7e0f 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -6,7 +6,6 @@ from typing import List, Optional, Tuple from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.const import ( MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_CHANNELS, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNELS, ) @@ -54,7 +53,7 @@ class MediaSourceItem: base = BrowseMediaSource( domain=None, identifier=None, - media_class=MEDIA_CLASS_CHANNELS, + media_class=MEDIA_CLASS_CHANNEL, media_content_type=MEDIA_TYPE_CHANNELS, title="Media Sources", can_play=False, diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index a780fe6b635..7da485fca74 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -12,7 +12,6 @@ from homeassistant.components.media_player import ( ) from homeassistant.components.media_player.const import ( MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_CHANNELS, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNELS, SUPPORT_BROWSE_MEDIA, @@ -290,7 +289,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): return BrowseMedia( title="Channels", - media_class=MEDIA_CLASS_CHANNELS, + media_class=MEDIA_CLASS_CHANNEL, media_content_id="", media_content_type=MEDIA_TYPE_CHANNELS, can_play=False, diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 7935206f114..3438b8c7add 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -12,9 +12,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.components.media_player.const import ( MEDIA_CLASS_APP, - MEDIA_CLASS_APPS, MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_CHANNELS, MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_APP, MEDIA_TYPE_APPS, @@ -95,7 +93,7 @@ def browse_media_library(channels: bool = False) -> BrowseMedia: library_info.children.append( BrowseMedia( title="Apps", - media_class=MEDIA_CLASS_APPS, + media_class=MEDIA_CLASS_APP, media_content_id="apps", media_content_type=MEDIA_TYPE_APPS, can_expand=True, @@ -107,7 +105,7 @@ def browse_media_library(channels: bool = False) -> BrowseMedia: library_info.children.append( BrowseMedia( title="Channels", - media_class=MEDIA_CLASS_CHANNELS, + media_class=MEDIA_CLASS_CHANNEL, media_content_id="channels", media_content_type=MEDIA_TYPE_CHANNELS, can_expand=True, @@ -294,7 +292,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): if media_content_type == MEDIA_TYPE_APPS: response = BrowseMedia( title="Apps", - media_class=MEDIA_CLASS_APPS, + media_class=MEDIA_CLASS_APP, media_content_id="apps", media_content_type=MEDIA_TYPE_APPS, can_expand=True, @@ -316,7 +314,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): if media_content_type == MEDIA_TYPE_CHANNELS: response = BrowseMedia( title="Channels", - media_class=MEDIA_CLASS_CHANNELS, + media_class=MEDIA_CLASS_CHANNEL, media_content_id="channels", media_content_type=MEDIA_TYPE_CHANNELS, can_expand=True, From 6d7dfc0804e151ce7087748f514f89644b21cdd2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 9 Sep 2020 20:35:18 +0000 Subject: [PATCH 034/185] Bumped version to 0.115.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c038d06bce2..a20d0effdaf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -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 4578baca3ec0b23799ff1c7320b8fc7522a76c40 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 9 Sep 2020 16:22:26 -0500 Subject: [PATCH 035/185] Improve Roku media browser structure (#39754) Co-authored-by: Martin Hjelmare --- homeassistant/components/roku/browse_media.py | 142 ++++++++++++++++++ homeassistant/components/roku/media_player.py | 97 +----------- 2 files changed, 149 insertions(+), 90 deletions(-) create mode 100644 homeassistant/components/roku/browse_media.py diff --git a/homeassistant/components/roku/browse_media.py b/homeassistant/components/roku/browse_media.py new file mode 100644 index 00000000000..809c6ac3578 --- /dev/null +++ b/homeassistant/components/roku/browse_media.py @@ -0,0 +1,142 @@ +"""Support for media browsing.""" + +from homeassistant.components.media_player import BrowseMedia +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_APP, + MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_DIRECTORY, + MEDIA_TYPE_APP, + MEDIA_TYPE_APPS, + MEDIA_TYPE_CHANNEL, + MEDIA_TYPE_CHANNELS, +) + +CONTENT_TYPE_MEDIA_CLASS = { + MEDIA_TYPE_APP: MEDIA_CLASS_APP, + MEDIA_TYPE_APPS: MEDIA_CLASS_APP, + MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL, + MEDIA_TYPE_CHANNELS: MEDIA_CLASS_CHANNEL, +} + +PLAYABLE_MEDIA_TYPES = [ + MEDIA_TYPE_APP, + MEDIA_TYPE_CHANNEL, +] + +EXPANDABLE_MEDIA_TYPES = [ + MEDIA_TYPE_APPS, + MEDIA_TYPE_CHANNELS, +] + + +def build_item_response(coordinator, payload): + """Create response payload for the provided media query.""" + search_id = payload["search_id"] + search_type = payload["search_type"] + + thumbnail = None + title = None + media = None + + if search_type == MEDIA_TYPE_APPS: + title = "Apps" + media = [ + {"app_id": item.app_id, "title": item.name, "type": MEDIA_TYPE_APP} + for item in coordinator.data.apps + ] + elif search_type == MEDIA_TYPE_CHANNELS: + title = "Channels" + media = [ + { + "channel_number": item.number, + "title": item.name, + "type": MEDIA_TYPE_CHANNEL, + } + for item in coordinator.data.channels + ] + + if media is None: + return None + + return BrowseMedia( + media_class=CONTENT_TYPE_MEDIA_CLASS[search_type], + media_content_id=search_id, + media_content_type=search_type, + title=title, + can_play=search_type in PLAYABLE_MEDIA_TYPES and search_id, + can_expand=True, + children=[item_payload(item, coordinator) for item in media], + thumbnail=thumbnail, + ) + + +def item_payload(item, coordinator): + """ + Create response payload for a single media item. + + Used by async_browse_media. + """ + thumbnail = None + + if "app_id" in item: + media_content_type = MEDIA_TYPE_APP + media_content_id = item["app_id"] + thumbnail = coordinator.roku.app_icon_url(item["app_id"]) + elif "channel_number" in item: + media_content_type = MEDIA_TYPE_CHANNEL + media_content_id = item["channel_number"] + else: + media_content_type = item["type"] + media_content_id = "" + + title = item["title"] + can_play = media_content_type in PLAYABLE_MEDIA_TYPES and media_content_id + can_expand = media_content_type in EXPANDABLE_MEDIA_TYPES + + return BrowseMedia( + title=title, + media_class=CONTENT_TYPE_MEDIA_CLASS[media_content_type], + media_content_type=media_content_type, + media_content_id=media_content_id, + can_play=can_play, + can_expand=can_expand, + thumbnail=thumbnail, + ) + + +def library_payload(coordinator): + """ + Create response payload to describe contents of a specific library. + + Used by async_browse_media. + """ + library_info = BrowseMedia( + media_class=MEDIA_CLASS_DIRECTORY, + media_content_id="library", + media_content_type="library", + title="Media Library", + can_play=False, + can_expand=True, + children=[], + ) + + library = { + MEDIA_TYPE_APPS: "Apps", + MEDIA_TYPE_CHANNELS: "Channels", + } + + for item in [{"title": name, "type": type_} for type_, name in library.items()]: + if ( + item["type"] == MEDIA_TYPE_CHANNELS + and coordinator.data.info.device_type != "tv" + ): + continue + + library_info.children.append( + item_payload( + {"title": item["title"], "type": item["type"]}, + coordinator, + ) + ) + + return library_info diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 3438b8c7add..0e035106824 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -7,17 +7,11 @@ import voluptuous as vol from homeassistant.components.media_player import ( DEVICE_CLASS_RECEIVER, DEVICE_CLASS_TV, - BrowseMedia, MediaPlayerEntity, ) from homeassistant.components.media_player.const import ( - MEDIA_CLASS_APP, - MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_APP, - MEDIA_TYPE_APPS, MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_CHANNELS, SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -42,6 +36,7 @@ from homeassistant.const import ( from homeassistant.helpers import entity_platform from . import RokuDataUpdateCoordinator, RokuEntity, roku_exception_handler +from .browse_media import build_item_response, library_payload from .const import ATTR_KEYWORD, DOMAIN, SERVICE_SEARCH _LOGGER = logging.getLogger(__name__) @@ -78,44 +73,6 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -def browse_media_library(channels: bool = False) -> BrowseMedia: - """Create response payload to describe contents of a specific library.""" - library_info = BrowseMedia( - title="Media Library", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_id="library", - media_content_type="library", - can_play=False, - can_expand=True, - children=[], - ) - - library_info.children.append( - BrowseMedia( - title="Apps", - media_class=MEDIA_CLASS_APP, - media_content_id="apps", - media_content_type=MEDIA_TYPE_APPS, - can_expand=True, - can_play=False, - ) - ) - - if channels: - library_info.children.append( - BrowseMedia( - title="Channels", - media_class=MEDIA_CLASS_CHANNEL, - media_content_id="channels", - media_content_type=MEDIA_TYPE_CHANNELS, - can_expand=True, - can_play=False, - ) - ) - - return library_info - - class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): """Representation of a Roku media player on the network.""" @@ -284,53 +241,13 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" if media_content_type in [None, "library"]: - is_tv = self.coordinator.data.info.device_type == "tv" - return browse_media_library(channels=is_tv) + return library_payload(self.coordinator) - response = None - - if media_content_type == MEDIA_TYPE_APPS: - response = BrowseMedia( - title="Apps", - media_class=MEDIA_CLASS_APP, - media_content_id="apps", - media_content_type=MEDIA_TYPE_APPS, - can_expand=True, - can_play=False, - children=[ - BrowseMedia( - title=app.name, - thumbnail=self.coordinator.roku.app_icon_url(app.app_id), - media_class=MEDIA_CLASS_APP, - media_content_id=app.app_id, - media_content_type=MEDIA_TYPE_APP, - can_play=True, - can_expand=False, - ) - for app in self.coordinator.data.apps - ], - ) - - if media_content_type == MEDIA_TYPE_CHANNELS: - response = BrowseMedia( - title="Channels", - media_class=MEDIA_CLASS_CHANNEL, - media_content_id="channels", - media_content_type=MEDIA_TYPE_CHANNELS, - can_expand=True, - can_play=False, - children=[ - BrowseMedia( - title=channel.name, - media_class=MEDIA_CLASS_CHANNEL, - media_content_id=channel.number, - media_content_type=MEDIA_TYPE_CHANNEL, - can_play=True, - can_expand=False, - ) - for channel in self.coordinator.data.channels - ], - ) + payload = { + "search_type": media_content_type, + "search_id": media_content_id, + } + response = build_item_response(self.coordinator, payload) if response is None: raise BrowseError( From be28dc0bca84e914c2ec3a67b84d96177f532783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 10 Sep 2020 14:52:49 +0200 Subject: [PATCH 036/185] Add exception for NoURLAvailableError in OAuth2FlowHandler (#39845) Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen --- homeassistant/components/almond/strings.json | 3 +- .../components/home_connect/strings.json | 3 +- homeassistant/components/netatmo/strings.json | 5 +- homeassistant/components/smappee/strings.json | 61 ++++++++++--------- homeassistant/components/somfy/strings.json | 3 +- homeassistant/components/spotify/strings.json | 1 + homeassistant/components/toon/strings.json | 3 +- .../components/withings/strings.json | 3 +- .../helpers/config_entry_oauth2_flow.py | 9 ++- homeassistant/strings.json | 3 +- .../helpers/test_config_entry_oauth2_flow.py | 17 ++++++ 11 files changed, 72 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/almond/strings.json b/homeassistant/components/almond/strings.json index 008d21c463b..e8244798e81 100644 --- a/homeassistant/components/almond/strings.json +++ b/homeassistant/components/almond/strings.json @@ -10,7 +10,8 @@ "abort": { "already_setup": "You can only configure one Almond account.", "cannot_connect": "Unable to connect to the Almond server.", - "missing_configuration": "Please check the documentation on how to set up Almond." + "missing_configuration": "Please check the documentation on how to set up Almond.", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" } } } diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 6125897c962..798fe2930a0 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -6,7 +6,8 @@ } }, "abort": { - "missing_configuration": "The Home Connect component is not configured. Please follow the documentation." + "missing_configuration": "The Home Connect component is not configured. Please follow the documentation.", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" }, "create_entry": { "default": "Successfully authenticated with Home Connect." diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index f1b761dd187..6e88d191610 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -8,7 +8,8 @@ "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", - "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]" + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" @@ -39,4 +40,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/smappee/strings.json b/homeassistant/components/smappee/strings.json index 9d4bb618832..1bec8fda0cc 100644 --- a/homeassistant/components/smappee/strings.json +++ b/homeassistant/components/smappee/strings.json @@ -1,34 +1,35 @@ { - "config": { - "flow_title": "Smappee: {name}", - "step": { - "environment": { - "description": "Set up your Smappee to integrate with Home Assistant.", - "data": { - "environment": "Environment" - } - }, - "local": { - "description": "Enter the host to initiate the Smappee local integration", - "data": { - "host": "[%key:common::config_flow::data::host%]" - } - }, - "zeroconf_confirm": { - "description": "Do you want to add the Smappee device with serialnumber `{serialnumber}` to Home Assistant?", - "title": "Discovered Smappee device" - }, - "pick_implementation": { - "title": "Pick Authentication Method" - } - }, - "abort": { - "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", - "already_configured_local_device": "Local device(s) is already configured. Please remove those first before configuring a cloud device.", - "authorize_url_timeout": "Timeout generating authorize url.", - "connection_error": "Failed to connect to Smappee device.", - "missing_configuration": "The component is not configured. Please follow the documentation.", - "invalid_mdns": "Unsupported device for the Smappee integration." + "config": { + "flow_title": "Smappee: {name}", + "step": { + "environment": { + "description": "Set up your Smappee to integrate with Home Assistant.", + "data": { + "environment": "Environment" } + }, + "local": { + "description": "Enter the host to initiate the Smappee local integration", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "zeroconf_confirm": { + "description": "Do you want to add the Smappee device with serialnumber `{serialnumber}` to Home Assistant?", + "title": "Discovered Smappee device" + }, + "pick_implementation": { + "title": "Pick Authentication Method" + } + }, + "abort": { + "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", + "already_configured_local_device": "Local device(s) is already configured. Please remove those first before configuring a cloud device.", + "authorize_url_timeout": "Timeout generating authorize url.", + "connection_error": "Failed to connect to Smappee device.", + "missing_configuration": "The component is not configured. Please follow the documentation.", + "invalid_mdns": "Unsupported device for the Smappee integration.", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" } + } } diff --git a/homeassistant/components/somfy/strings.json b/homeassistant/components/somfy/strings.json index 90ea98f7a87..d1fa921bb8e 100644 --- a/homeassistant/components/somfy/strings.json +++ b/homeassistant/components/somfy/strings.json @@ -6,7 +6,8 @@ "abort": { "already_setup": "You can only configure one Somfy account.", "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The Somfy component is not configured. Please follow the documentation." + "missing_configuration": "The Somfy component is not configured. Please follow the documentation.", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" }, "create_entry": { "default": "Successfully authenticated with Somfy." } } diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index 85ff9ff267b..8e3fa6fc679 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -10,6 +10,7 @@ "abort": { "already_setup": "You can only configure one Spotify account.", "authorize_url_timeout": "Timeout generating authorize url.", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "missing_configuration": "The Spotify integration is not configured. Please follow the documentation.", "reauth_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication." }, diff --git a/homeassistant/components/toon/strings.json b/homeassistant/components/toon/strings.json index 05eef817d28..c5ac07516b6 100644 --- a/homeassistant/components/toon/strings.json +++ b/homeassistant/components/toon/strings.json @@ -17,7 +17,8 @@ "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", - "no_agreements": "This account has no Toon displays." + "no_agreements": "This account has no Toon displays.", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" } } } diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index e7763a1db0c..c9d2d7ca22c 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -19,7 +19,8 @@ "abort": { "authorize_url_timeout": "Timeout generating authorize url.", "missing_configuration": "The Withings integration is not configured. Please follow the documentation.", - "already_configured": "Configuration updated for profile." + "already_configured": "Configuration updated for profile.", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" }, "create_entry": { "default": "Successfully authenticated with Withings." } } diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index da86c222c13..55ec3984b82 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -21,7 +21,7 @@ from yarl import URL from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.network import get_url +from homeassistant.helpers.network import NoURLAvailableError, get_url from .aiohttp_client import async_get_clientsession @@ -251,6 +251,13 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): url = await self.flow_impl.async_generate_authorize_url(self.flow_id) except asyncio.TimeoutError: return self.async_abort(reason="authorize_url_timeout") + except NoURLAvailableError: + return self.async_abort( + reason="no_url_available", + description_placeholders={ + "docs_url": "https://www.home-assistant.io/more-info/no-url-available" + }, + ) url = str(URL(url).update_query(self.extra_authorize_data)) diff --git a/homeassistant/strings.json b/homeassistant/strings.json index 63615094715..05bc2e3c247 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -53,7 +53,8 @@ "already_configured_device": "Device is already configured", "no_devices_found": "No devices found on the network", "oauth2_missing_configuration": "The component is not configured. Please follow the documentation.", - "oauth2_authorize_url_timeout": "Timeout generating authorize URL." + "oauth2_authorize_url_timeout": "Timeout generating authorize URL.", + "oauth2_no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})" } } } diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index dc34b0f7876..691b2e93d56 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -8,6 +8,7 @@ import pytest from homeassistant import config_entries, data_entry_flow, setup from homeassistant.config import async_process_ha_core_config from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.network import NoURLAvailableError from tests.async_mock import patch from tests.common import MockConfigEntry, mock_platform @@ -128,6 +129,22 @@ async def test_abort_if_authorization_timeout(hass, flow_handler, local_impl): assert result["reason"] == "authorize_url_timeout" +async def test_abort_if_no_url_available(hass, flow_handler, local_impl): + """Check no_url_available generating authorization url.""" + flow_handler.async_register_implementation(hass, local_impl) + + flow = flow_handler() + flow.hass = hass + + with patch.object( + local_impl, "async_generate_authorize_url", side_effect=NoURLAvailableError + ): + result = await flow.async_step_user() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_url_available" + + async def test_abort_if_oauth_error( hass, flow_handler, local_impl, aiohttp_client, aioclient_mock, current_request ): From fe371f04385d2b8357891f9bd7d5560e545b30de Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Sep 2020 00:36:58 +0200 Subject: [PATCH 037/185] Install stdlib-list in script/bootstrap (#39866) --- script/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index 2b599950625..3166b8c7701 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -8,4 +8,4 @@ cd "$(dirname "$0")/.." echo "Installing development dependencies..." python3 -m pip install wheel --constraint homeassistant/package_constraints.txt -python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) --constraint homeassistant/package_constraints.txt +python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) --constraint homeassistant/package_constraints.txt From 668c73010a7aa7422382ad4d58831cbcfa665dc6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Sep 2020 07:58:40 +0200 Subject: [PATCH 038/185] Disable Met.no hourly weather by default (#39867) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniel Hjelseth Høyer --- homeassistant/components/met/weather.py | 22 +++++++++++++----- tests/components/met/test_weather.py | 27 ++++++++++++++++++++++- tests/components/onboarding/test_views.py | 2 +- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index a53f66ab1dc..3355c497aab 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -104,7 +104,6 @@ class MetWeather(CoordinatorEntity, WeatherEntity): self._config = config self._is_metric = is_metric self._hourly = hourly - self._name_appendix = "-hourly" if hourly else "" @property def track_home(self): @@ -114,23 +113,34 @@ class MetWeather(CoordinatorEntity, WeatherEntity): @property def unique_id(self): """Return unique ID.""" + name_appendix = "" + if self._hourly: + name_appendix = "-hourly" if self.track_home: - return f"home{self._name_appendix}" + return f"home{name_appendix}" - return f"{self._config[CONF_LATITUDE]}-{self._config[CONF_LONGITUDE]}{self._name_appendix}" + return f"{self._config[CONF_LATITUDE]}-{self._config[CONF_LONGITUDE]}{name_appendix}" @property def name(self): """Return the name of the sensor.""" name = self._config.get(CONF_NAME) + name_appendix = "" + if self._hourly: + name_appendix = " Hourly" if name is not None: - return f"{name}{self._name_appendix}" + return f"{name}{name_appendix}" if self.track_home: - return f"{self.hass.config.location_name}{self._name_appendix}" + return f"{self.hass.config.location_name}{name_appendix}" - return f"{DEFAULT_NAME}{self._name_appendix}" + return f"{DEFAULT_NAME}{name_appendix}" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return not self._hourly @property def condition(self): diff --git a/tests/components/met/test_weather.py b/tests/components/met/test_weather.py index 064335a998c..242352c2498 100644 --- a/tests/components/met/test_weather.py +++ b/tests/components/met/test_weather.py @@ -1,13 +1,27 @@ """Test Met weather entity.""" +from homeassistant.components.met import DOMAIN +from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN + async def test_tracking_home(hass, mock_weather): """Test we track home.""" await hass.config_entries.flow.async_init("met", context={"source": "onboarding"}) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("weather")) == 2 + assert len(hass.states.async_entity_ids("weather")) == 1 assert len(mock_weather.mock_calls) == 4 + # Test the hourly sensor is disabled by default + registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("weather.test_home_hourly") + assert state is None + + entry = registry.async_get("weather.test_home_hourly") + assert entry + assert entry.disabled + assert entry.disabled_by == "integration" + # Test we track config await hass.config.async_update(latitude=10, longitude=20) await hass.async_block_till_done() @@ -21,6 +35,17 @@ async def test_tracking_home(hass, mock_weather): async def test_not_tracking_home(hass, mock_weather): """Test when we not track home.""" + + # Pre-create registry entry for disabled by default hourly weather + registry = await hass.helpers.entity_registry.async_get_registry() + registry.async_get_or_create( + WEATHER_DOMAIN, + DOMAIN, + "10-20-hourly", + suggested_object_id="somewhere_hourly", + disabled_by=None, + ) + await hass.config_entries.flow.async_init( "met", context={"source": "user"}, diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 8b8f9761bdb..0d425642622 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -276,4 +276,4 @@ async def test_onboarding_core_sets_up_met(hass, hass_storage, hass_client): assert resp.status == 200 await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("weather")) == 2 + assert len(hass.states.async_entity_ids("weather")) == 1 From 081bd22e596252ac1efe619082e4c0f118a570ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 10 Sep 2020 00:32:49 +0200 Subject: [PATCH 039/185] Updated frontend to 20200909.0 (#39869) Co-authored-by: Bram Kragten --- 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 379e08d30a7..7ccb606894a 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==20200908.0"], + "requirements": ["home-assistant-frontend==20200909.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 99654fafb68..300d56f5d47 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.36.1 -home-assistant-frontend==20200908.0 +home-assistant-frontend==20200909.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 2490e6b366d..7b0cccef3db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200908.0 +home-assistant-frontend==20200909.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 618facba40a..696bed5c751 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200908.0 +home-assistant-frontend==20200909.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From f0295d562d4a0320eb2441979e10dd5ca28f4c10 Mon Sep 17 00:00:00 2001 From: bsmappee <58250533+bsmappee@users.noreply.github.com> Date: Thu, 10 Sep 2020 10:27:07 +0200 Subject: [PATCH 040/185] Bump pysmappee to 0.2.13 (#39883) --- homeassistant/components/smappee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index 1d918c06cc0..c6cac118b72 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smappee", "dependencies": ["http"], "requirements": [ - "pysmappee==0.2.10" + "pysmappee==0.2.13" ], "codeowners": [ "@bsmappee" diff --git a/requirements_all.txt b/requirements_all.txt index 7b0cccef3db..a9e551418d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1632,7 +1632,7 @@ pyskyqhub==0.1.3 pysma==0.3.5 # homeassistant.components.smappee -pysmappee==0.2.10 +pysmappee==0.2.13 # homeassistant.components.smartthings pysmartapp==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 696bed5c751..f4f16a9aff5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -782,7 +782,7 @@ pysignalclirestapi==0.3.4 pysma==0.3.5 # homeassistant.components.smappee -pysmappee==0.2.10 +pysmappee==0.2.13 # homeassistant.components.smartthings pysmartapp==0.3.2 From 36f52a26f625a7d168ed69b1bf844de27a30927c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Sep 2020 11:25:56 +0200 Subject: [PATCH 041/185] Fix event trigger (#39884) Co-authored-by: Franck Nijhof --- .../components/homeassistant/triggers/event.py | 14 +++++++++----- .../homeassistant/triggers/test_event.py | 14 ++++++++++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index 8fa9207c3b0..800d6bb5f77 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -28,11 +28,15 @@ async def async_attach_trigger( ): """Listen for events based on configuration.""" event_type = config.get(CONF_EVENT_TYPE) - event_data_schema = ( - vol.Schema(config.get(CONF_EVENT_DATA), extra=vol.ALLOW_EXTRA) - if config.get(CONF_EVENT_DATA) - else None - ) + event_data_schema = None + if config.get(CONF_EVENT_DATA): + event_data_schema = vol.Schema( + { + vol.Required(key): value + for key, value in config.get(CONF_EVENT_DATA).items() + }, + extra=vol.ALLOW_EXTRA, + ) @callback def handle_event(event): diff --git a/tests/components/homeassistant/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py index cc9aecfdac4..84b7e725f0a 100644 --- a/tests/components/homeassistant/triggers/test_event.py +++ b/tests/components/homeassistant/triggers/test_event.py @@ -84,17 +84,27 @@ async def test_if_fires_on_event_with_data(hass, calls): "trigger": { "platform": "event", "event_type": "test_event", - "event_data": {"some_attr": "some_value"}, + "event_data": { + "some_attr": "some_value", + "second_attr": "second_value", + }, }, "action": {"service": "test.automation"}, } }, ) - hass.bus.async_fire("test_event", {"some_attr": "some_value", "another": "value"}) + hass.bus.async_fire( + "test_event", + {"some_attr": "some_value", "another": "value", "second_attr": "second_value"}, + ) await hass.async_block_till_done() assert len(calls) == 1 + hass.bus.async_fire("test_event", {"some_attr": "some_value", "another": "value"}) + await hass.async_block_till_done() + assert len(calls) == 1 # No new call + async def test_if_fires_on_event_with_empty_data_config(hass, calls): """Test the firing of events with empty data config. From b26ab2849b63d8a4a8e89247e7e75524ec4396d4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 10 Sep 2020 11:18:43 +0200 Subject: [PATCH 042/185] Bump hass-nabucasa 0.37.0 (#39885) --- 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 63afb402b9f..2da3562717d 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.36.1"], + "requirements": ["hass-nabucasa==0.37.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 300d56f5d47..53f22d3787b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.9.2 defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 -hass-nabucasa==0.36.1 +hass-nabucasa==0.37.0 home-assistant-frontend==20200909.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index a9e551418d8..b875fa5e4a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -720,7 +720,7 @@ habitipy==0.2.0 hangups==0.4.10 # homeassistant.components.cloud -hass-nabucasa==0.36.1 +hass-nabucasa==0.37.0 # homeassistant.components.jewish_calendar hdate==0.9.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4f16a9aff5..69b6d51416f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -352,7 +352,7 @@ ha-ffmpeg==2.0 hangups==0.4.10 # homeassistant.components.cloud -hass-nabucasa==0.36.1 +hass-nabucasa==0.37.0 # homeassistant.components.jewish_calendar hdate==0.9.5 From 896df60f3260815cb03f70b2a29ecad6ce73d1c3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Sep 2020 12:08:17 +0200 Subject: [PATCH 043/185] Shelly switch to guard for shelly 2 in roller mode (#39886) --- homeassistant/components/shelly/switch.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 1c3c48637e9..5550240478f 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -13,6 +13,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up switches for device.""" wrapper = hass.data[DOMAIN][config_entry.entry_id] + # In roller mode the relay blocks exist but do not contain required info + if wrapper.model == "SHSW-25" and wrapper.device.settings["mode"] != "relay": + return + relay_blocks = [block for block in wrapper.device.blocks if block.type == "relay"] if not relay_blocks: From 5098c3581474959dc76a8f7442972976e269dcbe Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 10 Sep 2020 12:06:18 +0200 Subject: [PATCH 044/185] Fix spotify media browser category (#39888) --- homeassistant/components/spotify/media_player.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index eb05fcc4ddc..e2850027229 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -15,6 +15,7 @@ from homeassistant.components.media_player.const import ( MEDIA_CLASS_ARTIST, MEDIA_CLASS_DIRECTORY, MEDIA_CLASS_EPISODE, + MEDIA_CLASS_GENRE, MEDIA_CLASS_PLAYLIST, MEDIA_CLASS_PODCAST, MEDIA_CLASS_TRACK, @@ -113,7 +114,7 @@ CONTENT_TYPE_MEDIA_CLASS = { "current_user_top_artists": MEDIA_CLASS_ARTIST, "current_user_top_tracks": MEDIA_CLASS_TRACK, "featured_playlists": MEDIA_CLASS_PLAYLIST, - "categories": MEDIA_CLASS_DIRECTORY, + "categories": MEDIA_CLASS_GENRE, "category_playlists": MEDIA_CLASS_PLAYLIST, "new_releases": MEDIA_CLASS_ALBUM, MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, @@ -566,6 +567,7 @@ def build_item_response(spotify, user, payload): can_expand=True, ) ) + return media_item if title is None: if "name" in media: From b7dacabbe40ed6035bc2dc1759bb63e714ff499b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 10 Sep 2020 15:56:05 +0200 Subject: [PATCH 045/185] Fix issue with grpcio build on 32bit arch (#39893) --- azure-pipelines-wheels.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 3e7821d77af..ac5f4fd824f 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -49,6 +49,7 @@ jobs: builderVersion: '$(versionWheels)' builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev' builderPip: 'Cython;numpy;scikit-build' + builderEnvFile: true skipBinary: 'aiohttp' wheelsRequirement: 'requirements_wheels.txt' wheelsRequirementDiff: 'requirements_diff.txt' @@ -90,4 +91,10 @@ jobs: sed -i "s|# bme680|bme680|g" ${requirement_file} sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} done + + # Write env for build settings + ( + echo "GRPC_BUILD_WITH_BORING_SSL_ASM=0" + echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=1" + ) > .env_file displayName: 'Prepare requirements files for Home Assistant wheels' From 209cf44e8e38f042236cdb01e9c1d2f312016145 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Sep 2020 20:41:42 +0200 Subject: [PATCH 046/185] Add default variables to script helper (#39895) --- .../components/automation/__init__.py | 27 +++++++- homeassistant/components/script/__init__.py | 5 +- homeassistant/const.py | 1 + homeassistant/helpers/config_validation.py | 3 + homeassistant/helpers/script.py | 26 +++++-- homeassistant/helpers/template.py | 17 ++++- homeassistant/helpers/typing.py | 4 +- tests/components/automation/test_init.py | 54 +++++++++++++++ tests/components/script/test_init.py | 68 ++++++++++++++++++- 9 files changed, 190 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index db97c3a321a..392ca710000 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_ID, CONF_MODE, CONF_PLATFORM, + CONF_VARIABLES, CONF_ZONE, EVENT_HOMEASSISTANT_STARTED, SERVICE_RELOAD, @@ -29,7 +30,7 @@ from homeassistant.core import ( split_entity_id, ) from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import condition, extract_domain_configs +from homeassistant.helpers import condition, extract_domain_configs, template import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent @@ -104,6 +105,7 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_HIDE_ENTITY): cv.boolean, vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA, vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA, + vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, }, SCRIPT_MODE_SINGLE, @@ -239,6 +241,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): cond_func, action_script, initial_state, + variables, ): """Initialize an automation entity.""" self._id = automation_id @@ -253,6 +256,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._referenced_entities: Optional[Set[str]] = None self._referenced_devices: Optional[Set[str]] = None self._logger = _LOGGER + self._variables = variables + self._variables_dynamic = template.is_complex(variables) @property def name(self): @@ -329,6 +334,9 @@ class AutomationEntity(ToggleEntity, RestoreEntity): """Startup with initial state or previous state.""" await super().async_added_to_hass() + if self._variables_dynamic: + template.attach(cast(HomeAssistant, self.hass), self._variables) + self._logger = logging.getLogger( f"{__name__}.{split_entity_id(self.entity_id)[1]}" ) @@ -378,11 +386,22 @@ class AutomationEntity(ToggleEntity, RestoreEntity): else: await self.async_disable() - async def async_trigger(self, variables, context=None, skip_condition=False): + async def async_trigger(self, run_variables, context=None, skip_condition=False): """Trigger automation. This method is a coroutine. """ + if self._variables: + if self._variables_dynamic: + variables = template.render_complex(self._variables, run_variables) + else: + variables = dict(self._variables) + else: + variables = {} + + if run_variables: + variables.update(run_variables) + if ( not skip_condition and self._cond_func is not None @@ -518,6 +537,9 @@ async def _async_process_config(hass, config, component): max_runs=config_block[CONF_MAX], max_exceeded=config_block[CONF_MAX_EXCEEDED], logger=_LOGGER, + # We don't pass variables here + # Automation will already render them to use them in the condition + # and so will pass them on to the script. ) if CONF_CONDITION in config_block: @@ -535,6 +557,7 @@ async def _async_process_config(hass, config, component): cond_func, action_script, initial_state, + config_block.get(CONF_VARIABLES), ) entities.append(entity) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 20f12361621..1e0fad9be5d 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_ICON, CONF_MODE, CONF_SEQUENCE, + CONF_VARIABLES, SERVICE_RELOAD, SERVICE_TOGGLE, SERVICE_TURN_OFF, @@ -59,6 +60,7 @@ SCRIPT_ENTRY_SCHEMA = make_script_schema( vol.Optional(CONF_ICON): cv.icon, vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA, vol.Optional(CONF_DESCRIPTION, default=""): cv.string, + vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, vol.Optional(CONF_FIELDS, default={}): { cv.string: { vol.Optional(CONF_DESCRIPTION): cv.string, @@ -75,7 +77,7 @@ CONFIG_SCHEMA = vol.Schema( SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) SCRIPT_TURN_ONOFF_SCHEMA = make_entity_service_schema( - {vol.Optional(ATTR_VARIABLES): dict} + {vol.Optional(ATTR_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA} ) RELOAD_SERVICE_SCHEMA = vol.Schema({}) @@ -263,6 +265,7 @@ class ScriptEntity(ToggleEntity): max_runs=cfg[CONF_MAX], max_exceeded=cfg[CONF_MAX_EXCEEDED], logger=logging.getLogger(f"{__name__}.{object_id}"), + variables=cfg.get(CONF_VARIABLES), ) self._changed = asyncio.Event() diff --git a/homeassistant/const.py b/homeassistant/const.py index a20d0effdaf..465f93c3f76 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -179,6 +179,7 @@ CONF_UNTIL = "until" CONF_URL = "url" CONF_USERNAME = "username" CONF_VALUE_TEMPLATE = "value_template" +CONF_VARIABLES = "variables" CONF_VERIFY_SSL = "verify_ssl" CONF_WAIT_FOR_TRIGGER = "wait_for_trigger" CONF_WAIT_TEMPLATE = "wait_template" diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index c3842c538d8..a54f97ec7e5 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -863,6 +863,9 @@ def make_entity_service_schema( ) +SCRIPT_VARIABLES_SCHEMA = vol.Schema({str: template_complex}) + + def script_action(value: Any) -> dict: """Validate a script action.""" if not isinstance(value, dict): diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 74660f8b391..cd664974431 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -53,11 +53,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, ) from homeassistant.core import SERVICE_CALL_LIMIT, Context, HomeAssistant, callback -from homeassistant.helpers import ( - condition, - config_validation as cv, - template as template, -) +from homeassistant.helpers import condition, config_validation as cv, template from homeassistant.helpers.event import async_call_later, async_track_template from homeassistant.helpers.service import ( CONF_SERVICE_DATA, @@ -721,6 +717,7 @@ class Script: logger: Optional[logging.Logger] = None, log_exceptions: bool = True, top_level: bool = True, + variables: Optional[Dict[str, Any]] = None, ) -> None: """Initialize the script.""" all_scripts = hass.data.get(DATA_SCRIPTS) @@ -759,6 +756,10 @@ class Script: self._choose_data: Dict[int, Dict[str, Any]] = {} self._referenced_entities: Optional[Set[str]] = None self._referenced_devices: Optional[Set[str]] = None + self.variables = variables + self._variables_dynamic = template.is_complex(variables) + if self._variables_dynamic: + template.attach(hass, variables) def _set_logger(self, logger: Optional[logging.Logger] = None) -> None: if logger: @@ -867,7 +868,7 @@ class Script: async def async_run( self, - variables: Optional[_VarsType] = None, + run_variables: Optional[_VarsType] = None, context: Optional[Context] = None, started_action: Optional[Callable[..., Any]] = None, ) -> None: @@ -898,8 +899,19 @@ class Script: # are read-only, but more importantly, so as not to leak any variables created # during the run back to the caller. if self._top_level: - variables = dict(variables) if variables is not None else {} + if self.variables: + if self._variables_dynamic: + variables = template.render_complex(self.variables, run_variables) + else: + variables = dict(self.variables) + else: + variables = {} + + if run_variables: + variables.update(run_variables) variables["context"] = context + else: + variables = cast(dict, run_variables) if self.script_mode != SCRIPT_MODE_QUEUED: cls = _ScriptRun diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index c771992caa4..917581fac07 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -65,7 +65,7 @@ def attach(hass: HomeAssistantType, obj: Any) -> None: if isinstance(obj, list): for child in obj: attach(hass, child) - elif isinstance(obj, dict): + elif isinstance(obj, collections.abc.Mapping): for child_key, child_value in obj.items(): attach(hass, child_key) attach(hass, child_value) @@ -77,7 +77,7 @@ def render_complex(value: Any, variables: TemplateVarsType = None) -> Any: """Recursive template creator helper function.""" if isinstance(value, list): return [render_complex(item, variables) for item in value] - if isinstance(value, dict): + if isinstance(value, collections.abc.Mapping): return { render_complex(key, variables): render_complex(item, variables) for key, item in value.items() @@ -88,6 +88,19 @@ def render_complex(value: Any, variables: TemplateVarsType = None) -> Any: return value +def is_complex(value: Any) -> bool: + """Test if data structure is a complex template.""" + if isinstance(value, Template): + return True + if isinstance(value, list): + return any(is_complex(val) for val in value) + if isinstance(value, collections.abc.Mapping): + return any(is_complex(val) for val in value.keys()) or any( + is_complex(val) for val in value.values() + ) + return False + + def is_template_string(maybe_template: str) -> bool: """Check if the input is a Jinja2 template.""" return _RE_JINJA_DELIMITERS.search(maybe_template) is not None diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 6bcc98c10a8..bed0d2b8d17 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -1,5 +1,5 @@ """Typing Helpers for Home Assistant.""" -from typing import Any, Dict, Optional, Tuple, Union +from typing import Any, Dict, Mapping, Optional, Tuple, Union import homeassistant.core @@ -12,7 +12,7 @@ HomeAssistantType = homeassistant.core.HomeAssistant ServiceCallType = homeassistant.core.ServiceCall ServiceDataType = Dict[str, Any] StateType = Union[None, str, int, float] -TemplateVarsType = Optional[Dict[str, Any]] +TemplateVarsType = Optional[Mapping[str, Any]] # Custom type for recorder Queries QueryType = Any diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 3952e781952..5ee0ff62af2 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1134,3 +1134,57 @@ async def test_logbook_humanify_automation_triggered_event(hass): assert event2["domain"] == "automation" assert event2["message"] == "has been triggered by source of trigger" assert event2["entity_id"] == "automation.bye" + + +async def test_automation_variables(hass): + """Test automation variables.""" + calls = async_mock_service(hass, "test", "automation") + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "variables": { + "test_var": "defined_in_config", + "event_type": "{{ trigger.event.event_type }}", + }, + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": { + "service": "test.automation", + "data": { + "value": "{{ test_var }}", + "event_type": "{{ event_type }}", + }, + }, + }, + { + "variables": { + "test_var": "defined_in_config", + }, + "trigger": {"platform": "event", "event_type": "test_event_2"}, + "condition": { + "condition": "template", + "value_template": "{{ trigger.event.data.pass_condition }}", + }, + "action": { + "service": "test.automation", + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["value"] == "defined_in_config" + assert calls[0].data["event_type"] == "test_event" + + hass.bus.async_fire("test_event_2") + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.bus.async_fire("test_event_2", {"pass_condition": True}) + await hass.async_block_till_done() + assert len(calls) == 2 diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 22625d46530..5fb832d0f36 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -23,7 +23,7 @@ from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component, setup_component from tests.async_mock import Mock, patch -from tests.common import get_test_home_assistant +from tests.common import async_mock_service, get_test_home_assistant from tests.components.logbook.test_init import MockLazyEventPartialState ENTITY_ID = "script.test" @@ -615,3 +615,69 @@ async def test_concurrent_script(hass, concurrently): assert not script.is_on(hass, "script.script1") assert not script.is_on(hass, "script.script2") + + +async def test_script_variables(hass): + """Test defining scripts.""" + assert await async_setup_component( + hass, + "script", + { + "script": { + "script1": { + "variables": { + "test_var": "from_config", + "templated_config_var": "{{ var_from_service | default('config-default') }}", + }, + "sequence": [ + { + "service": "test.script", + "data": { + "value": "{{ test_var }}", + "templated_config_var": "{{ templated_config_var }}", + }, + }, + ], + }, + "script2": { + "variables": { + "test_var": "from_config", + }, + "sequence": [ + { + "service": "test.script", + "data": { + "value": "{{ test_var }}", + }, + }, + ], + }, + } + }, + ) + + mock_calls = async_mock_service(hass, "test", "script") + + await hass.services.async_call( + "script", "script1", {"var_from_service": "hello"}, blocking=True + ) + + assert len(mock_calls) == 1 + assert mock_calls[0].data["value"] == "from_config" + assert mock_calls[0].data["templated_config_var"] == "hello" + + await hass.services.async_call( + "script", "script1", {"test_var": "from_service"}, blocking=True + ) + + assert len(mock_calls) == 2 + assert mock_calls[1].data["value"] == "from_service" + assert mock_calls[1].data["templated_config_var"] == "config-default" + + # Call script with vars but no templates in it + await hass.services.async_call( + "script", "script2", {"test_var": "from_service"}, blocking=True + ) + + assert len(mock_calls) == 3 + assert mock_calls[2].data["value"] == "from_service" From 7370b0ffc66adb041eda05ded626e7edf2ab619b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Sep 2020 13:50:11 -0500 Subject: [PATCH 047/185] Detect self-referencing loops in template entities and log a warning (#39897) --- .../components/template/template_entity.py | 8 ++++ homeassistant/helpers/event.py | 16 +++++++ tests/components/template/test_sensor.py | 44 +++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 20b0caec3ca..2e42b28fbd2 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -121,6 +121,7 @@ class TemplateEntity(Entity): """Template Entity.""" self._template_attrs = {} self._async_update = None + self._async_update_entity_ids_filter = None self._attribute_templates = attribute_templates self._attributes = {} self._availability_template = availability_template @@ -231,6 +232,9 @@ class TemplateEntity(Entity): event, update.template, update.last_result, update.result ) + if self._async_update_entity_ids_filter: + self._async_update_entity_ids_filter({self.entity_id}) + if self._async_update: self.async_write_ha_state() @@ -245,8 +249,12 @@ class TemplateEntity(Entity): ) self.async_on_remove(result_info.async_remove) result_info.async_refresh() + result_info.async_update_entity_ids_filter({self.entity_id}) self.async_write_ha_state() self._async_update = result_info.async_refresh + self._async_update_entity_ids_filter = ( + result_info.async_update_entity_ids_filter + ) async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index dcc05675a01..d9f1b8d9681 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -508,6 +508,7 @@ class _TrackTemplateResultInfo: self._info: Dict[Template, RenderInfo] = {} self._last_domains: Set = set() self._last_entities: Set = set() + self._entity_ids_filter: Set = set() def async_setup(self) -> None: """Activation of template tracking.""" @@ -659,12 +660,27 @@ class _TrackTemplateResultInfo: """Force recalculate the template.""" self._refresh(None) + @callback + def async_update_entity_ids_filter(self, entity_ids: Set) -> None: + """Update the filtered entity_ids.""" + self._entity_ids_filter = entity_ids + @callback def _refresh(self, event: Optional[Event]) -> None: entity_id = event and event.data.get(ATTR_ENTITY_ID) updates = [] info_changed = False + if entity_id and entity_id in self._entity_ids_filter: + # Skip self-referencing updates + for track_template_ in self._track_templates: + _LOGGER.warning( + "Template loop detected while processing event: %s, skipping template render for Template[%s]", + event, + track_template_.template.template, + ) + return + for track_template_ in self._track_templates: template = track_template_.template if ( diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 31b298330e8..439f154b4af 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -758,3 +758,47 @@ async def test_sun_renders_once_per_sensor(hass): "{{ state_attr('sun.sun', 'elevation') }}", "{{ state_attr('sun.sun', 'next_rising') }}", } + + +async def test_self_referencing_sensor_loop(hass, caplog): + """Test a self referencing sensor does not loop forever.""" + + template_str = """ +{% for state in states -%} + {{ state.last_updated }} +{%- endfor %} +""" + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "test": { + "value_template": template_str, + }, + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + value = hass.states.get("sensor.test").state + await hass.async_block_till_done() + + value2 = hass.states.get("sensor.test").state + assert value2 == value + + await hass.async_block_till_done() + + value3 = hass.states.get("sensor.test").state + assert value3 == value2 + + assert "Template loop detected" in caplog.text From b8ef87d84ceb554b65c51748a6c60a8f0318eb86 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Sep 2020 11:10:43 -0500 Subject: [PATCH 048/185] Fix ping log level to be debug instead of warning (#39900) --- homeassistant/components/ping/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index 5befe0b7f3a..ac73da0a13f 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -134,7 +134,7 @@ class PingDataICMPLib(PingData): async def async_update(self) -> None: """Retrieve the latest details from the host.""" - _LOGGER.warning("ping address: %s", self._ip_address) + _LOGGER.debug("ping address: %s", self._ip_address) data = await self.hass.async_add_executor_job( partial( icmp_ping, From 6f8060dea77b3484771a38467626348b16fc1932 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Sep 2020 20:52:23 +0200 Subject: [PATCH 049/185] Fix discovery update of MQTT state templates (#39901) --- homeassistant/components/mqtt/__init__.py | 2 +- .../components/mqtt/alarm_control_panel.py | 25 +- .../components/mqtt/binary_sensor.py | 25 +- homeassistant/components/mqtt/cover.py | 18 +- homeassistant/components/mqtt/fan.py | 31 +- .../components/mqtt/light/schema_basic.py | 28 +- homeassistant/components/mqtt/lock.py | 17 +- homeassistant/components/mqtt/sensor.py | 27 +- homeassistant/components/mqtt/switch.py | 17 +- .../mqtt/test_alarm_control_panel.py | 58 ++- tests/components/mqtt/test_binary_sensor.py | 62 ++- tests/components/mqtt/test_common.py | 42 +- tests/components/mqtt/test_cover.py | 8 + tests/components/mqtt/test_light.py | 471 +++++++++++++++++- tests/components/mqtt/test_sensor.py | 68 ++- tests/components/mqtt/test_switch.py | 82 ++- 16 files changed, 853 insertions(+), 128 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index d4263ee6ba3..2b5dca6474f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1305,7 +1305,7 @@ class MqttDiscoveryUpdate(Entity): debug_info.add_entity_discovery_data( self.hass, self._discovery_data, self.entity_id ) - # Set in case the entity has been removed and is re-added + # Set in case the entity has been removed and is re-added, for example when changing entity_id set_discovery_hash(self.hass, discovery_hash) self._remove_signal = async_dispatcher_connect( self.hass, diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 505c7616a0a..6d33175e6ca 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -104,7 +104,7 @@ async def async_setup_platform( ): """Set up MQTT alarm control panel through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(hass, config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -116,7 +116,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ) except Exception: clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) @@ -128,10 +128,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + hass, config, async_add_entities, config_entry=None, discovery_data=None ): """Set up the MQTT Alarm Control Panel platform.""" - async_add_entities([MqttAlarm(config, config_entry, discovery_data)]) + async_add_entities([MqttAlarm(hass, config, config_entry, discovery_data)]) class MqttAlarm( @@ -143,13 +143,16 @@ class MqttAlarm( ): """Representation of a MQTT alarm status.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Init the MQTT Alarm Control Panel.""" + self.hass = hass self._state = None - self._config = config self._unique_id = config.get(CONF_UNIQUE_ID) self._sub_state = None + # Load config + self._setup_from_config(config) + device_config = config.get(CONF_DEVICE) MqttAttributes.__init__(self, config) @@ -165,26 +168,30 @@ class MqttAlarm( async def discovery_update(self, discovery_payload): """Handle updated discovery message.""" config = PLATFORM_SCHEMA(discovery_payload) - self._config = config + self._setup_from_config(config) await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_write_ha_state() - async def _subscribe_topics(self): - """(Re)Subscribe to topics.""" + def _setup_from_config(self, config): + self._config = config value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = self.hass command_template = self._config[CONF_COMMAND_TEMPLATE] command_template.hass = self.hass + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + @callback @log_messages(self.hass, self.entity_id) def message_received(msg): """Run when new MQTT message has been received.""" payload = msg.payload + value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: payload = value_template.async_render_with_possible_json_value( msg.payload, self._state diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index c9fd2bba2b1..cf7b0fc3ca6 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -76,7 +76,7 @@ async def async_setup_platform( ): """Set up MQTT binary sensor through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(hass, config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -88,7 +88,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ) except Exception: clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) @@ -100,10 +100,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + hass, config, async_add_entities, config_entry=None, discovery_data=None ): """Set up the MQTT binary sensor.""" - async_add_entities([MqttBinarySensor(config, config_entry, discovery_data)]) + async_add_entities([MqttBinarySensor(hass, config, config_entry, discovery_data)]) class MqttBinarySensor( @@ -115,9 +115,9 @@ class MqttBinarySensor( ): """Representation a binary sensor that is updated by MQTT.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT binary sensor.""" - self._config = config + self.hass = hass self._unique_id = config.get(CONF_UNIQUE_ID) self._state = None self._sub_state = None @@ -128,6 +128,10 @@ class MqttBinarySensor( self._expired = True else: self._expired = None + + # Load config + self._setup_from_config(config) + device_config = config.get(CONF_DEVICE) MqttAttributes.__init__(self, config) @@ -143,19 +147,22 @@ class MqttBinarySensor( async def discovery_update(self, discovery_payload): """Handle updated discovery message.""" config = PLATFORM_SCHEMA(discovery_payload) - self._config = config + self._setup_from_config(config) await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_write_ha_state() - async def _subscribe_topics(self): - """(Re)Subscribe to topics.""" + def _setup_from_config(self, config): + self._config = config value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = self.hass + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + @callback def off_delay_listener(now): """Switch device off after a delay.""" diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index b8a5b778a98..20146b0b7d6 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -174,7 +174,7 @@ async def async_setup_platform( ): """Set up MQTT cover through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(hass, config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -186,7 +186,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ) except Exception: clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) @@ -198,10 +198,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + hass, config, async_add_entities, config_entry=None, discovery_data=None ): """Set up the MQTT Cover.""" - async_add_entities([MqttCover(config, config_entry, discovery_data)]) + async_add_entities([MqttCover(hass, config, config_entry, discovery_data)]) class MqttCover( @@ -213,8 +213,9 @@ class MqttCover( ): """Representation of a cover that can be controlled using MQTT.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the cover.""" + self.hass = hass self._unique_id = config.get(CONF_UNIQUE_ID) self._position = None self._state = None @@ -257,8 +258,6 @@ class MqttCover( ) self._tilt_optimistic = config[CONF_TILT_STATE_OPTIMISTIC] - async def _subscribe_topics(self): - """(Re)Subscribe to topics.""" template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: template.hass = self.hass @@ -269,6 +268,8 @@ class MqttCover( if tilt_status_template is not None: tilt_status_template.hass = self.hass + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" topics = {} @callback @@ -276,6 +277,7 @@ class MqttCover( def tilt_message_received(msg): """Handle tilt updates.""" payload = msg.payload + tilt_status_template = self._config.get(CONF_TILT_STATUS_TEMPLATE) if tilt_status_template is not None: payload = tilt_status_template.async_render_with_possible_json_value( payload @@ -296,6 +298,7 @@ class MqttCover( def state_message_received(msg): """Handle new MQTT state messages.""" payload = msg.payload + template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: payload = template.async_render_with_possible_json_value(payload) @@ -321,6 +324,7 @@ class MqttCover( def position_message_received(msg): """Handle new MQTT state messages.""" payload = msg.payload + template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: payload = template.async_render_with_possible_json_value(payload) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index b1ec7aabeef..14469e415e0 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -115,7 +115,7 @@ async def async_setup_platform( ): """Set up MQTT fan through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(hass, config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -127,7 +127,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ) except Exception: clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) @@ -139,10 +139,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + hass, config, async_add_entities, config_entry=None, discovery_data=None ): """Set up the MQTT fan.""" - async_add_entities([MqttFan(config, config_entry, discovery_data)]) + async_add_entities([MqttFan(hass, config, config_entry, discovery_data)]) class MqttFan( @@ -154,8 +154,9 @@ class MqttFan( ): """A MQTT fan component.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT fan.""" + self.hass = hass self._unique_id = config.get(CONF_UNIQUE_ID) self._state = False self._speed = None @@ -242,22 +243,22 @@ class MqttFan( self._topic[CONF_SPEED_COMMAND_TOPIC] is not None and SUPPORT_SET_SPEED ) + for key, tpl in list(self._templates.items()): + if tpl is None: + self._templates[key] = lambda value: value + else: + tpl.hass = self.hass + self._templates[key] = tpl.async_render_with_possible_json_value + async def _subscribe_topics(self): """(Re)Subscribe to topics.""" topics = {} - templates = {} - for key, tpl in list(self._templates.items()): - if tpl is None: - templates[key] = lambda value: value - else: - tpl.hass = self.hass - templates[key] = tpl.async_render_with_possible_json_value @callback @log_messages(self.hass, self.entity_id) def state_received(msg): """Handle new received MQTT message.""" - payload = templates[CONF_STATE](msg.payload) + payload = self._templates[CONF_STATE](msg.payload) if payload == self._payload["STATE_ON"]: self._state = True elif payload == self._payload["STATE_OFF"]: @@ -275,7 +276,7 @@ class MqttFan( @log_messages(self.hass, self.entity_id) def speed_received(msg): """Handle new received MQTT message for the speed.""" - payload = templates[ATTR_SPEED](msg.payload) + payload = self._templates[ATTR_SPEED](msg.payload) if payload == self._payload["SPEED_LOW"]: self._speed = SPEED_LOW elif payload == self._payload["SPEED_MEDIUM"]: @@ -298,7 +299,7 @@ class MqttFan( @log_messages(self.hass, self.entity_id) def oscillation_received(msg): """Handle new received MQTT message for the oscillation.""" - payload = templates[OSCILLATION](msg.payload) + payload = self._templates[OSCILLATION](msg.payload) if payload == self._payload["OSCILLATE_ON_PAYLOAD"]: self._oscillation = True elif payload == self._payload["OSCILLATE_OFF_PAYLOAD"]: diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index e19fcbf0e40..d7c373bf72b 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -254,7 +254,7 @@ class MqttLight( value_templates = {} for key in VALUE_TEMPLATE_KEYS: - value_templates[key] = lambda value: value + value_templates[key] = lambda value, _: value for key in VALUE_TEMPLATE_KEYS & config.keys(): tpl = config[key] value_templates[key] = tpl.async_render_with_possible_json_value @@ -304,7 +304,9 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def state_received(msg): """Handle new MQTT messages.""" - payload = self._value_templates[CONF_STATE_VALUE_TEMPLATE](msg.payload) + payload = self._value_templates[CONF_STATE_VALUE_TEMPLATE]( + msg.payload, None + ) if not payload: _LOGGER.debug("Ignoring empty state message from '%s'", msg.topic) return @@ -328,7 +330,9 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def brightness_received(msg): """Handle new MQTT messages for the brightness.""" - payload = self._value_templates[CONF_BRIGHTNESS_VALUE_TEMPLATE](msg.payload) + payload = self._value_templates[CONF_BRIGHTNESS_VALUE_TEMPLATE]( + msg.payload, None + ) if not payload: _LOGGER.debug("Ignoring empty brightness message from '%s'", msg.topic) return @@ -360,7 +364,7 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def rgb_received(msg): """Handle new MQTT messages for RGB.""" - payload = self._value_templates[CONF_RGB_VALUE_TEMPLATE](msg.payload) + payload = self._value_templates[CONF_RGB_VALUE_TEMPLATE](msg.payload, None) if not payload: _LOGGER.debug("Ignoring empty rgb message from '%s'", msg.topic) return @@ -392,7 +396,9 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def color_temp_received(msg): """Handle new MQTT messages for color temperature.""" - payload = self._value_templates[CONF_COLOR_TEMP_VALUE_TEMPLATE](msg.payload) + payload = self._value_templates[CONF_COLOR_TEMP_VALUE_TEMPLATE]( + msg.payload, None + ) if not payload: _LOGGER.debug("Ignoring empty color temp message from '%s'", msg.topic) return @@ -422,7 +428,9 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def effect_received(msg): """Handle new MQTT messages for effect.""" - payload = self._value_templates[CONF_EFFECT_VALUE_TEMPLATE](msg.payload) + payload = self._value_templates[CONF_EFFECT_VALUE_TEMPLATE]( + msg.payload, None + ) if not payload: _LOGGER.debug("Ignoring empty effect message from '%s'", msg.topic) return @@ -452,7 +460,7 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def hs_received(msg): """Handle new MQTT messages for hs color.""" - payload = self._value_templates[CONF_HS_VALUE_TEMPLATE](msg.payload) + payload = self._value_templates[CONF_HS_VALUE_TEMPLATE](msg.payload, None) if not payload: _LOGGER.debug("Ignoring empty hs message from '%s'", msg.topic) return @@ -484,7 +492,9 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def white_value_received(msg): """Handle new MQTT messages for white value.""" - payload = self._value_templates[CONF_WHITE_VALUE_TEMPLATE](msg.payload) + payload = self._value_templates[CONF_WHITE_VALUE_TEMPLATE]( + msg.payload, None + ) if not payload: _LOGGER.debug("Ignoring empty white value message from '%s'", msg.topic) return @@ -516,7 +526,7 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def xy_received(msg): """Handle new MQTT messages for xy color.""" - payload = self._value_templates[CONF_XY_VALUE_TEMPLATE](msg.payload) + payload = self._value_templates[CONF_XY_VALUE_TEMPLATE](msg.payload, None) if not payload: _LOGGER.debug("Ignoring empty xy-color message from '%s'", msg.topic) return diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index f6d56a30431..aea1e40b0f9 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -77,7 +77,7 @@ async def async_setup_platform( ): """Set up MQTT lock panel through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(hass, config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -89,7 +89,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ) except Exception: clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) @@ -101,10 +101,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + hass, config, async_add_entities, config_entry=None, discovery_data=None ): """Set up the MQTT Lock platform.""" - async_add_entities([MqttLock(config, config_entry, discovery_data)]) + async_add_entities([MqttLock(hass, config, config_entry, discovery_data)]) class MqttLock( @@ -116,8 +116,9 @@ class MqttLock( ): """Representation of a lock that can be toggled using MQTT.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the lock.""" + self.hass = hass self._unique_id = config.get(CONF_UNIQUE_ID) self._state = False self._sub_state = None @@ -154,17 +155,19 @@ class MqttLock( self._optimistic = config[CONF_OPTIMISTIC] - async def _subscribe_topics(self): - """(Re)Subscribe to topics.""" value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = self.hass + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + @callback @log_messages(self.hass, self.entity_id) def message_received(msg): """Handle new MQTT messages.""" payload = msg.payload + value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: payload = value_template.async_render_with_possible_json_value(payload) if payload == self._config[CONF_STATE_LOCKED]: diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index eb241bba7a0..ffd34cef8c9 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -70,7 +70,7 @@ async def async_setup_platform( ): """Set up MQTT sensors through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(hass, config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -82,7 +82,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ) except Exception: clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) @@ -94,10 +94,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity( - config: ConfigType, async_add_entities, config_entry=None, discovery_data=None + hass, config: ConfigType, async_add_entities, config_entry=None, discovery_data=None ): """Set up MQTT sensor.""" - async_add_entities([MqttSensor(config, config_entry, discovery_data)]) + async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)]) class MqttSensor( @@ -105,9 +105,9 @@ class MqttSensor( ): """Representation of a sensor that can be updated using MQTT.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the sensor.""" - self._config = config + self.hass = hass self._unique_id = config.get(CONF_UNIQUE_ID) self._state = None self._sub_state = None @@ -118,6 +118,10 @@ class MqttSensor( self._expired = True else: self._expired = None + + # Load config + self._setup_from_config(config) + device_config = config.get(CONF_DEVICE) MqttAttributes.__init__(self, config) @@ -133,19 +137,23 @@ class MqttSensor( async def discovery_update(self, discovery_payload): """Handle updated discovery message.""" config = PLATFORM_SCHEMA(discovery_payload) - self._config = config + self._setup_from_config(config) await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_write_ha_state() - async def _subscribe_topics(self): - """(Re)Subscribe to topics.""" + def _setup_from_config(self, config): + """(Re)Setup the entity.""" + self._config = config template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: template.hass = self.hass + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + @callback @log_messages(self.hass, self.entity_id) def message_received(msg): @@ -169,6 +177,7 @@ class MqttSensor( self.hass, self._value_is_expired, expiration_at ) + template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: payload = template.async_render_with_possible_json_value( payload, self._state diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 364f060ac14..761f19ef054 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -73,7 +73,7 @@ async def async_setup_platform( ): """Set up MQTT switch through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities, discovery_info) + await _async_setup_entity(hass, config, async_add_entities, discovery_info) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -85,7 +85,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ) except Exception: clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) @@ -97,10 +97,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + hass, config, async_add_entities, config_entry=None, discovery_data=None ): """Set up the MQTT switch.""" - async_add_entities([MqttSwitch(config, config_entry, discovery_data)]) + async_add_entities([MqttSwitch(hass, config, config_entry, discovery_data)]) class MqttSwitch( @@ -113,8 +113,9 @@ class MqttSwitch( ): """Representation of a switch that can be toggled using MQTT.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT switch.""" + self.hass = hass self._state = False self._sub_state = None @@ -160,17 +161,19 @@ class MqttSwitch( self._optimistic = config[CONF_OPTIMISTIC] - async def _subscribe_topics(self): - """(Re)Subscribe to topics.""" template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: template.hass = self.hass + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + @callback @log_messages(self.hass, self.entity_id) def state_message_received(msg): """Handle new MQTT state messages.""" payload = msg.payload + template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: payload = template.async_render_with_possible_json_value(payload) if payload == self._state_on: diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 30ec5316399..7eb890903fd 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -603,17 +603,71 @@ async def test_discovery_removal_alarm(hass, mqtt_mock, caplog): ) -async def test_discovery_update_alarm(hass, mqtt_mock, caplog): +async def test_discovery_update_alarm_topic_and_template(hass, mqtt_mock, caplog): """Test update of discovered alarm_control_panel.""" config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" + config1["state_topic"] = "alarm/state1" + config2["state_topic"] = "alarm/state2" + config1["value_template"] = "{{ value_json.state1.state }}" + config2["value_template"] = "{{ value_json.state2.state }}" + + state_data1 = [ + ([("alarm/state1", '{"state1":{"state":"armed_away"}}')], "armed_away", None), + ] + state_data2 = [ + ([("alarm/state1", '{"state1":{"state":"triggered"}}')], "armed_away", None), + ([("alarm/state1", '{"state2":{"state":"triggered"}}')], "armed_away", None), + ([("alarm/state2", '{"state1":{"state":"triggered"}}')], "armed_away", None), + ([("alarm/state2", '{"state2":{"state":"triggered"}}')], "triggered", None), + ] data1 = json.dumps(config1) data2 = json.dumps(config2) await help_test_discovery_update( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, data1, data2 + hass, + mqtt_mock, + caplog, + alarm_control_panel.DOMAIN, + data1, + data2, + state_data1=state_data1, + state_data2=state_data2, + ) + + +async def test_discovery_update_alarm_template(hass, mqtt_mock, caplog): + """Test update of discovered alarm_control_panel.""" + config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) + config1["name"] = "Beer" + config2["name"] = "Milk" + config1["state_topic"] = "alarm/state1" + config2["state_topic"] = "alarm/state1" + config1["value_template"] = "{{ value_json.state1.state }}" + config2["value_template"] = "{{ value_json.state2.state }}" + + state_data1 = [ + ([("alarm/state1", '{"state1":{"state":"armed_away"}}')], "armed_away", None), + ] + state_data2 = [ + ([("alarm/state1", '{"state1":{"state":"triggered"}}')], "armed_away", None), + ([("alarm/state1", '{"state2":{"state":"triggered"}}')], "triggered", None), + ] + + data1 = json.dumps(config1) + data2 = json.dumps(config2) + await help_test_discovery_update( + hass, + mqtt_mock, + caplog, + alarm_control_panel.DOMAIN, + data1, + data2, + state_data1=state_data1, + state_data2=state_data2, ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index c739f4378d1..d2375278c4d 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -580,17 +580,75 @@ async def test_discovery_removal_binary_sensor(hass, mqtt_mock, caplog): ) -async def test_discovery_update_binary_sensor(hass, mqtt_mock, caplog): +async def test_discovery_update_binary_sensor_topic_template(hass, mqtt_mock, caplog): """Test update of discovered binary_sensor.""" config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" + config1["state_topic"] = "sensor/state1" + config2["state_topic"] = "sensor/state2" + config1["value_template"] = "{{ value_json.state1.state }}" + config2["value_template"] = "{{ value_json.state2.state }}" + + state_data1 = [ + ([("sensor/state1", '{"state1":{"state":"ON"}}')], "on", None), + ] + state_data2 = [ + ([("sensor/state2", '{"state2":{"state":"OFF"}}')], "off", None), + ([("sensor/state2", '{"state2":{"state":"ON"}}')], "on", None), + ([("sensor/state1", '{"state1":{"state":"OFF"}}')], "on", None), + ([("sensor/state1", '{"state2":{"state":"OFF"}}')], "on", None), + ([("sensor/state2", '{"state1":{"state":"OFF"}}')], "on", None), + ([("sensor/state2", '{"state2":{"state":"OFF"}}')], "off", None), + ] data1 = json.dumps(config1) data2 = json.dumps(config2) await help_test_discovery_update( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, data1, data2 + hass, + mqtt_mock, + caplog, + binary_sensor.DOMAIN, + data1, + data2, + state_data1=state_data1, + state_data2=state_data2, + ) + + +async def test_discovery_update_binary_sensor_template(hass, mqtt_mock, caplog): + """Test update of discovered binary_sensor.""" + config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) + config1["name"] = "Beer" + config2["name"] = "Milk" + config1["state_topic"] = "sensor/state1" + config2["state_topic"] = "sensor/state1" + config1["value_template"] = "{{ value_json.state1.state }}" + config2["value_template"] = "{{ value_json.state2.state }}" + + state_data1 = [ + ([("sensor/state1", '{"state1":{"state":"ON"}}')], "on", None), + ] + state_data2 = [ + ([("sensor/state1", '{"state2":{"state":"OFF"}}')], "off", None), + ([("sensor/state1", '{"state2":{"state":"ON"}}')], "on", None), + ([("sensor/state1", '{"state1":{"state":"OFF"}}')], "on", None), + ([("sensor/state1", '{"state2":{"state":"OFF"}}')], "off", None), + ] + + data1 = json.dumps(config1) + data2 = json.dumps(config2) + await help_test_discovery_update( + hass, + mqtt_mock, + caplog, + binary_sensor.DOMAIN, + data1, + data2, + state_data1=state_data1, + state_data2=state_data2, ) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 75e1c12a46c..27ee060ebfa 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -512,10 +512,6 @@ async def help_test_discovery_update( discovery_data2, state_data1=None, state_data2=None, - state1=None, - state2=None, - attributes1=None, - attributes2=None, ): """Test update of discovered component. @@ -527,32 +523,38 @@ async def help_test_discovery_update( async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", discovery_data1) await hass.async_block_till_done() - if state_data1: - for (topic, data) in state_data1: - async_fire_mqtt_message(hass, topic, data) state = hass.states.get(f"{domain}.beer") assert state is not None assert state.name == "Beer" - if state1: - assert state.state == state1 - if attributes1: - for (attr, value) in attributes1: - assert state.attributes.get(attr) == value + + if state_data1: + for (mqtt_messages, expected_state, attributes) in state_data1: + for (topic, data) in mqtt_messages: + async_fire_mqtt_message(hass, topic, data) + state = hass.states.get(f"{domain}.beer") + if expected_state: + assert state.state == expected_state + if attributes: + for (attr, value) in attributes: + assert state.attributes.get(attr) == value async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", discovery_data2) await hass.async_block_till_done() - if state_data2: - for (topic, data) in state_data2: - async_fire_mqtt_message(hass, topic, data) state = hass.states.get(f"{domain}.beer") assert state is not None assert state.name == "Milk" - if state2: - assert state.state == state2 - if attributes2: - for (attr, value) in attributes2: - assert state.attributes.get(attr) == value + + if state_data2: + for (mqtt_messages, expected_state, attributes) in state_data2: + for (topic, data) in mqtt_messages: + async_fire_mqtt_message(hass, topic, data) + state = hass.states.get(f"{domain}.beer") + if expected_state: + assert state.state == expected_state + if attributes: + for (attr, value) in attributes: + assert state.attributes.get(attr) == value state = hass.states.get(f"{domain}.milk") assert state is None diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index f9036bcfa0f..d1529e63fc7 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -1397,6 +1397,7 @@ async def test_tilt_position_altered_range(hass, mqtt_mock): async def test_find_percentage_in_range_defaults(hass, mqtt_mock): """Test find percentage in range with default range.""" mqtt_cover = MqttCover( + hass, { "name": "cover.test", "state_topic": "state-topic", @@ -1440,6 +1441,7 @@ async def test_find_percentage_in_range_defaults(hass, mqtt_mock): async def test_find_percentage_in_range_altered(hass, mqtt_mock): """Test find percentage in range with altered range.""" mqtt_cover = MqttCover( + hass, { "name": "cover.test", "state_topic": "state-topic", @@ -1483,6 +1485,7 @@ async def test_find_percentage_in_range_altered(hass, mqtt_mock): async def test_find_percentage_in_range_defaults_inverted(hass, mqtt_mock): """Test find percentage in range with default range but inverted.""" mqtt_cover = MqttCover( + hass, { "name": "cover.test", "state_topic": "state-topic", @@ -1526,6 +1529,7 @@ async def test_find_percentage_in_range_defaults_inverted(hass, mqtt_mock): async def test_find_percentage_in_range_altered_inverted(hass, mqtt_mock): """Test find percentage in range with altered range and inverted.""" mqtt_cover = MqttCover( + hass, { "name": "cover.test", "state_topic": "state-topic", @@ -1569,6 +1573,7 @@ async def test_find_percentage_in_range_altered_inverted(hass, mqtt_mock): async def test_find_in_range_defaults(hass, mqtt_mock): """Test find in range with default range.""" mqtt_cover = MqttCover( + hass, { "name": "cover.test", "state_topic": "state-topic", @@ -1612,6 +1617,7 @@ async def test_find_in_range_defaults(hass, mqtt_mock): async def test_find_in_range_altered(hass, mqtt_mock): """Test find in range with altered range.""" mqtt_cover = MqttCover( + hass, { "name": "cover.test", "state_topic": "state-topic", @@ -1655,6 +1661,7 @@ async def test_find_in_range_altered(hass, mqtt_mock): async def test_find_in_range_defaults_inverted(hass, mqtt_mock): """Test find in range with default range but inverted.""" mqtt_cover = MqttCover( + hass, { "name": "cover.test", "state_topic": "state-topic", @@ -1698,6 +1705,7 @@ async def test_find_in_range_defaults_inverted(hass, mqtt_mock): async def test_find_in_range_altered_inverted(hass, mqtt_mock): """Test find in range with altered range and inverted.""" mqtt_cover = MqttCover( + hass, { "name": "cover.test", "state_topic": "state-topic", diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index da4d90ad5ec..5481b8b2565 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -153,6 +153,7 @@ light: payload_off: "off" """ +import json from os import path import pytest @@ -1466,20 +1467,249 @@ async def test_discovery_deprecated(hass, mqtt_mock, caplog): assert state.name == "Beer" -async def test_discovery_update_light(hass, mqtt_mock, caplog): +async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog): """Test update of discovered light.""" - data1 = ( - '{ "name": "Beer",' - ' "state_topic": "test_topic",' - ' "command_topic": "test_topic",' - ' "state_value_template": "{{value_json.power1}}" }' + data1 = json.dumps( + { + "name": "Beer", + "state_topic": "test_light_rgb/state1", + "command_topic": "test_light_rgb/set", + "brightness_command_topic": "test_light_rgb/state1", + "rgb_command_topic": "test_light_rgb/rgb/set", + "color_temp_command_topic": "test_light_rgb/state1", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_command_topic": "test_light_rgb/hs/set", + "white_value_command_topic": "test_light_rgb/white_value/set", + "xy_command_topic": "test_light_rgb/xy/set", + "brightness_state_topic": "test_light_rgb/state1", + "color_temp_state_topic": "test_light_rgb/state1", + "effect_state_topic": "test_light_rgb/state1", + "hs_state_topic": "test_light_rgb/state1", + "rgb_state_topic": "test_light_rgb/state1", + "white_value_state_topic": "test_light_rgb/state1", + "xy_state_topic": "test_light_rgb/state1", + "state_value_template": "{{ value_json.state1.state }}", + "brightness_value_template": "{{ value_json.state1.brightness }}", + "color_temp_value_template": "{{ value_json.state1.ct }}", + "effect_value_template": "{{ value_json.state1.fx }}", + "hs_value_template": "{{ value_json.state1.hs }}", + "rgb_value_template": "{{ value_json.state1.rgb }}", + "white_value_template": "{{ value_json.state1.white }}", + "xy_value_template": "{{ value_json.state1.xy }}", + } ) - data2 = ( - '{ "name": "Milk",' - ' "state_topic": "test_topic",' - ' "command_topic": "test_topic",' - ' "state_value_template": "{{value_json.power2}}" }' + + data2 = json.dumps( + { + "name": "Milk", + "state_topic": "test_light_rgb/state2", + "command_topic": "test_light_rgb/set", + "brightness_command_topic": "test_light_rgb/state2", + "rgb_command_topic": "test_light_rgb/rgb/set", + "color_temp_command_topic": "test_light_rgb/state2", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_command_topic": "test_light_rgb/hs/set", + "white_value_command_topic": "test_light_rgb/white_value/set", + "xy_command_topic": "test_light_rgb/xy/set", + "brightness_state_topic": "test_light_rgb/state2", + "color_temp_state_topic": "test_light_rgb/state2", + "effect_state_topic": "test_light_rgb/state2", + "hs_state_topic": "test_light_rgb/state2", + "rgb_state_topic": "test_light_rgb/state2", + "white_value_state_topic": "test_light_rgb/state2", + "xy_state_topic": "test_light_rgb/state2", + "state_value_template": "{{ value_json.state2.state }}", + "brightness_value_template": "{{ value_json.state2.brightness }}", + "color_temp_value_template": "{{ value_json.state2.ct }}", + "effect_value_template": "{{ value_json.state2.fx }}", + "hs_value_template": "{{ value_json.state2.hs }}", + "rgb_value_template": "{{ value_json.state2.rgb }}", + "white_value_template": "{{ value_json.state2.white }}", + "xy_value_template": "{{ value_json.state2.xy }}", + } ) + state_data1 = [ + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}', + ) + ], + "on", + [("brightness", 100), ("color_temp", 123), ("effect", "cycle")], + ), + ( + [("test_light_rgb/state1", '{"state1":{"state":"OFF"}}')], + "off", + None, + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"state":"ON", "hs":"1,2"}}', + ) + ], + "on", + [("hs_color", (1, 2))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"rgb":"255,127,63"}}', + ) + ], + "on", + [("rgb_color", (255, 127, 63))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"white":50, "xy":"0.3, 0.4"}}', + ) + ], + "on", + [("white_value", 50), ("xy_color", (0.3, 0.401))], + ), + ] + state_data2 = [ + ( + [ + ( + "test_light_rgb/state2", + '{"state2":{"state":"ON", "brightness":50, "ct":200, "fx":"loop"}}', + ) + ], + "on", + [("brightness", 50), ("color_temp", 200), ("effect", "loop")], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}', + ), + ( + "test_light_rgb/state1", + '{"state2":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}', + ), + ( + "test_light_rgb/state2", + '{"state1":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}', + ), + ], + "on", + [("brightness", 50), ("color_temp", 200), ("effect", "loop")], + ), + ( + [("test_light_rgb/state1", '{"state1":{"state":"OFF"}}')], + "on", + None, + ), + ( + [("test_light_rgb/state1", '{"state2":{"state":"OFF"}}')], + "on", + None, + ), + ( + [("test_light_rgb/state2", '{"state1":{"state":"OFF"}}')], + "on", + None, + ), + ( + [("test_light_rgb/state2", '{"state2":{"state":"OFF"}}')], + "off", + None, + ), + ( + [ + ( + "test_light_rgb/state2", + '{"state2":{"state":"ON", "hs":"1.2,2.2"}}', + ) + ], + "on", + [("hs_color", (1.2, 2.2))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"state":"ON", "hs":"1,2"}}', + ), + ( + "test_light_rgb/state1", + '{"state2":{"state":"ON", "hs":"1,2"}}', + ), + ( + "test_light_rgb/state2", + '{"state1":{"state":"ON", "hs":"1,2"}}', + ), + ], + "on", + [("hs_color", (1.2, 2.2))], + ), + ( + [ + ( + "test_light_rgb/state2", + '{"state2":{"rgb":"63,127,255"}}', + ) + ], + "on", + [("rgb_color", (63, 127, 255))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"rgb":"255,127,63"}}', + ), + ( + "test_light_rgb/state1", + '{"state2":{"rgb":"255,127,63"}}', + ), + ( + "test_light_rgb/state2", + '{"state1":{"rgb":"255,127,63"}}', + ), + ], + "on", + [("rgb_color", (63, 127, 255))], + ), + ( + [ + ( + "test_light_rgb/state2", + '{"state2":{"white":75, "xy":"0.4, 0.3"}}', + ) + ], + "on", + [("white_value", 75), ("xy_color", (0.4, 0.3))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"white":50, "xy":"0.3, 0.4"}}', + ), + ( + "test_light_rgb/state1", + '{"state2":{"white":50, "xy":"0.3, 0.4"}}', + ), + ( + "test_light_rgb/state2", + '{"state1":{"white":50, "xy":"0.3, 0.4"}}', + ), + ], + "on", + [("white_value", 75), ("xy_color", (0.4, 0.3))], + ), + ] + await help_test_discovery_update( hass, mqtt_mock, @@ -1487,10 +1717,221 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): light.DOMAIN, data1, data2, - state_data1=[("test_topic", '{"power1":"ON"}')], - state1="on", - state_data2=[("test_topic", '{"power2":"OFF"}')], - state2="off", + state_data1=state_data1, + state_data2=state_data2, + ) + + +async def test_discovery_update_light_template(hass, mqtt_mock, caplog): + """Test update of discovered light.""" + data1 = json.dumps( + { + "name": "Beer", + "state_topic": "test_light_rgb/state1", + "command_topic": "test_light_rgb/set", + "brightness_command_topic": "test_light_rgb/state1", + "rgb_command_topic": "test_light_rgb/rgb/set", + "color_temp_command_topic": "test_light_rgb/state1", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_command_topic": "test_light_rgb/hs/set", + "white_value_command_topic": "test_light_rgb/white_value/set", + "xy_command_topic": "test_light_rgb/xy/set", + "brightness_state_topic": "test_light_rgb/state1", + "color_temp_state_topic": "test_light_rgb/state1", + "effect_state_topic": "test_light_rgb/state1", + "hs_state_topic": "test_light_rgb/state1", + "rgb_state_topic": "test_light_rgb/state1", + "white_value_state_topic": "test_light_rgb/state1", + "xy_state_topic": "test_light_rgb/state1", + "state_value_template": "{{ value_json.state1.state }}", + "brightness_value_template": "{{ value_json.state1.brightness }}", + "color_temp_value_template": "{{ value_json.state1.ct }}", + "effect_value_template": "{{ value_json.state1.fx }}", + "hs_value_template": "{{ value_json.state1.hs }}", + "rgb_value_template": "{{ value_json.state1.rgb }}", + "white_value_template": "{{ value_json.state1.white }}", + "xy_value_template": "{{ value_json.state1.xy }}", + } + ) + + data2 = json.dumps( + { + "name": "Milk", + "state_topic": "test_light_rgb/state1", + "command_topic": "test_light_rgb/set", + "brightness_command_topic": "test_light_rgb/state1", + "rgb_command_topic": "test_light_rgb/rgb/set", + "color_temp_command_topic": "test_light_rgb/state1", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_command_topic": "test_light_rgb/hs/set", + "white_value_command_topic": "test_light_rgb/white_value/set", + "xy_command_topic": "test_light_rgb/xy/set", + "brightness_state_topic": "test_light_rgb/state1", + "color_temp_state_topic": "test_light_rgb/state1", + "effect_state_topic": "test_light_rgb/state1", + "hs_state_topic": "test_light_rgb/state1", + "rgb_state_topic": "test_light_rgb/state1", + "white_value_state_topic": "test_light_rgb/state1", + "xy_state_topic": "test_light_rgb/state1", + "state_value_template": "{{ value_json.state2.state }}", + "brightness_value_template": "{{ value_json.state2.brightness }}", + "color_temp_value_template": "{{ value_json.state2.ct }}", + "effect_value_template": "{{ value_json.state2.fx }}", + "hs_value_template": "{{ value_json.state2.hs }}", + "rgb_value_template": "{{ value_json.state2.rgb }}", + "white_value_template": "{{ value_json.state2.white }}", + "xy_value_template": "{{ value_json.state2.xy }}", + } + ) + state_data1 = [ + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}', + ) + ], + "on", + [("brightness", 100), ("color_temp", 123), ("effect", "cycle")], + ), + ( + [("test_light_rgb/state1", '{"state1":{"state":"OFF"}}')], + "off", + None, + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"state":"ON", "hs":"1,2"}}', + ) + ], + "on", + [("hs_color", (1, 2))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"rgb":"255,127,63"}}', + ) + ], + "on", + [("rgb_color", (255, 127, 63))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"white":50, "xy":"0.3, 0.4"}}', + ) + ], + "on", + [("white_value", 50), ("xy_color", (0.3, 0.401))], + ), + ] + state_data2 = [ + ( + [ + ( + "test_light_rgb/state1", + '{"state2":{"state":"ON", "brightness":50, "ct":200, "fx":"loop"}}', + ) + ], + "on", + [("brightness", 50), ("color_temp", 200), ("effect", "loop")], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}', + ), + ], + "on", + [("brightness", 50), ("color_temp", 200), ("effect", "loop")], + ), + ( + [("test_light_rgb/state1", '{"state1":{"state":"OFF"}}')], + "on", + None, + ), + ( + [("test_light_rgb/state1", '{"state2":{"state":"OFF"}}')], + "off", + None, + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state2":{"state":"ON", "hs":"1.2,2.2"}}', + ) + ], + "on", + [("hs_color", (1.2, 2.2))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"state":"ON", "hs":"1,2"}}', + ) + ], + "on", + [("hs_color", (1.2, 2.2))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state2":{"rgb":"63,127,255"}}', + ) + ], + "on", + [("rgb_color", (63, 127, 255))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"rgb":"255,127,63"}}', + ) + ], + "on", + [("rgb_color", (63, 127, 255))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state2":{"white":75, "xy":"0.4, 0.3"}}', + ) + ], + "on", + [("white_value", 75), ("xy_color", (0.4, 0.3))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"white":50, "xy":"0.3, 0.4"}}', + ) + ], + "on", + [("white_value", 75), ("xy_color", (0.4, 0.3))], + ), + ] + + await help_test_discovery_update( + hass, + mqtt_mock, + caplog, + light.DOMAIN, + data1, + data2, + state_data1=state_data1, + state_data2=state_data2, ) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 0d31b9f33f2..77fd8c561b2 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the MQTT sensor platform.""" +import copy from datetime import datetime, timedelta import json @@ -430,12 +431,71 @@ async def test_discovery_removal_sensor(hass, mqtt_mock, caplog): await help_test_discovery_removal(hass, mqtt_mock, caplog, sensor.DOMAIN, data) -async def test_discovery_update_sensor(hass, mqtt_mock, caplog): +async def test_discovery_update_sensor_topic_template(hass, mqtt_mock, caplog): """Test update of discovered sensor.""" - data1 = '{ "name": "Beer", "state_topic": "test_topic" }' - data2 = '{ "name": "Milk", "state_topic": "test_topic" }' + config = {"name": "test", "state_topic": "test_topic"} + config1 = copy.deepcopy(config) + config2 = copy.deepcopy(config) + config1["name"] = "Beer" + config2["name"] = "Milk" + config1["state_topic"] = "sensor/state1" + config2["state_topic"] = "sensor/state2" + config1["value_template"] = "{{ value_json.state | int }}" + config2["value_template"] = "{{ value_json.state | int * 2 }}" + + state_data1 = [ + ([("sensor/state1", '{"state":100}')], "100", None), + ] + state_data2 = [ + ([("sensor/state1", '{"state":1000}')], "100", None), + ([("sensor/state1", '{"state":1000}')], "100", None), + ([("sensor/state2", '{"state":100}')], "200", None), + ] + + data1 = json.dumps(config1) + data2 = json.dumps(config2) await help_test_discovery_update( - hass, mqtt_mock, caplog, sensor.DOMAIN, data1, data2 + hass, + mqtt_mock, + caplog, + sensor.DOMAIN, + data1, + data2, + state_data1=state_data1, + state_data2=state_data2, + ) + + +async def test_discovery_update_sensor_template(hass, mqtt_mock, caplog): + """Test update of discovered sensor.""" + config = {"name": "test", "state_topic": "test_topic"} + config1 = copy.deepcopy(config) + config2 = copy.deepcopy(config) + config1["name"] = "Beer" + config2["name"] = "Milk" + config1["state_topic"] = "sensor/state1" + config2["state_topic"] = "sensor/state1" + config1["value_template"] = "{{ value_json.state | int }}" + config2["value_template"] = "{{ value_json.state | int * 2 }}" + + state_data1 = [ + ([("sensor/state1", '{"state":100}')], "100", None), + ] + state_data2 = [ + ([("sensor/state1", '{"state":100}')], "200", None), + ] + + data1 = json.dumps(config1) + data2 = json.dumps(config2) + await help_test_discovery_update( + hass, + mqtt_mock, + caplog, + sensor.DOMAIN, + data1, + data2, + state_data1=state_data1, + state_data2=state_data2, ) diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index a6edb8d6f14..4d9c6dc2c77 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -1,4 +1,7 @@ """The tests for the MQTT switch platform.""" +import copy +import json + import pytest from homeassistant.components import switch @@ -304,20 +307,75 @@ async def test_discovery_removal_switch(hass, mqtt_mock, caplog): await help_test_discovery_removal(hass, mqtt_mock, caplog, switch.DOMAIN, data) -async def test_discovery_update_switch(hass, mqtt_mock, caplog): +async def test_discovery_update_switch_topic_template(hass, mqtt_mock, caplog): """Test update of discovered switch.""" - data1 = ( - '{ "name": "Beer",' - ' "state_topic": "test_topic",' - ' "command_topic": "test_topic" }' - ) - data2 = ( - '{ "name": "Milk",' - ' "state_topic": "test_topic",' - ' "command_topic": "test_topic" }' - ) + config1 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) + config1["name"] = "Beer" + config2["name"] = "Milk" + config1["state_topic"] = "switch/state1" + config2["state_topic"] = "switch/state2" + config1["value_template"] = "{{ value_json.state1.state }}" + config2["value_template"] = "{{ value_json.state2.state }}" + + state_data1 = [ + ([("switch/state1", '{"state1":{"state":"ON"}}')], "on", None), + ] + state_data2 = [ + ([("switch/state2", '{"state2":{"state":"OFF"}}')], "off", None), + ([("switch/state2", '{"state2":{"state":"ON"}}')], "on", None), + ([("switch/state1", '{"state1":{"state":"OFF"}}')], "on", None), + ([("switch/state1", '{"state2":{"state":"OFF"}}')], "on", None), + ([("switch/state2", '{"state1":{"state":"OFF"}}')], "on", None), + ([("switch/state2", '{"state2":{"state":"OFF"}}')], "off", None), + ] + + data1 = json.dumps(config1) + data2 = json.dumps(config2) await help_test_discovery_update( - hass, mqtt_mock, caplog, switch.DOMAIN, data1, data2 + hass, + mqtt_mock, + caplog, + switch.DOMAIN, + data1, + data2, + state_data1=state_data1, + state_data2=state_data2, + ) + + +async def test_discovery_update_switch_template(hass, mqtt_mock, caplog): + """Test update of discovered switch.""" + config1 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) + config1["name"] = "Beer" + config2["name"] = "Milk" + config1["state_topic"] = "switch/state1" + config2["state_topic"] = "switch/state1" + config1["value_template"] = "{{ value_json.state1.state }}" + config2["value_template"] = "{{ value_json.state2.state }}" + + state_data1 = [ + ([("switch/state1", '{"state1":{"state":"ON"}}')], "on", None), + ] + state_data2 = [ + ([("switch/state1", '{"state2":{"state":"OFF"}}')], "off", None), + ([("switch/state1", '{"state2":{"state":"ON"}}')], "on", None), + ([("switch/state1", '{"state1":{"state":"OFF"}}')], "on", None), + ([("switch/state1", '{"state2":{"state":"OFF"}}')], "off", None), + ] + + data1 = json.dumps(config1) + data2 = json.dumps(config2) + await help_test_discovery_update( + hass, + mqtt_mock, + caplog, + switch.DOMAIN, + data1, + data2, + state_data1=state_data1, + state_data2=state_data2, ) From 589086f0d07040227db9048911d6f07699fe7cc6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Sep 2020 18:53:37 +0000 Subject: [PATCH 050/185] Bumped version to 0.115.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 465f93c3f76..7a32ea1ee58 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -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 1720b71d62fba200b70fa19165aef7796b370bd7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Sep 2020 05:19:21 -0500 Subject: [PATCH 051/185] Limit zeroconf discovery to name/macaddress when provided (#39877) Co-authored-by: Paulus Schoutsen --- homeassistant/components/axis/manifest.json | 6 +- .../components/brother/manifest.json | 2 +- .../components/doorbird/manifest.json | 2 +- homeassistant/components/shelly/manifest.json | 2 +- .../components/smappee/manifest.json | 3 +- homeassistant/components/zeroconf/__init__.py | 21 +++- homeassistant/generated/zeroconf.py | 113 +++++++++++++---- homeassistant/loader.py | 19 ++- script/hassfest/manifest.py | 13 +- script/hassfest/zeroconf.py | 13 +- tests/components/zeroconf/test_init.py | 118 ++++++++++++++++-- tests/test_loader.py | 46 ++++++- 12 files changed, 303 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 3b08c5ad4d4..ceb926f326e 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -4,7 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/axis", "requirements": ["axis==35"], - "zeroconf": ["_axis-video._tcp.local."], + "zeroconf": [ + {"type":"_axis-video._tcp.local.","macaddress":"00408C*"}, + {"type":"_axis-video._tcp.local.","macaddress":"ACCC8E*"}, + {"type":"_axis-video._tcp.local.","macaddress":"B8A44F*"} + ], "after_dependencies": ["mqtt"], "codeowners": ["@Kane610"] } diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index f107c9573da..2e73f9b8450 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], "requirements": ["brother==0.1.17"], - "zeroconf": ["_printer._tcp.local."], + "zeroconf": [{"type": "_printer._tcp.local.", "name":"brother*"}], "config_flow": true, "quality_scale": "platinum" } diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index 23495a22bf8..c5805b15eac 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/doorbird", "requirements": ["doorbirdpy==2.1.0"], "dependencies": ["http"], - "zeroconf": ["_axis-video._tcp.local."], + "zeroconf": [{"type":"_axis-video._tcp.local.","macaddress":"1CCAE3*"}], "codeowners": ["@oblogic7", "@bdraco"], "config_flow": true } diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 357f2c10fda..1c12125fd89 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -4,6 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", "requirements": ["aioshelly==0.3.0"], - "zeroconf": ["_http._tcp.local."], + "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], "codeowners": ["@balloob", "@bieniu"] } diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index c6cac118b72..ba1005b87d4 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -11,6 +11,7 @@ "@bsmappee" ], "zeroconf": [ - "_ssh._tcp.local." + {"type":"_ssh._tcp.local.", "name":"smappee1*"}, + {"type":"_ssh._tcp.local.", "name":"smappee2*"} ] } diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index f570a30baa6..51da3638a9e 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,5 +1,6 @@ """Support for exposing Home Assistant via Zeroconf.""" import asyncio +import fnmatch import ipaddress import logging import socket @@ -268,10 +269,26 @@ def setup(hass, config): # likely bad homekit data return - for domain in zeroconf_types[service_type]: + for entry in zeroconf_types[service_type]: + if len(entry) > 1: + if "macaddress" in entry: + if "properties" not in info: + continue + if "macaddress" not in info["properties"]: + continue + if not fnmatch.fnmatch( + info["properties"]["macaddress"], entry["macaddress"] + ): + continue + if "name" in entry: + if "name" not in info: + continue + if not fnmatch.fnmatch(info["name"], entry["name"]): + continue + hass.add_job( hass.config_entries.flow.async_init( - domain, context={"source": DOMAIN}, data=info + entry["domain"], context={"source": DOMAIN}, data=info ) ) diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index ea61ccfbaeb..ba49666ded3 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -7,72 +7,137 @@ To update, run python3 -m script.hassfest ZEROCONF = { "_Volumio._tcp.local.": [ - "volumio" + { + "domain": "volumio" + } ], "_api._udp.local.": [ - "guardian" + { + "domain": "guardian" + } ], "_axis-video._tcp.local.": [ - "axis", - "doorbird" + { + "domain": "axis", + "macaddress": "00408C*" + }, + { + "domain": "axis", + "macaddress": "ACCC8E*" + }, + { + "domain": "axis", + "macaddress": "B8A44F*" + }, + { + "domain": "doorbird", + "macaddress": "1CCAE3*" + } ], "_bond._tcp.local.": [ - "bond" + { + "domain": "bond" + } ], "_daap._tcp.local.": [ - "forked_daapd" + { + "domain": "forked_daapd" + } ], "_dkapi._tcp.local.": [ - "daikin" + { + "domain": "daikin" + } ], "_elg._tcp.local.": [ - "elgato" + { + "domain": "elgato" + } ], "_esphomelib._tcp.local.": [ - "esphome" + { + "domain": "esphome" + } ], "_googlecast._tcp.local.": [ - "cast" + { + "domain": "cast" + } ], "_hap._tcp.local.": [ - "homekit_controller" + { + "domain": "homekit_controller" + } ], "_http._tcp.local.": [ - "shelly" + { + "domain": "shelly", + "name": "shelly*" + } ], "_ipp._tcp.local.": [ - "ipp" + { + "domain": "ipp" + } ], "_ipps._tcp.local.": [ - "ipp" + { + "domain": "ipp" + } ], "_miio._udp.local.": [ - "xiaomi_aqara", - "xiaomi_miio" + { + "domain": "xiaomi_aqara" + }, + { + "domain": "xiaomi_miio" + } ], "_nut._tcp.local.": [ - "nut" + { + "domain": "nut" + } ], "_plugwise._tcp.local.": [ - "plugwise" + { + "domain": "plugwise" + } ], "_printer._tcp.local.": [ - "brother" + { + "domain": "brother", + "name": "brother*" + } ], "_spotify-connect._tcp.local.": [ - "spotify" + { + "domain": "spotify" + } ], "_ssh._tcp.local.": [ - "smappee" + { + "domain": "smappee", + "name": "smappee1*" + }, + { + "domain": "smappee", + "name": "smappee2*" + } ], "_viziocast._tcp.local.": [ - "vizio" + { + "domain": "vizio" + } ], "_wled._tcp.local.": [ - "wled" + { + "domain": "wled" + } ], "_xbmc-jsonrpc-h._tcp.local.": [ - "kodi" + { + "domain": "kodi" + } ] } diff --git a/homeassistant/loader.py b/homeassistant/loader.py index c5027710c47..53f793678c0 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -145,18 +145,25 @@ async def async_get_config_flows(hass: "HomeAssistant") -> Set[str]: return flows -async def async_get_zeroconf(hass: "HomeAssistant") -> Dict[str, List]: +async def async_get_zeroconf(hass: "HomeAssistant") -> Dict[str, List[Dict[str, str]]]: """Return cached list of zeroconf types.""" - zeroconf: Dict[str, List] = ZEROCONF.copy() + zeroconf: Dict[str, List[Dict[str, str]]] = ZEROCONF.copy() integrations = await async_get_custom_components(hass) for integration in integrations.values(): if not integration.zeroconf: continue - for typ in integration.zeroconf: - zeroconf.setdefault(typ, []) - if integration.domain not in zeroconf[typ]: - zeroconf[typ].append(integration.domain) + for entry in integration.zeroconf: + data = {"domain": integration.domain} + if isinstance(entry, dict): + typ = entry["type"] + entry_without_type = entry.copy() + del entry_without_type["type"] + data.update(entry_without_type) + else: + typ = entry + + zeroconf.setdefault(typ, []).append(data) return zeroconf diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index cd3895f5f20..b0148b0911a 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -38,7 +38,18 @@ MANIFEST_SCHEMA = vol.Schema( vol.Required("domain"): str, vol.Required("name"): str, vol.Optional("config_flow"): bool, - vol.Optional("zeroconf"): [str], + vol.Optional("zeroconf"): [ + vol.Any( + str, + vol.Schema( + { + vol.Required("type"): str, + vol.Optional("macaddress"): str, + vol.Optional("name"): str, + } + ), + ) + ], vol.Optional("ssdp"): vol.Schema( vol.All([vol.All(vol.Schema({}, extra=vol.ALLOW_EXTRA), vol.Length(min=1))]) ), diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index d6b39bd0d27..61162b02761 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -37,8 +37,17 @@ def generate_and_validate(integrations: Dict[str, Integration]): if not (service_types or homekit_models): continue - for service_type in service_types: - service_type_dict[service_type].append(domain) + for entry in service_types: + data = {"domain": domain} + if isinstance(entry, dict): + typ = entry["type"] + entry_without_type = entry.copy() + del entry_without_type["type"] + data.update(entry_without_type) + else: + typ = entry + + service_type_dict[typ].append(data) for model in homekit_models: if model in homekit_dict: diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 73629596c35..ae1f6d5fd98 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -79,6 +79,24 @@ def get_homekit_info_mock(model, pairing_status): return mock_homekit_info +def get_zeroconf_info_mock(macaddress): + """Return info for get_service_info for an zeroconf device.""" + + def mock_zc_info(service_type, name): + return ServiceInfo( + service_type, + name, + addresses=[b"\n\x00\x00\x14"], + port=80, + weight=0, + priority=0, + server="name.local.", + properties={b"macaddress": macaddress.encode()}, + ) + + return mock_zc_info + + async def test_setup(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.object( @@ -94,7 +112,11 @@ async def test_setup(hass, mock_zeroconf): assert len(mock_service_browser.mock_calls) == 1 expected_flow_calls = 0 for matching_components in zc_gen.ZEROCONF.values(): - expected_flow_calls += len(matching_components) + domains = set() + for component in matching_components: + if len(component) == 1: + domains.add(component["domain"]) + expected_flow_calls += len(domains) assert len(mock_config_flow.mock_calls) == expected_flow_calls # Test instance is set. @@ -209,10 +231,77 @@ async def test_service_with_invalid_name(hass, mock_zeroconf, caplog): assert "Failed to get info for device name" in caplog.text +async def test_zeroconf_match(hass, mock_zeroconf): + """Test configured options for a device are loaded via config entry.""" + + def http_only_service_update_mock(zeroconf, services, handlers): + """Call service update handler.""" + handlers[0]( + zeroconf, + "_http._tcp.local.", + "shelly108._http._tcp.local.", + ServiceStateChange.Added, + ) + + with patch.dict( + zc_gen.ZEROCONF, + {"_http._tcp.local.": [{"domain": "shelly", "name": "shelly*"}]}, + clear=True, + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = get_zeroconf_info_mock( + "FFAADDCC11DD" + ) + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "shelly" + + +async def test_zeroconf_no_match(hass, mock_zeroconf): + """Test configured options for a device are loaded via config entry.""" + + def http_only_service_update_mock(zeroconf, services, handlers): + """Call service update handler.""" + handlers[0]( + zeroconf, + "_http._tcp.local.", + "somethingelse._http._tcp.local.", + ServiceStateChange.Added, + ) + + with patch.dict( + zc_gen.ZEROCONF, + {"_http._tcp.local.": [{"domain": "shelly", "name": "shelly*"}]}, + clear=True, + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = get_zeroconf_info_mock( + "FFAADDCC11DD" + ) + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 0 + + async def test_homekit_match_partial_space(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( - zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True + zc_gen.ZEROCONF, + {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( @@ -233,7 +322,9 @@ async def test_homekit_match_partial_space(hass, mock_zeroconf): async def test_homekit_match_partial_dash(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( - zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True + zc_gen.ZEROCONF, + {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( @@ -254,7 +345,9 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf): async def test_homekit_match_full(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( - zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True + zc_gen.ZEROCONF, + {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( @@ -267,11 +360,6 @@ async def test_homekit_match_full(hass, mock_zeroconf): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - homekit_mock = get_homekit_info_mock("BSB002", HOMEKIT_STATUS_UNPAIRED) - info = homekit_mock("_hap._tcp.local.", "BSB002._hap._tcp.local.") - import pprint - - pprint.pprint(["homekit", info]) assert len(mock_service_browser.mock_calls) == 1 assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "hue" @@ -280,7 +368,9 @@ async def test_homekit_match_full(hass, mock_zeroconf): async def test_homekit_already_paired(hass, mock_zeroconf): """Test that an already paired device is sent to homekit_controller.""" with patch.dict( - zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True + zc_gen.ZEROCONF, + {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( @@ -302,7 +392,9 @@ async def test_homekit_already_paired(hass, mock_zeroconf): async def test_homekit_invalid_paring_status(hass, mock_zeroconf): """Test that missing paring data is not sent to homekit_controller.""" with patch.dict( - zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True + zc_gen.ZEROCONF, + {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( @@ -323,7 +415,9 @@ async def test_homekit_invalid_paring_status(hass, mock_zeroconf): async def test_homekit_not_paired(hass, mock_zeroconf): """Test that an not paired device is sent to homekit_controller.""" with patch.dict( - zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True + zc_gen.ZEROCONF, + {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( diff --git a/tests/test_loader.py b/tests/test_loader.py index 272b0453469..f5ba54ff269 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -218,6 +218,23 @@ def test_integration_properties(hass): assert integration.zeroconf is None assert integration.ssdp is None + integration = loader.Integration( + hass, + "custom_components.hue", + None, + { + "name": "Philips Hue", + "domain": "hue", + "dependencies": ["test-dep"], + "zeroconf": [{"type": "_hue._tcp.local.", "name": "hue*"}], + "requirements": ["test-req==1.0.0"], + }, + ) + assert integration.is_built_in is False + assert integration.homekit is None + assert integration.zeroconf == [{"type": "_hue._tcp.local.", "name": "hue*"}] + assert integration.ssdp is None + async def test_integrations_only_once(hass): """Test that we load integrations only once.""" @@ -253,6 +270,25 @@ def _get_test_integration(hass, name, config_flow): ) +def _get_test_integration_with_zeroconf_matcher(hass, name, config_flow): + """Return a generated test integration with a zeroconf matcher.""" + return loader.Integration( + hass, + f"homeassistant.components.{name}", + None, + { + "name": name, + "domain": name, + "config_flow": config_flow, + "dependencies": [], + "requirements": [], + "zeroconf": [{"type": f"_{name}._tcp.local.", "name": f"{name}*"}], + "homekit": {"models": [name]}, + "ssdp": [{"manufacturer": name, "modelName": name}], + }, + ) + + async def test_get_custom_components(hass): """Verify that custom components are cached.""" test_1_integration = _get_test_integration(hass, "test_1", False) @@ -289,7 +325,9 @@ async def test_get_config_flows(hass): async def test_get_zeroconf(hass): """Verify that custom components with zeroconf are found.""" test_1_integration = _get_test_integration(hass, "test_1", True) - test_2_integration = _get_test_integration(hass, "test_2", True) + test_2_integration = _get_test_integration_with_zeroconf_matcher( + hass, "test_2", True + ) with patch("homeassistant.loader.async_get_custom_components") as mock_get: mock_get.return_value = { @@ -297,8 +335,10 @@ async def test_get_zeroconf(hass): "test_2": test_2_integration, } zeroconf = await loader.async_get_zeroconf(hass) - assert zeroconf["_test_1._tcp.local."] == ["test_1"] - assert zeroconf["_test_2._tcp.local."] == ["test_2"] + assert zeroconf["_test_1._tcp.local."] == [{"domain": "test_1"}] + assert zeroconf["_test_2._tcp.local."] == [ + {"domain": "test_2", "name": "test_2*"} + ] async def test_get_homekit(hass): From 3d4913348a00b35aa8ba0bcdee3b1e268ea07e29 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Fri, 11 Sep 2020 13:09:31 +0200 Subject: [PATCH 052/185] Warn users if KNX has no devices configured (#39899) Co-authored-by: Martin Hjelmare --- homeassistant/components/knx/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 7e56d2a955a..5a2f29e6247 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -148,6 +148,12 @@ async def async_setup(hass, config): discovery.async_load_platform(hass, platform.value, DOMAIN, {}, config) ) + if not hass.data[DATA_KNX].xknx.devices: + _LOGGER.warning( + "No KNX devices are configured. Please read " + "https://www.home-assistant.io/blog/2020/09/17/release-115/#breaking-changes" + ) + hass.services.async_register( DOMAIN, SERVICE_KNX_SEND, From 7eade4029ab3cde6b3f6b06b6a16c16cf51af032 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 11 Sep 2020 13:08:13 +0200 Subject: [PATCH 053/185] Add children media class (#39902) Co-authored-by: Paulus Schoutsen --- homeassistant/components/kodi/browse_media.py | 39 +++++++++++----- .../components/media_player/__init__.py | 38 +++++++++------- .../components/media_source/models.py | 4 +- .../components/netatmo/media_source.py | 5 ++- .../components/philips_js/media_player.py | 3 +- .../components/plex/media_browser.py | 40 ++++++++++++++--- homeassistant/components/roku/browse_media.py | 18 ++++++-- .../components/sonos/media_player.py | 40 ++++++++++++++--- .../components/spotify/media_player.py | 45 +++++++++---------- tests/components/media_source/test_models.py | 3 ++ tests/components/plex/mock_classes.py | 5 +++ tests/components/roku/test_media_player.py | 8 ++++ 12 files changed, 181 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index 3fc3b40cd38..c7df170b5c9 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -29,8 +29,15 @@ PLAYABLE_MEDIA_TYPES = [ MEDIA_TYPE_TRACK, ] -CONTENT_TYPE_MEDIA_CLASS = { - "library_music": MEDIA_CLASS_MUSIC, +CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS = { + MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, + MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, + MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, + MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON, + MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW, +} + +CHILD_TYPE_MEDIA_CLASS = { MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON, MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, @@ -151,8 +158,10 @@ async def build_item_response(media_library, payload): except UnknownMediaType: pass - return BrowseMedia( - media_class=CONTENT_TYPE_MEDIA_CLASS[search_type], + response = BrowseMedia( + media_class=CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS.get( + search_type, MEDIA_CLASS_DIRECTORY + ), media_content_id=search_id, media_content_type=search_type, title=title, @@ -162,6 +171,13 @@ async def build_item_response(media_library, payload): thumbnail=thumbnail, ) + if search_type == "library_music": + response.children_media_class = MEDIA_CLASS_MUSIC + else: + response.calculate_children_class() + + return response + def item_payload(item, media_library): """ @@ -170,11 +186,12 @@ def item_payload(item, media_library): Used by async_browse_media. """ title = item["label"] - thumbnail = item.get("thumbnail") if thumbnail: thumbnail = media_library.thumbnail_url(thumbnail) + media_class = None + if "songid" in item: media_content_type = MEDIA_TYPE_TRACK media_content_id = f"{item['songid']}" @@ -213,16 +230,18 @@ def item_payload(item, media_library): else: # this case is for the top folder of each type # possible content types: album, artist, movie, library_music, tvshow + media_class = MEDIA_CLASS_DIRECTORY media_content_type = item["type"] media_content_id = "" can_play = False can_expand = True - try: - media_class = CONTENT_TYPE_MEDIA_CLASS[media_content_type] - except KeyError as err: - _LOGGER.debug("Unknown media type received: %s", media_content_type) - raise UnknownMediaType from err + if media_class is None: + try: + media_class = CHILD_TYPE_MEDIA_CLASS[media_content_type] + except KeyError as err: + _LOGGER.debug("Unknown media type received: %s", media_content_type) + raise UnknownMediaType from err return BrowseMedia( title=title, diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 718011b4a76..348bc521a5a 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -85,6 +85,7 @@ from .const import ( ATTR_SOUND_MODE, ATTR_SOUND_MODE_LIST, DOMAIN, + MEDIA_CLASS_DIRECTORY, SERVICE_CLEAR_PLAYLIST, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOUND_MODE, @@ -816,24 +817,10 @@ class MediaPlayerEntity(Entity): media_content_type: Optional[str] = None, media_content_id: Optional[str] = None, ) -> "BrowseMedia": - """ - Return a payload for the "media_player/browse_media" websocket command. + """Return a BrowseMedia instance. - Payload should follow this format: - { - "title": str - Title of the item - "media_class": str - Media class - "media_content_type": str - see below - "media_content_id": str - see below - - Can be passed back in to browse further - - Can be used as-is with media_player.play_media service - "can_play": bool - If item is playable - "can_expand": bool - If item contains other media - "thumbnail": str (Optional) - URL to image thumbnail for item - "children": list (Optional) - [{}, ...] - } - - Note: Children should omit the children key. + The BrowseMedia instance will be used by the + "media_player/browse_media" websocket command. """ raise NotImplementedError() @@ -1054,6 +1041,7 @@ class BrowseMedia: can_play: bool, can_expand: bool, children: Optional[List["BrowseMedia"]] = None, + children_media_class: Optional[str] = None, thumbnail: Optional[str] = None, ): """Initialize browse media item.""" @@ -1064,10 +1052,14 @@ class BrowseMedia: self.can_play = can_play self.can_expand = can_expand self.children = children + self.children_media_class = children_media_class self.thumbnail = thumbnail def as_dict(self, *, parent: bool = True) -> dict: """Convert Media class to browse media dictionary.""" + if self.children_media_class is None: + self.calculate_children_class() + response = { "title": self.title, "media_class": self.media_class, @@ -1075,6 +1067,7 @@ class BrowseMedia: "media_content_id": self.media_content_id, "can_play": self.can_play, "can_expand": self.can_expand, + "children_media_class": self.children_media_class, "thumbnail": self.thumbnail, } @@ -1089,3 +1082,14 @@ class BrowseMedia: response["children"] = [] return response + + def calculate_children_class(self) -> None: + """Count the children media classes and calculate the correct class.""" + if self.children is None or len(self.children) == 0: + return + + self.children_media_class = MEDIA_CLASS_DIRECTORY + + proposed_class = self.children[0].media_class + if all(child.media_class == proposed_class for child in self.children): + self.children_media_class = proposed_class diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index 3b2044d7e0f..e16ecbe578e 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -6,6 +6,7 @@ from typing import List, Optional, Tuple from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.const import ( MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNELS, ) @@ -53,11 +54,12 @@ class MediaSourceItem: base = BrowseMediaSource( domain=None, identifier=None, - media_class=MEDIA_CLASS_CHANNEL, + media_class=MEDIA_CLASS_DIRECTORY, media_content_type=MEDIA_TYPE_CHANNELS, title="Media Sources", can_play=False, can_expand=True, + children_media_class=MEDIA_CLASS_CHANNEL, ) base.children = [ BrowseMediaSource( diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index 02ffd608472..76527677224 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -5,6 +5,7 @@ import re from typing import Optional, Tuple from homeassistant.components.media_player.const import ( + MEDIA_CLASS_DIRECTORY, MEDIA_CLASS_VIDEO, MEDIA_TYPE_VIDEO, ) @@ -91,10 +92,12 @@ class NetatmoSource(MediaSource): else: path = f"{source}/{camera_id}" + media_class = MEDIA_CLASS_DIRECTORY if event_id is None else MEDIA_CLASS_VIDEO + media = BrowseMediaSource( domain=DOMAIN, identifier=path, - media_class=MEDIA_CLASS_VIDEO, + media_class=media_class, media_content_type=MEDIA_TYPE_VIDEO, title=title, can_play=bool( diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 7da485fca74..7e2e126437c 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -12,6 +12,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.components.media_player.const import ( MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNELS, SUPPORT_BROWSE_MEDIA, @@ -289,7 +290,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): return BrowseMedia( title="Channels", - media_class=MEDIA_CLASS_CHANNEL, + media_class=MEDIA_CLASS_DIRECTORY, media_content_id="", media_content_type=MEDIA_TYPE_CHANNELS, can_play=False, diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index 9d5572a4faa..28444c4a351 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -26,7 +26,7 @@ class UnknownMediaType(BrowseError): EXPANDABLES = ["album", "artist", "playlist", "season", "show"] PLAYLISTS_BROWSE_PAYLOAD = { "title": "Playlists", - "media_class": MEDIA_CLASS_PLAYLIST, + "media_class": MEDIA_CLASS_DIRECTORY, "media_content_id": "all", "media_content_type": "playlists", "can_play": False, @@ -94,10 +94,21 @@ def browse_media( if special_folder: if media_content_type == "server": library_or_section = plex_server.library + children_media_class = MEDIA_CLASS_DIRECTORY title = plex_server.friendly_name elif media_content_type == "library": library_or_section = plex_server.library.sectionByID(media_content_id) title = library_or_section.title + try: + children_media_class = ITEM_TYPE_MEDIA_CLASS[library_or_section.TYPE] + except KeyError as err: + raise BrowseError( + f"Media not found: {media_content_type} / {media_content_id}" + ) from err + else: + raise BrowseError( + f"Media not found: {media_content_type} / {media_content_id}" + ) payload = { "title": title, @@ -107,6 +118,7 @@ def browse_media( "can_play": False, "can_expand": True, "children": [], + "children_media_class": children_media_class, } method = SPECIAL_METHODS[special_folder] @@ -116,13 +128,20 @@ def browse_media( payload["children"].append(item_payload(item)) except UnknownMediaType: continue + return BrowseMedia(**payload) - if media_content_type in ["server", None]: - return server_payload(plex_server) + try: + if media_content_type in ["server", None]: + return server_payload(plex_server) - if media_content_type == "library": - return library_payload(plex_server, media_content_id) + if media_content_type == "library": + return library_payload(plex_server, media_content_id) + + except UnknownMediaType as err: + raise BrowseError( + f"Media not found: {media_content_type} / {media_content_id}" + ) from err if media_content_type == "playlists": return playlists_payload(plex_server) @@ -160,6 +179,11 @@ def item_payload(item): def library_section_payload(section): """Create response payload for a single library section.""" + try: + children_media_class = ITEM_TYPE_MEDIA_CLASS[section.TYPE] + except KeyError as err: + _LOGGER.debug("Unknown type received: %s", section.TYPE) + raise UnknownMediaType from err return BrowseMedia( title=section.title, media_class=MEDIA_CLASS_DIRECTORY, @@ -167,6 +191,7 @@ def library_section_payload(section): media_content_type="library", can_play=False, can_expand=True, + children_media_class=children_media_class, ) @@ -194,6 +219,7 @@ def server_payload(plex_server): can_expand=True, ) server_info.children = [] + server_info.children_media_class = MEDIA_CLASS_DIRECTORY server_info.children.append(special_library_payload(server_info, "On Deck")) server_info.children.append(special_library_payload(server_info, "Recently Added")) for library in plex_server.library.sections(): @@ -229,4 +255,6 @@ def playlists_payload(plex_server): playlists_info["children"].append(item_payload(playlist)) except UnknownMediaType: continue - return BrowseMedia(**playlists_info) + response = BrowseMedia(**playlists_info) + response.children_media_class = MEDIA_CLASS_PLAYLIST + return response diff --git a/homeassistant/components/roku/browse_media.py b/homeassistant/components/roku/browse_media.py index 809c6ac3578..f6f8c8976f1 100644 --- a/homeassistant/components/roku/browse_media.py +++ b/homeassistant/components/roku/browse_media.py @@ -13,9 +13,9 @@ from homeassistant.components.media_player.const import ( CONTENT_TYPE_MEDIA_CLASS = { MEDIA_TYPE_APP: MEDIA_CLASS_APP, - MEDIA_TYPE_APPS: MEDIA_CLASS_APP, + MEDIA_TYPE_APPS: MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL, - MEDIA_TYPE_CHANNELS: MEDIA_CLASS_CHANNEL, + MEDIA_TYPE_CHANNELS: MEDIA_CLASS_DIRECTORY, } PLAYABLE_MEDIA_TYPES = [ @@ -59,7 +59,7 @@ def build_item_response(coordinator, payload): return None return BrowseMedia( - media_class=CONTENT_TYPE_MEDIA_CLASS[search_type], + media_class=MEDIA_CLASS_DIRECTORY, media_content_id=search_id, media_content_type=search_type, title=title, @@ -139,4 +139,16 @@ def library_payload(coordinator): ) ) + if all( + child.media_content_type == MEDIA_TYPE_APPS for child in library_info.children + ): + library_info.children_media_class = MEDIA_CLASS_APP + elif all( + child.media_content_type == MEDIA_TYPE_CHANNELS + for child in library_info.children + ): + library_info.children_media_class = MEDIA_CLASS_CHANNEL + else: + library_info.children_media_class = MEDIA_CLASS_DIRECTORY + return library_info diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 51287f9f288..2b50f2864dc 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -222,6 +222,10 @@ ATTR_STATUS_LIGHT = "status_light" UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None} +class UnknownMediaType(BrowseError): + """Unknown media type.""" + + class SonosData: """Storage class for platform global data.""" @@ -1487,7 +1491,20 @@ def build_item_response(media_library, payload): except IndexError: title = LIBRARY_TITLES_MAPPING[payload["idstring"]] - media_class = SONOS_TO_MEDIA_CLASSES[MEDIA_TYPES_TO_SONOS[payload["search_type"]]] + try: + media_class = SONOS_TO_MEDIA_CLASSES[ + MEDIA_TYPES_TO_SONOS[payload["search_type"]] + ] + except KeyError: + _LOGGER.debug("Unknown media type received %s", payload["search_type"]) + return None + + children = [] + for item in media: + try: + children.append(item_payload(item)) + except UnknownMediaType: + pass return BrowseMedia( title=title, @@ -1495,7 +1512,7 @@ def build_item_response(media_library, payload): media_class=media_class, media_content_id=payload["idstring"], media_content_type=payload["search_type"], - children=[item_payload(item) for item in media], + children=children, can_play=can_play(payload["search_type"]), can_expand=can_expand(payload["search_type"]), ) @@ -1507,12 +1524,18 @@ def item_payload(item): Used by async_browse_media. """ + media_type = get_media_type(item) + try: + media_class = SONOS_TO_MEDIA_CLASSES[media_type] + except KeyError as err: + _LOGGER.debug("Unknown media type received %s", media_type) + raise UnknownMediaType from err return BrowseMedia( title=item.title, thumbnail=getattr(item, "album_art_uri", None), - media_class=SONOS_TO_MEDIA_CLASSES[get_media_type(item)], + media_class=media_class, media_content_id=get_content_id(item), - media_content_type=SONOS_TO_MEDIA_TYPES[get_media_type(item)], + media_content_type=SONOS_TO_MEDIA_TYPES[media_type], can_play=can_play(item.item_class), can_expand=can_expand(item), ) @@ -1524,6 +1547,13 @@ def library_payload(media_library): Used by async_browse_media. """ + children = [] + for item in media_library.browse(): + try: + children.append(item_payload(item)) + except UnknownMediaType: + pass + return BrowseMedia( title="Music Library", media_class=MEDIA_CLASS_DIRECTORY, @@ -1531,7 +1561,7 @@ def library_payload(media_library): media_content_type="library", can_play=False, can_expand=True, - children=[item_payload(item) for item in media_library.browse()], + children=children, ) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index e2850027229..7beea59a7bd 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -105,18 +105,18 @@ LIBRARY_MAP = { } CONTENT_TYPE_MEDIA_CLASS = { - "current_user_playlists": MEDIA_CLASS_PLAYLIST, - "current_user_followed_artists": MEDIA_CLASS_ARTIST, - "current_user_saved_albums": MEDIA_CLASS_ALBUM, - "current_user_saved_tracks": MEDIA_CLASS_TRACK, - "current_user_saved_shows": MEDIA_CLASS_PODCAST, - "current_user_recently_played": MEDIA_CLASS_TRACK, - "current_user_top_artists": MEDIA_CLASS_ARTIST, - "current_user_top_tracks": MEDIA_CLASS_TRACK, - "featured_playlists": MEDIA_CLASS_PLAYLIST, - "categories": MEDIA_CLASS_GENRE, - "category_playlists": MEDIA_CLASS_PLAYLIST, - "new_releases": MEDIA_CLASS_ALBUM, + "current_user_playlists": MEDIA_CLASS_DIRECTORY, + "current_user_followed_artists": MEDIA_CLASS_DIRECTORY, + "current_user_saved_albums": MEDIA_CLASS_DIRECTORY, + "current_user_saved_tracks": MEDIA_CLASS_DIRECTORY, + "current_user_saved_shows": MEDIA_CLASS_DIRECTORY, + "current_user_recently_played": MEDIA_CLASS_DIRECTORY, + "current_user_top_artists": MEDIA_CLASS_DIRECTORY, + "current_user_top_tracks": MEDIA_CLASS_DIRECTORY, + "featured_playlists": MEDIA_CLASS_DIRECTORY, + "categories": MEDIA_CLASS_DIRECTORY, + "category_playlists": MEDIA_CLASS_DIRECTORY, + "new_releases": MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, @@ -567,6 +567,7 @@ def build_item_response(spotify, user, payload): can_expand=True, ) ) + media_item.children_media_class = MEDIA_CLASS_GENRE return media_item if title is None: @@ -575,7 +576,7 @@ def build_item_response(spotify, user, payload): else: title = LIBRARY_MAP.get(payload["media_content_id"]) - response = { + params = { "title": title, "media_class": media_class, "media_content_id": media_content_id, @@ -586,16 +587,16 @@ def build_item_response(spotify, user, payload): } for item in items: try: - response["children"].append(item_payload(item)) + params["children"].append(item_payload(item)) except (MissingMediaInformation, UnknownMediaType): continue if "images" in media: - response["thumbnail"] = fetch_image_url(media) + params["thumbnail"] = fetch_image_url(media) elif image: - response["thumbnail"] = image + params["thumbnail"] = image - return BrowseMedia(**response) + return BrowseMedia(**params) def item_payload(item): @@ -624,17 +625,13 @@ def item_payload(item): payload = { "title": item.get("name"), + "media_class": media_class, "media_content_id": media_id, "media_content_type": media_type, "can_play": media_type in PLAYABLE_MEDIA_TYPES, "can_expand": can_expand, } - payload = { - **payload, - "media_class": media_class, - } - if "images" in item: payload["thumbnail"] = fetch_image_url(item) elif MEDIA_TYPE_ALBUM in item: @@ -665,7 +662,9 @@ def library_payload(): {"name": item["name"], "type": item["type"], "uri": item["type"]} ) ) - return BrowseMedia(**library_info) + response = BrowseMedia(**library_info) + response.children_media_class = MEDIA_CLASS_DIRECTORY + return response def fetch_image_url(item, key="images"): diff --git a/tests/components/media_source/test_models.py b/tests/components/media_source/test_models.py index 3d19edd722d..8372382bb7a 100644 --- a/tests/components/media_source/test_models.py +++ b/tests/components/media_source/test_models.py @@ -17,6 +17,7 @@ async def test_browse_media_as_dict(): title="media/", can_play=False, can_expand=True, + children_media_class=MEDIA_CLASS_MUSIC, ) base.children = [ models.BrowseMediaSource( @@ -37,6 +38,7 @@ async def test_browse_media_as_dict(): assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media" assert not item["can_play"] assert item["can_expand"] + assert item["children_media_class"] == MEDIA_CLASS_MUSIC assert len(item["children"]) == 1 assert item["children"][0]["title"] == "test.mp3" assert item["children"][0]["media_class"] == MEDIA_CLASS_MUSIC @@ -62,6 +64,7 @@ async def test_browse_media_parent_no_children(): assert not item["can_play"] assert item["can_expand"] assert len(item["children"]) == 0 + assert item["children_media_class"] is None async def test_media_source_default_name(): diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 7cdac1b669a..e16d5cdc13b 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -409,6 +409,11 @@ class MockPlexLibrarySection: if self.title == "Photos": return "photo" + @property + def TYPE(self): + """Return the library type.""" + return self.type + @property def key(self): """Mock the key identifier property.""" diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 312770a873a..e9d5091d664 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -16,6 +16,9 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_TITLE, ATTR_MEDIA_VOLUME_MUTED, DOMAIN as MP_DOMAIN, + MEDIA_CLASS_APP, + MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_APP, MEDIA_TYPE_APPS, MEDIA_TYPE_CHANNEL, @@ -499,6 +502,7 @@ async def test_media_browse(hass, aioclient_mock, hass_ws_client): assert msg["result"] assert msg["result"]["title"] == "Media Library" + assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY assert msg["result"]["media_content_type"] == "library" assert msg["result"]["can_expand"] assert not msg["result"]["can_play"] @@ -523,10 +527,12 @@ async def test_media_browse(hass, aioclient_mock, hass_ws_client): assert msg["result"] assert msg["result"]["title"] == "Apps" + assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY assert msg["result"]["media_content_type"] == MEDIA_TYPE_APPS assert msg["result"]["can_expand"] assert not msg["result"]["can_play"] assert len(msg["result"]["children"]) == 11 + assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP assert msg["result"]["children"][0]["title"] == "Satellite TV" assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP @@ -565,10 +571,12 @@ async def test_media_browse(hass, aioclient_mock, hass_ws_client): assert msg["result"] assert msg["result"]["title"] == "Channels" + assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY assert msg["result"]["media_content_type"] == MEDIA_TYPE_CHANNELS assert msg["result"]["can_expand"] assert not msg["result"]["can_play"] assert len(msg["result"]["children"]) == 2 + assert msg["result"]["children_media_class"] == MEDIA_CLASS_CHANNEL assert msg["result"]["children"][0]["title"] == "WhatsOn" assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_CHANNEL From b0b957977838c7332e39e056df4b0b1e76a326b6 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 11 Sep 2020 03:55:55 +0800 Subject: [PATCH 054/185] Disable audio for HLS or mpegts input (#39906) --- homeassistant/components/stream/worker.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index d0ba30666b0..0e861c5cefc 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -64,11 +64,16 @@ def _stream_worker_internal(hass, stream, quit_event): video_stream = container.streams.video[0] except (KeyError, IndexError): _LOGGER.error("Stream has no video") + container.close() return try: audio_stream = container.streams.audio[0] except (KeyError, IndexError): audio_stream = None + # These formats need aac_adtstoasc bitstream filter, but auto_bsf not + # compatible with empty_moov and manual bitstream filters not in PyAV + if container.format.name in {"hls", "mpegts"}: + audio_stream = None # The presentation timestamps of the first packet in each stream we receive # Use to adjust before muxing or outputting, but we don't adjust internally @@ -238,7 +243,7 @@ def _stream_worker_internal(hass, stream, quit_event): # Update last_dts processed last_dts[packet.stream] = packet.dts - # mux video packets immediately, save audio packets to be muxed all at once + # mux packets if packet.stream == video_stream: mux_video_packet(packet) # mutates packet timestamps else: From b107e87d3842c49241b691d2894ecea278a35f42 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 11 Sep 2020 14:02:17 +0200 Subject: [PATCH 055/185] Don't trigger on attribute when the attribute doesn't change (#39910) Co-authored-by: Paulus Schoutsen --- .../homeassistant/triggers/state.py | 7 ++++ .../homeassistant/triggers/test_state.py | 36 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 0fa7a98b562..f57db0ed56a 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -80,6 +80,13 @@ async def async_attach_trigger( else: new_value = to_s.attributes.get(attribute) + # When we listen for state changes with `match_all`, we + # will trigger even if just an attribute changes. When + # we listen to just an attribute, we should ignore all + # other attribute changes. + if attribute is not None and old_value == new_value: + return + if ( not match_from_state(old_value) or not match_to_state(new_value) diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index 68ce907bdae..ce9ecaba1b0 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -1112,6 +1112,42 @@ async def test_attribute_if_fires_on_entity_change_with_both_filters(hass, calls assert len(calls) == 1 +async def test_attribute_if_fires_on_entity_where_attr_stays_constant(hass, calls): + """Test for firing if attribute stays the same.""" + hass.states.async_set("test.entity", "bla", {"name": "hello", "other": "old_value"}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "state", + "entity_id": "test.entity", + "attribute": "name", + }, + "action": {"service": "test.automation"}, + } + }, + ) + await hass.async_block_till_done() + + # Leave all attributes the same + hass.states.async_set("test.entity", "bla", {"name": "hello", "other": "old_value"}) + await hass.async_block_till_done() + assert len(calls) == 0 + + # Change the untracked attribute + hass.states.async_set("test.entity", "bla", {"name": "hello", "other": "new_value"}) + await hass.async_block_till_done() + assert len(calls) == 0 + + # Change the tracked attribute + hass.states.async_set("test.entity", "bla", {"name": "world", "other": "old_value"}) + await hass.async_block_till_done() + assert len(calls) == 1 + + async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( hass, calls ): From 8ef04268be1cb2c5d2bc489d62bef79e1b51214f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Sep 2020 12:24:16 +0200 Subject: [PATCH 056/185] Extract variable rendering (#39934) --- .../components/automation/__init__.py | 21 +++---- homeassistant/components/script/__init__.py | 2 +- homeassistant/helpers/config_validation.py | 11 +++- homeassistant/helpers/script.py | 19 +++--- homeassistant/helpers/script_variables.py | 57 ++++++++++++++++++ tests/components/automation/test_init.py | 21 ++++++- tests/components/script/test_init.py | 27 ++++++++- tests/helpers/test_script_variables.py | 60 +++++++++++++++++++ 8 files changed, 193 insertions(+), 25 deletions(-) create mode 100644 homeassistant/helpers/script_variables.py create mode 100644 tests/helpers/test_script_variables.py diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 392ca710000..dff751956a7 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -45,6 +45,7 @@ from homeassistant.helpers.script import ( Script, make_script_schema, ) +from homeassistant.helpers.script_variables import ScriptVariables from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.trigger import async_initialize_triggers from homeassistant.helpers.typing import TemplateVarsType @@ -256,8 +257,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._referenced_entities: Optional[Set[str]] = None self._referenced_devices: Optional[Set[str]] = None self._logger = _LOGGER - self._variables = variables - self._variables_dynamic = template.is_complex(variables) + self._variables: ScriptVariables = variables @property def name(self): @@ -334,9 +334,6 @@ class AutomationEntity(ToggleEntity, RestoreEntity): """Startup with initial state or previous state.""" await super().async_added_to_hass() - if self._variables_dynamic: - template.attach(cast(HomeAssistant, self.hass), self._variables) - self._logger = logging.getLogger( f"{__name__}.{split_entity_id(self.entity_id)[1]}" ) @@ -392,15 +389,13 @@ class AutomationEntity(ToggleEntity, RestoreEntity): This method is a coroutine. """ if self._variables: - if self._variables_dynamic: - variables = template.render_complex(self._variables, run_variables) - else: - variables = dict(self._variables) + try: + variables = self._variables.async_render(self.hass, run_variables) + except template.TemplateError as err: + self._logger.error("Error rendering variables: %s", err) + return else: - variables = {} - - if run_variables: - variables.update(run_variables) + variables = run_variables if ( not skip_condition diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 1e0fad9be5d..eab30e01ee2 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -77,7 +77,7 @@ CONFIG_SCHEMA = vol.Schema( SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) SCRIPT_TURN_ONOFF_SCHEMA = make_entity_service_schema( - {vol.Optional(ATTR_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA} + {vol.Optional(ATTR_VARIABLES): {str: cv.match_all}} ) RELOAD_SERVICE_SCHEMA = vol.Schema({}) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index a54f97ec7e5..602a8ebfd2a 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -81,7 +81,10 @@ from homeassistant.const import ( ) from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template as template_helper +from homeassistant.helpers import ( + script_variables as script_variables_helper, + template as template_helper, +) from homeassistant.helpers.logging import KeywordStyleAdapter from homeassistant.util import slugify as util_slugify import homeassistant.util.dt as dt_util @@ -863,7 +866,11 @@ def make_entity_service_schema( ) -SCRIPT_VARIABLES_SCHEMA = vol.Schema({str: template_complex}) +SCRIPT_VARIABLES_SCHEMA = vol.All( + vol.Schema({str: template_complex}), + # pylint: disable=unnecessary-lambda + lambda val: script_variables_helper.ScriptVariables(val), +) def script_action(value: Any) -> dict: diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index cd664974431..bd1442587eb 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -55,6 +55,7 @@ from homeassistant.const import ( from homeassistant.core import SERVICE_CALL_LIMIT, Context, HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv, 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, @@ -717,7 +718,7 @@ class Script: logger: Optional[logging.Logger] = None, log_exceptions: bool = True, top_level: bool = True, - variables: Optional[Dict[str, Any]] = None, + variables: Optional[ScriptVariables] = None, ) -> None: """Initialize the script.""" all_scripts = hass.data.get(DATA_SCRIPTS) @@ -900,15 +901,19 @@ class Script: # during the run back to the caller. if self._top_level: if self.variables: - if self._variables_dynamic: - variables = template.render_complex(self.variables, run_variables) - else: - variables = dict(self.variables) + try: + variables = self.variables.async_render( + self._hass, + run_variables, + ) + except template.TemplateError as err: + self._log("Error rendering variables: %s", err, level=logging.ERROR) + raise + elif run_variables: + variables = dict(run_variables) else: variables = {} - if run_variables: - variables.update(run_variables) variables["context"] = context else: variables = cast(dict, run_variables) diff --git a/homeassistant/helpers/script_variables.py b/homeassistant/helpers/script_variables.py new file mode 100644 index 00000000000..001c3b8667c --- /dev/null +++ b/homeassistant/helpers/script_variables.py @@ -0,0 +1,57 @@ +"""Script variables.""" +from typing import Any, Dict, Mapping, Optional + +from homeassistant.core import HomeAssistant, callback + +from . import template + + +class ScriptVariables: + """Class to hold and render script variables.""" + + def __init__(self, variables: Dict[str, Any]): + """Initialize script variables.""" + self.variables = variables + self._has_template: Optional[bool] = None + + @callback + def async_render( + self, + hass: HomeAssistant, + run_variables: Optional[Mapping[str, Any]], + ) -> Dict[str, Any]: + """Render script variables. + + The run variables are used to compute the static variables, but afterwards will also + be merged on top of the static variables. + """ + if self._has_template is None: + self._has_template = template.is_complex(self.variables) + template.attach(hass, self.variables) + + if not self._has_template: + rendered_variables = dict(self.variables) + + if run_variables is not None: + rendered_variables.update(run_variables) + + return rendered_variables + + rendered_variables = {} if run_variables is None else dict(run_variables) + + for key, value in self.variables.items(): + # We can skip if we're going to override this key with + # run variables anyway + if key in rendered_variables: + continue + + rendered_variables[key] = template.render_complex(value, rendered_variables) + + if run_variables: + rendered_variables.update(run_variables) + + return rendered_variables + + def as_dict(self) -> dict: + """Return dict version of this class.""" + return self.variables diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 5ee0ff62af2..9c38574945d 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1136,7 +1136,7 @@ async def test_logbook_humanify_automation_triggered_event(hass): assert event2["entity_id"] == "automation.bye" -async def test_automation_variables(hass): +async def test_automation_variables(hass, caplog): """Test automation variables.""" calls = async_mock_service(hass, "test", "automation") @@ -1172,6 +1172,15 @@ async def test_automation_variables(hass): "service": "test.automation", }, }, + { + "variables": { + "test_var": "{{ trigger.event.data.break + 1 }}", + }, + "trigger": {"platform": "event", "event_type": "test_event_3"}, + "action": { + "service": "test.automation", + }, + }, ] }, ) @@ -1188,3 +1197,13 @@ async def test_automation_variables(hass): hass.bus.async_fire("test_event_2", {"pass_condition": True}) await hass.async_block_till_done() assert len(calls) == 2 + + assert "Error rendering variables" not in caplog.text + hass.bus.async_fire("test_event_3") + await hass.async_block_till_done() + assert len(calls) == 2 + assert "Error rendering variables" in caplog.text + + hass.bus.async_fire("test_event_3", {"break": 0}) + await hass.async_block_till_done() + assert len(calls) == 3 diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 5fb832d0f36..152c74d8fe9 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -17,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, callback, split_entity_id from homeassistant.exceptions import ServiceNotFound +from homeassistant.helpers import template from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import bind_hass @@ -617,7 +618,7 @@ async def test_concurrent_script(hass, concurrently): assert not script.is_on(hass, "script.script2") -async def test_script_variables(hass): +async def test_script_variables(hass, caplog): """Test defining scripts.""" assert await async_setup_component( hass, @@ -652,6 +653,19 @@ async def test_script_variables(hass): }, ], }, + "script3": { + "variables": { + "test_var": "{{ break + 1 }}", + }, + "sequence": [ + { + "service": "test.script", + "data": { + "value": "{{ test_var }}", + }, + }, + ], + }, } }, ) @@ -681,3 +695,14 @@ async def test_script_variables(hass): assert len(mock_calls) == 3 assert mock_calls[2].data["value"] == "from_service" + + assert "Error rendering variables" not in caplog.text + with pytest.raises(template.TemplateError): + await hass.services.async_call("script", "script3", blocking=True) + assert "Error rendering variables" in caplog.text + assert len(mock_calls) == 3 + + await hass.services.async_call("script", "script3", {"break": 0}, blocking=True) + + assert len(mock_calls) == 4 + assert mock_calls[3].data["value"] == "1" diff --git a/tests/helpers/test_script_variables.py b/tests/helpers/test_script_variables.py new file mode 100644 index 00000000000..6e671d14a23 --- /dev/null +++ b/tests/helpers/test_script_variables.py @@ -0,0 +1,60 @@ +"""Test script variables.""" +import pytest + +from homeassistant.helpers import config_validation as cv, template + + +async def test_static_vars(): + """Test static vars.""" + orig = {"hello": "world"} + var = cv.SCRIPT_VARIABLES_SCHEMA(orig) + rendered = var.async_render(None, None) + assert rendered is not orig + assert rendered == orig + + +async def test_static_vars_run_args(): + """Test static vars.""" + orig = {"hello": "world"} + orig_copy = dict(orig) + var = cv.SCRIPT_VARIABLES_SCHEMA(orig) + rendered = var.async_render(None, {"hello": "override", "run": "var"}) + assert rendered == {"hello": "override", "run": "var"} + # Make sure we don't change original vars + assert orig == orig_copy + + +async def test_template_vars(hass): + """Test template vars.""" + var = cv.SCRIPT_VARIABLES_SCHEMA({"hello": "{{ 1 + 1 }}"}) + rendered = var.async_render(hass, None) + assert rendered == {"hello": "2"} + + +async def test_template_vars_run_args(hass): + """Test template vars.""" + var = cv.SCRIPT_VARIABLES_SCHEMA( + { + "something": "{{ run_var_ex + 1 }}", + "something_2": "{{ run_var_ex + 1 }}", + } + ) + rendered = var.async_render( + hass, + { + "run_var_ex": 5, + "something_2": 1, + }, + ) + assert rendered == { + "run_var_ex": 5, + "something": "6", + "something_2": 1, + } + + +async def test_template_vars_error(hass): + """Test template vars.""" + var = cv.SCRIPT_VARIABLES_SCHEMA({"hello": "{{ canont.work }}"}) + with pytest.raises(template.TemplateError): + var.async_render(hass, None) From b1b7944012167c59dbf957225037e67086665198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Lov=C3=A9n?= Date: Fri, 11 Sep 2020 13:16:25 +0200 Subject: [PATCH 057/185] Set variable values in scripts (#39915) Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/config_validation.py | 13 ++++++ homeassistant/helpers/script.py | 9 ++++ homeassistant/helpers/script_variables.py | 25 +++++++---- tests/helpers/test_script.py | 39 ++++++++++++++++ tests/helpers/test_script_variables.py | 52 ++++++++++++++++++++++ 5 files changed, 129 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 602a8ebfd2a..282e63e6440 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -67,6 +67,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_METRIC, CONF_UNTIL, CONF_VALUE_TEMPLATE, + CONF_VARIABLES, CONF_WAIT_FOR_TRIGGER, CONF_WAIT_TEMPLATE, CONF_WHILE, @@ -1127,6 +1128,13 @@ _SCRIPT_WAIT_FOR_TRIGGER_SCHEMA = vol.Schema( } ) +_SCRIPT_SET_SCHEMA = vol.Schema( + { + vol.Optional(CONF_ALIAS): string, + vol.Required(CONF_VARIABLES): SCRIPT_VARIABLES_SCHEMA, + } +) + SCRIPT_ACTION_DELAY = "delay" SCRIPT_ACTION_WAIT_TEMPLATE = "wait_template" SCRIPT_ACTION_CHECK_CONDITION = "condition" @@ -1137,6 +1145,7 @@ SCRIPT_ACTION_ACTIVATE_SCENE = "scene" SCRIPT_ACTION_REPEAT = "repeat" SCRIPT_ACTION_CHOOSE = "choose" SCRIPT_ACTION_WAIT_FOR_TRIGGER = "wait_for_trigger" +SCRIPT_ACTION_VARIABLES = "variables" def determine_script_action(action: dict) -> str: @@ -1168,6 +1177,9 @@ def determine_script_action(action: dict) -> str: if CONF_WAIT_FOR_TRIGGER in action: return SCRIPT_ACTION_WAIT_FOR_TRIGGER + if CONF_VARIABLES in action: + return SCRIPT_ACTION_VARIABLES + return SCRIPT_ACTION_CALL_SERVICE @@ -1182,4 +1194,5 @@ ACTION_TYPE_SCHEMAS: Dict[str, Callable[[Any], dict]] = { SCRIPT_ACTION_REPEAT: _SCRIPT_REPEAT_SCHEMA, SCRIPT_ACTION_CHOOSE: _SCRIPT_CHOOSE_SCHEMA, SCRIPT_ACTION_WAIT_FOR_TRIGGER: _SCRIPT_WAIT_FOR_TRIGGER_SCHEMA, + SCRIPT_ACTION_VARIABLES: _SCRIPT_SET_SCHEMA, } diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index bd1442587eb..717e9c3980c 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -46,6 +46,7 @@ from homeassistant.const import ( CONF_SEQUENCE, CONF_TIMEOUT, CONF_UNTIL, + CONF_VARIABLES, CONF_WAIT_FOR_TRIGGER, CONF_WAIT_TEMPLATE, CONF_WHILE, @@ -612,6 +613,14 @@ class _ScriptRun: task.cancel() remove_triggers() + async def _async_variables_step(self): + """Set a variable value.""" + self._script.last_action = self._action.get(CONF_ALIAS, "setting variables") + self._log("Executing step %s", self._script.last_action) + self._variables = self._action[CONF_VARIABLES].async_render( + self._hass, self._variables, render_as_defaults=False + ) + async def _async_run_script(self, script): """Execute a script.""" await self._async_run_long_action( diff --git a/homeassistant/helpers/script_variables.py b/homeassistant/helpers/script_variables.py index 001c3b8667c..3140fc4dced 100644 --- a/homeassistant/helpers/script_variables.py +++ b/homeassistant/helpers/script_variables.py @@ -19,21 +19,31 @@ class ScriptVariables: self, hass: HomeAssistant, run_variables: Optional[Mapping[str, Any]], + *, + render_as_defaults: bool = True, ) -> Dict[str, Any]: """Render script variables. - The run variables are used to compute the static variables, but afterwards will also - be merged on top of the static variables. + The run variables are used to compute the static variables. + + If `render_as_defaults` is True, the run variables will not be overridden. + """ if self._has_template is None: self._has_template = template.is_complex(self.variables) template.attach(hass, self.variables) if not self._has_template: - rendered_variables = dict(self.variables) + if render_as_defaults: + rendered_variables = dict(self.variables) - if run_variables is not None: - rendered_variables.update(run_variables) + if run_variables is not None: + rendered_variables.update(run_variables) + else: + rendered_variables = ( + {} if run_variables is None else dict(run_variables) + ) + rendered_variables.update(self.variables) return rendered_variables @@ -42,14 +52,11 @@ class ScriptVariables: for key, value in self.variables.items(): # We can skip if we're going to override this key with # run variables anyway - if key in rendered_variables: + if render_as_defaults and key in rendered_variables: continue rendered_variables[key] = template.render_complex(value, rendered_variables) - if run_variables: - rendered_variables.update(run_variables) - return rendered_variables def as_dict(self) -> dict: diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index d298283d11e..0bd353e1fa0 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1785,3 +1785,42 @@ async def test_started_action(hass, caplog): await hass.async_block_till_done() assert log_message in caplog.text + + +async def test_set_variable(hass, caplog): + """Test setting variables in scripts.""" + sequence = cv.SCRIPT_SCHEMA( + [ + {"variables": {"variable": "value"}}, + {"service": "test.script", "data": {"value": "{{ variable }}"}}, + ] + ) + script_obj = script.Script(hass, sequence, "test script", "test_domain") + + mock_calls = async_mock_service(hass, "test", "script") + + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert mock_calls[0].data["value"] == "value" + + +async def test_set_redefines_variable(hass, caplog): + """Test setting variables based on their current value.""" + sequence = cv.SCRIPT_SCHEMA( + [ + {"variables": {"variable": "1"}}, + {"service": "test.script", "data": {"value": "{{ variable }}"}}, + {"variables": {"variable": "{{ variable | int + 1 }}"}}, + {"service": "test.script", "data": {"value": "{{ variable }}"}}, + ] + ) + script_obj = script.Script(hass, sequence, "test script", "test_domain") + + mock_calls = async_mock_service(hass, "test", "script") + + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert mock_calls[0].data["value"] == "1" + assert mock_calls[1].data["value"] == "2" diff --git a/tests/helpers/test_script_variables.py b/tests/helpers/test_script_variables.py index 6e671d14a23..20a70cb33eb 100644 --- a/tests/helpers/test_script_variables.py +++ b/tests/helpers/test_script_variables.py @@ -24,6 +24,28 @@ async def test_static_vars_run_args(): assert orig == orig_copy +async def test_static_vars_no_default(): + """Test static vars.""" + orig = {"hello": "world"} + var = cv.SCRIPT_VARIABLES_SCHEMA(orig) + rendered = var.async_render(None, None, render_as_defaults=False) + assert rendered is not orig + assert rendered == orig + + +async def test_static_vars_run_args_no_default(): + """Test static vars.""" + orig = {"hello": "world"} + orig_copy = dict(orig) + var = cv.SCRIPT_VARIABLES_SCHEMA(orig) + rendered = var.async_render( + None, {"hello": "override", "run": "var"}, render_as_defaults=False + ) + assert rendered == {"hello": "world", "run": "var"} + # Make sure we don't change original vars + assert orig == orig_copy + + async def test_template_vars(hass): """Test template vars.""" var = cv.SCRIPT_VARIABLES_SCHEMA({"hello": "{{ 1 + 1 }}"}) @@ -53,6 +75,36 @@ async def test_template_vars_run_args(hass): } +async def test_template_vars_no_default(hass): + """Test template vars.""" + var = cv.SCRIPT_VARIABLES_SCHEMA({"hello": "{{ 1 + 1 }}"}) + rendered = var.async_render(hass, None, render_as_defaults=False) + assert rendered == {"hello": "2"} + + +async def test_template_vars_run_args_no_default(hass): + """Test template vars.""" + var = cv.SCRIPT_VARIABLES_SCHEMA( + { + "something": "{{ run_var_ex + 1 }}", + "something_2": "{{ run_var_ex + 1 }}", + } + ) + rendered = var.async_render( + hass, + { + "run_var_ex": 5, + "something_2": 1, + }, + render_as_defaults=False, + ) + assert rendered == { + "run_var_ex": 5, + "something": "6", + "something_2": "6", + } + + async def test_template_vars_error(hass): """Test template vars.""" var = cv.SCRIPT_VARIABLES_SCHEMA({"hello": "{{ canont.work }}"}) From 5201410e39e8d77bae52402ac7c14536403a5c8d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Sep 2020 23:07:23 +0200 Subject: [PATCH 058/185] Bump aioshelly to 0.3.1 (#39917) --- homeassistant/components/shelly/manifest.json | 2 +- homeassistant/components/shelly/switch.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 1c12125fd89..b2d3a7b7795 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==0.3.0"], + "requirements": ["aioshelly==0.3.1"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], "codeowners": ["@balloob", "@bieniu"] } diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 5550240478f..0aaa6dbc911 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -1,5 +1,5 @@ """Switch for Shelly.""" -from aioshelly import RelayBlock +from aioshelly import Block from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback @@ -28,7 +28,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RelaySwitch(ShellyBlockEntity, SwitchEntity): """Switch that controls a relay block on Shelly devices.""" - def __init__(self, wrapper: ShellyDeviceWrapper, block: RelayBlock) -> None: + def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None: """Initialize relay switch.""" super().__init__(wrapper, block) self.control_result = None diff --git a/requirements_all.txt b/requirements_all.txt index b875fa5e4a2..dc1e4bd8951 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -221,7 +221,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.shelly -aioshelly==0.3.0 +aioshelly==0.3.1 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69b6d51416f..952d4018989 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.shelly -aioshelly==0.3.0 +aioshelly==0.3.1 # homeassistant.components.switcher_kis aioswitcher==1.2.1 From 758e60a58de35247f6a71e87c003693129bb795d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Sep 2020 16:19:11 -0500 Subject: [PATCH 059/185] Prevent missing integration from failing HomeKit startup (#39918) --- homeassistant/components/homekit/__init__.py | 11 ++- tests/components/homekit/test_homekit.py | 93 +++++++++++++++++++- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index e77d4ef134e..acc96397601 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -38,7 +38,7 @@ from homeassistant.helpers import device_registry, entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import BASE_FILTER_SCHEMA, FILTER_SCHEMA from homeassistant.helpers.reload import async_integration_yaml_config -from homeassistant.loader import async_get_integration +from homeassistant.loader import IntegrationNotFound, async_get_integration from homeassistant.util import get_local_ip from .accessories import get_accessory @@ -712,8 +712,13 @@ class HomeKit: if dev_reg_ent.sw_version: ent_cfg[ATTR_SOFTWARE_VERSION] = dev_reg_ent.sw_version if ATTR_MANUFACTURER not in ent_cfg: - integration = await async_get_integration(self.hass, ent_reg_ent.platform) - ent_cfg[ATTR_INTERGRATION] = integration.name + try: + integration = await async_get_integration( + self.hass, ent_reg_ent.platform + ) + ent_cfg[ATTR_INTERGRATION] = integration.name + except IntegrationNotFound: + ent_cfg[ATTR_INTERGRATION] = ent_reg_ent.platform class HomeKitPairingQRView(HomeAssistantView): diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 16473cd7b22..757281af1e9 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -807,6 +807,91 @@ async def test_homekit_finds_linked_batteries( ) +async def test_homekit_async_get_integration_fails( + hass, hk_driver, debounce_patcher, device_reg, entity_reg +): + """Test that we continue if async_get_integration fails.""" + entry = await async_init_integration(hass) + + homekit = HomeKit( + hass, + None, + None, + None, + {}, + {"light.demo": {}}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + entry_id=entry.entry_id, + ) + homekit.driver = hk_driver + # pylint: disable=protected-access + homekit._filter = Mock(return_value=True) + homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge") + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + sw_version="0.16.0", + model="Powerwall 2", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + binary_charging_sensor = entity_reg.async_get_or_create( + "binary_sensor", + "invalid_integration_does_not_exist", + "battery_charging", + device_id=device_entry.id, + device_class=DEVICE_CLASS_BATTERY_CHARGING, + ) + battery_sensor = entity_reg.async_get_or_create( + "sensor", + "invalid_integration_does_not_exist", + "battery", + device_id=device_entry.id, + device_class=DEVICE_CLASS_BATTERY, + ) + light = entity_reg.async_get_or_create( + "light", "invalid_integration_does_not_exist", "demo", device_id=device_entry.id + ) + + hass.states.async_set( + binary_charging_sensor.entity_id, + STATE_ON, + {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING}, + ) + hass.states.async_set( + battery_sensor.entity_id, 30, {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY} + ) + hass.states.async_set(light.entity_id, STATE_ON) + + def _mock_get_accessory(*args, **kwargs): + return [None, "acc", None] + + with patch.object(homekit.bridge, "add_accessory"), patch( + f"{PATH_HOMEKIT}.show_setup_message" + ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( + "pyhap.accessory_driver.AccessoryDriver.start_service" + ): + await homekit.async_start() + await hass.async_block_till_done() + + mock_get_acc.assert_called_with( + hass, + hk_driver, + ANY, + ANY, + { + "model": "Powerwall 2", + "sw_version": "0.16.0", + "platform": "invalid_integration_does_not_exist", + "linked_battery_charging_sensor": "binary_sensor.invalid_integration_does_not_exist_battery_charging", + "linked_battery_sensor": "sensor.invalid_integration_does_not_exist_battery", + }, + ) + + async def test_setup_imported(hass): """Test async_setup with imported config options.""" legacy_persist_file_path = hass.config.path(HOMEKIT_FILE) @@ -1222,7 +1307,13 @@ async def test_reload(hass): ) with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), patch( f"{PATH_HOMEKIT}.HomeKit" - ) as mock_homekit2: + ) as mock_homekit2, patch.object(homekit.bridge, "add_accessory"), patch( + f"{PATH_HOMEKIT}.show_setup_message" + ), patch( + f"{PATH_HOMEKIT}.get_accessory" + ), patch( + "pyhap.accessory_driver.AccessoryDriver.start_service" + ): mock_homekit2.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() await hass.services.async_call( From 3fbde22cc41c06e7f05adc8772aa8ad965b41ec6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Sep 2020 00:15:13 -0500 Subject: [PATCH 060/185] Update zeroconf to 0.28.5 (#39923) --- 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 c703283e38d..7ac2cf9c5f1 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.4"], + "requirements": ["zeroconf==0.28.5"], "dependencies": ["api"], "codeowners": ["@Kane610"], "quality_scale": "internal" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 53f22d3787b..afbec0dd3d0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ sqlalchemy==1.3.19 voluptuous-serialize==2.4.0 voluptuous==0.11.7 yarl==1.4.2 -zeroconf==0.28.4 +zeroconf==0.28.5 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index dc1e4bd8951..3efe49e0f7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2287,7 +2287,7 @@ youtube_dl==2020.07.28 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.28.4 +zeroconf==0.28.5 # homeassistant.components.zha zha-quirks==0.0.44 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 952d4018989..c17fc21d7a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1053,7 +1053,7 @@ xmltodict==0.12.0 yeelight==0.5.3 # homeassistant.components.zeroconf -zeroconf==0.28.4 +zeroconf==0.28.5 # homeassistant.components.zha zha-quirks==0.0.44 From db64a9ebfa5fe90ab98ae3067ca38a3a9bd16d24 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 11 Sep 2020 13:00:00 +0200 Subject: [PATCH 061/185] Accept known hosts for get_url for OAuth (#39936) --- homeassistant/helpers/network.py | 32 +++++++++++++++++++ tests/helpers/test_network.py | 53 ++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index d40fd9fad2b..8bdfc286c1a 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -75,6 +75,38 @@ def get_url( except NoURLAvailableError: pass + # For current request, we accept loopback interfaces (e.g., 127.0.0.1), + # the Supervisor hostname and localhost transparently + request_host = _get_request_host() + if ( + require_current_request + and request_host is not None + and hass.config.api is not None + ): + scheme = "https" if hass.config.api.use_ssl else "http" + current_url = yarl.URL.build( + scheme=scheme, host=request_host, port=hass.config.api.port + ) + + known_hostname = None + if hass.components.hassio.is_hassio(): + host_info = hass.components.hassio.get_host_info() + known_hostname = f"{host_info['hostname']}.local" + + if ( + ( + ( + allow_ip + and is_ip_address(request_host) + and is_loopback(ip_address(request_host)) + ) + or request_host in ["localhost", known_hostname] + ) + and (not require_ssl or current_url.scheme == "https") + and (not require_standard_port or current_url.is_default_port()) + ): + return normalize_url(str(current_url)) + # We have to be honest now, we have no viable option available raise NoURLAvailableError diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index f51ee2090dc..ed97b3e3757 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -15,6 +15,7 @@ from homeassistant.helpers.network import ( ) from tests.async_mock import Mock, patch +from tests.common import mock_component async def test_get_url_internal(hass: HomeAssistant): @@ -799,3 +800,55 @@ async def test_get_external_url_with_base_url_fallback(hass: HomeAssistant): assert _get_external_url(hass, allow_ip=False) == "https://example.com" assert _get_external_url(hass, require_standard_port=True) == "https://example.com" assert _get_external_url(hass, require_ssl=True) == "https://example.com" + + +async def test_get_current_request_url_with_known_host( + hass: HomeAssistant, current_request +): + """Test getting current request URL with known hosts addresses.""" + hass.config.api = Mock( + use_ssl=False, port=8123, local_ip="127.0.0.1", deprecated_base_url=None + ) + assert hass.config.internal_url is None + + with pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True) + + # Ensure we accept localhost + with patch( + "homeassistant.helpers.network._get_request_host", return_value="localhost" + ): + assert get_url(hass, require_current_request=True) == "http://localhost:8123" + with pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True, require_ssl=True) + with pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True, require_standard_port=True) + + # Ensure we accept local loopback ip (e.g., 127.0.0.1) + with patch( + "homeassistant.helpers.network._get_request_host", return_value="127.0.0.8" + ): + assert get_url(hass, require_current_request=True) == "http://127.0.0.8:8123" + with pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True, allow_ip=False) + + # Ensure hostname from Supervisor is accepted transparently + mock_component(hass, "hassio") + hass.components.hassio.is_hassio = Mock(return_value=True) + hass.components.hassio.get_host_info = Mock( + return_value={"hostname": "homeassistant"} + ) + + with patch( + "homeassistant.helpers.network._get_request_host", + return_value="homeassistant.local", + ): + assert ( + get_url(hass, require_current_request=True) + == "http://homeassistant.local:8123" + ) + + with patch( + "homeassistant.helpers.network._get_request_host", return_value="unknown.local" + ), pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True) From a002e9b12fb6dad3a97f9f8ca06131fa3e4017c2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Sep 2020 12:18:53 +0000 Subject: [PATCH 062/185] Bumped version to 0.115.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7a32ea1ee58..2eb1d3c6d83 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -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 18be6cbadc73ed70d44565fd99fdede3dfacd130 Mon Sep 17 00:00:00 2001 From: On Freund Date: Sat, 12 Sep 2020 15:22:14 +0300 Subject: [PATCH 063/185] Handle Kodi shutdown (#39856) * Handle Kodi shutdown * Core review comments * Make async_on_quit a coroutine --- homeassistant/components/kodi/manifest.json | 2 +- homeassistant/components/kodi/media_player.py | 26 ++++++++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index b3794d5dfa2..da4daf85ada 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -2,7 +2,7 @@ "domain": "kodi", "name": "Kodi", "documentation": "https://www.home-assistant.io/integrations/kodi", - "requirements": ["pykodi==0.1.2"], + "requirements": ["pykodi==0.2.0"], "codeowners": [ "@OnFreund" ], diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index ce05f3fc732..f13d5301625 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -5,6 +5,7 @@ import logging import re import jsonrpc_base +from pykodi import CannotConnectError import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity @@ -324,11 +325,15 @@ class KodiEntity(MediaPlayerEntity): self._app_properties["muted"] = data["muted"] self.async_write_ha_state() - @callback - def async_on_quit(self, sender, data): + async def async_on_quit(self, sender, data): """Reset the player state on quit action.""" + await self._clear_connection() + + async def _clear_connection(self, close=True): self._reset_state() - self.hass.async_create_task(self._connection.close()) + self.async_write_ha_state() + if close: + await self._connection.close() @property def unique_id(self): @@ -386,14 +391,23 @@ class KodiEntity(MediaPlayerEntity): try: await self._connection.connect() self._on_ws_connected() - except jsonrpc_base.jsonrpc.TransportError: - _LOGGER.info("Unable to connect to Kodi via websocket") + except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError): _LOGGER.debug("Unable to connect to Kodi via websocket", exc_info=True) + await self._clear_connection(False) + + async def _ping(self): + try: + await self._kodi.ping() + except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError): + _LOGGER.debug("Unable to ping Kodi via websocket", exc_info=True) + await self._clear_connection() async def _async_connect_websocket_if_disconnected(self, *_): """Reconnect the websocket if it fails.""" if not self._connection.connected: await self._async_ws_connect() + else: + await self._ping() @callback def _register_ws_callbacks(self): @@ -464,7 +478,7 @@ class KodiEntity(MediaPlayerEntity): @property def should_poll(self): """Return True if entity has to be polled for state.""" - return (not self._connection.can_subscribe) or (not self._connection.connected) + return not self._connection.can_subscribe @property def volume_level(self): diff --git a/requirements_all.txt b/requirements_all.txt index 3efe49e0f7a..2fcd10be276 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1431,7 +1431,7 @@ pyitachip2ir==0.0.7 pykira==0.1.1 # homeassistant.components.kodi -pykodi==0.1.2 +pykodi==0.2.0 # homeassistant.components.kwb pykwb==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c17fc21d7a2..f07db06eff0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -692,7 +692,7 @@ pyisy==2.0.2 pykira==0.1.1 # homeassistant.components.kodi -pykodi==0.1.2 +pykodi==0.2.0 # homeassistant.components.lastfm pylast==3.3.0 From 3240be0bb6d6b2012361a94ea5f8a2eac5a123fd Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Fri, 11 Sep 2020 19:47:48 +0100 Subject: [PATCH 064/185] Bump pyloopenergy library to 0.2.1 (#39919) --- CODEOWNERS | 1 + homeassistant/components/loopenergy/manifest.json | 6 ++++-- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 70d89c5e45e..42c39544540 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -236,6 +236,7 @@ homeassistant/components/linux_battery/* @fabaff homeassistant/components/local_ip/* @issacg homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd +homeassistant/components/loopenergy/* @pavoni homeassistant/components/lovelace/* @home-assistant/frontend homeassistant/components/luci/* @fbradyirl @mzdrale homeassistant/components/luftdaten/* @fabaff diff --git a/homeassistant/components/loopenergy/manifest.json b/homeassistant/components/loopenergy/manifest.json index cf7343af6a4..9b421083d10 100644 --- a/homeassistant/components/loopenergy/manifest.json +++ b/homeassistant/components/loopenergy/manifest.json @@ -2,6 +2,8 @@ "domain": "loopenergy", "name": "Loop Energy", "documentation": "https://www.home-assistant.io/integrations/loopenergy", - "requirements": ["pyloopenergy==0.1.3"], - "codeowners": [] + "requirements": ["pyloopenergy==0.2.1"], + "codeowners": [ + "@pavoni" + ] } diff --git a/requirements_all.txt b/requirements_all.txt index 2fcd10be276..4c76d1c2d2c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1455,7 +1455,7 @@ pylibrespot-java==0.1.0 pylitejet==0.1 # homeassistant.components.loopenergy -pyloopenergy==0.1.3 +pyloopenergy==0.2.1 # homeassistant.components.lutron_caseta pylutron-caseta==0.6.1 From f81606cbf58ca5acba39b47a3fd861ceab626951 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Sep 2020 13:18:40 -0500 Subject: [PATCH 065/185] Return the listeners with the template result for the websocket api (#39925) --- .../components/websocket_api/commands.py | 8 +- homeassistant/helpers/event.py | 9 ++ .../components/websocket_api/test_commands.py | 25 ++++-- tests/helpers/test_event.py | 87 +++++++++++++++++-- 4 files changed, 118 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 04ad0ae3d3a..036cd690da2 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -253,9 +253,11 @@ def handle_render_template(hass, connection, msg): template.hass = hass variables = msg.get("variables") + info = None @callback def _template_listener(event, updates): + nonlocal info track_template_result = updates.pop() result = track_template_result.result if isinstance(result, TemplateError): @@ -267,7 +269,11 @@ def handle_render_template(hass, connection, msg): result = None - connection.send_message(messages.event_message(msg["id"], {"result": result})) + connection.send_message( + messages.event_message( + msg["id"], {"result": result, "listeners": info.listeners} # type: ignore + ) + ) info = async_track_template_result( hass, [TrackTemplate(template, variables)], _template_listener diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index d9f1b8d9681..52ebe2d4de4 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -527,6 +527,15 @@ class _TrackTemplateResultInfo: self._last_info = self._info.copy() self._create_listeners() + @property + def listeners(self) -> Dict: + """State changes that will cause a re-render.""" + return { + "all": self._all_listener is not None, + "entities": self._last_entities, + "domains": self._last_domains, + } + @property def _needs_all_listener(self) -> bool: for track_template_ in self._track_templates: diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 4113a833872..1b9eea86018 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -420,14 +420,20 @@ async def test_render_template_renders_template( assert msg["id"] == 5 assert msg["type"] == "event" event = msg["event"] - assert event == {"result": "State is: on"} + assert event == { + "result": "State is: on", + "listeners": {"all": False, "domains": [], "entities": ["light.test"]}, + } hass.states.async_set("light.test", "off") msg = await websocket_client.receive_json() assert msg["id"] == 5 assert msg["type"] == "event" event = msg["event"] - assert event == {"result": "State is: off"} + assert event == { + "result": "State is: off", + "listeners": {"all": False, "domains": [], "entities": ["light.test"]}, + } async def test_render_template_manual_entity_ids_no_longer_needed( @@ -453,14 +459,20 @@ async def test_render_template_manual_entity_ids_no_longer_needed( assert msg["id"] == 5 assert msg["type"] == "event" event = msg["event"] - assert event == {"result": "State is: on"} + assert event == { + "result": "State is: on", + "listeners": {"all": False, "domains": [], "entities": ["light.test"]}, + } hass.states.async_set("light.test", "off") msg = await websocket_client.receive_json() assert msg["id"] == 5 assert msg["type"] == "event" event = msg["event"] - assert event == {"result": "State is: off"} + assert event == { + "result": "State is: off", + "listeners": {"all": False, "domains": [], "entities": ["light.test"]}, + } async def test_render_template_with_error( @@ -480,7 +492,10 @@ async def test_render_template_with_error( assert msg["id"] == 5 assert msg["type"] == "event" event = msg["event"] - assert event == {"result": None} + assert event == { + "result": None, + "listeners": {"all": True, "domains": [], "entities": []}, + } assert "my_unknown_var" in caplog.text assert "TemplateError" in caplog.text diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index cc06c0fd19c..ba14c8a757f 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -682,20 +682,33 @@ async def test_track_template_result_complex(hass): hass.states.async_set("light.one", "on") hass.states.async_set("lock.one", "locked") - async_track_template_result( + info = async_track_template_result( hass, [TrackTemplate(template_complex, None)], specific_run_callback ) await hass.async_block_till_done() + assert info.listeners == {"all": True, "domains": set(), "entities": set()} + hass.states.async_set("sensor.domain", "light") await hass.async_block_till_done() assert len(specific_runs) == 1 assert specific_runs[0].strip() == "['light.one']" + assert info.listeners == { + "all": False, + "domains": {"light"}, + "entities": {"sensor.domain"}, + } + hass.states.async_set("sensor.domain", "lock") await hass.async_block_till_done() assert len(specific_runs) == 2 assert specific_runs[1].strip() == "['lock.one']" + assert info.listeners == { + "all": False, + "domains": {"lock"}, + "entities": {"sensor.domain"}, + } hass.states.async_set("sensor.domain", "all") await hass.async_block_till_done() @@ -703,11 +716,17 @@ async def test_track_template_result_complex(hass): assert "light.one" in specific_runs[2] assert "lock.one" in specific_runs[2] assert "sensor.domain" in specific_runs[2] + assert info.listeners == {"all": True, "domains": set(), "entities": set()} hass.states.async_set("sensor.domain", "light") await hass.async_block_till_done() assert len(specific_runs) == 4 assert specific_runs[3].strip() == "['light.one']" + assert info.listeners == { + "all": False, + "domains": {"light"}, + "entities": {"sensor.domain"}, + } hass.states.async_set("light.two", "on") await hass.async_block_till_done() @@ -715,6 +734,11 @@ async def test_track_template_result_complex(hass): assert "light.one" in specific_runs[4] assert "light.two" in specific_runs[4] assert "sensor.domain" not in specific_runs[4] + assert info.listeners == { + "all": False, + "domains": {"light"}, + "entities": {"sensor.domain"}, + } hass.states.async_set("light.three", "on") await hass.async_block_till_done() @@ -723,26 +747,51 @@ async def test_track_template_result_complex(hass): assert "light.two" in specific_runs[5] assert "light.three" in specific_runs[5] assert "sensor.domain" not in specific_runs[5] + assert info.listeners == { + "all": False, + "domains": {"light"}, + "entities": {"sensor.domain"}, + } hass.states.async_set("sensor.domain", "lock") await hass.async_block_till_done() assert len(specific_runs) == 7 assert specific_runs[6].strip() == "['lock.one']" + assert info.listeners == { + "all": False, + "domains": {"lock"}, + "entities": {"sensor.domain"}, + } hass.states.async_set("sensor.domain", "single_binary_sensor") await hass.async_block_till_done() assert len(specific_runs) == 8 assert specific_runs[7].strip() == "unknown" + assert info.listeners == { + "all": False, + "domains": set(), + "entities": {"binary_sensor.single", "sensor.domain"}, + } hass.states.async_set("binary_sensor.single", "binary_sensor_on") await hass.async_block_till_done() assert len(specific_runs) == 9 assert specific_runs[8].strip() == "binary_sensor_on" + assert info.listeners == { + "all": False, + "domains": set(), + "entities": {"binary_sensor.single", "sensor.domain"}, + } hass.states.async_set("sensor.domain", "lock") await hass.async_block_till_done() assert len(specific_runs) == 10 assert specific_runs[9].strip() == "['lock.one']" + assert info.listeners == { + "all": False, + "domains": {"lock"}, + "entities": {"sensor.domain"}, + } async def test_track_template_result_with_wildcard(hass): @@ -766,7 +815,7 @@ async def test_track_template_result_with_wildcard(hass): hass.states.async_set("cover.office_window", "closed") hass.states.async_set("cover.office_skylight", "open") - async_track_template_result( + info = async_track_template_result( hass, [TrackTemplate(template_complex, None)], specific_run_callback ) await hass.async_block_till_done() @@ -774,6 +823,7 @@ async def test_track_template_result_with_wildcard(hass): hass.states.async_set("cover.office_window", "open") await hass.async_block_till_done() assert len(specific_runs) == 1 + assert info.listeners == {"all": True, "domains": set(), "entities": set()} assert "cover.office_drapes=closed" in specific_runs[0] assert "cover.office_window=open" in specific_runs[0] @@ -808,11 +858,22 @@ async def test_track_template_result_with_group(hass): def specific_run_callback(event, updates): specific_runs.append(updates.pop().result) - async_track_template_result( + info = async_track_template_result( hass, [TrackTemplate(template_complex, None)], specific_run_callback ) await hass.async_block_till_done() + assert info.listeners == { + "all": False, + "domains": set(), + "entities": { + "group.power_sensors", + "sensor.power_1", + "sensor.power_2", + "sensor.power_3", + }, + } + hass.states.async_set("sensor.power_1", 100.1) await hass.async_block_till_done() assert len(specific_runs) == 1 @@ -851,10 +912,11 @@ async def test_track_template_result_and_conditional(hass): def specific_run_callback(event, updates): specific_runs.append(updates.pop().result) - async_track_template_result( + info = async_track_template_result( hass, [TrackTemplate(template, None)], specific_run_callback ) await hass.async_block_till_done() + assert info.listeners == {"all": False, "domains": set(), "entities": {"light.a"}} hass.states.async_set("light.b", "on") await hass.async_block_till_done() @@ -864,11 +926,21 @@ async def test_track_template_result_and_conditional(hass): await hass.async_block_till_done() assert len(specific_runs) == 1 assert specific_runs[0] == "on" + assert info.listeners == { + "all": False, + "domains": set(), + "entities": {"light.a", "light.b"}, + } hass.states.async_set("light.b", "off") await hass.async_block_till_done() assert len(specific_runs) == 2 assert specific_runs[1] == "off" + assert info.listeners == { + "all": False, + "domains": set(), + "entities": {"light.a", "light.b"}, + } hass.states.async_set("light.a", "off") await hass.async_block_till_done() @@ -924,7 +996,7 @@ async def test_track_template_result_iterator(hass): def filter_callback(event, updates): filter_runs.append(updates.pop().result) - async_track_template_result( + info = async_track_template_result( hass, [ TrackTemplate( @@ -939,6 +1011,11 @@ async def test_track_template_result_iterator(hass): filter_callback, ) await hass.async_block_till_done() + assert info.listeners == { + "all": False, + "domains": {"sensor"}, + "entities": {"sensor.test"}, + } hass.states.async_set("sensor.test", 6) await hass.async_block_till_done() From fcbcebea9ba7763edefca969e6124ad6ea3841ac Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 11 Sep 2020 16:50:17 +0200 Subject: [PATCH 066/185] Fix missing position attribute for MeteoFranceAlertSensor (#39938) --- homeassistant/components/meteo_france/sensor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 1e4c9b1215f..3c88914aafd 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -79,9 +79,10 @@ class MeteoFranceSensor(CoordinatorEntity): """Initialize the Meteo-France sensor.""" super().__init__(coordinator) self._type = sensor_type - city_name = self.coordinator.data.position["name"] - self._name = f"{city_name} {SENSOR_TYPES[self._type][ENTITY_NAME]}" - self._unique_id = f"{self.coordinator.data.position['lat']},{self.coordinator.data.position['lon']}_{self._type}" + if hasattr(self.coordinator.data, "position"): + city_name = self.coordinator.data.position["name"] + self._name = f"{city_name} {SENSOR_TYPES[self._type][ENTITY_NAME]}" + self._unique_id = f"{self.coordinator.data.position['lat']},{self.coordinator.data.position['lon']}_{self._type}" @property def unique_id(self): From 30f9e1b479f61b95cdee6764da86ae58172263f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 12 Sep 2020 07:20:21 -0500 Subject: [PATCH 067/185] Change template loop detection strategy to allow self-referencing updates when there are multiple templates (#39943) --- .../components/template/template_entity.py | 29 ++- homeassistant/helpers/event.py | 16 -- tests/components/template/test_sensor.py | 214 ++++++++++++++++-- 3 files changed, 220 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 2e42b28fbd2..632eeea8926 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -5,6 +5,7 @@ from typing import Any, Callable, List, Optional, Union import voluptuous as vol +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import EVENT_HOMEASSISTANT_START, CoreState, callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv @@ -121,7 +122,6 @@ class TemplateEntity(Entity): """Template Entity.""" self._template_attrs = {} self._async_update = None - self._async_update_entity_ids_filter = None self._attribute_templates = attribute_templates self._attributes = {} self._availability_template = availability_template @@ -130,6 +130,7 @@ class TemplateEntity(Entity): self._entity_picture_template = entity_picture_template self._icon = None self._entity_picture = None + self._self_ref_update_count = 0 @property def should_poll(self): @@ -223,18 +224,34 @@ class TemplateEntity(Entity): updates: List[TrackTemplateResult], ) -> None: """Call back the results to the attributes.""" + if event: self.async_set_context(event.context) + entity_id = event and event.data.get(ATTR_ENTITY_ID) + + if entity_id and entity_id == self.entity_id: + self._self_ref_update_count += 1 + else: + self._self_ref_update_count = 0 + + # If we need to make this less sensitive in the future, + # change the '>=' to a '>' here. + if self._self_ref_update_count >= len(self._template_attrs): + for update in updates: + _LOGGER.warning( + "Template loop detected while processing event: %s, skipping template render for Template[%s]", + event, + update.template.template, + ) + return + for update in updates: for attr in self._template_attrs[update.template]: attr.handle_result( event, update.template, update.last_result, update.result ) - if self._async_update_entity_ids_filter: - self._async_update_entity_ids_filter({self.entity_id}) - if self._async_update: self.async_write_ha_state() @@ -249,12 +266,8 @@ class TemplateEntity(Entity): ) self.async_on_remove(result_info.async_remove) result_info.async_refresh() - result_info.async_update_entity_ids_filter({self.entity_id}) self.async_write_ha_state() self._async_update = result_info.async_refresh - self._async_update_entity_ids_filter = ( - result_info.async_update_entity_ids_filter - ) async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 52ebe2d4de4..435a265d9e0 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -508,7 +508,6 @@ class _TrackTemplateResultInfo: self._info: Dict[Template, RenderInfo] = {} self._last_domains: Set = set() self._last_entities: Set = set() - self._entity_ids_filter: Set = set() def async_setup(self) -> None: """Activation of template tracking.""" @@ -669,27 +668,12 @@ class _TrackTemplateResultInfo: """Force recalculate the template.""" self._refresh(None) - @callback - def async_update_entity_ids_filter(self, entity_ids: Set) -> None: - """Update the filtered entity_ids.""" - self._entity_ids_filter = entity_ids - @callback def _refresh(self, event: Optional[Event]) -> None: entity_id = event and event.data.get(ATTR_ENTITY_ID) updates = [] info_changed = False - if entity_id and entity_id in self._entity_ids_filter: - # Skip self-referencing updates - for track_template_ in self._track_templates: - _LOGGER.warning( - "Template loop detected while processing event: %s, skipping template render for Template[%s]", - event, - track_template_.template.template, - ) - return - for track_template_ in self._track_templates: template = track_template_.template if ( diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 439f154b4af..6c9bfa7e632 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -4,6 +4,8 @@ from unittest.mock import patch from homeassistant.bootstrap import async_from_config_dict from homeassistant.const import ( + ATTR_ENTITY_PICTURE, + ATTR_ICON, EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START, STATE_OFF, @@ -763,12 +765,6 @@ async def test_sun_renders_once_per_sensor(hass): async def test_self_referencing_sensor_loop(hass, caplog): """Test a self referencing sensor does not loop forever.""" - template_str = """ -{% for state in states -%} - {{ state.last_updated }} -{%- endfor %} -""" - await async_setup_component( hass, "sensor", @@ -777,7 +773,7 @@ async def test_self_referencing_sensor_loop(hass, caplog): "platform": "template", "sensors": { "test": { - "value_template": template_str, + "value_template": "{{ ((states.sensor.test.state or 0) | int) + 1 }}", }, }, } @@ -790,15 +786,203 @@ async def test_self_referencing_sensor_loop(hass, caplog): assert len(hass.states.async_all()) == 1 - value = hass.states.get("sensor.test").state await hass.async_block_till_done() - - value2 = hass.states.get("sensor.test").state - assert value2 == value - await hass.async_block_till_done() - value3 = hass.states.get("sensor.test").state - assert value3 == value2 - assert "Template loop detected" in caplog.text + + state = hass.states.get("sensor.test") + assert int(state.state) == 1 + await hass.async_block_till_done() + assert int(state.state) == 1 + + +async def test_self_referencing_sensor_with_icon_loop(hass, caplog): + """Test a self referencing sensor loops forever with a valid self referencing icon.""" + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "test": { + "value_template": "{{ ((states.sensor.test.state or 0) | int) + 1 }}", + "icon_template": "{% if ((states.sensor.test.state or 0) | int) >= 1 %}mdi:greater{% else %}mdi:less{% endif %}", + }, + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert "Template loop detected" in caplog.text + + state = hass.states.get("sensor.test") + assert int(state.state) == 2 + assert state.attributes[ATTR_ICON] == "mdi:greater" + + await hass.async_block_till_done() + assert int(state.state) == 2 + + +async def test_self_referencing_sensor_with_icon_and_picture_entity_loop(hass, caplog): + """Test a self referencing sensor loop forevers with a valid self referencing icon.""" + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "test": { + "value_template": "{{ ((states.sensor.test.state or 0) | int) + 1 }}", + "icon_template": "{% if ((states.sensor.test.state or 0) | int) > 3 %}mdi:greater{% else %}mdi:less{% endif %}", + "entity_picture_template": "{% if ((states.sensor.test.state or 0) | int) >= 1 %}bigpic{% else %}smallpic{% endif %}", + }, + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert "Template loop detected" in caplog.text + + state = hass.states.get("sensor.test") + assert int(state.state) == 3 + assert state.attributes[ATTR_ICON] == "mdi:less" + assert state.attributes[ATTR_ENTITY_PICTURE] == "bigpic" + + await hass.async_block_till_done() + assert int(state.state) == 3 + + +async def test_self_referencing_entity_picture_loop(hass, caplog): + """Test a self referencing sensor does not loop forever with a looping self referencing entity picture.""" + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "test": { + "value_template": "{{ 1 }}", + "entity_picture_template": "{{ ((states.sensor.test.attributes['entity_picture'] or 0) | int) + 1 }}", + }, + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert "Template loop detected" in caplog.text + + state = hass.states.get("sensor.test") + assert int(state.state) == 1 + assert state.attributes[ATTR_ENTITY_PICTURE] == "1" + + await hass.async_block_till_done() + assert int(state.state) == 1 + + +async def test_self_referencing_icon_with_no_loop(hass, caplog): + """Test a self referencing icon that does not loop.""" + + hass.states.async_set("sensor.heartworm_high_80", 10) + hass.states.async_set("sensor.heartworm_low_57", 10) + hass.states.async_set("sensor.heartworm_avg_64", 10) + hass.states.async_set("sensor.heartworm_avg_57", 10) + + value_template_str = """{% if (states.sensor.heartworm_high_80.state|int >= 10) and (states.sensor.heartworm_low_57.state|int >= 10) %} + extreme + {% elif (states.sensor.heartworm_avg_64.state|int >= 30) %} + high + {% elif (states.sensor.heartworm_avg_64.state|int >= 14) %} + moderate + {% elif (states.sensor.heartworm_avg_64.state|int >= 5) %} + slight + {% elif (states.sensor.heartworm_avg_57.state|int >= 5) %} + marginal + {% elif (states.sensor.heartworm_avg_57.state|int < 5) %} + none + {% endif %}""" + + icon_template_str = """{% if is_state('sensor.heartworm_risk',"extreme") %} + mdi:hazard-lights + {% elif is_state('sensor.heartworm_risk',"high") %} + mdi:triangle-outline + {% elif is_state('sensor.heartworm_risk',"moderate") %} + mdi:alert-circle-outline + {% elif is_state('sensor.heartworm_risk',"slight") %} + mdi:exclamation + {% elif is_state('sensor.heartworm_risk',"marginal") %} + mdi:heart + {% elif is_state('sensor.heartworm_risk',"none") %} + mdi:snowflake + {% endif %}""" + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "heartworm_risk": { + "value_template": value_template_str, + "icon_template": icon_template_str, + }, + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 5 + + hass.states.async_set("sensor.heartworm_high_80", 10) + + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert "Template loop detected" not in caplog.text + + state = hass.states.get("sensor.heartworm_risk") + assert state.state == "extreme" + assert state.attributes[ATTR_ICON] == "mdi:hazard-lights" + + await hass.async_block_till_done() + assert state.state == "extreme" + assert state.attributes[ATTR_ICON] == "mdi:hazard-lights" + assert "Template loop detected" not in caplog.text From 5697f4b4e7f08271554cd34a50813adc27e18983 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sat, 12 Sep 2020 02:07:45 +0800 Subject: [PATCH 068/185] Set output timescale to input timescale (#39946) --- homeassistant/components/stream/worker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 0e861c5cefc..b76896b815a 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -25,7 +25,10 @@ def create_stream_buffer(stream_output, video_stream, audio_stream, sequence): segment, mode="w", format=stream_output.format, - container_options=container_options, + container_options={ + "video_track_timescale": str(int(1 / video_stream.time_base)), + **container_options, + }, ) vstream = output.add_stream(template=video_stream) # Check if audio is requested From b6f868f6295e705108d4a7d7dd6f80470a197f3a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 12 Sep 2020 10:35:51 +0200 Subject: [PATCH 069/185] Add children media class to children spotify media browser (#39953) --- .../components/spotify/media_player.py | 80 ++++++++++++++----- 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 7beea59a7bd..0782cb2f390 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -105,24 +105,57 @@ LIBRARY_MAP = { } CONTENT_TYPE_MEDIA_CLASS = { - "current_user_playlists": MEDIA_CLASS_DIRECTORY, - "current_user_followed_artists": MEDIA_CLASS_DIRECTORY, - "current_user_saved_albums": MEDIA_CLASS_DIRECTORY, - "current_user_saved_tracks": MEDIA_CLASS_DIRECTORY, - "current_user_saved_shows": MEDIA_CLASS_DIRECTORY, - "current_user_recently_played": MEDIA_CLASS_DIRECTORY, - "current_user_top_artists": MEDIA_CLASS_DIRECTORY, - "current_user_top_tracks": MEDIA_CLASS_DIRECTORY, - "featured_playlists": MEDIA_CLASS_DIRECTORY, - "categories": MEDIA_CLASS_DIRECTORY, - "category_playlists": MEDIA_CLASS_DIRECTORY, - "new_releases": MEDIA_CLASS_DIRECTORY, - MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, - MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, - MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, - MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE, - MEDIA_TYPE_SHOW: MEDIA_CLASS_PODCAST, - MEDIA_TYPE_TRACK: MEDIA_CLASS_TRACK, + "current_user_playlists": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_PLAYLIST, + }, + "current_user_followed_artists": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_ARTIST, + }, + "current_user_saved_albums": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_ALBUM, + }, + "current_user_saved_tracks": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_TRACK, + }, + "current_user_saved_shows": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_PODCAST, + }, + "current_user_recently_played": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_TRACK, + }, + "current_user_top_artists": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_ARTIST, + }, + "current_user_top_tracks": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_TRACK, + }, + "featured_playlists": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_PLAYLIST, + }, + "categories": {"parent": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_GENRE}, + "category_playlists": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_PLAYLIST, + }, + "new_releases": {"parent": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_ALBUM}, + MEDIA_TYPE_PLAYLIST: { + "parent": MEDIA_CLASS_PLAYLIST, + "children": MEDIA_CLASS_TRACK, + }, + MEDIA_TYPE_ALBUM: {"parent": MEDIA_CLASS_ALBUM, "children": MEDIA_CLASS_TRACK}, + MEDIA_TYPE_ARTIST: {"parent": MEDIA_CLASS_ARTIST, "children": MEDIA_CLASS_ALBUM}, + MEDIA_TYPE_EPISODE: {"parent": MEDIA_CLASS_EPISODE, "children": None}, + MEDIA_TYPE_SHOW: {"parent": MEDIA_CLASS_PODCAST, "children": MEDIA_CLASS_EPISODE}, + MEDIA_TYPE_TRACK: {"parent": MEDIA_CLASS_TRACK, "children": None}, } @@ -543,7 +576,8 @@ def build_item_response(spotify, user, payload): if media_content_type == "categories": media_item = BrowseMedia( title=LIBRARY_MAP.get(media_content_id), - media_class=media_class, + media_class=media_class["parent"], + children_media_class=media_class["children"], media_content_id=media_content_id, media_content_type=media_content_type, can_play=False, @@ -560,6 +594,7 @@ def build_item_response(spotify, user, payload): BrowseMedia( title=item.get("name"), media_class=MEDIA_CLASS_PLAYLIST, + children_media_class=MEDIA_CLASS_TRACK, media_content_id=item_id, media_content_type="category_playlists", thumbnail=fetch_image_url(item, key="icons"), @@ -567,7 +602,6 @@ def build_item_response(spotify, user, payload): can_expand=True, ) ) - media_item.children_media_class = MEDIA_CLASS_GENRE return media_item if title is None: @@ -578,7 +612,8 @@ def build_item_response(spotify, user, payload): params = { "title": title, - "media_class": media_class, + "media_class": media_class["parent"], + "children_media_class": media_class["children"], "media_content_id": media_content_id, "media_content_type": media_content_type, "can_play": media_content_type in PLAYABLE_MEDIA_TYPES, @@ -625,7 +660,8 @@ def item_payload(item): payload = { "title": item.get("name"), - "media_class": media_class, + "media_class": media_class["parent"], + "children_media_class": media_class["children"], "media_content_id": media_id, "media_content_type": media_type, "can_play": media_type in PLAYABLE_MEDIA_TYPES, From 172a02a605f090bc5b6498dfa9b801d12996cb69 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Sep 2020 12:28:57 +0000 Subject: [PATCH 070/185] Bumped version to 0.115.0b7 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2eb1d3c6d83..3d37720a718 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b6" +PATCH_VERSION = "0b7" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From d0e6b3e2683614d16a2f78761df64cec7d72f768 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sun, 13 Sep 2020 03:19:37 +0800 Subject: [PATCH 071/185] Remove skip_sidx container option in stream (#39970) * Remove skip_sidx container option * Add comment --- homeassistant/components/stream/hls.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 1e97ac222ec..816d1231c4c 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -148,7 +148,8 @@ class HlsStreamOutput(StreamOutput): def container_options(self) -> Callable[[int], dict]: """Return Callable which takes a sequence number and returns container options.""" return lambda sequence: { - "movflags": "frag_custom+empty_moov+default_base_moof+skip_sidx+frag_discont", + # Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970 + "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont", "avoid_negative_ts": "make_non_negative", "fragment_index": str(sequence), } From bc2173747c17ea43f741b41cc83a8579cc78dbd1 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 12 Sep 2020 08:54:00 -0500 Subject: [PATCH 072/185] Fix children_media_class for special folders (#39974) --- homeassistant/components/plex/media_browser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index 28444c4a351..3c91362deb9 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -205,6 +205,7 @@ def special_library_payload(parent_payload, special_type): media_content_type=parent_payload.media_content_type, can_play=False, can_expand=True, + children_media_class=parent_payload.children_media_class, ) From da6885af6c1e258036203f3dde6fcaa0441ad638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 12 Sep 2020 23:18:48 +0200 Subject: [PATCH 073/185] Bump frontend to 20200912.0 (#39997) --- 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 7ccb606894a..d5b48a9b185 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==20200909.0"], + "requirements": ["home-assistant-frontend==20200912.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index afbec0dd3d0..98c6a02efa2 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.37.0 -home-assistant-frontend==20200909.0 +home-assistant-frontend==20200912.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 4c76d1c2d2c..7a2f5147e14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200909.0 +home-assistant-frontend==20200912.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f07db06eff0..deae7ed6caa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200909.0 +home-assistant-frontend==20200912.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From b9b76b351983558a3c284b65369e5ed58a667375 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sat, 12 Sep 2020 14:53:41 -0700 Subject: [PATCH 074/185] Bump androidtv to 0.0.50 (#39998) --- homeassistant/components/androidtv/manifest.json | 2 +- homeassistant/components/androidtv/media_player.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 8e62813714e..f6c773941f1 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell[async]==0.2.1", - "androidtv[async]==0.0.49", + "androidtv[async]==0.0.50", "pure-python-adb[async]==0.3.0.dev0" ], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 959c85abd77..1ea20dbeca5 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -380,7 +380,7 @@ def adb_decorator(override_available=False): # An unforeseen exception occurred. Close the ADB connection so that # it doesn't happen over and over again, then raise the exception. await self.aftv.adb_close() - self._available = False # pylint: disable=protected-access + self._available = False raise return _adb_exception_catcher diff --git a/requirements_all.txt b/requirements_all.txt index 7a2f5147e14..a01bd361ef0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -248,7 +248,7 @@ ambiclimate==0.2.1 amcrest==1.7.0 # homeassistant.components.androidtv -androidtv[async]==0.0.49 +androidtv[async]==0.0.50 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index deae7ed6caa..bc875b264cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -149,7 +149,7 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv[async]==0.0.49 +androidtv[async]==0.0.50 # homeassistant.components.apns apns2==0.3.0 From 951c3731102374f243df0327cbce937bf8171a7b Mon Sep 17 00:00:00 2001 From: Quentame Date: Sun, 13 Sep 2020 01:13:57 +0200 Subject: [PATCH 075/185] Fix Freebox call sensor when no call in history (#40001) --- homeassistant/components/freebox/sensor.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index dc0d808c438..aeeaba438ff 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -146,11 +146,12 @@ class FreeboxCallSensor(FreeboxSensor): def async_update_state(self) -> None: """Update the Freebox call sensor.""" self._call_list_for_type = [] - for call in self._router.call_list: - if not call["new"]: - continue - if call["type"] == self._sensor_type: - self._call_list_for_type.append(call) + if self._router.call_list: + for call in self._router.call_list: + if not call["new"]: + continue + if call["type"] == self._sensor_type: + self._call_list_for_type.append(call) self._state = len(self._call_list_for_type) From 47326b2295a628a99c56f4ad7c1e10c72e223b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 13 Sep 2020 11:30:51 +0200 Subject: [PATCH 076/185] Bump pyhaversion to 3.4.0 (#40016) --- CODEOWNERS | 2 +- homeassistant/components/version/manifest.json | 4 ++-- homeassistant/components/version/sensor.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 42c39544540..75714f1aede 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -466,7 +466,7 @@ homeassistant/components/velbus/* @Cereal2nd @brefra homeassistant/components/velux/* @Julius2342 homeassistant/components/vera/* @vangorra homeassistant/components/versasense/* @flamm3blemuff1n -homeassistant/components/version/* @fabaff +homeassistant/components/version/* @fabaff @ludeeus homeassistant/components/vesync/* @markperdue @webdjoe @thegardenmonkey homeassistant/components/vicare/* @oischinger homeassistant/components/vilfo/* @ManneW diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index ed3158040d5..1f07c757ad8 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -2,7 +2,7 @@ "domain": "version", "name": "Version", "documentation": "https://www.home-assistant.io/integrations/version", - "requirements": ["pyhaversion==3.3.0"], - "codeowners": ["@fabaff"], + "requirements": ["pyhaversion==3.4.0"], + "codeowners": ["@fabaff", "@ludeeus"], "quality_scale": "internal" } diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index 636e564b816..dcfdc115376 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -35,6 +35,7 @@ ALL_IMAGES = [ "raspberrypi4-64", "tinker", "odroid-c2", + "odroid-n2", "odroid-xu", ] ALL_SOURCES = ["local", "pypi", "hassio", "docker", "haio"] diff --git a/requirements_all.txt b/requirements_all.txt index a01bd361ef0..0faab26721f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1377,7 +1377,7 @@ pygtfs==0.1.5 pygti==0.6.0 # homeassistant.components.version -pyhaversion==3.3.0 +pyhaversion==3.4.0 # homeassistant.components.heos pyheos==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc875b264cb..5519f7f3858 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -662,7 +662,7 @@ pygatt[GATTTOOL]==4.0.5 pygti==0.6.0 # homeassistant.components.version -pyhaversion==3.3.0 +pyhaversion==3.4.0 # homeassistant.components.heos pyheos==0.6.0 From b19fe17e76d5fcfe9a6861d21bddae690c333c6d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sun, 13 Sep 2020 11:41:46 +0200 Subject: [PATCH 077/185] Bumped version to 0.115.0b8 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3d37720a718..a1df155f1e7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b7" +PATCH_VERSION = "0b8" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 5a6492b76de269a94a7863c132ba39f3c12b282c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 13 Sep 2020 15:38:31 +0200 Subject: [PATCH 078/185] Update azure-pipelines-wheels.yml --- azure-pipelines-wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index ac5f4fd824f..bcf16e8dee7 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -47,7 +47,7 @@ jobs: - template: templates/azp-job-wheels.yaml@azure parameters: builderVersion: '$(versionWheels)' - builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev' + builderApk: 'build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev' builderPip: 'Cython;numpy;scikit-build' builderEnvFile: true skipBinary: 'aiohttp' @@ -94,7 +94,7 @@ jobs: # Write env for build settings ( - echo "GRPC_BUILD_WITH_BORING_SSL_ASM=0" + echo "GRPC_BUILD_WITH_BORING_SSL_ASM=" echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=1" ) > .env_file displayName: 'Prepare requirements files for Home Assistant wheels' From 30ef7a5e88bb7cb3caa2612ed160b0b13b19a25d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2020 09:12:10 -0500 Subject: [PATCH 079/185] Suppress homekit bridge discovery by homekit controller (#39990) --- .../homekit_controller/config_flow.py | 28 ++++++++++++- .../homekit_controller/strings.json | 1 + .../homekit_controller/test_config_flow.py | 39 ++++++++++++++++++- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 2c69512db9d..7e98fc40910 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -8,12 +8,19 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.core import callback +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + async_get_registry as async_get_device_registry, +) from .connection import get_accessory_name, get_bridge_information from .const import DOMAIN, KNOWN_DEVICES -HOMEKIT_IGNORE = ["Home Assistant Bridge"] HOMEKIT_DIR = ".homekit" +HOMEKIT_BRIDGE_DOMAIN = "homekit" +HOMEKIT_BRIDGE_SERIAL_NUMBER = "homekit.bridge" +HOMEKIT_BRIDGE_MODEL = "Home Assistant HomeKit Bridge" + PAIRING_FILE = "pairing.json" PIN_FORMAT = re.compile(r"^(\d{3})-{0,1}(\d{2})-{0,1}(\d{3})$") @@ -141,6 +148,17 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): return self.async_abort(reason="no_devices") + async def _hkid_is_homekit_bridge(self, hkid): + """Determine if the device is a homekit bridge.""" + dev_reg = await async_get_device_registry(self.hass) + device = dev_reg.async_get_device( + identifiers=set(), connections={(CONNECTION_NETWORK_MAC, hkid)} + ) + + if device is None: + return False + return device.model == HOMEKIT_BRIDGE_MODEL + async def async_step_zeroconf(self, discovery_info): """Handle a discovered HomeKit accessory. @@ -153,6 +171,12 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): key.lower(): value for (key, value) in discovery_info["properties"].items() } + if "id" not in properties: + _LOGGER.warning( + "HomeKit device %s: id not exposed, in violation of spec", properties + ) + return self.async_abort(reason="invalid_properties") + # The hkid is a unique random number that looks like a pairing code. # It changes if a device is factory reset. hkid = properties["id"] @@ -208,7 +232,7 @@ 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: + if await self._hkid_is_homekit_bridge(hkid): return self.async_abort(reason="ignored_model") self.model = model diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index bc07b71fa75..e685a46e144 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -44,6 +44,7 @@ "already_configured": "Accessory is already configured with this controller.", "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting configuration entry for it in Home Assistant that must first be removed.", "accessory_not_found_error": "Cannot add pairing as device can no longer be found.", + "invalid_properties": "Invalid properties announced by device.", "already_in_progress": "Config flow for device is already in progress." } } diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 09c823ff498..e5c8e381a5f 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -8,10 +8,11 @@ from aiohomekit.model.services import ServicesTypes import pytest from homeassistant.components.homekit_controller import config_flow +from homeassistant.helpers import device_registry import tests.async_mock from tests.async_mock import patch -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_device_registry PAIRING_START_FORM_ERRORS = [ (KeyError, "pairing_failed"), @@ -233,11 +234,45 @@ async def test_pair_already_paired_1(hass, controller): assert result["reason"] == "already_paired" +async def test_id_missing(hass, controller): + """Test id is missing.""" + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) + + # Remove id from device + del discovery_info["properties"]["id"] + + # 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"] == "invalid_properties" + + 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"]["md"] = config_flow.HOMEKIT_IGNORE[0] + + config_entry = MockConfigEntry(domain=config_flow.HOMEKIT_BRIDGE_DOMAIN, data={}) + formatted_mac = device_registry.format_mac("AA:BB:CC:DD:EE:FF") + + dev_reg = mock_device_registry(hass) + dev_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={ + ( + config_flow.HOMEKIT_BRIDGE_DOMAIN, + config_entry.entry_id, + config_flow.HOMEKIT_BRIDGE_SERIAL_NUMBER, + ) + }, + connections={(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)}, + model=config_flow.HOMEKIT_BRIDGE_MODEL, + ) + + discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF" # Device is discovered result = await hass.config_entries.flow.async_init( From b81c61dd99bfc7f0c28855f7190dac44b95ff5c5 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 13 Sep 2020 22:04:48 +0200 Subject: [PATCH 080/185] Fix requiring username or password for nzbget yaml config (#40003) --- homeassistant/components/nzbget/config_flow.py | 4 ++-- homeassistant/components/nzbget/coordinator.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nzbget/config_flow.py b/homeassistant/components/nzbget/config_flow.py index dfed7a9bfee..f593eeb0729 100644 --- a/homeassistant/components/nzbget/config_flow.py +++ b/homeassistant/components/nzbget/config_flow.py @@ -38,8 +38,8 @@ def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]: """ nzbget_api = NZBGetAPI( data[CONF_HOST], - data[CONF_USERNAME] if data[CONF_USERNAME] != "" else None, - data[CONF_PASSWORD] if data[CONF_PASSWORD] != "" else None, + data.get(CONF_USERNAME), + data.get(CONF_PASSWORD), data[CONF_SSL], data[CONF_VERIFY_SSL], data[CONF_PORT], diff --git a/homeassistant/components/nzbget/coordinator.py b/homeassistant/components/nzbget/coordinator.py index 8892475bc09..9a76d802bdd 100644 --- a/homeassistant/components/nzbget/coordinator.py +++ b/homeassistant/components/nzbget/coordinator.py @@ -29,8 +29,8 @@ class NZBGetDataUpdateCoordinator(DataUpdateCoordinator): """Initialize global NZBGet data updater.""" self.nzbget = NZBGetAPI( config[CONF_HOST], - config[CONF_USERNAME] if config[CONF_USERNAME] != "" else None, - config[CONF_PASSWORD] if config[CONF_PASSWORD] != "" else None, + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD), config[CONF_SSL], config[CONF_VERIFY_SSL], config[CONF_PORT], From f0ce65af7da57af38388685702c060b35149f319 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 8 Sep 2020 08:37:44 +0200 Subject: [PATCH 081/185] Add tests for Plugwise integration (#36371) --- .coveragerc | 5 - .../components/plugwise/config_flow.py | 1 - requirements_test.txt | 1 + tests/components/plugwise/common.py | 26 +++ tests/components/plugwise/conftest.py | 167 ++++++++++++++++++ .../components/plugwise/test_binary_sensor.py | 37 ++++ tests/components/plugwise/test_climate.py | 161 +++++++++++++++++ tests/components/plugwise/test_config_flow.py | 5 +- tests/components/plugwise/test_init.py | 45 +++++ tests/components/plugwise/test_sensor.py | 66 +++++++ tests/components/plugwise/test_switch.py | 50 ++++++ .../get_all_devices.json | 1 + .../02cf28bfec924855854c544690a609ef.json | 1 + .../21f2b542c49845e6bb416884c55778d6.json | 1 + .../4a810418d5394b3f82727340b91ba740.json | 1 + .../675416a629f343c495449970e2ca37b5.json | 1 + .../680423ff840043738f42cc7f1ff97a36.json | 1 + .../6a3bf693d05e48e0b460c815a4fdd09d.json | 1 + .../78d1126fc4c743db81b61c20e88342a7.json | 1 + .../90986d591dcd426cae3ec3e8111ff730.json | 1 + .../a28f588dc4a049a483fd03a30361ad3a.json | 1 + .../a2c3583e0a6349358998b760cea82d2a.json | 1 + .../b310b72a0e354bfab43089919b9a88bf.json | 1 + .../b59bcebaf94b499ea7d46e4a66fb62d8.json | 1 + .../cd0ddb54ef694e11ac18ed1cbce5dbbd.json | 1 + .../d3da73bde12a47d5a6b8f9dad971f2ec.json | 1 + .../df4a4a8169904cdb9c03d61a21f42140.json | 1 + .../e7693eb9582644e5b865dba8d4447cf1.json | 1 + .../f1fee6043d3642a9b0a65297455f008e.json | 1 + .../fe799307f1624099878210aa0b9f1475.json | 1 + .../anna_heatpump/get_all_devices.json | 1 + .../015ae9ea3f964e668e490fa39da3870b.json | 1 + .../1cbf783bb11e4a7c8a6843dee3a86927.json | 1 + .../3cb70739631c4d17a86b8b12e8a5161b.json | 1 + .../p1v3_full_option/get_all_devices.json | 1 + .../e950c7d5e1ee407a858e2a8b5016c8b3.json | 1 + 36 files changed, 580 insertions(+), 9 deletions(-) create mode 100644 tests/components/plugwise/common.py create mode 100644 tests/components/plugwise/conftest.py create mode 100644 tests/components/plugwise/test_binary_sensor.py create mode 100644 tests/components/plugwise/test_climate.py create mode 100644 tests/components/plugwise/test_init.py create mode 100644 tests/components/plugwise/test_sensor.py create mode 100644 tests/components/plugwise/test_switch.py create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/02cf28bfec924855854c544690a609ef.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/21f2b542c49845e6bb416884c55778d6.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/4a810418d5394b3f82727340b91ba740.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/675416a629f343c495449970e2ca37b5.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/78d1126fc4c743db81b61c20e88342a7.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a28f588dc4a049a483fd03a30361ad3a.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/cd0ddb54ef694e11ac18ed1cbce5dbbd.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/fe799307f1624099878210aa0b9f1475.json create mode 100644 tests/fixtures/plugwise/anna_heatpump/get_all_devices.json create mode 100644 tests/fixtures/plugwise/anna_heatpump/get_device_data/015ae9ea3f964e668e490fa39da3870b.json create mode 100644 tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json create mode 100644 tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json create mode 100644 tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json create mode 100644 tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json diff --git a/.coveragerc b/.coveragerc index a86b6312a77..162e0c65f06 100644 --- a/.coveragerc +++ b/.coveragerc @@ -656,11 +656,6 @@ omit = homeassistant/components/plaato/* homeassistant/components/plex/media_player.py homeassistant/components/plex/sensor.py - homeassistant/components/plugwise/__init__.py - homeassistant/components/plugwise/binary_sensor.py - homeassistant/components/plugwise/climate.py - homeassistant/components/plugwise/sensor.py - homeassistant/components/plugwise/switch.py homeassistant/components/plum_lightpad/light.py homeassistant/components/pocketcasts/sensor.py homeassistant/components/point/* diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 4d1752a2774..20c8a5a216c 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -106,7 +106,6 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" - if not errors: await self.async_set_unique_id(api.gateway_id) self._abort_if_unique_id_configured() diff --git a/requirements_test.txt b/requirements_test.txt index 86b8b496e83..e36837edf63 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,6 +7,7 @@ asynctest==0.13.0 codecov==2.1.0 coverage==5.2.1 +jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.780 pre-commit==2.7.1 diff --git a/tests/components/plugwise/common.py b/tests/components/plugwise/common.py new file mode 100644 index 00000000000..eb227322aa8 --- /dev/null +++ b/tests/components/plugwise/common.py @@ -0,0 +1,26 @@ +"""Common initialisation for the Plugwise integration.""" + +from homeassistant.components.plugwise import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def async_init_integration( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + skip_setup: bool = False, +): + """Initialize the Smile integration.""" + + entry = MockConfigEntry( + domain=DOMAIN, data={"host": "1.1.1.1", "password": "test-password"} + ) + entry.add_to_hass(hass) + + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py new file mode 100644 index 00000000000..8564b2c0d8c --- /dev/null +++ b/tests/components/plugwise/conftest.py @@ -0,0 +1,167 @@ +"""Setup mocks for the Plugwise integration tests.""" + +from functools import partial +import re + +from Plugwise_Smile.Smile import Smile +import jsonpickle +import pytest + +from tests.async_mock import AsyncMock, patch +from tests.common import load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +def _read_json(environment, call): + """Undecode the json data.""" + fixture = load_fixture(f"plugwise/{environment}/{call}.json") + return jsonpickle.decode(fixture) + + +@pytest.fixture(name="mock_smile") +def mock_smile(): + """Create a Mock Smile for testing exceptions.""" + with patch( + "homeassistant.components.plugwise.config_flow.Smile", + ) as smile_mock: + smile_mock.InvalidAuthentication = Smile.InvalidAuthentication + smile_mock.ConnectionFailedError = Smile.ConnectionFailedError + smile_mock.return_value.connect.return_value = True + yield smile_mock.return_value + + +@pytest.fixture(name="mock_smile_unauth") +def mock_smile_unauth(aioclient_mock: AiohttpClientMocker) -> None: + """Mock the Plugwise Smile unauthorized for Home Assistant.""" + aioclient_mock.get(re.compile(".*"), status=401) + aioclient_mock.put(re.compile(".*"), status=401) + + +@pytest.fixture(name="mock_smile_error") +def mock_smile_error(aioclient_mock: AiohttpClientMocker) -> None: + """Mock the Plugwise Smile server failure for Home Assistant.""" + aioclient_mock.get(re.compile(".*"), status=500) + aioclient_mock.put(re.compile(".*"), status=500) + + +@pytest.fixture(name="mock_smile_notconnect") +def mock_smile_notconnect(): + """Mock the Plugwise Smile general connection failure for Home Assistant.""" + with patch("homeassistant.components.plugwise.Smile") as smile_mock: + smile_mock.InvalidAuthentication = Smile.InvalidAuthentication + smile_mock.ConnectionFailedError = Smile.ConnectionFailedError + smile_mock.PlugwiseError = Smile.PlugwiseError + smile_mock.return_value.connect.side_effect = AsyncMock(return_value=False) + yield smile_mock.return_value + + +def _get_device_data(chosen_env, device_id): + """Mock return data for specific devices.""" + return _read_json(chosen_env, "get_device_data/" + device_id) + + +@pytest.fixture(name="mock_smile_adam") +def mock_smile_adam(): + """Create a Mock Adam environment for testing exceptions.""" + chosen_env = "adam_multiple_devices_per_zone" + with patch("homeassistant.components.plugwise.Smile") as smile_mock: + smile_mock.InvalidAuthentication = Smile.InvalidAuthentication + smile_mock.ConnectionFailedError = Smile.ConnectionFailedError + smile_mock.XMLDataMissingError = Smile.XMLDataMissingError + + smile_mock.return_value.gateway_id = "fe799307f1624099878210aa0b9f1475" + smile_mock.return_value.heater_id = "90986d591dcd426cae3ec3e8111ff730" + smile_mock.return_value.smile_version = "3.0.15" + smile_mock.return_value.smile_type = "thermostat" + + smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) + smile_mock.return_value.full_update_device.side_effect = AsyncMock( + return_value=True + ) + smile_mock.return_value.set_schedule_state.side_effect = AsyncMock( + return_value=True + ) + smile_mock.return_value.set_preset.side_effect = AsyncMock(return_value=True) + smile_mock.return_value.set_temperature.side_effect = AsyncMock( + return_value=True + ) + smile_mock.return_value.set_relay_state.side_effect = AsyncMock( + return_value=True + ) + + smile_mock.return_value.get_all_devices.return_value = _read_json( + chosen_env, "get_all_devices" + ) + smile_mock.return_value.get_device_data.side_effect = partial( + _get_device_data, chosen_env + ) + + yield smile_mock.return_value + + +@pytest.fixture(name="mock_smile_anna") +def mock_smile_anna(): + """Create a Mock Anna environment for testing exceptions.""" + chosen_env = "anna_heatpump" + with patch("homeassistant.components.plugwise.Smile") as smile_mock: + smile_mock.InvalidAuthentication = Smile.InvalidAuthentication + smile_mock.ConnectionFailedError = Smile.ConnectionFailedError + smile_mock.XMLDataMissingError = Smile.XMLDataMissingError + + smile_mock.return_value.gateway_id = "015ae9ea3f964e668e490fa39da3870b" + smile_mock.return_value.heater_id = "1cbf783bb11e4a7c8a6843dee3a86927" + smile_mock.return_value.smile_version = "4.0.15" + smile_mock.return_value.smile_type = "thermostat" + + smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) + smile_mock.return_value.full_update_device.side_effect = AsyncMock( + return_value=True + ) + smile_mock.return_value.set_schedule_state.side_effect = AsyncMock( + return_value=True + ) + smile_mock.return_value.set_preset.side_effect = AsyncMock(return_value=True) + smile_mock.return_value.set_temperature.side_effect = AsyncMock( + return_value=True + ) + smile_mock.return_value.set_relay_state.side_effect = AsyncMock( + return_value=True + ) + + smile_mock.return_value.get_all_devices.return_value = _read_json( + chosen_env, "get_all_devices" + ) + smile_mock.return_value.get_device_data.side_effect = partial( + _get_device_data, chosen_env + ) + + yield smile_mock.return_value + + +@pytest.fixture(name="mock_smile_p1") +def mock_smile_p1(): + """Create a Mock P1 DSMR environment for testing exceptions.""" + chosen_env = "p1v3_full_option" + with patch("homeassistant.components.plugwise.Smile") as smile_mock: + smile_mock.InvalidAuthentication = Smile.InvalidAuthentication + smile_mock.ConnectionFailedError = Smile.ConnectionFailedError + smile_mock.XMLDataMissingError = Smile.XMLDataMissingError + + smile_mock.return_value.gateway_id = "e950c7d5e1ee407a858e2a8b5016c8b3" + smile_mock.return_value.heater_id = None + smile_mock.return_value.smile_version = "3.3.9" + smile_mock.return_value.smile_type = "power" + + smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) + smile_mock.return_value.full_update_device.side_effect = AsyncMock( + return_value=True + ) + + smile_mock.return_value.get_all_devices.return_value = _read_json( + chosen_env, "get_all_devices" + ) + smile_mock.return_value.get_device_data.side_effect = partial( + _get_device_data, chosen_env + ) + + yield smile_mock.return_value diff --git a/tests/components/plugwise/test_binary_sensor.py b/tests/components/plugwise/test_binary_sensor.py new file mode 100644 index 00000000000..b2221194d8e --- /dev/null +++ b/tests/components/plugwise/test_binary_sensor.py @@ -0,0 +1,37 @@ +"""Tests for the Plugwise binary_sensor integration.""" + +from homeassistant.config_entries import ENTRY_STATE_LOADED +from homeassistant.const import STATE_OFF, STATE_ON + +from tests.components.plugwise.common import async_init_integration + + +async def test_anna_climate_binary_sensor_entities(hass, mock_smile_anna): + """Test creation of climate related binary_sensor entities.""" + entry = await async_init_integration(hass, mock_smile_anna) + assert entry.state == ENTRY_STATE_LOADED + + state = hass.states.get("binary_sensor.auxiliary_slave_boiler_state") + assert str(state.state) == STATE_OFF + + state = hass.states.get("binary_sensor.auxiliary_dhw_state") + assert str(state.state) == STATE_OFF + + +async def test_anna_climate_binary_sensor_change(hass, mock_smile_anna): + """Test change of climate related binary_sensor entities.""" + entry = await async_init_integration(hass, mock_smile_anna) + assert entry.state == ENTRY_STATE_LOADED + + hass.states.async_set("binary_sensor.auxiliary_dhw_state", STATE_ON, {}) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.auxiliary_dhw_state") + assert str(state.state) == STATE_ON + + await hass.helpers.entity_component.async_update_entity( + "binary_sensor.auxiliary_dhw_state" + ) + + state = hass.states.get("binary_sensor.auxiliary_dhw_state") + assert str(state.state) == STATE_OFF diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py new file mode 100644 index 00000000000..02375b49709 --- /dev/null +++ b/tests/components/plugwise/test_climate.py @@ -0,0 +1,161 @@ +"""Tests for the Plugwise Climate integration.""" + +from homeassistant.config_entries import ENTRY_STATE_LOADED + +from tests.components.plugwise.common import async_init_integration + + +async def test_adam_climate_entity_attributes(hass, mock_smile_adam): + """Test creation of adam climate device environment.""" + entry = await async_init_integration(hass, mock_smile_adam) + assert entry.state == ENTRY_STATE_LOADED + + state = hass.states.get("climate.zone_lisa_wk") + attrs = state.attributes + + assert attrs["hvac_modes"] is None + + assert "preset_modes" in attrs + assert "no_frost" in attrs["preset_modes"] + assert "home" in attrs["preset_modes"] + + assert attrs["current_temperature"] == 20.9 + assert attrs["temperature"] == 21.5 + + assert attrs["preset_mode"] == "home" + + assert attrs["supported_features"] == 17 + + state = hass.states.get("climate.zone_thermostat_jessie") + attrs = state.attributes + + assert attrs["hvac_modes"] is None + + assert "preset_modes" in attrs + assert "no_frost" in attrs["preset_modes"] + assert "home" in attrs["preset_modes"] + + assert attrs["current_temperature"] == 17.2 + assert attrs["temperature"] == 15.0 + + assert attrs["preset_mode"] == "asleep" + + +async def test_adam_climate_entity_climate_changes(hass, mock_smile_adam): + """Test handling of user requests in adam climate device environment.""" + entry = await async_init_integration(hass, mock_smile_adam) + assert entry.state == ENTRY_STATE_LOADED + + await hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": "climate.zone_lisa_wk", "temperature": 25}, + blocking=True, + ) + state = hass.states.get("climate.zone_lisa_wk") + attrs = state.attributes + + assert attrs["temperature"] == 25.0 + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": "climate.zone_lisa_wk", "preset_mode": "away"}, + blocking=True, + ) + state = hass.states.get("climate.zone_lisa_wk") + attrs = state.attributes + + assert attrs["preset_mode"] == "away" + + assert attrs["supported_features"] == 17 + + await hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": "climate.zone_thermostat_jessie", "temperature": 25}, + blocking=True, + ) + + state = hass.states.get("climate.zone_thermostat_jessie") + attrs = state.attributes + + assert attrs["temperature"] == 25.0 + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": "climate.zone_thermostat_jessie", "preset_mode": "home"}, + blocking=True, + ) + state = hass.states.get("climate.zone_thermostat_jessie") + attrs = state.attributes + + assert attrs["preset_mode"] == "home" + + +async def test_anna_climate_entity_attributes(hass, mock_smile_anna): + """Test creation of anna climate device environment.""" + entry = await async_init_integration(hass, mock_smile_anna) + assert entry.state == ENTRY_STATE_LOADED + + state = hass.states.get("climate.anna") + attrs = state.attributes + + assert "hvac_modes" in attrs + assert "heat_cool" in attrs["hvac_modes"] + + assert "preset_modes" in attrs + assert "no_frost" in attrs["preset_modes"] + assert "home" in attrs["preset_modes"] + + assert attrs["current_temperature"] == 23.3 + assert attrs["temperature"] == 21.0 + + assert state.state == "auto" + assert attrs["hvac_action"] == "idle" + assert attrs["preset_mode"] == "home" + + assert attrs["supported_features"] == 17 + + +async def test_anna_climate_entity_climate_changes(hass, mock_smile_anna): + """Test handling of user requests in anna climate device environment.""" + entry = await async_init_integration(hass, mock_smile_anna) + assert entry.state == ENTRY_STATE_LOADED + + await hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": "climate.anna", "temperature": 25}, + blocking=True, + ) + + state = hass.states.get("climate.anna") + attrs = state.attributes + + assert attrs["temperature"] == 25.0 + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": "climate.anna", "preset_mode": "away"}, + blocking=True, + ) + + state = hass.states.get("climate.anna") + attrs = state.attributes + + assert attrs["preset_mode"] == "away" + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": "climate.anna", "hvac_mode": "heat_cool"}, + blocking=True, + ) + + state = hass.states.get("climate.anna") + attrs = state.attributes + + assert state.state == "heat_cool" diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 11c0000977a..219ba8aee7f 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -62,14 +62,14 @@ async def test_form(hass): {"host": TEST_HOST, "password": TEST_PASSWORD}, ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["data"] == { "host": TEST_HOST, "password": TEST_PASSWORD, } - await hass.async_block_till_done() - assert result["errors"] == {} assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -108,7 +108,6 @@ async def test_zeroconf_form(hass): "password": TEST_PASSWORD, } - assert result["errors"] == {} assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py new file mode 100644 index 00000000000..713cd4930d7 --- /dev/null +++ b/tests/components/plugwise/test_init.py @@ -0,0 +1,45 @@ +"""Tests for the Plugwise Climate integration.""" + +import asyncio + +from Plugwise_Smile.Smile import Smile + +from homeassistant.config_entries import ( + ENTRY_STATE_SETUP_ERROR, + ENTRY_STATE_SETUP_RETRY, +) + +from tests.components.plugwise.common import async_init_integration + + +async def test_smile_unauthorized(hass, mock_smile_unauth): + """Test failing unauthorization by Smile.""" + entry = await async_init_integration(hass, mock_smile_unauth) + assert entry.state == ENTRY_STATE_SETUP_ERROR + + +async def test_smile_error(hass, mock_smile_error): + """Test server error handling by Smile.""" + entry = await async_init_integration(hass, mock_smile_error) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_smile_notconnect(hass, mock_smile_notconnect): + """Connection failure error handling by Smile.""" + mock_smile_notconnect.connect.return_value = False + entry = await async_init_integration(hass, mock_smile_notconnect) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_smile_timeout(hass, mock_smile_notconnect): + """Timeout error handling by Smile.""" + mock_smile_notconnect.connect.side_effect = asyncio.TimeoutError + entry = await async_init_integration(hass, mock_smile_notconnect) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_smile_adam_xmlerror(hass, mock_smile_adam): + """Detect malformed XML by Smile in Adam environment.""" + mock_smile_adam.full_update_device.side_effect = Smile.XMLDataMissingError + entry = await async_init_integration(hass, mock_smile_adam) + assert entry.state == ENTRY_STATE_SETUP_RETRY diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py new file mode 100644 index 00000000000..bc586120517 --- /dev/null +++ b/tests/components/plugwise/test_sensor.py @@ -0,0 +1,66 @@ +"""Tests for the Plugwise Sensor integration.""" + +from homeassistant.config_entries import ENTRY_STATE_LOADED + +from tests.components.plugwise.common import async_init_integration + + +async def test_adam_climate_sensor_entities(hass, mock_smile_adam): + """Test creation of climate related sensor entities.""" + entry = await async_init_integration(hass, mock_smile_adam) + assert entry.state == ENTRY_STATE_LOADED + + state = hass.states.get("sensor.adam_outdoor_temperature") + assert float(state.state) == 7.81 + + state = hass.states.get("sensor.cv_pomp_electricity_consumed") + assert float(state.state) == 35.6 + + state = hass.states.get("sensor.auxiliary_water_temperature") + assert float(state.state) == 70.0 + + state = hass.states.get("sensor.cv_pomp_electricity_consumed_interval") + assert float(state.state) == 7.37 + + await hass.helpers.entity_component.async_update_entity( + "sensor.zone_lisa_wk_battery" + ) + + state = hass.states.get("sensor.zone_lisa_wk_battery") + assert float(state.state) == 34 + + +async def test_anna_climate_sensor_entities(hass, mock_smile_anna): + """Test creation of climate related sensor entities.""" + entry = await async_init_integration(hass, mock_smile_anna) + assert entry.state == ENTRY_STATE_LOADED + + state = hass.states.get("sensor.auxiliary_outdoor_temperature") + assert float(state.state) == 18.0 + + state = hass.states.get("sensor.auxiliary_water_temperature") + assert float(state.state) == 29.1 + + state = hass.states.get("sensor.anna_illuminance") + assert float(state.state) == 86.0 + + +async def test_p1_dsmr_sensor_entities(hass, mock_smile_p1): + """Test creation of power related sensor entities.""" + entry = await async_init_integration(hass, mock_smile_p1) + assert entry.state == ENTRY_STATE_LOADED + + state = hass.states.get("sensor.p1_net_electricity_point") + assert float(state.state) == -2761.0 + + state = hass.states.get("sensor.p1_electricity_consumed_off_peak_cumulative") + assert int(state.state) == 551 + + state = hass.states.get("sensor.p1_electricity_produced_peak_point") + assert float(state.state) == 2761.0 + + state = hass.states.get("sensor.p1_electricity_consumed_peak_cumulative") + assert int(state.state) == 442 + + state = hass.states.get("sensor.p1_gas_consumed_cumulative") + assert float(state.state) == 584.9 diff --git a/tests/components/plugwise/test_switch.py b/tests/components/plugwise/test_switch.py new file mode 100644 index 00000000000..a58ebf83caa --- /dev/null +++ b/tests/components/plugwise/test_switch.py @@ -0,0 +1,50 @@ +"""Tests for the Plugwise switch integration.""" + +from homeassistant.config_entries import ENTRY_STATE_LOADED + +from tests.components.plugwise.common import async_init_integration + + +async def test_adam_climate_switch_entities(hass, mock_smile_adam): + """Test creation of climate related switch entities.""" + entry = await async_init_integration(hass, mock_smile_adam) + assert entry.state == ENTRY_STATE_LOADED + + state = hass.states.get("switch.cv_pomp") + assert str(state.state) == "on" + + state = hass.states.get("switch.fibaro_hc2") + assert str(state.state) == "on" + + +async def test_adam_climate_switch_changes(hass, mock_smile_adam): + """Test changing of climate related switch entities.""" + entry = await async_init_integration(hass, mock_smile_adam) + assert entry.state == ENTRY_STATE_LOADED + + await hass.services.async_call( + "switch", + "turn_off", + {"entity_id": "switch.cv_pomp"}, + blocking=True, + ) + state = hass.states.get("switch.cv_pomp") + assert str(state.state) == "off" + + await hass.services.async_call( + "switch", + "toggle", + {"entity_id": "switch.fibaro_hc2"}, + blocking=True, + ) + state = hass.states.get("switch.fibaro_hc2") + assert str(state.state) == "off" + + await hass.services.async_call( + "switch", + "toggle", + {"entity_id": "switch.fibaro_hc2"}, + blocking=True, + ) + state = hass.states.get("switch.fibaro_hc2") + assert str(state.state) == "on" diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json new file mode 100644 index 00000000000..61ebc4d9a6c --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json @@ -0,0 +1 @@ +{"df4a4a8169904cdb9c03d61a21f42140": {"name": "Zone Lisa Bios", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "12493538af164a409c6a1c79e38afe1c"}, "b310b72a0e354bfab43089919b9a88bf": {"name": "Floor kraan", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "c50f167537524366a5af7aa3942feb1e"}, "a2c3583e0a6349358998b760cea82d2a": {"name": "Bios Cv Thermostatic Radiator ", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "12493538af164a409c6a1c79e38afe1c"}, "b59bcebaf94b499ea7d46e4a66fb62d8": {"name": "Zone Lisa WK", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "c50f167537524366a5af7aa3942feb1e"}, "fe799307f1624099878210aa0b9f1475": {"name": "Adam", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "gateway", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "d3da73bde12a47d5a6b8f9dad971f2ec": {"name": "Thermostatic Radiator Jessie", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "82fa13f017d240daa0d0ea1775420f24"}, "21f2b542c49845e6bb416884c55778d6": {"name": "Playstation Smart Plug", "types": {"py/set": ["plug", "power"]}, "class": "game_console", "location": "cd143c07248f491493cea0533bc3d669"}, "78d1126fc4c743db81b61c20e88342a7": {"name": "CV Pomp", "types": {"py/set": ["plug", "power"]}, "class": "central_heating_pump", "location": "c50f167537524366a5af7aa3942feb1e"}, "90986d591dcd426cae3ec3e8111ff730": {"name": "Adam", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "heater_central", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "cd0ddb54ef694e11ac18ed1cbce5dbbd": {"name": "NAS", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "4a810418d5394b3f82727340b91ba740": {"name": "USG Smart Plug", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "02cf28bfec924855854c544690a609ef": {"name": "NVR", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "a28f588dc4a049a483fd03a30361ad3a": {"name": "Fibaro HC2", "types": {"py/set": ["plug", "power"]}, "class": "settop", "location": "cd143c07248f491493cea0533bc3d669"}, "6a3bf693d05e48e0b460c815a4fdd09d": {"name": "Zone Thermostat Jessie", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "82fa13f017d240daa0d0ea1775420f24"}, "680423ff840043738f42cc7f1ff97a36": {"name": "Thermostatic Radiator Badkamer", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "08963fec7c53423ca5680aa4cb502c63"}, "f1fee6043d3642a9b0a65297455f008e": {"name": "Zone Thermostat Badkamer", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "08963fec7c53423ca5680aa4cb502c63"}, "675416a629f343c495449970e2ca37b5": {"name": "Ziggo Modem", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "e7693eb9582644e5b865dba8d4447cf1": {"name": "CV Kraan Garage", "types": {"py/set": ["thermostat"]}, "class": "thermostatic_radiator_valve", "location": "446ac08dd04d4eff8ac57489757b7314"}} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/02cf28bfec924855854c544690a609ef.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/02cf28bfec924855854c544690a609ef.json new file mode 100644 index 00000000000..238da9d846a --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/02cf28bfec924855854c544690a609ef.json @@ -0,0 +1 @@ +{"electricity_consumed": 34.0, "electricity_consumed_interval": 9.15, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/21f2b542c49845e6bb416884c55778d6.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/21f2b542c49845e6bb416884c55778d6.json new file mode 100644 index 00000000000..4fcb40c4cf8 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/21f2b542c49845e6bb416884c55778d6.json @@ -0,0 +1 @@ +{"electricity_consumed": 82.6, "electricity_consumed_interval": 8.6, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/4a810418d5394b3f82727340b91ba740.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/4a810418d5394b3f82727340b91ba740.json new file mode 100644 index 00000000000..feb6290c9c4 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/4a810418d5394b3f82727340b91ba740.json @@ -0,0 +1 @@ +{"electricity_consumed": 8.5, "electricity_consumed_interval": 0.0, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/675416a629f343c495449970e2ca37b5.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/675416a629f343c495449970e2ca37b5.json new file mode 100644 index 00000000000..74d15fac374 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/675416a629f343c495449970e2ca37b5.json @@ -0,0 +1 @@ +{"electricity_consumed": 12.2, "electricity_consumed_interval": 2.97, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json new file mode 100644 index 00000000000..75bc62fbad4 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json @@ -0,0 +1 @@ +{"setpoint": 14.0, "temperature": 19.1, "battery": 0.51, "valve_position": 0.0, "temperature_difference": -0.4} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json new file mode 100644 index 00000000000..41333f374e1 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json @@ -0,0 +1 @@ +{"setpoint": 15.0, "temperature": 17.2, "battery": 0.37, "active_preset": "asleep", "presets": {"home": [20.0, 22.0], "no_frost": [10.0, 30.0], "away": [12.0, 25.0], "vacation": [11.0, 28.0], "asleep": [16.0, 24.0]}, "schedule_temperature": 15.0, "available_schedules": ["CV Jessie"], "selected_schedule": "CV Jessie", "last_used": "CV Jessie"} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/78d1126fc4c743db81b61c20e88342a7.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/78d1126fc4c743db81b61c20e88342a7.json new file mode 100644 index 00000000000..7a9c3e9be01 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/78d1126fc4c743db81b61c20e88342a7.json @@ -0,0 +1 @@ +{"electricity_consumed": 35.6, "electricity_consumed_interval": 7.37, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json new file mode 100644 index 00000000000..5e481d36b46 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json @@ -0,0 +1 @@ +{"water_temperature": 70.0, "intended_boiler_temperature": 70.0, "modulation_level": 0.01} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a28f588dc4a049a483fd03a30361ad3a.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a28f588dc4a049a483fd03a30361ad3a.json new file mode 100644 index 00000000000..0aeca4cc18e --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a28f588dc4a049a483fd03a30361ad3a.json @@ -0,0 +1 @@ +{"electricity_consumed": 12.5, "electricity_consumed_interval": 3.8, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json new file mode 100644 index 00000000000..eef83a67a20 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json @@ -0,0 +1 @@ +{"setpoint": 13.0, "temperature": 17.2, "battery": 0.62, "valve_position": 0.0, "temperature_difference": -0.2} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json new file mode 100644 index 00000000000..16da5f44ef5 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json @@ -0,0 +1 @@ +{"setpoint": 21.5, "temperature": 26.0, "valve_position": 1.0, "temperature_difference": 3.5} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json new file mode 100644 index 00000000000..65fa0dd3d52 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json @@ -0,0 +1 @@ +{"setpoint": 21.5, "temperature": 20.9, "battery": 0.34, "active_preset": "home", "presets": {"vacation": [15.0, 28.0], "asleep": [18.0, 24.0], "no_frost": [12.0, 30.0], "away": [17.0, 25.0], "home": [21.5, 22.0]}, "schedule_temperature": 21.5, "available_schedules": ["GF7 Woonkamer"], "selected_schedule": "GF7 Woonkamer", "last_used": "GF7 Woonkamer"} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/cd0ddb54ef694e11ac18ed1cbce5dbbd.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/cd0ddb54ef694e11ac18ed1cbce5dbbd.json new file mode 100644 index 00000000000..fbefc5bba25 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/cd0ddb54ef694e11ac18ed1cbce5dbbd.json @@ -0,0 +1 @@ +{"electricity_consumed": 16.5, "electricity_consumed_interval": 0.5, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json new file mode 100644 index 00000000000..fd202e05586 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json @@ -0,0 +1 @@ +{"setpoint": 15.0, "temperature": 17.1, "battery": 0.62, "valve_position": 0.0, "temperature_difference": 0.1} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json new file mode 100644 index 00000000000..12947c42ce0 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json @@ -0,0 +1 @@ +{"setpoint": 13.0, "temperature": 16.5, "battery": 0.67, "active_preset": "away", "presets": {"home": [20.0, 22.0], "away": [12.0, 25.0], "vacation": [12.0, 28.0], "no_frost": [8.0, 30.0], "asleep": [15.0, 24.0]}, "schedule_temperature": null, "available_schedules": [], "selected_schedule": null, "last_used": null} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json new file mode 100644 index 00000000000..151b4b41f70 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json @@ -0,0 +1 @@ +{"setpoint": 5.5, "temperature": 15.6, "battery": 0.68, "valve_position": 0.0, "temperature_difference": 0.0, "active_preset": "no_frost", "presets": {"home": [20.0, 22.0], "asleep": [17.0, 24.0], "away": [15.0, 25.0], "vacation": [15.0, 28.0], "no_frost": [10.0, 30.0]}, "schedule_temperature": null, "available_schedules": [], "selected_schedule": null, "last_used": null} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json new file mode 100644 index 00000000000..9934e109033 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json @@ -0,0 +1 @@ +{"setpoint": 14.0, "temperature": 18.9, "battery": 0.92, "active_preset": "away", "presets": {"asleep": [17.0, 24.0], "no_frost": [10.0, 30.0], "away": [14.0, 25.0], "home": [21.0, 22.0], "vacation": [12.0, 28.0]}, "schedule_temperature": 14.0, "available_schedules": ["Badkamer Schema"], "selected_schedule": "Badkamer Schema", "last_used": "Badkamer Schema"} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/fe799307f1624099878210aa0b9f1475.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/fe799307f1624099878210aa0b9f1475.json new file mode 100644 index 00000000000..ef325af7bc2 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/fe799307f1624099878210aa0b9f1475.json @@ -0,0 +1 @@ +{"outdoor_temperature": 7.81} \ No newline at end of file diff --git a/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json b/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json new file mode 100644 index 00000000000..4992a175b14 --- /dev/null +++ b/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json @@ -0,0 +1 @@ +{"1cbf783bb11e4a7c8a6843dee3a86927": {"name": "Anna", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "heater_central", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "015ae9ea3f964e668e490fa39da3870b": {"name": "Anna", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "gateway", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "3cb70739631c4d17a86b8b12e8a5161b": {"name": "Anna", "types": {"py/set": ["thermostat"]}, "class": "thermostat", "location": "c784ee9fdab44e1395b8dee7d7a497d5"}} \ No newline at end of file diff --git a/tests/fixtures/plugwise/anna_heatpump/get_device_data/015ae9ea3f964e668e490fa39da3870b.json b/tests/fixtures/plugwise/anna_heatpump/get_device_data/015ae9ea3f964e668e490fa39da3870b.json new file mode 100644 index 00000000000..750aa8b455c --- /dev/null +++ b/tests/fixtures/plugwise/anna_heatpump/get_device_data/015ae9ea3f964e668e490fa39da3870b.json @@ -0,0 +1 @@ +{"outdoor_temperature": 20.2} \ No newline at end of file diff --git a/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json b/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json new file mode 100644 index 00000000000..a8aea8e1357 --- /dev/null +++ b/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json @@ -0,0 +1 @@ +{"outdoor_temperature": 18.0, "heating_state": false, "dhw_state": false, "water_temperature": 29.1, "return_temperature": 25.1, "water_pressure": 1.57, "intended_boiler_temperature": 0.0, "modulation_level": 0.52, "cooling_state": false, "slave_boiler_state": false, "compressor_state": true, "flame_state": false} \ No newline at end of file diff --git a/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json b/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json new file mode 100644 index 00000000000..2a092e792d5 --- /dev/null +++ b/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json @@ -0,0 +1 @@ +{"setpoint": 21.0, "temperature": 23.3, "active_preset": "home", "presets": {"no_frost": [10.0, 30.0], "home": [21.0, 22.0], "away": [20.0, 25.0], "asleep": [20.5, 24.0], "vacation": [17.0, 28.0]}, "schedule_temperature": null, "available_schedules": ["standaard"], "selected_schedule": "standaard", "last_used": "standaard", "illuminance": 86.0} \ No newline at end of file diff --git a/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json b/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json new file mode 100644 index 00000000000..e25fcb953c8 --- /dev/null +++ b/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json @@ -0,0 +1 @@ +{"e950c7d5e1ee407a858e2a8b5016c8b3": {"name": "P1", "types": {"py/set": ["power", "home"]}, "class": "gateway", "location": "cd3e822288064775a7c4afcdd70bdda2"}} \ No newline at end of file diff --git a/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json b/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json new file mode 100644 index 00000000000..36cb66c7902 --- /dev/null +++ b/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json @@ -0,0 +1 @@ +{"net_electricity_point": -2761.0, "electricity_consumed_peak_point": 0.0, "electricity_consumed_off_peak_point": 0.0, "net_electricity_cumulative": 442972.0, "electricity_consumed_peak_cumulative": 442932.0, "electricity_consumed_off_peak_cumulative": 551090.0, "net_electricity_interval": 0.0, "electricity_consumed_peak_interval": 0.0, "electricity_consumed_off_peak_interval": 0.0, "electricity_produced_peak_point": 2761.0, "electricity_produced_off_peak_point": 0.0, "electricity_produced_peak_cumulative": 396559.0, "electricity_produced_off_peak_cumulative": 154491.0, "electricity_produced_peak_interval": 0.0, "electricity_produced_off_peak_interval": 0.0, "gas_consumed_cumulative": 584.9, "gas_consumed_interval": 0.0} \ No newline at end of file From 8b4e1936146b2455b8140f5ad28deef5f1831cfb Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sun, 13 Sep 2020 11:02:49 +0200 Subject: [PATCH 082/185] Ensure Plugwise unique_id is correctly set (#40014) * Ensure unique_id is correctly set * Removed unnec. line Co-authored-by: Tom Scholten --- homeassistant/components/plugwise/__init__.py | 4 +++ .../components/plugwise/config_flow.py | 8 +++++- tests/components/plugwise/conftest.py | 3 ++ tests/components/plugwise/test_config_flow.py | 28 +++++++++++++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py index 8c140f65af9..0e55c3e715c 100644 --- a/homeassistant/components/plugwise/__init__.py +++ b/homeassistant/components/plugwise/__init__.py @@ -94,6 +94,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api.get_all_devices() + if entry.unique_id is None: + if api.smile_version[0] != "1.8.0": + hass.config_entries.async_update_entry(entry, unique_id=api.smile_hostname) + undo_listener = entry.add_update_listener(_update_listener) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 20c8a5a216c..689bfb68f22 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -96,6 +96,10 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self.discovery_info: user_input[CONF_HOST] = self.discovery_info[CONF_HOST] + for entry in self._async_current_entries(): + if entry.data.get(CONF_HOST) == user_input[CONF_HOST]: + return self.async_abort(reason="already_configured") + try: api = await validate_input(self.hass, user_input) @@ -107,7 +111,9 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" if not errors: - await self.async_set_unique_id(api.gateway_id) + await self.async_set_unique_id( + api.smile_hostname or api.gateway_id, raise_on_progress=False + ) self._abort_if_unique_id_configured() return self.async_create_entry(title=api.smile_name, data=user_input) diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index 8564b2c0d8c..11e077c8a24 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -73,6 +73,7 @@ def mock_smile_adam(): smile_mock.return_value.heater_id = "90986d591dcd426cae3ec3e8111ff730" smile_mock.return_value.smile_version = "3.0.15" smile_mock.return_value.smile_type = "thermostat" + smile_mock.return_value.smile_hostname = "smile98765" smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) smile_mock.return_value.full_update_device.side_effect = AsyncMock( @@ -112,6 +113,7 @@ def mock_smile_anna(): smile_mock.return_value.heater_id = "1cbf783bb11e4a7c8a6843dee3a86927" smile_mock.return_value.smile_version = "4.0.15" smile_mock.return_value.smile_type = "thermostat" + smile_mock.return_value.smile_hostname = "smile98765" smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) smile_mock.return_value.full_update_device.side_effect = AsyncMock( @@ -151,6 +153,7 @@ def mock_smile_p1(): smile_mock.return_value.heater_id = None smile_mock.return_value.smile_version = "3.3.9" smile_mock.return_value.smile_type = "power" + smile_mock.return_value.smile_hostname = "smile98765" smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) smile_mock.return_value.full_update_device.side_effect = AsyncMock( diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 219ba8aee7f..e0f4993df55 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -111,6 +111,34 @@ async def test_zeroconf_form(hass): assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=TEST_DISCOVERY, + ) + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["errors"] == {} + + with patch( + "homeassistant.components.plugwise.config_flow.Smile.connect", + return_value=True, + ), patch( + "homeassistant.components.plugwise.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.plugwise.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {"password": TEST_PASSWORD}, + ) + + await hass.async_block_till_done() + + assert result4["type"] == "abort" + assert result4["reason"] == "already_configured" + async def test_form_invalid_auth(hass, mock_smile): """Test we handle invalid auth.""" From 4c2788a13c204b1401db8937e719a1c9373bcd1d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 13 Sep 2020 16:31:39 +0200 Subject: [PATCH 083/185] Improve handling of mireds being far out of spec (#40018) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_light.py | 26 ++++++++++++++++--- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 2cba87f74d6..6a47864375e 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==72"], + "requirements": ["pydeconz==73"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" diff --git a/requirements_all.txt b/requirements_all.txt index 0faab26721f..aa17999d7f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1295,7 +1295,7 @@ pydaikin==2.3.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==72 +pydeconz==73 # homeassistant.components.delijn pydelijn==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5519f7f3858..37fe829b0f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -625,7 +625,7 @@ pycountry==19.8.18 pydaikin==2.3.1 # homeassistant.components.deconz -pydeconz==72 +pydeconz==73 # homeassistant.components.dexcom pydexcom==0.2.0 diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index d070bd5b420..4e9f6b3d512 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -67,6 +67,15 @@ LIGHTS = { "type": "On and Off light", "uniqueid": "00:00:00:00:00:00:00:03-00", }, + "5": { + "ctmax": 1000, + "ctmin": 0, + "id": "Tunable white light with bad maxmin values id", + "name": "Tunable white light with bad maxmin values", + "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True}, + "type": "Tunable white light", + "uniqueid": "00:00:00:00:00:00:00:04-00", + }, } @@ -101,7 +110,7 @@ async def test_lights_and_groups(hass): assert "light.on_off_switch" not in gateway.deconz_ids assert "light.on_off_light" in gateway.deconz_ids - assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_all()) == 6 rgb_light = hass.states.get("light.rgb_light") assert rgb_light.state == "on" @@ -117,6 +126,15 @@ async def test_lights_and_groups(hass): assert tunable_white_light.attributes["min_mireds"] == 155 assert tunable_white_light.attributes["supported_features"] == 2 + tunable_white_light_bad_maxmin = hass.states.get( + "light.tunable_white_light_with_bad_maxmin_values" + ) + assert tunable_white_light_bad_maxmin.state == "on" + assert tunable_white_light_bad_maxmin.attributes["color_temp"] == 2500 + assert tunable_white_light_bad_maxmin.attributes["max_mireds"] == 650 + assert tunable_white_light_bad_maxmin.attributes["min_mireds"] == 140 + assert tunable_white_light_bad_maxmin.attributes["supported_features"] == 2 + on_off_light = hass.states.get("light.on_off_light") assert on_off_light.state == "on" assert on_off_light.attributes["supported_features"] == 0 @@ -256,7 +274,7 @@ async def test_disable_light_groups(hass): assert "light.empty_group" not in gateway.deconz_ids assert "light.on_off_switch" not in gateway.deconz_ids # 3 entities - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 rgb_light = hass.states.get("light.rgb_light") assert rgb_light is not None @@ -281,7 +299,7 @@ async def test_disable_light_groups(hass): assert "light.empty_group" not in gateway.deconz_ids assert "light.on_off_switch" not in gateway.deconz_ids # 3 entities - assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_all()) == 6 hass.config_entries.async_update_entry( gateway.config_entry, options={deconz.gateway.CONF_ALLOW_DECONZ_GROUPS: False} @@ -294,4 +312,4 @@ async def test_disable_light_groups(hass): assert "light.empty_group" not in gateway.deconz_ids assert "light.on_off_switch" not in gateway.deconz_ids # 3 entities - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 From 4ee7cdc8a0baebce17bf319129023a5588a2088b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2020 09:44:37 -0500 Subject: [PATCH 084/185] Do not log an error when a host is unreachable while pinging (#40024) --- homeassistant/components/ping/binary_sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index ac73da0a13f..79e0268c77b 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -210,7 +210,8 @@ class PingDataSubProcess(PingData): out_error, ) - if pinger.returncode != 0: + if pinger.returncode > 1: + # returncode of 1 means the host is unreachable _LOGGER.exception( "Error running command: `%s`, return code: %s", " ".join(self._ping_cmd), From a971b92899d01c47d9b7cd10d2c1c02d9a00a8de Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 13 Sep 2020 18:23:50 +0200 Subject: [PATCH 085/185] Fix slack notifications requiring an icon (#40027) --- homeassistant/components/slack/notify.py | 29 ++++++++++++------------ 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index d43a1fc25ad..8477b2fb501 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -202,22 +202,23 @@ class SlackNotificationService(BaseNotificationService): self, targets, message, title, blocks, username, icon ): """Send a text-only message.""" - if self._icon.lower().startswith(("http://", "https://")): - icon_type = "url" - else: - icon_type = "emoji" + message_dict = { + "blocks": blocks, + "link_names": True, + "text": message, + "username": username, + } + + if self._icon: + if self._icon.lower().startswith(("http://", "https://")): + icon_type = "url" + else: + icon_type = "emoji" + + message_dict[f"icon_{icon_type}"] = icon tasks = { - target: self._client.chat_postMessage( - **{ - "blocks": blocks, - "channel": target, - "link_names": True, - "text": message, - "username": username, - f"icon_{icon_type}": icon, - } - ) + target: self._client.chat_postMessage(**message_dict, channel=target) for target in targets } From 30b856554879e0b23a915c39bc4c0c5e93ecda9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2020 14:55:49 -0500 Subject: [PATCH 086/185] Ensure homekit_controller traps exceptions from find_ip_by_device_id (#40030) --- homeassistant/components/homekit_controller/config_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 7e98fc40910..9ca247382c7 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -304,9 +304,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): # Its possible that the first try may have been busy so # we always check to see if self.finish_paring has been # set. - discovery = await self.controller.find_ip_by_device_id(self.hkid) - try: + discovery = await self.controller.find_ip_by_device_id(self.hkid) self.finish_pairing = await discovery.start_pairing(self.hkid) except aiohomekit.BusyError: From afde5a7ece73bccd71307ee78b7e4c6c41206a51 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Sep 2020 22:05:45 +0200 Subject: [PATCH 087/185] Fix entity extraction from Template conditions (#40034) --- homeassistant/helpers/condition.py | 10 ++++++++-- tests/helpers/test_condition.py | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 1b09348415c..f67b9a4b0ab 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -649,13 +649,16 @@ async def async_validate_condition_config( @callback -def async_extract_entities(config: ConfigType) -> Set[str]: +def async_extract_entities(config: Union[ConfigType, Template]) -> Set[str]: """Extract entities from a condition.""" referenced: Set[str] = set() to_process = deque([config]) while to_process: config = to_process.popleft() + if isinstance(config, Template): + continue + condition = config[CONF_CONDITION] if condition in ("and", "not", "or"): @@ -674,13 +677,16 @@ def async_extract_entities(config: ConfigType) -> Set[str]: @callback -def async_extract_devices(config: ConfigType) -> Set[str]: +def async_extract_devices(config: Union[ConfigType, Template]) -> Set[str]: """Extract devices from a condition.""" referenced = set() to_process = deque([config]) while to_process: config = to_process.popleft() + if isinstance(config, Template): + continue + condition = config[CONF_CONDITION] if condition in ("and", "not", "or"): diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index af01163bfd5..71770d21186 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -5,6 +5,7 @@ import pytest from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition +from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -807,6 +808,7 @@ async def test_extract_entities(): "entity_id": ["sensor.temperature_9", "sensor.temperature_10"], "below": 110, }, + Template("{{ is_state('light.example', 'on') }}"), ], } ) == { @@ -867,6 +869,7 @@ async def test_extract_devices(): }, ], }, + Template("{{ is_state('light.example', 'on') }}"), ], } ) From ef1649383ca37182bedfc2746b231505cda50f65 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 13 Sep 2020 20:22:47 +0000 Subject: [PATCH 088/185] Bumped version to 0.115.0b9 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a1df155f1e7..9680e20161b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b8" +PATCH_VERSION = "0b9" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From db27079fa8ee2e2db7faa5ca21067041745b88fc Mon Sep 17 00:00:00 2001 From: r4nd0mbr1ck <23737685+r4nd0mbr1ck@users.noreply.github.com> Date: Tue, 15 Sep 2020 15:50:44 +1000 Subject: [PATCH 089/185] Speedtestdotnet - use server name to generate server list (#39775) --- homeassistant/components/speedtestdotnet/__init__.py | 7 +++++-- tests/components/speedtestdotnet/__init__.py | 4 ++-- tests/components/speedtestdotnet/test_config_flow.py | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 57557d4558a..32562251ed4 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -143,9 +143,12 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator): self.servers[DEFAULT_SERVER] = {} for server in sorted( - server_list.values(), key=lambda server: server[0]["country"] + server_list.values(), + key=lambda server: server[0]["country"] + server[0]["sponsor"], ): - self.servers[f"{server[0]['country']} - {server[0]['sponsor']}"] = server[0] + self.servers[ + f"{server[0]['country']} - {server[0]['sponsor']} - {server[0]['name']}" + ] = server[0] def update_data(self): """Get the latest data from speedtest.net.""" diff --git a/tests/components/speedtestdotnet/__init__.py b/tests/components/speedtestdotnet/__init__.py index f67a633e25f..f6f64b9c7bb 100644 --- a/tests/components/speedtestdotnet/__init__.py +++ b/tests/components/speedtestdotnet/__init__.py @@ -9,7 +9,7 @@ MOCK_SERVERS = { "name": "Server1", "country": "Country1", "cc": "LL1", - "sponsor": "Server1", + "sponsor": "Sponsor1", "id": "1", "host": "server1:8080", "d": 1, @@ -23,7 +23,7 @@ MOCK_SERVERS = { "name": "Server2", "country": "Country2", "cc": "LL2", - "sponsor": "server2", + "sponsor": "Sponsor2", "id": "2", "host": "server2:8080", "d": 2, diff --git a/tests/components/speedtestdotnet/test_config_flow.py b/tests/components/speedtestdotnet/test_config_flow.py index cfd79fb38f8..8e7edc2d986 100644 --- a/tests/components/speedtestdotnet/test_config_flow.py +++ b/tests/components/speedtestdotnet/test_config_flow.py @@ -108,7 +108,7 @@ async def test_options(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - CONF_SERVER_NAME: "Country1 - Server1", + CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1", CONF_SCAN_INTERVAL: 30, CONF_MANUAL: False, }, @@ -116,7 +116,7 @@ async def test_options(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == { - CONF_SERVER_NAME: "Country1 - Server1", + CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1", CONF_SERVER_ID: "1", CONF_SCAN_INTERVAL: 30, CONF_MANUAL: False, From 1a76a953c739818c89cb84d43f64bc3dd973e0f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Sep 2020 01:48:30 -0500 Subject: [PATCH 090/185] Update gogogate2-api to 2.0.2 (#40010) * Update gogogate2-api to 2.0.2 Resolves a timeout issue: https://github.com/vangorra/python_gogogate2_api/pull/11 * mock voltage --- homeassistant/components/gogogate2/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/gogogate2/test_cover.py | 12 ++++++++++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index edf69144f62..a12058d38d0 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -3,7 +3,7 @@ "name": "Gogogate2 and iSmartGate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gogogate2", - "requirements": ["gogogate2-api==2.0.1"], + "requirements": ["gogogate2-api==2.0.2"], "codeowners": ["@vangorra"], "homekit": { "models": [ diff --git a/requirements_all.txt b/requirements_all.txt index aa17999d7f1..930c388fea3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -669,7 +669,7 @@ glances_api==0.2.0 gntp==1.0.3 # homeassistant.components.gogogate2 -gogogate2-api==2.0.1 +gogogate2-api==2.0.2 # homeassistant.components.google google-api-python-client==1.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37fe829b0f1..d076089a6d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -334,7 +334,7 @@ gios==0.1.4 glances_api==0.2.0 # homeassistant.components.gogogate2 -gogogate2-api==2.0.1 +gogogate2-api==2.0.2 # homeassistant.components.google google-api-python-client==1.6.4 diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index eb2907e2b6e..b1ab2284580 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -110,6 +110,7 @@ async def test_import( camera=False, events=2, temperature=None, + voltage=40, ), door2=GogoGate2Door( door_id=2, @@ -123,6 +124,7 @@ async def test_import( camera=False, events=0, temperature=None, + voltage=40, ), door3=GogoGate2Door( door_id=3, @@ -136,6 +138,7 @@ async def test_import( camera=False, events=0, temperature=None, + voltage=40, ), outputs=Outputs(output1=True, output2=False, output3=True), network=Network(ip=""), @@ -170,6 +173,7 @@ async def test_import( enabled=True, apicode="apicode0", customimage=False, + voltage=40, ), door2=ISmartGateDoor( door_id=1, @@ -186,6 +190,7 @@ async def test_import( enabled=True, apicode="apicode0", customimage=False, + voltage=40, ), door3=ISmartGateDoor( door_id=1, @@ -202,6 +207,7 @@ async def test_import( enabled=True, apicode="apicode0", customimage=False, + voltage=40, ), network=Network(ip=""), wifi=Wifi(SSID="", linkquality="", signal=""), @@ -268,6 +274,7 @@ async def test_open_close_update(gogogat2api_mock, hass: HomeAssistant) -> None: camera=False, events=2, temperature=None, + voltage=40, ), door2=GogoGate2Door( door_id=2, @@ -281,6 +288,7 @@ async def test_open_close_update(gogogat2api_mock, hass: HomeAssistant) -> None: camera=False, events=0, temperature=None, + voltage=40, ), door3=GogoGate2Door( door_id=3, @@ -294,6 +302,7 @@ async def test_open_close_update(gogogat2api_mock, hass: HomeAssistant) -> None: camera=False, events=0, temperature=None, + voltage=40, ), outputs=Outputs(output1=True, output2=False, output3=True), network=Network(ip=""), @@ -381,6 +390,7 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: enabled=True, apicode="apicode0", customimage=False, + voltage=40, ), door2=ISmartGateDoor( door_id=2, @@ -397,6 +407,7 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: enabled=True, apicode="apicode0", customimage=False, + voltage=40, ), door3=ISmartGateDoor( door_id=3, @@ -413,6 +424,7 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: enabled=True, apicode="apicode0", customimage=False, + voltage=40, ), network=Network(ip=""), wifi=Wifi(SSID="", linkquality="", signal=""), From 7a7cad39eb59314e3018535e7697234661559785 Mon Sep 17 00:00:00 2001 From: b3nj1 Date: Mon, 14 Sep 2020 12:29:51 -0700 Subject: [PATCH 091/185] Fix ecobee weather forcast off by 1 bug (#40048) --- homeassistant/components/ecobee/weather.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 4ea90d27106..71c1d263e02 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -1,5 +1,5 @@ """Support for displaying weather info from Ecobee API.""" -from datetime import datetime +from datetime import timedelta from pyecobee.const import ECOBEE_STATE_UNKNOWN @@ -13,6 +13,7 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.util import dt as dt_util from .const import ( _LOGGER, @@ -165,10 +166,13 @@ class EcobeeWeather(WeatherEntity): return None forecasts = [] - for day in range(1, 5): + date = dt_util.utcnow() + for day in range(0, 5): forecast = _process_forecast(self.weather["forecasts"][day]) if forecast is None: continue + forecast[ATTR_FORECAST_TIME] = date.isoformat() + date += timedelta(days=1) forecasts.append(forecast) if forecasts: @@ -186,9 +190,6 @@ def _process_forecast(json): """Process a single ecobee API forecast to return expected values.""" forecast = {} try: - forecast[ATTR_FORECAST_TIME] = datetime.strptime( - json["dateTime"], "%Y-%m-%d %H:%M:%S" - ).isoformat() forecast[ATTR_FORECAST_CONDITION] = ECOBEE_WEATHER_SYMBOL_TO_HASS[ json["weatherSymbol"] ] From a9d24c2cd5c0d16c4089ed9a71352b15c2ad3906 Mon Sep 17 00:00:00 2001 From: Markus Bong <2Fake1987@gmail.com> Date: Mon, 14 Sep 2020 12:24:49 +0200 Subject: [PATCH 092/185] Correct devolo climate devices (#40061) --- homeassistant/components/devolo_home_control/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index d44a0c981f1..05f4363c384 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -27,7 +27,7 @@ async def async_setup_entry( for device in hass.data[DOMAIN]["homecontrol"].multi_level_switch_devices: for multi_level_switch in device.multi_level_switch_property: - if device.deviceModelUID in [ + if device.device_model_uid in [ "devolo.model.Thermostat:Valve", "devolo.model.Room:Thermostat", ]: From 0e823b566bcdee4b2987de656da3f1c9c084f8bb Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 14 Sep 2020 14:36:08 +0200 Subject: [PATCH 093/185] Fix default forecast mode OpenWeatherMap (#40062) --- homeassistant/components/openweathermap/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/openweathermap/config_flow.py b/homeassistant/components/openweathermap/config_flow.py index 365f55c5d44..5f960e52ecf 100644 --- a/homeassistant/components/openweathermap/config_flow.py +++ b/homeassistant/components/openweathermap/config_flow.py @@ -91,7 +91,7 @@ class OpenWeatherMapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if CONF_LONGITUDE not in config: config[CONF_LONGITUDE] = self.hass.config.longitude if CONF_MODE not in config: - config[CONF_MODE] = DEFAULT_LANGUAGE + config[CONF_MODE] = DEFAULT_FORECAST_MODE if CONF_LANGUAGE not in config: config[CONF_LANGUAGE] = DEFAULT_LANGUAGE return await self.async_step_user(config) From e0fcf9b6484ae52dcd0d83c432c70b6c260380d1 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 14 Sep 2020 13:18:43 +0100 Subject: [PATCH 094/185] Bump aiohomekit version (regression fix) (#40064) --- .../components/homekit_controller/manifest.json | 16 ++++++++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 1ee2f16ffcf..1fb4c05c595 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,8 +3,16 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit[IP]==0.2.49"], - "zeroconf": ["_hap._tcp.local."], - "after_dependencies": ["zeroconf"], - "codeowners": ["@Jc2k"] + "requirements": [ + "aiohomekit==0.2.53" + ], + "zeroconf": [ + "_hap._tcp.local." + ], + "after_dependencies": [ + "zeroconf" + ], + "codeowners": [ + "@Jc2k" + ] } diff --git a/requirements_all.txt b/requirements_all.txt index 930c388fea3..2ab44ff224b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -178,7 +178,7 @@ aioguardian==1.0.1 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.49 +aiohomekit==0.2.53 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d076089a6d8..1f6237fd130 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -103,7 +103,7 @@ aioguardian==1.0.1 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.49 +aiohomekit==0.2.53 # homeassistant.components.emulated_hue # homeassistant.components.http From a38e047e83765363f56de168f66d63cc19298e58 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 14 Sep 2020 14:50:39 +0200 Subject: [PATCH 095/185] Update docker base image to 8.4.0 (#40066) --- build.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.json b/build.json index 6d8763a019f..d63a245793b 100644 --- a/build.json +++ b/build.json @@ -1,11 +1,11 @@ { "image": "homeassistant/{arch}-homeassistant", "build_from": { - "aarch64": "homeassistant/aarch64-homeassistant-base:8.3.0", - "armhf": "homeassistant/armhf-homeassistant-base:8.3.0", - "armv7": "homeassistant/armv7-homeassistant-base:8.3.0", - "amd64": "homeassistant/amd64-homeassistant-base:8.3.0", - "i386": "homeassistant/i386-homeassistant-base:8.3.0" + "aarch64": "homeassistant/aarch64-homeassistant-base:8.4.0", + "armhf": "homeassistant/armhf-homeassistant-base:8.4.0", + "armv7": "homeassistant/armv7-homeassistant-base:8.4.0", + "amd64": "homeassistant/amd64-homeassistant-base:8.4.0", + "i386": "homeassistant/i386-homeassistant-base:8.4.0" }, "labels": { "io.hass.type": "core" From 3ef821d62f664f12d981f398bf64a5c3e387d17f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Sep 2020 15:40:32 +0200 Subject: [PATCH 096/185] Fix tag last scanned serialization (#40067) --- homeassistant/components/tag/__init__.py | 4 ++-- tests/components/tag/test_init.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 872d097d5de..0b445fbf575 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -70,7 +70,7 @@ class TagStorageCollection(collection.StorageCollection): data[TAG_ID] = str(uuid.uuid4()) # make last_scanned JSON serializeable if LAST_SCANNED in data: - data[LAST_SCANNED] = str(data[LAST_SCANNED]) + data[LAST_SCANNED] = data[LAST_SCANNED].isoformat() return data @callback @@ -83,7 +83,7 @@ class TagStorageCollection(collection.StorageCollection): data = {**data, **self.UPDATE_SCHEMA(update_data)} # make last_scanned JSON serializeable if LAST_SCANNED in data: - data[LAST_SCANNED] = str(data[LAST_SCANNED]) + data[LAST_SCANNED] = data[LAST_SCANNED].isoformat() return data diff --git a/tests/components/tag/test_init.py b/tests/components/tag/test_init.py index d10a59ef2f0..e4b810e0661 100644 --- a/tests/components/tag/test_init.py +++ b/tests/components/tag/test_init.py @@ -6,6 +6,9 @@ import pytest from homeassistant.components.tag import DOMAIN, TAGS, async_scan_tag from homeassistant.helpers import collection from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.async_mock import patch _LOGGER = logging.getLogger(__name__) @@ -60,7 +63,10 @@ async def test_tag_scanned(hass, hass_ws_client, storage_setup): assert len(result) == 1 assert "test tag" in result - await async_scan_tag(hass, "new tag", "some_scanner") + now = dt_util.utcnow() + with patch("homeassistant.util.dt.utcnow", return_value=now): + await async_scan_tag(hass, "new tag", "some_scanner") + await client.send_json({"id": 7, "type": f"{DOMAIN}/list"}) resp = await client.receive_json() assert resp["success"] @@ -70,7 +76,7 @@ async def test_tag_scanned(hass, hass_ws_client, storage_setup): assert len(result) == 2 assert "test tag" in result assert "new tag" in result - assert result["new tag"]["last_scanned"] is not None + assert result["new tag"]["last_scanned"] == now.isoformat() def track_changes(coll: collection.ObservableCollection): From 99a86046015e8021d225dc796c326ad668072106 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 15 Sep 2020 00:32:20 +0200 Subject: [PATCH 097/185] Fix netatmo media browser of outdoor events (#40079) * Fix outdoor events * Fix test data * Increase coverage --- homeassistant/components/netatmo/camera.py | 6 +-- .../components/netatmo/media_source.py | 16 ++++++- tests/components/netatmo/test_media_source.py | 42 ++++++++++++++++++- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 3f9720f3adb..dff6013c7c6 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -284,9 +284,9 @@ class NetatmoCamera(NetatmoBase, Camera): self._data.events.get(self._id, {}) ) elif self._model == "NOC": # Smart Outdoor Camera - self.hass.data[DOMAIN][DATA_EVENTS][ - self._id - ] = self._data.outdoor_events.get(self._id, {}) + self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events( + self._data.outdoor_events.get(self._id, {}) + ) def process_events(self, events): """Add meta data to events.""" diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index 76527677224..6375c46d394 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -80,8 +80,20 @@ class NetatmoSource(MediaSource): ) -> BrowseMediaSource: if event_id and event_id in self.events[camera_id]: created = dt.datetime.fromtimestamp(event_id) - thumbnail = self.events[camera_id][event_id].get("snapshot", {}).get("url") - message = remove_html_tags(self.events[camera_id][event_id]["message"]) + if self.events[camera_id][event_id]["type"] == "outdoor": + thumbnail = ( + self.events[camera_id][event_id]["event_list"][0] + .get("snapshot", {}) + .get("url") + ) + message = remove_html_tags( + self.events[camera_id][event_id]["event_list"][0]["message"] + ) + else: + thumbnail = ( + self.events[camera_id][event_id].get("snapshot", {}).get("url") + ) + message = remove_html_tags(self.events[camera_id][event_id]["message"]) title = f"{created} - {message}" else: title = self.hass.data[DOMAIN][DATA_CAMERAS].get(camera_id, MANUFACTURER) diff --git a/tests/components/netatmo/test_media_source.py b/tests/components/netatmo/test_media_source.py index 0405317f03e..1773c0d83da 100644 --- a/tests/components/netatmo/test_media_source.py +++ b/tests/components/netatmo/test_media_source.py @@ -18,6 +18,7 @@ async def test_async_browse_media(hass): "12:34:56:78:90:ab": { 1599152672: { "id": "12345", + "type": "person", "time": 1599152672, "camera_id": "12:34:56:78:90:ab", "snapshot": { @@ -30,6 +31,7 @@ async def test_async_browse_media(hass): }, 1599152673: { "id": "12346", + "type": "person", "time": 1599152673, "camera_id": "12:34:56:78:90:ab", "snapshot": { @@ -37,9 +39,47 @@ async def test_async_browse_media(hass): }, "message": "Tobias seen", }, + 1599152674: { + "id": "12347", + "type": "outdoor", + "time": 1599152674, + "camera_id": "12:34:56:78:90:ac", + "snapshot": { + "url": "https://netatmocameraimage", + }, + "video_id": "98766", + "video_status": "available", + "event_list": [ + { + "type": "vehicle", + "time": 1599152674, + "id": "12347-0", + "offset": 0, + "message": "Vehicle detected", + "snapshot": { + "url": "https://netatmocameraimage", + }, + }, + { + "type": "human", + "time": 1599152674, + "id": "12347-1", + "offset": 8, + "message": "Person detected", + "snapshot": { + "url": "https://netatmocameraimage", + }, + }, + ], + "media_url": "http:///files/high/index.m3u8", + }, } } - hass.data[DOMAIN][DATA_CAMERAS] = {"12:34:56:78:90:ab": "MyCamera"} + + hass.data[DOMAIN][DATA_CAMERAS] = { + "12:34:56:78:90:ab": "MyCamera", + "12:34:56:78:90:ac": "MyOutdoorCamera", + } assert await async_setup_component(hass, const.DOMAIN, {}) await hass.async_block_till_done() From e5c499c22e16b391730ca1b06e027d43c311ee09 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Sep 2020 22:10:30 +0200 Subject: [PATCH 098/185] Increase TIMEOUT_ACK to 2s (#40080) --- homeassistant/components/mqtt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 2b5dca6474f..470e43b5b44 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -134,7 +134,7 @@ CONNECTION_FAILED = "connection_failed" CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable" DISCOVERY_COOLDOWN = 2 -TIMEOUT_ACK = 1 +TIMEOUT_ACK = 2 PLATFORMS = [ "alarm_control_panel", From 24fe9cdd5ac2d2ece7e87456faae42967d34ba14 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 14 Sep 2020 16:48:39 -0400 Subject: [PATCH 099/185] Update ZHA dependency (#40083) --- 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 4bde073a933..c6b0fa78799 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.20.1", + "bellows==0.20.2", "pyserial==3.4", "zha-quirks==0.0.44", "zigpy-cc==0.5.2", diff --git a/requirements_all.txt b/requirements_all.txt index 2ab44ff224b..5bde6df6972 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -339,7 +339,7 @@ beautifulsoup4==4.9.1 # beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows==0.20.1 +bellows==0.20.2 # homeassistant.components.bmw_connected_drive bimmer_connected==0.7.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f6237fd130..7a9fb0efb70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -183,7 +183,7 @@ azure-eventhub==5.1.0 base36==0.1.1 # homeassistant.components.zha -bellows==0.20.1 +bellows==0.20.2 # homeassistant.components.blebox blebox_uniapi==1.3.2 From 81436fb68811256edbaad6c23f7652b1bf1fdcd9 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 15 Sep 2020 09:30:00 +0200 Subject: [PATCH 100/185] Check Sonos for local library before browsing (#40085) --- homeassistant/components/sonos/media_player.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 2b50f2864dc..307fee923a3 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -1547,6 +1547,13 @@ def library_payload(media_library): Used by async_browse_media. """ + if not media_library.browse_by_idstring( + "tracks", + "", + max_items=1, + ): + raise BrowseError("Local library not found") + children = [] for item in media_library.browse(): try: From 610a327b529215e9fe492308676e5cae4a098a79 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Sep 2020 02:27:30 -0500 Subject: [PATCH 101/185] Convert color temperature to hue and saturation for HomeKit (#40089) The HomeKit spec does not permit the color temp characteristic being exposed when color (hue, sat) is present. Since Home Assistant will still send color temp values, we need to convert them to hue, sat values for HomeKit --- homeassistant/components/homekit/type_lights.py | 17 ++++++++++++++--- tests/components/homekit/test_type_lights.py | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 612d8e53a02..086934ea6f7 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -24,6 +24,10 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import callback +from homeassistant.util.color import ( + color_temperature_mired_to_kelvin, + color_temperature_to_hs, +) from .accessories import TYPES, HomeAccessory from .const import ( @@ -64,8 +68,6 @@ class Light(HomeAccessory): if self._features & SUPPORT_COLOR: self.chars.append(CHAR_HUE) self.chars.append(CHAR_SATURATION) - self._hue = None - self._saturation = None elif self._features & SUPPORT_COLOR_TEMP: # ColorTemperature and Hue characteristic should not be # exposed both. Both states are tracked separately in HomeKit, @@ -179,7 +181,16 @@ class Light(HomeAccessory): # Handle Color if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: - hue, saturation = new_state.attributes.get(ATTR_HS_COLOR, (None, None)) + if ATTR_HS_COLOR in new_state.attributes: + hue, saturation = new_state.attributes[ATTR_HS_COLOR] + elif ATTR_COLOR_TEMP in new_state.attributes: + hue, saturation = color_temperature_to_hs( + color_temperature_mired_to_kelvin( + new_state.attributes[ATTR_COLOR_TEMP] + ) + ) + else: + hue, saturation = None, None if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)): hue = round(hue, 0) saturation = round(saturation, 0) diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 20029861adb..e82bc5bb15d 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -293,9 +293,25 @@ async def test_light_color_temperature_and_rgb_color(hass, hk_driver, cls, event ) await hass.async_block_till_done() acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None) + assert acc.char_hue.value == 260 + assert acc.char_saturation.value == 90 assert not hasattr(acc, "char_color_temperature") + hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 224}) + await hass.async_block_till_done() + await acc.run_handler() + await hass.async_block_till_done() + assert acc.char_hue.value == 27 + assert acc.char_saturation.value == 27 + + hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 352}) + await hass.async_block_till_done() + await acc.run_handler() + await hass.async_block_till_done() + assert acc.char_hue.value == 28 + assert acc.char_saturation.value == 61 + async def test_light_rgb_color(hass, hk_driver, cls, events): """Test light with rgb_color.""" From 3ef3d848f73d2b7e1dd484e4bdb10c512f41af3a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 15 Sep 2020 15:57:10 +0200 Subject: [PATCH 102/185] Update frontend to 20200915.0 (#40101) --- 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 d5b48a9b185..9b499f0376a 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==20200912.0"], + "requirements": ["home-assistant-frontend==20200915.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 98c6a02efa2..ccede9062a0 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.37.0 -home-assistant-frontend==20200912.0 +home-assistant-frontend==20200915.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 5bde6df6972..97e840bea16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200912.0 +home-assistant-frontend==20200915.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7a9fb0efb70..e32ce166be0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200912.0 +home-assistant-frontend==20200915.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From e095120023de7d32120c5ba9b5ec80c3ad89358a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 15 Sep 2020 16:21:32 +0200 Subject: [PATCH 103/185] Bumped version to 0.115.0b10 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9680e20161b..291bb110a99 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b9" +PATCH_VERSION = "0b10" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From ce86112612db26f179bdf9aad175169a5a93a5a6 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 15 Sep 2020 19:01:36 +0100 Subject: [PATCH 104/185] Address error in SQL query (#39939) --- homeassistant/components/sql/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index f19941ed043..27656c260d3 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -122,6 +122,7 @@ class SQLSensor(Entity): def update(self): """Retrieve sensor data from the query.""" + data = None try: sess = self.sessionmaker() result = sess.execute(self._query) @@ -147,7 +148,7 @@ class SQLSensor(Entity): finally: sess.close() - if self._template is not None: + if data is not None and self._template is not None: self._state = self._template.async_render_with_possible_json_value( data, None ) From 5d518b5365cbacb70367f113f42c43ffa2cb153a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Sep 2020 15:28:25 +0200 Subject: [PATCH 105/185] Add media dirs core configuration (#40071) Co-authored-by: Paulus Schoutsen --- .../components/media_source/const.py | 4 +- .../components/media_source/local_source.py | 98 +++++++++++++------ homeassistant/config.py | 13 ++- homeassistant/const.py | 1 + homeassistant/core.py | 3 + tests/common.py | 1 + tests/components/media_source/test_init.py | 19 +++- .../media_source/test_local_source.py | 39 +++++++- tests/test_config.py | 19 ++++ 9 files changed, 157 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/media_source/const.py b/homeassistant/components/media_source/const.py index 68a8244c3ce..739af47e653 100644 --- a/homeassistant/components/media_source/const.py +++ b/homeassistant/components/media_source/const.py @@ -15,4 +15,6 @@ MEDIA_CLASS_MAP = { "image": MEDIA_CLASS_IMAGE, } URI_SCHEME = "media-source://" -URI_SCHEME_REGEX = re.compile(r"^media-source://(?P[^/]+)?(?P.+)?") +URI_SCHEME_REGEX = re.compile( + r"^media-source:\/\/(?:(?P(?!.+__)(?!_)[\da-z_]+(?(?!\/).+))?)?$" +) diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index a558de775f8..e14735fb60b 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -21,26 +21,7 @@ def async_setup(hass: HomeAssistant): """Set up local media source.""" source = LocalSource(hass) hass.data[DOMAIN][DOMAIN] = source - hass.http.register_view(LocalMediaView(hass)) - - -@callback -def async_parse_identifier(item: MediaSourceItem) -> Tuple[str, str]: - """Parse identifier.""" - if not item.identifier: - source_dir_id = "media" - location = "" - - else: - source_dir_id, location = item.identifier.lstrip("/").split("/", 1) - - if source_dir_id != "media": - raise Unresolvable("Unknown source directory.") - - if location != sanitize_path(location): - raise Unresolvable("Invalid path.") - - return source_dir_id, location + hass.http.register_view(LocalMediaView(hass, source)) class LocalSource(MediaSource): @@ -56,22 +37,41 @@ class LocalSource(MediaSource): @callback def async_full_path(self, source_dir_id, location) -> Path: """Return full path.""" - return self.hass.config.path("media", location) + return Path(self.hass.config.media_dirs[source_dir_id], location) + + @callback + def async_parse_identifier(self, item: MediaSourceItem) -> Tuple[str, str]: + """Parse identifier.""" + if not item.identifier: + # Empty source_dir_id and location + return "", "" + + source_dir_id, location = item.identifier.split("/", 1) + if source_dir_id not in self.hass.config.media_dirs: + raise Unresolvable("Unknown source directory.") + + if location != sanitize_path(location): + raise Unresolvable("Invalid path.") + + return source_dir_id, location async def async_resolve_media(self, item: MediaSourceItem) -> str: """Resolve media to a url.""" - source_dir_id, location = async_parse_identifier(item) + source_dir_id, location = self.async_parse_identifier(item) + if source_dir_id == "" or source_dir_id not in self.hass.config.media_dirs: + raise Unresolvable("Unknown source directory.") + mime_type, _ = mimetypes.guess_type( - self.async_full_path(source_dir_id, location) + str(self.async_full_path(source_dir_id, location)) ) - return PlayMedia(item.identifier, mime_type) + return PlayMedia(f"/local_source/{item.identifier}", mime_type) async def async_browse_media( self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES ) -> BrowseMediaSource: """Return media.""" try: - source_dir_id, location = async_parse_identifier(item) + source_dir_id, location = self.async_parse_identifier(item) except Unresolvable as err: raise BrowseError(str(err)) from err @@ -79,9 +79,37 @@ class LocalSource(MediaSource): self._browse_media, source_dir_id, location ) - def _browse_media(self, source_dir_id, location): + def _browse_media(self, source_dir_id: str, location: Path): """Browse media.""" - full_path = Path(self.hass.config.path("media", location)) + + # If only one media dir is configured, use that as the local media root + if source_dir_id == "" and len(self.hass.config.media_dirs) == 1: + source_dir_id = list(self.hass.config.media_dirs)[0] + + # Multiple folder, root is requested + if source_dir_id == "": + if location: + raise BrowseError("Folder not found.") + + base = BrowseMediaSource( + domain=DOMAIN, + identifier="", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=None, + title=self.name, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_DIRECTORY, + ) + + base.children = [ + self._browse_media(source_dir_id, "") + for source_dir_id in self.hass.config.media_dirs + ] + + return base + + full_path = Path(self.hass.config.media_dirs[source_dir_id], location) if not full_path.exists(): if location == "": @@ -118,7 +146,7 @@ class LocalSource(MediaSource): media = BrowseMediaSource( domain=DOMAIN, - identifier=f"{source_dir_id}/{path.relative_to(self.hass.config.path('media'))}", + identifier=f"{source_dir_id}/{path.relative_to(self.hass.config.media_dirs[source_dir_id])}", media_class=media_class, media_content_type=mime_type or "", title=title, @@ -149,19 +177,25 @@ class LocalMediaView(HomeAssistantView): Returns media files in config/media. """ - url = "/media/{location:.*}" + url = "/local_source/{source_dir_id}/{location:.*}" name = "media" - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant, source: LocalSource): """Initialize the media view.""" self.hass = hass + self.source = source - async def get(self, request: web.Request, location: str) -> web.FileResponse: + async def get( + self, request: web.Request, source_dir_id: str, location: str + ) -> web.FileResponse: """Start a GET request.""" if location != sanitize_path(location): return web.HTTPNotFound() - media_path = Path(self.hass.config.path("media", location)) + if source_dir_id not in self.hass.config.media_dirs: + return web.HTTPNotFound() + + media_path = self.source.async_full_path(source_dir_id, location) # Check that the file exists if not media_path.is_file(): diff --git a/homeassistant/config.py b/homeassistant/config.py index 36a81f98fa3..3d6e3fb041c 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -33,6 +33,7 @@ from homeassistant.const import ( CONF_INTERNAL_URL, CONF_LATITUDE, CONF_LONGITUDE, + CONF_MEDIA_DIRS, CONF_NAME, CONF_PACKAGES, CONF_TEMPERATURE_UNIT, @@ -221,6 +222,8 @@ CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend( ], _no_duplicate_auth_mfa_module, ), + # pylint: disable=no-value-for-parameter + vol.Optional(CONF_MEDIA_DIRS): cv.schema_with_slug_keys(vol.IsDir()), } ) @@ -485,6 +488,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non CONF_UNIT_SYSTEM, CONF_EXTERNAL_URL, CONF_INTERNAL_URL, + CONF_MEDIA_DIRS, ] ): hac.config_source = SOURCE_YAML @@ -496,6 +500,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non (CONF_ELEVATION, "elevation"), (CONF_INTERNAL_URL, "internal_url"), (CONF_EXTERNAL_URL, "external_url"), + (CONF_MEDIA_DIRS, "media_dirs"), ): if key in config: setattr(hac, attr, config[key]) @@ -503,8 +508,14 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non if CONF_TIME_ZONE in config: hac.set_time_zone(config[CONF_TIME_ZONE]) + if CONF_MEDIA_DIRS not in config: + if is_docker_env(): + hac.media_dirs = {"media": "/media"} + else: + hac.media_dirs = {"media": hass.config.path("media")} + # Init whitelist external dir - hac.allowlist_external_dirs = {hass.config.path("www"), hass.config.path("media")} + hac.allowlist_external_dirs = {hass.config.path("www"), *hac.media_dirs.values()} if CONF_ALLOWLIST_EXTERNAL_DIRS in config: hac.allowlist_external_dirs.update(set(config[CONF_ALLOWLIST_EXTERNAL_DIRS])) diff --git a/homeassistant/const.py b/homeassistant/const.py index 291bb110a99..ec1f366fabe 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -116,6 +116,7 @@ CONF_LIGHTS = "lights" CONF_LONGITUDE = "longitude" CONF_MAC = "mac" CONF_MAXIMUM = "maximum" +CONF_MEDIA_DIRS = "media_dirs" CONF_METHOD = "method" CONF_MINIMUM = "minimum" CONF_MODE = "mode" diff --git a/homeassistant/core.py b/homeassistant/core.py index 8f3809bbd4c..f230fef01eb 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1390,6 +1390,9 @@ class Config: # List of allowed external URLs that integrations may use self.allowlist_external_urls: Set[str] = set() + # Dictionary of Media folders that integrations may use + self.media_dirs: Dict[str, str] = {} + # If Home Assistant is running in safe mode self.safe_mode: bool = False diff --git a/tests/common.py b/tests/common.py index 1cba478767f..b36439d4110 100644 --- a/tests/common.py +++ b/tests/common.py @@ -205,6 +205,7 @@ async def async_test_home_assistant(loop): hass.config.elevation = 0 hass.config.time_zone = date_util.get_time_zone("US/Pacific") hass.config.units = METRIC_SYSTEM + hass.config.media_dirs = {"media": get_test_config_dir("media")} hass.config.skip_pip = True hass.config_entries = config_entries.ConfigEntries(hass, {}) diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index 68e0fcda1d8..c7fc2dd6338 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -5,6 +5,7 @@ from homeassistant.components import media_source from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source import const +from homeassistant.components.media_source.error import Unresolvable from homeassistant.setup import async_setup_component from tests.async_mock import patch @@ -62,11 +63,23 @@ async def test_async_resolve_media(hass): assert await async_setup_component(hass, const.DOMAIN, {}) await hass.async_block_till_done() - # Test no media content - media = await media_source.async_resolve_media(hass, "") + media = await media_source.async_resolve_media( + hass, + media_source.generate_media_source_id(const.DOMAIN, "media/test.mp3"), + ) assert isinstance(media, media_source.models.PlayMedia) +async def test_async_unresolve_media(hass): + """Test browse media.""" + assert await async_setup_component(hass, const.DOMAIN, {}) + await hass.async_block_till_done() + + # Test no media content + with pytest.raises(Unresolvable): + await media_source.async_resolve_media(hass, "") + + async def test_websocket_browse_media(hass, hass_ws_client): """Test browse media websocket.""" assert await async_setup_component(hass, const.DOMAIN, {}) @@ -127,7 +140,7 @@ async def test_websocket_resolve_media(hass, hass_ws_client): client = await hass_ws_client(hass) - media = media_source.models.PlayMedia("/media/test.mp3", "audio/mpeg") + media = media_source.models.PlayMedia("/local_source/media/test.mp3", "audio/mpeg") with patch( "homeassistant.components.media_source.async_resolve_media", diff --git a/tests/components/media_source/test_local_source.py b/tests/components/media_source/test_local_source.py index 44d38107949..bd0a1435eef 100644 --- a/tests/components/media_source/test_local_source.py +++ b/tests/components/media_source/test_local_source.py @@ -3,11 +3,18 @@ import pytest from homeassistant.components import media_source from homeassistant.components.media_source import const +from homeassistant.config import async_process_ha_core_config from homeassistant.setup import async_setup_component async def test_async_browse_media(hass): """Test browse media.""" + local_media = hass.config.path("media") + await async_process_ha_core_config( + hass, {"media_dirs": {"media": local_media, "recordings": local_media}} + ) + await hass.async_block_till_done() + assert await async_setup_component(hass, const.DOMAIN, {}) await hass.async_block_till_done() @@ -40,27 +47,53 @@ async def test_async_browse_media(hass): assert str(excinfo.value) == "Invalid path." # Test successful listing + media = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{const.DOMAIN}" + ) + assert media + media = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/." ) assert media + media = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{const.DOMAIN}/recordings/." + ) + assert media + async def test_media_view(hass, hass_client): """Test media view.""" + local_media = hass.config.path("media") + await async_process_ha_core_config( + hass, {"media_dirs": {"media": local_media, "recordings": local_media}} + ) + await hass.async_block_till_done() + assert await async_setup_component(hass, const.DOMAIN, {}) await hass.async_block_till_done() client = await hass_client() # Protects against non-existent files - resp = await client.get("/media/invalid.txt") + resp = await client.get("/local_source/media/invalid.txt") + assert resp.status == 404 + + resp = await client.get("/local_source/recordings/invalid.txt") assert resp.status == 404 # Protects against non-media files - resp = await client.get("/media/not_media.txt") + resp = await client.get("/local_source/media/not_media.txt") + assert resp.status == 404 + + # Protects against unknown local media sources + resp = await client.get("/local_source/unknown_source/not_media.txt") assert resp.status == 404 # Fetch available media - resp = await client.get("/media/test.mp3") + resp = await client.get("/local_source/media/test.mp3") + assert resp.status == 200 + + resp = await client.get("/local_source/recordings/test.mp3") assert resp.status == 200 diff --git a/tests/test_config.py b/tests/test_config.py index c5443666bf5..a6c6ee86acc 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -440,6 +440,7 @@ async def test_loading_configuration(hass): "allowlist_external_dirs": "/etc", "external_url": "https://www.example.com", "internal_url": "http://example.local", + "media_dirs": {"mymedia": "/usr"}, }, ) @@ -453,6 +454,8 @@ async def test_loading_configuration(hass): assert hass.config.internal_url == "http://example.local" assert len(hass.config.allowlist_external_dirs) == 3 assert "/etc" in hass.config.allowlist_external_dirs + assert "/usr" in hass.config.allowlist_external_dirs + assert hass.config.media_dirs == {"mymedia": "/usr"} assert hass.config.config_source == config_util.SOURCE_YAML @@ -483,6 +486,22 @@ async def test_loading_configuration_temperature_unit(hass): assert hass.config.config_source == config_util.SOURCE_YAML +async def test_loading_configuration_default_media_dirs_docker(hass): + """Test loading core config onto hass object.""" + with patch("homeassistant.config.is_docker_env", return_value=True): + await config_util.async_process_ha_core_config( + hass, + { + "name": "Huis", + }, + ) + + assert hass.config.location_name == "Huis" + assert len(hass.config.allowlist_external_dirs) == 2 + assert "/media" in hass.config.allowlist_external_dirs + assert hass.config.media_dirs == {"media": "/media"} + + async def test_loading_configuration_from_packages(hass): """Test loading packages config onto hass object config.""" await config_util.async_process_ha_core_config( From b856b0e15ddac30c8614d5e0650d2ebd798f7cd6 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 15 Sep 2020 17:29:24 +0300 Subject: [PATCH 106/185] Guard both Shelly 2 & Shelly 2.5 in roller mode (#40086) Co-authored-by: Maciej Bieniek --- homeassistant/components/shelly/switch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 0aaa6dbc911..0dcefb51cf9 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -14,7 +14,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): wrapper = hass.data[DOMAIN][config_entry.entry_id] # In roller mode the relay blocks exist but do not contain required info - if wrapper.model == "SHSW-25" and wrapper.device.settings["mode"] != "relay": + if ( + wrapper.model in ["SHSW-21", "SHSW-25"] + and wrapper.device.settings["mode"] != "relay" + ): return relay_blocks = [block for block in wrapper.device.blocks if block.type == "relay"] From 4518335a563e75a9521ff4dcabddd780ada0bed2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 15 Sep 2020 16:30:33 +0200 Subject: [PATCH 107/185] Remove the unnecessary prefix from the sensor names in the Shelly integration (#40097) --- homeassistant/components/shelly/entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index b1a61bbbf59..96281e2aee1 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -42,11 +42,11 @@ async def async_setup_entry_attribute_entities( if not blocks: return - counts = Counter([item[0].type for item in blocks]) + counts = Counter([item[1] for item in blocks]) async_add_entities( [ - sensor_class(wrapper, block, sensor_id, description, counts[block.type]) + sensor_class(wrapper, block, sensor_id, description, counts[sensor_id]) for block, sensor_id, description in blocks ] ) From 39c4b338f138d34db6612fb22b54ad12e41bed87 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 16 Sep 2020 08:14:11 +0200 Subject: [PATCH 108/185] Increase TIMEOUT_ACK to 10s (#40117) --- homeassistant/components/mqtt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 470e43b5b44..d9bf1bbadfa 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -134,7 +134,7 @@ CONNECTION_FAILED = "connection_failed" CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable" DISCOVERY_COOLDOWN = 2 -TIMEOUT_ACK = 2 +TIMEOUT_ACK = 10 PLATFORMS = [ "alarm_control_panel", From 7f8a89838baa97d598108138a5bc362b92b6fb73 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 16 Sep 2020 00:12:59 +0200 Subject: [PATCH 109/185] Bump aioshelly library to version 0.3.2 (#40118) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index b2d3a7b7795..16467fa999c 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==0.3.1"], + "requirements": ["aioshelly==0.3.2"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], "codeowners": ["@balloob", "@bieniu"] } diff --git a/requirements_all.txt b/requirements_all.txt index 97e840bea16..5a9f88c4963 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -221,7 +221,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.shelly -aioshelly==0.3.1 +aioshelly==0.3.2 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e32ce166be0..95fb575eee8 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.shelly -aioshelly==0.3.1 +aioshelly==0.3.2 # homeassistant.components.switcher_kis aioswitcher==1.2.1 From d3bb2e5e16ba823aea4baeeebdad06c1995c64f9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Sep 2020 21:54:59 +1200 Subject: [PATCH 110/185] Allow ESPHome to trigger the HA tag scanned event (#40128) --- homeassistant/components/esphome/__init__.py | 10 ++++++++++ homeassistant/components/esphome/manifest.json | 4 +--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index f1b22c13bf1..497dc0deda8 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -129,6 +129,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool "Can only generate events under esphome domain! (%s)", host ) return + + # Call native tag scan + if service_name == "tag_scanned": + tag_id = service_data["tag_id"] + device_id = service_data["device_id"] + hass.async_create_task( + hass.components.tag.async_scan_tag(tag_id, device_id) + ) + return + hass.bus.async_fire(service.service, service_data) else: hass.async_create_task( diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index c57ff4a5520..123c7931e41 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -6,7 +6,5 @@ "requirements": ["aioesphomeapi==2.6.3"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter"], - "after_dependencies": [ - "zeroconf" - ] + "after_dependencies": ["zeroconf", "tag"] } From abca1778948412171688137eff729ae326ae74f6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Sep 2020 22:22:06 +1200 Subject: [PATCH 111/185] Use device name stored in device_info for tag scan in ESPHome (#40130) --- homeassistant/components/esphome/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 497dc0deda8..64aae3ce522 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -133,9 +133,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool # Call native tag scan if service_name == "tag_scanned": tag_id = service_data["tag_id"] - device_id = service_data["device_id"] hass.async_create_task( - hass.components.tag.async_scan_tag(tag_id, device_id) + hass.components.tag.async_scan_tag( + tag_id, entry_data.device_info.name + ) ) return From 8dde59be02217113fe715a28f1a37a8163ca8125 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Sep 2020 12:48:38 +0200 Subject: [PATCH 112/185] Guard for when Yandex Transport data fetching fails (#40131) --- homeassistant/components/yandex_transport/sensor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index cde115cb12f..957844e519d 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -60,7 +60,7 @@ class DiscoverYandexTransport(Entity): self._name = name self._attrs = None - async def async_update(self): + async def async_update(self, *, tries=0): """Get the latest data from maps.yandex.ru and update the states.""" attrs = {} closer_time = None @@ -73,8 +73,12 @@ class DiscoverYandexTransport(Entity): key_error, yandex_reply, ) + if tries > 0: + return await self.requester.set_new_session() - data = (await self.requester.get_stop_info(self._stop_id))["data"] + await self.async_update(tries=tries + 1) + return + stop_name = data["name"] transport_list = data["transports"] for transport in transport_list: From b28dbe20b696d5c25ce6e7216425aa8953f20919 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Sep 2020 13:17:05 +0200 Subject: [PATCH 113/185] Fix ESPHome scan tag device ID (#40132) --- homeassistant/components/esphome/__init__.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 64aae3ce522..c9d07a22ec6 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -66,6 +66,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] password = entry.data[CONF_PASSWORD] + device_id = None zeroconf_instance = await zeroconf.async_get_instance(hass) @@ -134,9 +135,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool if service_name == "tag_scanned": tag_id = service_data["tag_id"] hass.async_create_task( - hass.components.tag.async_scan_tag( - tag_id, entry_data.device_info.name - ) + hass.components.tag.async_scan_tag(tag_id, device_id) ) return @@ -177,10 +176,13 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool async def on_login() -> None: """Subscribe to states and list entities on successful API login.""" + nonlocal device_id try: entry_data.device_info = await cli.device_info() entry_data.available = True - await _async_setup_device_registry(hass, entry, entry_data.device_info) + device_id = await _async_setup_device_registry( + hass, entry, entry_data.device_info + ) entry_data.async_update_device_state(hass) entity_infos, services = await cli.list_entities_services() @@ -276,7 +278,7 @@ async def _async_setup_device_registry( if device_info.compilation_time: sw_version += f" ({device_info.compilation_time})" device_registry = await dr.async_get_registry(hass) - device_registry.async_get_or_create( + entry = device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}, name=device_info.name, @@ -284,6 +286,7 @@ async def _async_setup_device_registry( model=device_info.model, sw_version=sw_version, ) + return entry.id async def _register_service( From f1169120ae043bf687313739f49397492523f54a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Sep 2020 13:37:51 +0000 Subject: [PATCH 114/185] Bumped version to 0.115.0b11 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ec1f366fabe..33e059b5846 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b10" +PATCH_VERSION = "0b11" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From c62a6cd779d8ae9c02fb1567d9fd54a98295e845 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Sep 2020 16:26:34 +0200 Subject: [PATCH 115/185] Fix scene validator (#40140) --- homeassistant/components/homeassistant/scene.py | 8 ++++---- tests/components/homeassistant/test_scene.py | 9 +++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 7e4a0433344..1ff3915f121 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -35,15 +35,15 @@ def _convert_states(states): """Convert state definitions to State objects.""" result = {} - for entity_id in states: + for entity_id, info in states.items(): entity_id = cv.entity_id(entity_id) - if isinstance(states[entity_id], dict): - entity_attrs = states[entity_id].copy() + if isinstance(info, dict): + entity_attrs = info.copy() state = entity_attrs.pop(ATTR_STATE, None) attributes = entity_attrs else: - state = states[entity_id] + state = info attributes = {} # YAML translates 'on' to a boolean diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index a1f502a3475..8f47d891f9f 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -320,3 +320,12 @@ async def test_config(hass): no_icon = hass.states.get("scene.scene_no_icon") assert no_icon is not None assert "icon" not in no_icon.attributes + + +def test_validator(): + """Test validators.""" + parsed = ha_scene.STATES_SCHEMA({"light.Test": {"state": "on"}}) + assert len(parsed) == 1 + assert "light.test" in parsed + assert parsed["light.test"].entity_id == "light.test" + assert parsed["light.test"].state == "on" From d3a59652bb8502b25698ff28e6fb048b1f665eed Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Sep 2020 17:04:57 +0200 Subject: [PATCH 116/185] Fix missing f from f-strings in cast integration (#40144) --- homeassistant/components/cast/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 177babdb476..f948c51655b 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -375,9 +375,9 @@ class CastDevice(MediaPlayerEntity): if tts_base_url and media_status.content_id.startswith(tts_base_url): url_description = f" from tts.base_url ({tts_base_url})" if external_url and media_status.content_id.startswith(external_url): - url_description = " from external_url ({external_url})" + url_description = f" from external_url ({external_url})" if internal_url and media_status.content_id.startswith(internal_url): - url_description = " from internal_url ({internal_url})" + url_description = f" from internal_url ({internal_url})" _LOGGER.error( "Failed to cast media %s%s. Please make sure the URL is: " From 9ffcf35b23bc0707975db9042404c40f49319b54 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Sep 2020 21:38:40 +0200 Subject: [PATCH 117/185] Fix local media browser source conflicting with local www folder (#40151) --- .../components/media_source/local_source.py | 8 +++---- homeassistant/config.py | 4 ++-- tests/common.py | 2 +- tests/components/media_source/test_init.py | 6 ++--- .../media_source/test_local_source.py | 24 +++++++++---------- tests/test_config.py | 2 +- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index e14735fb60b..6c60da562e0 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -64,7 +64,7 @@ class LocalSource(MediaSource): mime_type, _ = mimetypes.guess_type( str(self.async_full_path(source_dir_id, location)) ) - return PlayMedia(f"/local_source/{item.identifier}", mime_type) + return PlayMedia(f"/media/{item.identifier}", mime_type) async def async_browse_media( self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES @@ -177,7 +177,7 @@ class LocalMediaView(HomeAssistantView): Returns media files in config/media. """ - url = "/local_source/{source_dir_id}/{location:.*}" + url = "/media/{source_dir_id}/{location:.*}" name = "media" def __init__(self, hass: HomeAssistant, source: LocalSource): @@ -190,10 +190,10 @@ class LocalMediaView(HomeAssistantView): ) -> web.FileResponse: """Start a GET request.""" if location != sanitize_path(location): - return web.HTTPNotFound() + raise web.HTTPNotFound() if source_dir_id not in self.hass.config.media_dirs: - return web.HTTPNotFound() + raise web.HTTPNotFound() media_path = self.source.async_full_path(source_dir_id, location) diff --git a/homeassistant/config.py b/homeassistant/config.py index 3d6e3fb041c..8f9dc7c3d62 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -510,9 +510,9 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non if CONF_MEDIA_DIRS not in config: if is_docker_env(): - hac.media_dirs = {"media": "/media"} + hac.media_dirs = {"local": "/media"} else: - hac.media_dirs = {"media": hass.config.path("media")} + hac.media_dirs = {"local": hass.config.path("media")} # Init whitelist external dir hac.allowlist_external_dirs = {hass.config.path("www"), *hac.media_dirs.values()} diff --git a/tests/common.py b/tests/common.py index b36439d4110..d516873786b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -205,7 +205,7 @@ async def async_test_home_assistant(loop): hass.config.elevation = 0 hass.config.time_zone = date_util.get_time_zone("US/Pacific") hass.config.units = METRIC_SYSTEM - hass.config.media_dirs = {"media": get_test_config_dir("media")} + hass.config.media_dirs = {"local": get_test_config_dir("media")} hass.config.skip_pip = True hass.config_entries = config_entries.ConfigEntries(hass, {}) diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index c7fc2dd6338..a891fb0d11d 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -65,7 +65,7 @@ async def test_async_resolve_media(hass): media = await media_source.async_resolve_media( hass, - media_source.generate_media_source_id(const.DOMAIN, "media/test.mp3"), + media_source.generate_media_source_id(const.DOMAIN, "local/test.mp3"), ) assert isinstance(media, media_source.models.PlayMedia) @@ -140,7 +140,7 @@ async def test_websocket_resolve_media(hass, hass_ws_client): client = await hass_ws_client(hass) - media = media_source.models.PlayMedia("/local_source/media/test.mp3", "audio/mpeg") + media = media_source.models.PlayMedia("/media/local/test.mp3", "audio/mpeg") with patch( "homeassistant.components.media_source.async_resolve_media", @@ -150,7 +150,7 @@ async def test_websocket_resolve_media(hass, hass_ws_client): { "id": 1, "type": "media_source/resolve_media", - "media_content_id": f"{const.URI_SCHEME}{const.DOMAIN}/media/test.mp3", + "media_content_id": f"{const.URI_SCHEME}{const.DOMAIN}/local/test.mp3", } ) diff --git a/tests/components/media_source/test_local_source.py b/tests/components/media_source/test_local_source.py index bd0a1435eef..e3e2a3f1617 100644 --- a/tests/components/media_source/test_local_source.py +++ b/tests/components/media_source/test_local_source.py @@ -11,7 +11,7 @@ async def test_async_browse_media(hass): """Test browse media.""" local_media = hass.config.path("media") await async_process_ha_core_config( - hass, {"media_dirs": {"media": local_media, "recordings": local_media}} + hass, {"media_dirs": {"local": local_media, "recordings": local_media}} ) await hass.async_block_till_done() @@ -21,14 +21,14 @@ async def test_async_browse_media(hass): # Test path not exists with pytest.raises(media_source.BrowseError) as excinfo: await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/test/not/exist" + hass, f"{const.URI_SCHEME}{const.DOMAIN}/local/test/not/exist" ) assert str(excinfo.value) == "Path does not exist." # Test browse file with pytest.raises(media_source.BrowseError) as excinfo: await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/test.mp3" + hass, f"{const.URI_SCHEME}{const.DOMAIN}/local/test.mp3" ) assert str(excinfo.value) == "Path is not a directory." @@ -42,7 +42,7 @@ async def test_async_browse_media(hass): # Test directory traversal with pytest.raises(media_source.BrowseError) as excinfo: await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/../configuration.yaml" + hass, f"{const.URI_SCHEME}{const.DOMAIN}/local/../configuration.yaml" ) assert str(excinfo.value) == "Invalid path." @@ -53,7 +53,7 @@ async def test_async_browse_media(hass): assert media media = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/." + hass, f"{const.URI_SCHEME}{const.DOMAIN}/local/." ) assert media @@ -67,7 +67,7 @@ async def test_media_view(hass, hass_client): """Test media view.""" local_media = hass.config.path("media") await async_process_ha_core_config( - hass, {"media_dirs": {"media": local_media, "recordings": local_media}} + hass, {"media_dirs": {"local": local_media, "recordings": local_media}} ) await hass.async_block_till_done() @@ -77,23 +77,23 @@ async def test_media_view(hass, hass_client): client = await hass_client() # Protects against non-existent files - resp = await client.get("/local_source/media/invalid.txt") + resp = await client.get("/media/local/invalid.txt") assert resp.status == 404 - resp = await client.get("/local_source/recordings/invalid.txt") + resp = await client.get("/media/recordings/invalid.txt") assert resp.status == 404 # Protects against non-media files - resp = await client.get("/local_source/media/not_media.txt") + resp = await client.get("/media/local/not_media.txt") assert resp.status == 404 # Protects against unknown local media sources - resp = await client.get("/local_source/unknown_source/not_media.txt") + resp = await client.get("/media/unknown_source/not_media.txt") assert resp.status == 404 # Fetch available media - resp = await client.get("/local_source/media/test.mp3") + resp = await client.get("/media/local/test.mp3") assert resp.status == 200 - resp = await client.get("/local_source/recordings/test.mp3") + resp = await client.get("/media/recordings/test.mp3") assert resp.status == 200 diff --git a/tests/test_config.py b/tests/test_config.py index a6c6ee86acc..fb22ee1118e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -499,7 +499,7 @@ async def test_loading_configuration_default_media_dirs_docker(hass): assert hass.config.location_name == "Huis" assert len(hass.config.allowlist_external_dirs) == 2 assert "/media" in hass.config.allowlist_external_dirs - assert hass.config.media_dirs == {"media": "/media"} + assert hass.config.media_dirs == {"local": "/media"} async def test_loading_configuration_from_packages(hass): From f7d7765d5edfb8ed225e8838817e3173ab6c3229 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 16 Sep 2020 22:31:43 +0200 Subject: [PATCH 118/185] Update frontend to 20200916.0 (#40153) --- 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 9b499f0376a..b7bb77346de 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==20200915.0"], + "requirements": ["home-assistant-frontend==20200916.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ccede9062a0..a535d83d39c 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.37.0 -home-assistant-frontend==20200915.0 +home-assistant-frontend==20200916.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 5a9f88c4963..87504a86112 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200915.0 +home-assistant-frontend==20200916.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 95fb575eee8..7b774ab7a5a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200915.0 +home-assistant-frontend==20200916.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From a9e220c96b17a34b293b92ce30499831c970cb47 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Sep 2020 20:50:22 +0000 Subject: [PATCH 119/185] Bumped version to 0.115.0b12 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 33e059b5846..e01c5335757 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b11" +PATCH_VERSION = "0b12" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 99a57f5a4e245fd737141348657605ada3c4927b Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 17 Sep 2020 09:01:28 -0500 Subject: [PATCH 120/185] Check mpd time type before splitting it (#40139) --- homeassistant/components/mpd/media_player.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 8a46cef6eb3..845b0ae506b 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -138,12 +138,12 @@ class MpdDevice(MediaPlayerEntity): if position is None: position = self._status.get("time") - if position is not None and ":" in position: + if isinstance(position, str) and ":" in position: position = position.split(":")[0] if position is not None and self._media_position != position: self._media_position_updated_at = dt_util.utcnow() - self._media_position = int(position) + self._media_position = int(float(position)) self._update_playlists() @@ -159,8 +159,9 @@ class MpdDevice(MediaPlayerEntity): self._connect() self._fetch_status() - except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError): + except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError) as error: # Cleanly disconnect in case connection is not in valid state + _LOGGER.debug("Error updating status: %s", error) self._disconnect() @property From 78022bf145fec02d50f2bc40b57c11c667f54b6a Mon Sep 17 00:00:00 2001 From: cagnulein Date: Thu, 17 Sep 2020 11:35:13 +0200 Subject: [PATCH 121/185] Fix luci device_tracker not reliably reporting home/away state (#40160) --- homeassistant/components/luci/device_tracker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index 9d71b3d263a..fe64c90bf4c 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -94,6 +94,7 @@ class LuciDeviceScanner(DeviceScanner): last_results = [] for device in result: - last_results.append(device) + if device.reachable: + last_results.append(device) self.last_results = last_results From 5271a3eb1e9bbbc693d082cfd6fe6e37404c21c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 17 Sep 2020 12:08:09 +0200 Subject: [PATCH 122/185] Update pyhaversion to 3.4.2 (#40161) --- homeassistant/components/version/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index 1f07c757ad8..7fc1a097d81 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -2,7 +2,7 @@ "domain": "version", "name": "Version", "documentation": "https://www.home-assistant.io/integrations/version", - "requirements": ["pyhaversion==3.4.0"], + "requirements": ["pyhaversion==3.4.2"], "codeowners": ["@fabaff", "@ludeeus"], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index 87504a86112..e34f48898fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1377,7 +1377,7 @@ pygtfs==0.1.5 pygti==0.6.0 # homeassistant.components.version -pyhaversion==3.4.0 +pyhaversion==3.4.2 # homeassistant.components.heos pyheos==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b774ab7a5a..c1b419263e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -662,7 +662,7 @@ pygatt[GATTTOOL]==4.0.5 pygti==0.6.0 # homeassistant.components.version -pyhaversion==3.4.0 +pyhaversion==3.4.2 # homeassistant.components.heos pyheos==0.6.0 From 13cfd1bae1f80be1c0100e532cce173eedbcd19c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 17 Sep 2020 14:44:19 +0200 Subject: [PATCH 123/185] Updated frontend to 20200917.1 (#40170) --- 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 b7bb77346de..96d2a51cb09 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==20200916.0"], + "requirements": ["home-assistant-frontend==20200917.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a535d83d39c..e5978245bac 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.37.0 -home-assistant-frontend==20200916.0 +home-assistant-frontend==20200917.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index e34f48898fb..7ff2d0695b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200916.0 +home-assistant-frontend==20200917.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1b419263e5..d492e3acbb5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200916.0 +home-assistant-frontend==20200917.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 6f26722f692691b6fd480bb36ea6c7ca778cdae8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 17 Sep 2020 16:45:55 +0200 Subject: [PATCH 124/185] Fix editing tags only get isoformat from datetime (#40174) Co-authored-by: Paulus Schoutsen --- homeassistant/components/tag/__init__.py | 3 ++- tests/components/tag/test_init.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 0b445fbf575..321dce9a296 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -82,7 +82,7 @@ class TagStorageCollection(collection.StorageCollection): """Return a new updated data object.""" data = {**data, **self.UPDATE_SCHEMA(update_data)} # make last_scanned JSON serializeable - if LAST_SCANNED in data: + if LAST_SCANNED in update_data: data[LAST_SCANNED] = data[LAST_SCANNED].isoformat() return data @@ -100,6 +100,7 @@ async def async_setup(hass: HomeAssistant, config: dict): collection.StorageCollectionWebsocket( storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) + return True diff --git a/tests/components/tag/test_init.py b/tests/components/tag/test_init.py index e4b810e0661..ecb927c1c5c 100644 --- a/tests/components/tag/test_init.py +++ b/tests/components/tag/test_init.py @@ -48,6 +48,30 @@ async def test_ws_list(hass, hass_ws_client, storage_setup): assert "test tag" in result +async def test_ws_update(hass, hass_ws_client, storage_setup): + """Test listing tags via WS.""" + assert await storage_setup() + await async_scan_tag(hass, "test tag", "some_scanner") + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": "test tag", + "name": "New name", + } + ) + resp = await client.receive_json() + assert resp["success"] + + item = resp["result"] + + assert item["id"] == "test tag" + assert item["name"] == "New name" + + async def test_tag_scanned(hass, hass_ws_client, storage_setup): """Test scanning tags.""" assert await storage_setup() From f5aee6b886e9f52abdcd95a5eb877b754a7ec624 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Sep 2020 09:47:23 -0500 Subject: [PATCH 125/185] Add missing conext preservation to bayesian and universal (#40178) We already do this for template sensors, but it was missing for bayesian and universal --- .../components/bayesian/binary_sensor.py | 3 + .../components/universal/media_player.py | 7 +- .../components/bayesian/test_binary_sensor.py | 85 ++++++++++++++++++- .../components/universal/test_media_player.py | 11 ++- 4 files changed, 103 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 90540e456c5..4768b3f4fe6 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -182,6 +182,7 @@ class BayesianBinarySensor(BinarySensorEntity): entity = event.data.get("entity_id") self.current_observations.update(self._record_entity_observations(entity)) + self.async_set_context(event.context) self._recalculate_and_write_state() self.async_on_remove( @@ -220,6 +221,8 @@ class BayesianBinarySensor(BinarySensorEntity): obs_entry = None self.current_observations[obs["id"]] = obs_entry + if event: + self.async_set_context(event.context) self._recalculate_and_write_state() for template in self.observations_by_template: diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index c38afc139cf..aedc27c2a29 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -145,8 +145,9 @@ class UniversalMediaPlayer(MediaPlayerEntity): """Subscribe to children and template state changes.""" @callback - def _async_on_dependency_update(*_): + def _async_on_dependency_update(event): """Update ha state when dependencies update.""" + self.async_set_context(event.context) self.async_schedule_update_ha_state(True) @callback @@ -158,6 +159,10 @@ class UniversalMediaPlayer(MediaPlayerEntity): self._state_template_result = None else: self._state_template_result = result + + if event: + self.async_set_context(event.context) + self.async_schedule_update_ha_state(True) if self._state_template is not None: diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 57c7f404c7f..5755a7e24e9 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -9,7 +9,14 @@ from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_RELOAD, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_RELOAD, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, +) +from homeassistant.core import Context, callback from homeassistant.setup import async_setup_component, setup_component from tests.async_mock import patch @@ -686,3 +693,79 @@ async def test_reload(hass): def _get_fixtures_base_path(): return path.dirname(path.dirname(path.dirname(__file__))) + + +async def test_template_triggers(hass): + """Test sensor with template triggers.""" + hass.states.async_set("input_boolean.test", STATE_OFF) + config = { + "binary_sensor": { + "name": "Test_Binary", + "platform": "bayesian", + "observations": [ + { + "platform": "template", + "value_template": "{{ states.input_boolean.test.state }}", + "prob_given_true": 1999.9, + }, + ], + "prior": 0.2, + "probability_threshold": 0.32, + } + } + + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test_binary").state == STATE_OFF + + events = [] + hass.helpers.event.async_track_state_change_event( + "binary_sensor.test_binary", callback(lambda event: events.append(event)) + ) + + context = Context() + hass.states.async_set("input_boolean.test", STATE_ON, context=context) + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert events[0].context == context + + +async def test_state_triggers(hass): + """Test sensor with state triggers.""" + hass.states.async_set("sensor.test_monitored", STATE_OFF) + + config = { + "binary_sensor": { + "name": "Test_Binary", + "platform": "bayesian", + "observations": [ + { + "platform": "state", + "entity_id": "sensor.test_monitored", + "to_state": "off", + "prob_given_true": 999.9, + "prob_given_false": 999.4, + }, + ], + "prior": 0.2, + "probability_threshold": 0.32, + } + } + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test_binary").state == STATE_OFF + + events = [] + hass.helpers.event.async_track_state_change_event( + "binary_sensor.test_binary", callback(lambda event: events.append(event)) + ) + + context = Context() + hass.states.async_set("sensor.test_monitored", STATE_ON, context=context) + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert events[0].context == context diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 38949c32ebb..76a397496ad 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -20,6 +20,7 @@ from homeassistant.const import ( STATE_PLAYING, STATE_UNKNOWN, ) +from homeassistant.core import Context, callback from homeassistant.setup import async_setup_component from tests.async_mock import patch @@ -812,10 +813,18 @@ async def test_master_state_with_template(hass): await hass.async_block_till_done() hass.states.get("media_player.tv").state == STATE_ON - hass.states.async_set("input_boolean.test", STATE_ON) + events = [] + + hass.helpers.event.async_track_state_change_event( + "media_player.tv", callback(lambda event: events.append(event)) + ) + + context = Context() + hass.states.async_set("input_boolean.test", STATE_ON, context=context) await hass.async_block_till_done() hass.states.get("media_player.tv").state == STATE_OFF + assert events[0].context == context async def test_reload(hass): From f77b3d4714faec0196bb3ceae780111fdc2b6115 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 17 Sep 2020 17:10:26 +0200 Subject: [PATCH 126/185] Bumped version to 0.115.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e01c5335757..80129e097d0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b12" +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 7cb0c98c03919af95865930b77506998e2a99c4b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Sep 2020 14:45:30 -0500 Subject: [PATCH 127/185] Log template listeners when debug logging is on (#40180) --- homeassistant/helpers/event.py | 14 ++++++++++++++ tests/helpers/test_event.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 435a265d9e0..3033b51b605 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -525,6 +525,11 @@ class _TrackTemplateResultInfo: self._last_info = self._info.copy() self._create_listeners() + _LOGGER.debug( + "Template group %s listens for %s", + self._track_templates, + self.listeners, + ) @property def listeners(self) -> Dict: @@ -683,6 +688,10 @@ class _TrackTemplateResultInfo: ): continue + _LOGGER.debug( + "Template update %s triggered by event: %s", template.template, event + ) + self._info[template] = template.async_render_to_info( track_template_.variables ) @@ -708,6 +717,11 @@ class _TrackTemplateResultInfo: if info_changed: self._update_listeners() + _LOGGER.debug( + "Template group %s listens for %s", + self._track_templates, + self.listeners, + ) self._last_info = self._info.copy() if not updates: diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index ba14c8a757f..ced25f1ad5b 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -541,7 +541,7 @@ async def test_track_template_error(hass, caplog): hass.states.async_set("switch.not_exist", "off") await hass.async_block_till_done() - assert "lunch" not in caplog.text + assert "no filter named 'lunch'" not in caplog.text assert "TemplateAssertionError" not in caplog.text From b8fe0c6c3a4d3ae50e2857ced97b35901237f194 Mon Sep 17 00:00:00 2001 From: Julius Mittenzwei Date: Fri, 18 Sep 2020 03:18:55 +0200 Subject: [PATCH 128/185] Upgrade pyvlx to 0.2.17 (#40182) --- homeassistant/components/velux/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json index 73306bca7b5..0fdbfb64999 100644 --- a/homeassistant/components/velux/manifest.json +++ b/homeassistant/components/velux/manifest.json @@ -2,6 +2,6 @@ "domain": "velux", "name": "Velux", "documentation": "https://www.home-assistant.io/integrations/velux", - "requirements": ["pyvlx==0.2.16"], + "requirements": ["pyvlx==0.2.17"], "codeowners": ["@Julius2342"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7ff2d0695b5..18d0e6812f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1837,7 +1837,7 @@ pyvesync==1.1.0 pyvizio==0.1.56 # homeassistant.components.velux -pyvlx==0.2.16 +pyvlx==0.2.17 # homeassistant.components.volumio pyvolumio==0.1.2 From 0383030266340013d34c8717f452e95151962cd3 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 18 Sep 2020 07:18:56 -0400 Subject: [PATCH 129/185] Fix Vizio async_unload_entry bug (#40210) --- homeassistant/components/vizio/__init__.py | 2 +- tests/components/vizio/test_init.py | 27 +++++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index 25960da72cf..a7a9c404f74 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -94,7 +94,7 @@ async def async_unload_entry( and entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV for entry in hass.config_entries.async_entries(DOMAIN) ): - hass.data[DOMAIN].pop(CONF_APPS) + hass.data[DOMAIN].pop(CONF_APPS, None) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py index 33764de8696..cd611662597 100644 --- a/tests/components/vizio/test_init.py +++ b/tests/components/vizio/test_init.py @@ -6,7 +6,7 @@ from homeassistant.components.vizio.const import DOMAIN from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component -from .const import MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID +from .const import MOCK_SPEAKER_CONFIG, MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID from tests.common import MockConfigEntry @@ -24,12 +24,12 @@ async def test_setup_component( assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 -async def test_load_and_unload( +async def test_tv_load_and_unload( hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture, ) -> None: - """Test loading and unloading entry.""" + """Test loading and unloading TV entry.""" config_entry = MockConfigEntry( domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG, unique_id=UNIQUE_ID ) @@ -43,3 +43,24 @@ async def test_load_and_unload( await hass.async_block_till_done() assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0 assert DOMAIN not in hass.data + + +async def test_speaker_load_and_unload( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_update: pytest.fixture, +) -> None: + """Test loading and unloading speaker entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, unique_id=UNIQUE_ID + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 + assert DOMAIN in hass.data + + assert await config_entry.async_unload(hass) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0 + assert DOMAIN not in hass.data From 33b56b0cf92adf6900535415266171b1c9dfbe8c Mon Sep 17 00:00:00 2001 From: MeIchthys <10717998+meichthys@users.noreply.github.com> Date: Fri, 18 Sep 2020 09:34:17 -0400 Subject: [PATCH 130/185] Fix Nextcloud sensors becoming unavailable (#40212) --- homeassistant/components/nextcloud/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py index 12c17e6081d..ff94fd708db 100644 --- a/homeassistant/components/nextcloud/__init__.py +++ b/homeassistant/components/nextcloud/__init__.py @@ -100,7 +100,6 @@ def setup(hass, config): _LOGGER.error("Nextcloud setup failed - Check configuration") hass.data[DOMAIN] = get_data_points(ncm.data) - hass.data[DOMAIN]["instance"] = conf[CONF_URL] def nextcloud_update(event_time): """Update data from nextcloud api.""" @@ -111,6 +110,7 @@ def setup(hass, config): return False hass.data[DOMAIN] = get_data_points(ncm.data) + hass.data[DOMAIN]["instance"] = conf[CONF_URL] # Update sensors on time interval track_time_interval(hass, nextcloud_update, conf[CONF_SCAN_INTERVAL]) From d37fe1fbb629bc83929713440ef148c41582ed83 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Fri, 18 Sep 2020 09:32:38 -0400 Subject: [PATCH 131/185] Disable async on Apprise (#40213) --- homeassistant/components/apprise/notify.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/apprise/notify.py b/homeassistant/components/apprise/notify.py index 0c8c5b26eec..f999da94531 100644 --- a/homeassistant/components/apprise/notify.py +++ b/homeassistant/components/apprise/notify.py @@ -29,8 +29,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the Apprise notification service.""" - # Create our object - a_obj = apprise.Apprise() + # Create our Apprise Asset Object + asset = apprise.AppriseAsset(async_mode=False) + + # Create our Apprise Instance (reference our asset) + a_obj = apprise.Apprise(asset=asset) if config.get(CONF_FILE): # Sourced from a Configuration File From 8a39bea7614590f5579359a8b4e6f5f23eec17ff Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Fri, 18 Sep 2020 10:31:25 -0300 Subject: [PATCH 132/185] Fix RM mini 3 update manager (#40215) --- homeassistant/components/broadlink/updater.py | 22 +++++++++++++++++++ tests/components/broadlink/__init__.py | 3 +++ tests/components/broadlink/test_device.py | 4 ++-- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index bd124d1e1ac..8b6f1316f52 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -1,12 +1,15 @@ """Support for fetching data from Broadlink devices.""" from abc import ABC, abstractmethod from datetime import timedelta +from functools import partial import logging +import broadlink as blk from broadlink.exceptions import ( AuthorizationError, BroadlinkException, CommandNotSupportedError, + DeviceOfflineError, StorageError, ) @@ -18,6 +21,9 @@ _LOGGER = logging.getLogger(__name__) def get_update_manager(device): """Return an update manager for a given Broadlink device.""" + if device.api.model.startswith("RM mini"): + return BroadlinkRMMini3UpdateManager(device) + update_managers = { "A1": BroadlinkA1UpdateManager, "MP1": BroadlinkMP1UpdateManager, @@ -95,6 +101,22 @@ class BroadlinkMP1UpdateManager(BroadlinkUpdateManager): return await self.device.async_request(self.device.api.check_power) +class BroadlinkRMMini3UpdateManager(BroadlinkUpdateManager): + """Manages updates for Broadlink RM mini 3 devices.""" + + async def async_fetch_data(self): + """Fetch data from the device.""" + hello = partial( + blk.discover, + discover_ip_address=self.device.api.host[0], + timeout=self.device.api.timeout, + ) + devices = await self.device.hass.async_add_executor_job(hello) + if not devices: + raise DeviceOfflineError("The device is offline") + return {} + + class BroadlinkRMUpdateManager(BroadlinkUpdateManager): """Manages updates for Broadlink RM2 and RM4 devices.""" diff --git a/tests/components/broadlink/__init__.py b/tests/components/broadlink/__init__.py index 4051f94c0d3..95a2ef97c6f 100644 --- a/tests/components/broadlink/__init__.py +++ b/tests/components/broadlink/__init__.py @@ -85,6 +85,9 @@ class BroadlinkDevice: with patch( "homeassistant.components.broadlink.device.blk.gendevice", return_value=mock_api, + ), patch( + "homeassistant.components.broadlink.updater.blk.discover", + return_value=[mock_api], ): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py index 5cd0457b552..5b9d0e77508 100644 --- a/tests/components/broadlink/test_device.py +++ b/tests/components/broadlink/test_device.py @@ -179,7 +179,7 @@ async def test_device_setup_update_authorization_error(hass): async def test_device_setup_update_authentication_error(hass): """Test we handle an authentication error in the update step.""" - device = get_device("Living Room") + device = get_device("Garage") mock_api = device.get_mock_api() mock_api.check_sensors.side_effect = blke.AuthorizationError() mock_api.auth.side_effect = (None, blke.AuthenticationError()) @@ -207,7 +207,7 @@ async def test_device_setup_update_authentication_error(hass): async def test_device_setup_update_broadlink_exception(hass): """Test we handle a Broadlink exception in the update step.""" - device = get_device("Living Room") + device = get_device("Garage") mock_api = device.get_mock_api() mock_api.check_sensors.side_effect = blke.BroadlinkException() mock_entry = device.get_mock_entry() From 152b380a2f16377b7f85bd07c9477e4b4794c297 Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 18 Sep 2020 16:24:14 +0300 Subject: [PATCH 133/185] Fix kodi.call_method (#40236) --- homeassistant/components/kodi/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index f13d5301625..a1b9987b5c9 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -714,7 +714,7 @@ class KodiEntity(MediaPlayerEntity): _LOGGER.debug("Run API method %s, kwargs=%s", method, kwargs) result_ok = False try: - result = self._kodi.call_method(method, **kwargs) + result = await self._kodi.call_method(method, **kwargs) result_ok = True except jsonrpc_base.jsonrpc.ProtocolError as exc: result = exc.args[2]["error"] From ef279b125dbbe5fee1c388a1dc9bdeb057d31382 Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 18 Sep 2020 16:28:39 +0300 Subject: [PATCH 134/185] Handle systems without groups (#40238) --- homeassistant/components/risco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 80b132b0fb2..7f13af252f3 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", "requirements": [ - "pyrisco==0.3.0" + "pyrisco==0.3.1" ], "codeowners": [ "@OnFreund" diff --git a/requirements_all.txt b/requirements_all.txt index 18d0e6812f1..c1b8ac7321e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1595,7 +1595,7 @@ pyrecswitch==1.0.2 pyrepetier==3.0.5 # homeassistant.components.risco -pyrisco==0.3.0 +pyrisco==0.3.1 # homeassistant.components.sabnzbd pysabnzbd==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d492e3acbb5..e243b4e62a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -769,7 +769,7 @@ pyps4-2ndscreen==1.1.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.3.0 +pyrisco==0.3.1 # homeassistant.components.acer_projector # homeassistant.components.zha From e9b355bd8ae510f2f158633550d203cb2f863dbf Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 18 Sep 2020 16:28:02 +0300 Subject: [PATCH 135/185] Fix coolmaster.info (#40240) --- homeassistant/components/coolmaster/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index 58bd51fca4d..85bd3b1893f 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -3,6 +3,6 @@ "name": "CoolMasterNet", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coolmaster", - "requirements": ["pycoolmasternet-async==0.1.1"], + "requirements": ["pycoolmasternet-async==0.1.2"], "codeowners": ["@OnFreund"] } diff --git a/requirements_all.txt b/requirements_all.txt index c1b8ac7321e..cc83c47950f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1277,7 +1277,7 @@ pycocotools==2.0.1 pycomfoconnect==0.3 # homeassistant.components.coolmaster -pycoolmasternet-async==0.1.1 +pycoolmasternet-async==0.1.2 # homeassistant.components.avri pycountry==19.8.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e243b4e62a0..84f2f0c9a64 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ pybotvac==0.0.17 pychromecast==7.2.1 # homeassistant.components.coolmaster -pycoolmasternet-async==0.1.1 +pycoolmasternet-async==0.1.2 # homeassistant.components.avri pycountry==19.8.18 From 2df709c7d03a1df028849e99a87c33d6b21fbf79 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Sep 2020 14:05:11 +0000 Subject: [PATCH 136/185] Bumped version to 0.115.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 80129e097d0..79d91ef7cb2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -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 d35f06ac156603b7a77aa5b96f52742ef047d630 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 19 Sep 2020 22:10:01 +0200 Subject: [PATCH 137/185] Get option flow defaults from yaml for non configured MQTT options (#40177) --- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/mqtt/config_flow.py | 41 +++++++++++++------- homeassistant/components/mqtt/const.py | 2 + 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index d9bf1bbadfa..bafbead96d6 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -60,6 +60,7 @@ from .const import ( CONF_RETAIN, CONF_STATE_TOPIC, CONF_WILL_MESSAGE, + DATA_MQTT_CONFIG, DEFAULT_BIRTH, DEFAULT_DISCOVERY, DEFAULT_PAYLOAD_AVAILABLE, @@ -88,7 +89,6 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "mqtt" DATA_MQTT = "mqtt" -DATA_MQTT_CONFIG = "mqtt_config" SERVICE_PUBLISH = "publish" SERVICE_DUMP = "dump" diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 8b1c350323c..5c4016437a6 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -24,6 +24,7 @@ from .const import ( CONF_BROKER, CONF_DISCOVERY, CONF_WILL_MESSAGE, + DATA_MQTT_CONFIG, DEFAULT_BIRTH, DEFAULT_DISCOVERY, DEFAULT_WILL, @@ -162,6 +163,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): """Manage the MQTT options.""" errors = {} current_config = self.config_entry.data + yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {}) if user_input is not None: can_connect = await self.hass.async_add_executor_job( try_connection, @@ -178,20 +180,22 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): errors["base"] = "cannot_connect" fields = OrderedDict() - fields[vol.Required(CONF_BROKER, default=current_config[CONF_BROKER])] = str - fields[vol.Required(CONF_PORT, default=current_config[CONF_PORT])] = vol.Coerce( - int - ) + current_broker = current_config.get(CONF_BROKER, yaml_config.get(CONF_BROKER)) + current_port = current_config.get(CONF_PORT, yaml_config.get(CONF_PORT)) + current_user = current_config.get(CONF_USERNAME, yaml_config.get(CONF_USERNAME)) + current_pass = current_config.get(CONF_PASSWORD, yaml_config.get(CONF_PASSWORD)) + fields[vol.Required(CONF_BROKER, default=current_broker)] = str + fields[vol.Required(CONF_PORT, default=current_port)] = vol.Coerce(int) fields[ vol.Optional( CONF_USERNAME, - description={"suggested_value": current_config.get(CONF_USERNAME)}, + description={"suggested_value": current_user}, ) ] = str fields[ vol.Optional( CONF_PASSWORD, - description={"suggested_value": current_config.get(CONF_PASSWORD)}, + description={"suggested_value": current_pass}, ) ] = str @@ -205,6 +209,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): """Manage the MQTT options.""" errors = {} current_config = self.config_entry.data + yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {}) options_config = {} if user_input is not None: bad_birth = False @@ -253,16 +258,24 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): ) return self.async_create_entry(title="", data=None) - birth = {**DEFAULT_BIRTH, **current_config.get(CONF_BIRTH_MESSAGE, {})} - will = {**DEFAULT_WILL, **current_config.get(CONF_WILL_MESSAGE, {})} + birth = { + **DEFAULT_BIRTH, + **current_config.get( + CONF_BIRTH_MESSAGE, yaml_config.get(CONF_BIRTH_MESSAGE, {}) + ), + } + will = { + **DEFAULT_WILL, + **current_config.get( + CONF_WILL_MESSAGE, yaml_config.get(CONF_WILL_MESSAGE, {}) + ), + } + discovery = current_config.get( + CONF_DISCOVERY, yaml_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY) + ) fields = OrderedDict() - fields[ - vol.Optional( - CONF_DISCOVERY, - default=current_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY), - ) - ] = bool + fields[vol.Optional(CONF_DISCOVERY, default=discovery)] = bool # Birth message is disabled if CONF_BIRTH_MESSAGE = {} fields[ diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 7ea6d9d348b..5ab3f756311 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -17,6 +17,8 @@ CONF_RETAIN = ATTR_RETAIN CONF_STATE_TOPIC = "state_topic" CONF_WILL_MESSAGE = "will_message" +DATA_MQTT_CONFIG = "mqtt_config" + DEFAULT_PREFIX = "homeassistant" DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status" DEFAULT_DISCOVERY = False From 57b7ed6a07b1adb4c520181e7717da5597f32973 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 19 Sep 2020 12:14:51 +0200 Subject: [PATCH 138/185] Correct modbus switch to return correct coil (#40190) --- homeassistant/components/modbus/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 8037d926ef1..c238e105659 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -171,7 +171,7 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity): return self._available = True - return bool(result.bits[0]) + return bool(result.bits[coil]) def _write_coil(self, coil, value): """Write coil using the Modbus hub slave.""" From 1eb8035122c170beb0d144980b5688e35ab54984 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Fri, 18 Sep 2020 10:29:26 -0300 Subject: [PATCH 139/185] Handle an unsupported device in the Broadlink config flow (#40242) --- .../components/broadlink/config_flow.py | 16 ++++++++- .../components/broadlink/strings.json | 1 + .../components/broadlink/translations/en.json | 1 + tests/components/broadlink/__init__.py | 10 ++++++ .../components/broadlink/test_config_flow.py | 35 +++++++++++++++++++ 5 files changed, 62 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index 284a9bffe19..4dfc80c6fe9 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -11,7 +11,7 @@ from broadlink.exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE from homeassistant.helpers import config_validation as cv @@ -20,6 +20,7 @@ from .const import ( # pylint: disable=unused-import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN, + DOMAINS_AND_TYPES, ) from .helpers import format_mac @@ -36,6 +37,19 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_set_device(self, device, raise_on_progress=True): """Define a device for the config flow.""" + supported_types = { + device_type + for _, device_types in DOMAINS_AND_TYPES + for device_type in device_types + } + if device.type not in supported_types: + LOGGER.error( + "Unsupported device: %s. If it worked before, please open " + "an issue at https://github.com/home-assistant/core/issues", + hex(device.devtype), + ) + raise data_entry_flow.AbortFlow("not_supported") + await self.async_set_unique_id( device.mac.hex(), raise_on_progress=raise_on_progress ) diff --git a/homeassistant/components/broadlink/strings.json b/homeassistant/components/broadlink/strings.json index 44cb1801ede..d17c639469a 100644 --- a/homeassistant/components/broadlink/strings.json +++ b/homeassistant/components/broadlink/strings.json @@ -35,6 +35,7 @@ "already_in_progress": "There is already a configuration flow in progress for this device", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_host": "Invalid hostname or IP address", + "not_supported": "Device not supported", "unknown": "[%key:common::config_flow::error::unknown%]" }, "error": { diff --git a/homeassistant/components/broadlink/translations/en.json b/homeassistant/components/broadlink/translations/en.json index fa3feb88008..bd8dfd0c403 100644 --- a/homeassistant/components/broadlink/translations/en.json +++ b/homeassistant/components/broadlink/translations/en.json @@ -5,6 +5,7 @@ "already_in_progress": "There is already a configuration flow in progress for this device", "cannot_connect": "Failed to connect", "invalid_host": "Invalid hostname or IP address", + "not_supported": "Device not supported", "unknown": "Unexpected error" }, "error": { diff --git a/tests/components/broadlink/__init__.py b/tests/components/broadlink/__init__.py index 95a2ef97c6f..86756c922f1 100644 --- a/tests/components/broadlink/__init__.py +++ b/tests/components/broadlink/__init__.py @@ -56,6 +56,16 @@ BROADLINK_DEVICES = { 20025, 5, ), + "Kitchen": ( # Not supported. + "192.168.0.64", + "34ea34b61d2c", + "LB1", + "Broadlink", + "SmartBulb", + 0x504E, + 57, + 5, + ), } diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py index 4089c551ff5..920ff8be1a6 100644 --- a/tests/components/broadlink/test_config_flow.py +++ b/tests/components/broadlink/test_config_flow.py @@ -172,6 +172,25 @@ async def test_flow_user_device_not_found(hass): assert result["errors"] == {"base": "cannot_connect"} +async def test_flow_user_device_not_supported(hass): + """Test we handle a device not supported in the user step.""" + device = get_device("Kitchen") + mock_api = device.get_mock_api() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": device.host}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_supported" + + async def test_flow_user_network_unreachable(hass): """Test we handle a network unreachable in the user step.""" result = await hass.config_entries.flow.async_init( @@ -631,6 +650,22 @@ async def test_flow_import_device_not_found(hass): assert result["reason"] == "cannot_connect" +async def test_flow_import_device_not_supported(hass): + """Test we handle a device not supported in the import step.""" + device = get_device("Kitchen") + mock_api = device.get_mock_api() + + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"host": device.host}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_supported" + + async def test_flow_import_invalid_ip_address(hass): """Test we handle an invalid IP address in the import step.""" with patch("broadlink.discover", side_effect=OSError(errno.EINVAL, None)): From e582caccc999fa56885dca80d2267ab20755d403 Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 18 Sep 2020 18:30:46 +0300 Subject: [PATCH 140/185] Fix Kodi discovery title (#40247) --- homeassistant/components/kodi/config_flow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index f10dcbb2d28..067ee8be476 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -116,6 +116,9 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } ) + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context.update({"title_placeholders": {CONF_NAME: self._name}}) + try: await validate_http(self.hass, self._get_data()) await validate_ws(self.hass, self._get_data()) @@ -129,8 +132,6 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") return self.async_abort(reason="unknown") - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - self.context.update({"title_placeholders": {CONF_NAME: self._name}}) return await self.async_step_discovery_confirm() async def async_step_discovery_confirm(self, user_input=None): From 7e6d64a24ccff7ec23cdb60c3eefb2c46daf81cd Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Fri, 18 Sep 2020 08:39:12 -0700 Subject: [PATCH 141/185] Fix high CPU usage in vera integration. (#40249) --- homeassistant/components/vera/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index a6afcce65b3..b41d289e6b3 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -3,6 +3,6 @@ "name": "Vera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/vera", - "requirements": ["pyvera==0.3.9"], + "requirements": ["pyvera==0.3.10"], "codeowners": ["@vangorra"] } diff --git a/requirements_all.txt b/requirements_all.txt index cc83c47950f..dee5b04c41c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1825,7 +1825,7 @@ pyuptimerobot==0.0.5 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.3.9 +pyvera==0.3.10 # homeassistant.components.versasense pyversasense==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 84f2f0c9a64..b581c2b6597 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -854,7 +854,7 @@ pytraccar==0.9.0 pytradfri[async]==7.0.2 # homeassistant.components.vera -pyvera==0.3.9 +pyvera==0.3.10 # homeassistant.components.vesync pyvesync==1.1.0 From ab8ef1c9e1c14208daa50f453bacdb9c25b24631 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 18 Sep 2020 19:12:10 +0200 Subject: [PATCH 142/185] Updated frontend to 20200918.0 (#40253) --- 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 96d2a51cb09..4368bff8d0f 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==20200917.1"], + "requirements": ["home-assistant-frontend==20200918.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e5978245bac..0f88f88469b 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.37.0 -home-assistant-frontend==20200917.1 +home-assistant-frontend==20200918.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index dee5b04c41c..bb75fe2dc67 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200917.1 +home-assistant-frontend==20200918.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b581c2b6597..7862baba5f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200917.1 +home-assistant-frontend==20200918.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 9955e7e5e18e71f9d080e3cfb9670fd047d72cbe Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Fri, 18 Sep 2020 11:43:36 -0700 Subject: [PATCH 143/185] Bump hangups to 0.4.11 (#40258) --- homeassistant/components/hangouts/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hangouts/manifest.json b/homeassistant/components/hangouts/manifest.json index 80eed48cde9..a2605124dc4 100644 --- a/homeassistant/components/hangouts/manifest.json +++ b/homeassistant/components/hangouts/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hangouts", "requirements": [ - "hangups==0.4.10" + "hangups==0.4.11" ], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index bb75fe2dc67..79d18146703 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -717,7 +717,7 @@ ha-philipsjs==0.0.8 habitipy==0.2.0 # homeassistant.components.hangouts -hangups==0.4.10 +hangups==0.4.11 # homeassistant.components.cloud hass-nabucasa==0.37.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7862baba5f4..ce76b34c595 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -349,7 +349,7 @@ griddypower==0.1.0 ha-ffmpeg==2.0 # homeassistant.components.hangouts -hangups==0.4.10 +hangups==0.4.11 # homeassistant.components.cloud hass-nabucasa==0.37.0 From 68cc34df6faac0e32a5338d8b35ac8b4cebc7ef9 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 19 Sep 2020 12:39:02 -0400 Subject: [PATCH 144/185] Update ZHA dependencies (#40283) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index c6b0fa78799..b03d4afd971 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -8,8 +8,8 @@ "pyserial==3.4", "zha-quirks==0.0.44", "zigpy-cc==0.5.2", - "zigpy-deconz==0.9.2", - "zigpy==0.23.2", + "zigpy-deconz==0.10.0", + "zigpy==0.24.1", "zigpy-xbee==0.13.0", "zigpy-zigate==0.6.2", "zigpy-znp==0.1.1" diff --git a/requirements_all.txt b/requirements_all.txt index 79d18146703..6aea10a43cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2302,7 +2302,7 @@ ziggo-mediabox-xl==1.1.0 zigpy-cc==0.5.2 # homeassistant.components.zha -zigpy-deconz==0.9.2 +zigpy-deconz==0.10.0 # homeassistant.components.zha zigpy-xbee==0.13.0 @@ -2314,7 +2314,7 @@ zigpy-zigate==0.6.2 zigpy-znp==0.1.1 # homeassistant.components.zha -zigpy==0.23.2 +zigpy==0.24.1 # homeassistant.components.zoneminder zm-py==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce76b34c595..d5bd1f036b2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1062,7 +1062,7 @@ zha-quirks==0.0.44 zigpy-cc==0.5.2 # homeassistant.components.zha -zigpy-deconz==0.9.2 +zigpy-deconz==0.10.0 # homeassistant.components.zha zigpy-xbee==0.13.0 @@ -1074,4 +1074,4 @@ zigpy-zigate==0.6.2 zigpy-znp==0.1.1 # homeassistant.components.zha -zigpy==0.23.2 +zigpy==0.24.1 From 85ae63c6560923f73f149e94a03cb2b38fa14992 Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Sat, 19 Sep 2020 13:08:14 +0200 Subject: [PATCH 145/185] Fix error creating duplicate ConfigEntry upon import for rfxtrx (#40296) --- homeassistant/components/rfxtrx/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 287e1ec4baf..596f1d0b5e9 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -20,4 +20,5 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if entry and import_config.items() != entry.data.items(): self.hass.config_entries.async_update_entry(entry, data=import_config) return self.async_abort(reason="already_configured") + self._abort_if_unique_id_configured() return self.async_create_entry(title="RFXTRX", data=import_config) From a8b6464d7f90bd478ae2059bebe5d7aa258c7a88 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 19 Sep 2020 20:13:52 +0000 Subject: [PATCH 146/185] Bumped version to 0.115.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 79d91ef7cb2..818f4a715a4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -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) From cf81a5c09ac5456bb9fb1b0815689df07ee6c427 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Fri, 11 Sep 2020 22:00:28 -0300 Subject: [PATCH 147/185] Improve tests for Broadlink config flow (#39894) --- .../components/broadlink/test_config_flow.py | 83 ++++++++++--------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py index 920ff8be1a6..a7660b03da5 100644 --- a/tests/components/broadlink/test_config_flow.py +++ b/tests/components/broadlink/test_config_flow.py @@ -12,13 +12,16 @@ from . import get_device from tests.async_mock import call, patch +DEVICE_DISCOVERY = "homeassistant.components.broadlink.config_flow.blk.discover" +DEVICE_FACTORY = "homeassistant.components.broadlink.config_flow.blk.gendevice" + @pytest.fixture(autouse=True) def broadlink_setup_fixture(): """Mock broadlink entry setup.""" with patch( - "homeassistant.components.broadlink.async_setup_entry", return_value=True - ): + "homeassistant.components.broadlink.async_setup", return_value=True + ), patch("homeassistant.components.broadlink.async_setup_entry", return_value=True): yield @@ -38,7 +41,7 @@ async def test_flow_user_works(hass): assert result["step_id"] == "user" assert result["errors"] == {} - with patch("broadlink.discover", return_value=[mock_api]) as mock_discover: + with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -69,7 +72,7 @@ async def test_flow_user_already_in_progress(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[device.get_mock_api()]): + with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -79,7 +82,7 @@ async def test_flow_user_already_in_progress(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[device.get_mock_api()]): + with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -106,7 +109,7 @@ async def test_flow_user_mac_already_configured(hass): device.timeout = 20 mock_api = device.get_mock_api() - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -125,7 +128,7 @@ async def test_flow_user_invalid_ip_address(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", side_effect=OSError(errno.EINVAL, None)): + with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.EINVAL, None)): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "0.0.0.1"}, @@ -142,7 +145,7 @@ async def test_flow_user_invalid_hostname(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", side_effect=OSError(socket.EAI_NONAME, None)): + with patch(DEVICE_DISCOVERY, side_effect=OSError(socket.EAI_NONAME, None)): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "pancakemaster.local"}, @@ -161,7 +164,7 @@ async def test_flow_user_device_not_found(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[]): + with patch(DEVICE_DISCOVERY, return_value=[]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host}, @@ -197,7 +200,7 @@ async def test_flow_user_network_unreachable(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", side_effect=OSError(errno.ENETUNREACH, None)): + with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.ENETUNREACH, None)): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "192.168.1.32"}, @@ -214,7 +217,7 @@ async def test_flow_user_os_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", side_effect=OSError()): + with patch(DEVICE_DISCOVERY, side_effect=OSError()): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "192.168.1.32"}, @@ -235,7 +238,7 @@ async def test_flow_auth_authentication_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -256,7 +259,7 @@ async def test_flow_auth_device_offline(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host}, @@ -277,7 +280,7 @@ async def test_flow_auth_firmware_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host}, @@ -298,7 +301,7 @@ async def test_flow_auth_network_unreachable(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host}, @@ -319,7 +322,7 @@ async def test_flow_auth_os_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host}, @@ -340,13 +343,13 @@ async def test_flow_reset_works(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, ) - with patch("broadlink.discover", return_value=[device.get_mock_api()]): + with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -372,7 +375,7 @@ async def test_flow_unlock_works(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -411,7 +414,7 @@ async def test_flow_unlock_device_offline(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -438,7 +441,7 @@ async def test_flow_unlock_firmware_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -465,7 +468,7 @@ async def test_flow_unlock_network_unreachable(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -492,7 +495,7 @@ async def test_flow_unlock_os_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -518,7 +521,7 @@ async def test_flow_do_not_unlock(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -546,7 +549,7 @@ async def test_flow_import_works(hass): device = get_device("Living Room") mock_api = device.get_mock_api() - with patch("broadlink.discover", return_value=[mock_api]) as mock_discover: + with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -577,12 +580,12 @@ async def test_flow_import_already_in_progress(hass): device = get_device("Living Room") data = {"host": device.host} - with patch("broadlink.discover", return_value=[device.get_mock_api()]): + with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=data ) - with patch("broadlink.discover", return_value=[device.get_mock_api()]): + with patch(DEVICE_DISCOVERY, return_value=[device.get_mock_api()]): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=data ) @@ -598,7 +601,7 @@ async def test_flow_import_host_already_configured(hass): mock_entry.add_to_hass(hass) mock_api = device.get_mock_api() - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -621,7 +624,7 @@ async def test_flow_import_mac_already_configured(hass): device.host = "192.168.1.16" mock_api = device.get_mock_api() - with patch("broadlink.discover", return_value=[mock_api]): + with patch(DEVICE_DISCOVERY, return_value=[mock_api]): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -639,7 +642,7 @@ async def test_flow_import_mac_already_configured(hass): async def test_flow_import_device_not_found(hass): """Test we handle a device not found in the import step.""" - with patch("broadlink.discover", return_value=[]): + with patch(DEVICE_DISCOVERY, return_value=[]): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -668,7 +671,7 @@ async def test_flow_import_device_not_supported(hass): async def test_flow_import_invalid_ip_address(hass): """Test we handle an invalid IP address in the import step.""" - with patch("broadlink.discover", side_effect=OSError(errno.EINVAL, None)): + with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.EINVAL, None)): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -681,7 +684,7 @@ async def test_flow_import_invalid_ip_address(hass): async def test_flow_import_invalid_hostname(hass): """Test we handle an invalid hostname in the import step.""" - with patch("broadlink.discover", side_effect=OSError(socket.EAI_NONAME, None)): + with patch(DEVICE_DISCOVERY, side_effect=OSError(socket.EAI_NONAME, None)): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -694,7 +697,7 @@ async def test_flow_import_invalid_hostname(hass): async def test_flow_import_network_unreachable(hass): """Test we handle a network unreachable in the import step.""" - with patch("broadlink.discover", side_effect=OSError(errno.ENETUNREACH, None)): + with patch(DEVICE_DISCOVERY, side_effect=OSError(errno.ENETUNREACH, None)): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -707,7 +710,7 @@ async def test_flow_import_network_unreachable(hass): async def test_flow_import_os_error(hass): """Test we handle an OS error in the import step.""" - with patch("broadlink.discover", side_effect=OSError()): + with patch(DEVICE_DISCOVERY, side_effect=OSError()): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -727,7 +730,7 @@ async def test_flow_reauth_works(hass): mock_api.auth.side_effect = blke.AuthenticationError() data = {"name": device.name, **device.get_entry_data()} - with patch("broadlink.gendevice", return_value=mock_api): + with patch(DEVICE_FACTORY, return_value=mock_api): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "reauth"}, data=data ) @@ -737,7 +740,7 @@ async def test_flow_reauth_works(hass): mock_api = device.get_mock_api() - with patch("broadlink.discover", return_value=[mock_api]) as mock_discover: + with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -763,7 +766,7 @@ async def test_flow_reauth_invalid_host(hass): mock_api.auth.side_effect = blke.AuthenticationError() data = {"name": device.name, **device.get_entry_data()} - with patch("broadlink.gendevice", return_value=mock_api): + with patch(DEVICE_FACTORY, return_value=mock_api): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "reauth"}, data=data ) @@ -771,7 +774,7 @@ async def test_flow_reauth_invalid_host(hass): device.mac = get_device("Office").mac mock_api = device.get_mock_api() - with patch("broadlink.discover", return_value=[mock_api]) as mock_discover: + with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, @@ -797,7 +800,7 @@ async def test_flow_reauth_valid_host(hass): mock_api.auth.side_effect = blke.AuthenticationError() data = {"name": device.name, **device.get_entry_data()} - with patch("broadlink.gendevice", return_value=mock_api): + with patch(DEVICE_FACTORY, return_value=mock_api): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "reauth"}, data=data ) @@ -805,7 +808,7 @@ async def test_flow_reauth_valid_host(hass): device.host = "192.168.1.128" mock_api = device.get_mock_api() - with patch("broadlink.discover", return_value=[mock_api]) as mock_discover: + with patch(DEVICE_DISCOVERY, return_value=[mock_api]) as mock_discover: result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": device.host, "timeout": device.timeout}, From a5f00d1db26db6a9ac0f06950dfce78ea9d99cc5 Mon Sep 17 00:00:00 2001 From: Harrison Pace Date: Tue, 22 Sep 2020 17:01:58 +1000 Subject: [PATCH 148/185] Use Cloud State as alternative state if condition unknown (#37121) --- homeassistant/components/bom/weather.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bom/weather.py b/homeassistant/components/bom/weather.py index 94b9960c851..9229d0c11d4 100644 --- a/homeassistant/components/bom/weather.py +++ b/homeassistant/components/bom/weather.py @@ -54,7 +54,9 @@ class BOMWeather(WeatherEntity): @property def condition(self): """Return the current condition.""" - return self.bom_data.get_reading("weather") + return self.bom_data.get_reading("weather") or self.bom_data.get_reading( + "cloud" + ) # Now implement the WeatherEntity interface From 3efda8d1d2fdeb6e6b9895846951e030a2aacc2e Mon Sep 17 00:00:00 2001 From: Markus Haack Date: Wed, 23 Sep 2020 04:04:01 +0200 Subject: [PATCH 149/185] Guard SolarEdge for inverters without batteries (#40295) --- homeassistant/components/solaredge/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 3888b8bf536..2b085d1ba40 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -267,7 +267,8 @@ class SolarEdgeStorageLevelSensor(SolarEdgeSensor): """Get the latest inventory data and update state and attributes.""" self.data_service.update() attr = self.data_service.attributes.get(self._json_key) - self._state = attr["soc"] + if attr and "soc" in attr: + self._state = attr["soc"] class SolarEdgeDataService: From f1ee7fed4c5bcc75b02d818a529526e29ee40897 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 21 Sep 2020 09:26:24 +0800 Subject: [PATCH 150/185] Ignore packets with missing dts in peek_first_pts (#40299) --- homeassistant/components/stream/worker.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index b76896b815a..40231d87a53 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -113,7 +113,11 @@ def _stream_worker_internal(hass, stream, quit_event): # Get to first video keyframe while first_packet[video_stream] is None: packet = next(container.demux()) - if packet.stream == video_stream and packet.is_keyframe: + if ( + packet.stream == video_stream + and packet.is_keyframe + and packet.dts is not None + ): first_packet[video_stream] = packet initial_packets.append(packet) # Get first_pts from subsequent frame to first keyframe @@ -121,6 +125,8 @@ def _stream_worker_internal(hass, stream, quit_event): [pts is None for pts in {**first_packet, **first_pts}.values()] ) and (len(initial_packets) < PACKETS_TO_WAIT_FOR_AUDIO): packet = next(container.demux((video_stream, audio_stream))) + if packet.dts is None: + continue # Discard packets with no dts if ( first_packet[packet.stream] is None ): # actually video already found above so only for audio From b3e2426967a96bb194dfcbd514590c0d05b63927 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 22 Sep 2020 10:57:26 +0200 Subject: [PATCH 151/185] Axis - Fix list applications breaks if empty response (#40360) --- homeassistant/components/axis/manifest.json | 8 ++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index ceb926f326e..959d53a01ae 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -3,11 +3,11 @@ "name": "Axis", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/axis", - "requirements": ["axis==35"], + "requirements": ["axis==37"], "zeroconf": [ - {"type":"_axis-video._tcp.local.","macaddress":"00408C*"}, - {"type":"_axis-video._tcp.local.","macaddress":"ACCC8E*"}, - {"type":"_axis-video._tcp.local.","macaddress":"B8A44F*"} + { "type": "_axis-video._tcp.local.", "macaddress": "00408C*" }, + { "type": "_axis-video._tcp.local.", "macaddress": "ACCC8E*" }, + { "type": "_axis-video._tcp.local.", "macaddress": "B8A44F*" } ], "after_dependencies": ["mqtt"], "codeowners": ["@Kane610"] diff --git a/requirements_all.txt b/requirements_all.txt index 6aea10a43cf..84a213a71c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -309,7 +309,7 @@ av==8.0.2 avri-api==0.1.7 # homeassistant.components.axis -axis==35 +axis==37 # homeassistant.components.azure_event_hub azure-eventhub==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d5bd1f036b2..381a9e71817 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -174,7 +174,7 @@ av==8.0.2 avri-api==0.1.7 # homeassistant.components.axis -axis==35 +axis==37 # homeassistant.components.azure_event_hub azure-eventhub==5.1.0 From babaf48867b529ded74a64a0725720acb6f69d86 Mon Sep 17 00:00:00 2001 From: Michael Thingnes Date: Mon, 21 Sep 2020 09:56:04 +1200 Subject: [PATCH 152/185] Fix Met.no missing conditions in API forecasts (#40373) --- homeassistant/components/met/weather.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 3355c497aab..3abd7638516 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -214,8 +214,9 @@ class MetWeather(CoordinatorEntity, WeatherEntity): ha_item = { k: met_item[v] for k, v in FORECAST_MAP.items() if met_item.get(v) } - ha_item[ATTR_FORECAST_CONDITION] = format_condition( - ha_item[ATTR_FORECAST_CONDITION] - ) + if ha_item.get(ATTR_FORECAST_CONDITION): + ha_item[ATTR_FORECAST_CONDITION] = format_condition( + ha_item[ATTR_FORECAST_CONDITION] + ) ha_forecast.append(ha_item) return ha_forecast From 1901cc962e6b151312482d9ff6fa30a27709308f Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Mon, 21 Sep 2020 16:40:29 -0400 Subject: [PATCH 153/185] Bump pyinsteon to 1.0.8 (#40383) --- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 871629b6877..d20f56054b3 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -2,7 +2,7 @@ "domain": "insteon", "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", - "requirements": ["pyinsteon==1.0.7"], + "requirements": ["pyinsteon==1.0.8"], "codeowners": ["@teharris1"], "config_flow": true } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 84a213a71c3..61129b6e4a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1401,7 +1401,7 @@ pyialarm==0.3 pyicloud==0.9.7 # homeassistant.components.insteon -pyinsteon==1.0.7 +pyinsteon==1.0.8 # homeassistant.components.intesishome pyintesishome==1.7.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 381a9e71817..2cd39e7d9c5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -674,7 +674,7 @@ pyhomematic==0.1.68 pyicloud==0.9.7 # homeassistant.components.insteon -pyinsteon==1.0.7 +pyinsteon==1.0.8 # homeassistant.components.ipma pyipma==2.0.5 From b1ac9205057170882460e2da46f954af5b0106b2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 21 Sep 2020 21:10:02 +0200 Subject: [PATCH 154/185] Fix OSError (#40393) --- homeassistant/components/shelly/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 83d5d7b9f3a..c3b701449c2 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -86,7 +86,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): try: async with async_timeout.timeout(5): return await self.device.update() - except aiocoap_error.Error as err: + except (aiocoap_error.Error, OSError) as err: raise update_coordinator.UpdateFailed("Error fetching data") from err @property From 44be19037875fe1573272f1667aa0cb021d388b6 Mon Sep 17 00:00:00 2001 From: On Freund Date: Mon, 21 Sep 2020 21:27:00 +0300 Subject: [PATCH 155/185] Fix handling of empty ws port (#40399) --- homeassistant/components/kodi/config_flow.py | 4 ++ tests/components/kodi/test_config_flow.py | 45 ++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index 067ee8be476..c11255aba87 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -202,6 +202,10 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: self._ws_port = user_input.get(CONF_WS_PORT) + # optional ints return 0 rather than None when empty + if self._ws_port == 0: + self._ws_port = None + try: await validate_ws(self.hass, self._get_data()) except WSCannotConnect: diff --git a/tests/components/kodi/test_config_flow.py b/tests/components/kodi/test_config_flow.py index 4fd61ede8ba..71c2bce1307 100644 --- a/tests/components/kodi/test_config_flow.py +++ b/tests/components/kodi/test_config_flow.py @@ -165,6 +165,51 @@ async def test_form_valid_ws_port(hass, user_flow): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_empty_ws_port(hass, user_flow): + """Test we handle an empty websocket port input.""" + with patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, + ), patch.object( + MockWSConnection, + "connect", + AsyncMock(side_effect=CannotConnectError), + ), patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + new=get_kodi_connection, + ): + result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) + + assert result["type"] == "form" + assert result["step_id"] == "ws_port" + assert result["errors"] == {} + + with patch( + "homeassistant.components.kodi.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.kodi.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"ws_port": 0} + ) + + assert result["type"] == "create_entry" + assert result["title"] == TEST_HOST["host"] + assert result["data"] == { + **TEST_HOST, + "ws_port": None, + "password": None, + "username": None, + "name": None, + "timeout": DEFAULT_TIMEOUT, + } + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_form_invalid_auth(hass, user_flow): """Test we handle invalid auth.""" with patch( From ed17a81f501136a4b033d8ecc86fdfbcc45be94c Mon Sep 17 00:00:00 2001 From: Michael Thingnes Date: Tue, 22 Sep 2020 07:56:08 +1200 Subject: [PATCH 156/185] Validate Met.no forecast entries before passing them on to HA (#40400) --- homeassistant/components/met/weather.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 3abd7638516..e4c64f9aeda 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -5,6 +5,8 @@ import voluptuous as vol from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TIME, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, @@ -209,8 +211,11 @@ class MetWeather(CoordinatorEntity, WeatherEntity): met_forecast = self.coordinator.data.hourly_forecast else: met_forecast = self.coordinator.data.daily_forecast + required_keys = {ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME} ha_forecast = [] for met_item in met_forecast: + if not set(met_item).issuperset(required_keys): + continue ha_item = { k: met_item[v] for k, v in FORECAST_MAP.items() if met_item.get(v) } From 532c624d0169c263dd20bd46d4c99fba0467b241 Mon Sep 17 00:00:00 2001 From: cagnulein Date: Tue, 22 Sep 2020 10:53:46 +0200 Subject: [PATCH 157/185] Fix luci device_tracker incorrectly reporting devices status (#40409) --- homeassistant/components/luci/device_tracker.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index fe64c90bf4c..40b111d0d83 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -94,7 +94,11 @@ class LuciDeviceScanner(DeviceScanner): last_results = [] for device in result: - if device.reachable: + if ( + not hasattr(self.router.router.owrt_version, "release") + or self.router.router.owrt_version.release[0] < 19 + or device.reachable + ): last_results.append(device) self.last_results = last_results From 6cccd87318c2eee0c10ce01c1d87ce2131ad9eec Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 23 Sep 2020 15:50:01 +0200 Subject: [PATCH 158/185] Make modbus switch read_coil failure resistent (#40417) * Make modbus switch read_coil failure resistent. Make sure all return paths return true/false. * Add comment how binary_sensor get its value (is_on). --- homeassistant/components/modbus/switch.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index c238e105659..fa5b42807b0 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -158,20 +158,23 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity): """Update the state of the switch.""" self._is_on = self._read_coil(self._coil) - def _read_coil(self, coil) -> Optional[bool]: + def _read_coil(self, coil) -> bool: """Read coil using the Modbus hub slave.""" try: result = self._hub.read_coils(self._slave, coil, 1) except ConnectionException: self._available = False - return + return False if isinstance(result, (ModbusException, ExceptionResponse)): self._available = False - return + return False self._available = True - return bool(result.bits[coil]) + # bits[0] select the lowest bit in result, + # is_on for a binary_sensor is true if the bit are 1 + # The other bits are not considered. + return bool(result.bits[0]) def _write_coil(self, coil, value): """Write coil using the Modbus hub slave.""" From 297e5300b4122083c45e668587db2b55b5dc63bb Mon Sep 17 00:00:00 2001 From: Pedro Lamas Date: Tue, 22 Sep 2020 09:49:44 +0100 Subject: [PATCH 159/185] Fix webostv supported features for "external_speaker" sound output (#40435) --- homeassistant/components/webostv/media_player.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index e53e3185651..66645ee1fb9 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -305,7 +305,9 @@ class LgWebOSMediaPlayerEntity(MediaPlayerEntity): """Flag media player features that are supported.""" supported = SUPPORT_WEBOSTV - if self._client.sound_output == "external_arc": + if (self._client.sound_output == "external_arc") or ( + self._client.sound_output == "external_speaker" + ): supported = supported | SUPPORT_WEBOSTV_VOLUME elif self._client.sound_output != "lineout": supported = supported | SUPPORT_WEBOSTV_VOLUME | SUPPORT_VOLUME_SET From 95e6969912b24417df2d80344323a17a07f1941b Mon Sep 17 00:00:00 2001 From: MeIchthys <10717998+meichthys@users.noreply.github.com> Date: Tue, 22 Sep 2020 04:58:51 -0400 Subject: [PATCH 160/185] Fix regression in Nextcloud component (#40438) --- homeassistant/components/nextcloud/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py index ff94fd708db..1a773040980 100644 --- a/homeassistant/components/nextcloud/__init__.py +++ b/homeassistant/components/nextcloud/__init__.py @@ -100,6 +100,7 @@ def setup(hass, config): _LOGGER.error("Nextcloud setup failed - Check configuration") hass.data[DOMAIN] = get_data_points(ncm.data) + hass.data[DOMAIN]["instance"] = conf[CONF_URL] def nextcloud_update(event_time): """Update data from nextcloud api.""" From 34c0e0f58dacb6317c1ef435cdeb84328f98ab38 Mon Sep 17 00:00:00 2001 From: square99 Date: Wed, 23 Sep 2020 00:34:23 +0900 Subject: [PATCH 161/185] Fix proxy camera conversion with PNG Alpha(RGBA) (#40446) --- homeassistant/components/proxy/camera.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 754f09fa199..c3f7151431a 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -77,6 +77,8 @@ def _precheck_image(image, opts): if imgfmt not in ("PNG", "JPEG"): _LOGGER.warning("Image is of unsupported type: %s", imgfmt) raise ValueError() + if not img.mode == "RGB": + img = img.convert("RGB") return img From 016834185ac0385efa46b8a6e4467cecfb44fa83 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 22 Sep 2020 17:36:44 +0200 Subject: [PATCH 162/185] Bump accuweather library to version 0.0.11 (#40458) --- homeassistant/components/accuweather/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index a383c49f348..6ccd6a4f10b 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -2,7 +2,7 @@ "domain": "accuweather", "name": "AccuWeather", "documentation": "https://www.home-assistant.io/integrations/accuweather/", - "requirements": ["accuweather==0.0.10"], + "requirements": ["accuweather==0.0.11"], "codeowners": ["@bieniu"], "config_flow": true, "quality_scale": "platinum" diff --git a/requirements_all.txt b/requirements_all.txt index 61129b6e4a5..c6dfacff9c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -102,7 +102,7 @@ YesssSMS==0.4.1 abodepy==1.1.0 # homeassistant.components.accuweather -accuweather==0.0.10 +accuweather==0.0.11 # homeassistant.components.mcp23017 adafruit-blinka==3.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2cd39e7d9c5..15aa3affb8c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -45,7 +45,7 @@ YesssSMS==0.4.1 abodepy==1.1.0 # homeassistant.components.accuweather -accuweather==0.0.10 +accuweather==0.0.11 # homeassistant.components.androidtv adb-shell[async]==0.2.1 From f953454374fb3d30f972096cecc3a11e414f9415 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Tue, 22 Sep 2020 16:00:27 -0700 Subject: [PATCH 163/185] Increase gogogate2 request timeout (#40461) --- homeassistant/components/gogogate2/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index a12058d38d0..893294da25e 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -3,7 +3,7 @@ "name": "Gogogate2 and iSmartGate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gogogate2", - "requirements": ["gogogate2-api==2.0.2"], + "requirements": ["gogogate2-api==2.0.3"], "codeowners": ["@vangorra"], "homekit": { "models": [ diff --git a/requirements_all.txt b/requirements_all.txt index c6dfacff9c9..3d3bd4ace9e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -669,7 +669,7 @@ glances_api==0.2.0 gntp==1.0.3 # homeassistant.components.gogogate2 -gogogate2-api==2.0.2 +gogogate2-api==2.0.3 # homeassistant.components.google google-api-python-client==1.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 15aa3affb8c..911da004e3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -334,7 +334,7 @@ gios==0.1.4 glances_api==0.2.0 # homeassistant.components.gogogate2 -gogogate2-api==2.0.2 +gogogate2-api==2.0.3 # homeassistant.components.google google-api-python-client==1.6.4 From 0ebeb161e1df3cc6d751950b384be0f3c1cc8b55 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 23 Sep 2020 09:21:57 +0200 Subject: [PATCH 164/185] Fix handling of quoted time_pattern values (#40470) Co-authored-by: J. Nick Koston Co-authored-by: Franck Nijhof --- .../homeassistant/triggers/time_pattern.py | 2 +- .../triggers/test_time_pattern.py | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant/triggers/time_pattern.py b/homeassistant/components/homeassistant/triggers/time_pattern.py index adacc939870..5f03fb593d6 100644 --- a/homeassistant/components/homeassistant/triggers/time_pattern.py +++ b/homeassistant/components/homeassistant/triggers/time_pattern.py @@ -36,7 +36,7 @@ class TimePattern: if isinstance(value, str) and value.startswith("/"): number = int(value[1:]) else: - number = int(value) + value = number = int(value) if not (0 <= number <= self.maximum): raise vol.Invalid(f"must be a value between 0 and {self.maximum}") diff --git a/tests/components/homeassistant/triggers/test_time_pattern.py b/tests/components/homeassistant/triggers/test_time_pattern.py index 0ef071aadb6..3d32748c176 100644 --- a/tests/components/homeassistant/triggers/test_time_pattern.py +++ b/tests/components/homeassistant/triggers/test_time_pattern.py @@ -1,4 +1,6 @@ """The tests for the time_pattern automation.""" +from datetime import timedelta + import pytest import voluptuous as vol @@ -123,6 +125,39 @@ async def test_if_fires_when_second_matches(hass, calls): assert len(calls) == 1 +async def test_if_fires_when_second_as_string_matches(hass, calls): + """Test for firing if seconds are matching.""" + now = dt_util.utcnow() + time_that_will_not_match_right_away = dt_util.utcnow().replace( + year=now.year + 1, second=15 + ) + 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": "time_pattern", + "hours": "*", + "minutes": "*", + "seconds": "30", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + async_fire_time_changed( + hass, time_that_will_not_match_right_away + timedelta(seconds=15) + ) + + await hass.async_block_till_done() + assert len(calls) == 1 + + async def test_if_fires_when_all_matches(hass, calls): """Test for firing if everything matches.""" now = dt_util.utcnow() From fe056f518e1f1467b74f5c34aea49cd7e0d49847 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Sep 2020 13:55:32 -0500 Subject: [PATCH 165/185] Ensure group state is recalculated when re-adding on reload (#40497) --- homeassistant/components/group/cover.py | 6 +++++- homeassistant/components/group/light.py | 7 ++++++- tests/components/group/test_light.py | 7 +++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index b0d0b8b7bd2..ab2ad65713d 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -39,7 +39,7 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, ) -from homeassistant.core import State +from homeassistant.core import CoreState, State import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change_event @@ -162,6 +162,10 @@ class CoverGroup(GroupEntity, CoverEntity): self.hass, self._entities, self._update_supported_features_event ) ) + + if self.hass.state == CoreState.running: + await self.async_update() + return await super().async_added_to_hass() @property diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 289bb8df3f0..007e05edfbb 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -36,7 +36,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import State +from homeassistant.core import CoreState, State import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -111,6 +111,11 @@ class LightGroup(GroupEntity, light.LightEntity): self.hass, self._entity_ids, async_state_changed_listener ) ) + + if self.hass.state == CoreState.running: + await self.async_update() + return + await super().async_added_to_hass() @property diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index a22c56b4bfc..ba8fecbed32 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -737,6 +737,11 @@ async def test_reload_with_base_integration_platform_not_setup(hass): }, ) await hass.async_block_till_done() + hass.states.async_set("light.master_hall_lights", STATE_ON) + hass.states.async_set("light.master_hall_lights_2", STATE_OFF) + + hass.states.async_set("light.outside_patio_lights", STATE_OFF) + hass.states.async_set("light.outside_patio_lights_2", STATE_OFF) yaml_path = path.join( _get_fixtures_base_path(), @@ -755,6 +760,8 @@ async def test_reload_with_base_integration_platform_not_setup(hass): assert hass.states.get("light.light_group") is None assert hass.states.get("light.master_hall_lights_g") is not None assert hass.states.get("light.outside_patio_lights_g") is not None + assert hass.states.get("light.master_hall_lights_g").state == STATE_ON + assert hass.states.get("light.outside_patio_lights_g").state == STATE_OFF def _get_fixtures_base_path(): From ff39cd753c415dc332d104994bc056b057459d80 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Thu, 24 Sep 2020 20:35:52 +0800 Subject: [PATCH 166/185] Disable audio in stream when audio stream profile is None (#40521) --- homeassistant/components/stream/worker.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 40231d87a53..9e036a764f8 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -77,6 +77,9 @@ def _stream_worker_internal(hass, stream, quit_event): # compatible with empty_moov and manual bitstream filters not in PyAV if container.format.name in {"hls", "mpegts"}: audio_stream = None + # Some audio streams do not have a profile and throw errors when remuxing + if audio_stream and audio_stream.profile is None: + audio_stream = None # The presentation timestamps of the first packet in each stream we receive # Use to adjust before muxing or outputting, but we don't adjust internally From a404c51797597087d3ae85c1d1d85178ed90cee9 Mon Sep 17 00:00:00 2001 From: cagnulein Date: Fri, 25 Sep 2020 09:06:24 +0200 Subject: [PATCH 167/185] Fix luci device_tracker when release is none (#40524) Co-authored-by: Martin Hjelmare --- homeassistant/components/luci/device_tracker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index 40b111d0d83..d4fb1d5f7bc 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -96,6 +96,7 @@ class LuciDeviceScanner(DeviceScanner): for device in result: if ( not hasattr(self.router.router.owrt_version, "release") + or not self.router.router.owrt_version.release or self.router.router.owrt_version.release[0] < 19 or device.reachable ): From 82c137d69b066c0814d51a77f542723cec0ab35c Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Thu, 24 Sep 2020 18:27:55 +0200 Subject: [PATCH 168/185] Increase upnp timeout from 5 seconds to 10 seconds (#40540) --- homeassistant/components/upnp/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index c4a81db1ff4..5f29043a1fe 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -67,7 +67,7 @@ class Device: """Create UPnP/IGD device.""" # build async_upnp_client requester session = async_get_clientsession(hass) - requester = AiohttpSessionRequester(session, True) + requester = AiohttpSessionRequester(session, True, 10) # create async_upnp_client device factory = UpnpFactory(requester, disable_state_variable_validation=True) From 6bd72c3ff5609cbeda99ce41c3d180a6e3ff6262 Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Thu, 24 Sep 2020 22:50:30 +0200 Subject: [PATCH 169/185] Fix connection validation during import for dsmr integration (#40548) * Close transport when equipment identifier is received * Minor fix --- homeassistant/components/dsmr/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index d0d0304a02a..724f9393fbf 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -48,9 +48,9 @@ class DSMRConnection: """Test if we can validate connection with the device.""" def update_telegram(telegram): - self._telegram = telegram - - transport.close() + if obis_ref.EQUIPMENT_IDENTIFIER in telegram: + self._telegram = telegram + transport.close() if self._host is None: reader_factory = partial( From da7f206414d2bb43e0e3ecfce5685b10fe4764eb Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 24 Sep 2020 22:43:34 +0200 Subject: [PATCH 170/185] Updated frontend to 20200918.2 (#40549) --- 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 4368bff8d0f..d0c86f2cdf5 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==20200918.0"], + "requirements": ["home-assistant-frontend==20200918.2"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0f88f88469b..0894039d99a 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.37.0 -home-assistant-frontend==20200918.0 +home-assistant-frontend==20200918.2 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 3d3bd4ace9e..1ec5be4ab3a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200918.0 +home-assistant-frontend==20200918.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 911da004e3c..d904e74a08c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200918.0 +home-assistant-frontend==20200918.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 1c8e8419b636558800b101c0ade5e379f06861d3 Mon Sep 17 00:00:00 2001 From: Kevin Cathcart Date: Fri, 25 Sep 2020 03:15:04 -0400 Subject: [PATCH 171/185] Fix bug in state trigger when using for: without to: (#40556) --- .../homeassistant/triggers/state.py | 2 +- .../homeassistant/triggers/test_state.py | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index f57db0ed56a..a7377ffe43e 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -145,7 +145,7 @@ async def async_attach_trigger( else: cur_value = new_st.attributes.get(attribute) - if CONF_TO not in config: + if CONF_FROM in config and CONF_TO not in config: return cur_value != old_value return cur_value == new_value diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index ce9ecaba1b0..990fc9cc956 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -538,6 +538,39 @@ async def test_if_fires_on_entity_change_with_for_without_to(hass, calls): assert len(calls) == 1 +async def test_if_does_not_fires_on_entity_change_with_for_without_to_2(hass, calls): + """Test for firing on entity change with for.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "state", + "entity_id": "test.entity", + "for": {"seconds": 5}, + }, + "action": {"service": "test.automation"}, + } + }, + ) + await hass.async_block_till_done() + + utcnow = dt_util.utcnow() + with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: + mock_utcnow.return_value = utcnow + + for i in range(10): + hass.states.async_set("test.entity", str(i)) + await hass.async_block_till_done() + + mock_utcnow.return_value += timedelta(seconds=1) + async_fire_time_changed(hass, mock_utcnow.return_value) + await hass.async_block_till_done() + + assert len(calls) == 0 + + async def test_if_fires_on_entity_creation_and_removal(hass, calls): """Test for firing on entity creation and removal, with to/from constraints.""" # set automations for multiple combinations to/from From 7be494f8453ab95e9441dd7e2c85fbd328dd8c31 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 25 Sep 2020 09:28:38 +0200 Subject: [PATCH 172/185] Bumped version to 0.115.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 818f4a715a4..86a73b5308e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "2" +PATCH_VERSION = "3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From c07c6ca4138d091386348106c8ade3c7ccc75541 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 26 Sep 2020 22:39:02 +0200 Subject: [PATCH 173/185] Bump aioshelly library to version 0.3.3 (#40415) --- .../components/shelly/config_flow.py | 4 ++ homeassistant/components/shelly/manifest.json | 2 +- homeassistant/components/shelly/strings.json | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/shelly/test_config_flow.py | 45 ++++++++++++++----- 6 files changed, 42 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 6446d2dd2d2..23ba5eb1228 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -57,6 +57,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): info = await self._async_get_info(host) except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" + except aioshelly.FirmwareUnsupported: + return self.async_abort(reason="unsupported_firmware") except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -128,6 +130,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.info = info = await self._async_get_info(zeroconf_info["host"]) except HTTP_CONNECT_ERRORS: return self.async_abort(reason="cannot_connect") + except aioshelly.FirmwareUnsupported: + return self.async_abort(reason="unsupported_firmware") await self.async_set_unique_id(info["mac"]) self._abort_if_unique_id_configured({CONF_HOST: zeroconf_info["host"]}) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 16467fa999c..e996e43a14a 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==0.3.2"], + "requirements": ["aioshelly==0.3.3"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], "codeowners": ["@balloob", "@bieniu"] } diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 16dc331e452..1a7c8c78189 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -24,7 +24,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "unsupported_firmware": "The device is using an unsupported firmware version." } } } diff --git a/requirements_all.txt b/requirements_all.txt index 1ec5be4ab3a..dbd5d526183 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -221,7 +221,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.shelly -aioshelly==0.3.2 +aioshelly==0.3.3 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d904e74a08c..ad5c58c73aa 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.shelly -aioshelly==0.3.2 +aioshelly==0.3.3 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 93192e89df3..e9967faee91 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -2,6 +2,7 @@ import asyncio import aiohttp +import aioshelly import pytest from homeassistant import config_entries, setup @@ -109,10 +110,7 @@ async def test_form_errors_get_info(hass, error): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "aioshelly.get_info", - side_effect=exc, - ): + with patch("aioshelly.get_info", side_effect=exc): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.1.1.1"}, @@ -134,10 +132,7 @@ async def test_form_errors_test_connection(hass, error): with patch( "aioshelly.get_info", return_value={"mac": "test-mac", "auth": False} - ), patch( - "aioshelly.Device.create", - side_effect=exc, - ): + ), patch("aioshelly.Device.create", new=AsyncMock(side_effect=exc)): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.1.1.1"}, @@ -175,6 +170,22 @@ async def test_form_already_configured(hass): assert entry.data["host"] == "1.1.1.1" +async def test_form_firmware_unsupported(hass): + """Test we abort if device firmware is unsupported.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("aioshelly.get_info", side_effect=aioshelly.FirmwareUnsupported): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "unsupported_firmware" + + @pytest.mark.parametrize( "error", [ @@ -309,12 +320,22 @@ async def test_zeroconf_already_configured(hass): assert entry.data["host"] == "1.1.1.1" +async def test_zeroconf_firmware_unsupported(hass): + """Test we abort if device firmware is unsupported.""" + with patch("aioshelly.get_info", side_effect=aioshelly.FirmwareUnsupported): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"host": "1.1.1.1", "name": "shelly1pm-12345"}, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "unsupported_firmware" + + async def test_zeroconf_cannot_connect(hass): """Test we get the form.""" - with patch( - "aioshelly.get_info", - side_effect=asyncio.TimeoutError, - ): + with patch("aioshelly.get_info", side_effect=asyncio.TimeoutError): result = await hass.config_entries.flow.async_init( DOMAIN, data={"host": "1.1.1.1", "name": "shelly1pm-12345"}, From d952a2b756a785711d13c11fbbe9cfc7149807da Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 28 Sep 2020 04:38:14 +0800 Subject: [PATCH 174/185] Create master playlist for cast (#40483) Co-authored-by: Jason Hunter --- homeassistant/components/stream/fmp4utils.py | 111 ++++++++++++++++ homeassistant/components/stream/hls.py | 126 +++++++++++-------- homeassistant/components/stream/recorder.py | 2 +- 3 files changed, 188 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index 00603807215..dc929e531c1 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -36,3 +36,114 @@ def get_m4s(segment: io.BytesIO, sequence: int) -> bytes: mfra_location = next(find_box(segment, b"mfra")) segment.seek(moof_location) return segment.read(mfra_location - moof_location) + + +def get_codec_string(segment: io.BytesIO) -> str: + """Get RFC 6381 codec string.""" + codecs = [] + + # Find moov + moov_location = next(find_box(segment, b"moov")) + + # Find tracks + for trak_location in find_box(segment, b"trak", moov_location): + # Drill down to media info + mdia_location = next(find_box(segment, b"mdia", trak_location)) + minf_location = next(find_box(segment, b"minf", mdia_location)) + stbl_location = next(find_box(segment, b"stbl", minf_location)) + stsd_location = next(find_box(segment, b"stsd", stbl_location)) + + # Get stsd box + segment.seek(stsd_location) + stsd_length = int.from_bytes(segment.read(4), byteorder="big") + segment.seek(stsd_location) + stsd_box = segment.read(stsd_length) + + # Base Codec + codec = stsd_box[20:24].decode("utf-8") + + # Handle H264 + if ( + codec in ("avc1", "avc2", "avc3", "avc4") + and stsd_length > 110 + and stsd_box[106:110] == b"avcC" + ): + profile = stsd_box[111:112].hex() + compatibility = stsd_box[112:113].hex() + level = stsd_box[113:114].hex() + codec += "." + profile + compatibility + level + + # Handle H265 + elif ( + codec in ("hev1", "hvc1") + and stsd_length > 110 + and stsd_box[106:110] == b"hvcC" + ): + tmp_byte = int.from_bytes(stsd_box[111:112], byteorder="big") + + # Profile Space + codec += "." + profile_space_map = {0: "", 1: "A", 2: "B", 3: "C"} + profile_space = tmp_byte >> 6 + codec += profile_space_map[profile_space] + general_profile_idc = tmp_byte & 31 + codec += str(general_profile_idc) + + # Compatibility + codec += "." + general_profile_compatibility = int.from_bytes( + stsd_box[112:116], byteorder="big" + ) + reverse = 0 + for i in range(0, 32): + reverse |= general_profile_compatibility & 1 + if i == 31: + break + reverse <<= 1 + general_profile_compatibility >>= 1 + codec += hex(reverse)[2:] + + # Tier Flag + if (tmp_byte & 32) >> 5 == 0: + codec += ".L" + else: + codec += ".H" + codec += str(int.from_bytes(stsd_box[122:123], byteorder="big")) + + # Constraint String + has_byte = False + constraint_string = "" + for i in range(121, 115, -1): + gci = int.from_bytes(stsd_box[i : i + 1], byteorder="big") + if gci or has_byte: + constraint_string = "." + hex(gci)[2:] + constraint_string + has_byte = True + codec += constraint_string + + # Handle Audio + elif codec == "mp4a": + oti = None + dsi = None + + # Parse ES Descriptors + oti_loc = stsd_box.find(b"\x04\x80\x80\x80") + if oti_loc > 0: + oti = stsd_box[oti_loc + 5 : oti_loc + 6].hex() + codec += f".{oti}" + + dsi_loc = stsd_box.find(b"\x05\x80\x80\x80") + if dsi_loc > 0: + dsi_length = int.from_bytes( + stsd_box[dsi_loc + 4 : dsi_loc + 5], byteorder="big" + ) + dsi_data = stsd_box[dsi_loc + 5 : dsi_loc + 5 + dsi_length] + dsi0 = int.from_bytes(dsi_data[0:1], byteorder="big") + dsi = (dsi0 & 248) >> 3 + if dsi == 31 and len(dsi_data) >= 2: + dsi1 = int.from_bytes(dsi_data[1:2], byteorder="big") + dsi = 32 + ((dsi0 & 7) << 3) + ((dsi1 & 224) >> 5) + codec += f".{dsi}" + + codecs.append(codec) + + return ",".join(codecs) diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 816d1231c4c..09729f79ada 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -1,4 +1,5 @@ """Provide functionality to stream HLS.""" +import io from typing import Callable from aiohttp import web @@ -7,7 +8,7 @@ from homeassistant.core import callback from .const import FORMAT_CONTENT_TYPE from .core import PROVIDERS, StreamOutput, StreamView -from .fmp4utils import get_init, get_m4s +from .fmp4utils import get_codec_string, get_init, get_m4s @callback @@ -16,7 +17,43 @@ def async_setup_hls(hass): hass.http.register_view(HlsPlaylistView()) hass.http.register_view(HlsSegmentView()) hass.http.register_view(HlsInitView()) - return "/api/hls/{}/playlist.m3u8" + hass.http.register_view(HlsMasterPlaylistView()) + return "/api/hls/{}/master_playlist.m3u8" + + +class HlsMasterPlaylistView(StreamView): + """Stream view used only for Chromecast compatibility.""" + + url = r"/api/hls/{token:[a-f0-9]+}/master_playlist.m3u8" + name = "api:stream:hls:master_playlist" + cors_allowed = True + + @staticmethod + def render(track): + """Render M3U8 file.""" + # Need to calculate max bandwidth as input_container.bit_rate doesn't seem to work + # Calculate file size / duration and use a multiplier to account for variation + segment = track.get_segment(track.segments[-1]) + bandwidth = round( + segment.segment.seek(0, io.SEEK_END) * 8 / segment.duration * 3 + ) + codecs = get_codec_string(segment.segment) + lines = [ + "#EXTM3U", + f'#EXT-X-STREAM-INF:BANDWIDTH={bandwidth},CODECS="{codecs}"', + "playlist.m3u8", + ] + return "\n".join(lines) + "\n" + + async def handle(self, request, stream, sequence): + """Return m3u8 playlist.""" + track = stream.add_provider("hls") + stream.start() + # Wait for a segment to be ready + if not track.segments: + await track.recv() + headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]} + return web.Response(body=self.render(track).encode("utf-8"), headers=headers) class HlsPlaylistView(StreamView): @@ -26,18 +63,50 @@ class HlsPlaylistView(StreamView): name = "api:stream:hls:playlist" cors_allowed = True + @staticmethod + def render_preamble(track): + """Render preamble.""" + return [ + "#EXT-X-VERSION:7", + f"#EXT-X-TARGETDURATION:{track.target_duration}", + '#EXT-X-MAP:URI="init.mp4"', + ] + + @staticmethod + def render_playlist(track): + """Render playlist.""" + segments = track.segments + + if not segments: + return [] + + playlist = ["#EXT-X-MEDIA-SEQUENCE:{}".format(segments[0])] + + for sequence in segments: + segment = track.get_segment(sequence) + playlist.extend( + [ + "#EXTINF:{:.04f},".format(float(segment.duration)), + f"./segment/{segment.sequence}.m4s", + ] + ) + + return playlist + + def render(self, track): + """Render M3U8 file.""" + lines = ["#EXTM3U"] + self.render_preamble(track) + self.render_playlist(track) + return "\n".join(lines) + "\n" + async def handle(self, request, stream, sequence): """Return m3u8 playlist.""" - renderer = M3U8Renderer(stream) track = stream.add_provider("hls") stream.start() # Wait for a segment to be ready if not track.segments: await track.recv() headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]} - return web.Response( - body=renderer.render(track).encode("utf-8"), headers=headers - ) + return web.Response(body=self.render(track).encode("utf-8"), headers=headers) class HlsInitView(StreamView): @@ -77,49 +146,6 @@ class HlsSegmentView(StreamView): ) -class M3U8Renderer: - """M3U8 Render Helper.""" - - def __init__(self, stream): - """Initialize renderer.""" - self.stream = stream - - @staticmethod - def render_preamble(track): - """Render preamble.""" - return [ - "#EXT-X-VERSION:7", - f"#EXT-X-TARGETDURATION:{track.target_duration}", - '#EXT-X-MAP:URI="init.mp4"', - ] - - @staticmethod - def render_playlist(track): - """Render playlist.""" - segments = track.segments - - if not segments: - return [] - - playlist = ["#EXT-X-MEDIA-SEQUENCE:{}".format(segments[0])] - - for sequence in segments: - segment = track.get_segment(sequence) - playlist.extend( - [ - "#EXTINF:{:.04f},".format(float(segment.duration)), - f"./segment/{segment.sequence}.m4s", - ] - ) - - return playlist - - def render(self, track): - """Render M3U8 file.""" - lines = ["#EXTM3U"] + self.render_preamble(track) + self.render_playlist(track) - return "\n".join(lines) + "\n" - - @PROVIDERS.register("hls") class HlsStreamOutput(StreamOutput): """Represents HLS Output formats.""" @@ -137,7 +163,7 @@ class HlsStreamOutput(StreamOutput): @property def audio_codecs(self) -> str: """Return desired audio codecs.""" - return {"aac", "ac3", "mp3"} + return {"aac", "mp3"} @property def video_codecs(self) -> tuple: diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 82b146cc51f..d0b8789f602 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -78,7 +78,7 @@ class RecorderOutput(StreamOutput): @property def audio_codecs(self) -> str: """Return desired audio codec.""" - return {"aac", "ac3", "mp3"} + return {"aac", "mp3"} @property def video_codecs(self) -> tuple: From 837c3d3c9d5d1bb259871e6835d7cb7c0d5bdf3c Mon Sep 17 00:00:00 2001 From: Colin Frei Date: Sun, 27 Sep 2020 23:19:46 +0200 Subject: [PATCH 175/185] Fix fitbit current URL not available while configuring (#40547) Co-authored-by: Franck Nijhof Co-authored-by: Paulus Schoutsen --- homeassistant/components/fitbit/sensor.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index f0914ab35f0..f6e3fd90fe5 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -185,9 +185,7 @@ def request_app_setup(hass, config, add_entities, config_path, discovery_info=No else: setup_platform(hass, config, add_entities, discovery_info) - start_url = ( - f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}" - ) + start_url = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}" description = f"""Please create a Fitbit developer app at https://dev.fitbit.com/apps/new. @@ -222,7 +220,7 @@ def request_oauth_completion(hass): def fitbit_configuration_callback(callback_data): """Handle configuration updates.""" - start_url = f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_START}" + start_url = f"{get_url(hass)}{FITBIT_AUTH_START}" description = f"Please authorize Fitbit by visiting {start_url}" @@ -314,9 +312,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): config_file.get(CONF_CLIENT_ID), config_file.get(CONF_CLIENT_SECRET) ) - redirect_uri = ( - f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}" - ) + redirect_uri = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}" fitbit_auth_start_url, _ = oauth.authorize_token_url( redirect_uri=redirect_uri, From af88a78aae778fbd43277ea0e8231f046360e384 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Fri, 25 Sep 2020 13:18:21 +0200 Subject: [PATCH 176/185] Bump Plugwise-Smile to v1.5.1 (#40572) --- homeassistant/components/plugwise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index f4cb9164e5d..222db34b344 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["Plugwise_Smile==1.4.0"], + "requirements": ["Plugwise_Smile==1.5.1"], "codeowners": ["@CoMPaTech", "@bouwew"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index dbd5d526183..6d6a1f2371b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -26,7 +26,7 @@ Mastodon.py==1.5.1 OPi.GPIO==0.4.0 # homeassistant.components.plugwise -Plugwise_Smile==1.4.0 +Plugwise_Smile==1.5.1 # homeassistant.components.essent PyEssent==0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ad5c58c73aa..1469501fd42 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ HAP-python==3.0.0 # homeassistant.components.plugwise -Plugwise_Smile==1.4.0 +Plugwise_Smile==1.5.1 # homeassistant.components.flick_electric PyFlick==0.0.2 From 59daab94346bc628f2edee374bb29ae9dc6d3c8f Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Mon, 28 Sep 2020 08:24:30 -0400 Subject: [PATCH 177/185] Fix camera play stream (#40641) Co-authored-by: Justin Wong <46082645+uvjustin@users.noreply.github.com> --- homeassistant/components/camera/__init__.py | 44 ++++++++++++++++--- homeassistant/components/cast/manifest.json | 2 +- homeassistant/components/cast/media_player.py | 5 ++- .../components/media_player/__init__.py | 2 + .../components/media_player/const.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 49 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index f6b909231ca..f4950751fc9 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -19,6 +19,7 @@ from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_EXTRA, DOMAIN as DOMAIN_MP, SERVICE_PLAY_MEDIA, ) @@ -46,7 +47,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, entity_sources from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.network import get_url from homeassistant.loader import bind_hass @@ -695,14 +696,47 @@ async def async_handle_play_stream_service(camera, service_call): options=camera.stream_options, ) data = { - ATTR_ENTITY_ID: entity_ids, ATTR_MEDIA_CONTENT_ID: f"{get_url(hass)}{url}", ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt], } - await hass.services.async_call( - DOMAIN_MP, SERVICE_PLAY_MEDIA, data, blocking=True, context=service_call.context - ) + # It is required to send a different payload for cast media players + cast_entity_ids = [ + entity + for entity, source in entity_sources(hass).items() + if entity in entity_ids and source["domain"] == "cast" + ] + other_entity_ids = list(set(entity_ids) - set(cast_entity_ids)) + + if cast_entity_ids: + await hass.services.async_call( + DOMAIN_MP, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: cast_entity_ids, + **data, + ATTR_MEDIA_EXTRA: { + "stream_type": "LIVE", + "media_info": { + "hlsVideoSegmentFormat": "fmp4", + }, + }, + }, + blocking=True, + context=service_call.context, + ) + + if other_entity_ids: + await hass.services.async_call( + DOMAIN_MP, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: other_entity_ids, + **data, + }, + blocking=True, + context=service_call.context, + ) async def async_handle_record_service(camera, call): diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 49d26431f5b..03412d3b6df 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==7.2.1"], + "requirements": ["pychromecast==7.5.0"], "after_dependencies": ["cloud", "http", "media_source", "tts", "zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index f948c51655b..788da18e8bd 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -21,6 +21,7 @@ from homeassistant.components import media_source, zeroconf from homeassistant.components.http.auth import async_sign_path from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( + ATTR_MEDIA_EXTRA, MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, @@ -574,7 +575,9 @@ class CastDevice(MediaPlayerEntity): except NotImplementedError: _LOGGER.error("App %s not supported", app_name) else: - self._chromecast.media_controller.play_media(media_id, media_type) + self._chromecast.media_controller.play_media( + media_id, media_type, **kwargs.get(ATTR_MEDIA_EXTRA, {}) + ) # ========== Properties ========== @property diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 348bc521a5a..41c1a4e8690 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -71,6 +71,7 @@ from .const import ( ATTR_MEDIA_DURATION, ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_EPISODE, + ATTR_MEDIA_EXTRA, ATTR_MEDIA_PLAYLIST, ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT, @@ -139,6 +140,7 @@ MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = { vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string, vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean, + vol.Optional(ATTR_MEDIA_EXTRA, default={}): dict, } ATTR_TO_PROPERTY = [ diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 0035fc9f4d2..3db31006341 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -12,6 +12,7 @@ ATTR_MEDIA_CONTENT_ID = "media_content_id" ATTR_MEDIA_CONTENT_TYPE = "media_content_type" ATTR_MEDIA_DURATION = "media_duration" ATTR_MEDIA_ENQUEUE = "enqueue" +ATTR_MEDIA_EXTRA = "extra" ATTR_MEDIA_EPISODE = "media_episode" ATTR_MEDIA_PLAYLIST = "media_playlist" ATTR_MEDIA_POSITION = "media_position" diff --git a/requirements_all.txt b/requirements_all.txt index 6d6a1f2371b..2606c5da079 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1265,7 +1265,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==7.2.1 +pychromecast==7.5.0 # homeassistant.components.cmus pycmus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1469501fd42..56341864a6a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -613,7 +613,7 @@ pyblackbird==0.5 pybotvac==0.0.17 # homeassistant.components.cast -pychromecast==7.2.1 +pychromecast==7.5.0 # homeassistant.components.coolmaster pycoolmasternet-async==0.1.2 From 94f3a085ed407d3f334eb0666e47dade0abadace Mon Sep 17 00:00:00 2001 From: Chris <1828125+digitallyserviced@users.noreply.github.com> Date: Sun, 27 Sep 2020 14:40:57 -0400 Subject: [PATCH 178/185] set ID3 tags as TextFrame types (#40666) --- homeassistant/components/tts/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 0f758d4a2eb..e5f762af647 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -11,6 +11,7 @@ from typing import Dict, Optional from aiohttp import web import mutagen +from mutagen.id3 import TextFrame as ID3Text import voluptuous as vol from homeassistant.components.http import HomeAssistantView @@ -467,9 +468,9 @@ class SpeechManager: try: tts_file = mutagen.File(data_bytes, easy=True) if tts_file is not None: - tts_file["artist"] = artist - tts_file["album"] = album - tts_file["title"] = message + tts_file["artist"] = ID3Text(encoding=3, text=artist) + tts_file["album"] = ID3Text(encoding=3, text=album) + tts_file["title"] = ID3Text(encoding=3, text=message) tts_file.save(data_bytes) except mutagen.MutagenError as err: _LOGGER.error("ID3 tag error: %s", err) From b01d04c4cad38c93cd4404e02481d34b78a0d8c8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 27 Sep 2020 21:51:37 +0200 Subject: [PATCH 179/185] Pin gRPC to 1.31.0 to workaround amrv7 issues (#40678) --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0894039d99a..d88ede1a1d9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,6 +39,10 @@ urllib3>=1.24.3 # Constrain httplib2 to protect against CVE-2020-11078 httplib2>=0.18.0 +# gRPC 1.32+ currently causes issues on ARMv7, see: +# https://github.com/home-assistant/core/issues/40148 +grpcio==1.31.0 + # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 27482a0c215..5e3a2d8b1f3 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -67,6 +67,10 @@ urllib3>=1.24.3 # Constrain httplib2 to protect against CVE-2020-11078 httplib2>=0.18.0 +# gRPC 1.32+ currently causes issues on ARMv7, see: +# https://github.com/home-assistant/core/issues/40148 +grpcio==1.31.0 + # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 From 48f5ffa0bd638317f363f427b631df67520f3bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Stankowski?= Date: Mon, 28 Sep 2020 11:12:35 +0200 Subject: [PATCH 180/185] Bump Airly package to 1.0.0 (#40695) --- homeassistant/components/airly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airly/manifest.json b/homeassistant/components/airly/manifest.json index 8140bc91c5f..77de843ffce 100644 --- a/homeassistant/components/airly/manifest.json +++ b/homeassistant/components/airly/manifest.json @@ -3,7 +3,7 @@ "name": "Airly", "documentation": "https://www.home-assistant.io/integrations/airly", "codeowners": ["@bieniu"], - "requirements": ["airly==0.0.2"], + "requirements": ["airly==1.0.0"], "config_flow": true, "quality_scale": "platinum" } diff --git a/requirements_all.txt b/requirements_all.txt index 2606c5da079..4d9f14ee88c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -233,7 +233,7 @@ aiounifi==23 aioymaps==1.1.0 # homeassistant.components.airly -airly==0.0.2 +airly==1.0.0 # homeassistant.components.aladdin_connect aladdin_connect==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 56341864a6a..de0240710f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -143,7 +143,7 @@ aiounifi==23 aioymaps==1.1.0 # homeassistant.components.airly -airly==0.0.2 +airly==1.0.0 # homeassistant.components.ambiclimate ambiclimate==0.2.1 From 2ee5b0a9c13ddc9c99ceb7d8c84b3b318b859d2e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 28 Sep 2020 12:29:16 +0000 Subject: [PATCH 181/185] Bumped version to 0.115.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 86a73b5308e..bac9a5c5e93 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "3" +PATCH_VERSION = "4" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From e54ec3ba05f7f9f7cc01e160043db51541e524eb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Sep 2020 10:19:26 +0200 Subject: [PATCH 182/185] Fix ID3 tagging in TTS (#40740) --- homeassistant/components/tts/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index e5f762af647..2eb12750fe8 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -466,7 +466,7 @@ class SpeechManager: artist = options.get("voice") try: - tts_file = mutagen.File(data_bytes, easy=True) + tts_file = mutagen.File(data_bytes) if tts_file is not None: tts_file["artist"] = ID3Text(encoding=3, text=artist) tts_file["album"] = ID3Text(encoding=3, text=album) From ebacc15e72bfee93e5492b2ee460d9543c908750 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Sep 2020 12:38:30 +0200 Subject: [PATCH 183/185] Bumped version to 0.115.5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index bac9a5c5e93..7e695cb28ab 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -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 c802a63700f4782eea18ddebd91256a2506e2307 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Sep 2020 10:33:56 +0200 Subject: [PATCH 184/185] Use custom app to play camera stream using Google Assistant (#40750) --- homeassistant/components/google_assistant/trait.py | 6 +++++- homeassistant/const.py | 3 +++ tests/components/google_assistant/test_smart_home.py | 1 + tests/components/google_assistant/test_trait.py | 3 ++- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 5d2d59f2144..653324758e0 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -29,6 +29,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, + CAST_APP_ID_HOMEASSISTANT, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_CUSTOM_BYPASS, SERVICE_ALARM_ARM_HOME, @@ -287,7 +288,10 @@ class CameraStreamTrait(_Trait): url = await self.hass.components.camera.async_request_stream( self.state.entity_id, "hls" ) - self.stream_info = {"cameraStreamAccessUrl": f"{get_url(self.hass)}{url}"} + self.stream_info = { + "cameraStreamAccessUrl": f"{get_url(self.hass)}{url}", + "cameraStreamReceiverAppId": CAST_APP_ID_HOMEASSISTANT, + } @register_trait diff --git a/homeassistant/const.py b/homeassistant/const.py index 7e695cb28ab..cd093b4e9fa 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -608,3 +608,6 @@ PRECISION_TENTHS = 0.1 # Static list of entities that will never be exposed to # cloud, alexa, or google_home components CLOUD_NEVER_EXPOSED_ENTITIES = ["group.all_locks"] + +# The ID of the Home Assistant Cast App +CAST_APP_ID_HOMEASSISTANT = "B12CE3CA" diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 78d403c2038..f35415ee9e4 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -976,6 +976,7 @@ async def test_trait_execute_adding_query_data(hass): "states": { "online": True, "cameraStreamAccessUrl": "https://example.com/api/streams/bla", + "cameraStreamReceiverAppId": "B12CE3CA", }, } ] diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index ac0db986f42..d415c4f2476 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -135,7 +135,8 @@ async def test_camera_stream(hass): await trt.execute(trait.COMMAND_GET_CAMERA_STREAM, BASIC_DATA, {}, {}) assert trt.query_attributes() == { - "cameraStreamAccessUrl": "https://example.com/api/streams/bla" + "cameraStreamAccessUrl": "https://example.com/api/streams/bla", + "cameraStreamReceiverAppId": "B12CE3CA", } From dbda9a520e05200d0f0729b4335673d1ef8293a6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 30 Sep 2020 08:36:30 +0000 Subject: [PATCH 185/185] Bumped version to 0.115.6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index cd093b4e9fa..3ed80859fa5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "5" +PATCH_VERSION = "6" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1)