Merge pull request #68592 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen
2022-03-23 21:04:33 -07:00
committed by GitHub
23 changed files with 127 additions and 76 deletions

View File

@@ -19,6 +19,7 @@ from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PIN
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.network import is_ipv6_address
from .const import CONF_CREDENTIALS, CONF_IDENTIFIERS, CONF_START_OFF, DOMAIN
@@ -166,6 +167,8 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) -> data_entry_flow.FlowResult:
"""Handle device found via zeroconf."""
host = discovery_info.host
if is_ipv6_address(host):
return self.async_abort(reason="ipv6_not_supported")
self._async_abort_entries_match({CONF_ADDRESS: host})
service_type = discovery_info.type[:-1] # Remove leading .
name = discovery_info.name.replace(f".{service_type}.", "")

View File

@@ -48,6 +48,7 @@
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
"ipv6_not_supported": "IPv6 is not supported.",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"device_did_not_pair": "No attempt to finish pairing process was made from the device.",

View File

@@ -2,13 +2,12 @@
"config": {
"abort": {
"already_configured": "Device is already configured",
"already_configured_device": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress",
"backoff": "Device does not accept pairing requests at this time (you might have entered an invalid PIN code too many times), try again later.",
"device_did_not_pair": "No attempt to finish pairing process was made from the device.",
"device_not_found": "Device was not found during discovery, please try adding it again.",
"inconsistent_device": "Expected protocols were not found during discovery. This normally indicates a problem with multicast DNS (Zeroconf). Please try adding the device again.",
"invalid_config": "The configuration for this device is incomplete. Please try adding it again.",
"ipv6_not_supported": "IPv6 is not supported.",
"no_devices_found": "No devices found on the network",
"reauth_successful": "Re-authentication was successful",
"setup_failed": "Failed to set up device.",
@@ -18,7 +17,6 @@
"already_configured": "Device is already configured",
"invalid_auth": "Invalid authentication",
"no_devices_found": "No devices found on the network",
"no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.",
"unknown": "Unexpected error"
},
"flow_title": "{name} ({type})",
@@ -72,6 +70,5 @@
"description": "Configure general device settings"
}
}
},
"title": "Apple TV"
}
}

View File

@@ -2,7 +2,7 @@
"domain": "emulated_kasa",
"name": "Emulated Kasa",
"documentation": "https://www.home-assistant.io/integrations/emulated_kasa",
"requirements": ["sense_energy==0.10.2"],
"requirements": ["sense_energy==0.10.3"],
"codeowners": ["@kbickar"],
"quality_scale": "internal",
"iot_class": "local_push",

View File

@@ -6,6 +6,7 @@ from collections.abc import Callable
import logging
from typing import Any
import aiohttp
from aiohttp import client_exceptions
from aiohue import HueBridgeV1, HueBridgeV2, LinkButtonNotPressed, Unauthorized
from aiohue.errors import AiohueException, BridgeBusy
@@ -14,7 +15,7 @@ import async_timeout
from homeassistant import core
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_HOST, Platform
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import aiohttp_client
from .const import CONF_API_VERSION, DOMAIN
@@ -116,22 +117,23 @@ class HueBridge:
self.authorized = True
return True
async def async_request_call(
self, task: Callable, *args, allowed_errors: list[str] | None = None, **kwargs
) -> Any:
"""Send request to the Hue bridge, optionally omitting error(s)."""
async def async_request_call(self, task: Callable, *args, **kwargs) -> Any:
"""Send request to the Hue bridge."""
try:
return await task(*args, **kwargs)
except AiohueException as err:
# The (new) Hue api can be a bit fanatic with throwing errors
# some of which we accept in certain conditions
# handle that here. Note that these errors are strings and do not have
# an identifier or something.
if allowed_errors is not None and str(err) in allowed_errors:
# The (new) Hue api can be a bit fanatic with throwing errors so
# we have some logic to treat some responses as warning only.
msg = f"Request failed: {err}"
if "may not have effect" in str(err):
# log only
self.logger.debug("Ignored error/warning from Hue API: %s", str(err))
self.logger.debug(msg)
return None
raise err
raise HomeAssistantError(msg) from err
except aiohttp.ClientError as err:
raise HomeAssistantError(
f"Request failed due connection error: {err}"
) from err
async def async_reset(self) -> bool:
"""Reset this bridge to default state.

View File

@@ -3,7 +3,7 @@
"name": "Philips Hue",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hue",
"requirements": ["aiohue==4.3.0"],
"requirements": ["aiohue==4.4.1"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",

View File

@@ -37,21 +37,6 @@ from .helpers import (
normalize_hue_transition,
)
ALLOWED_ERRORS = [
"device (groupedLight) has communication issues, command (on) may not have effect",
'device (groupedLight) is "soft off", command (on) may not have effect',
"device (light) has communication issues, command (on) may not have effect",
'device (light) is "soft off", command (on) may not have effect',
"device (grouped_light) has communication issues, command (.on) may not have effect",
'device (grouped_light) is "soft off", command (.on) may not have effect'
"device (grouped_light) has communication issues, command (.on.on) may not have effect",
'device (grouped_light) is "soft off", command (.on.on) may not have effect'
"device (light) has communication issues, command (.on) may not have effect",
'device (light) is "soft off", command (.on) may not have effect',
"device (light) has communication issues, command (.on.on) may not have effect",
'device (light) is "soft off", command (.on.on) may not have effect',
]
async def async_setup_entry(
hass: HomeAssistant,
@@ -183,10 +168,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
and flash is None
):
await self.bridge.async_request_call(
self.controller.set_state,
id=self.resource.id,
on=True,
allowed_errors=ALLOWED_ERRORS,
self.controller.set_state, id=self.resource.id, on=True
)
return
@@ -202,7 +184,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
color_xy=xy_color if light.supports_color else None,
color_temp=color_temp if light.supports_color_temperature else None,
transition_time=transition,
allowed_errors=ALLOWED_ERRORS,
)
for light in self.controller.get_lights(self.resource.id)
]
@@ -222,10 +203,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
# To set other features, you'll have to control the attached lights
if transition is None:
await self.bridge.async_request_call(
self.controller.set_state,
id=self.resource.id,
on=False,
allowed_errors=ALLOWED_ERRORS,
self.controller.set_state, id=self.resource.id, on=False
)
return
@@ -237,7 +215,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
light.id,
on=False,
transition_time=transition,
allowed_errors=ALLOWED_ERRORS,
)
for light in self.controller.get_lights(self.resource.id)
]

View File

@@ -36,15 +36,6 @@ from .helpers import (
normalize_hue_transition,
)
ALLOWED_ERRORS = [
"device (light) has communication issues, command (on) may not have effect",
'device (light) is "soft off", command (on) may not have effect',
"device (light) has communication issues, command (.on) may not have effect",
'device (light) is "soft off", command (.on) may not have effect',
"device (light) has communication issues, command (.on.on) may not have effect",
'device (light) is "soft off", command (.on.on) may not have effect',
]
async def async_setup_entry(
hass: HomeAssistant,
@@ -182,7 +173,6 @@ class HueLight(HueBaseEntity, LightEntity):
color_xy=xy_color,
color_temp=color_temp,
transition_time=transition,
allowed_errors=ALLOWED_ERRORS,
)
async def async_turn_off(self, **kwargs: Any) -> None:
@@ -202,7 +192,6 @@ class HueLight(HueBaseEntity, LightEntity):
id=self.resource.id,
on=False,
transition_time=transition,
allowed_errors=ALLOWED_ERRORS,
)
async def async_set_flash(self, flash: str) -> None:

View File

@@ -12,7 +12,7 @@ from homeassistant import config_entries
from homeassistant.const import CONF_CODE, CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, DOMAIN, SENSE_TIMEOUT_EXCEPTIONS
from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, DOMAIN, SENSE_CONNECT_EXCEPTIONS
_LOGGER = logging.getLogger(__name__)
@@ -76,7 +76,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.validate_input(user_input)
except SenseMFARequiredException:
return await self.async_step_validation()
except SENSE_TIMEOUT_EXCEPTIONS:
except SENSE_CONNECT_EXCEPTIONS:
errors["base"] = "cannot_connect"
except SenseAuthenticationException:
errors["base"] = "invalid_auth"
@@ -93,7 +93,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if user_input:
try:
await self._gateway.validate_mfa(user_input[CONF_CODE])
except SENSE_TIMEOUT_EXCEPTIONS:
except SENSE_CONNECT_EXCEPTIONS:
errors["base"] = "cannot_connect"
except SenseAuthenticationException:
errors["base"] = "invalid_auth"

View File

@@ -3,8 +3,11 @@
import asyncio
import socket
from sense_energy import SenseAPITimeoutException
from sense_energy.sense_exceptions import SenseWebsocketException
from sense_energy import (
SenseAPIException,
SenseAPITimeoutException,
SenseWebsocketException,
)
DOMAIN = "sense"
DEFAULT_TIMEOUT = 10
@@ -40,6 +43,11 @@ ICON = "mdi:flash"
SENSE_TIMEOUT_EXCEPTIONS = (asyncio.TimeoutError, SenseAPITimeoutException)
SENSE_EXCEPTIONS = (socket.gaierror, SenseWebsocketException)
SENSE_CONNECT_EXCEPTIONS = (
asyncio.TimeoutError,
SenseAPITimeoutException,
SenseAPIException,
)
MDI_ICONS = {
"ac": "air-conditioner",

View File

@@ -2,7 +2,7 @@
"domain": "sense",
"name": "Sense",
"documentation": "https://www.home-assistant.io/integrations/sense",
"requirements": ["sense_energy==0.10.2"],
"requirements": ["sense_energy==0.10.3"],
"codeowners": ["@kbickar"],
"config_flow": true,
"dhcp": [

View File

@@ -63,6 +63,7 @@ from .media import SonosMedia
from .statistics import ActivityStatistics, EventStatistics
NEVER_TIME = -1200.0
RESUB_COOLDOWN_SECONDS = 10.0
EVENT_CHARGING = {
"CHARGING": True,
"NOT_CHARGING": False,
@@ -126,6 +127,7 @@ class SonosSpeaker:
self._last_event_cache: dict[str, Any] = {}
self.activity_stats: ActivityStatistics = ActivityStatistics(self.zone_name)
self.event_stats: EventStatistics = EventStatistics(self.zone_name)
self._resub_cooldown_expires_at: float | None = None
# Scheduled callback handles
self._poll_timer: Callable | None = None
@@ -502,6 +504,16 @@ class SonosSpeaker:
@callback
def speaker_activity(self, source):
"""Track the last activity on this speaker, set availability and resubscribe."""
if self._resub_cooldown_expires_at:
if time.monotonic() < self._resub_cooldown_expires_at:
_LOGGER.debug(
"Activity on %s from %s while in cooldown, ignoring",
self.zone_name,
source,
)
return
self._resub_cooldown_expires_at = None
_LOGGER.debug("Activity on %s from %s", self.zone_name, source)
self._last_activity = time.monotonic()
self.activity_stats.activity(source, self._last_activity)
@@ -542,6 +554,10 @@ class SonosSpeaker:
if not self.available:
return
if self._resub_cooldown_expires_at is None and not self.hass.is_stopping:
self._resub_cooldown_expires_at = time.monotonic() + RESUB_COOLDOWN_SECONDS
_LOGGER.debug("Starting resubscription cooldown for %s", self.zone_name)
self.available = False
self.async_write_entity_states()

View File

@@ -2,7 +2,7 @@
"domain": "synology_dsm",
"name": "Synology DSM",
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
"requirements": ["py-synologydsm-api==1.0.6"],
"requirements": ["py-synologydsm-api==1.0.7"],
"codeowners": ["@hacf-fr", "@Quentame", "@mib1185"],
"config_flow": true,
"ssdp": [

View File

@@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 3
PATCH_VERSION: Final = "6"
PATCH_VERSION: Final = "7"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@@ -218,9 +218,12 @@ def async_prepare_call_from_config(
if CONF_ENTITY_ID in target:
registry = entity_registry.async_get(hass)
target[CONF_ENTITY_ID] = entity_registry.async_resolve_entity_ids(
registry, cv.comp_entity_ids_or_uuids(target[CONF_ENTITY_ID])
entity_ids = cv.comp_entity_ids_or_uuids(target[CONF_ENTITY_ID])
if entity_ids not in (ENTITY_MATCH_ALL, ENTITY_MATCH_NONE):
entity_ids = entity_registry.async_resolve_entity_ids(
registry, entity_ids
)
target[CONF_ENTITY_ID] = entity_ids
except TemplateError as ex:
raise HomeAssistantError(
f"Error rendering service target template: {ex}"

View File

@@ -191,7 +191,7 @@ aiohomekit==0.7.16
aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==4.3.0
aiohue==4.4.1
# homeassistant.components.homewizard
aiohwenergy==0.8.0
@@ -1338,7 +1338,7 @@ py-nightscout==1.2.2
py-schluter==0.1.7
# homeassistant.components.synology_dsm
py-synologydsm-api==1.0.6
py-synologydsm-api==1.0.7
# homeassistant.components.zabbix
py-zabbix==1.1.7
@@ -2179,7 +2179,7 @@ sense-hat==2.2.0
# homeassistant.components.emulated_kasa
# homeassistant.components.sense
sense_energy==0.10.2
sense_energy==0.10.3
# homeassistant.components.sentry
sentry-sdk==1.5.5

View File

@@ -141,7 +141,7 @@ aiohomekit==0.7.16
aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==4.3.0
aiohue==4.4.1
# homeassistant.components.homewizard
aiohwenergy==0.8.0
@@ -842,7 +842,7 @@ py-melissa-climate==2.1.4
py-nightscout==1.2.2
# homeassistant.components.synology_dsm
py-synologydsm-api==1.0.6
py-synologydsm-api==1.0.7
# homeassistant.components.seventeentrack
py17track==2021.12.2
@@ -1344,7 +1344,7 @@ screenlogicpy==0.5.4
# homeassistant.components.emulated_kasa
# homeassistant.components.sense
sense_energy==0.10.2
sense_energy==0.10.3
# homeassistant.components.sentry
sentry-sdk==1.5.5

View File

@@ -1,6 +1,6 @@
[metadata]
name = homeassistant
version = 2022.3.6
version = 2022.3.7
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0

View File

@@ -1066,3 +1066,22 @@ async def test_option_start_off(hass):
assert result2["type"] == "create_entry"
assert config_entry.options[CONF_START_OFF]
async def test_zeroconf_rejects_ipv6(hass):
"""Test zeroconf discovery rejects ipv6."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=zeroconf.ZeroconfServiceInfo(
host="fd00::b27c:63bb:cc85:4ea0",
addresses=["fd00::b27c:63bb:cc85:4ea0"],
hostname="mock_hostname",
port=None,
type="_touch-able._tcp.local.",
name="dmapid._touch-able._tcp.local.",
properties={"CtlN": "Apple TV"},
),
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "ipv6_not_supported"

View File

@@ -60,7 +60,7 @@ def create_mock_bridge(hass, api_version=1):
bridge.async_initialize_bridge = async_initialize_bridge
async def async_request_call(task, *args, allowed_errors=None, **kwargs):
async def async_request_call(task, *args, **kwargs):
await task(*args, **kwargs)
bridge.async_request_call = async_request_call

View File

@@ -460,7 +460,7 @@
"model_id": "BSB002",
"product_archetype": "bridge_v2",
"product_name": "Philips hue",
"software_version": "1.48.1948086000"
"software_version": "1.50.1950111030"
},
"services": [
{

View File

@@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, patch
import pytest
from sense_energy import (
SenseAPIException,
SenseAPITimeoutException,
SenseAuthenticationException,
SenseMFARequiredException,
@@ -189,7 +190,7 @@ async def test_form_mfa_required_exception(hass, mock_sense):
assert result3["errors"] == {"base": "unknown"}
async def test_form_cannot_connect(hass):
async def test_form_timeout(hass):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -208,6 +209,25 @@ async def test_form_cannot_connect(hass):
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_cannot_connect(hass):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"sense_energy.ASyncSenseable.authenticate",
side_effect=SenseAPIException,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"timeout": "6", "email": "test-email", "password": "test-password"},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_unknown_exception(hass):
"""Test we handle unknown error."""
result = await hass.config_entries.flow.async_init(

View File

@@ -397,6 +397,22 @@ async def test_service_call_entry_id(hass):
assert dict(calls[0].data) == {"entity_id": ["hello.world"]}
@pytest.mark.parametrize("target", ("all", "none"))
async def test_service_call_all_none(hass, target):
"""Test service call targeting all."""
calls = async_mock_service(hass, "test_domain", "test_service")
config = {
"service": "test_domain.test_service",
"target": {"entity_id": target},
}
await service.async_call_from_config(hass, config)
await hass.async_block_till_done()
assert dict(calls[0].data) == {"entity_id": target}
async def test_extract_entity_ids(hass):
"""Test extract_entity_ids method."""
hass.states.async_set("light.Bowl", STATE_ON)