diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index adabd297c55..c9b8534c2bc 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -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 diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index fa43cb61ffe..88310579e72 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -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 diff --git a/homeassistant/components/enphase_envoy/translations/en.json b/homeassistant/components/enphase_envoy/translations/en.json index 5d4617ed9fa..ff600fea454 100644 --- a/homeassistant/components/enphase_envoy/translations/en.json +++ b/homeassistant/components/enphase_envoy/translations/en.json @@ -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", diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 0901d9a1e2c..265777814a8 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -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 diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 31c5a502853..948609f4c13 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -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', ] diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index ee40222b083..5b4574c717c 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -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', ] diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index e798aca5831..dfe88048ba4 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -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() diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index 76e2630b26e..03772630a9e 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -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 ) diff --git a/homeassistant/components/opensensemap/air_quality.py b/homeassistant/components/opensensemap/air_quality.py index b8028431796..5999eb91580 100644 --- a/homeassistant/components/opensensemap/air_quality.py +++ b/homeassistant/components/opensensemap/air_quality.py @@ -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() diff --git a/homeassistant/components/opensensemap/manifest.json b/homeassistant/components/opensensemap/manifest.json index 513cb5ac3da..baf62985448 100644 --- a/homeassistant/components/opensensemap/manifest.json +++ b/homeassistant/components/opensensemap/manifest.json @@ -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"] diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 288c16df14a..99108323187 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -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") diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 71e2e7d64b8..d41686f0c1f 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -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" diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 481d3588bb7..616820aec26 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -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"]) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 6efabe537f7..182bef586ee 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -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 diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index 2e2ceb761a9..4afe02f4637 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.py @@ -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]) diff --git a/homeassistant/const.py b/homeassistant/const.py index 36f5c21b279..224bf89b0e2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -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) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 20b3c5badd3..8ec6f3cc67d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index 565cdcae1a3..df63a7159c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ec2c864424..6550c353e05 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index fe8962e4f1e..a8d1d40a7d5 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -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 = ( diff --git a/setup.cfg b/setup.cfg index 5e229a0da9d..c4e7a968cc2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/tests/components/device_tracker/test_config_entry.py b/tests/components/device_tracker/test_config_entry.py index 5134123074e..73b07d31026 100644 --- a/tests/components/device_tracker/test_config_entry.py +++ b/tests/components/device_tracker/test_config_entry.py @@ -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.""" diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index 76179c02e22..caba2296927 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -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: