Merge pull request #40706 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2020-09-28 14:58:19 +02:00 committed by GitHub
commit b4237be609
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 301 additions and 93 deletions

View File

@ -3,7 +3,7 @@
"name": "Airly", "name": "Airly",
"documentation": "https://www.home-assistant.io/integrations/airly", "documentation": "https://www.home-assistant.io/integrations/airly",
"codeowners": ["@bieniu"], "codeowners": ["@bieniu"],
"requirements": ["airly==0.0.2"], "requirements": ["airly==1.0.0"],
"config_flow": true, "config_flow": true,
"quality_scale": "platinum" "quality_scale": "platinum"
} }

View File

@ -19,6 +19,7 @@ from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_TYPE,
ATTR_MEDIA_EXTRA,
DOMAIN as DOMAIN_MP, DOMAIN as DOMAIN_MP,
SERVICE_PLAY_MEDIA, SERVICE_PLAY_MEDIA,
) )
@ -46,7 +47,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE, 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.entity_component import EntityComponent
from homeassistant.helpers.network import get_url from homeassistant.helpers.network import get_url
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
@ -695,13 +696,46 @@ async def async_handle_play_stream_service(camera, service_call):
options=camera.stream_options, options=camera.stream_options,
) )
data = { data = {
ATTR_ENTITY_ID: entity_ids,
ATTR_MEDIA_CONTENT_ID: f"{get_url(hass)}{url}", ATTR_MEDIA_CONTENT_ID: f"{get_url(hass)}{url}",
ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt], ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt],
} }
# 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( await hass.services.async_call(
DOMAIN_MP, SERVICE_PLAY_MEDIA, data, blocking=True, context=service_call.context 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,
) )

View File

@ -3,7 +3,7 @@
"name": "Google Cast", "name": "Google Cast",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast", "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"], "after_dependencies": ["cloud", "http", "media_source", "tts", "zeroconf"],
"zeroconf": ["_googlecast._tcp.local."], "zeroconf": ["_googlecast._tcp.local."],
"codeowners": ["@emontnemery"] "codeowners": ["@emontnemery"]

View File

@ -21,6 +21,7 @@ from homeassistant.components import media_source, zeroconf
from homeassistant.components.http.auth import async_sign_path from homeassistant.components.http.auth import async_sign_path
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
ATTR_MEDIA_EXTRA,
MEDIA_TYPE_MOVIE, MEDIA_TYPE_MOVIE,
MEDIA_TYPE_MUSIC, MEDIA_TYPE_MUSIC,
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_TVSHOW,
@ -574,7 +575,9 @@ class CastDevice(MediaPlayerEntity):
except NotImplementedError: except NotImplementedError:
_LOGGER.error("App %s not supported", app_name) _LOGGER.error("App %s not supported", app_name)
else: 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 ========== # ========== Properties ==========
@property @property

View File

@ -185,9 +185,7 @@ def request_app_setup(hass, config, add_entities, config_path, discovery_info=No
else: else:
setup_platform(hass, config, add_entities, discovery_info) setup_platform(hass, config, add_entities, discovery_info)
start_url = ( start_url = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}"
f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}"
)
description = f"""Please create a Fitbit developer app at description = f"""Please create a Fitbit developer app at
https://dev.fitbit.com/apps/new. https://dev.fitbit.com/apps/new.
@ -222,7 +220,7 @@ def request_oauth_completion(hass):
def fitbit_configuration_callback(callback_data): def fitbit_configuration_callback(callback_data):
"""Handle configuration updates.""" """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}" 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) config_file.get(CONF_CLIENT_ID), config_file.get(CONF_CLIENT_SECRET)
) )
redirect_uri = ( redirect_uri = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}"
f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}"
)
fitbit_auth_start_url, _ = oauth.authorize_token_url( fitbit_auth_start_url, _ = oauth.authorize_token_url(
redirect_uri=redirect_uri, redirect_uri=redirect_uri,

View File

@ -71,6 +71,7 @@ from .const import (
ATTR_MEDIA_DURATION, ATTR_MEDIA_DURATION,
ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_ENQUEUE,
ATTR_MEDIA_EPISODE, ATTR_MEDIA_EPISODE,
ATTR_MEDIA_EXTRA,
ATTR_MEDIA_PLAYLIST, ATTR_MEDIA_PLAYLIST,
ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION,
ATTR_MEDIA_POSITION_UPDATED_AT, 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_TYPE): cv.string,
vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string,
vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean, vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean,
vol.Optional(ATTR_MEDIA_EXTRA, default={}): dict,
} }
ATTR_TO_PROPERTY = [ ATTR_TO_PROPERTY = [

View File

@ -12,6 +12,7 @@ ATTR_MEDIA_CONTENT_ID = "media_content_id"
ATTR_MEDIA_CONTENT_TYPE = "media_content_type" ATTR_MEDIA_CONTENT_TYPE = "media_content_type"
ATTR_MEDIA_DURATION = "media_duration" ATTR_MEDIA_DURATION = "media_duration"
ATTR_MEDIA_ENQUEUE = "enqueue" ATTR_MEDIA_ENQUEUE = "enqueue"
ATTR_MEDIA_EXTRA = "extra"
ATTR_MEDIA_EPISODE = "media_episode" ATTR_MEDIA_EPISODE = "media_episode"
ATTR_MEDIA_PLAYLIST = "media_playlist" ATTR_MEDIA_PLAYLIST = "media_playlist"
ATTR_MEDIA_POSITION = "media_position" ATTR_MEDIA_POSITION = "media_position"

View File

@ -2,7 +2,7 @@
"domain": "plugwise", "domain": "plugwise",
"name": "Plugwise", "name": "Plugwise",
"documentation": "https://www.home-assistant.io/integrations/plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise",
"requirements": ["Plugwise_Smile==1.4.0"], "requirements": ["Plugwise_Smile==1.5.1"],
"codeowners": ["@CoMPaTech", "@bouwew"], "codeowners": ["@CoMPaTech", "@bouwew"],
"zeroconf": ["_plugwise._tcp.local."], "zeroconf": ["_plugwise._tcp.local."],
"config_flow": true "config_flow": true

View File

@ -57,6 +57,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
info = await self._async_get_info(host) info = await self._async_get_info(host)
except HTTP_CONNECT_ERRORS: except HTTP_CONNECT_ERRORS:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except aioshelly.FirmwareUnsupported:
return self.async_abort(reason="unsupported_firmware")
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" 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"]) self.info = info = await self._async_get_info(zeroconf_info["host"])
except HTTP_CONNECT_ERRORS: except HTTP_CONNECT_ERRORS:
return self.async_abort(reason="cannot_connect") 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"]) await self.async_set_unique_id(info["mac"])
self._abort_if_unique_id_configured({CONF_HOST: zeroconf_info["host"]}) self._abort_if_unique_id_configured({CONF_HOST: zeroconf_info["host"]})

View File

@ -3,7 +3,7 @@
"name": "Shelly", "name": "Shelly",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/shelly", "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*" }], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }],
"codeowners": ["@balloob", "@bieniu"] "codeowners": ["@balloob", "@bieniu"]
} }

View File

@ -24,7 +24,8 @@
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
}, },
"abort": { "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."
} }
} }
} }

View File

@ -36,3 +36,114 @@ def get_m4s(segment: io.BytesIO, sequence: int) -> bytes:
mfra_location = next(find_box(segment, b"mfra")) mfra_location = next(find_box(segment, b"mfra"))
segment.seek(moof_location) segment.seek(moof_location)
return segment.read(mfra_location - 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)

View File

@ -1,4 +1,5 @@
"""Provide functionality to stream HLS.""" """Provide functionality to stream HLS."""
import io
from typing import Callable from typing import Callable
from aiohttp import web from aiohttp import web
@ -7,7 +8,7 @@ from homeassistant.core import callback
from .const import FORMAT_CONTENT_TYPE from .const import FORMAT_CONTENT_TYPE
from .core import PROVIDERS, StreamOutput, StreamView from .core import PROVIDERS, StreamOutput, StreamView
from .fmp4utils import get_init, get_m4s from .fmp4utils import get_codec_string, get_init, get_m4s
@callback @callback
@ -16,7 +17,43 @@ def async_setup_hls(hass):
hass.http.register_view(HlsPlaylistView()) hass.http.register_view(HlsPlaylistView())
hass.http.register_view(HlsSegmentView()) hass.http.register_view(HlsSegmentView())
hass.http.register_view(HlsInitView()) 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): class HlsPlaylistView(StreamView):
@ -26,18 +63,50 @@ class HlsPlaylistView(StreamView):
name = "api:stream:hls:playlist" name = "api:stream:hls:playlist"
cors_allowed = True 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): async def handle(self, request, stream, sequence):
"""Return m3u8 playlist.""" """Return m3u8 playlist."""
renderer = M3U8Renderer(stream)
track = stream.add_provider("hls") track = stream.add_provider("hls")
stream.start() stream.start()
# Wait for a segment to be ready # Wait for a segment to be ready
if not track.segments: if not track.segments:
await track.recv() await track.recv()
headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]} headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]}
return web.Response( return web.Response(body=self.render(track).encode("utf-8"), headers=headers)
body=renderer.render(track).encode("utf-8"), headers=headers
)
class HlsInitView(StreamView): 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") @PROVIDERS.register("hls")
class HlsStreamOutput(StreamOutput): class HlsStreamOutput(StreamOutput):
"""Represents HLS Output formats.""" """Represents HLS Output formats."""
@ -137,7 +163,7 @@ class HlsStreamOutput(StreamOutput):
@property @property
def audio_codecs(self) -> str: def audio_codecs(self) -> str:
"""Return desired audio codecs.""" """Return desired audio codecs."""
return {"aac", "ac3", "mp3"} return {"aac", "mp3"}
@property @property
def video_codecs(self) -> tuple: def video_codecs(self) -> tuple:

View File

@ -78,7 +78,7 @@ class RecorderOutput(StreamOutput):
@property @property
def audio_codecs(self) -> str: def audio_codecs(self) -> str:
"""Return desired audio codec.""" """Return desired audio codec."""
return {"aac", "ac3", "mp3"} return {"aac", "mp3"}
@property @property
def video_codecs(self) -> tuple: def video_codecs(self) -> tuple:

View File

@ -11,6 +11,7 @@ from typing import Dict, Optional
from aiohttp import web from aiohttp import web
import mutagen import mutagen
from mutagen.id3 import TextFrame as ID3Text
import voluptuous as vol import voluptuous as vol
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
@ -467,9 +468,9 @@ class SpeechManager:
try: try:
tts_file = mutagen.File(data_bytes, easy=True) tts_file = mutagen.File(data_bytes, easy=True)
if tts_file is not None: if tts_file is not None:
tts_file["artist"] = artist tts_file["artist"] = ID3Text(encoding=3, text=artist)
tts_file["album"] = album tts_file["album"] = ID3Text(encoding=3, text=album)
tts_file["title"] = message tts_file["title"] = ID3Text(encoding=3, text=message)
tts_file.save(data_bytes) tts_file.save(data_bytes)
except mutagen.MutagenError as err: except mutagen.MutagenError as err:
_LOGGER.error("ID3 tag error: %s", err) _LOGGER.error("ID3 tag error: %s", err)

View File

@ -1,7 +1,7 @@
"""Constants used by Home Assistant components.""" """Constants used by Home Assistant components."""
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 115 MINOR_VERSION = 115
PATCH_VERSION = "3" PATCH_VERSION = "4"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7, 1) REQUIRED_PYTHON_VER = (3, 7, 1)

View File

@ -39,6 +39,10 @@ urllib3>=1.24.3
# Constrain httplib2 to protect against CVE-2020-11078 # Constrain httplib2 to protect against CVE-2020-11078
httplib2>=0.18.0 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 # This is a old unmaintained library and is replaced with pycryptodome
pycrypto==1000000000.0.0 pycrypto==1000000000.0.0

View File

@ -26,7 +26,7 @@ Mastodon.py==1.5.1
OPi.GPIO==0.4.0 OPi.GPIO==0.4.0
# homeassistant.components.plugwise # homeassistant.components.plugwise
Plugwise_Smile==1.4.0 Plugwise_Smile==1.5.1
# homeassistant.components.essent # homeassistant.components.essent
PyEssent==0.13 PyEssent==0.13
@ -221,7 +221,7 @@ aiopvpc==2.0.2
aiopylgtv==0.3.3 aiopylgtv==0.3.3
# homeassistant.components.shelly # homeassistant.components.shelly
aioshelly==0.3.2 aioshelly==0.3.3
# homeassistant.components.switcher_kis # homeassistant.components.switcher_kis
aioswitcher==1.2.1 aioswitcher==1.2.1
@ -233,7 +233,7 @@ aiounifi==23
aioymaps==1.1.0 aioymaps==1.1.0
# homeassistant.components.airly # homeassistant.components.airly
airly==0.0.2 airly==1.0.0
# homeassistant.components.aladdin_connect # homeassistant.components.aladdin_connect
aladdin_connect==0.3 aladdin_connect==0.3
@ -1265,7 +1265,7 @@ pycfdns==0.0.1
pychannels==1.0.0 pychannels==1.0.0
# homeassistant.components.cast # homeassistant.components.cast
pychromecast==7.2.1 pychromecast==7.5.0
# homeassistant.components.cmus # homeassistant.components.cmus
pycmus==0.1.1 pycmus==0.1.1

View File

@ -7,7 +7,7 @@
HAP-python==3.0.0 HAP-python==3.0.0
# homeassistant.components.plugwise # homeassistant.components.plugwise
Plugwise_Smile==1.4.0 Plugwise_Smile==1.5.1
# homeassistant.components.flick_electric # homeassistant.components.flick_electric
PyFlick==0.0.2 PyFlick==0.0.2
@ -131,7 +131,7 @@ aiopvpc==2.0.2
aiopylgtv==0.3.3 aiopylgtv==0.3.3
# homeassistant.components.shelly # homeassistant.components.shelly
aioshelly==0.3.2 aioshelly==0.3.3
# homeassistant.components.switcher_kis # homeassistant.components.switcher_kis
aioswitcher==1.2.1 aioswitcher==1.2.1
@ -143,7 +143,7 @@ aiounifi==23
aioymaps==1.1.0 aioymaps==1.1.0
# homeassistant.components.airly # homeassistant.components.airly
airly==0.0.2 airly==1.0.0
# homeassistant.components.ambiclimate # homeassistant.components.ambiclimate
ambiclimate==0.2.1 ambiclimate==0.2.1
@ -613,7 +613,7 @@ pyblackbird==0.5
pybotvac==0.0.17 pybotvac==0.0.17
# homeassistant.components.cast # homeassistant.components.cast
pychromecast==7.2.1 pychromecast==7.5.0
# homeassistant.components.coolmaster # homeassistant.components.coolmaster
pycoolmasternet-async==0.1.2 pycoolmasternet-async==0.1.2

View File

@ -67,6 +67,10 @@ urllib3>=1.24.3
# Constrain httplib2 to protect against CVE-2020-11078 # Constrain httplib2 to protect against CVE-2020-11078
httplib2>=0.18.0 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 # This is a old unmaintained library and is replaced with pycryptodome
pycrypto==1000000000.0.0 pycrypto==1000000000.0.0

View File

@ -2,6 +2,7 @@
import asyncio import asyncio
import aiohttp import aiohttp
import aioshelly
import pytest import pytest
from homeassistant import config_entries, setup 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} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
with patch( with patch("aioshelly.get_info", side_effect=exc):
"aioshelly.get_info",
side_effect=exc,
):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{"host": "1.1.1.1"}, {"host": "1.1.1.1"},
@ -134,10 +132,7 @@ async def test_form_errors_test_connection(hass, error):
with patch( with patch(
"aioshelly.get_info", return_value={"mac": "test-mac", "auth": False} "aioshelly.get_info", return_value={"mac": "test-mac", "auth": False}
), patch( ), patch("aioshelly.Device.create", new=AsyncMock(side_effect=exc)):
"aioshelly.Device.create",
side_effect=exc,
):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{"host": "1.1.1.1"}, {"host": "1.1.1.1"},
@ -175,6 +170,22 @@ async def test_form_already_configured(hass):
assert entry.data["host"] == "1.1.1.1" 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( @pytest.mark.parametrize(
"error", "error",
[ [
@ -309,12 +320,22 @@ async def test_zeroconf_already_configured(hass):
assert entry.data["host"] == "1.1.1.1" 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): async def test_zeroconf_cannot_connect(hass):
"""Test we get the form.""" """Test we get the form."""
with patch( with patch("aioshelly.get_info", side_effect=asyncio.TimeoutError):
"aioshelly.get_info",
side_effect=asyncio.TimeoutError,
):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
data={"host": "1.1.1.1", "name": "shelly1pm-12345"}, data={"host": "1.1.1.1", "name": "shelly1pm-12345"},