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/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/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, 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/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/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/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: 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) 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) 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/requirements_all.txt b/requirements_all.txt index 1ec5be4ab3a..4d9f14ee88c 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 @@ -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 @@ -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 @@ -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 d904e74a08c..de0240710f3 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 @@ -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 @@ -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 @@ -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 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 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"},