Compare commits

..

39 Commits

Author SHA1 Message Date
Franck Nijhof
ef1c8c55f8 Merge pull request #40742 from home-assistant/rc 2020-09-29 13:33:10 +02:00
Franck Nijhof
ebacc15e72 Bumped version to 0.115.5 2020-09-29 12:38:30 +02:00
Franck Nijhof
e54ec3ba05 Fix ID3 tagging in TTS (#40740) 2020-09-29 12:38:04 +02:00
Paulus Schoutsen
b4237be609 Merge pull request #40706 from home-assistant/rc 2020-09-28 14:58:19 +02:00
Paulus Schoutsen
2ee5b0a9c1 Bumped version to 0.115.4 2020-09-28 12:29:16 +00:00
Paweł Stankowski
48f5ffa0bd Bump Airly package to 1.0.0 (#40695) 2020-09-28 12:29:06 +00:00
Franck Nijhof
b01d04c4ca Pin gRPC to 1.31.0 to workaround amrv7 issues (#40678) 2020-09-28 12:29:05 +00:00
Chris
94f3a085ed set ID3 tags as TextFrame types (#40666) 2020-09-28 12:29:04 +00:00
Jason Hunter
59daab9434 Fix camera play stream (#40641)
Co-authored-by: Justin Wong <46082645+uvjustin@users.noreply.github.com>
2020-09-28 12:28:44 +00:00
Bouwe Westerdijk
af88a78aae Bump Plugwise-Smile to v1.5.1 (#40572) 2020-09-28 12:27:54 +00:00
Colin Frei
837c3d3c9d Fix fitbit current URL not available while configuring (#40547)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2020-09-28 12:27:53 +00:00
uvjustin
d952a2b756 Create master playlist for cast (#40483)
Co-authored-by: Jason Hunter <hunterjm@gmail.com>
2020-09-28 12:27:53 +00:00
Maciej Bieniek
c07c6ca413 Bump aioshelly library to version 0.3.3 (#40415) 2020-09-28 12:27:40 +00:00
Franck Nijhof
9fdab64e8b Merge pull request #40571 from home-assistant/rc 2020-09-25 10:29:07 +02:00
Franck Nijhof
7be494f845 Bumped version to 0.115.3 2020-09-25 09:28:38 +02:00
Kevin Cathcart
1c8e8419b6 Fix bug in state trigger when using for: without to: (#40556) 2020-09-25 09:27:49 +02:00
Bram Kragten
da7f206414 Updated frontend to 20200918.2 (#40549) 2020-09-25 09:27:43 +02:00
Rob Bierbooms
6bd72c3ff5 Fix connection validation during import for dsmr integration (#40548)
* Close transport when equipment identifier is received

* Minor fix
2020-09-25 09:27:37 +02:00
Steven Looman
82c137d69b Increase upnp timeout from 5 seconds to 10 seconds (#40540) 2020-09-25 09:27:32 +02:00
cagnulein
a404c51797 Fix luci device_tracker when release is none (#40524)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2020-09-25 09:27:27 +02:00
uvjustin
ff39cd753c Disable audio in stream when audio stream profile is None (#40521) 2020-09-25 09:27:22 +02:00
J. Nick Koston
fe056f518e Ensure group state is recalculated when re-adding on reload (#40497) 2020-09-25 09:27:15 +02:00
Anders Melchiorsen
0ebeb161e1 Fix handling of quoted time_pattern values (#40470)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2020-09-25 09:27:10 +02:00
Robert Van Gorkom
f953454374 Increase gogogate2 request timeout (#40461) 2020-09-25 09:27:07 +02:00
Maciej Bieniek
016834185a Bump accuweather library to version 0.0.11 (#40458) 2020-09-25 09:27:00 +02:00
square99
34c0e0f58d Fix proxy camera conversion with PNG Alpha(RGBA) (#40446) 2020-09-25 09:26:56 +02:00
MeIchthys
95e6969912 Fix regression in Nextcloud component (#40438) 2020-09-25 09:26:52 +02:00
Pedro Lamas
297e5300b4 Fix webostv supported features for "external_speaker" sound output (#40435) 2020-09-25 09:26:48 +02:00
jan iversen
6cccd87318 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).
2020-09-25 09:26:43 +02:00
cagnulein
532c624d01 Fix luci device_tracker incorrectly reporting devices status (#40409) 2020-09-25 09:26:39 +02:00
Michael Thingnes
ed17a81f50 Validate Met.no forecast entries before passing them on to HA (#40400) 2020-09-25 09:26:33 +02:00
On Freund
44be190378 Fix handling of empty ws port (#40399) 2020-09-25 09:26:29 +02:00
Maciej Bieniek
b1ac920505 Fix OSError (#40393) 2020-09-25 09:26:25 +02:00
Tom Harris
1901cc962e Bump pyinsteon to 1.0.8 (#40383) 2020-09-25 09:26:21 +02:00
Michael Thingnes
babaf48867 Fix Met.no missing conditions in API forecasts (#40373) 2020-09-25 09:26:17 +02:00
Robert Svensson
b3e2426967 Axis - Fix list applications breaks if empty response (#40360) 2020-09-25 09:26:14 +02:00
uvjustin
f1ee7fed4c Ignore packets with missing dts in peek_first_pts (#40299) 2020-09-25 09:26:10 +02:00
Markus Haack
3efda8d1d2 Guard SolarEdge for inverters without batteries (#40295) 2020-09-25 09:26:07 +02:00
Harrison Pace
a5f00d1db2 Use Cloud State as alternative state if condition unknown (#37121) 2020-09-25 09:26:03 +02:00
47 changed files with 506 additions and 134 deletions

View File

@@ -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"

View File

@@ -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"
}

View File

@@ -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"]

View File

@@ -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

View File

@@ -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):

View File

@@ -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"]

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.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

View File

@@ -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(

View File

@@ -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,

View File

@@ -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",

View File

@@ -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": [

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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}")

View File

@@ -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
}

View File

@@ -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:

View File

@@ -94,7 +94,12 @@ class LuciDeviceScanner(DeviceScanner):
last_results = []
for device in result:
if device.reachable:
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
):
last_results.append(device)
self.last_results = last_results

View File

@@ -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 = [

View File

@@ -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"

View File

@@ -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,13 +211,17 @@ 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)
}
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

View File

@@ -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."""

View File

@@ -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."""

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"]})

View File

@@ -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"]
}

View File

@@ -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."
}
}
}

View File

@@ -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:

View File

@@ -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)

View File

@@ -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:

View File

@@ -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:

View File

@@ -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
@@ -113,7 +116,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 +128,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

View File

@@ -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
@@ -465,11 +466,11 @@ 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"] = 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)

View File

@@ -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)

View File

@@ -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

View File

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

View File

@@ -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
@@ -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

View File

@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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

View File

@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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

View File

@@ -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

View File

@@ -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():

View File

@@ -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

View File

@@ -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()

View File

@@ -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(

View File

@@ -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"},