Compare commits

..

38 Commits

Author SHA1 Message Date
Paulus Schoutsen
c3aa9f9a6f Merge pull request #38443 from home-assistant/rc 2020-08-01 15:37:05 +02:00
Franck Nijhof
c4fcd8bd2e Temporary lock pip to 20.1.1 to avoid build issue (#38358) 2020-08-01 13:18:38 +00:00
Paulus Schoutsen
b209c1a7b5 Bumped version to 0.113.3 2020-08-01 02:02:48 +00:00
Franck Nijhof
82b3fe1ab6 Fix double encoding issue in google_translate TTS (#38429) 2020-08-01 02:02:40 +00:00
Franck Nijhof
7a8d9b6065 Pin yarl dependency to 1.4.2 as core dependency (#38428) 2020-08-01 02:02:22 +00:00
Stefan Lehmann
f4afa2dc68 Fix ads integration after 0.113 (#38402) 2020-08-01 01:59:47 +00:00
cgtobi
2a3947f7cc Fix rmvtransport breaking when destinations don't match (#38401) 2020-08-01 01:59:47 +00:00
Franck Nijhof
47a729495d Ensure Toon webhook ID isn't registered on re-registration (#38376) 2020-08-01 01:59:46 +00:00
J. Nick Koston
293655f988 Prevent nut config flow error when checking ignored entries (#38372) 2020-08-01 01:59:45 +00:00
J. Nick Koston
0ecaab1a7d Avoid error with ignored harmony config entries (#38367) 2020-08-01 01:59:45 +00:00
ehendrix23
9819d70941 Update aioharmony to 0.2.6 (#38360) 2020-08-01 01:59:44 +00:00
Erik Montnemery
abad6dfdd7 Bump pychromecast to 7.2.0 (#38351) 2020-08-01 01:59:43 +00:00
Jeff Irion
dd4e0511a3 Bump androidtv to 0.0.47 and adb-shell to 0.2.1 (#38344) 2020-08-01 01:59:43 +00:00
jjlawren
d65545964b Ignore remote Plex clients during plex.tv lookup (#38327) 2020-08-01 01:59:42 +00:00
J. Nick Koston
d1fbcba7b6 Prevent kodi from blocking startup (#38257)
* Prevent kodi from blocking startup

* Update homeassistant/components/kodi/media_player.py

* isort

* ignore args

* adjustments per review

* asyncio
2020-08-01 01:59:41 +00:00
Xiaonan Shen
1b73bcbff7 Fix songpal already configured check in config flow (#37813)
* Fix already configured check

* Mark endpoint duplicate check as callback
2020-08-01 01:59:41 +00:00
shred86
7a2f6a5006 Add Abode camera on and off support (#35164)
* Add Abode camera controls

* Add tests for camera turn on and off service

* Bump abodepy version

* Bump abodepy version and updates to reflect changes

* Update manifest
2020-08-01 01:59:40 +00:00
Franck Nijhof
d24b02646e Merge pull request #38332 from home-assistant/rc
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Phil Bruckner <pnbruckner@gmail.com>
Co-authored-by: Joakim Plate <elupus@ecce.se>
Co-authored-by: Jeroen Van den Keybus <jeroen.vandenkeybus@gmail.com>
Co-authored-by: Mister Wil <1091741+MisterWil@users.noreply.github.com>
Co-authored-by: Greg Dowling <pavoni@users.noreply.github.com>
Co-authored-by: Marcio Granzotto Rodrigues <oscensores@gmail.com>
Co-authored-by: Teemu R <tpr@iki.fi>
Co-authored-by: Kyle Hendricks <kylehendricks@users.noreply.github.com>
2020-07-28 20:24:28 +02:00
Franck Nijhof
60cab62da5 Revert "Make rfxtrx RfyDevices have sun automation switches (#38210)"
This reverts commit ee0c32cbb7.
2020-07-28 20:05:31 +02:00
Franck Nijhof
612e27b5ff Bumped version to 0.113.2 2020-07-28 19:37:50 +02:00
Franck Nijhof
d9953a8c2f Remove AdGuard version check (#38326) 2020-07-28 19:32:14 +02:00
J. Nick Koston
e984e0d414 Add debug logging for when a chain of tasks blocks startup (#38311)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2020-07-28 19:32:11 +02:00
Kyle Hendricks
f8fbe8dba3 Fix issue with certain Samsung TVs repeatedly showing auth dialog (#38308)
Through some testing with the samsungtvws library, it was determined
that the issue is related to the short read timeout (1s).  Increasing
the timeout to 10s should solve the issue.
2020-07-28 19:32:08 +02:00
J. Nick Koston
b8d6b20c96 Prevent speedtest from blocking startup or causing other intergations to fail setup (#38305)
When speedtest starts up, it would saturate the network interface and cause other
integrations to randomly fail to setup. We now wait to do the first speed test
until after the started event is fired.
2020-07-28 19:32:05 +02:00
Teemu R
c4038c8652 Bump python-miio to 0.5.3 (#38300) 2020-07-28 19:32:01 +02:00
Marcio Granzotto Rodrigues
b5a64b3752 Fix #38289 issue with xboxapi lib (#38293) 2020-07-28 19:31:56 +02:00
J. Nick Koston
bd1336cbdf Prevent onvif from blocking startup (#38256) 2020-07-28 19:31:51 +02:00
Greg Dowling
8768fe1652 Don't set up callbacks until entity is created. (#38251) 2020-07-28 19:09:36 +02:00
J. Nick Koston
37e029b2c1 Improve setup retry logic to handle inconsistent powerview hub availability (#38249) 2020-07-28 19:09:33 +02:00
Mister Wil
c55c415933 Fix Skybell useragent (#38245) 2020-07-28 19:09:30 +02:00
Phil Bruckner
293db61b32 Fix parallel script containing repeat or choose action with max_runs > 10 (#38243) 2020-07-28 19:09:25 +02:00
Phil Bruckner
bdab437574 Fix repeat action when variables present (#38237) 2020-07-28 18:58:10 +02:00
Jeroen Van den Keybus
e89c475856 Fix detection of zones 2 and 3 in Onkyo/Pioneer amplifiers (#38234) 2020-07-28 18:31:06 +02:00
Joakim Plate
ee0c32cbb7 Make rfxtrx RfyDevices have sun automation switches (#38210)
* RfyDevices have sun automation

* We must accept sun automation commands for switch

* Add test for Rfy sun automation
2020-07-28 18:31:03 +02:00
J. Nick Koston
2e89ec24f7 Ignore harmony hubs ips that are already configured during ssdp discovery (#38181)
We would connect to the hub via discovery and via setup
around the same time.  This put additional load on the hub
which can increase the risk of timeouts.
2020-07-28 18:30:59 +02:00
J. Nick Koston
b82e64d9cb Bump tesla-powerwall to 0.2.12 to handle powerwall firmware 1.48+ (#38180) 2020-07-28 18:30:56 +02:00
Phil Bruckner
61fa572068 Stop automation runs when turned off or reloaded (#38174)
* Add automation turn off / reload test

* Stop automation runs when turned off or reloaded
2020-07-28 18:30:53 +02:00
J. Nick Koston
7f2a2ed23b Bump netdisco to 2.8.1 (#38173)
* Bump netdisco to 2.8.1

* bump ssdp
2020-07-28 18:30:49 +02:00
57 changed files with 561 additions and 240 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

@@ -1,5 +1,4 @@
"""Support for AdGuard Home."""
from distutils.version import LooseVersion
import logging
from typing import Any, Dict
@@ -11,7 +10,6 @@ from homeassistant.components.adguard.const import (
DATA_ADGUARD_CLIENT,
DATA_ADGUARD_VERION,
DOMAIN,
MIN_ADGUARD_HOME_VERSION,
SERVICE_ADD_URL,
SERVICE_DISABLE_URL,
SERVICE_ENABLE_URL,
@@ -67,16 +65,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
hass.data.setdefault(DOMAIN, {})[DATA_ADGUARD_CLIENT] = adguard
try:
version = await adguard.version()
await adguard.version()
except AdGuardHomeConnectionError as exception:
raise ConfigEntryNotReady from exception
if version and LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version):
_LOGGER.error(
"This integration requires AdGuard Home v0.99.0 or higher to work correctly"
)
raise ConfigEntryNotReady
for component in "sensor", "switch":
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)

View File

@@ -1,12 +1,11 @@
"""Config flow to configure the AdGuard Home integration."""
from distutils.version import LooseVersion
import logging
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.adguard.const import DOMAIN, MIN_ADGUARD_HOME_VERSION
from homeassistant.components.adguard.const import DOMAIN
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import (
CONF_HOST,
@@ -79,20 +78,11 @@ class AdGuardHomeFlowHandler(ConfigFlow):
)
try:
version = await adguard.version()
await adguard.version()
except AdGuardHomeConnectionError:
errors["base"] = "connection_error"
return await self._show_setup_form(errors)
if version and LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version):
return self.async_abort(
reason="adguard_home_outdated",
description_placeholders={
"current_version": version,
"minimal_version": MIN_ADGUARD_HOME_VERSION,
},
)
return self.async_create_entry(
title=user_input[CONF_HOST],
data={
@@ -160,20 +150,11 @@ class AdGuardHomeFlowHandler(ConfigFlow):
)
try:
version = await adguard.version()
await adguard.version()
except AdGuardHomeConnectionError:
errors["base"] = "connection_error"
return await self._show_hassio_form(errors)
if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version):
return self.async_abort(
reason="adguard_home_addon_outdated",
description_placeholders={
"current_version": version,
"minimal_version": MIN_ADGUARD_HOME_VERSION,
},
)
return self.async_create_entry(
title=self._hassio_discovery["addon"],
data={

View File

@@ -7,8 +7,6 @@ DATA_ADGUARD_VERION = "adguard_version"
CONF_FORCE = "force"
MIN_ADGUARD_HOME_VERSION = "v0.99.0"
SERVICE_ADD_URL = "add_url"
SERVICE_DISABLE_URL = "disable_url"
SERVICE_ENABLE_URL = "enable_url"

View File

@@ -19,8 +19,6 @@
},
"error": { "connection_error": "Failed to connect." },
"abort": {
"adguard_home_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}.",
"adguard_home_addon_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}. Please update your Hass.io AdGuard Home add-on.",
"existing_instance_updated": "Updated existing configuration.",
"single_instance_allowed": "Only a single configuration of AdGuard Home is allowed."
}

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

@@ -455,6 +455,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
self._async_detach_triggers()
self._async_detach_triggers = None
await self.action_script.async_stop()
self.async_write_ha_state()
async def _async_attach_triggers(

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

@@ -2,7 +2,7 @@
"domain": "discovery",
"name": "Discovery",
"documentation": "https://www.home-assistant.io/integrations/discovery",
"requirements": ["netdisco==2.8.0"],
"requirements": ["netdisco==2.8.1"],
"after_dependencies": ["zeroconf"],
"codeowners": [],
"quality_scale": "internal"

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

@@ -86,6 +86,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION])
friendly_name = discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME]
if self._host_already_configured(parsed_url.hostname):
return self.async_abort(reason="already_configured")
# pylint: disable=no-member
self.context["title_placeholders"] = {"name": friendly_name}
@@ -158,6 +161,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry(title=validated[CONF_NAME], data=data)
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
def _options_from_user_input(user_input):
options = {}

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

@@ -112,22 +112,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
try:
async with async_timeout.timeout(10):
device_info = await async_get_device_info(pv_request)
async with async_timeout.timeout(10):
rooms = Rooms(pv_request)
room_data = _async_map_data_by_id((await rooms.get_resources())[ROOM_DATA])
async with async_timeout.timeout(10):
scenes = Scenes(pv_request)
scene_data = _async_map_data_by_id(
(await scenes.get_resources())[SCENE_DATA]
)
async with async_timeout.timeout(10):
shades = Shades(pv_request)
shade_data = _async_map_data_by_id(
(await shades.get_resources())[SHADE_DATA]
)
except HUB_EXCEPTIONS:
_LOGGER.error("Connection error to PowerView hub: %s", hub_address)
raise ConfigEntryNotReady
if not device_info:
_LOGGER.error("Unable to initialize PowerView hub: %s", hub_address)
raise ConfigEntryNotReady
rooms = Rooms(pv_request)
room_data = _async_map_data_by_id((await rooms.get_resources())[ROOM_DATA])
scenes = Scenes(pv_request)
scene_data = _async_map_data_by_id((await scenes.get_resources())[SCENE_DATA])
shades = Shades(pv_request)
shade_data = _async_map_data_by_id((await shades.get_resources())[SHADE_DATA])
async def async_update_data():
"""Fetch data from shade endpoint."""
async with async_timeout.timeout(10):

View File

@@ -2,6 +2,7 @@
import asyncio
from aiohttp.client_exceptions import ServerDisconnectedError
from aiopvapi.helpers.aiorequest import PvApiConnectionError
DOMAIN = "hunterdouglas_powerview"
@@ -64,7 +65,7 @@ PV_SHADE_DATA = "pv_shade_data"
PV_ROOM_DATA = "pv_room_data"
COORDINATOR = "coordinator"
HUB_EXCEPTIONS = (asyncio.TimeoutError, PvApiConnectionError)
HUB_EXCEPTIONS = (ServerDisconnectedError, asyncio.TimeoutError, PvApiConnectionError)
LEGACY_DEVICE_SUB_REVISION = 1
LEGACY_DEVICE_REVISION = 0

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

@@ -120,7 +120,7 @@ def determine_zones(receiver):
out = {"zone2": False, "zone3": False}
try:
_LOGGER.debug("Checking for zone 2 capability")
receiver.raw("ZPW")
receiver.raw("ZPWQSTN")
out["zone2"] = True
except ValueError as error:
if str(error) != TIMEOUT_MESSAGE:
@@ -128,7 +128,7 @@ def determine_zones(receiver):
_LOGGER.debug("Zone 2 timed out, assuming no functionality")
try:
_LOGGER.debug("Checking for zone 3 capability")
receiver.raw("PW3")
receiver.raw("PW3QSTN")
out["zone3"] = True
except ValueError as error:
if str(error) != TIMEOUT_MESSAGE:

View File

@@ -6,7 +6,7 @@ from aiohttp.client_exceptions import ServerDisconnectedError
from onvif import ONVIFCamera, ONVIFService
from zeep.exceptions import Fault
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util import dt as dt_util
@@ -114,30 +114,31 @@ class EventManager:
async def async_pull_messages(self, _now: dt = None) -> None:
"""Pull messages from device."""
try:
pullpoint = self.device.create_pullpoint_service()
req = pullpoint.create_type("PullMessages")
req.MessageLimit = 100
req.Timeout = dt.timedelta(seconds=60)
response = await pullpoint.PullMessages(req)
if self.hass.state == CoreState.running:
try:
pullpoint = self.device.create_pullpoint_service()
req = pullpoint.create_type("PullMessages")
req.MessageLimit = 100
req.Timeout = dt.timedelta(seconds=60)
response = await pullpoint.PullMessages(req)
# Renew subscription if less than two hours is left
if (
dt_util.as_utc(response.TerminationTime) - dt_util.utcnow()
).total_seconds() < 7200:
await self.async_renew()
# Renew subscription if less than two hours is left
if (
dt_util.as_utc(response.TerminationTime) - dt_util.utcnow()
).total_seconds() < 7200:
await self.async_renew()
# Parse response
await self.async_parse_messages(response.NotificationMessage)
# Parse response
await self.async_parse_messages(response.NotificationMessage)
except ServerDisconnectedError:
pass
except Fault:
pass
except ServerDisconnectedError:
pass
except Fault:
pass
# Update entities
for update_callback in self._listeners:
update_callback()
# Update entities
for update_callback in self._listeners:
update_callback()
# Reschedule another pull
if self._listeners:

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

@@ -3,6 +3,6 @@
"name": "Tesla Powerwall",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/powerwall",
"requirements": ["tesla-powerwall==0.2.11"],
"requirements": ["tesla-powerwall==0.2.12"],
"codeowners": ["@bdraco", "@jrester"]
}

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

@@ -250,7 +250,7 @@ class SamsungTVWSBridge(SamsungTVBridge):
host=self.host,
port=self.port,
token=self.token,
timeout=1,
timeout=10,
name=VALUE_CONF_NAME,
)
self._remote.open()

View File

@@ -5,7 +5,12 @@ from requests.exceptions import ConnectTimeout, HTTPError
from skybellpy import Skybell
import voluptuous as vol
from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_PASSWORD,
CONF_USERNAME,
__version__,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -20,6 +25,8 @@ DOMAIN = "skybell"
DEFAULT_CACHEDB = "./skybell_cache.pickle"
DEFAULT_ENTITY_NAMESPACE = "skybell"
AGENT_IDENTIFIER = f"HomeAssistant/{__version__}"
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
@@ -42,7 +49,11 @@ def setup(hass, config):
try:
cache = hass.config.path(DEFAULT_CACHEDB)
skybell = Skybell(
username=username, password=password, get_devices=True, cache_path=cache
username=username,
password=password,
get_devices=True,
cache_path=cache,
agent_identifier=AGENT_IDENTIFIER,
)
hass.data[DOMAIN] = skybell

View File

@@ -2,6 +2,6 @@
"domain": "skybell",
"name": "SkyBell",
"documentation": "https://www.home-assistant.io/integrations/skybell",
"requirements": ["skybellpy==0.4.0"],
"requirements": ["skybellpy==0.6.1"],
"codeowners": []
}

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

@@ -6,7 +6,12 @@ import speedtest
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL
from homeassistant.const import (
CONF_MONITORED_CONDITIONS,
CONF_SCAN_INTERVAL,
EVENT_HOMEASSISTANT_STARTED,
)
from homeassistant.core import CoreState
from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -70,10 +75,25 @@ async def async_setup_entry(hass, config_entry):
coordinator = SpeedTestDataCoordinator(hass, config_entry)
await coordinator.async_setup()
if not config_entry.options[CONF_MANUAL]:
async def _enable_scheduled_speedtests(*_):
"""Activate the data update coordinator."""
coordinator.update_interval = timedelta(
minutes=config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
)
await coordinator.async_refresh()
if not coordinator.last_update_success:
raise ConfigEntryNotReady
if not config_entry.options[CONF_MANUAL]:
if hass.state == CoreState.running:
await _enable_scheduled_speedtests()
if not coordinator.last_update_success:
raise ConfigEntryNotReady
else:
# Running a speed test during startup can prevent
# integrations from being able to setup because it
# can saturate the network interface.
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STARTED, _enable_scheduled_speedtests
)
hass.data[DOMAIN] = coordinator
@@ -107,12 +127,6 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator):
super().__init__(
self.hass, _LOGGER, name=DOMAIN, update_method=self.async_update,
)
if not self.config_entry.options.get(CONF_MANUAL):
self.update_interval = timedelta(
minutes=self.config_entry.options.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
)
)
def update_servers(self):
"""Update list of test servers."""

View File

@@ -12,7 +12,6 @@ from .const import (
ATTR_SERVER_ID,
ATTR_SERVER_NAME,
ATTRIBUTION,
CONF_MANUAL,
DEFAULT_NAME,
DOMAIN,
ICON,
@@ -97,10 +96,9 @@ class SpeedtestSensor(RestoreEntity):
async def async_added_to_hass(self):
"""Handle entity which will be added."""
await super().async_added_to_hass()
if self.coordinator.config_entry.options[CONF_MANUAL]:
state = await self.async_get_last_state()
if state:
self._state = state.state
state = await self.async_get_last_state()
if state:
self._state = state.state
@callback
def update():

View File

@@ -2,7 +2,7 @@
"domain": "ssdp",
"name": "Simple Service Discovery Protocol (SSDP)",
"documentation": "https://www.home-assistant.io/integrations/ssdp",
"requirements": ["defusedxml==0.6.0", "netdisco==2.8.0"],
"requirements": ["defusedxml==0.6.0", "netdisco==2.8.1"],
"after_dependencies": ["zeroconf"],
"codeowners": []
}

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

@@ -194,7 +194,9 @@ class VeraDevice(Entity):
slugify(vera_device.name), vera_device.device_id
)
self.controller.register(vera_device, self._update_callback)
async def async_added_to_hass(self):
"""Subscribe to updates."""
self.controller.register(self.vera_device, self._update_callback)
def _update_callback(self, _device):
"""Update the state."""

View File

@@ -2,6 +2,6 @@
"domain": "xbox_live",
"name": "Xbox Live",
"documentation": "https://www.home-assistant.io/integrations/xbox_live",
"requirements": ["xboxapi==2.0.0"],
"requirements": ["xboxapi==2.0.1"],
"codeowners": ["@MartinHjelmare"]
}

View File

@@ -3,7 +3,7 @@
"name": "Xiaomi Miio",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
"requirements": ["construct==2.9.45", "python-miio==0.5.2.1"],
"requirements": ["construct==2.9.45", "python-miio==0.5.3"],
"codeowners": ["@rytilahti", "@syssi"],
"zeroconf": ["_miio._udp.local."]
}

View File

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

View File

@@ -392,12 +392,28 @@ class HomeAssistant:
"""Block until all pending work is done."""
# To flush out any call_soon_threadsafe
await asyncio.sleep(0)
start_time: Optional[float] = None
while self._pending_tasks:
pending = [task for task in self._pending_tasks if not task.done()]
self._pending_tasks.clear()
if pending:
await self._await_and_log_pending(pending)
if start_time is None:
# Avoid calling monotonic() until we know
# we may need to start logging blocked tasks.
start_time = 0
elif start_time == 0:
# If we have waited twice then we set the start
# time
start_time = monotonic()
elif monotonic() - start_time > BLOCK_LOG_TIMEOUT:
# We have waited at least three loops and new tasks
# continue to block. At this point we start
# logging all waiting tasks.
for task in pending:
_LOGGER.debug("Waiting for task: %s", task)
else:
await asyncio.sleep(0)

View File

@@ -4,7 +4,19 @@ from datetime import datetime
from functools import partial
import itertools
import logging
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple
from types import MappingProxyType
from typing import (
Any,
Callable,
Dict,
List,
Optional,
Sequence,
Set,
Tuple,
Union,
cast,
)
from async_timeout import timeout
import voluptuous as vol
@@ -134,13 +146,13 @@ class _ScriptRun:
self,
hass: HomeAssistant,
script: "Script",
variables: Optional[Sequence],
variables: Dict[str, Any],
context: Optional[Context],
log_exceptions: bool,
) -> None:
self._hass = hass
self._script = script
self._variables = variables or {}
self._variables = variables
self._context = context
self._log_exceptions = log_exceptions
self._step = -1
@@ -595,6 +607,9 @@ async def _async_stop_scripts_at_shutdown(hass, event):
)
_VarsType = Union[Dict[str, Any], MappingProxyType]
class Script:
"""Representation of a script."""
@@ -617,6 +632,7 @@ class Script:
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, partial(_async_stop_scripts_at_shutdown, hass)
)
self._top_level = top_level
if top_level:
all_scripts.append(
{"instance": self, "started_before_shutdown": not hass.is_stopping}
@@ -732,14 +748,16 @@ class Script:
self._referenced_entities = referenced
return referenced
def run(self, variables=None, context=None):
def run(
self, variables: Optional[_VarsType] = None, context: Optional[Context] = None
) -> None:
"""Run script."""
asyncio.run_coroutine_threadsafe(
self.async_run(variables, context), self._hass.loop
).result()
async def async_run(
self, variables: Optional[Sequence] = None, context: Optional[Context] = None
self, variables: Optional[_VarsType] = None, context: Optional[Context] = None
) -> None:
"""Run script."""
if self.is_running:
@@ -753,11 +771,19 @@ class Script:
self._log("Maximum number of runs exceeded", level=logging.WARNING)
return
# If this is a top level Script then make a copy of the variables in case they
# are read-only, but more importantly, so as not to leak any variables created
# during the run back to the caller.
if self._top_level:
variables = dict(variables) if variables is not None else {}
if self.script_mode != SCRIPT_MODE_QUEUED:
cls = _ScriptRun
else:
cls = _QueuedScriptRun
run = cls(self._hass, self, variables, context, self._log_exceptions)
run = cls(
self._hass, self, cast(dict, variables), context, self._log_exceptions
)
self._runs.append(run)
try:
@@ -795,6 +821,7 @@ class Script:
action[CONF_REPEAT][CONF_SEQUENCE],
f"{self.name}: {step_name}",
script_mode=SCRIPT_MODE_PARALLEL,
max_runs=self.max_runs,
logger=self._logger,
top_level=False,
)
@@ -822,6 +849,7 @@ class Script:
choice[CONF_SEQUENCE],
f"{self.name}: {step_name}: choice {idx}",
script_mode=SCRIPT_MODE_PARALLEL,
max_runs=self.max_runs,
logger=self._logger,
top_level=False,
)
@@ -836,6 +864,7 @@ class Script:
action[CONF_DEFAULT],
f"{self.name}: {step_name}: default",
script_mode=SCRIPT_MODE_PARALLEL,
max_runs=self.max_runs,
logger=self._logger,
top_level=False,
)

View File

@@ -16,7 +16,7 @@ hass-nabucasa==0.34.7
home-assistant-frontend==20200716.0
importlib-metadata==1.6.0;python_version<'3.8'
jinja2>=2.11.1
netdisco==2.8.0
netdisco==2.8.1
paho-mqtt==1.5.0
pip>=8.0.3
python-slugify==4.0.0
@@ -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
@@ -936,7 +936,7 @@ netdata==0.2.0
# homeassistant.components.discovery
# homeassistant.components.ssdp
netdisco==2.8.0
netdisco==2.8.1
# homeassistant.components.neurio_energy
neurio==0.3.1
@@ -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
@@ -1701,7 +1701,7 @@ python-juicenet==1.0.1
# python-lirc==1.2.3
# homeassistant.components.xiaomi_miio
python-miio==0.5.2.1
python-miio==0.5.3
# homeassistant.components.mpd
python-mpd2==1.0.0
@@ -1945,7 +1945,7 @@ simplisafe-python==9.2.1
sisyphus-control==2.2.1
# homeassistant.components.skybell
skybellpy==0.4.0
skybellpy==0.6.1
# homeassistant.components.slack
slackclient==2.5.0
@@ -2080,7 +2080,7 @@ temperusb==1.5.3
# tensorflow==1.13.2
# homeassistant.components.powerwall
tesla-powerwall==0.2.11
tesla-powerwall==0.2.12
# homeassistant.components.tesla
teslajsonpy==0.9.3
@@ -2204,7 +2204,7 @@ wled==0.4.3
xbee-helper==0.0.7
# homeassistant.components.xbox_live
xboxapi==2.0.0
xboxapi==2.0.1
# homeassistant.components.xfinity
xfinity-gateway==0.0.4

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
@@ -431,7 +431,7 @@ nessclient==0.9.15
# homeassistant.components.discovery
# homeassistant.components.ssdp
netdisco==2.8.0
netdisco==2.8.1
# homeassistant.components.nexia
nexia==0.9.3
@@ -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
@@ -767,7 +767,7 @@ python-izone==1.1.2
python-juicenet==1.0.1
# homeassistant.components.xiaomi_miio
python-miio==0.5.2.1
python-miio==0.5.3
# homeassistant.components.nest
python-nest==4.1.0
@@ -909,7 +909,7 @@ sunwatcher==0.2.1
tellduslive==0.10.11
# homeassistant.components.powerwall
tesla-powerwall==0.2.11
tesla-powerwall==0.2.12
# homeassistant.components.tesla
teslajsonpy==0.9.3

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

@@ -4,7 +4,7 @@ import aiohttp
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.adguard import config_flow
from homeassistant.components.adguard.const import DOMAIN, MIN_ADGUARD_HOME_VERSION
from homeassistant.components.adguard.const import DOMAIN
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
@@ -229,52 +229,3 @@ async def test_hassio_connection_error(hass, aioclient_mock):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "hassio_confirm"
assert result["errors"] == {"base": "connection_error"}
async def test_outdated_adguard_version(hass, aioclient_mock):
"""Test we show abort when connecting with unsupported AdGuard version."""
aioclient_mock.get(
f"{'https' if FIXTURE_USER_INPUT[CONF_SSL] else 'http'}"
f"://{FIXTURE_USER_INPUT[CONF_HOST]}"
f":{FIXTURE_USER_INPUT[CONF_PORT]}/control/status",
json={"version": "v0.98.0"},
headers={"Content-Type": "application/json"},
)
flow = config_flow.AdGuardHomeFlowHandler()
flow.hass = hass
result = await flow.async_step_user(user_input=None)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "adguard_home_outdated"
assert result["description_placeholders"] == {
"current_version": "v0.98.0",
"minimal_version": MIN_ADGUARD_HOME_VERSION,
}
async def test_outdated_adguard_addon_version(hass, aioclient_mock):
"""Test we show abort when connecting with unsupported AdGuard add-on version."""
aioclient_mock.get(
"http://mock-adguard:3000/control/status",
json={"version": "v0.98.0"},
headers={"Content-Type": "application/json"},
)
result = await hass.config_entries.flow.async_init(
"adguard",
data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": 3000},
context={"source": "hassio"},
)
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "adguard_home_addon_outdated"
assert result["description_placeholders"] == {
"current_version": "v0.98.0",
"minimal_version": MIN_ADGUARD_HOME_VERSION,
}

View File

@@ -1,4 +1,6 @@
"""The tests for the automation component."""
import asyncio
import pytest
from homeassistant.components import logbook
@@ -12,10 +14,11 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_NAME,
EVENT_HOMEASSISTANT_STARTED,
SERVICE_TURN_OFF,
STATE_OFF,
STATE_ON,
)
from homeassistant.core import Context, CoreState, State
from homeassistant.core import Context, CoreState, State, callback
from homeassistant.exceptions import HomeAssistantError, Unauthorized
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -553,6 +556,58 @@ async def test_reload_config_handles_load_fails(hass, calls):
assert len(calls) == 2
@pytest.mark.parametrize("service", ["turn_off", "reload"])
async def test_automation_stops(hass, calls, service):
"""Test that turning off / reloading an automation stops any running actions."""
entity_id = "automation.hello"
test_entity = "test.entity"
config = {
automation.DOMAIN: {
"alias": "hello",
"trigger": {"platform": "event", "event_type": "test_event"},
"action": [
{"event": "running"},
{"wait_template": "{{ is_state('test.entity', 'goodbye') }}"},
{"service": "test.automation"},
],
}
}
assert await async_setup_component(hass, automation.DOMAIN, config,)
running = asyncio.Event()
@callback
def running_cb(event):
running.set()
hass.bus.async_listen_once("running", running_cb)
hass.states.async_set(test_entity, "hello")
hass.bus.async_fire("test_event")
await running.wait()
if service == "turn_off":
await hass.services.async_call(
automation.DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
else:
with patch(
"homeassistant.config.load_yaml_config_file",
autospec=True,
return_value=config,
):
await common.async_reload(hass)
hass.states.async_set(test_entity, "goodbye")
await hass.async_block_till_done()
assert len(calls) == 0
async def test_automation_restore_state(hass):
"""Ensure states are restored on startup."""
time = dt_util.utcnow()

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

@@ -145,6 +145,33 @@ async def test_form_ssdp(hass):
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_ssdp_aborts_before_checking_remoteid_if_host_known(hass):
"""Test we abort without connecting if the host is already known."""
await setup.async_setup_component(hass, "persistent_notification", {})
config_entry = MockConfigEntry(
domain=DOMAIN, data={"host": "2.2.2.2", "name": "any"},
)
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(
"homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_SSDP},
data={
"friendlyName": "Harmony Hub",
"ssdp_location": "http://2.2.2.2:8088/description",
},
)
assert result["type"] == "abort"
async def test_form_cannot_connect(hass):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(

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"

View File

@@ -97,7 +97,7 @@ MOCK_CALLS_ENTRY_WS = {
"host": "fake",
"name": "HomeAssistant",
"port": 8001,
"timeout": 1,
"timeout": 10,
"token": "abcde",
}

View File

@@ -4,6 +4,7 @@ import asyncio
from contextlib import contextmanager
from datetime import timedelta
import logging
from types import MappingProxyType
from unittest import mock
import pytest
@@ -122,7 +123,7 @@ async def test_firing_event_template(hass):
)
script_obj = script.Script(hass, sequence)
await script_obj.async_run({"is_world": "yes"}, context=context)
await script_obj.async_run(MappingProxyType({"is_world": "yes"}), context=context)
await hass.async_block_till_done()
assert len(events) == 1
@@ -175,7 +176,7 @@ async def test_calling_service_template(hass):
)
script_obj = script.Script(hass, sequence)
await script_obj.async_run({"is_world": "yes"}, context=context)
await script_obj.async_run(MappingProxyType({"is_world": "yes"}), context=context)
await hass.async_block_till_done()
assert len(calls) == 1
@@ -235,7 +236,9 @@ async def test_multiple_runs_no_wait(hass):
logger.debug("starting 1st script")
hass.async_create_task(
script_obj.async_run(
{"fire1": "1", "listen1": "2", "fire2": "3", "listen2": "4"}
MappingProxyType(
{"fire1": "1", "listen1": "2", "fire2": "3", "listen2": "4"}
)
)
)
await asyncio.wait_for(heard_event.wait(), 1)
@@ -243,7 +246,7 @@ async def test_multiple_runs_no_wait(hass):
logger.debug("starting 2nd script")
await script_obj.async_run(
{"fire1": "2", "listen1": "3", "fire2": "4", "listen2": "4"}
MappingProxyType({"fire1": "2", "listen1": "3", "fire2": "4", "listen2": "4"})
)
await hass.async_block_till_done()
@@ -670,7 +673,9 @@ async def test_wait_template_variables(hass):
try:
hass.states.async_set("switch.test", "on")
hass.async_create_task(script_obj.async_run({"data": "switch.test"}))
hass.async_create_task(
script_obj.async_run(MappingProxyType({"data": "switch.test"}))
)
await asyncio.wait_for(wait_started_flag.wait(), 1)
assert script_obj.is_running
@@ -882,7 +887,14 @@ async def test_repeat_var_in_condition(hass, condition):
assert len(events) == 2
async def test_repeat_nested(hass):
@pytest.mark.parametrize(
"variables,first_last,inside_x",
[
(None, {"repeat": "None", "x": "None"}, "None"),
(MappingProxyType({"x": 1}), {"repeat": "None", "x": "1"}, "1"),
],
)
async def test_repeat_nested(hass, variables, first_last, inside_x):
"""Test nested repeats."""
event = "test_event"
events = async_capture_events(hass, event)
@@ -892,7 +904,8 @@ async def test_repeat_nested(hass):
{
"event": event,
"event_data_template": {
"repeat": "{{ None if repeat is not defined else repeat }}"
"repeat": "{{ None if repeat is not defined else repeat }}",
"x": "{{ None if x is not defined else x }}",
},
},
{
@@ -905,6 +918,7 @@ async def test_repeat_nested(hass):
"first": "{{ repeat.first }}",
"index": "{{ repeat.index }}",
"last": "{{ repeat.last }}",
"x": "{{ None if x is not defined else x }}",
},
},
{
@@ -916,6 +930,7 @@ async def test_repeat_nested(hass):
"first": "{{ repeat.first }}",
"index": "{{ repeat.index }}",
"last": "{{ repeat.last }}",
"x": "{{ None if x is not defined else x }}",
},
},
}
@@ -926,6 +941,7 @@ async def test_repeat_nested(hass):
"first": "{{ repeat.first }}",
"index": "{{ repeat.index }}",
"last": "{{ repeat.last }}",
"x": "{{ None if x is not defined else x }}",
},
},
],
@@ -934,7 +950,8 @@ async def test_repeat_nested(hass):
{
"event": event,
"event_data_template": {
"repeat": "{{ None if repeat is not defined else repeat }}"
"repeat": "{{ None if repeat is not defined else repeat }}",
"x": "{{ None if x is not defined else x }}",
},
},
]
@@ -945,21 +962,21 @@ async def test_repeat_nested(hass):
"homeassistant.helpers.condition._LOGGER.error",
side_effect=AssertionError("Template Error"),
):
await script_obj.async_run()
await script_obj.async_run(variables)
assert len(events) == 10
assert events[0].data == {"repeat": "None"}
assert events[-1].data == {"repeat": "None"}
assert events[0].data == first_last
assert events[-1].data == first_last
for index, result in enumerate(
(
("True", "1", "False"),
("True", "1", "False"),
("False", "2", "True"),
("True", "1", "False"),
("False", "2", "True"),
("True", "1", "False"),
("False", "2", "True"),
("False", "2", "True"),
("True", "1", "False", inside_x),
("True", "1", "False", inside_x),
("False", "2", "True", inside_x),
("True", "1", "False", inside_x),
("False", "2", "True", inside_x),
("True", "1", "False", inside_x),
("False", "2", "True", inside_x),
("False", "2", "True", inside_x),
),
1,
):
@@ -967,6 +984,7 @@ async def test_repeat_nested(hass):
"first": result[0],
"index": result[1],
"last": result[2],
"x": result[3],
}
@@ -998,13 +1016,38 @@ async def test_choose(hass, var, result):
)
script_obj = script.Script(hass, sequence)
await script_obj.async_run({"var": var})
await script_obj.async_run(MappingProxyType({"var": var}))
await hass.async_block_till_done()
assert len(events) == 1
assert events[0].data["choice"] == result
@pytest.mark.parametrize(
"action",
[
{"repeat": {"count": 1, "sequence": {"event": "abc"}}},
{"choose": {"conditions": [], "sequence": {"event": "abc"}}},
{"choose": [], "default": {"event": "abc"}},
],
)
async def test_multiple_runs_repeat_choose(hass, caplog, action):
"""Test parallel runs with repeat & choose actions & max_runs > default."""
max_runs = script.DEFAULT_MAX + 1
script_obj = script.Script(
hass, cv.SCRIPT_SCHEMA(action), script_mode="parallel", max_runs=max_runs
)
events = async_capture_events(hass, "abc")
for _ in range(max_runs):
hass.async_create_task(script_obj.async_run())
await hass.async_block_till_done()
assert "WARNING" not in caplog.text
assert "ERROR" not in caplog.text
assert len(events) == max_runs
async def test_last_triggered(hass):
"""Test the last_triggered."""
event = "test_event"

View File

@@ -1408,9 +1408,62 @@ async def test_log_blocking_events(hass, caplog):
hass.async_create_task(_wait_a_bit_1())
await hass.async_block_till_done()
with patch.object(ha, "BLOCK_LOG_TIMEOUT", 0.00001):
with patch.object(ha, "BLOCK_LOG_TIMEOUT", 0.0001):
hass.async_create_task(_wait_a_bit_2())
await hass.async_block_till_done()
assert "_wait_a_bit_2" in caplog.text
assert "_wait_a_bit_1" not in caplog.text
async def test_chained_logging_hits_log_timeout(hass, caplog):
"""Ensure we log which task is blocking startup when there is a task chain and debug logging is on."""
caplog.set_level(logging.DEBUG)
created = 0
async def _task_chain_1():
nonlocal created
created += 1
if created > 10:
return
hass.async_create_task(_task_chain_2())
async def _task_chain_2():
nonlocal created
created += 1
if created > 10:
return
hass.async_create_task(_task_chain_1())
with patch.object(ha, "BLOCK_LOG_TIMEOUT", 0.0001):
hass.async_create_task(_task_chain_1())
await hass.async_block_till_done()
assert "_task_chain_" in caplog.text
async def test_chained_logging_misses_log_timeout(hass, caplog):
"""Ensure we do not log which task is blocking startup if we do not hit the timeout."""
caplog.set_level(logging.DEBUG)
created = 0
async def _task_chain_1():
nonlocal created
created += 1
if created > 10:
return
hass.async_create_task(_task_chain_2())
async def _task_chain_2():
nonlocal created
created += 1
if created > 10:
return
hass.async_create_task(_task_chain_1())
hass.async_create_task(_task_chain_1())
await hass.async_block_till_done()
assert "_task_chain_" not in caplog.text