mirror of
https://github.com/home-assistant/core.git
synced 2026-01-10 09:37:16 +01:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3aa9f9a6f | ||
|
|
c4fcd8bd2e | ||
|
|
b209c1a7b5 | ||
|
|
82b3fe1ab6 | ||
|
|
7a8d9b6065 | ||
|
|
f4afa2dc68 | ||
|
|
2a3947f7cc | ||
|
|
47a729495d | ||
|
|
293655f988 | ||
|
|
0ecaab1a7d | ||
|
|
9819d70941 | ||
|
|
abad6dfdd7 | ||
|
|
dd4e0511a3 | ||
|
|
d65545964b | ||
|
|
d1fbcba7b6 | ||
|
|
1b73bcbff7 | ||
|
|
7a2f6a5006 | ||
|
|
d24b02646e | ||
|
|
60cab62da5 | ||
|
|
612e27b5ff | ||
|
|
d9953a8c2f | ||
|
|
e984e0d414 | ||
|
|
f8fbe8dba3 | ||
|
|
b8d6b20c96 | ||
|
|
c4038c8652 | ||
|
|
b5a64b3752 | ||
|
|
bd1336cbdf | ||
|
|
8768fe1652 | ||
|
|
37e029b2c1 | ||
|
|
c55c415933 | ||
|
|
293db61b32 | ||
|
|
bdab437574 | ||
|
|
e89c475856 | ||
|
|
ee0c32cbb7 | ||
|
|
2e89ec24f7 | ||
|
|
b82e64d9cb | ||
|
|
61fa572068 | ||
|
|
7f2a2ed23b |
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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."]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
1
setup.py
1
setup.py
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -97,7 +97,7 @@ MOCK_CALLS_ENTRY_WS = {
|
||||
"host": "fake",
|
||||
"name": "HomeAssistant",
|
||||
"port": 8001,
|
||||
"timeout": 1,
|
||||
"timeout": 10,
|
||||
"token": "abcde",
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user