Merge pull request #38443 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2020-08-01 15:37:05 +02:00 committed by GitHub
commit c3aa9f9a6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 193 additions and 59 deletions

View File

@ -46,7 +46,7 @@ jobs:
run: |
python -m venv venv
. venv/bin/activate
pip install -U pip setuptools
pip install -U pip==20.1.1 setuptools
pip install -r requirements.txt -r requirements_test.txt
# Uninstalling typing as a workaround. Eventually we should make sure
# all our dependencies drop typing.
@ -603,7 +603,7 @@ jobs:
run: |
python -m venv venv
. venv/bin/activate
pip install -U pip setuptools wheel
pip install -U pip==20.1.1 setuptools wheel
pip install -r requirements_all.txt
pip install -r requirements_test.txt
# Uninstalling typing as a workaround. Eventually we should make sure

View File

@ -261,6 +261,7 @@ def setup_abode_events(hass):
TIMELINE.AUTOMATION_GROUP,
TIMELINE.DISARM_GROUP,
TIMELINE.ARM_GROUP,
TIMELINE.ARM_FAULT_GROUP,
TIMELINE.TEST_GROUP,
TIMELINE.CAPTURE_GROUP,
TIMELINE.DEVICE_GROUP,

View File

@ -82,8 +82,21 @@ class AbodeCamera(AbodeDevice, Camera):
return None
def turn_on(self):
"""Turn on camera."""
self._device.privacy_mode(False)
def turn_off(self):
"""Turn off camera."""
self._device.privacy_mode(True)
def _capture_callback(self, capture):
"""Update the image with the device then refresh device."""
self._device.update_image_location(capture)
self.get_image()
self.schedule_update_ha_state()
@property
def is_on(self):
"""Return true if on."""
return self._device.is_on

View File

@ -3,7 +3,7 @@
"name": "Abode",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/abode",
"requirements": ["abodepy==0.19.0"],
"requirements": ["abodepy==1.1.0"],
"codeowners": ["@shred86"],
"homekit": {
"models": ["Abode", "Iota"]

View File

@ -230,7 +230,13 @@ class AdsHub:
hnotify = int(contents.hNotification)
_LOGGER.debug("Received notification %d", hnotify)
data = contents.data
# get dynamically sized data array
data_size = contents.cbSampleSize
data = (ctypes.c_ubyte * data_size).from_address(
ctypes.addressof(contents)
+ pyads.structs.SAdsNotificationHeader.data.offset
)
try:
with self._lock:
@ -241,17 +247,17 @@ class AdsHub:
# Parse data to desired datatype
if notification_item.plc_datatype == self.PLCTYPE_BOOL:
value = bool(struct.unpack("<?", bytearray(data)[:1])[0])
value = bool(struct.unpack("<?", bytearray(data))[0])
elif notification_item.plc_datatype == self.PLCTYPE_INT:
value = struct.unpack("<h", bytearray(data)[:2])[0]
value = struct.unpack("<h", bytearray(data))[0]
elif notification_item.plc_datatype == self.PLCTYPE_BYTE:
value = struct.unpack("<B", bytearray(data)[:1])[0]
value = struct.unpack("<B", bytearray(data))[0]
elif notification_item.plc_datatype == self.PLCTYPE_UINT:
value = struct.unpack("<H", bytearray(data)[:2])[0]
value = struct.unpack("<H", bytearray(data))[0]
elif notification_item.plc_datatype == self.PLCTYPE_DINT:
value = struct.unpack("<i", bytearray(data)[:4])[0]
value = struct.unpack("<i", bytearray(data))[0]
elif notification_item.plc_datatype == self.PLCTYPE_UDINT:
value = struct.unpack("<I", bytearray(data)[:4])[0]
value = struct.unpack("<I", bytearray(data))[0]
else:
value = bytearray(data)
_LOGGER.warning("No callback available for this datatype")

View File

@ -2,6 +2,6 @@
"domain": "ads",
"name": "ADS",
"documentation": "https://www.home-assistant.io/integrations/ads",
"requirements": ["pyads==3.1.3"],
"requirements": ["pyads==3.2.1"],
"codeowners": []
}

View File

@ -3,8 +3,8 @@
"name": "Android TV",
"documentation": "https://www.home-assistant.io/integrations/androidtv",
"requirements": [
"adb-shell[async]==0.2.0",
"androidtv[async]==0.0.46",
"adb-shell[async]==0.2.1",
"androidtv[async]==0.0.47",
"pure-python-adb==0.2.2.dev0"
],
"codeowners": ["@JeffLIrion"]

View File

@ -1,16 +1,25 @@
"""Config flow for Cast."""
from pychromecast.discovery import discover_chromecasts
import functools
from pychromecast.discovery import discover_chromecasts, stop_discovery
from homeassistant import config_entries
from homeassistant.helpers import config_entry_flow
from .const import DOMAIN
from .helpers import ChromeCastZeroconf
async def _async_has_devices(hass):
"""Return if there are devices that can be discovered."""
return await hass.async_add_executor_job(discover_chromecasts)
casts, browser = await hass.async_add_executor_job(
functools.partial(
discover_chromecasts, zeroconf_instance=ChromeCastZeroconf.get_zeroconf()
)
)
stop_discovery(browser)
return casts
config_entry_flow.register_discovery_flow(

View File

@ -3,7 +3,7 @@
"name": "Google Cast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast",
"requirements": ["pychromecast==7.1.2"],
"requirements": ["pychromecast==7.2.0"],
"after_dependencies": ["cloud","zeroconf"],
"zeroconf": ["_googlecast._tcp.local."],
"codeowners": ["@emontnemery"]

View File

@ -8,7 +8,6 @@ from aiohttp.hdrs import REFERER, USER_AGENT
import async_timeout
from gtts_token import gtts_token
import voluptuous as vol
import yarl
from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider
from homeassistant.const import HTTP_OK
@ -129,7 +128,7 @@ class GoogleProvider(Provider):
url_param = {
"ie": "UTF-8",
"tl": language,
"q": yarl.URL(part).raw_path,
"q": part,
"tk": part_token,
"total": len(message_parts),
"idx": idx,

View File

@ -164,6 +164,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def _host_already_configured(self, host):
"""See if we already have a harmony entry matching the host."""
for entry in self._async_current_entries():
if CONF_HOST not in entry.data:
continue
if entry.data[CONF_HOST] == host:
return True
return False

View File

@ -2,7 +2,7 @@
"domain": "harmony",
"name": "Logitech Harmony Hub",
"documentation": "https://www.home-assistant.io/integrations/harmony",
"requirements": ["aioharmony==0.2.5"],
"requirements": ["aioharmony==0.2.6"],
"codeowners": ["@ehendrix23", "@bramkragten", "@bdraco"],
"ssdp": [
{

View File

@ -1,5 +1,7 @@
"""Support for interfacing with the XBMC/Kodi JSON-RPC API."""
import asyncio
from collections import OrderedDict
from datetime import timedelta
from functools import wraps
import logging
import re
@ -53,6 +55,7 @@ from homeassistant.const import (
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv, script
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.template import Template
import homeassistant.util.dt as dt_util
from homeassistant.util.yaml import dump
@ -82,6 +85,8 @@ DEPRECATED_TURN_OFF_ACTIONS = {
"shutdown": "System.Shutdown",
}
WEBSOCKET_WATCHDOG_INTERVAL = timedelta(minutes=3)
# https://github.com/xbmc/xbmc/blob/master/xbmc/media/MediaType.h
MEDIA_TYPES = {
"music": MEDIA_TYPE_MUSIC,
@ -435,6 +440,26 @@ class KodiDevice(MediaPlayerEntity):
# run until the websocket connection is closed.
self.hass.loop.create_task(ws_loop_wrapper())
async def async_added_to_hass(self):
"""Connect the websocket if needed."""
if not self._enable_websocket:
return
asyncio.create_task(self.async_ws_connect())
self.async_on_remove(
async_track_time_interval(
self.hass,
self._async_connect_websocket_if_disconnected,
WEBSOCKET_WATCHDOG_INTERVAL,
)
)
async def _async_connect_websocket_if_disconnected(self, *_):
"""Reconnect the websocket if it fails."""
if not self._ws_server.connected:
await self.async_ws_connect()
async def async_update(self):
"""Retrieve latest state."""
self._players = await self._get_players()
@ -445,9 +470,6 @@ class KodiDevice(MediaPlayerEntity):
self._app_properties = {}
return
if self._enable_websocket and not self._ws_server.connected:
self.hass.async_create_task(self.async_ws_connect())
self._app_properties = await self.server.Application.GetProperties(
["volume", "muted"]
)

View File

@ -216,6 +216,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
existing_host_port_aliases = {
_format_host_port_alias(entry.data)
for entry in self._async_current_entries()
if CONF_HOST in entry.data
}
return _format_host_port_alias(user_input) in existing_host_port_aliases

View File

@ -115,7 +115,7 @@ class PlexServer:
self._plextv_clients = [
x
for x in self.account.resources()
if "player" in x.provides and x.presence
if "player" in x.provides and x.presence and x.publicAddressMatches
]
_LOGGER.debug(
"Current available clients from plex.tv: %s", self._plextv_clients

View File

@ -100,7 +100,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
tasks = [sensor.async_update() for sensor in sensors]
if tasks:
await asyncio.wait(tasks)
if not all(sensor.data.departures for sensor in sensors):
if not any(sensor.data for sensor in sensors):
raise PlatformNotReady
async_add_entities(sensors)
@ -165,6 +166,7 @@ class RMVDepartureSensor(Entity):
"minutes": self.data.departures[0].get("minutes"),
"departure_time": self.data.departures[0].get("departure_time"),
"product": self.data.departures[0].get("product"),
ATTR_ATTRIBUTION: ATTRIBUTION,
}
except IndexError:
return {}
@ -183,13 +185,16 @@ class RMVDepartureSensor(Entity):
"""Get the latest data and update the state."""
await self.data.async_update()
if self._name == DEFAULT_NAME:
self._name = self.data.station
self._station = self.data.station
if not self.data.departures:
self._state = None
self._icon = ICONS[None]
return
if self._name == DEFAULT_NAME:
self._name = self.data.station
self._station = self.data.station
self._state = self.data.departures[0].get("minutes")
self._icon = ICONS[self.data.departures[0].get("product")]
@ -220,6 +225,7 @@ class RMVDepartureData:
self._max_journeys = max_journeys
self.rmv = RMVtransport(session, timeout)
self.departures = []
self._error_notification = False
@Throttle(SCAN_INTERVAL)
async def async_update(self):
@ -231,31 +237,49 @@ class RMVDepartureData:
direction_id=self._direction,
max_journeys=50,
)
except RMVtransportApiConnectionError:
self.departures = []
_LOGGER.warning("Could not retrieve data from rmv.de")
return
self.station = _data.get("station")
_deps = []
_deps_not_found = set(self._destinations)
for journey in _data["journeys"]:
# find the first departure meeting the criteria
_nextdep = {ATTR_ATTRIBUTION: ATTRIBUTION}
_nextdep = {}
if self._destinations:
dest_found = False
for dest in self._destinations:
if dest in journey["stops"]:
dest_found = True
if dest in _deps_not_found:
_deps_not_found.remove(dest)
_nextdep["destination"] = dest
if not dest_found:
continue
elif self._lines and journey["number"] not in self._lines:
continue
elif journey["minutes"] < self._time_offset:
continue
for attr in ["direction", "departure_time", "product", "minutes"]:
_nextdep[attr] = journey.get(attr, "")
_nextdep["line"] = journey.get("number", "")
_deps.append(_nextdep)
if len(_deps) > self._max_journeys:
break
if not self._error_notification and _deps_not_found:
self._error_notification = True
_LOGGER.info("Destination(s) %s not found", ", ".join(_deps_not_found))
self.departures = _deps

View File

@ -9,6 +9,7 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import ssdp
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.core import callback
from .const import CONF_ENDPOINT, DOMAIN # pylint: disable=unused-import
@ -74,7 +75,7 @@ class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
# Check if already configured
if self._endpoint_already_configured():
if self._async_endpoint_already_configured():
return self.async_abort(reason="already_configured")
if user_input is None:
@ -145,9 +146,10 @@ class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_init(user_input)
def _endpoint_already_configured(self):
@callback
def _async_endpoint_already_configured(self):
"""See if we already have an endpoint matching user input configured."""
existing_endpoints = [
entry.data[CONF_ENDPOINT] for entry in self._async_current_entries()
]
return self.conf.endpoint in existing_endpoints
for entry in self._async_current_entries():
if entry.data.get(CONF_ENDPOINT) == self.conf.endpoint:
return True
return False

View File

@ -71,6 +71,9 @@ class ToonDataUpdateCoordinator(DataUpdateCoordinator):
self.entry.data[CONF_WEBHOOK_ID]
)
# Ensure the webhook is not registered already
webhook_unregister(self.hass, self.entry.data[CONF_WEBHOOK_ID])
webhook_register(
self.hass,
DOMAIN,

View File

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

View File

@ -27,6 +27,7 @@ ruamel.yaml==0.15.100
sqlalchemy==1.3.18
voluptuous-serialize==2.4.0
voluptuous==0.11.7
yarl==1.4.2
zeroconf==0.27.1
pycryptodome>=3.6.6

View File

@ -20,3 +20,4 @@ requests==2.24.0
ruamel.yaml==0.15.100
voluptuous==0.11.7
voluptuous-serialize==2.4.0
yarl==1.4.2

View File

@ -100,7 +100,7 @@ WazeRouteCalculator==0.12
YesssSMS==0.4.1
# homeassistant.components.abode
abodepy==0.19.0
abodepy==1.1.0
# homeassistant.components.mcp23017
adafruit-blinka==3.9.0
@ -112,7 +112,7 @@ adafruit-circuitpython-bmp280==3.1.1
adafruit-circuitpython-mcp230xx==2.2.2
# homeassistant.components.androidtv
adb-shell[async]==0.2.0
adb-shell[async]==0.2.1
# homeassistant.components.alarmdecoder
adext==0.3
@ -164,7 +164,7 @@ aioftp==0.12.0
aioguardian==1.0.1
# homeassistant.components.harmony
aioharmony==0.2.5
aioharmony==0.2.6
# homeassistant.components.homekit_controller
aiohomekit[IP]==0.2.45
@ -231,7 +231,7 @@ ambiclimate==0.2.1
amcrest==1.7.0
# homeassistant.components.androidtv
androidtv[async]==0.0.46
androidtv[async]==0.0.47
# homeassistant.components.anel_pwrctrl
anel_pwrctrl-homeassistant==0.0.1.dev2
@ -1190,7 +1190,7 @@ py_nextbusnext==0.1.4
# py_noaa==0.3.0
# homeassistant.components.ads
pyads==3.1.3
pyads==3.2.1
# homeassistant.components.hisense_aehw4a1
pyaehw4a1==0.3.5
@ -1241,7 +1241,7 @@ pycfdns==0.0.1
pychannels==1.0.0
# homeassistant.components.cast
pychromecast==7.1.2
pychromecast==7.2.0
# homeassistant.components.cmus
pycmus==0.1.1

View File

@ -43,10 +43,10 @@ WSDiscovery==2.0.0
YesssSMS==0.4.1
# homeassistant.components.abode
abodepy==0.19.0
abodepy==1.1.0
# homeassistant.components.androidtv
adb-shell[async]==0.2.0
adb-shell[async]==0.2.1
# homeassistant.components.adguard
adguardhome==0.4.2
@ -89,7 +89,7 @@ aiofreepybox==0.0.8
aioguardian==1.0.1
# homeassistant.components.harmony
aioharmony==0.2.5
aioharmony==0.2.6
# homeassistant.components.homekit_controller
aiohomekit[IP]==0.2.45
@ -132,7 +132,7 @@ airly==0.0.2
ambiclimate==0.2.1
# homeassistant.components.androidtv
androidtv[async]==0.0.46
androidtv[async]==0.0.47
# homeassistant.components.apns
apns2==0.3.0
@ -577,7 +577,7 @@ pyblackbird==0.5
pybotvac==0.0.17
# homeassistant.components.cast
pychromecast==7.1.2
pychromecast==7.2.0
# homeassistant.components.coolmaster
pycoolmasternet==0.0.4

View File

@ -52,6 +52,7 @@ REQUIRES = [
"ruamel.yaml==0.15.100",
"voluptuous==0.11.7",
"voluptuous-serialize==2.4.0",
"yarl==1.4.2",
]
MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER))

View File

@ -38,3 +38,33 @@ async def test_capture_image(hass):
)
await hass.async_block_till_done()
mock_capture.assert_called_once()
async def test_camera_on(hass):
"""Test the camera turn on service."""
await setup_platform(hass, CAMERA_DOMAIN)
with patch("abodepy.AbodeCamera.privacy_mode") as mock_capture:
await hass.services.async_call(
CAMERA_DOMAIN,
"turn_on",
{ATTR_ENTITY_ID: "camera.test_cam"},
blocking=True,
)
await hass.async_block_till_done()
mock_capture.assert_called_once_with(False)
async def test_camera_off(hass):
"""Test the camera turn off service."""
await setup_platform(hass, CAMERA_DOMAIN)
with patch("abodepy.AbodeCamera.privacy_mode") as mock_capture:
await hass.services.async_call(
CAMERA_DOMAIN,
"turn_off",
{ATTR_ENTITY_ID: "camera.test_cam"},
blocking=True,
)
await hass.async_block_till_done()
mock_capture.assert_called_once_with(True)

View File

@ -13,7 +13,9 @@ async def test_creating_entry_sets_up_media_player(hass):
"homeassistant.components.cast.media_player.async_setup_entry",
return_value=True,
) as mock_setup, patch(
"pychromecast.discovery.discover_chromecasts", return_value=True
"pychromecast.discovery.discover_chromecasts", return_value=(True, None)
), patch(
"pychromecast.discovery.stop_discovery"
):
result = await hass.config_entries.flow.async_init(
cast.DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -34,9 +36,7 @@ async def test_configuring_cast_creates_entry(hass):
"""Test that specifying config will create an entry."""
with patch(
"homeassistant.components.cast.async_setup_entry", return_value=True
) as mock_setup, patch(
"pychromecast.discovery.discover_chromecasts", return_value=True
):
) as mock_setup:
await async_setup_component(
hass, cast.DOMAIN, {"cast": {"some_config": "to_trigger_import"}}
)
@ -49,9 +49,7 @@ async def test_not_configuring_cast_not_creates_entry(hass):
"""Test that no config will not create an entry."""
with patch(
"homeassistant.components.cast.async_setup_entry", return_value=True
) as mock_setup, patch(
"pychromecast.discovery.discover_chromecasts", return_value=True
):
) as mock_setup:
await async_setup_component(hass, cast.DOMAIN, {})
await hass.async_block_till_done()

View File

@ -153,6 +153,9 @@ async def test_form_ssdp_aborts_before_checking_remoteid_if_host_known(hass):
)
config_entry.add_to_hass(hass)
config_entry_without_host = MockConfigEntry(domain=DOMAIN, data={"name": "other"},)
config_entry_without_host.add_to_hass(hass)
harmonyapi = _get_mock_harmonyapi(connect=True)
with patch(

View File

@ -247,7 +247,25 @@ async def test_form_import_dupe(hass):
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "import"}, data=VALID_CONFIG
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=VALID_CONFIG
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
async def test_form_import_with_ignored_entry(hass):
"""Test we get abort on duplicate import when there is an ignored one."""
await setup.async_setup_component(hass, "persistent_notification", {})
entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG)
entry.add_to_hass(hass)
ignored_entry = MockConfigEntry(
domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE
)
ignored_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=VALID_CONFIG
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"

View File

@ -53,6 +53,7 @@ class MockResource:
self.provides = ["player"]
self.device = MockPlexClient(f"http://192.168.0.1{index}:32500", index + 10)
self.presence = index == 0
self.publicAddressMatches = True
def connect(self, timeout):
"""Mock the resource connect method."""

View File

@ -48,7 +48,7 @@ VALID_CONFIG_DEST = {
def get_departures_mock():
"""Mock rmvtransport departures loading."""
data = {
return {
"station": "Frankfurt (Main) Hauptbahnhof",
"stationId": "3000010",
"filter": "11111111111",
@ -145,18 +145,16 @@ def get_departures_mock():
},
],
}
return data
def get_no_departures_mock():
"""Mock no departures in results."""
data = {
return {
"station": "Frankfurt (Main) Hauptbahnhof",
"stationId": "3000010",
"filter": "11111111111",
"journeys": [],
}
return data
async def test_rmvtransport_min_config(hass):
@ -232,4 +230,4 @@ async def test_rmvtransport_no_departures(hass):
await hass.async_block_till_done()
state = hass.states.get("sensor.frankfurt_main_hauptbahnhof")
assert not state
assert state.state == "unavailable"