Merge pull request #57944 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2021-10-17 23:32:02 -07:00 committed by GitHub
commit 388339fee2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 122 additions and 63 deletions

View file

@ -122,7 +122,7 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity):
if has_check_control_messages: if has_check_control_messages:
cbs_list = [] cbs_list = []
for message in check_control_messages: for message in check_control_messages:
cbs_list.append(message.description_short) cbs_list.append(message["ccmDescriptionShort"])
result["check_control_messages"] = cbs_list result["check_control_messages"] = cbs_list
else: else:
result["check_control_messages"] = "OK" result["check_control_messages"] = "OK"

View file

@ -3,7 +3,7 @@
"name": "Bond", "name": "Bond",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bond", "documentation": "https://www.home-assistant.io/integrations/bond",
"requirements": ["bond-api==0.1.13"], "requirements": ["bond-api==0.1.14"],
"zeroconf": ["_bond._tcp.local."], "zeroconf": ["_bond._tcp.local."],
"codeowners": ["@prystupa", "@joshs85"], "codeowners": ["@prystupa", "@joshs85"],
"quality_scale": "platinum", "quality_scale": "platinum",

View file

@ -1,12 +1,10 @@
"""The Logitech Harmony Hub integration.""" """The Logitech Harmony Hub integration."""
import asyncio
import logging import logging
from homeassistant.components.remote import ATTR_ACTIVITY, ATTR_DELAY_SECS from homeassistant.components.remote import ATTR_ACTIVITY, ATTR_DELAY_SECS
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry from homeassistant.helpers import entity_registry
from homeassistant.helpers.dispatcher import async_dispatcher_send 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] address = entry.data[CONF_HOST]
name = entry.data[CONF_NAME] name = entry.data[CONF_NAME]
data = HarmonyData(hass, address, name, entry.unique_id) data = HarmonyData(hass, address, name, entry.unique_id)
try: await data.connect()
connected_ok = await data.connect()
except (asyncio.TimeoutError, ValueError, AttributeError) as err:
raise ConfigEntryNotReady from err
if not connected_ok:
raise ConfigEntryNotReady
await _migrate_old_unique_ids(hass, entry.entry_id, data) 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) cancel_stop = hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, _async_on_stop)
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
hass.data[DOMAIN][entry.entry_id] = {
HARMONY_DATA: data, HARMONY_DATA: data,
CANCEL_LISTENER: cancel_listener, CANCEL_LISTENER: cancel_listener,
CANCEL_STOP: cancel_stop, CANCEL_STOP: cancel_stop,

View file

@ -1,7 +1,10 @@
"""Config flow for Logitech Harmony Hub integration.""" """Config flow for Logitech Harmony Hub integration."""
import asyncio
import logging import logging
from urllib.parse import urlparse from urllib.parse import urlparse
from aioharmony.hubconnector_websocket import HubConnector
import aiohttp
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, exceptions from homeassistant import config_entries, exceptions
@ -94,16 +97,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
CONF_NAME: friendly_name, CONF_NAME: friendly_name,
} }
harmony = await get_harmony_client_if_available(parsed_url.hostname) connector = HubConnector(parsed_url.hostname, asyncio.Queue())
try:
if harmony: remote_id = await connector.get_remote_id()
unique_id = find_unique_id_for_remote(harmony) except aiohttp.ClientError:
await self.async_set_unique_id(unique_id) return self.async_abort(reason="cannot_connect")
self._abort_if_unique_id_configured( finally:
updates={CONF_HOST: self.harmony_config[CONF_HOST]} await connector.async_close_session()
)
self.harmony_config[UNIQUE_ID] = unique_id
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() return await self.async_step_link()
async def async_step_link(self, user_input=None): async def async_step_link(self, user_input=None):

View file

@ -1,6 +1,7 @@
"""Harmony data object which contains the Harmony Client.""" """Harmony data object which contains the Harmony Client."""
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import Iterable from collections.abc import Iterable
import logging import logging
@ -8,6 +9,8 @@ from aioharmony.const import ClientCallbackType, SendCommandDevice
import aioharmony.exceptions as aioexc import aioharmony.exceptions as aioexc
from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient
from homeassistant.exceptions import ConfigEntryNotReady
from .const import ACTIVITY_POWER_OFF from .const import ACTIVITY_POWER_OFF
from .subscriber import HarmonySubscriberMixin from .subscriber import HarmonySubscriberMixin
@ -109,16 +112,24 @@ class HarmonyData(HarmonySubscriberMixin):
ip_address=self._address, callbacks=ClientCallbackType(**callbacks) ip_address=self._address, callbacks=ClientCallbackType(**callbacks)
) )
connected = False
try: try:
if not await self._client.connect(): connected = await self._client.connect()
_LOGGER.warning("%s: Unable to connect to HUB", self._name) except (asyncio.TimeoutError, aioexc.TimeOut) as err:
await self._client.close() await self._client.close()
return False raise ConfigEntryNotReady(
except aioexc.TimeOut: f"{self._name}: Connection timed-out to {self._address}:8088"
_LOGGER.warning("%s: Connection timed-out", self._name) ) from err
return False except (ValueError, AttributeError) as err:
await self._client.close()
return True 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): async def shutdown(self):
"""Close connection on shutdown.""" """Close connection on shutdown."""

View file

@ -2,7 +2,7 @@
"domain": "harmony", "domain": "harmony",
"name": "Logitech Harmony Hub", "name": "Logitech Harmony Hub",
"documentation": "https://www.home-assistant.io/integrations/harmony", "documentation": "https://www.home-assistant.io/integrations/harmony",
"requirements": ["aioharmony==0.2.7"], "requirements": ["aioharmony==0.2.8"],
"codeowners": [ "codeowners": [
"@ehendrix23", "@ehendrix23",
"@bramkragten", "@bramkragten",

View file

@ -5,6 +5,7 @@ import logging
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ( from homeassistant.const import (
DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_POWER, DEVICE_CLASS_POWER,
DEVICE_CLASS_PRESSURE, DEVICE_CLASS_PRESSURE,
@ -68,32 +69,32 @@ ENERGY_SENSOR_MAP = {
"electricity_consumed_interval": [ "electricity_consumed_interval": [
"Consumed Power Interval", "Consumed Power Interval",
ENERGY_WATT_HOUR, ENERGY_WATT_HOUR,
DEVICE_CLASS_POWER, DEVICE_CLASS_ENERGY,
], ],
"electricity_consumed_peak_interval": [ "electricity_consumed_peak_interval": [
"Consumed Power Interval", "Consumed Power Interval",
ENERGY_WATT_HOUR, ENERGY_WATT_HOUR,
DEVICE_CLASS_POWER, DEVICE_CLASS_ENERGY,
], ],
"electricity_consumed_off_peak_interval": [ "electricity_consumed_off_peak_interval": [
"Consumed Power Interval (off peak)", "Consumed Power Interval (off peak)",
ENERGY_WATT_HOUR, ENERGY_WATT_HOUR,
DEVICE_CLASS_POWER, DEVICE_CLASS_ENERGY,
], ],
"electricity_produced_interval": [ "electricity_produced_interval": [
"Produced Power Interval", "Produced Power Interval",
ENERGY_WATT_HOUR, ENERGY_WATT_HOUR,
DEVICE_CLASS_POWER, DEVICE_CLASS_ENERGY,
], ],
"electricity_produced_peak_interval": [ "electricity_produced_peak_interval": [
"Produced Power Interval", "Produced Power Interval",
ENERGY_WATT_HOUR, ENERGY_WATT_HOUR,
DEVICE_CLASS_POWER, DEVICE_CLASS_ENERGY,
], ],
"electricity_produced_off_peak_interval": [ "electricity_produced_off_peak_interval": [
"Produced Power Interval (off peak)", "Produced Power Interval (off peak)",
ENERGY_WATT_HOUR, ENERGY_WATT_HOUR,
DEVICE_CLASS_POWER, DEVICE_CLASS_ENERGY,
], ],
"electricity_consumed_off_peak_point": [ "electricity_consumed_off_peak_point": [
"Current Consumed Power (off peak)", "Current Consumed Power (off peak)",
@ -108,12 +109,12 @@ ENERGY_SENSOR_MAP = {
"electricity_consumed_off_peak_cumulative": [ "electricity_consumed_off_peak_cumulative": [
"Cumulative Consumed Power (off peak)", "Cumulative Consumed Power (off peak)",
ENERGY_KILO_WATT_HOUR, ENERGY_KILO_WATT_HOUR,
DEVICE_CLASS_POWER, DEVICE_CLASS_ENERGY,
], ],
"electricity_consumed_peak_cumulative": [ "electricity_consumed_peak_cumulative": [
"Cumulative Consumed Power", "Cumulative Consumed Power",
ENERGY_KILO_WATT_HOUR, ENERGY_KILO_WATT_HOUR,
DEVICE_CLASS_POWER, DEVICE_CLASS_ENERGY,
], ],
"electricity_produced_off_peak_point": [ "electricity_produced_off_peak_point": [
"Current Produced Power (off peak)", "Current Produced Power (off peak)",
@ -128,12 +129,12 @@ ENERGY_SENSOR_MAP = {
"electricity_produced_off_peak_cumulative": [ "electricity_produced_off_peak_cumulative": [
"Cumulative Produced Power (off peak)", "Cumulative Produced Power (off peak)",
ENERGY_KILO_WATT_HOUR, ENERGY_KILO_WATT_HOUR,
DEVICE_CLASS_POWER, DEVICE_CLASS_ENERGY,
], ],
"electricity_produced_peak_cumulative": [ "electricity_produced_peak_cumulative": [
"Cumulative Produced Power", "Cumulative Produced Power",
ENERGY_KILO_WATT_HOUR, ENERGY_KILO_WATT_HOUR,
DEVICE_CLASS_POWER, DEVICE_CLASS_ENERGY,
], ],
"gas_consumed_interval": [ "gas_consumed_interval": [
"Current Consumed Gas Interval", "Current Consumed Gas Interval",
@ -145,7 +146,7 @@ ENERGY_SENSOR_MAP = {
"net_electricity_cumulative": [ "net_electricity_cumulative": [
"Cumulative net Power", "Cumulative net Power",
ENERGY_KILO_WATT_HOUR, ENERGY_KILO_WATT_HOUR,
DEVICE_CLASS_POWER, DEVICE_CLASS_ENERGY,
], ],
} }

View file

@ -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) 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: try:
client = await async_login( client = await async_login(

View file

@ -3,7 +3,7 @@
"name": "Tile", "name": "Tile",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tile", "documentation": "https://www.home-assistant.io/integrations/tile",
"requirements": ["pytile==5.2.3"], "requirements": ["pytile==5.2.4"],
"codeowners": ["@bachya"], "codeowners": ["@bachya"],
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
} }

View file

@ -8,6 +8,7 @@ from yeelight.aio import AsyncBulb
from homeassistant import config_entries, exceptions from homeassistant import config_entries, exceptions
from homeassistant.components.dhcp import IP_ADDRESS 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.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_NAME
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -66,18 +67,30 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id( await self.async_set_unique_id(
"{0:#0{1}x}".format(int(discovery_info["name"][-26:-18]), 18) "{0:#0{1}x}".format(int(discovery_info["name"][-26:-18]), 18)
) )
self._abort_if_unique_id_configured( return await self._async_handle_discovery_with_unique_id()
updates={CONF_HOST: self._discovered_ip}, reload_on_update=False
)
return await self._async_handle_discovery()
async def async_step_ssdp(self, discovery_info): async def async_step_ssdp(self, discovery_info):
"""Handle discovery from ssdp.""" """Handle discovery from ssdp."""
self._discovered_ip = urlparse(discovery_info["location"]).hostname self._discovered_ip = urlparse(discovery_info["location"]).hostname
await self.async_set_unique_id(discovery_info["id"]) await self.async_set_unique_id(discovery_info["id"])
self._abort_if_unique_id_configured( return await self._async_handle_discovery_with_unique_id()
updates={CONF_HOST: self._discovered_ip}, reload_on_update=False
) 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() return await self._async_handle_discovery()
async def _async_handle_discovery(self): async def _async_handle_discovery(self):
@ -86,6 +99,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
for progress in self._async_in_progress(): for progress in self._async_in_progress():
if progress.get("context", {}).get(CONF_HOST) == self._discovered_ip: if progress.get("context", {}).get(CONF_HOST) == self._discovered_ip:
return self.async_abort(reason="already_in_progress") return self.async_abort(reason="already_in_progress")
self._async_abort_entries_match({CONF_HOST: self._discovered_ip})
try: try:
self._discovered_model = await self._async_try_connect( self._discovered_model = await self._async_try_connect(

View file

@ -5,7 +5,7 @@ from typing import Final
MAJOR_VERSION: Final = 2021 MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 10 MINOR_VERSION: Final = 10
PATCH_VERSION: Final = "5" PATCH_VERSION: Final = "6"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)

View file

@ -179,7 +179,7 @@ aiogithubapi==21.8.0
aioguardian==1.0.8 aioguardian==1.0.8
# homeassistant.components.harmony # homeassistant.components.harmony
aioharmony==0.2.7 aioharmony==0.2.8
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==0.6.3 aiohomekit==0.6.3
@ -415,7 +415,7 @@ blockchain==1.4.4
# bme680==1.0.5 # bme680==1.0.5
# homeassistant.components.bond # homeassistant.components.bond
bond-api==0.1.13 bond-api==0.1.14
# homeassistant.components.bosch_shc # homeassistant.components.bosch_shc
boschshcpy==0.2.19 boschshcpy==0.2.19
@ -1964,7 +1964,7 @@ python_opendata_transport==0.2.1
pythonegardia==1.0.40 pythonegardia==1.0.40
# homeassistant.components.tile # homeassistant.components.tile
pytile==5.2.3 pytile==5.2.4
# homeassistant.components.touchline # homeassistant.components.touchline
pytouchline==0.7 pytouchline==0.7

View file

@ -118,7 +118,7 @@ aioflo==0.4.1
aioguardian==1.0.8 aioguardian==1.0.8
# homeassistant.components.harmony # homeassistant.components.harmony
aioharmony==0.2.7 aioharmony==0.2.8
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==0.6.3 aiohomekit==0.6.3
@ -254,7 +254,7 @@ blebox_uniapi==1.3.3
blinkpy==0.17.0 blinkpy==0.17.0
# homeassistant.components.bond # homeassistant.components.bond
bond-api==0.1.13 bond-api==0.1.14
# homeassistant.components.bosch_shc # homeassistant.components.bosch_shc
boschshcpy==0.2.19 boschshcpy==0.2.19
@ -1127,7 +1127,7 @@ python-twitch-client==0.6.0
python_awair==0.2.1 python_awair==0.2.1
# homeassistant.components.tile # homeassistant.components.tile
pytile==5.2.3 pytile==5.2.4
# homeassistant.components.traccar # homeassistant.components.traccar
pytraccar==0.9.0 pytraccar==0.9.0

View file

@ -1,6 +1,8 @@
"""Test the Logitech Harmony Hub config flow.""" """Test the Logitech Harmony Hub config flow."""
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
import aiohttp
from homeassistant import config_entries, data_entry_flow, setup from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.harmony.config_flow import CannotConnect from homeassistant.components.harmony.config_flow import CannotConnect
from homeassistant.components.harmony.const import DOMAIN, PREVIOUS_ACTIVE_ACTIVITY 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.""" """Test we get the form with ssdp source."""
await setup.async_setup_component(hass, "persistent_notification", {}) await setup.async_setup_component(hass, "persistent_notification", {})
harmonyapi = _get_mock_harmonyapi(connect=True)
with patch( with patch(
"homeassistant.components.harmony.util.HarmonyAPI", "homeassistant.components.harmony.config_flow.HubConnector.get_remote_id",
return_value=harmonyapi, return_value=1234,
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
@ -76,6 +76,8 @@ async def test_form_ssdp(hass):
assert progress[0]["flow_id"] == result["flow_id"] assert progress[0]["flow_id"] == result["flow_id"]
assert progress[0]["context"]["confirm_only"] is True assert progress[0]["context"]["confirm_only"] is True
harmonyapi = _get_mock_harmonyapi(connect=True)
with patch( with patch(
"homeassistant.components.harmony.util.HarmonyAPI", "homeassistant.components.harmony.util.HarmonyAPI",
return_value=harmonyapi, return_value=harmonyapi,
@ -95,6 +97,25 @@ async def test_form_ssdp(hass):
assert len(mock_setup_entry.mock_calls) == 1 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): async def test_form_ssdp_aborts_before_checking_remoteid_if_host_known(hass):
"""Test we abort without connecting if the host is already known.""" """Test we abort without connecting if the host is already known."""
await setup.async_setup_component(hass, "persistent_notification", {}) await setup.async_setup_component(hass, "persistent_notification", {})

View file

@ -502,6 +502,18 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data):
assert mock_async_setup.called assert mock_async_setup.called
assert mock_async_setup_entry.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( @pytest.mark.parametrize(
"source, data", "source, data",