Merge pull request #68493 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen
2022-03-21 21:57:00 -07:00
committed by GitHub
23 changed files with 212 additions and 37 deletions

View File

@@ -149,9 +149,19 @@ def _async_register_mac(
return
# Make sure entity has a config entry and was disabled by the
# default disable logic in the integration.
# default disable logic in the integration and new entities
# are allowed to be added.
if (
entity_entry.config_entry_id is None
or (
(
config_entry := hass.config_entries.async_get_entry(
entity_entry.config_entry_id
)
)
is not None
and config_entry.pref_disable_new_entities
)
or entity_entry.disabled_by != er.RegistryEntryDisabler.INTEGRATION
):
return

View File

@@ -16,6 +16,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.util.network import is_ipv4_address
from .const import DOMAIN
@@ -86,6 +87,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> FlowResult:
"""Handle a flow initialized by zeroconf discovery."""
if not is_ipv4_address(discovery_info.host):
return self.async_abort(reason="not_ipv4_address")
serial = discovery_info.properties["serialnum"]
await self.async_set_unique_id(serial)
self.ip_address = discovery_info.host

View File

@@ -2,7 +2,8 @@
"config": {
"abort": {
"already_configured": "Device is already configured",
"reauth_successful": "Re-authentication was successful"
"reauth_successful": "Re-authentication was successful",
"not_ipv4_address": "Only IPv4 addresess are supported"
},
"error": {
"cannot_connect": "Failed to connect",

View File

@@ -6,6 +6,7 @@ import logging
from typing import Any
from urllib.parse import urlparse
import aiohttp
from aiohue import LinkButtonNotPressed, create_app_key
from aiohue.discovery import DiscoveredHueBridge, discover_bridge, discover_nupnp
from aiohue.util import normalize_bridge_id
@@ -70,9 +71,12 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self, host: str, bridge_id: str | None = None
) -> DiscoveredHueBridge:
"""Return a DiscoveredHueBridge object."""
bridge = await discover_bridge(
host, websession=aiohttp_client.async_get_clientsession(self.hass)
)
try:
bridge = await discover_bridge(
host, websession=aiohttp_client.async_get_clientsession(self.hass)
)
except aiohttp.ClientError:
return None
if bridge_id is not None:
bridge_id = normalize_bridge_id(bridge_id)
assert bridge_id == bridge.id

View File

@@ -42,6 +42,14 @@ ALLOWED_ERRORS = [
'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',
]

View File

@@ -39,6 +39,10 @@ from .helpers import (
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',
]

View File

@@ -1,6 +1,7 @@
"""Support for Honeywell Lyric climate platform."""
from __future__ import annotations
import asyncio
import logging
from time import localtime, strftime, time
@@ -22,6 +23,7 @@ from homeassistant.components.climate.const import (
HVAC_MODE_OFF,
SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE
@@ -45,7 +47,11 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
# Only LCC models support presets
SUPPORT_FLAGS_LCC = (
SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE_RANGE
)
SUPPORT_FLAGS_TCC = SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE
LYRIC_HVAC_ACTION_OFF = "EquipmentOff"
LYRIC_HVAC_ACTION_HEAT = "Heat"
@@ -166,7 +172,11 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
@property
def supported_features(self) -> int:
"""Return the list of supported features."""
return SUPPORT_FLAGS
if self.device.changeableValues.thermostatSetpointStatus:
support_flags = SUPPORT_FLAGS_LCC
else:
support_flags = SUPPORT_FLAGS_TCC
return support_flags
@property
def temperature_unit(self) -> str:
@@ -200,25 +210,28 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
device = self.device
if not device.hasDualSetpointStatus:
if (
not device.changeableValues.autoChangeoverActive
and HVAC_MODES[device.changeableValues.mode] != HVAC_MODE_OFF
):
if self.hvac_mode == HVAC_MODE_COOL:
return device.changeableValues.coolSetpoint
return device.changeableValues.heatSetpoint
return None
@property
def target_temperature_low(self) -> float | None:
"""Return the upper bound temperature we try to reach."""
def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach."""
device = self.device
if device.hasDualSetpointStatus:
if device.changeableValues.autoChangeoverActive:
return device.changeableValues.coolSetpoint
return None
@property
def target_temperature_high(self) -> float | None:
"""Return the upper bound temperature we try to reach."""
def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach."""
device = self.device
if device.hasDualSetpointStatus:
if device.changeableValues.autoChangeoverActive:
return device.changeableValues.heatSetpoint
return None
@@ -256,11 +269,11 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
async def async_set_temperature(self, **kwargs) -> None:
"""Set new target temperature."""
device = self.device
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
device = self.device
if device.hasDualSetpointStatus:
if device.changeableValues.autoChangeoverActive:
if target_temp_low is None or target_temp_high is None:
raise HomeAssistantError(
"Could not find target_temp_low and/or target_temp_high in arguments"
@@ -270,11 +283,13 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
await self._update_thermostat(
self.location,
device,
coolSetpoint=target_temp_low,
heatSetpoint=target_temp_high,
coolSetpoint=target_temp_high,
heatSetpoint=target_temp_low,
mode=HVAC_MODES[device.changeableValues.heatCoolMode],
)
except LYRIC_EXCEPTIONS as exception:
_LOGGER.error(exception)
await self.coordinator.async_refresh()
else:
temp = kwargs.get(ATTR_TEMPERATURE)
_LOGGER.debug("Set temperature: %s", temp)
@@ -289,15 +304,58 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
)
except LYRIC_EXCEPTIONS as exception:
_LOGGER.error(exception)
await self.coordinator.async_refresh()
await self.coordinator.async_refresh()
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set hvac mode."""
_LOGGER.debug("Set hvac mode: %s", hvac_mode)
_LOGGER.debug("HVAC mode: %s", hvac_mode)
try:
await self._update_thermostat(
self.location, self.device, mode=LYRIC_HVAC_MODES[hvac_mode]
)
if LYRIC_HVAC_MODES[hvac_mode] == LYRIC_HVAC_MODE_HEAT_COOL:
# If the system is off, turn it to Heat first then to Auto, otherwise it turns to
# Auto briefly and then reverts to Off (perhaps related to heatCoolMode). This is the
# behavior that happens with the native app as well, so likely a bug in the api itself
if HVAC_MODES[self.device.changeableValues.mode] == HVAC_MODE_OFF:
_LOGGER.debug(
"HVAC mode passed to lyric: %s",
HVAC_MODES[LYRIC_HVAC_MODE_COOL],
)
await self._update_thermostat(
self.location,
self.device,
mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
autoChangeoverActive=False,
)
# Sleep 3 seconds before proceeding
await asyncio.sleep(3)
_LOGGER.debug(
"HVAC mode passed to lyric: %s",
HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
)
await self._update_thermostat(
self.location,
self.device,
mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
autoChangeoverActive=True,
)
else:
_LOGGER.debug(
"HVAC mode passed to lyric: %s",
HVAC_MODES[self.device.changeableValues.mode],
)
await self._update_thermostat(
self.location, self.device, autoChangeoverActive=True
)
else:
_LOGGER.debug(
"HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode]
)
await self._update_thermostat(
self.location,
self.device,
mode=LYRIC_HVAC_MODES[hvac_mode],
autoChangeoverActive=False,
)
except LYRIC_EXCEPTIONS as exception:
_LOGGER.error(exception)
await self.coordinator.async_refresh()

View File

@@ -243,7 +243,10 @@ class MatrixBot:
room.update_aliases()
self._aliases_fetched_for.add(room.room_id)
if room_id_or_alias in room.aliases:
if (
room_id_or_alias in room.aliases
or room_id_or_alias == room.canonical_alias
):
_LOGGER.debug(
"Already in room %s (known as %s)", room.room_id, room_id_or_alias
)

View File

@@ -43,7 +43,7 @@ async def async_setup_platform(
station_id = config[CONF_STATION_ID]
session = async_get_clientsession(hass)
osm_api = OpenSenseMapData(OpenSenseMap(station_id, hass.loop, session))
osm_api = OpenSenseMapData(OpenSenseMap(station_id, session))
await osm_api.async_update()

View File

@@ -2,7 +2,7 @@
"domain": "opensensemap",
"name": "openSenseMap",
"documentation": "https://www.home-assistant.io/integrations/opensensemap",
"requirements": ["opensensemap-api==0.1.5"],
"requirements": ["opensensemap-api==0.2.0"],
"codeowners": [],
"iot_class": "cloud_polling",
"loggers": ["opensensemap_api"]

View File

@@ -97,6 +97,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
token_saver=token_saver,
)
try:
# pylint: disable-next=fixme
# TODO Remove authlib constraint when refactoring this code
await session.ensure_active_token()
except ConnectTimeout as err:
_LOGGER.debug("Connection Timeout")

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/renault",
"requirements": [
"renault-api==0.1.9"
"renault-api==0.1.10"
],
"codeowners": [
"@epenet"

View File

@@ -319,8 +319,9 @@ class SamsungTVWSBridge(SamsungTVBridge):
def _get_app_list(self) -> dict[str, str] | None:
"""Get installed app list."""
if self._app_list is None and (remote := self._get_remote()):
with contextlib.suppress(WebSocketTimeoutException):
with contextlib.suppress(TypeError, WebSocketTimeoutException):
raw_app_list: list[dict[str, str]] = remote.app_list()
LOGGER.debug("Received app list: %s", raw_app_list)
self._app_list = {
app["name"]: app["appId"]
for app in sorted(raw_app_list, key=lambda app: app["name"])

View File

@@ -88,7 +88,10 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
# Handle turning to temp mode
if ATTR_COLOR_TEMP in kwargs:
color_tmp = mired_to_kelvin(int(kwargs[ATTR_COLOR_TEMP]))
# Handle temp conversion mireds -> kelvin being slightly outside of valid range
kelvin = mired_to_kelvin(int(kwargs[ATTR_COLOR_TEMP]))
kelvin_range = self.device.valid_temperature_range
color_tmp = max(kelvin_range.min, min(kelvin_range.max, kelvin))
_LOGGER.debug("Changing color temp to %s", color_tmp)
await self.device.set_color_temp(
color_tmp, brightness=brightness, transition=transition

View File

@@ -78,4 +78,4 @@ class VelbusCover(VelbusEntity, CoverEntity):
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
self._channel.set_position(100 - kwargs[ATTR_POSITION])
await self._channel.set_position(100 - kwargs[ATTR_POSITION])

View File

@@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 3
PATCH_VERSION: Final = "5"
PATCH_VERSION: Final = "6"
__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

@@ -95,3 +95,7 @@ python-socketio>=4.6.0,<5.0
# Constrain multidict to avoid typing issues
# https://github.com/home-assistant/core/pull/67046
multidict>=6.0.2
# Required for compatibility with point integration - ensure_active_token
# https://github.com/home-assistant/core/pull/68176
authlib<1.0

View File

@@ -1180,7 +1180,7 @@ openevsewifi==1.1.0
openhomedevice==2.0.1
# homeassistant.components.opensensemap
opensensemap-api==0.1.5
opensensemap-api==0.2.0
# homeassistant.components.enigma2
openwebifpy==3.2.7
@@ -2097,7 +2097,7 @@ raspyrfm-client==1.2.8
regenmaschine==2022.01.0
# homeassistant.components.renault
renault-api==0.1.9
renault-api==0.1.10
# homeassistant.components.python_script
restrictedpython==5.2

View File

@@ -1301,7 +1301,7 @@ radios==0.1.1
regenmaschine==2022.01.0
# homeassistant.components.renault
renault-api==0.1.9
renault-api==0.1.10
# homeassistant.components.python_script
restrictedpython==5.2

View File

@@ -124,6 +124,10 @@ python-socketio>=4.6.0,<5.0
# Constrain multidict to avoid typing issues
# https://github.com/home-assistant/core/pull/67046
multidict>=6.0.2
# Required for compatibility with point integration - ensure_active_token
# https://github.com/home-assistant/core/pull/68176
authlib<1.0
"""
IGNORE_PRE_COMMIT_HOOK_ID = (

View File

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

View File

@@ -137,6 +137,39 @@ async def test_register_mac(hass):
assert entity_entry_1.disabled_by is None
async def test_register_mac_ignored(hass):
"""Test ignoring registering a mac."""
dev_reg = dr.async_get(hass)
ent_reg = er.async_get(hass)
config_entry = MockConfigEntry(domain="test", pref_disable_new_entities=True)
config_entry.add_to_hass(hass)
mac1 = "12:34:56:AB:CD:EF"
entity_entry_1 = ent_reg.async_get_or_create(
"device_tracker",
"test",
mac1 + "yo1",
original_name="name 1",
config_entry=config_entry,
disabled_by=er.RegistryEntryDisabler.INTEGRATION,
)
ce._async_register_mac(hass, "test", mac1, mac1 + "yo1")
dev_reg.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, mac1)},
)
await hass.async_block_till_done()
entity_entry_1 = ent_reg.async_get(entity_entry_1.entity_id)
assert entity_entry_1.disabled_by == er.RegistryEntryDisabler.INTEGRATION
async def test_connected_device_registered(hass):
"""Test dispatch on connected device being registered."""

View File

@@ -6,6 +6,7 @@ import httpx
from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.components.enphase_envoy.const import DOMAIN
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@@ -312,8 +313,8 @@ async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None:
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=zeroconf.ZeroconfServiceInfo(
host="1.1.1.1",
addresses=["1.1.1.1"],
host="4.4.4.4",
addresses=["4.4.4.4"],
hostname="mock_hostname",
name="mock_name",
port=None,
@@ -324,6 +325,42 @@ async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None:
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
assert config_entry.data[CONF_HOST] == "4.4.4.4"
async def test_zeroconf_serial_already_exists_ignores_ipv6(hass: HomeAssistant) -> None:
"""Test serial number already exists from zeroconf but the discovery is ipv6."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
"host": "1.1.1.1",
"name": "Envoy",
"username": "test-username",
"password": "test-password",
},
unique_id="1234",
title="Envoy",
)
config_entry.add_to_hass(hass)
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",
name="mock_name",
port=None,
properties={"serialnum": "1234"},
type="mock_type",
),
)
assert result["type"] == "abort"
assert result["reason"] == "not_ipv4_address"
assert config_entry.data[CONF_HOST] == "1.1.1.1"
async def test_zeroconf_host_already_exists(hass: HomeAssistant) -> None: