diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 225ec5f7f99..a7fd72fc1a7 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -122,7 +122,7 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): if has_check_control_messages: cbs_list = [] for message in check_control_messages: - cbs_list.append(message.description_short) + cbs_list.append(message["ccmDescriptionShort"]) result["check_control_messages"] = cbs_list else: result["check_control_messages"] = "OK" diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index 7d1486b2e8f..6f11b8c66e3 100644 --- a/homeassistant/components/bond/manifest.json +++ b/homeassistant/components/bond/manifest.json @@ -3,7 +3,7 @@ "name": "Bond", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bond", - "requirements": ["bond-api==0.1.13"], + "requirements": ["bond-api==0.1.14"], "zeroconf": ["_bond._tcp.local."], "codeowners": ["@prystupa", "@joshs85"], "quality_scale": "platinum", diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index c541aa0e0e3..e2ac03e259c 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -1,12 +1,10 @@ """The Logitech Harmony Hub integration.""" -import asyncio import logging from homeassistant.components.remote import ATTR_ACTIVITY, ATTR_DELAY_SECS from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -34,13 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.data[CONF_HOST] name = entry.data[CONF_NAME] data = HarmonyData(hass, address, name, entry.unique_id) - try: - connected_ok = await data.connect() - except (asyncio.TimeoutError, ValueError, AttributeError) as err: - raise ConfigEntryNotReady from err - - if not connected_ok: - raise ConfigEntryNotReady + await data.connect() await _migrate_old_unique_ids(hass, entry.entry_id, data) @@ -51,8 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: cancel_stop = hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, _async_on_stop) - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { HARMONY_DATA: data, CANCEL_LISTENER: cancel_listener, CANCEL_STOP: cancel_stop, diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index b1e71ac2dab..1c735b0747d 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -1,7 +1,10 @@ """Config flow for Logitech Harmony Hub integration.""" +import asyncio import logging from urllib.parse import urlparse +from aioharmony.hubconnector_websocket import HubConnector +import aiohttp import voluptuous as vol from homeassistant import config_entries, exceptions @@ -94,16 +97,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_NAME: friendly_name, } - harmony = await get_harmony_client_if_available(parsed_url.hostname) - - if harmony: - unique_id = find_unique_id_for_remote(harmony) - await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured( - updates={CONF_HOST: self.harmony_config[CONF_HOST]} - ) - self.harmony_config[UNIQUE_ID] = unique_id + connector = HubConnector(parsed_url.hostname, asyncio.Queue()) + try: + remote_id = await connector.get_remote_id() + except aiohttp.ClientError: + return self.async_abort(reason="cannot_connect") + finally: + await connector.async_close_session() + unique_id = str(remote_id) + await self.async_set_unique_id(str(unique_id)) + self._abort_if_unique_id_configured( + updates={CONF_HOST: self.harmony_config[CONF_HOST]} + ) + self.harmony_config[UNIQUE_ID] = unique_id return await self.async_step_link() async def async_step_link(self, user_input=None): diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index 6fdf18df612..78377265c07 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -1,6 +1,7 @@ """Harmony data object which contains the Harmony Client.""" from __future__ import annotations +import asyncio from collections.abc import Iterable import logging @@ -8,6 +9,8 @@ from aioharmony.const import ClientCallbackType, SendCommandDevice import aioharmony.exceptions as aioexc from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient +from homeassistant.exceptions import ConfigEntryNotReady + from .const import ACTIVITY_POWER_OFF from .subscriber import HarmonySubscriberMixin @@ -109,16 +112,24 @@ class HarmonyData(HarmonySubscriberMixin): ip_address=self._address, callbacks=ClientCallbackType(**callbacks) ) + connected = False try: - if not await self._client.connect(): - _LOGGER.warning("%s: Unable to connect to HUB", self._name) - await self._client.close() - return False - except aioexc.TimeOut: - _LOGGER.warning("%s: Connection timed-out", self._name) - return False - - return True + connected = await self._client.connect() + except (asyncio.TimeoutError, aioexc.TimeOut) as err: + await self._client.close() + raise ConfigEntryNotReady( + f"{self._name}: Connection timed-out to {self._address}:8088" + ) from err + except (ValueError, AttributeError) as err: + await self._client.close() + raise ConfigEntryNotReady( + f"{self._name}: Error {err} while connected HUB at: {self._address}:8088" + ) from err + if not connected: + await self._client.close() + raise ConfigEntryNotReady( + f"{self._name}: Unable to connect to HUB at: {self._address}:8088" + ) async def shutdown(self): """Close connection on shutdown.""" diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index f35f4e99303..d1b1073ebad 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -2,7 +2,7 @@ "domain": "harmony", "name": "Logitech Harmony Hub", "documentation": "https://www.home-assistant.io/integrations/harmony", - "requirements": ["aioharmony==0.2.7"], + "requirements": ["aioharmony==0.2.8"], "codeowners": [ "@ehendrix23", "@bramkragten", diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 854c2e6676c..888f5bac5c2 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -5,6 +5,7 @@ import logging from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, @@ -68,32 +69,32 @@ ENERGY_SENSOR_MAP = { "electricity_consumed_interval": [ "Consumed Power Interval", ENERGY_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "electricity_consumed_peak_interval": [ "Consumed Power Interval", ENERGY_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "electricity_consumed_off_peak_interval": [ "Consumed Power Interval (off peak)", ENERGY_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "electricity_produced_interval": [ "Produced Power Interval", ENERGY_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "electricity_produced_peak_interval": [ "Produced Power Interval", ENERGY_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "electricity_produced_off_peak_interval": [ "Produced Power Interval (off peak)", ENERGY_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "electricity_consumed_off_peak_point": [ "Current Consumed Power (off peak)", @@ -108,12 +109,12 @@ ENERGY_SENSOR_MAP = { "electricity_consumed_off_peak_cumulative": [ "Cumulative Consumed Power (off peak)", ENERGY_KILO_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "electricity_consumed_peak_cumulative": [ "Cumulative Consumed Power", ENERGY_KILO_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "electricity_produced_off_peak_point": [ "Current Produced Power (off peak)", @@ -128,12 +129,12 @@ ENERGY_SENSOR_MAP = { "electricity_produced_off_peak_cumulative": [ "Cumulative Produced Power (off peak)", ENERGY_KILO_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "electricity_produced_peak_cumulative": [ "Cumulative Produced Power", ENERGY_KILO_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "gas_consumed_interval": [ "Current Consumed Gas Interval", @@ -145,7 +146,7 @@ ENERGY_SENSOR_MAP = { "net_electricity_cumulative": [ "Cumulative net Power", ENERGY_KILO_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], } diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 5b52e637c64..56073d7a63d 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -60,7 +60,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_migrate_entries(hass, entry.entry_id, async_migrate_callback) - websession = aiohttp_client.async_get_clientsession(hass) + # Tile's API uses cookies to identify a consumer; in order to allow for multiple + # instances of this config entry, we use a new session each time: + websession = aiohttp_client.async_create_clientsession(hass) try: client = await async_login( diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 39295eed646..4e9913615a9 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -3,7 +3,7 @@ "name": "Tile", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tile", - "requirements": ["pytile==5.2.3"], + "requirements": ["pytile==5.2.4"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index d59e03c965d..f4aab12a34a 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -8,6 +8,7 @@ from yeelight.aio import AsyncBulb from homeassistant import config_entries, exceptions from homeassistant.components.dhcp import IP_ADDRESS +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -66,18 +67,30 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id( "{0:#0{1}x}".format(int(discovery_info["name"][-26:-18]), 18) ) - self._abort_if_unique_id_configured( - updates={CONF_HOST: self._discovered_ip}, reload_on_update=False - ) - return await self._async_handle_discovery() + return await self._async_handle_discovery_with_unique_id() async def async_step_ssdp(self, discovery_info): """Handle discovery from ssdp.""" self._discovered_ip = urlparse(discovery_info["location"]).hostname await self.async_set_unique_id(discovery_info["id"]) - self._abort_if_unique_id_configured( - updates={CONF_HOST: self._discovered_ip}, reload_on_update=False - ) + return await self._async_handle_discovery_with_unique_id() + + async def _async_handle_discovery_with_unique_id(self): + """Handle any discovery with a unique id.""" + for entry in self._async_current_entries(): + if entry.unique_id != self.unique_id: + continue + reload = entry.state == ConfigEntryState.SETUP_RETRY + if entry.data[CONF_HOST] != self._discovered_ip: + self.hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_HOST: self._discovered_ip} + ) + reload = True + if reload: + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") return await self._async_handle_discovery() async def _async_handle_discovery(self): @@ -86,6 +99,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): for progress in self._async_in_progress(): if progress.get("context", {}).get(CONF_HOST) == self._discovered_ip: return self.async_abort(reason="already_in_progress") + self._async_abort_entries_match({CONF_HOST: self._discovered_ip}) try: self._discovered_model = await self._async_try_connect( diff --git a/homeassistant/const.py b/homeassistant/const.py index 02d3b6db432..6895f18472a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 10 -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, 8, 0) diff --git a/requirements_all.txt b/requirements_all.txt index 8a7eb79934d..32793e053b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -179,7 +179,7 @@ aiogithubapi==21.8.0 aioguardian==1.0.8 # homeassistant.components.harmony -aioharmony==0.2.7 +aioharmony==0.2.8 # homeassistant.components.homekit_controller aiohomekit==0.6.3 @@ -415,7 +415,7 @@ blockchain==1.4.4 # bme680==1.0.5 # homeassistant.components.bond -bond-api==0.1.13 +bond-api==0.1.14 # homeassistant.components.bosch_shc boschshcpy==0.2.19 @@ -1964,7 +1964,7 @@ python_opendata_transport==0.2.1 pythonegardia==1.0.40 # homeassistant.components.tile -pytile==5.2.3 +pytile==5.2.4 # homeassistant.components.touchline pytouchline==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 349912a5b25..13bb6f707ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -118,7 +118,7 @@ aioflo==0.4.1 aioguardian==1.0.8 # homeassistant.components.harmony -aioharmony==0.2.7 +aioharmony==0.2.8 # homeassistant.components.homekit_controller aiohomekit==0.6.3 @@ -254,7 +254,7 @@ blebox_uniapi==1.3.3 blinkpy==0.17.0 # homeassistant.components.bond -bond-api==0.1.13 +bond-api==0.1.14 # homeassistant.components.bosch_shc boschshcpy==0.2.19 @@ -1127,7 +1127,7 @@ python-twitch-client==0.6.0 python_awair==0.2.1 # homeassistant.components.tile -pytile==5.2.3 +pytile==5.2.4 # homeassistant.components.traccar pytraccar==0.9.0 diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index d81adabb916..0e8b6dd67d9 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -1,6 +1,8 @@ """Test the Logitech Harmony Hub config flow.""" from unittest.mock import AsyncMock, MagicMock, patch +import aiohttp + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.harmony.config_flow import CannotConnect from homeassistant.components.harmony.const import DOMAIN, PREVIOUS_ACTIVE_ACTIVITY @@ -50,11 +52,9 @@ async def test_form_ssdp(hass): """Test we get the form with ssdp source.""" await setup.async_setup_component(hass, "persistent_notification", {}) - harmonyapi = _get_mock_harmonyapi(connect=True) - with patch( - "homeassistant.components.harmony.util.HarmonyAPI", - return_value=harmonyapi, + "homeassistant.components.harmony.config_flow.HubConnector.get_remote_id", + return_value=1234, ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -76,6 +76,8 @@ async def test_form_ssdp(hass): assert progress[0]["flow_id"] == result["flow_id"] assert progress[0]["context"]["confirm_only"] is True + harmonyapi = _get_mock_harmonyapi(connect=True) + with patch( "homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi, @@ -95,6 +97,25 @@ async def test_form_ssdp(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_ssdp_fails_to_get_remote_id(hass): + """Test we abort if we cannot get the remote id.""" + + with patch( + "homeassistant.components.harmony.config_flow.HubConnector.get_remote_id", + side_effect=aiohttp.ClientError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "friendlyName": "Harmony Hub", + "ssdp_location": "http://192.168.1.12:8088/description", + }, + ) + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + + 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", {}) diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 8d4b7f48543..d03c6b90c11 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -502,6 +502,18 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data): assert mock_async_setup.called assert mock_async_setup_entry.called + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", side_effect=CannotConnect + ): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + await hass.async_block_till_done() + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "already_configured" + @pytest.mark.parametrize( "source, data",