From 21baf50fc9f989d21e8fdfa140f95fb5956eef0f Mon Sep 17 00:00:00 2001 From: Dennis Schroer Date: Thu, 3 Nov 2022 12:53:58 +0100 Subject: [PATCH 001/114] Update energyflip-client dependency to 0.2.2 (#81426) --- homeassistant/components/huisbaasje/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/huisbaasje/manifest.json b/homeassistant/components/huisbaasje/manifest.json index 2963a82512b..47d47da182b 100644 --- a/homeassistant/components/huisbaasje/manifest.json +++ b/homeassistant/components/huisbaasje/manifest.json @@ -3,7 +3,7 @@ "name": "Huisbaasje", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huisbaasje", - "requirements": ["energyflip-client==0.2.1"], + "requirements": ["energyflip-client==0.2.2"], "codeowners": ["@dennisschroer"], "iot_class": "cloud_polling", "loggers": ["huisbaasje"] diff --git a/requirements_all.txt b/requirements_all.txt index bb2e4308e48..06def1c34d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -623,7 +623,7 @@ elmax_api==0.0.2 emulated_roku==0.2.1 # homeassistant.components.huisbaasje -energyflip-client==0.2.1 +energyflip-client==0.2.2 # homeassistant.components.enocean enocean==0.50 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a78ce61f213..0c784df961c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -476,7 +476,7 @@ elmax_api==0.0.2 emulated_roku==0.2.1 # homeassistant.components.huisbaasje -energyflip-client==0.2.1 +energyflip-client==0.2.2 # homeassistant.components.enocean enocean==0.50 From 32c5248ddbb8781b9fe2071c5d56eca2fe209879 Mon Sep 17 00:00:00 2001 From: Austin Brunkhorst Date: Wed, 2 Nov 2022 19:10:07 -0700 Subject: [PATCH 002/114] Update pysnooz to 0.8.3 (#81428) --- homeassistant/components/snooz/config_flow.py | 2 +- homeassistant/components/snooz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/snooz/{test_config.py => test_init.py} | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename tests/components/snooz/{test_config.py => test_init.py} (100%) diff --git a/homeassistant/components/snooz/config_flow.py b/homeassistant/components/snooz/config_flow.py index 48f9370e403..eb05edcbefa 100644 --- a/homeassistant/components/snooz/config_flow.py +++ b/homeassistant/components/snooz/config_flow.py @@ -82,7 +82,7 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: name = user_input[CONF_NAME] - discovered = self._discovered_devices.get(name) + discovered = self._discovered_devices[name] assert discovered is not None diff --git a/homeassistant/components/snooz/manifest.json b/homeassistant/components/snooz/manifest.json index 1384767e8b8..91185bcd5b2 100644 --- a/homeassistant/components/snooz/manifest.json +++ b/homeassistant/components/snooz/manifest.json @@ -3,7 +3,7 @@ "name": "Snooz", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/snooz", - "requirements": ["pysnooz==0.8.2"], + "requirements": ["pysnooz==0.8.3"], "dependencies": ["bluetooth"], "codeowners": ["@AustinBrunkhorst"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 06def1c34d6..29e72b4a292 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1911,7 +1911,7 @@ pysml==0.0.8 pysnmplib==5.0.15 # homeassistant.components.snooz -pysnooz==0.8.2 +pysnooz==0.8.3 # homeassistant.components.soma pysoma==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c784df961c..e698890ca02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1346,7 +1346,7 @@ pysmartthings==0.7.6 pysnmplib==5.0.15 # homeassistant.components.snooz -pysnooz==0.8.2 +pysnooz==0.8.3 # homeassistant.components.soma pysoma==0.0.10 diff --git a/tests/components/snooz/test_config.py b/tests/components/snooz/test_init.py similarity index 100% rename from tests/components/snooz/test_config.py rename to tests/components/snooz/test_init.py From e25cf0b338d80716bfc54fca29bd9c61bef29e7a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 3 Nov 2022 04:43:48 -0400 Subject: [PATCH 003/114] Fix eight sleep client creation (#81440) Fix eight sleep bug --- homeassistant/components/eight_sleep/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 67ff6c59a54..2642505fbea 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -95,7 +95,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], hass.config.time_zone, - async_get_clientsession(hass), + client_session=async_get_clientsession(hass), ) # Authenticate, build sensors From 632231912e494cff7a89b53c9c027de9999cc0da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Nov 2022 08:51:08 +0100 Subject: [PATCH 004/114] Skip flume devices with location missing (#81441) fixes #81438 --- homeassistant/components/flume/util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/flume/util.py b/homeassistant/components/flume/util.py index b943124b877..58b3920c9be 100644 --- a/homeassistant/components/flume/util.py +++ b/homeassistant/components/flume/util.py @@ -14,5 +14,6 @@ def get_valid_flume_devices(flume_devices: FlumeDeviceList) -> list[dict[str, An return [ device for device in flume_devices.device_list - if KEY_DEVICE_LOCATION_NAME in device[KEY_DEVICE_LOCATION] + if KEY_DEVICE_LOCATION in device + and KEY_DEVICE_LOCATION_NAME in device[KEY_DEVICE_LOCATION] ] From 758e06b4b6d36aeb8ceeb9e954dd80fd1ebae281 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 3 Nov 2022 11:13:23 +0100 Subject: [PATCH 005/114] Fix SSDP failure to start on missing URLs (#81453) --- homeassistant/components/ssdp/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index d081ef877de..8e037602b90 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -52,7 +52,7 @@ from homeassistant.helpers import discovery_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.instance_id import async_get as async_get_instance_id -from homeassistant.helpers.network import get_url +from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.system_info import async_get_system_info from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_ssdp, bind_hass @@ -697,7 +697,16 @@ class Server: udn = await self._async_get_instance_udn() system_info = await async_get_system_info(self.hass) model_name = system_info["installation_type"] - presentation_url = get_url(self.hass, allow_ip=True, prefer_external=False) + try: + presentation_url = get_url(self.hass, allow_ip=True, prefer_external=False) + except NoURLAvailableError: + _LOGGER.warning( + "Could not set up UPnP/SSDP server, as a presentation URL could" + " not be determined; Please configure your internal URL" + " in the Home Assistant general configuration" + ) + return + serial_number = await async_get_instance_id(self.hass) HassUpnpServiceDevice.DEVICE_DEFINITION = ( HassUpnpServiceDevice.DEVICE_DEFINITION._replace( From d4b7c00ed6e843694e49a3285cc68fd2187f0619 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Nov 2022 11:14:01 +0100 Subject: [PATCH 006/114] Bump aiohomekit to 2.2.14 (#81454) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 18884d59307..b2aec75c3ad 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.13"], + "requirements": ["aiohomekit==2.2.14"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 29e72b4a292..c5ab246a560 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.13 +aiohomekit==2.2.14 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e698890ca02..9a0c2a8d8ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.13 +aiohomekit==2.2.14 # homeassistant.components.emulated_hue # homeassistant.components.http From 8cb4e8452d59c20a7ab8b0ea852a84bc534288a0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 3 Nov 2022 11:18:25 +0100 Subject: [PATCH 007/114] Update cryptography to 38.0.3 (#81455) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 39158d63d55..53966a90141 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 -cryptography==38.0.1 +cryptography==38.0.3 dbus-fast==1.61.1 fnvhash==0.1.0 hass-nabucasa==0.56.0 diff --git a/pyproject.toml b/pyproject.toml index b41ac861aca..f68f6e450be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ dependencies = [ "lru-dict==1.1.8", "PyJWT==2.5.0", # PyJWT has loose dependency. We want the latest one. - "cryptography==38.0.1", + "cryptography==38.0.3", "orjson==3.8.1", "pip>=21.0,<22.4", "python-slugify==4.0.1", diff --git a/requirements.txt b/requirements.txt index 962a9d59dc6..96a9f801df9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 PyJWT==2.5.0 -cryptography==38.0.1 +cryptography==38.0.3 orjson==3.8.1 pip>=21.0,<22.4 python-slugify==4.0.1 From 48edd54e622906b447aa22314c01c8b64206f780 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Nov 2022 14:49:12 +0100 Subject: [PATCH 008/114] Fix HomeKit thermostat to take priority over fans (#81473) --- homeassistant/components/homekit/type_thermostats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index a924548816b..a8c7a53718a 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -306,7 +306,7 @@ class Thermostat(HomeAccessory): if attributes.get(ATTR_HVAC_ACTION) is not None: self.fan_chars.append(CHAR_CURRENT_FAN_STATE) serv_fan = self.add_preload_service(SERV_FANV2, self.fan_chars) - serv_thermostat.add_linked_service(serv_fan) + serv_fan.add_linked_service(serv_thermostat) self.char_active = serv_fan.configure_char( CHAR_ACTIVE, value=1, setter_callback=self._set_fan_active ) From 8cbe3036774b4c16b1488433ae5e7e8de26d3b2f Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 3 Nov 2022 10:39:02 -0400 Subject: [PATCH 009/114] Bump AIOAladdinConnect to 0.1.47 (#81479) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 50ab6af6f85..6888eb4d8b2 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.46"], + "requirements": ["AIOAladdinConnect==0.1.47"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index c5ab246a560..84d01bf2361 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.46 +AIOAladdinConnect==0.1.47 # homeassistant.components.adax Adax-local==0.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a0c2a8d8ea..3f27a3b75f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.46 +AIOAladdinConnect==0.1.47 # homeassistant.components.adax Adax-local==0.1.5 From dd004d62d49bc37aa3b69dc969e4f66051e82507 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Nov 2022 17:00:47 +0100 Subject: [PATCH 010/114] Bumped version to 2022.11.1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5ba07ebf8fd..ef94db7e2b6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __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/pyproject.toml b/pyproject.toml index f68f6e450be..e616441fadd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0" +version = "2022.11.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From a53d1e072db1ea30fcf2bbe5b973af89e9beb218 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Nov 2022 10:56:08 +0100 Subject: [PATCH 011/114] Fix scrape scan interval (#81763) --- homeassistant/components/scrape/sensor.py | 6 ++++-- tests/components/scrape/test_sensor.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index b13b5d8463b..b5ba471c301 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_RESOURCE, + CONF_SCAN_INTERVAL, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, @@ -43,7 +44,7 @@ from .coordinator import ScrapeCoordinator _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=10) +DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) CONF_ATTR = "attribute" CONF_SELECT = "select" @@ -111,7 +112,8 @@ async def async_setup_platform( rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) - coordinator = ScrapeCoordinator(hass, rest, SCAN_INTERVAL) + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + coordinator = ScrapeCoordinator(hass, rest, scan_interval) await coordinator.async_refresh() if coordinator.data is None: raise PlatformNotReady diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index aacd89b2eb9..9affc1f9db4 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import datetime from unittest.mock import patch -from homeassistant.components.scrape.sensor import SCAN_INTERVAL +from homeassistant.components.scrape.sensor import DEFAULT_SCAN_INTERVAL from homeassistant.components.sensor import ( CONF_STATE_CLASS, SensorDeviceClass, @@ -189,7 +189,7 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: assert state.state == "Current Version: 2021.12.10" mocker.payload = "test_scrape_sensor_no_data" - async_fire_time_changed(hass, datetime.utcnow() + SCAN_INTERVAL) + async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") From d3bd80b87640962b0ae291987464fa3d174eab5f Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Tue, 8 Nov 2022 11:02:53 +0100 Subject: [PATCH 012/114] Fix ignored upnp discoveries not being matched when device changes its unique identifier (#81240) Fixes https://github.com/home-assistant/core/issues/78454 fixes undefined --- homeassistant/components/upnp/config_flow.py | 60 ++++++++++++++------ homeassistant/components/upnp/const.py | 1 + tests/components/upnp/test_config_flow.py | 38 +++++++++++++ 3 files changed, 83 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 6b488398461..b7d6425707d 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -20,6 +20,7 @@ from .const import ( CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, DOMAIN, + DOMAIN_DISCOVERIES, LOGGER, ST_IGD_V1, ST_IGD_V2, @@ -47,7 +48,7 @@ def _is_complete_discovery(discovery_info: ssdp.SsdpServiceInfo) -> bool: ) -async def _async_discover_igd_devices( +async def _async_discovered_igd_devices( hass: HomeAssistant, ) -> list[ssdp.SsdpServiceInfo]: """Discovery IGD devices.""" @@ -79,9 +80,19 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # - ssdp(discovery_info) --> ssdp_confirm(None) --> ssdp_confirm({}) --> create_entry() # - user(None): scan --> user({...}) --> create_entry() - def __init__(self) -> None: - """Initialize the UPnP/IGD config flow.""" - self._discoveries: list[SsdpServiceInfo] | None = None + @property + def _discoveries(self) -> dict[str, SsdpServiceInfo]: + """Get current discoveries.""" + domain_data: dict = self.hass.data.setdefault(DOMAIN, {}) + return domain_data.setdefault(DOMAIN_DISCOVERIES, {}) + + def _add_discovery(self, discovery: SsdpServiceInfo) -> None: + """Add a discovery.""" + self._discoveries[discovery.ssdp_usn] = discovery + + def _remove_discovery(self, usn: str) -> SsdpServiceInfo: + """Remove a discovery by its USN/unique_id.""" + return self._discoveries.pop(usn) async def async_step_user( self, user_input: Mapping[str, Any] | None = None @@ -95,7 +106,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): discovery = next( iter( discovery - for discovery in self._discoveries + for discovery in self._discoveries.values() if discovery.ssdp_usn == user_input["unique_id"] ) ) @@ -103,21 +114,19 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self._async_create_entry_from_discovery(discovery) # Discover devices. - discoveries = await _async_discover_igd_devices(self.hass) + discoveries = await _async_discovered_igd_devices(self.hass) # Store discoveries which have not been configured. current_unique_ids = { entry.unique_id for entry in self._async_current_entries() } - self._discoveries = [ - discovery - for discovery in discoveries + for discovery in discoveries: if ( _is_complete_discovery(discovery) and _is_igd_device(discovery) and discovery.ssdp_usn not in current_unique_ids - ) - ] + ): + self._add_discovery(discovery) # Ensure anything to add. if not self._discoveries: @@ -128,7 +137,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required("unique_id"): vol.In( { discovery.ssdp_usn: _friendly_name_from_discovery(discovery) - for discovery in self._discoveries + for discovery in self._discoveries.values() } ), } @@ -163,12 +172,13 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): mac_address = await _async_mac_address_from_discovery(self.hass, discovery_info) host = discovery_info.ssdp_headers["_host"] self._abort_if_unique_id_configured( - # Store mac address for older entries. + # Store mac address and other data for older entries. # The location is stored in the config entry such that when the location changes, the entry is reloaded. updates={ CONFIG_ENTRY_MAC_ADDRESS: mac_address, CONFIG_ENTRY_LOCATION: discovery_info.ssdp_location, CONFIG_ENTRY_HOST: host, + CONFIG_ENTRY_ST: discovery_info.ssdp_st, }, ) @@ -204,7 +214,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="config_entry_updated") # Store discovery. - self._discoveries = [discovery_info] + self._add_discovery(discovery_info) # Ensure user recognizable. self.context["title_placeholders"] = { @@ -221,10 +231,27 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is None: return self.async_show_form(step_id="ssdp_confirm") - assert self._discoveries - discovery = self._discoveries[0] + assert self.unique_id + discovery = self._remove_discovery(self.unique_id) return await self._async_create_entry_from_discovery(discovery) + async def async_step_ignore(self, user_input: dict[str, Any]) -> FlowResult: + """Ignore this config flow.""" + usn = user_input["unique_id"] + discovery = self._remove_discovery(usn) + mac_address = await _async_mac_address_from_discovery(self.hass, discovery) + data = { + CONFIG_ENTRY_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN], + CONFIG_ENTRY_ST: discovery.ssdp_st, + CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN], + CONFIG_ENTRY_MAC_ADDRESS: mac_address, + CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"], + CONFIG_ENTRY_LOCATION: discovery.ssdp_location, + } + + await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False) + return self.async_create_entry(title=user_input["title"], data=data) + async def _async_create_entry_from_discovery( self, discovery: SsdpServiceInfo, @@ -243,5 +270,6 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN], CONFIG_ENTRY_LOCATION: discovery.ssdp_location, CONFIG_ENTRY_MAC_ADDRESS: mac_address, + CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"], } return self.async_create_entry(title=title, data=data) diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index 8d98790983a..5f73b1e63c9 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -7,6 +7,7 @@ from homeassistant.const import TIME_SECONDS LOGGER = logging.getLogger(__package__) DOMAIN = "upnp" +DOMAIN_DISCOVERIES = "discoveries" BYTES_RECEIVED = "bytes_received" BYTES_SENT = "bytes_sent" PACKETS_RECEIVED = "packets_received" diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index f0a1de1ce37..7850554f751 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -63,6 +63,42 @@ async def test_flow_ssdp(hass: HomeAssistant): CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, CONFIG_ENTRY_LOCATION: TEST_LOCATION, CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, + CONFIG_ENTRY_HOST: TEST_HOST, + } + + +@pytest.mark.usefixtures( + "ssdp_instant_discovery", + "mock_setup_entry", + "mock_get_source_ip", + "mock_mac_address_from_host", +) +async def test_flow_ssdp_ignore(hass: HomeAssistant): + """Test config flow: discovered + ignore through ssdp.""" + # Discovered via step ssdp. + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=TEST_DISCOVERY, + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "ssdp_confirm" + + # Ignore entry. + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": TEST_USN, "title": TEST_FRIENDLY_NAME}, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_FRIENDLY_NAME + assert result["data"] == { + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, + CONFIG_ENTRY_LOCATION: TEST_LOCATION, + CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, + CONFIG_ENTRY_HOST: TEST_HOST, } @@ -138,6 +174,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant): CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, CONFIG_ENTRY_LOCATION: TEST_LOCATION, CONFIG_ENTRY_MAC_ADDRESS: None, + CONFIG_ENTRY_HOST: TEST_HOST, } @@ -382,6 +419,7 @@ async def test_flow_user(hass: HomeAssistant): CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, CONFIG_ENTRY_LOCATION: TEST_LOCATION, CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, + CONFIG_ENTRY_HOST: TEST_HOST, } From 42444872b97be4f4b749f0b6e7ae1ca9f4385da6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 15:28:47 -0500 Subject: [PATCH 013/114] Align esphome ble client notify behavior to match BlueZ (#81463) --- .../components/esphome/bluetooth/client.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 72531a2503a..c6b60831577 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -137,6 +137,7 @@ class ESPHomeClient(BaseBleakClient): was_connected = self._is_connected self.services = BleakGATTServiceCollection() # type: ignore[no-untyped-call] self._is_connected = False + self._notify_cancels.clear() if self._disconnected_event: self._disconnected_event.set() self._disconnected_event = None @@ -463,12 +464,20 @@ class ESPHomeClient(BaseBleakClient): UUID or directly by the BleakGATTCharacteristic object representing it. callback (function): The function to be called on notification. """ + ble_handle = characteristic.handle + if ble_handle in self._notify_cancels: + raise BleakError( + "Notifications are already enabled on " + f"service:{characteristic.service_uuid} " + f"characteristic:{characteristic.uuid} " + f"handle:{ble_handle}" + ) cancel_coro = await self._client.bluetooth_gatt_start_notify( self._address_as_int, - characteristic.handle, + ble_handle, lambda handle, data: callback(data), ) - self._notify_cancels[characteristic.handle] = cancel_coro + self._notify_cancels[ble_handle] = cancel_coro @api_error_as_bleak_error async def stop_notify( @@ -483,5 +492,7 @@ class ESPHomeClient(BaseBleakClient): directly by the BleakGATTCharacteristic object representing it. """ characteristic = self._resolve_characteristic(char_specifier) - coro = self._notify_cancels.pop(characteristic.handle) - await coro() + # Do not raise KeyError if notifications are not enabled on this characteristic + # to be consistent with the behavior of the BlueZ backend + if coro := self._notify_cancels.pop(characteristic.handle, None): + await coro() From c93c13d8bf5ba2ea567e7cf4765aa0fef77d21c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 09:55:43 -0500 Subject: [PATCH 014/114] Bump nexia to 2.0.6 (#81474) * Bump nexia to 2.0.6 - Marks thermostat unavailable when it is offline * is property --- homeassistant/components/nexia/entity.py | 5 +++++ homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/entity.py b/homeassistant/components/nexia/entity.py index 4f806d03eda..6b017db4d34 100644 --- a/homeassistant/components/nexia/entity.py +++ b/homeassistant/components/nexia/entity.py @@ -80,6 +80,11 @@ class NexiaThermostatEntity(NexiaEntity): self.hass, f"{SIGNAL_THERMOSTAT_UPDATE}-{self._thermostat.thermostat_id}" ) + @property + def available(self) -> bool: + """Return True if thermostat is available and data is available.""" + return super().available and self._thermostat.is_online + class NexiaThermostatZoneEntity(NexiaThermostatEntity): """Base class for nexia devices attached to a thermostat.""" diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 78576e06b8a..99eb7c14798 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==2.0.5"], + "requirements": ["nexia==2.0.6"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 84d01bf2361..48d7947df20 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1135,7 +1135,7 @@ nettigo-air-monitor==1.4.2 neurio==0.3.1 # homeassistant.components.nexia -nexia==2.0.5 +nexia==2.0.6 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f27a3b75f9..9007cf73452 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -825,7 +825,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.4.2 # homeassistant.components.nexia -nexia==2.0.5 +nexia==2.0.6 # homeassistant.components.discord nextcord==2.0.0a8 From 7832a7fd80de367b18c98b207c7f6dfea35c7d63 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Nov 2022 20:35:05 +0100 Subject: [PATCH 015/114] Bump oralb-ble to 0.10.1 (#81491) fixes #81489 changelog: https://github.com/Bluetooth-Devices/oralb-ble/compare/v0.10.0...v0.10.1 --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index cad6167228c..520306aed03 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.10.0"], + "requirements": ["oralb-ble==0.10.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 48d7947df20..98101012775 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1238,7 +1238,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.10.0 +oralb-ble==0.10.1 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9007cf73452..da1dbb63730 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -883,7 +883,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.10.0 +oralb-ble==0.10.1 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 51ab5d18083e3a5a3e2493b0869778f65b041cf9 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Sun, 6 Nov 2022 08:46:00 +1100 Subject: [PATCH 016/114] Fix lifx.set_state so it works with kelvin and color_temp_kelvin and color names (#81515) --- homeassistant/components/lifx/util.py | 26 +++++- tests/components/lifx/test_light.py | 128 ++++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lifx/util.py b/homeassistant/components/lifx/util.py index 135e1a7e8e9..6a9bff465ee 100644 --- a/homeassistant/components/lifx/util.py +++ b/homeassistant/components/lifx/util.py @@ -14,8 +14,11 @@ from awesomeversion import AwesomeVersion from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_NAME, ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, + ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_XY_COLOR, ) @@ -24,7 +27,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr import homeassistant.util.color as color_util -from .const import DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT +from .const import _LOGGER, DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT FIX_MAC_FW = AwesomeVersion("3.70") @@ -80,6 +83,17 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] | """ hue, saturation, brightness, kelvin = [None] * 4 + if (color_name := kwargs.get(ATTR_COLOR_NAME)) is not None: + try: + hue, saturation = color_util.color_RGB_to_hs( + *color_util.color_name_to_rgb(color_name) + ) + except ValueError: + _LOGGER.warning( + "Got unknown color %s, falling back to neutral white", color_name + ) + hue, saturation = (0, 0) + if ATTR_HS_COLOR in kwargs: hue, saturation = kwargs[ATTR_HS_COLOR] elif ATTR_RGB_COLOR in kwargs: @@ -93,6 +107,13 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] | saturation = int(saturation / 100 * 65535) kelvin = 3500 + if ATTR_KELVIN in kwargs: + _LOGGER.warning( + "The 'kelvin' parameter is deprecated. Please use 'color_temp_kelvin' for all service calls" + ) + kelvin = kwargs.pop(ATTR_KELVIN) + saturation = 0 + if ATTR_COLOR_TEMP_KELVIN in kwargs: kelvin = kwargs.pop(ATTR_COLOR_TEMP_KELVIN) saturation = 0 @@ -100,6 +121,9 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] | if ATTR_BRIGHTNESS in kwargs: brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) + if ATTR_BRIGHTNESS_PCT in kwargs: + brightness = convert_8_to_16(round(255 * kwargs[ATTR_BRIGHTNESS_PCT] / 100)) + hsbk = [hue, saturation, brightness, kelvin] return None if hsbk == [None] * 4 else hsbk diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index 6fe63b14b6a..5a9b250034a 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -21,11 +21,14 @@ from homeassistant.components.lifx.manager import ( ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, ATTR_COLOR_MODE, + ATTR_COLOR_NAME, ATTR_COLOR_TEMP, ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, @@ -1397,6 +1400,131 @@ async def test_transitions_color_bulb(hass: HomeAssistant) -> None: bulb.set_color.reset_mock() +async def test_lifx_set_state_color(hass: HomeAssistant) -> None: + """Test lifx.set_state works with color names and RGB.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 2700] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + # brightness should convert from 8 to 16 bits + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, None, 65535, 2700] + bulb.set_color.reset_mock() + + # brightness_pct should convert into 16 bit + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 90}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, None, 59110, 2700] + bulb.set_color.reset_mock() + + # color name should turn into hue, saturation + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_NAME: "red", ATTR_BRIGHTNESS_PCT: 100}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [0, 65535, 65535, 3500] + bulb.set_color.reset_mock() + + # unknown color name should reset back to neutral white, i.e. 3500K + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_NAME: "deepblack"}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [0, 0, 32000, 3500] + bulb.set_color.reset_mock() + + # RGB should convert to hue, saturation + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (0, 255, 0)}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [21845, 65535, 32000, 3500] + bulb.set_color.reset_mock() + + # XY should convert to hue, saturation + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_XY_COLOR: (0.34, 0.339)}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [5461, 5139, 32000, 3500] + bulb.set_color.reset_mock() + + +async def test_lifx_set_state_kelvin(hass: HomeAssistant) -> None: + """Test set_state works with old and new kelvin parameter names.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255, ATTR_KELVIN: 3500}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, 0, 65535, 3500] + bulb.set_color.reset_mock() + + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100, ATTR_COLOR_TEMP_KELVIN: 2700}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, 0, 25700, 2700] + bulb.set_color.reset_mock() + + async def test_infrared_color_bulb(hass: HomeAssistant) -> None: """Test setting infrared with a color bulb.""" already_migrated_config_entry = MockConfigEntry( From 7b769b39c2ffe4255e034c1bbc66eae8761bc937 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 09:57:32 -0500 Subject: [PATCH 017/114] Add additional coverage for adding multiple elkm1 instances (#81528) * Add additional coverage for adding multiple elkm1 instances * fix copy error --- tests/components/elkm1/test_config_flow.py | 137 +++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index e47dc402b64..7ce0e2163ac 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -2,6 +2,7 @@ from dataclasses import asdict from unittest.mock import patch +from elkm1_lib.discovery import ElkSystem import pytest from homeassistant import config_entries @@ -1317,3 +1318,139 @@ async def test_discovered_by_dhcp_no_udp_response(hass): assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" + + +async def test_multiple_instances_with_discovery(hass): + """Test we can setup a secure elk.""" + + elk_discovery_1 = ElkSystem("aa:bb:cc:dd:ee:ff", "127.0.0.1", 2601) + elk_discovery_2 = ElkSystem("aa:bb:cc:dd:ee:fe", "127.0.0.2", 2601) + + with _patch_discovery(device=elk_discovery_1): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert not result["errors"] + assert result["step_id"] == "user" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_elk(elk=mocked_elk): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device": elk_discovery_1.mac_address}, + ) + await hass.async_block_till_done() + + with _patch_discovery(device=elk_discovery_1), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "ElkM1 ddeeff" + assert result3["data"] == { + "auto_configure": True, + "host": "elks://127.0.0.1", + "password": "test-password", + "prefix": "", + "username": "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + # Now try to add another instance with the different discovery info + with _patch_discovery(device=elk_discovery_2): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert not result["errors"] + assert result["step_id"] == "user" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_elk(elk=mocked_elk): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device": elk_discovery_2.mac_address}, + ) + await hass.async_block_till_done() + + with _patch_discovery(device=elk_discovery_2), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "ElkM1 ddeefe" + assert result3["data"] == { + "auto_configure": True, + "host": "elks://127.0.0.2", + "password": "test-password", + "prefix": "ddeefe", + "username": "test-username", + } + assert len(mock_setup_entry.mock_calls) == 1 + + # Finally, try to add another instance manually with no discovery info + + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "manual_connection" + + mocked_elk = mock_elk(invalid_auth=None, sync_complete=True) + + with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "non-secure", + "address": "1.2.3.4", + "prefix": "guest_house", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "guest_house" + assert result2["data"] == { + "auto_configure": True, + "host": "elk://1.2.3.4", + "prefix": "guest_house", + "username": "", + "password": "", + } + assert len(mock_setup_entry.mock_calls) == 1 From 087ede959dc50509297462edc0f9f3b88f364267 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 4 Nov 2022 14:42:37 +0100 Subject: [PATCH 018/114] Bump oralb-ble to 0.10.2 (#81537) Fixes some more missing pressure mappings changelog: https://github.com/Bluetooth-Devices/oralb-ble/compare/v0.10.1...v0.10.2 --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index 520306aed03..ba89c73a240 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.10.1"], + "requirements": ["oralb-ble==0.10.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 98101012775..88208e1e0f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1238,7 +1238,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.10.1 +oralb-ble==0.10.2 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da1dbb63730..7fdd465045f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -883,7 +883,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.10.1 +oralb-ble==0.10.2 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 7124cedd7a02e7d11977d2970a84df2f7707d1a1 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 5 Nov 2022 11:58:47 -0600 Subject: [PATCH 019/114] Bump pyairvisual to 2022.11.1 (#81556) --- homeassistant/components/airvisual/__init__.py | 10 +++------- homeassistant/components/airvisual/config_flow.py | 6 +++--- homeassistant/components/airvisual/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airvisual/test_config_flow.py | 6 +++--- 6 files changed, 12 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index a2a3d76c3db..2a544edb20a 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -7,13 +7,9 @@ from math import ceil from typing import Any from pyairvisual import CloudAPI, NodeSamba -from pyairvisual.errors import ( - AirVisualError, - InvalidKeyError, - KeyExpiredError, - NodeProError, - UnauthorizedError, -) +from pyairvisual.cloud_api import InvalidKeyError, KeyExpiredError, UnauthorizedError +from pyairvisual.errors import AirVisualError +from pyairvisual.node import NodeProError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 385c9f55753..9510c938cb0 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -6,14 +6,14 @@ from collections.abc import Mapping from typing import Any from pyairvisual import CloudAPI, NodeSamba -from pyairvisual.errors import ( - AirVisualError, +from pyairvisual.cloud_api import ( InvalidKeyError, KeyExpiredError, - NodeProError, NotFoundError, UnauthorizedError, ) +from pyairvisual.errors import AirVisualError +from pyairvisual.node import NodeProError import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index 73bbf0cd589..ae9eeb270a8 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -3,7 +3,7 @@ "name": "AirVisual", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airvisual", - "requirements": ["pyairvisual==2022.07.0"], + "requirements": ["pyairvisual==2022.11.1"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyairvisual", "pysmb"], diff --git a/requirements_all.txt b/requirements_all.txt index 88208e1e0f4..397093afbbb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1433,7 +1433,7 @@ pyaftership==21.11.0 pyairnow==1.1.0 # homeassistant.components.airvisual -pyairvisual==2022.07.0 +pyairvisual==2022.11.1 # homeassistant.components.almond pyalmond==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7fdd465045f..50147ff604f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1021,7 +1021,7 @@ pyaehw4a1==0.3.9 pyairnow==1.1.0 # homeassistant.components.airvisual -pyairvisual==2022.07.0 +pyairvisual==2022.11.1 # homeassistant.components.almond pyalmond==0.0.2 diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index f97ee845dba..7603917eddc 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -1,14 +1,14 @@ """Define tests for the AirVisual config flow.""" from unittest.mock import patch -from pyairvisual.errors import ( - AirVisualError, +from pyairvisual.cloud_api import ( InvalidKeyError, KeyExpiredError, - NodeProError, NotFoundError, UnauthorizedError, ) +from pyairvisual.errors import AirVisualError +from pyairvisual.node import NodeProError import pytest from homeassistant import data_entry_flow From 2a34d3a56ffb14d495387c2cef86bbddabc7e381 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 5 Nov 2022 17:06:34 -0700 Subject: [PATCH 020/114] Bump gcal_sync to 4.0.0 (#81562) * Bump gcal_sync to 2.2.4 * Bump gcal sync to 4.0.0 * Add Calendar accessRole fields which are now required --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/google/test_config_flow.py | 2 +- tests/components/google/test_init.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index f6ebc665cd7..9fc265fa287 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", - "requirements": ["gcal-sync==2.2.3", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==4.0.0", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 397093afbbb..5e133c0058f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -725,7 +725,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==2.2.3 +gcal-sync==4.0.0 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 50147ff604f..8c1ae96837f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -541,7 +541,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==2.2.3 +gcal-sync==4.0.0 # homeassistant.components.geocaching geocachingapi==0.2.1 diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index d8ddd6fe588..bce3f4855c7 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -104,7 +104,7 @@ async def primary_calendar( """Fixture to return the primary calendar.""" mock_calendar_get( "primary", - {"id": primary_calendar_email, "summary": "Personal"}, + {"id": primary_calendar_email, "summary": "Personal", "accessRole": "owner"}, exc=primary_calendar_error, ) diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 5e7696eec68..a2f16f778fd 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -768,7 +768,7 @@ async def test_assign_unique_id( mock_calendar_get( "primary", - {"id": EMAIL_ADDRESS, "summary": "Personal"}, + {"id": EMAIL_ADDRESS, "summary": "Personal", "accessRole": "reader"}, ) mock_calendars_list({"items": [test_api_calendar]}) From bf5ecc30ed706735fd5ef2480529759a294f2c0d Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 4 Nov 2022 18:29:00 +0200 Subject: [PATCH 021/114] Fix Shelly Plus HT missing battery entity (#81564) --- homeassistant/components/shelly/sensor.py | 8 +------- homeassistant/components/shelly/utils.py | 7 ------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 3ddabf7ca2b..cf1eb7508c7 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -47,12 +47,7 @@ from .entity import ( async_setup_entry_rest, async_setup_entry_rpc, ) -from .utils import ( - get_device_entry_gen, - get_device_uptime, - is_rpc_device_externally_powered, - temperature_unit, -) +from .utils import get_device_entry_gen, get_device_uptime, temperature_unit @dataclass @@ -407,7 +402,6 @@ RPC_SENSORS: Final = { value=lambda status, _: status["percent"], device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, - removal_condition=is_rpc_device_externally_powered, entity_registry_enabled_default=True, entity_category=EntityCategory.DIAGNOSTIC, ), diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index c3b6d24752f..a13d84d32be 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -364,13 +364,6 @@ def is_rpc_channel_type_light(config: dict[str, Any], channel: int) -> bool: return con_types is not None and con_types[channel].lower().startswith("light") -def is_rpc_device_externally_powered( - config: dict[str, Any], status: dict[str, Any], key: str -) -> bool: - """Return true if device has external power instead of battery.""" - return cast(bool, status[key]["external"]["present"]) - - def get_rpc_input_triggers(device: RpcDevice) -> list[tuple[str, str]]: """Return list of input triggers for RPC device.""" triggers = [] From d24e272d5e5099b15f7c3f99d7ccf5382a352b98 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sun, 6 Nov 2022 14:51:19 +0100 Subject: [PATCH 022/114] Fix watermeter issue for old P1 Monitor versions (#81570) * Bump the python package version * Add exception to check if user has a water meter --- homeassistant/components/p1_monitor/__init__.py | 5 +++-- homeassistant/components/p1_monitor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/p1_monitor/__init__.py b/homeassistant/components/p1_monitor/__init__.py index b157f3e8116..e6178ffeb41 100644 --- a/homeassistant/components/p1_monitor/__init__.py +++ b/homeassistant/components/p1_monitor/__init__.py @@ -5,6 +5,7 @@ from typing import TypedDict from p1monitor import ( P1Monitor, + P1MonitorConnectionError, P1MonitorNoDataError, Phases, Settings, @@ -101,8 +102,8 @@ class P1MonitorDataUpdateCoordinator(DataUpdateCoordinator[P1MonitorData]): try: data[SERVICE_WATERMETER] = await self.p1monitor.watermeter() self.has_water_meter = True - except P1MonitorNoDataError: - LOGGER.debug("No watermeter data received from P1 Monitor") + except (P1MonitorNoDataError, P1MonitorConnectionError): + LOGGER.debug("No water meter data received from P1 Monitor") if self.has_water_meter is None: self.has_water_meter = False diff --git a/homeassistant/components/p1_monitor/manifest.json b/homeassistant/components/p1_monitor/manifest.json index 626cff15dfa..2e699276caa 100644 --- a/homeassistant/components/p1_monitor/manifest.json +++ b/homeassistant/components/p1_monitor/manifest.json @@ -3,7 +3,7 @@ "name": "P1 Monitor", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/p1_monitor", - "requirements": ["p1monitor==2.1.0"], + "requirements": ["p1monitor==2.1.1"], "codeowners": ["@klaasnicolaas"], "quality_scale": "platinum", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 5e133c0058f..35a09916fc9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1250,7 +1250,7 @@ orvibo==1.1.1 ovoenergy==1.2.0 # homeassistant.components.p1_monitor -p1monitor==2.1.0 +p1monitor==2.1.1 # homeassistant.components.mqtt # homeassistant.components.shiftr diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c1ae96837f..7d7325c4467 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -889,7 +889,7 @@ oralb-ble==0.10.2 ovoenergy==1.2.0 # homeassistant.components.p1_monitor -p1monitor==2.1.0 +p1monitor==2.1.1 # homeassistant.components.mqtt # homeassistant.components.shiftr From a8e1afb9660a72a46951dd54799a17ca0fbe59ab Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sat, 5 Nov 2022 08:57:57 -0600 Subject: [PATCH 023/114] Bump pylitterbot to 2022.11.0 (#81572) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 0965670e569..6384df2f25a 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,7 +3,7 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2022.10.2"], + "requirements": ["pylitterbot==2022.11.0"], "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index 35a09916fc9..fa541d0d0fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1688,7 +1688,7 @@ pylibrespot-java==0.1.1 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.10.2 +pylitterbot==2022.11.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.17.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d7325c4467..42bae213644 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1189,7 +1189,7 @@ pylibrespot-java==0.1.1 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.10.2 +pylitterbot==2022.11.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.17.1 From 6110700e1812857eb3decc9b70dafb905f87654b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Nov 2022 04:03:37 -0600 Subject: [PATCH 024/114] Fix HomeKit reset accessory procedure (#81573) fixes https://github.com/home-assistant/core/issues/81571 --- homeassistant/components/homekit/__init__.py | 28 ++++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index b809f6db205..333d6052d17 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -10,8 +10,10 @@ import os from typing import Any, cast from aiohttp import web +from pyhap.characteristic import Characteristic from pyhap.const import STANDALONE_AID from pyhap.loader import get_loader +from pyhap.service import Service import voluptuous as vol from zeroconf.asyncio import AsyncZeroconf @@ -139,7 +141,7 @@ STATUS_WAIT = 3 PORT_CLEANUP_CHECK_INTERVAL_SECS = 1 _HOMEKIT_CONFIG_UPDATE_TIME = ( - 5 # number of seconds to wait for homekit to see the c# change + 10 # number of seconds to wait for homekit to see the c# change ) @@ -529,6 +531,7 @@ class HomeKit: self.status = STATUS_READY self.driver: HomeDriver | None = None self.bridge: HomeBridge | None = None + self._reset_lock = asyncio.Lock() def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> None: """Set up bridge and accessory driver.""" @@ -558,21 +561,24 @@ class HomeKit: async def async_reset_accessories(self, entity_ids: Iterable[str]) -> None: """Reset the accessory to load the latest configuration.""" - if not self.bridge: - await self.async_reset_accessories_in_accessory_mode(entity_ids) - return - await self.async_reset_accessories_in_bridge_mode(entity_ids) + async with self._reset_lock: + if not self.bridge: + await self.async_reset_accessories_in_accessory_mode(entity_ids) + return + await self.async_reset_accessories_in_bridge_mode(entity_ids) async def _async_shutdown_accessory(self, accessory: HomeAccessory) -> None: """Shutdown an accessory.""" assert self.driver is not None await accessory.stop() # Deallocate the IIDs for the accessory - iid_manager = self.driver.iid_manager - for service in accessory.services: - iid_manager.remove_iid(iid_manager.remove_obj(service)) - for char in service.characteristics: - iid_manager.remove_iid(iid_manager.remove_obj(char)) + iid_manager = accessory.iid_manager + services: list[Service] = accessory.services + for service in services: + iid_manager.remove_obj(service) + characteristics: list[Characteristic] = service.characteristics + for char in characteristics: + iid_manager.remove_obj(char) async def async_reset_accessories_in_accessory_mode( self, entity_ids: Iterable[str] @@ -581,7 +587,6 @@ class HomeKit: assert self.driver is not None acc = cast(HomeAccessory, self.driver.accessory) - await self._async_shutdown_accessory(acc) if acc.entity_id not in entity_ids: return if not (state := self.hass.states.get(acc.entity_id)): @@ -589,6 +594,7 @@ class HomeKit: "The underlying entity %s disappeared during reset", acc.entity_id ) return + await self._async_shutdown_accessory(acc) if new_acc := self._async_create_single_accessory([state]): self.driver.accessory = new_acc self.hass.async_add_job(new_acc.run) From 55c87c733a65c6014ead909864cc738b9de500bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Nov 2022 04:04:24 -0600 Subject: [PATCH 025/114] Ensure HomeKit temperature controls appear before fan controls on thermostat accessories (#81586) --- homeassistant/components/homekit/type_thermostats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index a8c7a53718a..a924548816b 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -306,7 +306,7 @@ class Thermostat(HomeAccessory): if attributes.get(ATTR_HVAC_ACTION) is not None: self.fan_chars.append(CHAR_CURRENT_FAN_STATE) serv_fan = self.add_preload_service(SERV_FANV2, self.fan_chars) - serv_fan.add_linked_service(serv_thermostat) + serv_thermostat.add_linked_service(serv_fan) self.char_active = serv_fan.configure_char( CHAR_ACTIVE, value=1, setter_callback=self._set_fan_active ) From f9c77320903880f814cad9690fe0222509dc7b49 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 7 Nov 2022 06:28:59 -0500 Subject: [PATCH 026/114] Bump ZHA quirks and associated changes (#81587) --- .../zha/core/channels/manufacturerspecific.py | 74 ++++++++----------- homeassistant/components/zha/device_action.py | 31 +++++--- homeassistant/components/zha/manifest.json | 2 +- homeassistant/components/zha/switch.py | 12 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zha/test_device_action.py | 2 +- 7 files changed, 67 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 814e7700d01..c4baccf4ae6 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import TYPE_CHECKING, Any -from zigpy import types +from zhaquirks.inovelli.types import AllLEDEffectType, SingleLEDEffectType from zigpy.exceptions import ZigbeeException import zigpy.zcl @@ -183,59 +183,47 @@ class InovelliNotificationChannel(ClientChannel): class InovelliConfigEntityChannel(ZigbeeChannel): """Inovelli Configuration Entity channel.""" - class LEDEffectType(types.enum8): - """Effect type for Inovelli Blue Series switch.""" - - Off = 0x00 - Solid = 0x01 - Fast_Blink = 0x02 - Slow_Blink = 0x03 - Pulse = 0x04 - Chase = 0x05 - Open_Close = 0x06 - Small_To_Big = 0x07 - Clear = 0xFF - REPORT_CONFIG = () ZCL_INIT_ATTRS = { - "dimming_speed_up_remote": False, - "dimming_speed_up_local": False, - "ramp_rate_off_to_on_local": False, - "ramp_rate_off_to_on_remote": False, - "dimming_speed_down_remote": False, - "dimming_speed_down_local": False, - "ramp_rate_on_to_off_local": False, - "ramp_rate_on_to_off_remote": False, - "minimum_level": False, - "maximum_level": False, - "invert_switch": False, - "auto_off_timer": False, - "default_level_local": False, - "default_level_remote": False, - "state_after_power_restored": False, - "load_level_indicator_timeout": False, - "active_power_reports": False, - "periodic_power_and_energy_reports": False, - "active_energy_reports": False, + "dimming_speed_up_remote": True, + "dimming_speed_up_local": True, + "ramp_rate_off_to_on_local": True, + "ramp_rate_off_to_on_remote": True, + "dimming_speed_down_remote": True, + "dimming_speed_down_local": True, + "ramp_rate_on_to_off_local": True, + "ramp_rate_on_to_off_remote": True, + "minimum_level": True, + "maximum_level": True, + "invert_switch": True, + "auto_off_timer": True, + "default_level_local": True, + "default_level_remote": True, + "state_after_power_restored": True, + "load_level_indicator_timeout": True, + "active_power_reports": True, + "periodic_power_and_energy_reports": True, + "active_energy_reports": True, "power_type": False, "switch_type": False, "button_delay": False, "smart_bulb_mode": False, - "double_tap_up_for_full_brightness": False, - "led_color_when_on": False, - "led_color_when_off": False, - "led_intensity_when_on": False, - "led_intensity_when_off": False, + "double_tap_up_for_full_brightness": True, + "led_color_when_on": True, + "led_color_when_off": True, + "led_intensity_when_on": True, + "led_intensity_when_off": True, "local_protection": False, "output_mode": False, - "on_off_led_mode": False, - "firmware_progress_led": False, - "relay_click_in_on_off_mode": False, + "on_off_led_mode": True, + "firmware_progress_led": True, + "relay_click_in_on_off_mode": True, + "disable_clear_notifications_double_tap": True, } async def issue_all_led_effect( self, - effect_type: LEDEffectType | int = LEDEffectType.Fast_Blink, + effect_type: AllLEDEffectType | int = AllLEDEffectType.Fast_Blink, color: int = 200, level: int = 100, duration: int = 3, @@ -251,7 +239,7 @@ class InovelliConfigEntityChannel(ZigbeeChannel): async def issue_individual_led_effect( self, led_number: int = 1, - effect_type: LEDEffectType | int = LEDEffectType.Fast_Blink, + effect_type: SingleLEDEffectType | int = SingleLEDEffectType.Fast_Blink, color: int = 200, level: int = 100, duration: int = 3, diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 3e2a3591c80..01a08bc2f32 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -13,7 +13,7 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DOMAIN from .api import SERVICE_WARNING_DEVICE_SQUAWK, SERVICE_WARNING_DEVICE_WARN -from .core.channels.manufacturerspecific import InovelliConfigEntityChannel +from .core.channels.manufacturerspecific import AllLEDEffectType, SingleLEDEffectType from .core.const import CHANNEL_IAS_WD, CHANNEL_INOVELLI from .core.helpers import async_get_zha_device @@ -40,9 +40,7 @@ INOVELLI_ALL_LED_EFFECT_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): INOVELLI_ALL_LED_EFFECT, vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required( - "effect_type" - ): InovelliConfigEntityChannel.LEDEffectType.__getitem__, + vol.Required("effect_type"): AllLEDEffectType.__getitem__, vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)), vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)), vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)), @@ -52,10 +50,16 @@ INOVELLI_ALL_LED_EFFECT_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA = INOVELLI_ALL_LED_EFFECT_SCHEMA.extend( { vol.Required(CONF_TYPE): INOVELLI_INDIVIDUAL_LED_EFFECT, - vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(1, 7)), + vol.Required("effect_type"): SingleLEDEffectType.__getitem__, + vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)), } ) +ACTION_SCHEMA_MAP = { + INOVELLI_ALL_LED_EFFECT: INOVELLI_ALL_LED_EFFECT_SCHEMA, + INOVELLI_INDIVIDUAL_LED_EFFECT: INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA, +} + ACTION_SCHEMA = vol.Any( INOVELLI_ALL_LED_EFFECT_SCHEMA, INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA, @@ -83,9 +87,7 @@ DEVICE_ACTION_TYPES = { DEVICE_ACTION_SCHEMAS = { INOVELLI_ALL_LED_EFFECT: vol.Schema( { - vol.Required("effect_type"): vol.In( - InovelliConfigEntityChannel.LEDEffectType.__members__.keys() - ), + vol.Required("effect_type"): vol.In(AllLEDEffectType.__members__.keys()), vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)), vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)), vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)), @@ -94,9 +96,7 @@ DEVICE_ACTION_SCHEMAS = { INOVELLI_INDIVIDUAL_LED_EFFECT: vol.Schema( { vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)), - vol.Required("effect_type"): vol.In( - InovelliConfigEntityChannel.LEDEffectType.__members__.keys() - ), + vol.Required("effect_type"): vol.In(SingleLEDEffectType.__members__.keys()), vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)), vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)), vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)), @@ -127,6 +127,15 @@ async def async_call_action_from_config( ) +async def async_validate_action_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + schema = ACTION_SCHEMA_MAP.get(config[CONF_TYPE], DEFAULT_ACTION_SCHEMA) + config = schema(config) + return config + + async def async_get_actions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e40a54c11bc..c8aebe3b0c0 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.34.2", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.84", + "zha-quirks==0.0.85", "zigpy-deconz==0.19.0", "zigpy==0.51.5", "zigpy-xbee==0.16.2", diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 0bd55cdbe68..0c2e5e7ebe2 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -418,3 +418,15 @@ class InovelliRelayClickInOnOffMode( _zcl_attribute: str = "relay_click_in_on_off_mode" _attr_name: str = "Disable relay click in on off mode" + + +@CONFIG_DIAGNOSTIC_MATCH( + channel_names=CHANNEL_INOVELLI, +) +class InovelliDisableDoubleTapClearNotificationsMode( + ZHASwitchConfigurationEntity, id_suffix="disable_clear_notifications_double_tap" +): + """Inovelli disable clear notifications double tap control.""" + + _zcl_attribute: str = "disable_clear_notifications_double_tap" + _attr_name: str = "Disable config 2x tap to clear notifications" diff --git a/requirements_all.txt b/requirements_all.txt index fa541d0d0fa..0880ff772b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2607,7 +2607,7 @@ zengge==0.2 zeroconf==0.39.4 # homeassistant.components.zha -zha-quirks==0.0.84 +zha-quirks==0.0.85 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 42bae213644..20f356af540 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1808,7 +1808,7 @@ zamg==0.1.1 zeroconf==0.39.4 # homeassistant.components.zha -zha-quirks==0.0.84 +zha-quirks==0.0.85 # homeassistant.components.zha zigpy-deconz==0.19.0 diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 584abbaecdb..19125558b52 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -290,7 +290,7 @@ async def test_action(hass, device_ias, device_inovelli): "domain": DOMAIN, "device_id": inovelli_reg_device.id, "type": "issue_individual_led_effect", - "effect_type": "Open_Close", + "effect_type": "Falling", "led_number": 1, "duration": 5, "level": 10, From 5a6423a944fa43315efb3cded93dcb837dab63ed Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 6 Nov 2022 21:23:48 +0100 Subject: [PATCH 027/114] Always use Celsius in Shelly integration, part 2 (#81602) * Always use Celsius in Shelly integration * Update homeassistant/components/shelly/sensor.py Co-authored-by: Aarni Koskela * Restore unit from the registry during HA startup Co-authored-by: Aarni Koskela --- homeassistant/components/shelly/entity.py | 5 ++++ homeassistant/components/shelly/sensor.py | 30 +++++++++++++++-------- homeassistant/components/shelly/utils.py | 11 ++------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index fd92ea41408..96f566f6a2e 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -9,6 +9,7 @@ from aioshelly.block_device import Block from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry, entity, entity_registry @@ -615,6 +616,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti """Initialize the sleeping sensor.""" self.sensors = sensors self.last_state: StateType = None + self.last_unit: str | None = None self.coordinator = coordinator self.attribute = attribute self.block: Block | None = block # type: ignore[assignment] @@ -644,6 +646,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti if last_state is not None: self.last_state = last_state.state + self.last_unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @callback def _update_callback(self) -> None: @@ -696,6 +699,7 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity, RestoreEntity): ) -> None: """Initialize the sleeping sensor.""" self.last_state: StateType = None + self.last_unit: str | None = None self.coordinator = coordinator self.key = key self.attribute = attribute @@ -725,3 +729,4 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity, RestoreEntity): if last_state is not None: self.last_state = last_state.state + self.last_unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index cf1eb7508c7..922a5ac8b5c 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -47,7 +47,7 @@ from .entity import ( async_setup_entry_rest, async_setup_entry_rpc, ) -from .utils import get_device_entry_gen, get_device_uptime, temperature_unit +from .utils import get_device_entry_gen, get_device_uptime @dataclass @@ -79,7 +79,7 @@ SENSORS: Final = { ("device", "deviceTemp"): BlockSensorDescription( key="device|deviceTemp", name="Device Temperature", - unit_fn=temperature_unit, + native_unit_of_measurement=TEMP_CELSIUS, value=lambda value: round(value, 1), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -221,7 +221,7 @@ SENSORS: Final = { ("sensor", "temp"): BlockSensorDescription( key="sensor|temp", name="Temperature", - unit_fn=temperature_unit, + native_unit_of_measurement=TEMP_CELSIUS, value=lambda value: round(value, 1), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -230,7 +230,7 @@ SENSORS: Final = { ("sensor", "extTemp"): BlockSensorDescription( key="sensor|extTemp", name="Temperature", - unit_fn=temperature_unit, + native_unit_of_measurement=TEMP_CELSIUS, value=lambda value: round(value, 1), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -499,8 +499,6 @@ class BlockSensor(ShellyBlockAttributeEntity, SensorEntity): super().__init__(coordinator, block, attribute, description) self._attr_native_unit_of_measurement = description.native_unit_of_measurement - if unit_fn := description.unit_fn: - self._attr_native_unit_of_measurement = unit_fn(block.info(attribute)) @property def native_value(self) -> StateType: @@ -547,10 +545,6 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity): """Initialize the sleeping sensor.""" super().__init__(coordinator, block, attribute, description, entry, sensors) - self._attr_native_unit_of_measurement = description.native_unit_of_measurement - if block and (unit_fn := description.unit_fn): - self._attr_native_unit_of_measurement = unit_fn(block.info(attribute)) - @property def native_value(self) -> StateType: """Return value of sensor.""" @@ -559,6 +553,14 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity): return self.last_state + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the sensor, if any.""" + if self.block is not None: + return self.entity_description.native_unit_of_measurement + + return self.last_unit + class RpcSleepingSensor(ShellySleepingRpcAttributeEntity, SensorEntity): """Represent a RPC sleeping sensor.""" @@ -572,3 +574,11 @@ class RpcSleepingSensor(ShellySleepingRpcAttributeEntity, SensorEntity): return self.attribute_value return self.last_state + + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the sensor, if any.""" + if self.coordinator.device.initialized: + return self.entity_description.native_unit_of_measurement + + return self.last_unit diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index a13d84d32be..bf242d47e6c 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -5,13 +5,13 @@ from datetime import datetime, timedelta from typing import Any, cast from aiohttp.web import Request, WebSocketResponse -from aioshelly.block_device import BLOCK_VALUE_UNIT, COAP, Block, BlockDevice +from aioshelly.block_device import COAP, Block, BlockDevice from aioshelly.const import MODEL_NAMES from aioshelly.rpc_device import RpcDevice, WsServer from homeassistant.components.http import HomeAssistantView from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry, entity_registry, singleton from homeassistant.helpers.typing import EventType @@ -43,13 +43,6 @@ def async_remove_shelly_entity( entity_reg.async_remove(entity_id) -def temperature_unit(block_info: dict[str, Any]) -> str: - """Detect temperature unit.""" - if block_info[BLOCK_VALUE_UNIT] == "F": - return TEMP_FAHRENHEIT - return TEMP_CELSIUS - - def get_block_device_name(device: BlockDevice) -> str: """Naming for device.""" return cast(str, device.settings["name"] or device.settings["device"]["hostname"]) From 0983f8aadf8ab512a06fc5961f941360379b5704 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 6 Nov 2022 13:02:59 +0100 Subject: [PATCH 028/114] Bump PyXiaomiGateway to 0.14.3 (#81603) Fixes: #80249 --- homeassistant/components/xiaomi_aqara/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/manifest.json b/homeassistant/components/xiaomi_aqara/manifest.json index a70fb90f961..8152a77a73e 100644 --- a/homeassistant/components/xiaomi_aqara/manifest.json +++ b/homeassistant/components/xiaomi_aqara/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Gateway (Aqara)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara", - "requirements": ["PyXiaomiGateway==0.14.1"], + "requirements": ["PyXiaomiGateway==0.14.3"], "after_dependencies": ["discovery"], "codeowners": ["@danielhiversen", "@syssi"], "zeroconf": ["_miio._udp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index 0880ff772b0..ecbe0b8e6b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -50,7 +50,7 @@ PyTurboJPEG==1.6.7 PyViCare==2.17.0 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.14.1 +PyXiaomiGateway==0.14.3 # homeassistant.components.remember_the_milk RtmAPI==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 20f356af540..c594deaa475 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -46,7 +46,7 @@ PyTurboJPEG==1.6.7 PyViCare==2.17.0 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.14.1 +PyXiaomiGateway==0.14.3 # homeassistant.components.remember_the_milk RtmAPI==0.7.2 From 9771147a1e05c87fc1eb22422e93cd2711196914 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 5 Nov 2022 08:40:28 -0400 Subject: [PATCH 029/114] Fix invalid min and max color temp in bad ZHA light devices (#81604) * Fix ZHA default color temps * update test --- .../components/zha/core/channels/lighting.py | 18 ++++++++++++++++-- tests/components/zha/test_light.py | 6 +++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index e70eea11a87..ffbbc32a7a5 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -98,12 +98,26 @@ class ColorChannel(ZigbeeChannel): @property def min_mireds(self) -> int: """Return the coldest color_temp that this channel supports.""" - return self.cluster.get("color_temp_physical_min", self.MIN_MIREDS) + min_mireds = self.cluster.get("color_temp_physical_min", self.MIN_MIREDS) + if min_mireds == 0: + self.warning( + "[Min mireds is 0, setting to %s] Please open an issue on the quirks repo to have this device corrected", + self.MIN_MIREDS, + ) + min_mireds = self.MIN_MIREDS + return min_mireds @property def max_mireds(self) -> int: """Return the warmest color_temp that this channel supports.""" - return self.cluster.get("color_temp_physical_max", self.MAX_MIREDS) + max_mireds = self.cluster.get("color_temp_physical_max", self.MAX_MIREDS) + if max_mireds == 0: + self.warning( + "[Max mireds is 0, setting to %s] Please open an issue on the quirks repo to have this device corrected", + self.MAX_MIREDS, + ) + max_mireds = self.MAX_MIREDS + return max_mireds @property def hs_supported(self) -> bool: diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index a9b8c7a14ee..d40605f81dc 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -242,7 +242,9 @@ async def eWeLink_light(hass, zigpy_device_mock, zha_device_joined): color_cluster = zigpy_device.endpoints[1].light_color color_cluster.PLUGGED_ATTR_READS = { "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature - | lighting.Color.ColorCapabilities.XY_attributes + | lighting.Color.ColorCapabilities.XY_attributes, + "color_temp_physical_min": 0, + "color_temp_physical_max": 0, } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True @@ -1192,6 +1194,8 @@ async def test_transitions( assert eWeLink_state.state == STATE_ON assert eWeLink_state.attributes["color_temp"] == 235 assert eWeLink_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + assert eWeLink_state.attributes["min_mireds"] == 153 + assert eWeLink_state.attributes["max_mireds"] == 500 async def async_test_on_off_from_light(hass, cluster, entity_id): From 9beb9f6fc027b165f21d2fa9526d0cce72f8b8c2 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sat, 5 Nov 2022 23:11:59 +0100 Subject: [PATCH 030/114] Fix repeating SSDP errors by checking address scope_ids and proper hostname (#81611) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 9d05b02000b..e4620386b98 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.32.1"], + "requirements": ["async-upnp-client==0.32.2"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 98ad81e653a..7c7a312159b 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dms", - "requirements": ["async-upnp-client==0.32.1"], + "requirements": ["async-upnp-client==0.32.2"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index a7aa842f485..b7988a558bd 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -7,7 +7,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.5.0", "wakeonlan==2.1.0", - "async-upnp-client==0.32.1" + "async-upnp-client==0.32.2" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 59d9d6ddad8..3b30146e756 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.32.1"], + "requirements": ["async-upnp-client==0.32.2"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 9b4151c35c5..4c45b099193 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.32.1", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.32.2", "getmac==0.8.2"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 16fe2ae7700..6c450548135 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.1"], + "requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.2"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 53966a90141..cabe62b50ec 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.13 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.32.1 +async-upnp-client==0.32.2 async_timeout==4.0.2 atomicwrites-homeassistant==1.4.1 attrs==21.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index ecbe0b8e6b8..d6473b7a5c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -353,7 +353,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.32.1 +async-upnp-client==0.32.2 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c594deaa475..a7d4b6bfb10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -307,7 +307,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.32.1 +async-upnp-client==0.32.2 # homeassistant.components.sleepiq asyncsleepiq==1.2.3 From b9757235a768efbd2f82a04996304b76277aacbf Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sat, 5 Nov 2022 21:26:19 +0100 Subject: [PATCH 031/114] Bump plugwise to v0.25.7 (#81612) --- homeassistant/components/plugwise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 7f3e979ab7d..6bb1c941bf3 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.25.3"], + "requirements": ["plugwise==0.25.7"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index d6473b7a5c2..3c44a05c335 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1312,7 +1312,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.25.3 +plugwise==0.25.7 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7d4b6bfb10..40828d4f161 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -939,7 +939,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.25.3 +plugwise==0.25.7 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 From ac15f2cf9dfc30f107c22a3c3797731b2dfaa1ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Nov 2022 04:15:16 -0600 Subject: [PATCH 032/114] Fix homekit bridge iid allocations (#81613) fixes undefined --- homeassistant/components/homekit/__init__.py | 10 +- .../components/homekit/accessories.py | 8 +- .../components/homekit/diagnostics.py | 2 + .../components/homekit/iidmanager.py | 72 ++++- tests/components/homekit/conftest.py | 8 +- tests/components/homekit/fixtures/iids_v1 | 249 ++++++++++++++++ .../homekit/fixtures/iids_v1_with_underscore | 50 ++++ tests/components/homekit/fixtures/iids_v2 | 273 ++++++++++++++++++ .../homekit/fixtures/iids_v2_with_underscore | 54 ++++ tests/components/homekit/test_accessories.py | 22 +- tests/components/homekit/test_diagnostics.py | 28 ++ tests/components/homekit/test_homekit.py | 6 +- tests/components/homekit/test_iidmanager.py | 83 +++++- 13 files changed, 806 insertions(+), 59 deletions(-) create mode 100644 tests/components/homekit/fixtures/iids_v1 create mode 100644 tests/components/homekit/fixtures/iids_v1_with_underscore create mode 100644 tests/components/homekit/fixtures/iids_v2 create mode 100644 tests/components/homekit/fixtures/iids_v2_with_underscore diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 333d6052d17..ca73c7dc242 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -76,13 +76,7 @@ from . import ( # noqa: F401 type_switches, type_thermostats, ) -from .accessories import ( - HomeAccessory, - HomeBridge, - HomeDriver, - HomeIIDManager, - get_accessory, -) +from .accessories import HomeAccessory, HomeBridge, HomeDriver, get_accessory from .aidmanager import AccessoryAidStorage from .const import ( ATTR_INTEGRATION, @@ -551,7 +545,7 @@ class HomeKit: async_zeroconf_instance=async_zeroconf_instance, zeroconf_server=f"{uuid}-hap.local.", loader=get_loader(), - iid_manager=HomeIIDManager(self.iid_storage), + iid_storage=self.iid_storage, ) # If we do not load the mac address will be wrong diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 61c2e3cd5dd..44b73b41ded 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -270,7 +270,7 @@ class HomeAccessory(Accessory): # type: ignore[misc] driver=driver, display_name=cleanup_name_for_homekit(name), aid=aid, - iid_manager=driver.iid_manager, + iid_manager=HomeIIDManager(driver.iid_storage), *args, **kwargs, ) @@ -570,7 +570,7 @@ class HomeBridge(Bridge): # type: ignore[misc] def __init__(self, hass: HomeAssistant, driver: HomeDriver, name: str) -> None: """Initialize a Bridge object.""" - super().__init__(driver, name, iid_manager=driver.iid_manager) + super().__init__(driver, name, iid_manager=HomeIIDManager(driver.iid_storage)) self.set_info_service( firmware_revision=format_version(__version__), manufacturer=MANUFACTURER, @@ -603,7 +603,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc] entry_id: str, bridge_name: str, entry_title: str, - iid_manager: HomeIIDManager, + iid_storage: AccessoryIIDStorage, **kwargs: Any, ) -> None: """Initialize a AccessoryDriver object.""" @@ -612,7 +612,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc] self._entry_id = entry_id self._bridge_name = bridge_name self._entry_title = entry_title - self.iid_manager = iid_manager + self.iid_storage = iid_storage @pyhap_callback # type: ignore[misc] def pair( diff --git a/homeassistant/components/homekit/diagnostics.py b/homeassistant/components/homekit/diagnostics.py index dbd40c1d6f5..1d0bfb92fcc 100644 --- a/homeassistant/components/homekit/diagnostics.py +++ b/homeassistant/components/homekit/diagnostics.py @@ -31,6 +31,8 @@ async def async_get_config_entry_diagnostics( "options": dict(entry.options), }, } + if homekit.iid_storage: + data["iid_storage"] = homekit.iid_storage.allocations if not homekit.driver: # not started yet or startup failed return data driver: AccessoryDriver = homekit.driver diff --git a/homeassistant/components/homekit/iidmanager.py b/homeassistant/components/homekit/iidmanager.py index 1b5cc7d6722..3805748225a 100644 --- a/homeassistant/components/homekit/iidmanager.py +++ b/homeassistant/components/homekit/iidmanager.py @@ -17,7 +17,7 @@ from homeassistant.helpers.storage import Store from .util import get_iid_storage_filename_for_entry_id -IID_MANAGER_STORAGE_VERSION = 1 +IID_MANAGER_STORAGE_VERSION = 2 IID_MANAGER_SAVE_DELAY = 2 ALLOCATIONS_KEY = "allocations" @@ -26,6 +26,40 @@ IID_MIN = 1 IID_MAX = 18446744073709551615 +ACCESSORY_INFORMATION_SERVICE = "3E" + + +class IIDStorage(Store): + """Storage class for IIDManager.""" + + async def _async_migrate_func( + self, + old_major_version: int, + old_minor_version: int, + old_data: dict, + ): + """Migrate to the new version.""" + if old_major_version == 1: + # Convert v1 to v2 format which uses a unique iid set per accessory + # instead of per pairing since we need the ACCESSORY_INFORMATION_SERVICE + # to always have iid 1 for each bridged accessory as well as the bridge + old_allocations: dict[str, int] = old_data.pop(ALLOCATIONS_KEY, {}) + new_allocation: dict[str, dict[str, int]] = {} + old_data[ALLOCATIONS_KEY] = new_allocation + for allocation_key, iid in old_allocations.items(): + aid_str, new_allocation_key = allocation_key.split("_", 1) + service_type, _, char_type, *_ = new_allocation_key.split("_") + accessory_allocation = new_allocation.setdefault(aid_str, {}) + if service_type == ACCESSORY_INFORMATION_SERVICE and not char_type: + accessory_allocation[new_allocation_key] = 1 + elif iid != 1: + accessory_allocation[new_allocation_key] = iid + + return old_data + + raise NotImplementedError + + class AccessoryIIDStorage: """ Provide stable allocation of IIDs for the lifetime of an accessory. @@ -37,15 +71,15 @@ class AccessoryIIDStorage: def __init__(self, hass: HomeAssistant, entry_id: str) -> None: """Create a new iid store.""" self.hass = hass - self.allocations: dict[str, int] = {} - self.allocated_iids: list[int] = [] + self.allocations: dict[str, dict[str, int]] = {} + self.allocated_iids: dict[str, list[int]] = {} self.entry_id = entry_id - self.store: Store | None = None + self.store: IIDStorage | None = None async def async_initialize(self) -> None: """Load the latest IID data.""" iid_store = get_iid_storage_filename_for_entry_id(self.entry_id) - self.store = Store(self.hass, IID_MANAGER_STORAGE_VERSION, iid_store) + self.store = IIDStorage(self.hass, IID_MANAGER_STORAGE_VERSION, iid_store) if not (raw_storage := await self.store.async_load()): # There is no data about iid allocations yet @@ -53,7 +87,8 @@ class AccessoryIIDStorage: assert isinstance(raw_storage, dict) self.allocations = raw_storage.get(ALLOCATIONS_KEY, {}) - self.allocated_iids = sorted(self.allocations.values()) + for aid_str, allocations in self.allocations.items(): + self.allocated_iids[aid_str] = sorted(allocations.values()) def get_or_allocate_iid( self, @@ -68,16 +103,25 @@ class AccessoryIIDStorage: char_hap_type: str | None = uuid_to_hap_type(char_uuid) if char_uuid else None # Allocation key must be a string since we are saving it to JSON allocation_key = ( - f'{aid}_{service_hap_type}_{service_unique_id or ""}_' + f'{service_hap_type}_{service_unique_id or ""}_' f'{char_hap_type or ""}_{char_unique_id or ""}' ) - if allocation_key in self.allocations: - return self.allocations[allocation_key] - next_iid = self.allocated_iids[-1] + 1 if self.allocated_iids else 1 - self.allocations[allocation_key] = next_iid - self.allocated_iids.append(next_iid) + # AID must be a string since JSON keys cannot be int + aid_str = str(aid) + accessory_allocation = self.allocations.setdefault(aid_str, {}) + accessory_allocated_iids = self.allocated_iids.setdefault(aid_str, []) + if service_hap_type == ACCESSORY_INFORMATION_SERVICE and char_uuid is None: + allocated_iid = 1 + elif allocation_key in accessory_allocation: + return accessory_allocation[allocation_key] + elif accessory_allocated_iids: + allocated_iid = accessory_allocated_iids[-1] + 1 + else: + allocated_iid = 2 + accessory_allocation[allocation_key] = allocated_iid + accessory_allocated_iids.append(allocated_iid) self._async_schedule_save() - return next_iid + return allocated_iid @callback def _async_schedule_save(self) -> None: @@ -91,6 +135,6 @@ class AccessoryIIDStorage: return await self.store.async_save(self._data_to_save()) @callback - def _data_to_save(self) -> dict[str, dict[str, int]]: + def _data_to_save(self) -> dict[str, dict[str, dict[str, int]]]: """Return data of entity map to store in a file.""" return {ALLOCATIONS_KEY: self.allocations} diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 7b79e0f9b6b..b0422a40f72 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest from homeassistant.components.device_tracker.legacy import YAML_DEVICES -from homeassistant.components.homekit.accessories import HomeDriver, HomeIIDManager +from homeassistant.components.homekit.accessories import HomeDriver from homeassistant.components.homekit.const import BRIDGE_NAME, EVENT_HOMEKIT_CHANGED from homeassistant.components.homekit.iidmanager import AccessoryIIDStorage @@ -39,7 +39,7 @@ def run_driver(hass, loop, iid_storage): entry_id="", entry_title="mock entry", bridge_name=BRIDGE_NAME, - iid_manager=HomeIIDManager(iid_storage), + iid_storage=iid_storage, address="127.0.0.1", loop=loop, ) @@ -63,7 +63,7 @@ def hk_driver(hass, loop, iid_storage): entry_id="", entry_title="mock entry", bridge_name=BRIDGE_NAME, - iid_manager=HomeIIDManager(iid_storage), + iid_storage=iid_storage, address="127.0.0.1", loop=loop, ) @@ -91,7 +91,7 @@ def mock_hap(hass, loop, iid_storage, mock_zeroconf): entry_id="", entry_title="mock entry", bridge_name=BRIDGE_NAME, - iid_manager=HomeIIDManager(iid_storage), + iid_storage=iid_storage, address="127.0.0.1", loop=loop, ) diff --git a/tests/components/homekit/fixtures/iids_v1 b/tests/components/homekit/fixtures/iids_v1 new file mode 100644 index 00000000000..1da11d8de67 --- /dev/null +++ b/tests/components/homekit/fixtures/iids_v1 @@ -0,0 +1,249 @@ +{ + "version": 1, + "minor_version": 1, + "key": "homekit.v1.iids", + "data": { + "allocations": { + "1_3E___": 1, + "1_3E__14_": 2, + "1_3E__20_": 3, + "1_3E__21_": 4, + "1_3E__23_": 5, + "1_3E__30_": 6, + "1_3E__52_": 7, + "1_A2___": 8, + "1_A2__37_": 9, + "935391877_3E___": 10, + "935391877_3E__14_": 11, + "935391877_3E__20_": 12, + "935391877_3E__21_": 13, + "935391877_3E__23_": 14, + "935391877_3E__30_": 15, + "935391877_3E__52_": 16, + "935391877_4A___": 17, + "935391877_4A__F_": 18, + "935391877_4A__33_": 19, + "935391877_4A__11_": 20, + "935391877_4A__35_": 21, + "935391877_4A__36_": 22, + "935391877_4A__D_": 23, + "935391877_4A__12_": 24, + "935391877_4A__34_": 25, + "935391877_4A__10_": 26, + "935391877_B7___": 27, + "935391877_B7__B0_": 28, + "935391877_B7__BF_": 29, + "935391877_B7__AF_": 30, + "985724734_3E___": 31, + "985724734_3E__14_": 32, + "985724734_3E__20_": 33, + "985724734_3E__21_": 34, + "985724734_3E__23_": 35, + "985724734_3E__30_": 36, + "985724734_3E__52_": 37, + "985724734_4A___": 38, + "985724734_4A__F_": 39, + "985724734_4A__33_": 40, + "985724734_4A__11_": 41, + "985724734_4A__35_": 42, + "985724734_4A__36_": 43, + "985724734_4A__D_": 44, + "985724734_4A__12_": 45, + "985724734_4A__34_": 46, + "985724734_4A__10_": 47, + "985724734_B7___": 48, + "985724734_B7__B0_": 49, + "985724734_B7__BF_": 50, + "985724734_B7__AF_": 51, + "3083074204_3E___": 52, + "3083074204_3E__14_": 53, + "3083074204_3E__20_": 54, + "3083074204_3E__21_": 55, + "3083074204_3E__23_": 56, + "3083074204_3E__30_": 57, + "3083074204_3E__52_": 58, + "3083074204_4A___": 59, + "3083074204_4A__F_": 60, + "3083074204_4A__33_": 61, + "3083074204_4A__11_": 62, + "3083074204_4A__35_": 63, + "3083074204_4A__36_": 64, + "3083074204_4A__D_": 65, + "3083074204_4A__12_": 66, + "3083074204_4A__34_": 67, + "3083074204_4A__10_": 68, + "3083074204_B7___": 69, + "3083074204_B7__B0_": 70, + "3083074204_B7__BF_": 71, + "3083074204_B7__AF_": 72, + "3032741347_3E___": 73, + "3032741347_3E__14_": 74, + "3032741347_3E__20_": 75, + "3032741347_3E__21_": 76, + "3032741347_3E__23_": 77, + "3032741347_3E__30_": 78, + "3032741347_3E__52_": 79, + "3032741347_4A___": 80, + "3032741347_4A__F_": 81, + "3032741347_4A__33_": 82, + "3032741347_4A__11_": 83, + "3032741347_4A__35_": 84, + "3032741347_4A__36_": 85, + "3032741347_4A__D_": 86, + "3032741347_4A__12_": 87, + "3032741347_4A__34_": 88, + "3032741347_4A__10_": 89, + "3032741347_B7___": 90, + "3032741347_B7__B0_": 91, + "3032741347_B7__BF_": 92, + "3032741347_B7__AF_": 93, + "3681509609_3E___": 94, + "3681509609_3E__14_": 95, + "3681509609_3E__20_": 96, + "3681509609_3E__21_": 97, + "3681509609_3E__23_": 98, + "3681509609_3E__30_": 99, + "3681509609_3E__52_": 100, + "3681509609_4A___": 101, + "3681509609_4A__F_": 102, + "3681509609_4A__33_": 103, + "3681509609_4A__11_": 104, + "3681509609_4A__35_": 105, + "3681509609_4A__36_": 106, + "3681509609_4A__D_": 107, + "3681509609_4A__12_": 108, + "3681509609_4A__34_": 109, + "3681509609_4A__10_": 110, + "3681509609_B7___": 111, + "3681509609_B7__B0_": 112, + "3681509609_B7__BF_": 113, + "3681509609_B7__AF_": 114, + "3866063418_3E___": 115, + "3866063418_3E__14_": 116, + "3866063418_3E__20_": 117, + "3866063418_3E__21_": 118, + "3866063418_3E__23_": 119, + "3866063418_3E__30_": 120, + "3866063418_3E__52_": 121, + "3866063418_4A___": 122, + "3866063418_4A__F_": 123, + "3866063418_4A__33_": 124, + "3866063418_4A__11_": 125, + "3866063418_4A__35_": 126, + "3866063418_4A__36_": 127, + "3866063418_4A__D_": 128, + "3866063418_4A__12_": 129, + "3866063418_4A__34_": 130, + "3866063418_4A__10_": 131, + "3866063418_B7___": 132, + "3866063418_B7__B0_": 133, + "3866063418_B7__BF_": 134, + "3866063418_B7__AF_": 135, + "3239498961_3E___": 136, + "3239498961_3E__14_": 137, + "3239498961_3E__20_": 138, + "3239498961_3E__21_": 139, + "3239498961_3E__23_": 140, + "3239498961_3E__30_": 141, + "3239498961_3E__52_": 142, + "3239498961_4A___": 143, + "3239498961_4A__F_": 144, + "3239498961_4A__33_": 145, + "3239498961_4A__11_": 146, + "3239498961_4A__35_": 147, + "3239498961_4A__36_": 148, + "3239498961_4A__D_": 149, + "3239498961_4A__12_": 150, + "3239498961_4A__34_": 151, + "3239498961_4A__10_": 152, + "3239498961_B7___": 153, + "3239498961_B7__B0_": 154, + "3239498961_B7__BF_": 155, + "3239498961_B7__AF_": 156, + "3289831818_3E___": 157, + "3289831818_3E__14_": 158, + "3289831818_3E__20_": 159, + "3289831818_3E__21_": 160, + "3289831818_3E__23_": 161, + "3289831818_3E__30_": 162, + "3289831818_3E__52_": 163, + "3289831818_4A___": 164, + "3289831818_4A__F_": 165, + "3289831818_4A__33_": 166, + "3289831818_4A__11_": 167, + "3289831818_4A__35_": 168, + "3289831818_4A__36_": 169, + "3289831818_4A__D_": 170, + "3289831818_4A__12_": 171, + "3289831818_4A__34_": 172, + "3289831818_4A__10_": 173, + "3289831818_B7___": 174, + "3289831818_B7__B0_": 175, + "3289831818_B7__BF_": 176, + "3289831818_B7__AF_": 177, + "3071722771_3E___": 178, + "3071722771_3E__14_": 179, + "3071722771_3E__20_": 180, + "3071722771_3E__21_": 181, + "3071722771_3E__23_": 182, + "3071722771_3E__30_": 183, + "3071722771_3E__52_": 184, + "3071722771_4A___": 185, + "3071722771_4A__F_": 186, + "3071722771_4A__33_": 187, + "3071722771_4A__11_": 188, + "3071722771_4A__35_": 189, + "3071722771_4A__36_": 190, + "3071722771_4A__D_": 191, + "3071722771_4A__12_": 192, + "3071722771_4A__34_": 193, + "3071722771_4A__10_": 194, + "3071722771_B7___": 195, + "3071722771_B7__B0_": 196, + "3071722771_B7__BF_": 197, + "3071722771_B7__AF_": 198, + "3391630365_3E___": 199, + "3391630365_3E__14_": 200, + "3391630365_3E__20_": 201, + "3391630365_3E__21_": 202, + "3391630365_3E__23_": 203, + "3391630365_3E__30_": 204, + "3391630365_3E__52_": 205, + "3391630365_4A___": 206, + "3391630365_4A__F_": 207, + "3391630365_4A__33_": 208, + "3391630365_4A__11_": 209, + "3391630365_4A__35_": 210, + "3391630365_4A__36_": 211, + "3391630365_4A__D_": 212, + "3391630365_4A__12_": 213, + "3391630365_4A__34_": 214, + "3391630365_4A__10_": 215, + "3391630365_B7___": 216, + "3391630365_B7__B0_": 217, + "3391630365_B7__BF_": 218, + "3391630365_B7__AF_": 219, + "3274187032_3E___": 220, + "3274187032_3E__14_": 221, + "3274187032_3E__20_": 222, + "3274187032_3E__21_": 223, + "3274187032_3E__23_": 224, + "3274187032_3E__30_": 225, + "3274187032_3E__52_": 226, + "3274187032_4A___": 227, + "3274187032_4A__F_": 228, + "3274187032_4A__33_": 229, + "3274187032_4A__11_": 230, + "3274187032_4A__35_": 231, + "3274187032_4A__36_": 232, + "3274187032_4A__D_": 233, + "3274187032_4A__12_": 234, + "3274187032_4A__34_": 235, + "3274187032_4A__10_": 236, + "3274187032_B7___": 237, + "3274187032_B7__B0_": 238, + "3274187032_B7__BF_": 239, + "3274187032_B7__AF_": 240 + } + } +} diff --git a/tests/components/homekit/fixtures/iids_v1_with_underscore b/tests/components/homekit/fixtures/iids_v1_with_underscore new file mode 100644 index 00000000000..844c17a4746 --- /dev/null +++ b/tests/components/homekit/fixtures/iids_v1_with_underscore @@ -0,0 +1,50 @@ +{ + "version": 1, + "minor_version": 1, + "key": "homekit.8a47205bd97c07d7a908f10166ebe636.iids", + "data": { + "allocations": { + "1_3E___": 1, + "1_3E__14_": 2, + "1_3E__20_": 3, + "1_3E__21_": 4, + "1_3E__23_": 5, + "1_3E__30_": 6, + "1_3E__52_": 7, + "1_A2___": 8, + "1_A2__37_": 9, + "1973560704_3E___": 10, + "1973560704_3E__14_": 11, + "1973560704_3E__20_": 12, + "1973560704_3E__21_": 13, + "1973560704_3E__23_": 14, + "1973560704_3E__30_": 15, + "1973560704_3E__52_": 16, + "1973560704_3E__53_": 17, + "1973560704_89_pressed-__": 18, + "1973560704_89_pressed-_73_": 19, + "1973560704_89_pressed-_23_": 20, + "1973560704_89_pressed-_CB_": 21, + "1973560704_CC_pressed-__": 22, + "1973560704_CC_pressed-_CD_": 23, + "1973560704_89_changed_states-__": 24, + "1973560704_89_changed_states-_73_": 25, + "1973560704_89_changed_states-_23_": 26, + "1973560704_89_changed_states-_CB_": 27, + "1973560704_CC_changed_states-__": 28, + "1973560704_CC_changed_states-_CD_": 29, + "1973560704_89_turned_off-__": 30, + "1973560704_89_turned_off-_73_": 31, + "1973560704_89_turned_off-_23_": 32, + "1973560704_89_turned_off-_CB_": 33, + "1973560704_CC_turned_off-__": 34, + "1973560704_CC_turned_off-_CD_": 35, + "1973560704_89_turned_on-__": 36, + "1973560704_89_turned_on-_73_": 37, + "1973560704_89_turned_on-_23_": 38, + "1973560704_89_turned_on-_CB_": 39, + "1973560704_CC_turned_on-__": 40, + "1973560704_CC_turned_on-_CD_": 41 + } + } +} \ No newline at end of file diff --git a/tests/components/homekit/fixtures/iids_v2 b/tests/components/homekit/fixtures/iids_v2 new file mode 100644 index 00000000000..76bff55e935 --- /dev/null +++ b/tests/components/homekit/fixtures/iids_v2 @@ -0,0 +1,273 @@ +{ + "version": 2, + "minor_version": 1, + "key": "homekit.v2.iids", + "data": { + "allocations": { + "1": { + "3E___": 1, + "3E__14_": 2, + "3E__20_": 3, + "3E__21_": 4, + "3E__23_": 5, + "3E__30_": 6, + "3E__52_": 7, + "A2___": 8, + "A2__37_": 9 + }, + "935391877": { + "3E___": 1, + "3E__14_": 11, + "3E__20_": 12, + "3E__21_": 13, + "3E__23_": 14, + "3E__30_": 15, + "3E__52_": 16, + "4A___": 17, + "4A__F_": 18, + "4A__33_": 19, + "4A__11_": 20, + "4A__35_": 21, + "4A__36_": 22, + "4A__D_": 23, + "4A__12_": 24, + "4A__34_": 25, + "4A__10_": 26, + "B7___": 27, + "B7__B0_": 28, + "B7__BF_": 29, + "B7__AF_": 30 + }, + "985724734": { + "3E___": 1, + "3E__14_": 32, + "3E__20_": 33, + "3E__21_": 34, + "3E__23_": 35, + "3E__30_": 36, + "3E__52_": 37, + "4A___": 38, + "4A__F_": 39, + "4A__33_": 40, + "4A__11_": 41, + "4A__35_": 42, + "4A__36_": 43, + "4A__D_": 44, + "4A__12_": 45, + "4A__34_": 46, + "4A__10_": 47, + "B7___": 48, + "B7__B0_": 49, + "B7__BF_": 50, + "B7__AF_": 51 + }, + "3083074204": { + "3E___": 1, + "3E__14_": 53, + "3E__20_": 54, + "3E__21_": 55, + "3E__23_": 56, + "3E__30_": 57, + "3E__52_": 58, + "4A___": 59, + "4A__F_": 60, + "4A__33_": 61, + "4A__11_": 62, + "4A__35_": 63, + "4A__36_": 64, + "4A__D_": 65, + "4A__12_": 66, + "4A__34_": 67, + "4A__10_": 68, + "B7___": 69, + "B7__B0_": 70, + "B7__BF_": 71, + "B7__AF_": 72 + }, + "3032741347": { + "3E___": 1, + "3E__14_": 74, + "3E__20_": 75, + "3E__21_": 76, + "3E__23_": 77, + "3E__30_": 78, + "3E__52_": 79, + "4A___": 80, + "4A__F_": 81, + "4A__33_": 82, + "4A__11_": 83, + "4A__35_": 84, + "4A__36_": 85, + "4A__D_": 86, + "4A__12_": 87, + "4A__34_": 88, + "4A__10_": 89, + "B7___": 90, + "B7__B0_": 91, + "B7__BF_": 92, + "B7__AF_": 93 + }, + "3681509609": { + "3E___": 1, + "3E__14_": 95, + "3E__20_": 96, + "3E__21_": 97, + "3E__23_": 98, + "3E__30_": 99, + "3E__52_": 100, + "4A___": 101, + "4A__F_": 102, + "4A__33_": 103, + "4A__11_": 104, + "4A__35_": 105, + "4A__36_": 106, + "4A__D_": 107, + "4A__12_": 108, + "4A__34_": 109, + "4A__10_": 110, + "B7___": 111, + "B7__B0_": 112, + "B7__BF_": 113, + "B7__AF_": 114 + }, + "3866063418": { + "3E___": 1, + "3E__14_": 116, + "3E__20_": 117, + "3E__21_": 118, + "3E__23_": 119, + "3E__30_": 120, + "3E__52_": 121, + "4A___": 122, + "4A__F_": 123, + "4A__33_": 124, + "4A__11_": 125, + "4A__35_": 126, + "4A__36_": 127, + "4A__D_": 128, + "4A__12_": 129, + "4A__34_": 130, + "4A__10_": 131, + "B7___": 132, + "B7__B0_": 133, + "B7__BF_": 134, + "B7__AF_": 135 + }, + "3239498961": { + "3E___": 1, + "3E__14_": 137, + "3E__20_": 138, + "3E__21_": 139, + "3E__23_": 140, + "3E__30_": 141, + "3E__52_": 142, + "4A___": 143, + "4A__F_": 144, + "4A__33_": 145, + "4A__11_": 146, + "4A__35_": 147, + "4A__36_": 148, + "4A__D_": 149, + "4A__12_": 150, + "4A__34_": 151, + "4A__10_": 152, + "B7___": 153, + "B7__B0_": 154, + "B7__BF_": 155, + "B7__AF_": 156 + }, + "3289831818": { + "3E___": 1, + "3E__14_": 158, + "3E__20_": 159, + "3E__21_": 160, + "3E__23_": 161, + "3E__30_": 162, + "3E__52_": 163, + "4A___": 164, + "4A__F_": 165, + "4A__33_": 166, + "4A__11_": 167, + "4A__35_": 168, + "4A__36_": 169, + "4A__D_": 170, + "4A__12_": 171, + "4A__34_": 172, + "4A__10_": 173, + "B7___": 174, + "B7__B0_": 175, + "B7__BF_": 176, + "B7__AF_": 177 + }, + "3071722771": { + "3E___": 1, + "3E__14_": 179, + "3E__20_": 180, + "3E__21_": 181, + "3E__23_": 182, + "3E__30_": 183, + "3E__52_": 184, + "4A___": 185, + "4A__F_": 186, + "4A__33_": 187, + "4A__11_": 188, + "4A__35_": 189, + "4A__36_": 190, + "4A__D_": 191, + "4A__12_": 192, + "4A__34_": 193, + "4A__10_": 194, + "B7___": 195, + "B7__B0_": 196, + "B7__BF_": 197, + "B7__AF_": 198 + }, + "3391630365": { + "3E___": 1, + "3E__14_": 200, + "3E__20_": 201, + "3E__21_": 202, + "3E__23_": 203, + "3E__30_": 204, + "3E__52_": 205, + "4A___": 206, + "4A__F_": 207, + "4A__33_": 208, + "4A__11_": 209, + "4A__35_": 210, + "4A__36_": 211, + "4A__D_": 212, + "4A__12_": 213, + "4A__34_": 214, + "4A__10_": 215, + "B7___": 216, + "B7__B0_": 217, + "B7__BF_": 218, + "B7__AF_": 219 + }, + "3274187032": { + "3E___": 1, + "3E__14_": 221, + "3E__20_": 222, + "3E__21_": 223, + "3E__23_": 224, + "3E__30_": 225, + "3E__52_": 226, + "4A___": 227, + "4A__F_": 228, + "4A__33_": 229, + "4A__11_": 230, + "4A__35_": 231, + "4A__36_": 232, + "4A__D_": 233, + "4A__12_": 234, + "4A__34_": 235, + "4A__10_": 236, + "B7___": 237, + "B7__B0_": 238, + "B7__BF_": 239, + "B7__AF_": 240 + } + } + } +} diff --git a/tests/components/homekit/fixtures/iids_v2_with_underscore b/tests/components/homekit/fixtures/iids_v2_with_underscore new file mode 100644 index 00000000000..52e874e41a5 --- /dev/null +++ b/tests/components/homekit/fixtures/iids_v2_with_underscore @@ -0,0 +1,54 @@ +{ + "version": 2, + "minor_version": 1, + "key": "homekit.8a47205bd97c07d7a908f10166ebe636.iids", + "data": { + "allocations": { + "1": { + "3E___": 1, + "3E__14_": 2, + "3E__20_": 3, + "3E__21_": 4, + "3E__23_": 5, + "3E__30_": 6, + "3E__52_": 7, + "A2___": 8, + "A2__37_": 9 + }, + "1973560704": { + "3E___": 1, + "3E__14_": 11, + "3E__20_": 12, + "3E__21_": 13, + "3E__23_": 14, + "3E__30_": 15, + "3E__52_": 16, + "3E__53_": 17, + "89_pressed-__": 18, + "89_pressed-_73_": 19, + "89_pressed-_23_": 20, + "89_pressed-_CB_": 21, + "CC_pressed-__": 22, + "CC_pressed-_CD_": 23, + "89_changed_states-__": 24, + "89_changed_states-_73_": 25, + "89_changed_states-_23_": 26, + "89_changed_states-_CB_": 27, + "CC_changed_states-__": 28, + "CC_changed_states-_CD_": 29, + "89_turned_off-__": 30, + "89_turned_off-_73_": 31, + "89_turned_off-_23_": 32, + "89_turned_off-_CB_": 33, + "CC_turned_off-__": 34, + "CC_turned_off-_CD_": 35, + "89_turned_on-__": 36, + "89_turned_on-_73_": 37, + "89_turned_on-_23_": 38, + "89_turned_on-_CB_": 39, + "CC_turned_on-__": 40, + "CC_turned_on-_CD_": 41 + } + } + } +} \ No newline at end of file diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 2a0f3f2f718..36eaeff91b4 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -10,7 +10,6 @@ from homeassistant.components.homekit.accessories import ( HomeAccessory, HomeBridge, HomeDriver, - HomeIIDManager, ) from homeassistant.components.homekit.const import ( ATTR_DISPLAY_NAME, @@ -724,7 +723,7 @@ def test_home_driver(iid_storage): "entry_id", "name", "title", - iid_manager=HomeIIDManager(iid_storage), + iid_storage=iid_storage, address=ip_address, port=port, persist_file=path, @@ -752,22 +751,3 @@ def test_home_driver(iid_storage): mock_unpair.assert_called_with("client_uuid") mock_show_msg.assert_called_with("hass", "entry_id", "title (any)", pin, "X-HM://0") - - -async def test_iid_collision_raises(hass, hk_driver): - """Test iid collision raises. - - If we try to allocate the same IID to the an accessory twice, we should - raise an exception. - """ - - entity_id = "light.accessory" - entity_id2 = "light.accessory2" - - hass.states.async_set(entity_id, STATE_OFF) - hass.states.async_set(entity_id2, STATE_OFF) - - HomeAccessory(hass, hk_driver, "Home Accessory", entity_id, 2, {}) - - with pytest.raises(RuntimeError): - HomeAccessory(hass, hk_driver, "Home Accessory", entity_id2, 2, {}) diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py index 1f6f7c584f3..be98c3bacdd 100644 --- a/tests/components/homekit/test_diagnostics.py +++ b/tests/components/homekit/test_diagnostics.py @@ -43,6 +43,19 @@ async def test_config_entry_running(hass, hass_client, hk_driver, mock_async_zer diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) assert diag == { "bridge": {}, + "iid_storage": { + "1": { + "3E__14_": 2, + "3E__20_": 3, + "3E__21_": 4, + "3E__23_": 5, + "3E__30_": 6, + "3E__52_": 7, + "3E___": 1, + "A2__37_": 9, + "A2___": 8, + } + }, "accessories": [ { "aid": 1, @@ -257,6 +270,21 @@ async def test_config_entry_accessory( }, "config_version": 2, "pairing_id": ANY, + "iid_storage": { + "1": { + "3E__14_": 2, + "3E__20_": 3, + "3E__21_": 4, + "3E__23_": 5, + "3E__30_": 6, + "3E__52_": 7, + "3E___": 1, + "43__25_": 11, + "43___": 10, + "A2__37_": 9, + "A2___": 8, + } + }, "status": 1, } with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 21dc94a4b54..d8c02aa98c4 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -262,7 +262,7 @@ async def test_homekit_setup(hass, hk_driver, mock_async_zeroconf): async_zeroconf_instance=zeroconf_mock, zeroconf_server=f"{uuid}-hap.local.", loader=ANY, - iid_manager=ANY, + iid_storage=ANY, ) assert homekit.driver.safe_mode is False @@ -306,7 +306,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_async_zeroconf): async_zeroconf_instance=mock_async_zeroconf, zeroconf_server=f"{uuid}-hap.local.", loader=ANY, - iid_manager=ANY, + iid_storage=ANY, ) @@ -350,7 +350,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_async_zeroconf): async_zeroconf_instance=async_zeroconf_instance, zeroconf_server=f"{uuid}-hap.local.", loader=ANY, - iid_manager=ANY, + iid_storage=ANY, ) diff --git a/tests/components/homekit/test_iidmanager.py b/tests/components/homekit/test_iidmanager.py index a791c30a341..3e4a19c9045 100644 --- a/tests/components/homekit/test_iidmanager.py +++ b/tests/components/homekit/test_iidmanager.py @@ -1,6 +1,5 @@ """Tests for the HomeKit IID manager.""" - from uuid import UUID from homeassistant.components.homekit.const import DOMAIN @@ -8,9 +7,10 @@ from homeassistant.components.homekit.iidmanager import ( AccessoryIIDStorage, get_iid_storage_filename_for_entry_id, ) +from homeassistant.helpers.json import json_loads from homeassistant.util.uuid import random_uuid_hex -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture async def test_iid_generation_and_restore(hass, iid_storage, hass_storage): @@ -77,9 +77,6 @@ async def test_iid_generation_and_restore(hass, iid_storage, hass_storage): unique_service_unique_char_new_aid_iid1 == unique_service_unique_char_new_aid_iid2 ) - assert unique_service_unique_char_new_aid_iid1 != iid1 - assert unique_service_unique_char_new_aid_iid1 != unique_service_unique_char_iid1 - await iid_storage.async_save() iid_storage2 = AccessoryIIDStorage(hass, entry.entry_id) @@ -99,3 +96,79 @@ async def test_iid_storage_filename(hass, iid_storage, hass_storage): assert iid_storage.store.path.endswith( get_iid_storage_filename_for_entry_id(entry.entry_id) ) + + +async def test_iid_migration_to_v2(hass, iid_storage, hass_storage): + """Test iid storage migration.""" + v1_iids = json_loads(load_fixture("iids_v1", DOMAIN)) + v2_iids = json_loads(load_fixture("iids_v2", DOMAIN)) + hass_storage["homekit.v1.iids"] = v1_iids + hass_storage["homekit.v2.iids"] = v2_iids + + iid_storage_v2 = AccessoryIIDStorage(hass, "v1") + await iid_storage_v2.async_initialize() + + iid_storage_v1 = AccessoryIIDStorage(hass, "v2") + await iid_storage_v1.async_initialize() + + assert iid_storage_v1.allocations == iid_storage_v2.allocations + assert iid_storage_v1.allocated_iids == iid_storage_v2.allocated_iids + + assert len(iid_storage_v2.allocations) == 12 + + for allocations in iid_storage_v2.allocations.values(): + assert allocations["3E___"] == 1 + + +async def test_iid_migration_to_v2_with_underscore(hass, iid_storage, hass_storage): + """Test iid storage migration with underscore.""" + v1_iids = json_loads(load_fixture("iids_v1_with_underscore", DOMAIN)) + v2_iids = json_loads(load_fixture("iids_v2_with_underscore", DOMAIN)) + hass_storage["homekit.v1_with_underscore.iids"] = v1_iids + hass_storage["homekit.v2_with_underscore.iids"] = v2_iids + + iid_storage_v2 = AccessoryIIDStorage(hass, "v1_with_underscore") + await iid_storage_v2.async_initialize() + + iid_storage_v1 = AccessoryIIDStorage(hass, "v2_with_underscore") + await iid_storage_v1.async_initialize() + + assert iid_storage_v1.allocations == iid_storage_v2.allocations + assert iid_storage_v1.allocated_iids == iid_storage_v2.allocated_iids + + assert len(iid_storage_v2.allocations) == 2 + + for allocations in iid_storage_v2.allocations.values(): + assert allocations["3E___"] == 1 + + +async def test_iid_generation_and_restore_v2(hass, iid_storage, hass_storage): + """Test generating iids and restoring them from storage.""" + entry = MockConfigEntry(domain=DOMAIN) + + iid_storage = AccessoryIIDStorage(hass, entry.entry_id) + await iid_storage.async_initialize() + not_accessory_info_service_iid = iid_storage.get_or_allocate_iid( + 1, "000000AA-0000-1000-8000-0026BB765291", None, None, None + ) + assert not_accessory_info_service_iid == 2 + not_accessory_info_service_iid_2 = iid_storage.get_or_allocate_iid( + 1, "000000BB-0000-1000-8000-0026BB765291", None, None, None + ) + assert not_accessory_info_service_iid_2 == 3 + not_accessory_info_service_iid_2 = iid_storage.get_or_allocate_iid( + 1, "000000BB-0000-1000-8000-0026BB765291", None, None, None + ) + assert not_accessory_info_service_iid_2 == 3 + accessory_info_service_iid = iid_storage.get_or_allocate_iid( + 1, "0000003E-0000-1000-8000-0026BB765291", None, None, None + ) + assert accessory_info_service_iid == 1 + accessory_info_service_iid = iid_storage.get_or_allocate_iid( + 1, "0000003E-0000-1000-8000-0026BB765291", None, None, None + ) + assert accessory_info_service_iid == 1 + accessory_info_service_iid = iid_storage.get_or_allocate_iid( + 2, "0000003E-0000-1000-8000-0026BB765291", None, None, None + ) + assert accessory_info_service_iid == 1 From c60c99bd74821f61114730e5c3e0aebb1f410865 Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Sat, 5 Nov 2022 13:29:20 -0700 Subject: [PATCH 033/114] Bump venstarcolortouch to 0.19 to fix API rev 3 devices (#81614) --- homeassistant/components/venstar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index 4a6eea28e24..ce40e53105a 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -3,7 +3,7 @@ "name": "Venstar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": ["venstarcolortouch==0.18"], + "requirements": ["venstarcolortouch==0.19"], "codeowners": ["@garbled1"], "iot_class": "local_polling", "loggers": ["venstarcolortouch"] diff --git a/requirements_all.txt b/requirements_all.txt index 3c44a05c335..1d52708eacb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2487,7 +2487,7 @@ vehicle==0.4.0 velbus-aio==2022.10.4 # homeassistant.components.venstar -venstarcolortouch==0.18 +venstarcolortouch==0.19 # homeassistant.components.vilfo vilfo-api-client==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40828d4f161..b0955ae7b62 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1721,7 +1721,7 @@ vehicle==0.4.0 velbus-aio==2022.10.4 # homeassistant.components.venstar -venstarcolortouch==0.18 +venstarcolortouch==0.19 # homeassistant.components.vilfo vilfo-api-client==0.3.2 From 4391640734df8d8825b0d84bc2a19af33b38f9dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Nov 2022 15:50:45 -0600 Subject: [PATCH 034/114] Ignore unspecified addresses from zeroconf (#81620) --- homeassistant/components/zeroconf/__init__.py | 12 ++++++++++-- tests/components/zeroconf/test_init.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 62783e641d3..82a9604a08c 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -552,12 +552,20 @@ def _first_non_link_local_address( """Return the first ipv6 or non-link local ipv4 address, preferring IPv4.""" for address in addresses: ip_addr = ip_address(address) - if not ip_addr.is_link_local and ip_addr.version == 4: + if ( + not ip_addr.is_link_local + and not ip_addr.is_unspecified + and ip_addr.version == 4 + ): return str(ip_addr) # If we didn't find a good IPv4 address, check for IPv6 addresses. for address in addresses: ip_addr = ip_address(address) - if not ip_addr.is_link_local and ip_addr.version == 6: + if ( + not ip_addr.is_link_local + and not ip_addr.is_unspecified + and ip_addr.version == 6 + ): return str(ip_addr) return None diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 039672b9955..a0684d6e10e 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -819,6 +819,24 @@ async def test_info_from_service_with_link_local_address_first(hass): assert info.host == "192.168.66.12" +async def test_info_from_service_with_unspecified_address_first(hass): + """Test that the unspecified address is ignored.""" + service_type = "_test._tcp.local." + service_info = get_service_info_mock(service_type, f"test.{service_type}") + service_info.addresses = ["0.0.0.0", "192.168.66.12"] + info = zeroconf.info_from_service(service_info) + assert info.host == "192.168.66.12" + + +async def test_info_from_service_with_unspecified_address_only(hass): + """Test that the unspecified address is ignored.""" + service_type = "_test._tcp.local." + service_info = get_service_info_mock(service_type, f"test.{service_type}") + service_info.addresses = ["0.0.0.0"] + info = zeroconf.info_from_service(service_info) + assert info is None + + async def test_info_from_service_with_link_local_address_second(hass): """Test that the link local address is ignored.""" service_type = "_test._tcp.local." From 6fa69022f4fe3e6eb85f091636484a11d2c7feac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 15:58:11 -0500 Subject: [PATCH 035/114] Bump aiohomekit to 2.2.16 (#81621) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index b2aec75c3ad..e4a2b5d79bb 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.14"], + "requirements": ["aiohomekit==2.2.16"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 1d52708eacb..f929c7f2d22 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.14 +aiohomekit==2.2.16 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0955ae7b62..e106bfe0331 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.14 +aiohomekit==2.2.16 # homeassistant.components.emulated_hue # homeassistant.components.http From 6fb5c93182f34b9a207c93e10e48d1c02bcd0e9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 05:38:01 -0600 Subject: [PATCH 036/114] Bump oralb-ble to 0.13.0 (#81622) * Bump oralb-ble to 0.11.1 adds some more missing pressure mappings changelog: https://github.com/Bluetooth-Devices/oralb-ble/compare/v0.10.2...v0.11.1 * bump again to update for additional reports * bump again for more data from issue reports --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index ba89c73a240..1738558770e 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.10.2"], + "requirements": ["oralb-ble==0.13.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index f929c7f2d22..b58f8a61743 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1238,7 +1238,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.10.2 +oralb-ble==0.13.0 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e106bfe0331..8d8863832e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -883,7 +883,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.10.2 +oralb-ble==0.13.0 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From e4269ff8b20c942c0000c584495780f50ce0a76f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Nov 2022 05:09:36 -0600 Subject: [PATCH 037/114] Fix creating multiple ElkM1 systems with TLS 1.2 (#81627) fixes https://github.com/home-assistant/core/issues/81516 --- homeassistant/components/elkm1/__init__.py | 9 +- homeassistant/components/elkm1/config_flow.py | 13 +- tests/components/elkm1/test_config_flow.py | 146 ++++++++++++++++++ 3 files changed, 159 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 7833bfd66b3..7b14c7e85ed 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -7,11 +7,11 @@ import logging import re from types import MappingProxyType from typing import Any, cast -from urllib.parse import urlparse import async_timeout from elkm1_lib.elements import Element from elkm1_lib.elk import Elk +from elkm1_lib.util import parse_url import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -96,6 +96,11 @@ SET_TIME_SERVICE_SCHEMA = vol.Schema( ) +def hostname_from_url(url: str) -> str: + """Return the hostname from a url.""" + return parse_url(url)[1] + + def _host_validator(config: dict[str, str]) -> dict[str, str]: """Validate that a host is properly configured.""" if config[CONF_HOST].startswith("elks://"): @@ -231,7 +236,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elk-M1 Control from a config entry.""" conf: MappingProxyType[str, Any] = entry.data - host = urlparse(entry.data[CONF_HOST]).hostname + host = hostname_from_url(entry.data[CONF_HOST]) _LOGGER.debug("Setting up elkm1 %s", conf["host"]) diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index 8675ff45ee7..ac7fc903330 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio import logging from typing import Any -from urllib.parse import urlparse from elkm1_lib.discovery import ElkSystem from elkm1_lib.elk import Elk @@ -26,7 +25,7 @@ from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.util import slugify from homeassistant.util.network import is_ip_address -from . import async_wait_for_elk_to_sync +from . import async_wait_for_elk_to_sync, hostname_from_url from .const import CONF_AUTO_CONFIGURE, DISCOVER_SCAN_TIMEOUT, DOMAIN, LOGIN_TIMEOUT from .discovery import ( _short_mac, @@ -170,7 +169,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): for entry in self._async_current_entries(include_ignore=False): if ( entry.unique_id == mac - or urlparse(entry.data[CONF_HOST]).hostname == host + or hostname_from_url(entry.data[CONF_HOST]) == host ): if async_update_entry_from_discovery(self.hass, entry, device): self.hass.async_create_task( @@ -214,7 +213,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): current_unique_ids = self._async_current_ids() current_hosts = { - urlparse(entry.data[CONF_HOST]).hostname + hostname_from_url(entry.data[CONF_HOST]) for entry in self._async_current_entries(include_ignore=False) } discovered_devices = await async_discover_devices( @@ -344,7 +343,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self._url_already_configured(url): return self.async_abort(reason="address_already_configured") - host = urlparse(url).hostname + host = hostname_from_url(url) _LOGGER.debug( "Importing is trying to fill unique id from discovery for %s", host ) @@ -367,10 +366,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def _url_already_configured(self, url: str) -> bool: """See if we already have a elkm1 matching user input configured.""" existing_hosts = { - urlparse(entry.data[CONF_HOST]).hostname + hostname_from_url(entry.data[CONF_HOST]) for entry in self._async_current_entries() } - return urlparse(url).hostname in existing_hosts + return hostname_from_url(url) in existing_hosts class InvalidAuth(exceptions.HomeAssistantError): diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 7ce0e2163ac..a58528b700a 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -1454,3 +1454,149 @@ async def test_multiple_instances_with_discovery(hass): "password": "", } assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_multiple_instances_with_tls_v12(hass): + """Test we can setup a secure elk with tls v1_2.""" + + elk_discovery_1 = ElkSystem("aa:bb:cc:dd:ee:ff", "127.0.0.1", 2601) + elk_discovery_2 = ElkSystem("aa:bb:cc:dd:ee:fe", "127.0.0.2", 2601) + + with _patch_discovery(device=elk_discovery_1): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert not result["errors"] + assert result["step_id"] == "user" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_elk(elk=mocked_elk): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device": elk_discovery_1.mac_address}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert not result["errors"] + assert result2["step_id"] == "discovered_connection" + with _patch_discovery(device=elk_discovery_1), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "protocol": "TLS 1.2", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "ElkM1 ddeeff" + assert result3["data"] == { + "auto_configure": True, + "host": "elksv1_2://127.0.0.1", + "password": "test-password", + "prefix": "", + "username": "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + # Now try to add another instance with the different discovery info + with _patch_discovery(device=elk_discovery_2): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert not result["errors"] + assert result["step_id"] == "user" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_elk(elk=mocked_elk): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device": elk_discovery_2.mac_address}, + ) + await hass.async_block_till_done() + + with _patch_discovery(device=elk_discovery_2), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "protocol": "TLS 1.2", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "ElkM1 ddeefe" + assert result3["data"] == { + "auto_configure": True, + "host": "elksv1_2://127.0.0.2", + "password": "test-password", + "prefix": "ddeefe", + "username": "test-username", + } + assert len(mock_setup_entry.mock_calls) == 1 + + # Finally, try to add another instance manually with no discovery info + + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "manual_connection" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "TLS 1.2", + "address": "1.2.3.4", + "prefix": "guest_house", + "password": "test-password", + "username": "test-username", + }, + ) + await hass.async_block_till_done() + + import pprint + + pprint.pprint(result2) + assert result2["type"] == "create_entry" + assert result2["title"] == "guest_house" + assert result2["data"] == { + "auto_configure": True, + "host": "elksv1_2://1.2.3.4", + "prefix": "guest_house", + "password": "test-password", + "username": "test-username", + } + assert len(mock_setup_entry.mock_calls) == 1 From c8981f78b7abc0cba17700712b55dbf88a19bffc Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 5 Nov 2022 21:23:36 +0100 Subject: [PATCH 038/114] Fix situation where deCONZ sensor platform setup would fail (#81629) * Fix situation where deCONZ sensor platform setup would fail * Don't use try --- homeassistant/components/deconz/sensor.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 66c186e20d7..f1bd0118030 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -89,6 +89,7 @@ T = TypeVar( class DeconzSensorDescriptionMixin(Generic[T]): """Required values when describing secondary sensor attributes.""" + supported_fn: Callable[[T], bool] update_key: str value_fn: Callable[[T], datetime | StateType] @@ -105,6 +106,7 @@ class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMi ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( DeconzSensorDescription[AirQuality]( key="air_quality", + supported_fn=lambda device: device.air_quality is not None, update_key="airquality", value_fn=lambda device: device.air_quality, instance_check=AirQuality, @@ -112,6 +114,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[AirQuality]( key="air_quality_ppb", + supported_fn=lambda device: device.air_quality_ppb is not None, update_key="airqualityppb", value_fn=lambda device: device.air_quality_ppb, instance_check=AirQuality, @@ -122,6 +125,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[Consumption]( key="consumption", + supported_fn=lambda device: device.consumption is not None, update_key="consumption", value_fn=lambda device: device.scaled_consumption, instance_check=Consumption, @@ -131,6 +135,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[Daylight]( key="daylight_status", + supported_fn=lambda device: True, update_key="status", value_fn=lambda device: DAYLIGHT_STATUS[device.daylight_status], instance_check=Daylight, @@ -139,12 +144,14 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[GenericStatus]( key="status", + supported_fn=lambda device: device.status is not None, update_key="status", value_fn=lambda device: device.status, instance_check=GenericStatus, ), DeconzSensorDescription[Humidity]( key="humidity", + supported_fn=lambda device: device.humidity is not None, update_key="humidity", value_fn=lambda device: device.scaled_humidity, instance_check=Humidity, @@ -154,6 +161,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[LightLevel]( key="light_level", + supported_fn=lambda device: device.light_level is not None, update_key="lightlevel", value_fn=lambda device: device.scaled_light_level, instance_check=LightLevel, @@ -163,6 +171,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[Power]( key="power", + supported_fn=lambda device: device.power is not None, update_key="power", value_fn=lambda device: device.power, instance_check=Power, @@ -172,6 +181,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[Pressure]( key="pressure", + supported_fn=lambda device: device.pressure is not None, update_key="pressure", value_fn=lambda device: device.pressure, instance_check=Pressure, @@ -181,6 +191,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[Temperature]( key="temperature", + supported_fn=lambda device: device.temperature is not None, update_key="temperature", value_fn=lambda device: device.scaled_temperature, instance_check=Temperature, @@ -190,6 +201,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[Time]( key="last_set", + supported_fn=lambda device: device.last_set is not None, update_key="lastset", value_fn=lambda device: dt_util.parse_datetime(device.last_set), instance_check=Time, @@ -197,6 +209,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[SensorResources]( key="battery", + supported_fn=lambda device: device.battery is not None, update_key="battery", value_fn=lambda device: device.battery, name_suffix="Battery", @@ -208,6 +221,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ), DeconzSensorDescription[SensorResources]( key="internal_temperature", + supported_fn=lambda device: device.internal_temperature is not None, update_key="temperature", value_fn=lambda device: device.internal_temperature, name_suffix="Temperature", @@ -268,7 +282,7 @@ async def async_setup_entry( continue no_sensor_data = False - if description.value_fn(sensor) is None: + if not description.supported_fn(sensor): no_sensor_data = True if description.instance_check is None: From 08debee94fd6052e15b7ed57476cc644172e6ec4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 12:59:32 -0600 Subject: [PATCH 039/114] Add missing h2 dep to iaqualink (#81630) fixes https://github.com/home-assistant/core/issues/81439 --- homeassistant/components/iaqualink/manifest.json | 2 +- requirements_all.txt | 3 +++ requirements_test_all.txt | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index d5b7d7de0d8..f274cd5ea1c 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iaqualink/", "codeowners": ["@flz"], - "requirements": ["iaqualink==0.5.0"], + "requirements": ["iaqualink==0.5.0", "h2==4.1.0"], "iot_class": "cloud_polling", "loggers": ["iaqualink"] } diff --git a/requirements_all.txt b/requirements_all.txt index b58f8a61743..dcefb2ba7fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -815,6 +815,9 @@ gstreamer-player==1.1.2 # homeassistant.components.profiler guppy3==3.1.2 +# homeassistant.components.iaqualink +h2==4.1.0 + # homeassistant.components.homekit ha-HAP-python==4.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d8863832e5..d46e2ae375a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -607,6 +607,9 @@ gspread==5.5.0 # homeassistant.components.profiler guppy3==3.1.2 +# homeassistant.components.iaqualink +h2==4.1.0 + # homeassistant.components.homekit ha-HAP-python==4.5.2 From f861137de4700fec31274750a059619151540faa Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 6 Nov 2022 10:21:48 +0100 Subject: [PATCH 040/114] Bump pyatmo to 7.4.0 (#81636) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index e34156ff589..0d4681197bd 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -2,7 +2,7 @@ "domain": "netatmo", "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==7.3.0"], + "requirements": ["pyatmo==7.4.0"], "after_dependencies": ["cloud", "media_source"], "dependencies": ["application_credentials", "webhook"], "codeowners": ["@cgtobi"], diff --git a/requirements_all.txt b/requirements_all.txt index dcefb2ba7fd..f4f4df8e8a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1445,7 +1445,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.3.0 +pyatmo==7.4.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d46e2ae375a..8a647e1cae6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1033,7 +1033,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.3.0 +pyatmo==7.4.0 # homeassistant.components.apple_tv pyatv==0.10.3 From 63afb30f57ff24072c48d1f6fb55cd4bc95f7e57 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Sun, 6 Nov 2022 23:26:40 +0300 Subject: [PATCH 041/114] Fix Bravia TV options flow when device is off (#81644) * Fix options flow when tv is off * abort with message --- homeassistant/components/braviatv/config_flow.py | 6 +++++- homeassistant/components/braviatv/strings.json | 3 +++ homeassistant/components/braviatv/translations/en.json | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index f5c7826b825..45a2bad0036 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -262,7 +262,11 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow): self.config_entry.entry_id ] - await coordinator.async_update_sources() + try: + await coordinator.async_update_sources() + except BraviaTVError: + return self.async_abort(reason="failed_update") + sources = coordinator.source_map.values() self.source_list = [item["title"] for item in sources] return await self.async_step_user() diff --git a/homeassistant/components/braviatv/strings.json b/homeassistant/components/braviatv/strings.json index 4dd08135896..ac651955166 100644 --- a/homeassistant/components/braviatv/strings.json +++ b/homeassistant/components/braviatv/strings.json @@ -48,6 +48,9 @@ "ignored_sources": "List of ignored sources" } } + }, + "abort": { + "failed_update": "An error occurred while updating the list of sources.\n\n Ensure that your TV is turned on before trying to set it up." } } } diff --git a/homeassistant/components/braviatv/translations/en.json b/homeassistant/components/braviatv/translations/en.json index c3341d33112..39c95c706de 100644 --- a/homeassistant/components/braviatv/translations/en.json +++ b/homeassistant/components/braviatv/translations/en.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "An error occurred while updating the list of sources.\n\n Ensure that your TV is turned on before trying to set it up." + }, "step": { "user": { "data": { From 2684a6e8eda6df4da469ae405da3f79ffba1a35a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 09:47:07 -0600 Subject: [PATCH 042/114] Bump aiohomekit to 2.2.17 (#81657) Improve BLE pairing reliability, especially with esp32 proxies changelog: https://github.com/Jc2k/aiohomekit/compare/2.2.16...2.2.17 --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index e4a2b5d79bb..7a088993e9f 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.16"], + "requirements": ["aiohomekit==2.2.17"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index f4f4df8e8a5..462faa1d813 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.16 +aiohomekit==2.2.17 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a647e1cae6..efe5a96b00d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.16 +aiohomekit==2.2.17 # homeassistant.components.emulated_hue # homeassistant.components.http From 11013bd78090ebc1beca17fd7f88934e6f3f7cd5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 6 Nov 2022 12:38:55 -0700 Subject: [PATCH 043/114] Fix missing RainMachine restrictions switches (#81673) --- homeassistant/components/rainmachine/switch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index db560c3c64c..39087f36660 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -237,6 +237,7 @@ async def async_setup_entry( # Add switches to control restrictions: for description in RESTRICTIONS_SWITCH_DESCRIPTIONS: + coordinator = data.coordinators[description.api_category] if not key_exists(coordinator.data, description.data_key): continue entities.append(RainMachineRestrictionSwitch(entry, data, description)) From a8a3f012f66e2a61928c33d63342ed1a0adc3bdd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 04:44:05 -0500 Subject: [PATCH 044/114] Bump bluetooth-adapters to 0.7.0 (#81576) changelog: https://github.com/Bluetooth-Devices/bluetooth-adapters/releases/tag/v0.7.0 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 89281323541..93aabb3cc26 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -8,7 +8,7 @@ "requirements": [ "bleak==0.19.1", "bleak-retry-connector==2.8.2", - "bluetooth-adapters==0.6.0", + "bluetooth-adapters==0.7.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.61.1" ], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cabe62b50ec..f57a2affd6a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ awesomeversion==22.9.0 bcrypt==3.1.7 bleak-retry-connector==2.8.2 bleak==0.19.1 -bluetooth-adapters==0.6.0 +bluetooth-adapters==0.7.0 bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 462faa1d813..48437186a5f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -438,7 +438,7 @@ bluemaestro-ble==0.2.0 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.6.0 +bluetooth-adapters==0.7.0 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index efe5a96b00d..96d2ed5a20b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -352,7 +352,7 @@ blinkpy==0.19.2 bluemaestro-ble==0.2.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.6.0 +bluetooth-adapters==0.7.0 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.6 From c8ed3fd302cd8e544b0382256861f3484c8e512e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 17:45:53 -0600 Subject: [PATCH 045/114] Bump bleak-retry-connector to 2.8.3 (#81675) Improves chances of making a BLE connection with the ESP32s changelog: https://github.com/Bluetooth-Devices/bleak-retry-connector/compare/v2.8.2...v2.8.3 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 93aabb3cc26..0569bc68e87 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.1", - "bleak-retry-connector==2.8.2", + "bleak-retry-connector==2.8.3", "bluetooth-adapters==0.7.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.61.1" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f57a2affd6a..b25e684fe8c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.8.2 +bleak-retry-connector==2.8.3 bleak==0.19.1 bluetooth-adapters==0.7.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index 48437186a5f..2b712cf48c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.2 +bleak-retry-connector==2.8.3 # homeassistant.components.bluetooth bleak==0.19.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 96d2ed5a20b..16e43e8fc24 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.2 +bleak-retry-connector==2.8.3 # homeassistant.components.bluetooth bleak==0.19.1 From 7ca5bd341bed081c4b6e4c7db8200e78e5a8d685 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 17:13:14 -0600 Subject: [PATCH 046/114] Bump aioesphomeapi to 11.4.3 (#81676) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 64cd6b4029c..2070b8ae362 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==11.4.2"], + "requirements": ["aioesphomeapi==11.4.3"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index 2b712cf48c2..ae8bd698262 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -153,7 +153,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.2 +aioesphomeapi==11.4.3 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 16e43e8fc24..2819bfcbb6b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.2 +aioesphomeapi==11.4.3 # homeassistant.components.flo aioflo==2021.11.0 From efb984aa83c18f881b0e03c4ae84573c46cf7f24 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 19:07:21 -0600 Subject: [PATCH 047/114] Bump bleak to 0.19.2 (#81688) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 0569bc68e87..2e038cda76e 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "after_dependencies": ["hassio"], "quality_scale": "internal", "requirements": [ - "bleak==0.19.1", + "bleak==0.19.2", "bleak-retry-connector==2.8.3", "bluetooth-adapters==0.7.0", "bluetooth-auto-recovery==0.3.6", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b25e684fe8c..ebf4eff0d2b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 bleak-retry-connector==2.8.3 -bleak==0.19.1 +bleak==0.19.2 bluetooth-adapters==0.7.0 bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 diff --git a/requirements_all.txt b/requirements_all.txt index ae8bd698262..b77191f095f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -416,7 +416,7 @@ bizkaibus==0.1.1 bleak-retry-connector==2.8.3 # homeassistant.components.bluetooth -bleak==0.19.1 +bleak==0.19.2 # homeassistant.components.blebox blebox_uniapi==2.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2819bfcbb6b..8bcae988264 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -340,7 +340,7 @@ bimmer_connected==0.10.4 bleak-retry-connector==2.8.3 # homeassistant.components.bluetooth -bleak==0.19.1 +bleak==0.19.2 # homeassistant.components.blebox blebox_uniapi==2.1.3 From 34ae83b4e2714b1730b52ef473d8ae32d93a3233 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 7 Nov 2022 01:16:58 +0100 Subject: [PATCH 048/114] Restore negative values for shelly power factors (#81689) fixes undefined --- homeassistant/components/shelly/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 922a5ac8b5c..7d99f015c5e 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -140,7 +140,7 @@ SENSORS: Final = { key="emeter|powerFactor", name="Power Factor", native_unit_of_measurement=PERCENTAGE, - value=lambda value: abs(round(value * 100, 1)), + value=lambda value: round(value * 100, 1), device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, ), From 823ec88c520b0b549bd6c980721d628a4b07b9a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 18:04:45 -0600 Subject: [PATCH 049/114] Bump aiohomekit to 2.2.18 (#81693) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 7a088993e9f..b18f35390b7 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.17"], + "requirements": ["aiohomekit==2.2.18"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index b77191f095f..c95d3258fcf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.17 +aiohomekit==2.2.18 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8bcae988264..487595d4e07 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.17 +aiohomekit==2.2.18 # homeassistant.components.emulated_hue # homeassistant.components.http From b9db84ed57948402b5c222865989f6619621a693 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 7 Nov 2022 06:23:49 -0500 Subject: [PATCH 050/114] Bump aiopyarr to 22.11.0 (#81694) --- homeassistant/components/lidarr/manifest.json | 2 +- homeassistant/components/radarr/manifest.json | 2 +- homeassistant/components/sonarr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lidarr/manifest.json b/homeassistant/components/lidarr/manifest.json index 4c07e0e1762..eab24ef7e42 100644 --- a/homeassistant/components/lidarr/manifest.json +++ b/homeassistant/components/lidarr/manifest.json @@ -2,7 +2,7 @@ "domain": "lidarr", "name": "Lidarr", "documentation": "https://www.home-assistant.io/integrations/lidarr", - "requirements": ["aiopyarr==22.10.0"], + "requirements": ["aiopyarr==22.11.0"], "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/radarr/manifest.json b/homeassistant/components/radarr/manifest.json index 9b140def96a..5117fd161d3 100644 --- a/homeassistant/components/radarr/manifest.json +++ b/homeassistant/components/radarr/manifest.json @@ -2,7 +2,7 @@ "domain": "radarr", "name": "Radarr", "documentation": "https://www.home-assistant.io/integrations/radarr", - "requirements": ["aiopyarr==22.10.0"], + "requirements": ["aiopyarr==22.11.0"], "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json index daf9e20586b..511842a3e99 100644 --- a/homeassistant/components/sonarr/manifest.json +++ b/homeassistant/components/sonarr/manifest.json @@ -3,7 +3,7 @@ "name": "Sonarr", "documentation": "https://www.home-assistant.io/integrations/sonarr", "codeowners": ["@ctalkington"], - "requirements": ["aiopyarr==22.10.0"], + "requirements": ["aiopyarr==22.11.0"], "config_flow": true, "quality_scale": "silver", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index c95d3258fcf..a9d1c292018 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -237,7 +237,7 @@ aiopvpc==3.0.0 # homeassistant.components.lidarr # homeassistant.components.radarr # homeassistant.components.sonarr -aiopyarr==22.10.0 +aiopyarr==22.11.0 # homeassistant.components.qnap_qsw aioqsw==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 487595d4e07..7d3fa0e0698 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aiopvpc==3.0.0 # homeassistant.components.lidarr # homeassistant.components.radarr # homeassistant.components.sonarr -aiopyarr==22.10.0 +aiopyarr==22.11.0 # homeassistant.components.qnap_qsw aioqsw==0.2.2 From 3e8bea8fbd592aa766a14287d4404afe43bd08c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Nov 2022 04:41:02 -0600 Subject: [PATCH 051/114] Fix flapping logbook tests (#81695) --- .../components/logbook/test_websocket_api.py | 91 ++++++++++++++----- 1 file changed, 67 insertions(+), 24 deletions(-) diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index fb0defca93c..42f604b3d4c 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -24,6 +24,7 @@ from homeassistant.const import ( CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, + EVENT_HOMEASSISTANT_FINAL_WRITE, EVENT_HOMEASSISTANT_START, STATE_OFF, STATE_ON, @@ -52,6 +53,15 @@ def set_utc(hass): hass.config.set_time_zone("UTC") +def listeners_without_writes(listeners: dict[str, int]) -> dict[str, int]: + """Return listeners without final write listeners since we are not testing for these.""" + return { + key: value + for key, value in listeners.items() + if key != EVENT_HOMEASSISTANT_FINAL_WRITE + } + + async def _async_mock_logbook_platform(hass: HomeAssistant) -> None: class MockLogbookPlatform: """Mock a logbook platform.""" @@ -684,7 +694,9 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -892,7 +904,9 @@ async def test_subscribe_unsubscribe_logbook_stream_included_entities( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1083,7 +1097,9 @@ async def test_logbook_stream_excluded_entities_inherits_filters_from_recorder( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1386,7 +1402,9 @@ async def test_subscribe_unsubscribe_logbook_stream( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1484,7 +1502,9 @@ async def test_subscribe_unsubscribe_logbook_stream_entities( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1586,12 +1606,9 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_with_end_time( assert msg["success"] # Check our listener got unsubscribed - listeners = hass.bus.async_listeners() - # The async_fire_time_changed above triggers unsubscribe from - # homeassistant_final_write, don't worry about those - init_listeners.pop("homeassistant_final_write") - listeners.pop("homeassistant_final_write") - assert listeners == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1659,7 +1676,9 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_past_only( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1759,7 +1778,9 @@ async def test_subscribe_unsubscribe_logbook_stream_big_query( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1853,7 +1874,9 @@ async def test_subscribe_unsubscribe_logbook_stream_device( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) async def test_event_stream_bad_start_time(recorder_mock, hass, hass_ws_client): @@ -1968,7 +1991,9 @@ async def test_logbook_stream_match_multiple_entities( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) async def test_event_stream_bad_end_time(recorder_mock, hass, hass_ws_client): @@ -2091,7 +2116,9 @@ async def test_live_stream_with_one_second_commit_interval( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2146,7 +2173,9 @@ async def test_subscribe_disconnected(recorder_mock, hass, hass_ws_client): await hass.async_block_till_done() # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2189,7 +2218,9 @@ async def test_stream_consumer_stop_processing(recorder_mock, hass, hass_ws_clie assert msg["type"] == TYPE_RESULT assert msg["success"] - assert hass.bus.async_listeners() != init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) != listeners_without_writes(init_listeners) for _ in range(5): hass.states.async_set("binary_sensor.is_light", STATE_ON) hass.states.async_set("binary_sensor.is_light", STATE_OFF) @@ -2197,9 +2228,13 @@ async def test_stream_consumer_stop_processing(recorder_mock, hass, hass_ws_clie # Check our listener got unsubscribed because # the queue got full and the overload safety tripped - assert hass.bus.async_listeners() == after_ws_created_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(after_ws_created_listeners) await websocket_client.close() - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2332,7 +2367,9 @@ async def test_subscribe_all_entities_are_continuous( await hass.async_block_till_done() # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2494,7 +2531,9 @@ async def test_subscribe_entities_some_have_uom_multiple( await hass.async_block_till_done() # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2608,7 +2647,9 @@ async def test_logbook_stream_ignores_forced_updates( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2703,4 +2744,6 @@ async def test_subscribe_all_entities_are_continuous_with_device( await hass.async_block_till_done() # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) From 797ea3ace496ffa73f3085beaf9cdbf326b8a7a9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Nov 2022 11:18:58 +0100 Subject: [PATCH 052/114] Adjust REST schema validation (#81723) fixes undefined --- homeassistant/components/rest/schema.py | 9 ++++++++- tests/components/rest/test_init.py | 20 +++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index f881dc8b028..d124ce5789c 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -89,6 +89,13 @@ COMBINED_SCHEMA = vol.Schema( ) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.All(cv.ensure_list, [COMBINED_SCHEMA])}, + { + DOMAIN: vol.All( + # convert empty dict to empty list + lambda x: [] if x == {} else x, + cv.ensure_list, + [COMBINED_SCHEMA], + ) + }, extra=vol.ALLOW_EXTRA, ) diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index 988f88b348e..b6861a098b1 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -18,7 +18,11 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed, get_fixture_path +from tests.common import ( + assert_setup_component, + async_fire_time_changed, + get_fixture_path, +) @respx.mock @@ -399,3 +403,17 @@ async def test_multiple_rest_endpoints(hass): assert hass.states.get("sensor.json_date_time").state == "07:11:08 PM" assert hass.states.get("sensor.json_time").state == "07:11:39 PM" assert hass.states.get("binary_sensor.binary_sensor").state == "on" + + +async def test_empty_config(hass: HomeAssistant) -> None: + """Test setup with empty configuration. + + For example (with rest.yaml an empty file): + rest: !include rest.yaml + """ + assert await async_setup_component( + hass, + DOMAIN, + {DOMAIN: {}}, + ) + assert_setup_component(0, DOMAIN) From 1f878433ace40a44d627613cca15e70a95d803d2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Nov 2022 21:19:57 -0600 Subject: [PATCH 053/114] Fix check for duplicate config entry reauth when context is passed or augmented (#81753) fixes https://github.com/home-assistant/core/issues/77578 --- homeassistant/config_entries.py | 29 +++++++++++++++-------------- tests/test_config_entries.py | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 8e618deb3d2..b7ee9a4d654 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -660,24 +660,25 @@ class ConfigEntry: data: dict[str, Any] | None = None, ) -> None: """Start a reauth flow.""" - flow_context = { - "source": SOURCE_REAUTH, - "entry_id": self.entry_id, - "title_placeholders": {"name": self.title}, - "unique_id": self.unique_id, - } - - if context: - flow_context.update(context) - - for flow in hass.config_entries.flow.async_progress_by_handler(self.domain): - if flow["context"] == flow_context: - return + if any( + flow + for flow in hass.config_entries.flow.async_progress() + if flow["context"].get("source") == SOURCE_REAUTH + and flow["context"].get("entry_id") == self.entry_id + ): + # Reauth flow already in progress for this entry + return hass.async_create_task( hass.config_entries.flow.async_init( self.domain, - context=flow_context, + context={ + "source": SOURCE_REAUTH, + "entry_id": self.entry_id, + "title_placeholders": {"name": self.title}, + "unique_id": self.unique_id, + } + | (context or {}), data=self.data | (data or {}), ) ) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 83343146d47..99e26be6d75 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3287,6 +3287,7 @@ async def test_disallow_entry_reload_with_setup_in_progresss(hass, manager): async def test_reauth(hass): """Test the async_reauth_helper.""" entry = MockConfigEntry(title="test_title", domain="test") + entry2 = MockConfigEntry(title="test_title", domain="test") mock_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) @@ -3313,7 +3314,19 @@ async def test_reauth(hass): assert mock_init.call_args.kwargs["data"]["extra_data"] == 1234 + assert entry.entry_id != entry2.entry_id + # Check we can't start duplicate flows entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) await hass.async_block_till_done() - assert len(flows) == 1 + assert len(hass.config_entries.flow.async_progress()) == 1 + + # Check we can't start duplicate when the context context is different + entry.async_start_reauth(hass, {"diff": "diff"}) + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress()) == 1 + + # Check we can start a reauth for a different entry + entry2.async_start_reauth(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress()) == 2 From 815249eaeb03a3fda8cc8e35d14f31359581596d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Nov 2022 04:20:54 -0600 Subject: [PATCH 054/114] Use more efficient async_progress_by_handler call in async_start_reauth (#81757) --- homeassistant/config_entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b7ee9a4d654..bd985517ca7 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -662,7 +662,7 @@ class ConfigEntry: """Start a reauth flow.""" if any( flow - for flow in hass.config_entries.flow.async_progress() + for flow in hass.config_entries.flow.async_progress_by_handler(self.domain) if flow["context"].get("source") == SOURCE_REAUTH and flow["context"].get("entry_id") == self.entry_id ): From f614df29bd1abd170f85a745759f9222b541b338 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 8 Nov 2022 02:21:13 -0800 Subject: [PATCH 055/114] Partially revert google local sync for search cases (#81761) fixes undefined --- homeassistant/components/google/calendar.py | 154 +++++++++++++++----- 1 file changed, 118 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index ca1228759cd..4eb57cff49c 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -3,11 +3,12 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable from datetime import datetime, timedelta import logging from typing import Any -from gcal_sync.api import SyncEventsRequest +from gcal_sync.api import GoogleCalendarService, ListEventsRequest, SyncEventsRequest from gcal_sync.exceptions import ApiException from gcal_sync.model import DateOrDatetime, Event from gcal_sync.store import ScopedCalendarStore @@ -196,21 +197,30 @@ async def async_setup_entry( entity_registry.async_remove( entity_entry.entity_id, ) - request_template = SyncEventsRequest( - calendar_id=calendar_id, - search=data.get(CONF_SEARCH), - start_time=dt_util.now() + SYNC_EVENT_MIN_TIME, - ) - sync = CalendarEventSyncManager( - calendar_service, - store=ScopedCalendarStore(store, unique_id or entity_name), - request_template=request_template, - ) - coordinator = CalendarUpdateCoordinator( - hass, - sync, - data[CONF_NAME], - ) + coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator + if search := data.get(CONF_SEARCH): + coordinator = CalendarQueryUpdateCoordinator( + hass, + calendar_service, + data[CONF_NAME], + calendar_id, + search, + ) + else: + request_template = SyncEventsRequest( + calendar_id=calendar_id, + start_time=dt_util.now() + SYNC_EVENT_MIN_TIME, + ) + sync = CalendarEventSyncManager( + calendar_service, + store=ScopedCalendarStore(store, unique_id or entity_name), + request_template=request_template, + ) + coordinator = CalendarSyncUpdateCoordinator( + hass, + sync, + data[CONF_NAME], + ) entities.append( GoogleCalendarEntity( coordinator, @@ -242,8 +252,8 @@ async def async_setup_entry( ) -class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]): - """Coordinator for calendar RPC calls.""" +class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]): + """Coordinator for calendar RPC calls that use an efficient sync.""" def __init__( self, @@ -251,7 +261,7 @@ class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]): sync: CalendarEventSyncManager, name: str, ) -> None: - """Create the Calendar event device.""" + """Create the CalendarSyncUpdateCoordinator.""" super().__init__( hass, _LOGGER, @@ -271,6 +281,87 @@ class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]): dt_util.DEFAULT_TIME_ZONE ) + async def async_get_events( + self, start_date: datetime, end_date: datetime + ) -> Iterable[Event]: + """Get all events in a specific time frame.""" + if not self.data: + raise HomeAssistantError( + "Unable to get events: Sync from server has not completed" + ) + return self.data.overlapping( + dt_util.as_local(start_date), + dt_util.as_local(end_date), + ) + + @property + def upcoming(self) -> Iterable[Event] | None: + """Return upcoming events if any.""" + if self.data: + return self.data.active_after(dt_util.now()) + return None + + +class CalendarQueryUpdateCoordinator(DataUpdateCoordinator[list[Event]]): + """Coordinator for calendar RPC calls. + + This sends a polling RPC, not using sync, as a workaround + for limitations in the calendar API for supporting search. + """ + + def __init__( + self, + hass: HomeAssistant, + calendar_service: GoogleCalendarService, + name: str, + calendar_id: str, + search: str | None, + ) -> None: + """Create the CalendarQueryUpdateCoordinator.""" + super().__init__( + hass, + _LOGGER, + name=name, + update_interval=MIN_TIME_BETWEEN_UPDATES, + ) + self.calendar_service = calendar_service + self.calendar_id = calendar_id + self._search = search + + async def async_get_events( + self, start_date: datetime, end_date: datetime + ) -> Iterable[Event]: + """Get all events in a specific time frame.""" + request = ListEventsRequest( + calendar_id=self.calendar_id, + start_time=start_date, + end_time=end_date, + search=self._search, + ) + result_items = [] + try: + result = await self.calendar_service.async_list_events(request) + async for result_page in result: + result_items.extend(result_page.items) + except ApiException as err: + self.async_set_update_error(err) + raise HomeAssistantError(str(err)) from err + return result_items + + async def _async_update_data(self) -> list[Event]: + """Fetch data from API endpoint.""" + request = ListEventsRequest(calendar_id=self.calendar_id, search=self._search) + try: + result = await self.calendar_service.async_list_events(request) + except ApiException as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + return result.items + + @property + def upcoming(self) -> Iterable[Event] | None: + """Return the next upcoming event if any.""" + return self.data + class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity): """A calendar event entity.""" @@ -279,7 +370,7 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity): def __init__( self, - coordinator: CalendarUpdateCoordinator, + coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator, calendar_id: str, data: dict[str, Any], entity_id: str, @@ -352,14 +443,7 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity): self, hass: HomeAssistant, start_date: datetime, end_date: datetime ) -> list[CalendarEvent]: """Get all events in a specific time frame.""" - if not (timeline := self.coordinator.data): - raise HomeAssistantError( - "Unable to get events: Sync from server has not completed" - ) - result_items = timeline.overlapping( - dt_util.as_local(start_date), - dt_util.as_local(end_date), - ) + result_items = await self.coordinator.async_get_events(start_date, end_date) return [ _get_calendar_event(event) for event in filter(self._event_filter, result_items) @@ -367,14 +451,12 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity): def _apply_coordinator_update(self) -> None: """Copy state from the coordinator to this entity.""" - if (timeline := self.coordinator.data) and ( - api_event := next( - filter( - self._event_filter, - timeline.active_after(dt_util.now()), - ), - None, - ) + if api_event := next( + filter( + self._event_filter, + self.coordinator.upcoming or [], + ), + None, ): self._event = _get_calendar_event(api_event) (self._event.summary, self._offset_value) = extract_offset( From 42c09d881165cfdfa112dabd3eadd3055d380df4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 8 Nov 2022 12:44:44 +0100 Subject: [PATCH 056/114] Bumped version to 2022.11.2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ef94db7e2b6..f3b51bd5d3a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __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/pyproject.toml b/pyproject.toml index e616441fadd..5058c46641f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.1" +version = "2022.11.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 345d356e9a63d6b19b74f8ed1f9f545a7f1514e3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Nov 2022 13:35:12 +0100 Subject: [PATCH 057/114] Fix rest import (#81784) --- tests/components/rest/test_init.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index b6861a098b1..6f35301b1c7 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -15,6 +15,7 @@ from homeassistant.const import ( SERVICE_RELOAD, STATE_UNAVAILABLE, ) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow From 59ec8291063a89010b02b5e2ad3238f0b6b825c8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 8 Nov 2022 15:00:30 +0100 Subject: [PATCH 058/114] Update frontend to 20221108.0 (#81787) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index f4f46a1f89b..ec7001006b1 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20221102.1"], + "requirements": ["home-assistant-frontend==20221108.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ebf4eff0d2b..9db814d2dea 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.61.1 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 -home-assistant-frontend==20221102.1 +home-assistant-frontend==20221108.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index a9d1c292018..5d84cebc108 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -871,7 +871,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221102.1 +home-assistant-frontend==20221108.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d3fa0e0698..152bf8ee1db 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -651,7 +651,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221102.1 +home-assistant-frontend==20221108.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 7ab20290710a267a55fa0fe1c98a8f8c4847b1b8 Mon Sep 17 00:00:00 2001 From: ztamas83 <71548739+ztamas83@users.noreply.github.com> Date: Tue, 8 Nov 2022 16:09:58 +0100 Subject: [PATCH 059/114] Retry tibber setup (#81785) * Handle integration setup retries * Fix black error * Update to falsy check Co-authored-by: Martin Hjelmare * Remove duplicated log * Update exception message Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/tibber/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 35507986f90..4d9c0560682 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -53,6 +53,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await tibber_connection.update_info() + if not tibber_connection.name: + raise ConfigEntryNotReady("Could not fetch Tibber data.") + except asyncio.TimeoutError as err: raise ConfigEntryNotReady from err except aiohttp.ClientError as err: From d88b2bf19c1fb465fe541cd3c3e000e68a99ac56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Nov 2022 09:20:03 -0600 Subject: [PATCH 060/114] Fix off by one in HomeKit iid allocator (#81793) --- homeassistant/components/homekit/iidmanager.py | 8 ++++---- tests/components/homekit/test_diagnostics.py | 2 -- tests/components/homekit/test_iidmanager.py | 6 ++++++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/iidmanager.py b/homeassistant/components/homekit/iidmanager.py index 3805748225a..8bac268800b 100644 --- a/homeassistant/components/homekit/iidmanager.py +++ b/homeassistant/components/homekit/iidmanager.py @@ -109,12 +109,12 @@ class AccessoryIIDStorage: # AID must be a string since JSON keys cannot be int aid_str = str(aid) accessory_allocation = self.allocations.setdefault(aid_str, {}) - accessory_allocated_iids = self.allocated_iids.setdefault(aid_str, []) + accessory_allocated_iids = self.allocated_iids.setdefault(aid_str, [1]) if service_hap_type == ACCESSORY_INFORMATION_SERVICE and char_uuid is None: - allocated_iid = 1 - elif allocation_key in accessory_allocation: + return 1 + if allocation_key in accessory_allocation: return accessory_allocation[allocation_key] - elif accessory_allocated_iids: + if accessory_allocated_iids: allocated_iid = accessory_allocated_iids[-1] + 1 else: allocated_iid = 2 diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py index be98c3bacdd..d114c462e2f 100644 --- a/tests/components/homekit/test_diagnostics.py +++ b/tests/components/homekit/test_diagnostics.py @@ -51,7 +51,6 @@ async def test_config_entry_running(hass, hass_client, hk_driver, mock_async_zer "3E__23_": 5, "3E__30_": 6, "3E__52_": 7, - "3E___": 1, "A2__37_": 9, "A2___": 8, } @@ -278,7 +277,6 @@ async def test_config_entry_accessory( "3E__23_": 5, "3E__30_": 6, "3E__52_": 7, - "3E___": 1, "43__25_": 11, "43___": 10, "A2__37_": 9, diff --git a/tests/components/homekit/test_iidmanager.py b/tests/components/homekit/test_iidmanager.py index 3e4a19c9045..c16cbf01d7d 100644 --- a/tests/components/homekit/test_iidmanager.py +++ b/tests/components/homekit/test_iidmanager.py @@ -152,23 +152,29 @@ async def test_iid_generation_and_restore_v2(hass, iid_storage, hass_storage): 1, "000000AA-0000-1000-8000-0026BB765291", None, None, None ) assert not_accessory_info_service_iid == 2 + assert iid_storage.allocated_iids == {"1": [1, 2]} not_accessory_info_service_iid_2 = iid_storage.get_or_allocate_iid( 1, "000000BB-0000-1000-8000-0026BB765291", None, None, None ) assert not_accessory_info_service_iid_2 == 3 + assert iid_storage.allocated_iids == {"1": [1, 2, 3]} not_accessory_info_service_iid_2 = iid_storage.get_or_allocate_iid( 1, "000000BB-0000-1000-8000-0026BB765291", None, None, None ) assert not_accessory_info_service_iid_2 == 3 + assert iid_storage.allocated_iids == {"1": [1, 2, 3]} accessory_info_service_iid = iid_storage.get_or_allocate_iid( 1, "0000003E-0000-1000-8000-0026BB765291", None, None, None ) assert accessory_info_service_iid == 1 + assert iid_storage.allocated_iids == {"1": [1, 2, 3]} accessory_info_service_iid = iid_storage.get_or_allocate_iid( 1, "0000003E-0000-1000-8000-0026BB765291", None, None, None ) assert accessory_info_service_iid == 1 + assert iid_storage.allocated_iids == {"1": [1, 2, 3]} accessory_info_service_iid = iid_storage.get_or_allocate_iid( 2, "0000003E-0000-1000-8000-0026BB765291", None, None, None ) assert accessory_info_service_iid == 1 + assert iid_storage.allocated_iids == {"1": [1, 2, 3], "2": [1]} From f9ebbb936a496b1121d7fda874b117271adae863 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 15 Nov 2022 18:30:56 +0100 Subject: [PATCH 061/114] Fix UniFi block client switches on 2022.11.2 (#81884) fixes undefined --- homeassistant/components/unifi/switch.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 65d0041187e..43a08b58eac 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -367,8 +367,6 @@ class UnifiBlockClientSwitch(SwitchEntity): self.hass.async_create_task(self.remove_item({self._obj_id})) return - client = self.controller.api.clients[self._obj_id] - self._attr_is_on = not client.blocked self._attr_available = self.controller.available self.async_write_ha_state() From 18842ef57108a066f83d0909dd06022fe536ddd3 Mon Sep 17 00:00:00 2001 From: Yukon Vinecki Date: Wed, 16 Nov 2022 04:14:14 -0800 Subject: [PATCH 062/114] Fix Z-Wave JS cover stop support (#78723) Co-authored-by: Franck Nijhof --- homeassistant/components/zwave_js/cover.py | 20 ++------- tests/components/zwave_js/test_cover.py | 48 ++-------------------- 2 files changed, 8 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index b3f3aeaf1c0..43b51048de4 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -7,9 +7,6 @@ from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import TARGET_STATE_PROPERTY, TARGET_VALUE_PROPERTY from zwave_js_server.const.command_class.barrier_operator import BarrierState from zwave_js_server.const.command_class.multilevel_switch import ( - COVER_CLOSE_PROPERTY, - COVER_DOWN_PROPERTY, - COVER_OFF_PROPERTY, COVER_ON_PROPERTY, COVER_OPEN_PROPERTY, COVER_UP_PROPERTY, @@ -156,23 +153,14 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): async def async_stop_cover(self, **kwargs: Any) -> None: """Stop cover.""" - open_value = ( + cover_property = ( self.get_zwave_value(COVER_OPEN_PROPERTY) or self.get_zwave_value(COVER_UP_PROPERTY) or self.get_zwave_value(COVER_ON_PROPERTY) ) - if open_value: - # Stop the cover if it's opening - await self.info.node.async_set_value(open_value, False) - - close_value = ( - self.get_zwave_value(COVER_CLOSE_PROPERTY) - or self.get_zwave_value(COVER_DOWN_PROPERTY) - or self.get_zwave_value(COVER_OFF_PROPERTY) - ) - if close_value: - # Stop the cover if it's closing - await self.info.node.async_set_value(close_value, False) + if cover_property: + # Stop the cover, will stop regardless of the actual direction of travel. + await self.info.node.async_set_value(cover_property, False) class ZWaveTiltCover(ZWaveCover): diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index f26b0d29069..0ca2e36d853 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -116,7 +116,7 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 2 + assert len(client.async_send_command.call_args_list) == 1 open_args = client.async_send_command.call_args_list[0][0][0] assert open_args["command"] == "node.set_value" assert open_args["nodeId"] == 6 @@ -127,16 +127,6 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): } assert not open_args["value"] - close_args = client.async_send_command.call_args_list[1][0][0] - assert close_args["command"] == "node.set_value" - assert close_args["nodeId"] == 6 - assert close_args["valueId"] == { - "commandClass": 38, - "endpoint": 0, - "property": "Close", - } - assert not close_args["value"] - # Test position update from value updated event event = Event( type="value updated", @@ -189,7 +179,7 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): blocking=True, ) - assert len(client.async_send_command.call_args_list) == 2 + assert len(client.async_send_command.call_args_list) == 1 open_args = client.async_send_command.call_args_list[0][0][0] assert open_args["command"] == "node.set_value" assert open_args["nodeId"] == 6 @@ -200,16 +190,6 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): } assert not open_args["value"] - close_args = client.async_send_command.call_args_list[1][0][0] - assert close_args["command"] == "node.set_value" - assert close_args["nodeId"] == 6 - assert close_args["valueId"] == { - "commandClass": 38, - "endpoint": 0, - "property": "Close", - } - assert not close_args["value"] - client.async_send_command.reset_mock() event = Event( @@ -329,7 +309,7 @@ async def test_aeotec_nano_shutter_cover( blocking=True, ) - assert len(client.async_send_command.call_args_list) == 2 + assert len(client.async_send_command.call_args_list) == 1 open_args = client.async_send_command.call_args_list[0][0][0] assert open_args["command"] == "node.set_value" assert open_args["nodeId"] == 3 @@ -340,16 +320,6 @@ async def test_aeotec_nano_shutter_cover( } assert not open_args["value"] - close_args = client.async_send_command.call_args_list[1][0][0] - assert close_args["command"] == "node.set_value" - assert close_args["nodeId"] == 3 - assert close_args["valueId"] == { - "commandClass": 38, - "endpoint": 0, - "property": "Off", - } - assert not close_args["value"] - # Test position update from value updated event event = Event( type="value updated", @@ -403,7 +373,7 @@ async def test_aeotec_nano_shutter_cover( blocking=True, ) - assert len(client.async_send_command.call_args_list) == 2 + assert len(client.async_send_command.call_args_list) == 1 open_args = client.async_send_command.call_args_list[0][0][0] assert open_args["command"] == "node.set_value" assert open_args["nodeId"] == 3 @@ -414,16 +384,6 @@ async def test_aeotec_nano_shutter_cover( } assert not open_args["value"] - close_args = client.async_send_command.call_args_list[1][0][0] - assert close_args["command"] == "node.set_value" - assert close_args["nodeId"] == 3 - assert close_args["valueId"] == { - "commandClass": 38, - "endpoint": 0, - "property": "Off", - } - assert not close_args["value"] - async def test_blind_cover(hass, client, iblinds_v2, integration): """Test a blind cover entity.""" From d94e969dc11a4ee9d7aff11ff630f5968a2d0918 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Nov 2022 11:41:55 -0600 Subject: [PATCH 063/114] Fix instability with HomeKit trigger accessories (#80703) fixes https://github.com/home-assistant/core/issues/78774 fixes https://github.com/home-assistant/core/issues/81685 --- homeassistant/components/homekit/__init__.py | 61 ++-- .../components/homekit/accessories.py | 2 +- .../components/homekit/diagnostics.py | 11 +- .../components/homekit/type_triggers.py | 22 +- tests/components/homekit/test_diagnostics.py | 322 +++++++++++++++++- tests/components/homekit/test_init.py | 64 +++- 6 files changed, 450 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index ca73c7dc242..1129e8c3f66 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -23,6 +23,9 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, ) from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.device_automation.trigger import ( + async_validate_trigger_config, +) from homeassistant.components.http import HomeAssistantView from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN from homeassistant.components.network import MDNS_TARGET_IP @@ -906,29 +909,47 @@ class HomeKit: self.bridge = HomeBridge(self.hass, self.driver, self._name) for state in entity_states: self.add_bridge_accessory(state) - dev_reg = device_registry.async_get(self.hass) if self._devices: - valid_device_ids = [] - for device_id in self._devices: - if not dev_reg.async_get(device_id): - _LOGGER.warning( - "HomeKit %s cannot add device %s because it is missing from the device registry", - self._name, - device_id, - ) - else: - valid_device_ids.append(device_id) - for device_id, device_triggers in ( - await device_automation.async_get_device_automations( - self.hass, - device_automation.DeviceAutomationType.TRIGGER, - valid_device_ids, - ) - ).items(): - if device := dev_reg.async_get(device_id): - self.add_bridge_triggers_accessory(device, device_triggers) + await self._async_add_trigger_accessories() return self.bridge + async def _async_add_trigger_accessories(self) -> None: + """Add devices with triggers to the bridge.""" + dev_reg = device_registry.async_get(self.hass) + valid_device_ids = [] + for device_id in self._devices: + if not dev_reg.async_get(device_id): + _LOGGER.warning( + "HomeKit %s cannot add device %s because it is missing from the device registry", + self._name, + device_id, + ) + else: + valid_device_ids.append(device_id) + for device_id, device_triggers in ( + await device_automation.async_get_device_automations( + self.hass, + device_automation.DeviceAutomationType.TRIGGER, + valid_device_ids, + ) + ).items(): + device = dev_reg.async_get(device_id) + assert device is not None + valid_device_triggers: list[dict[str, Any]] = [] + for trigger in device_triggers: + try: + await async_validate_trigger_config(self.hass, trigger) + except vol.Invalid as ex: + _LOGGER.debug( + "%s: cannot add unsupported trigger %s because it requires additional inputs which are not supported by HomeKit: %s", + self._name, + trigger, + ex, + ) + continue + valid_device_triggers.append(trigger) + self.add_bridge_triggers_accessory(device, valid_device_triggers) + async def _async_create_accessories(self) -> bool: """Create the accessories.""" assert self.driver is not None diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 44b73b41ded..aca53a50105 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -653,7 +653,7 @@ class HomeIIDManager(IIDManager): # type: ignore[misc] """Get IID for object.""" aid = obj.broker.aid if isinstance(obj, Characteristic): - service = obj.service + service: Service = obj.service iid = self._iid_storage.get_or_allocate_iid( aid, service.type_id, service.unique_id, obj.type_id, obj.unique_id ) diff --git a/homeassistant/components/homekit/diagnostics.py b/homeassistant/components/homekit/diagnostics.py index 1d0bfb92fcc..f27171e6eae 100644 --- a/homeassistant/components/homekit/diagnostics.py +++ b/homeassistant/components/homekit/diagnostics.py @@ -67,13 +67,16 @@ def _get_accessory_diagnostics( hass: HomeAssistant, accessory: HomeAccessory ) -> dict[str, Any]: """Return diagnostics for an accessory.""" - return { + entity_state = None + if accessory.entity_id: + entity_state = hass.states.get(accessory.entity_id) + data = { "aid": accessory.aid, "config": accessory.config, "category": accessory.category, "name": accessory.display_name, "entity_id": accessory.entity_id, - "entity_state": async_redact_data( - hass.states.get(accessory.entity_id), TO_REDACT - ), } + if entity_state: + data["entity_state"] = async_redact_data(entity_state, TO_REDACT) + return data diff --git a/homeassistant/components/homekit/type_triggers.py b/homeassistant/components/homekit/type_triggers.py index b9b2ad6ce8f..b239d67877c 100644 --- a/homeassistant/components/homekit/type_triggers.py +++ b/homeassistant/components/homekit/type_triggers.py @@ -7,9 +7,11 @@ from typing import Any from pyhap.const import CATEGORY_SENSOR from homeassistant.core import CALLBACK_TYPE, Context +from homeassistant.helpers import entity_registry from homeassistant.helpers.trigger import async_initialize_triggers from .accessories import TYPES, HomeAccessory +from .aidmanager import get_system_unique_id from .const import ( CHAR_NAME, CHAR_PROGRAMMABLE_SWITCH_EVENT, @@ -18,6 +20,7 @@ from .const import ( SERV_SERVICE_LABEL, SERV_STATELESS_PROGRAMMABLE_SWITCH, ) +from .util import cleanup_name_for_homekit _LOGGER = logging.getLogger(__name__) @@ -39,13 +42,22 @@ class DeviceTriggerAccessory(HomeAccessory): self._remove_triggers: CALLBACK_TYPE | None = None self.triggers = [] assert device_triggers is not None + ent_reg = entity_registry.async_get(self.hass) for idx, trigger in enumerate(device_triggers): - type_ = trigger["type"] - subtype = trigger.get("subtype") + type_: str = trigger["type"] + subtype: str | None = trigger.get("subtype") unique_id = f'{type_}-{subtype or ""}' - trigger_name = ( - f"{type_.title()} {subtype.title()}" if subtype else type_.title() - ) + if (entity_id := trigger.get("entity_id")) and ( + entry := ent_reg.async_get(entity_id) + ): + unique_id += f"-entity_unique_id:{get_system_unique_id(entry)}" + trigger_name_parts = [] + if entity_id and (state := self.hass.states.get(entity_id)): + trigger_name_parts.append(state.name) + trigger_name_parts.append(type_.replace("_", " ").title()) + if subtype: + trigger_name_parts.append(subtype.replace("_", " ").title()) + trigger_name = cleanup_name_for_homekit(" ".join(trigger_name_parts)) serv_stateless_switch = self.add_preload_service( SERV_STATELESS_PROGRAMMABLE_SWITCH, [CHAR_NAME, CHAR_SERVICE_LABEL_INDEX], diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py index d114c462e2f..30fe5f2d8fc 100644 --- a/tests/components/homekit/test_diagnostics.py +++ b/tests/components/homekit/test_diagnostics.py @@ -1,12 +1,14 @@ """Test homekit diagnostics.""" -from unittest.mock import ANY, patch +from unittest.mock import ANY, MagicMock, patch from homeassistant.components.homekit.const import ( + CONF_DEVICES, CONF_HOMEKIT_MODE, DOMAIN, HOMEKIT_MODE_ACCESSORY, ) from homeassistant.const import CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STARTED +from homeassistant.setup import async_setup_component from .util import async_init_integration @@ -290,3 +292,321 @@ async def test_config_entry_accessory( ), patch("homeassistant.components.homekit.async_port_is_available"): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_config_entry_with_trigger_accessory( + hass, + hass_client, + hk_driver, + mock_async_zeroconf, + events, + demo_cleanup, + device_reg, + entity_reg, +): + """Test generating diagnostics for a bridge config entry with a trigger accessory.""" + assert await async_setup_component(hass, "demo", {"demo": {}}) + hk_driver.publish = MagicMock() + + demo_config_entry = MockConfigEntry(domain="domain") + demo_config_entry.add_to_hass(hass) + assert await async_setup_component(hass, "demo", {"demo": {}}) + await hass.async_block_till_done() + + entry = entity_reg.async_get("light.ceiling_lights") + assert entry is not None + device_id = entry.device_id + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_NAME: "mock_name", + CONF_PORT: 12345, + CONF_DEVICES: [device_id], + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": [], + "include_entities": ["light.none"], + }, + }, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + diag.pop("iid_storage") + diag.pop("bridge") + assert diag == { + "accessories": [ + { + "aid": 1, + "services": [ + { + "characteristics": [ + {"format": "bool", "iid": 2, "perms": ["pw"], "type": "14"}, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "20", + "value": "Home Assistant", + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "21", + "value": "Bridge", + }, + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "23", + "value": "mock_name", + }, + { + "format": "string", + "iid": 6, + "perms": ["pr"], + "type": "30", + "value": "homekit.bridge", + }, + { + "format": "string", + "iid": 7, + "perms": ["pr"], + "type": "52", + "value": "2022.12.0", + }, + ], + "iid": 1, + "type": "3E", + }, + { + "characteristics": [ + { + "format": "string", + "iid": 9, + "perms": ["pr", "ev"], + "type": "37", + "value": "01.01.00", + } + ], + "iid": 8, + "type": "A2", + }, + ], + }, + { + "aid": ANY, + "services": [ + { + "characteristics": [ + {"format": "bool", "iid": 2, "perms": ["pw"], "type": "14"}, + { + "format": "string", + "iid": 3, + "perms": ["pr"], + "type": "20", + "value": "Demo", + }, + { + "format": "string", + "iid": 4, + "perms": ["pr"], + "type": "21", + "value": "Home Assistant", + }, + { + "format": "string", + "iid": 5, + "perms": ["pr"], + "type": "23", + "value": "Ceiling Lights", + }, + { + "format": "string", + "iid": 6, + "perms": ["pr"], + "type": "30", + "value": ANY, + }, + { + "format": "string", + "iid": 7, + "perms": ["pr"], + "type": "52", + "value": ANY, + }, + ], + "iid": 1, + "type": "3E", + }, + { + "characteristics": [ + { + "format": "uint8", + "iid": 9, + "perms": ["pr", "ev"], + "type": "73", + "valid-values": [0], + "value": None, + }, + { + "format": "string", + "iid": 10, + "perms": ["pr"], + "type": "23", + "value": "Ceiling Lights " "Changed States", + }, + { + "format": "uint8", + "iid": 11, + "maxValue": 255, + "minStep": 1, + "minValue": 1, + "perms": ["pr"], + "type": "CB", + "value": 1, + }, + ], + "iid": 8, + "linked": [12], + "type": "89", + }, + { + "characteristics": [ + { + "format": "uint8", + "iid": 13, + "perms": ["pr"], + "type": "CD", + "valid-values": [0, 1], + "value": 1, + } + ], + "iid": 12, + "type": "CC", + }, + { + "characteristics": [ + { + "format": "uint8", + "iid": 15, + "perms": ["pr", "ev"], + "type": "73", + "valid-values": [0], + "value": None, + }, + { + "format": "string", + "iid": 16, + "perms": ["pr"], + "type": "23", + "value": "Ceiling Lights " "Turned Off", + }, + { + "format": "uint8", + "iid": 17, + "maxValue": 255, + "minStep": 1, + "minValue": 1, + "perms": ["pr"], + "type": "CB", + "value": 2, + }, + ], + "iid": 14, + "linked": [18], + "type": "89", + }, + { + "characteristics": [ + { + "format": "uint8", + "iid": 19, + "perms": ["pr"], + "type": "CD", + "valid-values": [0, 1], + "value": 1, + } + ], + "iid": 18, + "type": "CC", + }, + { + "characteristics": [ + { + "format": "uint8", + "iid": 21, + "perms": ["pr", "ev"], + "type": "73", + "valid-values": [0], + "value": None, + }, + { + "format": "string", + "iid": 22, + "perms": ["pr"], + "type": "23", + "value": "Ceiling Lights " "Turned On", + }, + { + "format": "uint8", + "iid": 23, + "maxValue": 255, + "minStep": 1, + "minValue": 1, + "perms": ["pr"], + "type": "CB", + "value": 3, + }, + ], + "iid": 20, + "linked": [24], + "type": "89", + }, + { + "characteristics": [ + { + "format": "uint8", + "iid": 25, + "perms": ["pr"], + "type": "CD", + "valid-values": [0, 1], + "value": 1, + } + ], + "iid": 24, + "type": "CC", + }, + ], + }, + ], + "client_properties": {}, + "config-entry": { + "data": {"name": "mock_name", "port": 12345}, + "options": { + "devices": [device_id], + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": [], + "include_entities": ["light.none"], + }, + }, + "title": "Mock Title", + "version": 1, + }, + "config_version": 2, + "pairing_id": ANY, + "status": 1, + } + with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( + "homeassistant.components.homekit.HomeKit.async_stop" + ), patch("homeassistant.components.homekit.async_port_is_available"): + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/homekit/test_init.py b/tests/components/homekit/test_init.py index 17933616fc4..f91855b31b5 100644 --- a/tests/components/homekit/test_init.py +++ b/tests/components/homekit/test_init.py @@ -7,9 +7,17 @@ from homeassistant.components.homekit.const import ( DOMAIN as DOMAIN_HOMEKIT, EVENT_HOMEKIT_CHANGED, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_SERVICE +from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigEntryState +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SERVICE, + EVENT_HOMEASSISTANT_STARTED, +) from homeassistant.setup import async_setup_component +from .util import PATH_HOMEKIT + +from tests.common import MockConfigEntry from tests.components.logbook.common import MockRow, mock_humanify @@ -52,3 +60,57 @@ async def test_humanify_homekit_changed_event(hass, hk_driver, mock_get_source_i assert event2["domain"] == DOMAIN_HOMEKIT assert event2["message"] == "send command set_cover_position to 75 for Window" assert event2["entity_id"] == "cover.window" + + +async def test_bridge_with_triggers( + hass, hk_driver, mock_async_zeroconf, entity_reg, caplog +): + """Test we can setup a bridge with triggers and we ignore numeric states. + + Since numeric states are not supported by HomeKit as they require + an above or below additional configuration which we have no way + to input, we ignore them. + """ + assert await async_setup_component(hass, "demo", {"demo": {}}) + await hass.async_block_till_done() + + entry = entity_reg.async_get("cover.living_room_window") + assert entry is not None + device_id = entry.device_id + + entry = MockConfigEntry( + domain=DOMAIN_HOMEKIT, + source=SOURCE_ZEROCONF, + data={ + "name": "HASS Bridge", + "port": 12345, + }, + options={ + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": [], + "include_entities": ["cover.living_room_window"], + }, + "exclude_accessory_mode": True, + "mode": "bridge", + "devices": [device_id], + }, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" + ), patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert ( + "requires additional inputs which are not supported by HomeKit" in caplog.text + ) From 7d20bb05326de46eabc6f78a31f1761350138117 Mon Sep 17 00:00:00 2001 From: rappenze Date: Sat, 12 Nov 2022 10:43:11 +0100 Subject: [PATCH 064/114] Fix accelerator sensor in fibaro integration (#81237) * Fix accelerator sensor in fibaro integration * Implement suggestions from code review * Implement suggestions from code review * Changes as suggested in code review * Adjust as suggested in code review --- homeassistant/components/fibaro/__init__.py | 1 + .../components/fibaro/binary_sensor.py | 56 ++++++++++++++++--- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 3661721810b..19f36742740 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -84,6 +84,7 @@ FIBARO_TYPEMAP = { "com.fibaro.thermostatDanfoss": Platform.CLIMATE, "com.fibaro.doorLock": Platform.LOCK, "com.fibaro.binarySensor": Platform.BINARY_SENSOR, + "com.fibaro.accelerometer": Platform.BINARY_SENSOR, } DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema( diff --git a/homeassistant/components/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py index 359869efc25..f9baa33c41f 100644 --- a/homeassistant/components/fibaro/binary_sensor.py +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Fibaro binary sensors.""" from __future__ import annotations +from collections.abc import Mapping +import json from typing import Any from homeassistant.components.binary_sensor import ( @@ -28,6 +30,11 @@ SENSOR_TYPES = { "com.fibaro.smokeSensor": ["Smoke", "mdi:smoking", BinarySensorDeviceClass.SMOKE], "com.fibaro.FGMS001": ["Motion", "mdi:run", BinarySensorDeviceClass.MOTION], "com.fibaro.heatDetector": ["Heat", "mdi:fire", BinarySensorDeviceClass.HEAT], + "com.fibaro.accelerometer": [ + "Moving", + "mdi:axis-arrow", + BinarySensorDeviceClass.MOVING, + ], } @@ -55,15 +62,50 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorEntity): """Initialize the binary_sensor.""" super().__init__(fibaro_device) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) - stype = None + self._own_extra_state_attributes: Mapping[str, Any] = {} + self._fibaro_sensor_type = None if fibaro_device.type in SENSOR_TYPES: - stype = fibaro_device.type + self._fibaro_sensor_type = fibaro_device.type elif fibaro_device.baseType in SENSOR_TYPES: - stype = fibaro_device.baseType - if stype: - self._attr_device_class = SENSOR_TYPES[stype][2] - self._attr_icon = SENSOR_TYPES[stype][1] + self._fibaro_sensor_type = fibaro_device.baseType + if self._fibaro_sensor_type: + self._attr_device_class = SENSOR_TYPES[self._fibaro_sensor_type][2] + self._attr_icon = SENSOR_TYPES[self._fibaro_sensor_type][1] + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return the extra state attributes of the device.""" + return super().extra_state_attributes | self._own_extra_state_attributes def update(self) -> None: """Get the latest data and update the state.""" - self._attr_is_on = self.current_binary_state + if self._fibaro_sensor_type == "com.fibaro.accelerometer": + # Accelerator sensors have values for the three axis x, y and z + moving_values = self._get_moving_values() + self._attr_is_on = self._is_moving(moving_values) + self._own_extra_state_attributes = self._get_xyz_moving(moving_values) + else: + self._attr_is_on = self.current_binary_state + + def _get_xyz_moving(self, moving_values: Mapping[str, Any]) -> Mapping[str, Any]: + """Return x y z values of the accelerator sensor value.""" + attrs = {} + for axis_name in ("x", "y", "z"): + attrs[axis_name] = float(moving_values[axis_name]) + return attrs + + def _is_moving(self, moving_values: Mapping[str, Any]) -> bool: + """Return that a moving is detected when one axis reports a value.""" + for axis_name in ("x", "y", "z"): + if float(moving_values[axis_name]) != 0: + return True + return False + + def _get_moving_values(self) -> Mapping[str, Any]: + """Get the moving values of the accelerator sensor in a dict.""" + value = self.fibaro_device.properties.value + if isinstance(value, str): + # HC2 returns dict as str + return json.loads(value) + # HC3 returns a real dict + return value From 3f666396c9933bc21ffdcd90dccecd98a0e7d932 Mon Sep 17 00:00:00 2001 From: chpego <38792705+chpego@users.noreply.github.com> Date: Tue, 8 Nov 2022 13:39:53 +0000 Subject: [PATCH 065/114] Fix Fully Kiosk start application service field (#81738) Fix attributes services to start_application --- homeassistant/components/fully_kiosk/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fully_kiosk/services.yaml b/homeassistant/components/fully_kiosk/services.yaml index b8ea6b371d7..88178e35809 100644 --- a/homeassistant/components/fully_kiosk/services.yaml +++ b/homeassistant/components/fully_kiosk/services.yaml @@ -20,7 +20,7 @@ start_application: device: integration: fully_kiosk fields: - url: + application: name: Application description: Package name of the application to start. example: "de.ozerov.fully" From 2eacbef0610ea930d5b96450a7ea4b9675d5dc66 Mon Sep 17 00:00:00 2001 From: Oliver Dippel Date: Sun, 13 Nov 2022 21:17:59 +0100 Subject: [PATCH 066/114] Fix ibeacon source attribute not being updated (#81740) fixes undefined --- .../components/ibeacon/coordinator.py | 6 +- tests/components/ibeacon/test_coordinator.py | 69 ++++++++++++++++++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py index 33b33c56ed0..ed62649de2d 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -396,7 +396,11 @@ class IBeaconCoordinator: ) continue - if service_info.rssi != ibeacon_advertisement.rssi: + if ( + service_info.rssi != ibeacon_advertisement.rssi + or service_info.source != ibeacon_advertisement.source + ): + ibeacon_advertisement.source = service_info.source ibeacon_advertisement.update_rssi(service_info.rssi) async_dispatcher_send( self.hass, diff --git a/tests/components/ibeacon/test_coordinator.py b/tests/components/ibeacon/test_coordinator.py index 25ce7154a37..6acbf5569f8 100644 --- a/tests/components/ibeacon/test_coordinator.py +++ b/tests/components/ibeacon/test_coordinator.py @@ -3,16 +3,19 @@ from dataclasses import replace from datetime import timedelta +import time +from bleak.backends.scanner import BLEDevice import pytest -from homeassistant.components.ibeacon.const import DOMAIN, UPDATE_INTERVAL +from homeassistant.components.ibeacon.const import ATTR_SOURCE, DOMAIN, UPDATE_INTERVAL from homeassistant.const import STATE_HOME from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.util import dt as dt_util from . import ( BLUECHARM_BEACON_SERVICE_INFO, + BLUECHARM_BEACON_SERVICE_INFO_2, BLUECHARM_BEACON_SERVICE_INFO_DBUS, TESLA_TRANSIENT, TESLA_TRANSIENT_BLE_DEVICE, @@ -20,6 +23,8 @@ from . import ( from tests.common import MockConfigEntry, async_fire_time_changed from tests.components.bluetooth import ( + generate_advertisement_data, + inject_advertisement_with_time_and_source_connectable, inject_bluetooth_service_info, patch_all_discovered_devices, ) @@ -252,3 +257,65 @@ async def test_ignore_transient_devices_unless_we_see_them_a_few_times(hass): await hass.async_block_till_done() assert hass.states.get("device_tracker.s6da7c9389bd5452cc_cccc").state == STATE_HOME + + +async def test_changing_source_attribute(hass): + """Test update of the source attribute.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + now = time.monotonic() + info = BLUECHARM_BEACON_SERVICE_INFO_2 + device = BLEDevice( + address=info.address, + name=info.name, + details={}, + ) + advertisement_data = generate_advertisement_data( + local_name=info.name, + manufacturer_data=info.manufacturer_data, + service_data=info.service_data, + service_uuids=info.service_uuids, + rssi=info.rssi, + ) + + inject_advertisement_with_time_and_source_connectable( + hass, + device, + advertisement_data, + now, + "local", + True, + ) + await hass.async_block_till_done() + + attributes = hass.states.get( + "sensor.bluecharm_177999_8105_estimated_distance" + ).attributes + assert attributes[ATTR_SOURCE] == "local" + + inject_advertisement_with_time_and_source_connectable( + hass, + device, + advertisement_data, + now, + "proxy", + True, + ) + await hass.async_block_till_done() + with patch_all_discovered_devices([BLUECHARM_BEACON_SERVICE_INFO_2]): + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=UPDATE_INTERVAL.total_seconds() * 2), + ) + await hass.async_block_till_done() + + attributes = hass.states.get( + "sensor.bluecharm_177999_8105_estimated_distance" + ).attributes + assert attributes[ATTR_SOURCE] == "proxy" From 252941ae263286df6a9b738d39c074f092cb27b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 9 Nov 2022 16:51:33 +0200 Subject: [PATCH 067/114] Upgrade huawei-lte-api to 1.6.7, fixes empty username issues (#81751) Recentish versions of huawei-lte-api behave differently with regards to empty/default username compared to the older versions this integration was originally written against. 1.6.5+ changes the behavior so that our existing implementation works as expected when no username is supplied for the config entry. https://github.com/Salamek/huawei-lte-api/releases/tag/1.6.4 https://github.com/Salamek/huawei-lte-api/releases/tag/1.6.5 https://github.com/Salamek/huawei-lte-api/releases/tag/1.6.6 https://github.com/Salamek/huawei-lte-api/releases/tag/1.6.7 --- homeassistant/components/huawei_lte/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index c658fff1b0f..2c777aa4339 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ - "huawei-lte-api==1.6.3", + "huawei-lte-api==1.6.7", "stringcase==1.2.0", "url-normalize==1.4.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 5d84cebc108..23accfd4b89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -889,7 +889,7 @@ horimote==0.4.1 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.6.3 +huawei-lte-api==1.6.7 # homeassistant.components.hydrawise hydrawiser==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 152bf8ee1db..67f2709e1c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -666,7 +666,7 @@ homepluscontrol==0.0.5 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.6.3 +huawei-lte-api==1.6.7 # homeassistant.components.hyperion hyperion-py==0.7.5 From 04fda5638c7ee445fe86e28e7a1b8c411d71f7e8 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 9 Nov 2022 08:44:30 -0600 Subject: [PATCH 068/114] Change life360 timeouts & retries (#81799) Change from single timeout of 10 to socket timeout of 15, total timeout of 60, and retry up to 3 times. Bump life360 package to 5.3.0. --- homeassistant/components/life360/const.py | 5 ++++- homeassistant/components/life360/coordinator.py | 2 ++ homeassistant/components/life360/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/life360/const.py b/homeassistant/components/life360/const.py index d148a06c634..333ce14fbf6 100644 --- a/homeassistant/components/life360/const.py +++ b/homeassistant/components/life360/const.py @@ -3,11 +3,14 @@ from datetime import timedelta import logging +from aiohttp import ClientTimeout + DOMAIN = "life360" LOGGER = logging.getLogger(__package__) ATTRIBUTION = "Data provided by life360.com" -COMM_TIMEOUT = 10 +COMM_MAX_RETRIES = 3 +COMM_TIMEOUT = ClientTimeout(sock_connect=15, total=60) SPEED_FACTOR_MPH = 2.25 SPEED_DIGITS = 1 UPDATE_INTERVAL = timedelta(seconds=10) diff --git a/homeassistant/components/life360/coordinator.py b/homeassistant/components/life360/coordinator.py index 0b9641bfcae..b7121cc7fdb 100644 --- a/homeassistant/components/life360/coordinator.py +++ b/homeassistant/components/life360/coordinator.py @@ -26,6 +26,7 @@ from homeassistant.util.unit_conversion import DistanceConverter from homeassistant.util.unit_system import METRIC_SYSTEM from .const import ( + COMM_MAX_RETRIES, COMM_TIMEOUT, CONF_AUTHORIZATION, DOMAIN, @@ -106,6 +107,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator[Life360Data]): self._api = Life360( session=async_get_clientsession(hass), timeout=COMM_TIMEOUT, + max_retries=COMM_MAX_RETRIES, authorization=entry.data[CONF_AUTHORIZATION], ) self._missing_loc_reason = hass.data[DOMAIN].missing_loc_reason diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index 8f0c44f342b..eb3290e41e1 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/life360", "codeowners": ["@pnbruckner"], - "requirements": ["life360==5.1.1"], + "requirements": ["life360==5.3.0"], "iot_class": "cloud_polling", "loggers": ["life360"] } diff --git a/requirements_all.txt b/requirements_all.txt index 23accfd4b89..79643e571d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1006,7 +1006,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==5.1.1 +life360==5.3.0 # homeassistant.components.osramlightify lightify==1.0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67f2709e1c0..4ab4ae3bd9a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -744,7 +744,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==5.1.1 +life360==5.3.0 # homeassistant.components.logi_circle logi_circle==0.2.3 From 3a60466e7ce19b98f6f5d37d885356519fdb2a16 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Nov 2022 01:09:06 -0600 Subject: [PATCH 069/114] Fix switchbot not becoming available again after unavailable (#81822) * Fix switchbot not becoming available again after unavailable If the advertisment was the same and we were previously marked as unavailable we would not mark the device as available again until the advertisment changed. For lights there is a counter but for the bots there is no counter which means the bots would show unavailable even though they were available again * naming * naming --- .../components/switchbot/coordinator.py | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index ee93c74af37..f68c1effc0c 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -61,6 +61,15 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): self.base_unique_id = base_unique_id self.model = model self._ready_event = asyncio.Event() + self._was_unavailable = True + + @callback + def _async_handle_unavailable( + self, service_info: bluetooth.BluetoothServiceInfoBleak + ) -> None: + """Handle the device going unavailable.""" + super()._async_handle_unavailable(service_info) + self._was_unavailable = True @callback def _async_handle_bluetooth_event( @@ -70,16 +79,20 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): ) -> None: """Handle a Bluetooth event.""" self.ble_device = service_info.device - if adv := switchbot.parse_advertisement_data( - service_info.device, service_info.advertisement + if not ( + adv := switchbot.parse_advertisement_data( + service_info.device, service_info.advertisement + ) ): - if "modelName" in adv.data: - self._ready_event.set() - _LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data) - if not self.device.advertisement_changed(adv): - return - self.data = flatten_sensors_data(adv.data) - self.device.update_from_advertisement(adv) + return + if "modelName" in adv.data: + self._ready_event.set() + _LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data) + if not self.device.advertisement_changed(adv) and not self._was_unavailable: + return + self._was_unavailable = False + self.data = flatten_sensors_data(adv.data) + self.device.update_from_advertisement(adv) super()._async_handle_bluetooth_event(service_info, change) async def async_wait_ready(self) -> bool: From 248ed3660fa7d4b236d458f1d82325db9ad821d1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Nov 2022 12:46:29 +0100 Subject: [PATCH 070/114] Fix statistic_during_period for data with holes (#81847) --- .../components/recorder/statistics.py | 201 +++++++----- .../components/recorder/test_websocket_api.py | 289 +++++++++++++++--- 2 files changed, 375 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 8a744fd4daa..e361249580f 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -1216,11 +1216,29 @@ def _get_max_mean_min_statistic( return result +def _first_statistic( + session: Session, + table: type[Statistics | StatisticsShortTerm], + metadata_id: int, +) -> datetime | None: + """Return the data of the oldest statistic row for a given metadata id.""" + stmt = lambda_stmt( + lambda: select(table.start) + .filter(table.metadata_id == metadata_id) + .order_by(table.start.asc()) + .limit(1) + ) + if stats := execute_stmt_lambda_element(session, stmt): + return process_timestamp(stats[0].start) # type: ignore[no-any-return] + return None + + def _get_oldest_sum_statistic( session: Session, head_start_time: datetime | None, main_start_time: datetime | None, tail_start_time: datetime | None, + oldest_stat: datetime | None, tail_only: bool, metadata_id: int, ) -> float | None: @@ -1231,10 +1249,10 @@ def _get_oldest_sum_statistic( start_time: datetime | None, table: type[Statistics | StatisticsShortTerm], metadata_id: int, - ) -> tuple[float | None, datetime | None]: + ) -> float | None: """Return the oldest non-NULL sum during the period.""" stmt = lambda_stmt( - lambda: select(table.sum, table.start) + lambda: select(table.sum) .filter(table.metadata_id == metadata_id) .filter(table.sum.is_not(None)) .order_by(table.start.asc()) @@ -1248,49 +1266,49 @@ def _get_oldest_sum_statistic( else: period = start_time.replace(minute=0, second=0, microsecond=0) prev_period = period - table.duration - stmt += lambda q: q.filter(table.start == prev_period) + stmt += lambda q: q.filter(table.start >= prev_period) stats = execute_stmt_lambda_element(session, stmt) - return ( - (stats[0].sum, process_timestamp(stats[0].start)) if stats else (None, None) - ) + return stats[0].sum if stats else None - oldest_start: datetime | None oldest_sum: float | None = None - if head_start_time is not None: - oldest_sum, oldest_start = _get_oldest_sum_statistic_in_sub_period( - session, head_start_time, StatisticsShortTerm, metadata_id + # This function won't be called if tail_only is False and main_start_time is None + # the extra checks are added to satisfy MyPy + if not tail_only and main_start_time is not None and oldest_stat is not None: + period = main_start_time.replace(minute=0, second=0, microsecond=0) + prev_period = period - Statistics.duration + if prev_period < oldest_stat: + return 0 + + if ( + head_start_time is not None + and ( + oldest_sum := _get_oldest_sum_statistic_in_sub_period( + session, head_start_time, StatisticsShortTerm, metadata_id + ) ) - if ( - oldest_start is not None - and oldest_start < head_start_time - and oldest_sum is not None - ): - return oldest_sum + is not None + ): + return oldest_sum if not tail_only: - assert main_start_time is not None - oldest_sum, oldest_start = _get_oldest_sum_statistic_in_sub_period( - session, main_start_time, Statistics, metadata_id - ) if ( - oldest_start is not None - and oldest_start < main_start_time - and oldest_sum is not None - ): + oldest_sum := _get_oldest_sum_statistic_in_sub_period( + session, main_start_time, Statistics, metadata_id + ) + ) is not None: return oldest_sum return 0 - if tail_start_time is not None: - oldest_sum, oldest_start = _get_oldest_sum_statistic_in_sub_period( - session, tail_start_time, StatisticsShortTerm, metadata_id + if ( + tail_start_time is not None + and ( + oldest_sum := _get_oldest_sum_statistic_in_sub_period( + session, tail_start_time, StatisticsShortTerm, metadata_id + ) ) - if ( - oldest_start is not None - and oldest_start < tail_start_time - and oldest_sum is not None - ): - return oldest_sum + ) is not None: + return oldest_sum return 0 @@ -1373,51 +1391,79 @@ def statistic_during_period( result: dict[str, Any] = {} - # To calculate the summary, data from the statistics (hourly) and short_term_statistics - # (5 minute) tables is combined - # - The short term statistics table is used for the head and tail of the period, - # if the period it doesn't start or end on a full hour - # - The statistics table is used for the remainder of the time - now = dt_util.utcnow() - if end_time is not None and end_time > now: - end_time = now - - tail_only = ( - start_time is not None - and end_time is not None - and end_time - start_time < timedelta(hours=1) - ) - - # Calculate the head period - head_start_time: datetime | None = None - head_end_time: datetime | None = None - if not tail_only and start_time is not None and start_time.minute: - head_start_time = start_time - head_end_time = start_time.replace( - minute=0, second=0, microsecond=0 - ) + timedelta(hours=1) - - # Calculate the tail period - tail_start_time: datetime | None = None - tail_end_time: datetime | None = None - if end_time is None: - tail_start_time = now.replace(minute=0, second=0, microsecond=0) - elif end_time.minute: - tail_start_time = ( - start_time - if tail_only - else end_time.replace(minute=0, second=0, microsecond=0) - ) - tail_end_time = end_time - - # Calculate the main period - main_start_time: datetime | None = None - main_end_time: datetime | None = None - if not tail_only: - main_start_time = start_time if head_end_time is None else head_end_time - main_end_time = end_time if tail_start_time is None else tail_start_time - with session_scope(hass=hass) as session: + # Fetch metadata for the given statistic_id + if not ( + metadata := get_metadata_with_session(session, statistic_ids=[statistic_id]) + ): + return result + + metadata_id = metadata[statistic_id][0] + + oldest_stat = _first_statistic(session, Statistics, metadata_id) + oldest_5_min_stat = None + if not valid_statistic_id(statistic_id): + oldest_5_min_stat = _first_statistic( + session, StatisticsShortTerm, metadata_id + ) + + # To calculate the summary, data from the statistics (hourly) and + # short_term_statistics (5 minute) tables is combined + # - The short term statistics table is used for the head and tail of the period, + # if the period it doesn't start or end on a full hour + # - The statistics table is used for the remainder of the time + now = dt_util.utcnow() + if end_time is not None and end_time > now: + end_time = now + + tail_only = ( + start_time is not None + and end_time is not None + and end_time - start_time < timedelta(hours=1) + ) + + # Calculate the head period + head_start_time: datetime | None = None + head_end_time: datetime | None = None + if ( + not tail_only + and oldest_stat is not None + and oldest_5_min_stat is not None + and oldest_5_min_stat - oldest_stat < timedelta(hours=1) + and (start_time is None or start_time < oldest_5_min_stat) + ): + # To improve accuracy of averaged for statistics which were added within + # recorder's retention period. + head_start_time = oldest_5_min_stat + head_end_time = oldest_5_min_stat.replace( + minute=0, second=0, microsecond=0 + ) + timedelta(hours=1) + elif not tail_only and start_time is not None and start_time.minute: + head_start_time = start_time + head_end_time = start_time.replace( + minute=0, second=0, microsecond=0 + ) + timedelta(hours=1) + + # Calculate the tail period + tail_start_time: datetime | None = None + tail_end_time: datetime | None = None + if end_time is None: + tail_start_time = now.replace(minute=0, second=0, microsecond=0) + elif end_time.minute: + tail_start_time = ( + start_time + if tail_only + else end_time.replace(minute=0, second=0, microsecond=0) + ) + tail_end_time = end_time + + # Calculate the main period + main_start_time: datetime | None = None + main_end_time: datetime | None = None + if not tail_only: + main_start_time = start_time if head_end_time is None else head_end_time + main_end_time = end_time if tail_start_time is None else tail_start_time + # Fetch metadata for the given statistic_id metadata = get_metadata_with_session(session, statistic_ids=[statistic_id]) if not metadata: @@ -1449,6 +1495,7 @@ def statistic_during_period( head_start_time, main_start_time, tail_start_time, + oldest_stat, tail_only, metadata_id, ) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 00e9d0d35b4..423ab4bbc52 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -182,7 +182,8 @@ async def test_statistics_during_period(recorder_mock, hass, hass_ws_client): @freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.timezone.utc)) -async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): +@pytest.mark.parametrize("offset", (0, 1, 2)) +async def test_statistic_during_period(recorder_mock, hass, hass_ws_client, offset): """Test statistic_during_period.""" id = 1 @@ -197,7 +198,9 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): client = await hass_ws_client() zero = now - start = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=-3) + start = zero.replace(minute=offset * 5, second=0, microsecond=0) + timedelta( + hours=-3 + ) imported_stats_5min = [ { @@ -209,22 +212,37 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): } for i in range(0, 39) ] - imported_stats = [ + imported_stats = [] + slice_end = 12 - offset + imported_stats.append( { - "start": imported_stats_5min[i * 12]["start"], - "max": max( - stat["max"] for stat in imported_stats_5min[i * 12 : (i + 1) * 12] - ), - "mean": fmean( - stat["mean"] for stat in imported_stats_5min[i * 12 : (i + 1) * 12] - ), - "min": min( - stat["min"] for stat in imported_stats_5min[i * 12 : (i + 1) * 12] - ), - "sum": imported_stats_5min[i * 12 + 11]["sum"], + "start": imported_stats_5min[0]["start"].replace(minute=0), + "max": max(stat["max"] for stat in imported_stats_5min[0:slice_end]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[0:slice_end]), + "min": min(stat["min"] for stat in imported_stats_5min[0:slice_end]), + "sum": imported_stats_5min[slice_end - 1]["sum"], } - for i in range(0, 3) - ] + ) + for i in range(0, 2): + slice_start = i * 12 + (12 - offset) + slice_end = (i + 1) * 12 + (12 - offset) + assert imported_stats_5min[slice_start]["start"].minute == 0 + imported_stats.append( + { + "start": imported_stats_5min[slice_start]["start"], + "max": max( + stat["max"] for stat in imported_stats_5min[slice_start:slice_end] + ), + "mean": fmean( + stat["mean"] for stat in imported_stats_5min[slice_start:slice_end] + ), + "min": min( + stat["min"] for stat in imported_stats_5min[slice_start:slice_end] + ), + "sum": imported_stats_5min[slice_end - 1]["sum"], + } + ) + imported_metadata = { "has_mean": False, "has_sum": True, @@ -285,8 +303,14 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): } # This should also include imported_statistics_5min[:] - start_time = "2022-10-21T04:00:00+00:00" - end_time = "2022-10-21T07:15:00+00:00" + start_time = ( + dt_util.parse_datetime("2022-10-21T04:00:00+00:00") + + timedelta(minutes=5 * offset) + ).isoformat() + end_time = ( + dt_util.parse_datetime("2022-10-21T07:15:00+00:00") + + timedelta(minutes=5 * offset) + ).isoformat() await client.send_json( { "id": next_id(), @@ -308,8 +332,14 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): } # This should also include imported_statistics_5min[:] - start_time = "2022-10-20T04:00:00+00:00" - end_time = "2022-10-21T08:20:00+00:00" + start_time = ( + dt_util.parse_datetime("2022-10-21T04:00:00+00:00") + + timedelta(minutes=5 * offset) + ).isoformat() + end_time = ( + dt_util.parse_datetime("2022-10-21T08:20:00+00:00") + + timedelta(minutes=5 * offset) + ).isoformat() await client.send_json( { "id": next_id(), @@ -331,7 +361,10 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): } # This should include imported_statistics_5min[26:] - start_time = "2022-10-21T06:10:00+00:00" + start_time = ( + dt_util.parse_datetime("2022-10-21T06:10:00+00:00") + + timedelta(minutes=5 * offset) + ).isoformat() assert imported_stats_5min[26]["start"].isoformat() == start_time await client.send_json( { @@ -353,7 +386,10 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): } # This should also include imported_statistics_5min[26:] - start_time = "2022-10-21T06:09:00+00:00" + start_time = ( + dt_util.parse_datetime("2022-10-21T06:09:00+00:00") + + timedelta(minutes=5 * offset) + ).isoformat() await client.send_json( { "id": next_id(), @@ -374,7 +410,10 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): } # This should include imported_statistics_5min[:26] - end_time = "2022-10-21T06:10:00+00:00" + end_time = ( + dt_util.parse_datetime("2022-10-21T06:10:00+00:00") + + timedelta(minutes=5 * offset) + ).isoformat() assert imported_stats_5min[26]["start"].isoformat() == end_time await client.send_json( { @@ -396,9 +435,15 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): } # This should include imported_statistics_5min[26:32] (less than a full hour) - start_time = "2022-10-21T06:10:00+00:00" + start_time = ( + dt_util.parse_datetime("2022-10-21T06:10:00+00:00") + + timedelta(minutes=5 * offset) + ).isoformat() assert imported_stats_5min[26]["start"].isoformat() == start_time - end_time = "2022-10-21T06:40:00+00:00" + end_time = ( + dt_util.parse_datetime("2022-10-21T06:40:00+00:00") + + timedelta(minutes=5 * offset) + ).isoformat() assert imported_stats_5min[32]["start"].isoformat() == end_time await client.send_json( { @@ -422,7 +467,7 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): # This should include imported_statistics[2:] + imported_statistics_5min[36:] start_time = "2022-10-21T06:00:00+00:00" - assert imported_stats_5min[24]["start"].isoformat() == start_time + assert imported_stats_5min[24 - offset]["start"].isoformat() == start_time assert imported_stats[2]["start"].isoformat() == start_time await client.send_json( { @@ -437,10 +482,11 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): response = await client.receive_json() assert response["success"] assert response["result"] == { - "max": max(stat["max"] for stat in imported_stats_5min[24:]), - "mean": fmean(stat["mean"] for stat in imported_stats_5min[24:]), - "min": min(stat["min"] for stat in imported_stats_5min[24:]), - "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[23]["sum"], + "max": max(stat["max"] for stat in imported_stats_5min[24 - offset :]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[24 - offset :]), + "min": min(stat["min"] for stat in imported_stats_5min[24 - offset :]), + "change": imported_stats_5min[-1]["sum"] + - imported_stats_5min[23 - offset]["sum"], } # This should also include imported_statistics[2:] + imported_statistics_5min[36:] @@ -457,10 +503,11 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): response = await client.receive_json() assert response["success"] assert response["result"] == { - "max": max(stat["max"] for stat in imported_stats_5min[24:]), - "mean": fmean(stat["mean"] for stat in imported_stats_5min[24:]), - "min": min(stat["min"] for stat in imported_stats_5min[24:]), - "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[23]["sum"], + "max": max(stat["max"] for stat in imported_stats_5min[24 - offset :]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[24 - offset :]), + "min": min(stat["min"] for stat in imported_stats_5min[24 - offset :]), + "change": imported_stats_5min[-1]["sum"] + - imported_stats_5min[23 - offset]["sum"], } # This should include imported_statistics[2:3] @@ -477,11 +524,16 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): ) response = await client.receive_json() assert response["success"] + slice_start = 24 - offset + slice_end = 36 - offset assert response["result"] == { - "max": max(stat["max"] for stat in imported_stats_5min[24:36]), - "mean": fmean(stat["mean"] for stat in imported_stats_5min[24:36]), - "min": min(stat["min"] for stat in imported_stats_5min[24:36]), - "change": imported_stats_5min[35]["sum"] - imported_stats_5min[23]["sum"], + "max": max(stat["max"] for stat in imported_stats_5min[slice_start:slice_end]), + "mean": fmean( + stat["mean"] for stat in imported_stats_5min[slice_start:slice_end] + ), + "min": min(stat["min"] for stat in imported_stats_5min[slice_start:slice_end]), + "change": imported_stats_5min[slice_end - 1]["sum"] + - imported_stats_5min[slice_start - 1]["sum"], } # Test we can get only selected types @@ -539,6 +591,167 @@ async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): } +@freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.timezone.utc)) +async def test_statistic_during_period_hole(recorder_mock, hass, hass_ws_client): + """Test statistic_during_period when there are holes in the data.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + now = dt_util.utcnow() + + await async_recorder_block_till_done(hass) + client = await hass_ws_client() + + zero = now + start = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=-18) + + imported_stats = [ + { + "start": (start + timedelta(hours=3 * i)), + "max": i * 2, + "mean": i, + "min": -76 + i * 2, + "sum": i, + } + for i in range(0, 6) + ] + + imported_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "recorder", + "statistic_id": "sensor.test", + "unit_of_measurement": "kWh", + } + + recorder.get_instance(hass).async_import_statistics( + imported_metadata, + imported_stats, + Statistics, + ) + await async_wait_recording_done(hass) + + # This should include imported_stats[:] + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats[:]), + "mean": fmean(stat["mean"] for stat in imported_stats[:]), + "min": min(stat["min"] for stat in imported_stats[:]), + "change": imported_stats[-1]["sum"] - imported_stats[0]["sum"], + } + + # This should also include imported_stats[:] + start_time = "2022-10-20T13:00:00+00:00" + end_time = "2022-10-21T05:00:00+00:00" + assert imported_stats[0]["start"].isoformat() == start_time + assert imported_stats[-1]["start"].isoformat() < end_time + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + "fixed_period": { + "start_time": start_time, + "end_time": end_time, + }, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats[:]), + "mean": fmean(stat["mean"] for stat in imported_stats[:]), + "min": min(stat["min"] for stat in imported_stats[:]), + "change": imported_stats[-1]["sum"] - imported_stats[0]["sum"], + } + + # This should also include imported_stats[:] + start_time = "2022-10-20T13:00:00+00:00" + end_time = "2022-10-21T08:20:00+00:00" + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + "fixed_period": { + "start_time": start_time, + "end_time": end_time, + }, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats[:]), + "mean": fmean(stat["mean"] for stat in imported_stats[:]), + "min": min(stat["min"] for stat in imported_stats[:]), + "change": imported_stats[-1]["sum"] - imported_stats[0]["sum"], + } + + # This should include imported_stats[1:4] + start_time = "2022-10-20T16:00:00+00:00" + end_time = "2022-10-20T23:00:00+00:00" + assert imported_stats[1]["start"].isoformat() == start_time + assert imported_stats[3]["start"].isoformat() < end_time + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + "fixed_period": { + "start_time": start_time, + "end_time": end_time, + }, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats[1:4]), + "mean": fmean(stat["mean"] for stat in imported_stats[1:4]), + "min": min(stat["min"] for stat in imported_stats[1:4]), + "change": imported_stats[3]["sum"] - imported_stats[1]["sum"], + } + + # This should also include imported_stats[1:4] + start_time = "2022-10-20T15:00:00+00:00" + end_time = "2022-10-21T00:00:00+00:00" + assert imported_stats[1]["start"].isoformat() > start_time + assert imported_stats[3]["start"].isoformat() < end_time + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + "fixed_period": { + "start_time": start_time, + "end_time": end_time, + }, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats[1:4]), + "mean": fmean(stat["mean"] for stat in imported_stats[1:4]), + "min": min(stat["min"] for stat in imported_stats[1:4]), + "change": imported_stats[3]["sum"] - imported_stats[1]["sum"], + } + + @freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.timezone.utc)) @pytest.mark.parametrize( "calendar_period, start_time, end_time", From 1ecb7ab88751e3d1edc86c3ac62c4922eaee0edb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 11 Nov 2022 17:08:26 +0100 Subject: [PATCH 071/114] Fix rest schema (#81857) --- homeassistant/components/rest/schema.py | 3 +-- tests/components/rest/test_init.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index d124ce5789c..cfd8f8a3852 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -91,9 +91,8 @@ COMBINED_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - # convert empty dict to empty list - lambda x: [] if x == {} else x, cv.ensure_list, + cv.remove_falsy, [COMBINED_SCHEMA], ) }, diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index 6f35301b1c7..0c1670315fc 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -418,3 +418,19 @@ async def test_empty_config(hass: HomeAssistant) -> None: {DOMAIN: {}}, ) assert_setup_component(0, DOMAIN) + + +async def test_config_schema_via_packages(hass: HomeAssistant) -> None: + """Test configuration via packages.""" + packages = { + "pack_dict": {"rest": {}}, + "pack_11": {"rest": {"resource": "http://url1"}}, + "pack_list": {"rest": [{"resource": "http://url2"}]}, + } + config = {hass_config.CONF_CORE: {hass_config.CONF_PACKAGES: packages}} + await hass_config.merge_packages_config(hass, config, packages) + + assert len(config) == 2 + assert len(config["rest"]) == 2 + assert config["rest"][0]["resource"] == "http://url1" + assert config["rest"][1]["resource"] == "http://url2" From 930dc3615e8b8dcd37b76ebb7e09e85ffb970eb8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Nov 2022 10:01:12 -0600 Subject: [PATCH 072/114] Bump aiohomekit to 2.2.19 (#81867) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index b18f35390b7..f0438a7b841 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.18"], + "requirements": ["aiohomekit==2.2.19"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 79643e571d5..f2531e17d91 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.18 +aiohomekit==2.2.19 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ab4ae3bd9a..c013403b65e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.18 +aiohomekit==2.2.19 # homeassistant.components.emulated_hue # homeassistant.components.http From c8177f48cea2064733e5f67c274fa7f85d3646ae Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 9 Nov 2022 15:27:36 +0000 Subject: [PATCH 073/114] Fix homekit_controller climate entity not becoming active when changing modes (#81868) --- .../components/homekit_controller/climate.py | 1 + .../homekit_controller/test_climate.py | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index de42243a6bb..41e88725121 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -209,6 +209,7 @@ class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity): ) await self.async_put_characteristics( { + CharacteristicsTypes.ACTIVE: ActivationStateValues.ACTIVE, CharacteristicsTypes.TARGET_HEATER_COOLER_STATE: TARGET_HEATER_COOLER_STATE_HASS_TO_HOMEKIT[ hvac_mode ], diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 0f10f0f9fa0..bf544c5aff4 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -760,6 +760,42 @@ async def test_heater_cooler_change_thermostat_state(hass, utcnow): ) +async def test_can_turn_on_after_off(hass, utcnow): + """ + Test that we always force device from inactive to active when setting mode. + + This is a regression test for #81863. + """ + helper = await setup_test_component(hass, create_heater_cooler_service) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVACMode.OFF}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.ACTIVE: ActivationStateValues.INACTIVE, + }, + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVACMode.HEAT}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.ACTIVE: ActivationStateValues.ACTIVE, + CharacteristicsTypes.TARGET_HEATER_COOLER_STATE: TargetHeaterCoolerStateValues.HEAT, + }, + ) + + async def test_heater_cooler_change_thermostat_temperature(hass, utcnow): """Test that we can change the target temperature.""" helper = await setup_test_component(hass, create_heater_cooler_service) From 5488e9d5f3cf47ba37bdef8acdc62d84720074e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Nov 2022 10:21:31 -0600 Subject: [PATCH 074/114] Bump oralb-ble to 0.14.1 (#81869) --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index 1738558770e..eff6c999c30 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.13.0"], + "requirements": ["oralb-ble==0.14.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index f2531e17d91..acf76565c6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1241,7 +1241,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.13.0 +oralb-ble==0.14.1 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c013403b65e..ddc2d2f78dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -886,7 +886,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.13.0 +oralb-ble==0.14.1 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 70b360b1f8b3efa1b2a5e3eb7707c866160d77ad Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 9 Nov 2022 19:34:31 -0800 Subject: [PATCH 075/114] Bump gcal_sync to 4.0.1 to fix Google Calendar config flow (#81873) Bump gcal_sync to 4.0.1 This reverts test chagnes from PR #81562 that were actually incorrect given the calendar "get" API returns less information that the "CalendarList" api. --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/google/test_config_flow.py | 2 +- tests/components/google/test_init.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 9fc265fa287..2bc84827cd6 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", - "requirements": ["gcal-sync==4.0.0", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==4.0.1", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index acf76565c6f..9ee94199fa4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -725,7 +725,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==4.0.0 +gcal-sync==4.0.1 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ddc2d2f78dd..b6c0fc50c20 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -541,7 +541,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==4.0.0 +gcal-sync==4.0.1 # homeassistant.components.geocaching geocachingapi==0.2.1 diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index bce3f4855c7..d8ddd6fe588 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -104,7 +104,7 @@ async def primary_calendar( """Fixture to return the primary calendar.""" mock_calendar_get( "primary", - {"id": primary_calendar_email, "summary": "Personal", "accessRole": "owner"}, + {"id": primary_calendar_email, "summary": "Personal"}, exc=primary_calendar_error, ) diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index a2f16f778fd..5e7696eec68 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -768,7 +768,7 @@ async def test_assign_unique_id( mock_calendar_get( "primary", - {"id": EMAIL_ADDRESS, "summary": "Personal", "accessRole": "reader"}, + {"id": EMAIL_ADDRESS, "summary": "Personal"}, ) mock_calendars_list({"items": [test_api_calendar]}) From 082d4079ef64ac4116dcff198d6d9060b72c557f Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 13 Nov 2022 08:30:16 -0500 Subject: [PATCH 076/114] Fix ZHA configuration APIs (#81874) * Fix ZHA configuration loading and saving issues * add tests --- homeassistant/components/zha/api.py | 10 +- homeassistant/components/zha/core/helpers.py | 4 +- tests/components/zha/data.py | 153 +++++++++++++++++++ tests/components/zha/test_api.py | 75 +++++++++ 4 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 tests/components/zha/data.py diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index c68136c23da..eb5fc2e4343 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -1090,11 +1090,17 @@ async def websocket_update_zha_configuration( ): data_to_save[CUSTOM_CONFIGURATION][section].pop(entry) # remove entire section block if empty - if not data_to_save[CUSTOM_CONFIGURATION][section]: + if ( + not data_to_save[CUSTOM_CONFIGURATION].get(section) + and section in data_to_save[CUSTOM_CONFIGURATION] + ): data_to_save[CUSTOM_CONFIGURATION].pop(section) # remove entire custom_configuration block if empty - if not data_to_save[CUSTOM_CONFIGURATION]: + if ( + not data_to_save.get(CUSTOM_CONFIGURATION) + and CUSTOM_CONFIGURATION in data_to_save + ): data_to_save.pop(CUSTOM_CONFIGURATION) _LOGGER.info( diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 1ea9a2a4c9b..2bc7d53fd79 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -221,11 +221,13 @@ def async_get_zha_config_value( ) -def async_cluster_exists(hass, cluster_id): +def async_cluster_exists(hass, cluster_id, skip_coordinator=True): """Determine if a device containing the specified in cluster is paired.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] zha_devices = zha_gateway.devices.values() for zha_device in zha_devices: + if skip_coordinator and zha_device.is_coordinator: + continue clusters_by_endpoint = zha_device.async_get_clusters() for clusters in clusters_by_endpoint.values(): if ( diff --git a/tests/components/zha/data.py b/tests/components/zha/data.py new file mode 100644 index 00000000000..8b613ec2971 --- /dev/null +++ b/tests/components/zha/data.py @@ -0,0 +1,153 @@ +"""Test data for ZHA API tests.""" + +BASE_CUSTOM_CONFIGURATION = { + "schemas": { + "zha_options": [ + { + "type": "integer", + "valueMin": 0, + "name": "default_light_transition", + "optional": True, + "default": 0, + }, + { + "type": "boolean", + "name": "enhanced_light_transition", + "required": True, + "default": False, + }, + { + "type": "boolean", + "name": "light_transitioning_flag", + "required": True, + "default": True, + }, + { + "type": "boolean", + "name": "always_prefer_xy_color_mode", + "required": True, + "default": True, + }, + { + "type": "boolean", + "name": "enable_identify_on_join", + "required": True, + "default": True, + }, + { + "type": "integer", + "valueMin": 0, + "name": "consider_unavailable_mains", + "optional": True, + "default": 7200, + }, + { + "type": "integer", + "valueMin": 0, + "name": "consider_unavailable_battery", + "optional": True, + "default": 21600, + }, + ] + }, + "data": { + "zha_options": { + "enhanced_light_transition": True, + "default_light_transition": 0, + "light_transitioning_flag": True, + "always_prefer_xy_color_mode": True, + "enable_identify_on_join": True, + "consider_unavailable_mains": 7200, + "consider_unavailable_battery": 21600, + } + }, +} + +CONFIG_WITH_ALARM_OPTIONS = { + "schemas": { + "zha_options": [ + { + "type": "integer", + "valueMin": 0, + "name": "default_light_transition", + "optional": True, + "default": 0, + }, + { + "type": "boolean", + "name": "enhanced_light_transition", + "required": True, + "default": False, + }, + { + "type": "boolean", + "name": "light_transitioning_flag", + "required": True, + "default": True, + }, + { + "type": "boolean", + "name": "always_prefer_xy_color_mode", + "required": True, + "default": True, + }, + { + "type": "boolean", + "name": "enable_identify_on_join", + "required": True, + "default": True, + }, + { + "type": "integer", + "valueMin": 0, + "name": "consider_unavailable_mains", + "optional": True, + "default": 7200, + }, + { + "type": "integer", + "valueMin": 0, + "name": "consider_unavailable_battery", + "optional": True, + "default": 21600, + }, + ], + "zha_alarm_options": [ + { + "type": "string", + "name": "alarm_master_code", + "required": True, + "default": "1234", + }, + { + "type": "integer", + "valueMin": 0, + "name": "alarm_failed_tries", + "required": True, + "default": 3, + }, + { + "type": "boolean", + "name": "alarm_arm_requires_code", + "required": True, + "default": False, + }, + ], + }, + "data": { + "zha_options": { + "enhanced_light_transition": True, + "default_light_transition": 0, + "light_transitioning_flag": True, + "always_prefer_xy_color_mode": True, + "enable_identify_on_join": True, + "consider_unavailable_mains": 7200, + "consider_unavailable_battery": 21600, + }, + "zha_alarm_options": { + "alarm_arm_requires_code": False, + "alarm_master_code": "4321", + "alarm_failed_tries": 2, + }, + }, +} diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index e4daf7f365e..defc9842b01 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -1,5 +1,6 @@ """Test ZHA API.""" from binascii import unhexlify +from copy import deepcopy from unittest.mock import AsyncMock, patch import pytest @@ -8,6 +9,7 @@ import zigpy.backups import zigpy.profiles.zha import zigpy.types import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.security as security from homeassistant.components.websocket_api import const from homeassistant.components.zha import DOMAIN @@ -50,6 +52,7 @@ from .conftest import ( SIG_EP_PROFILE, SIG_EP_TYPE, ) +from .data import BASE_CUSTOM_CONFIGURATION, CONFIG_WITH_ALARM_OPTIONS IEEE_SWITCH_DEVICE = "01:2d:6f:00:0a:90:69:e7" IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" @@ -61,6 +64,7 @@ def required_platform_only(): with patch( "homeassistant.components.zha.PLATFORMS", ( + Platform.ALARM_CONTROL_PANEL, Platform.SELECT, Platform.SENSOR, Platform.SWITCH, @@ -89,6 +93,25 @@ async def device_switch(hass, zigpy_device_mock, zha_device_joined): return zha_device +@pytest.fixture +async def device_ias_ace(hass, zigpy_device_mock, zha_device_joined): + """Test alarm control panel device.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [security.IasAce.cluster_id], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.IAS_ANCILLARY_CONTROL, + SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, + } + }, + ) + zha_device = await zha_device_joined(zigpy_device) + zha_device.available = True + return zha_device + + @pytest.fixture async def device_groupable(hass, zigpy_device_mock, zha_device_joined): """Test zha light platform.""" @@ -225,6 +248,58 @@ async def test_list_devices(zha_client): assert device == device2 +async def test_get_zha_config(zha_client): + """Test getting zha custom configuration.""" + await zha_client.send_json({ID: 5, TYPE: "zha/configuration"}) + + msg = await zha_client.receive_json() + + configuration = msg["result"] + assert configuration == BASE_CUSTOM_CONFIGURATION + + +async def test_get_zha_config_with_alarm(hass, zha_client, device_ias_ace): + """Test getting zha custom configuration.""" + await zha_client.send_json({ID: 5, TYPE: "zha/configuration"}) + + msg = await zha_client.receive_json() + + configuration = msg["result"] + assert configuration == CONFIG_WITH_ALARM_OPTIONS + + # test that the alarm options are not in the config when we remove the device + device_ias_ace.gateway.device_removed(device_ias_ace.device) + await hass.async_block_till_done() + await zha_client.send_json({ID: 6, TYPE: "zha/configuration"}) + + msg = await zha_client.receive_json() + + configuration = msg["result"] + assert configuration == BASE_CUSTOM_CONFIGURATION + + +async def test_update_zha_config(zha_client, zigpy_app_controller): + """Test updating zha custom configuration.""" + + configuration = deepcopy(CONFIG_WITH_ALARM_OPTIONS) + configuration["data"]["zha_options"]["default_light_transition"] = 10 + + with patch( + "bellows.zigbee.application.ControllerApplication.new", + return_value=zigpy_app_controller, + ): + await zha_client.send_json( + {ID: 5, TYPE: "zha/configuration/update", "data": configuration["data"]} + ) + msg = await zha_client.receive_json() + assert msg["success"] + + await zha_client.send_json({ID: 6, TYPE: "zha/configuration"}) + msg = await zha_client.receive_json() + configuration = msg["result"] + assert configuration == configuration + + async def test_device_not_found(zha_client): """Test not found response from get device API.""" await zha_client.send_json( From 223d864b04368f252c42e7d3eeda16da0e50f261 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 10 Nov 2022 08:31:28 -0800 Subject: [PATCH 077/114] Revert google calendar back to old API for free/busy readers (#81894) * Revert google calendar back to old API for free/busy readers * Update homeassistant/components/google/calendar.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/google/calendar.py | 10 ++++++++-- tests/components/google/conftest.py | 14 +++++++++++--- tests/components/google/test_calendar.py | 21 ++++++++++++++++----- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 4eb57cff49c..eff26c2fbc4 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -10,7 +10,7 @@ from typing import Any from gcal_sync.api import GoogleCalendarService, ListEventsRequest, SyncEventsRequest from gcal_sync.exceptions import ApiException -from gcal_sync.model import DateOrDatetime, Event +from gcal_sync.model import AccessRole, DateOrDatetime, Event from gcal_sync.store import ScopedCalendarStore from gcal_sync.sync import CalendarEventSyncManager from gcal_sync.timeline import Timeline @@ -198,7 +198,13 @@ async def async_setup_entry( entity_entry.entity_id, ) coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator - if search := data.get(CONF_SEARCH): + # Prefer calendar sync down of resources when possible. However, sync does not work + # for search. Also free-busy calendars denormalize recurring events as individual + # events which is not efficient for sync + if ( + search := data.get(CONF_SEARCH) + or calendar_item.access_role == AccessRole.FREE_BUSY_READER + ): coordinator = CalendarQueryUpdateCoordinator( hass, calendar_service, diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 2f5efd829bf..ad27e971ece 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -47,7 +47,6 @@ TEST_API_CALENDAR = { "id": CALENDAR_ID, "etag": '"3584134138943410"', "timeZone": "UTC", - "accessRole": "reader", "foregroundColor": "#000000", "selected": True, "kind": "calendar#calendarListEntry", @@ -62,10 +61,19 @@ CLIENT_ID = "client-id" CLIENT_SECRET = "client-secret" +@pytest.fixture(name="calendar_access_role") +def test_calendar_access_role() -> str: + """Default access role to use for test_api_calendar in tests.""" + return "reader" + + @pytest.fixture -def test_api_calendar(): +def test_api_calendar(calendar_access_role: str): """Return a test calendar object used in API responses.""" - return TEST_API_CALENDAR + return { + **TEST_API_CALENDAR, + "accessRole": calendar_access_role, + } @pytest.fixture diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 3bd584f4c6f..c813bd55782 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -60,6 +60,14 @@ TEST_EVENT = { } +@pytest.fixture( + autouse=True, scope="module", params=["reader", "owner", "freeBusyReader"] +) +def calendar_access_role(request) -> str: + """Fixture to exercise access roles in tests.""" + return request.param + + @pytest.fixture(autouse=True) def mock_test_setup( hass, @@ -724,12 +732,15 @@ async def test_invalid_unique_id_cleanup( @pytest.mark.parametrize( - "time_zone,event_order", + "time_zone,event_order,calendar_access_role", + # This only tests the reader role to force testing against the local + # database filtering based on start/end time. (free busy reader would + # just use the API response which this test is not exercising) [ - ("America/Los_Angeles", ["One", "Two", "All Day Event"]), - ("America/Regina", ["One", "Two", "All Day Event"]), - ("UTC", ["One", "All Day Event", "Two"]), - ("Asia/Tokyo", ["All Day Event", "One", "Two"]), + ("America/Los_Angeles", ["One", "Two", "All Day Event"], "reader"), + ("America/Regina", ["One", "Two", "All Day Event"], "reader"), + ("UTC", ["One", "All Day Event", "Two"], "reader"), + ("Asia/Tokyo", ["All Day Event", "One", "Two"], "reader"), ], ) async def test_all_day_iter_order( From 0d62d800388272bf62953f2c94de9836bcc743f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Nov 2022 14:14:37 -0600 Subject: [PATCH 078/114] Fix bluetooth adapters with missing firmware patch files not being discovered (#81926) --- .../components/bluetooth/__init__.py | 25 +++++- homeassistant/components/bluetooth/const.py | 9 +++ tests/components/bluetooth/test_init.py | 77 +++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 1d0b8824fb5..8590d1ad90a 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from asyncio import Future from collections.abc import Callable, Iterable +import datetime import logging import platform from typing import TYPE_CHECKING, cast @@ -21,6 +22,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_ca from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, discovery_flow from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.issue_registry import ( IssueSeverity, async_create_issue, @@ -33,6 +35,7 @@ from .const import ( ADAPTER_ADDRESS, ADAPTER_HW_VERSION, ADAPTER_SW_VERSION, + BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, CONF_ADAPTER, CONF_DETAILS, CONF_PASSIVE, @@ -40,6 +43,7 @@ from .const import ( DEFAULT_ADDRESS, DOMAIN, FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, + LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS, SOURCE_LOCAL, AdapterDetails, ) @@ -298,9 +302,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await async_discover_adapters(hass, discovered_adapters) discovery_debouncer = Debouncer( - hass, _LOGGER, cooldown=5, immediate=False, function=_async_rediscover_adapters + hass, + _LOGGER, + cooldown=BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, + immediate=False, + function=_async_rediscover_adapters, ) + async def _async_call_debouncer(now: datetime.datetime) -> None: + """Call the debouncer at a later time.""" + await discovery_debouncer.async_call() + def _async_trigger_discovery() -> None: # There are so many bluetooth adapter models that # we check the bus whenever a usb device is plugged in @@ -310,6 +322,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # present. _LOGGER.debug("Triggering bluetooth usb discovery") hass.async_create_task(discovery_debouncer.async_call()) + # Because it can take 120s for the firmware loader + # fallback to timeout we need to wait that plus + # the debounce time to ensure we do not miss the + # adapter becoming available to DBus since otherwise + # we will never see the new adapter until + # Home Assistant is restarted + async_call_later( + hass, + BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS + LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS, + _async_call_debouncer, + ) cancel = usb.async_register_scan_request_callback(hass, _async_trigger_discovery) hass.bus.async_listen_once( diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index 6d6751f6ac4..038c2b1988f 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -59,6 +59,15 @@ SCANNER_WATCHDOG_TIMEOUT: Final = 90 SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=30) +# When the linux kernel is configured with +# CONFIG_FW_LOADER_USER_HELPER_FALLBACK it +# can take up to 120s before the USB device +# is available if the firmware files +# are not present +LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS = 120 +BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS = 5 + + class AdapterDetails(TypedDict, total=False): """Adapter details.""" diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index c9a5e6c78a7..5a5437af71a 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -20,9 +20,11 @@ from homeassistant.components.bluetooth import ( scanner, ) from homeassistant.components.bluetooth.const import ( + BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, CONF_PASSIVE, DEFAULT_ADDRESS, DOMAIN, + LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS, SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, ) @@ -2737,6 +2739,81 @@ async def test_discover_new_usb_adapters(hass, mock_bleak_scanner_start, one_ada assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 1 +async def test_discover_new_usb_adapters_with_firmware_fallback_delay( + hass, mock_bleak_scanner_start, one_adapter +): + """Test we can discover new usb adapters with a firmware fallback delay.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01" + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_scan_request_callback(_hass, _callback): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.usb.async_register_scan_request_callback", + _async_register_scan_request_callback, + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + + assert not hass.config_entries.flow.async_progress(DOMAIN) + + saved_callback() + assert not hass.config_entries.flow.async_progress(DOMAIN) + + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.get_bluetooth_adapter_details", + return_value={}, + ): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS * 2) + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 0 + + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.get_bluetooth_adapter_details", + return_value={ + "hci0": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:01", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + "hci1": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:02", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + }, + ): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta( + seconds=LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS + + (BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS * 2) + ), + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 1 + + async def test_issue_outdated_haos( hass, mock_bleak_scanner_start, one_adapter, operating_system_85 ): From f24549f7d167185054a2d5800bba7760b4821ab4 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 10 Nov 2022 14:32:49 -0700 Subject: [PATCH 079/114] Bump aioridwell to 2022.11.0 (#81929) --- homeassistant/components/ridwell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ridwell/manifest.json b/homeassistant/components/ridwell/manifest.json index aec0faf5dd3..785457a57e0 100644 --- a/homeassistant/components/ridwell/manifest.json +++ b/homeassistant/components/ridwell/manifest.json @@ -3,7 +3,7 @@ "name": "Ridwell", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ridwell", - "requirements": ["aioridwell==2022.03.0"], + "requirements": ["aioridwell==2022.11.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["aioridwell"], diff --git a/requirements_all.txt b/requirements_all.txt index 9ee94199fa4..858194c739a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -246,7 +246,7 @@ aioqsw==0.2.2 aiorecollect==1.0.8 # homeassistant.components.ridwell -aioridwell==2022.03.0 +aioridwell==2022.11.0 # homeassistant.components.senseme aiosenseme==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6c0fc50c20..3c6ab32dc73 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -221,7 +221,7 @@ aioqsw==0.2.2 aiorecollect==1.0.8 # homeassistant.components.ridwell -aioridwell==2022.03.0 +aioridwell==2022.11.0 # homeassistant.components.senseme aiosenseme==0.6.1 From a9d461a109ae49bdf490ad5840da6872b1362464 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Nov 2022 02:09:28 -0600 Subject: [PATCH 080/114] Fix esphome bleak client seeing disconnects too late (#81932) * Fix esphome bleak client seeing disconnects too late Because allbacks are delivered asynchronously its possible that we find out during the operation before the callback is delivered telling us about the disconnected. We now watch for error code -1 which indicates the connection dropped out from under us * debug logging * cleanup comment * Fix comment grammar Co-authored-by: Martin Hjelmare --- .../components/esphome/bluetooth/client.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index c6b60831577..ceac4e5aaae 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -13,6 +13,7 @@ from aioesphomeapi import ( BLEConnectionError, ) from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError +from aioesphomeapi.core import BluetoothGATTAPIError import async_timeout from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.client import BaseBleakClient, NotifyCallback @@ -83,6 +84,24 @@ def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType: return await func(self, *args, **kwargs) except TimeoutAPIError as err: raise asyncio.TimeoutError(str(err)) from err + except BluetoothGATTAPIError as ex: + # If the device disconnects in the middle of an operation + # be sure to mark it as disconnected so any library using + # the proxy knows to reconnect. + # + # Because callbacks are delivered asynchronously it's possible + # that we find out about the disconnection during the operation + # before the callback is delivered. + if ex.error.error == -1: + _LOGGER.debug( + "%s: %s - %s: BLE device disconnected during %s operation", + self._source, # pylint: disable=protected-access + self._ble_device.name, # pylint: disable=protected-access + self._ble_device.address, # pylint: disable=protected-access + func.__name__, + ) + self._async_ble_device_disconnected() # pylint: disable=protected-access + raise BleakError(str(ex)) from ex except APIConnectionError as err: raise BleakError(str(err)) from err From 97929bd234543797701c01707ef934aaa9ce9764 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Nov 2022 01:51:58 -0600 Subject: [PATCH 081/114] Bump bleak-retry-connector to 2.8.4 (#81937) changelog: https://github.com/Bluetooth-Devices/bleak-retry-connector/compare/v2.8.3...v2.8.4 reduces the risk that the operation will fail because the adapter temporarily runs out of connection slots --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 2e038cda76e..2b6847b65b8 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.2", - "bleak-retry-connector==2.8.3", + "bleak-retry-connector==2.8.4", "bluetooth-adapters==0.7.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.61.1" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9db814d2dea..69eb6f4976c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.8.3 +bleak-retry-connector==2.8.4 bleak==0.19.2 bluetooth-adapters==0.7.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index 858194c739a..21a4d97fabb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.3 +bleak-retry-connector==2.8.4 # homeassistant.components.bluetooth bleak==0.19.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c6ab32dc73..bf913bcfa3d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.3 +bleak-retry-connector==2.8.4 # homeassistant.components.bluetooth bleak==0.19.2 From 8f3449d942254e40593ee2ba856e9e46b309e159 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Nov 2022 01:53:07 -0600 Subject: [PATCH 082/114] Bump PySwitchbot to 0.20.3 (#81938) changelog: https://github.com/Danielhiversen/pySwitchbot/compare/0.20.2...0.20.3 releases the connection sooner to reduce the risk of running out of connection slots on the ble adapter --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 532edac7d43..2c95327beef 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.20.2"], + "requirements": ["PySwitchbot==0.20.3"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 21a4d97fabb..ec9f1535666 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.20.2 +PySwitchbot==0.20.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf913bcfa3d..c4ba49a917f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.20.2 +PySwitchbot==0.20.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 0457a744284e427761ee3ece867d239fc5cd8356 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 16 Nov 2022 11:51:14 +0000 Subject: [PATCH 083/114] Fix ONVIF subscription errors (#81965) fixes undefined --- homeassistant/components/onvif/event.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 3801d8081db..2dd5d226e37 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -8,7 +8,7 @@ import datetime as dt from httpx import RemoteProtocolError, TransportError from onvif import ONVIFCamera, ONVIFService -from zeep.exceptions import Fault +from zeep.exceptions import Fault, XMLParseError from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback from homeassistant.helpers.event import async_call_later @@ -20,6 +20,7 @@ from .parsers import PARSERS UNHANDLED_TOPICS = set() SUBSCRIPTION_ERRORS = ( + XMLParseError, Fault, asyncio.TimeoutError, TransportError, @@ -153,7 +154,8 @@ class EventManager: .isoformat(timespec="seconds") .replace("+00:00", "Z") ) - await self._subscription.Renew(termination_time) + with suppress(*SUBSCRIPTION_ERRORS): + await self._subscription.Renew(termination_time) def async_schedule_pull(self) -> None: """Schedule async_pull_messages to run.""" From 4e82f134b2c4afe7706e39700d1252b5a08bddd8 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 11 Nov 2022 17:03:32 -0500 Subject: [PATCH 084/114] Bump ZHA quirks lib to 0.0.86 (#81966) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index c8aebe3b0c0..312b93aff6f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.34.2", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.85", + "zha-quirks==0.0.86", "zigpy-deconz==0.19.0", "zigpy==0.51.5", "zigpy-xbee==0.16.2", diff --git a/requirements_all.txt b/requirements_all.txt index ec9f1535666..7a8443fd382 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2610,7 +2610,7 @@ zengge==0.2 zeroconf==0.39.4 # homeassistant.components.zha -zha-quirks==0.0.85 +zha-quirks==0.0.86 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4ba49a917f..5afffcd0b3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1811,7 +1811,7 @@ zamg==0.1.1 zeroconf==0.39.4 # homeassistant.components.zha -zha-quirks==0.0.85 +zha-quirks==0.0.86 # homeassistant.components.zha zigpy-deconz==0.19.0 From 5306b32a48aeb53f9a821b93503a562dd979ac2f Mon Sep 17 00:00:00 2001 From: Jeef Date: Tue, 15 Nov 2022 09:29:32 -0800 Subject: [PATCH 085/114] Increasing device usage update interval for Flume (#81968) --- homeassistant/components/flume/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/flume/const.py b/homeassistant/components/flume/const.py index 2d53db4c486..b9192207e75 100644 --- a/homeassistant/components/flume/const.py +++ b/homeassistant/components/flume/const.py @@ -17,7 +17,7 @@ DEFAULT_NAME = "Flume Sensor" # Flume API limits individual endpoints to 120 queries per hour NOTIFICATION_SCAN_INTERVAL = timedelta(minutes=1) -DEVICE_SCAN_INTERVAL = timedelta(minutes=1) +DEVICE_SCAN_INTERVAL = timedelta(minutes=5) DEVICE_CONNECTION_SCAN_INTERVAL = timedelta(minutes=1) _LOGGER = logging.getLogger(__package__) From 3bf3a1fd8599aa2ad3a5b1c89a7258487afaa050 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Nov 2022 16:01:54 -0600 Subject: [PATCH 086/114] Bump oralb-ble to 0.14.2 (#81969) fixes detection of the black 9000 model fixes #81967 changelog: https://github.com/Bluetooth-Devices/oralb-ble/compare/v0.14.1...v0.14.2 --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index eff6c999c30..8868330a7e7 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.14.1"], + "requirements": ["oralb-ble==0.14.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 7a8443fd382..3ba9c7941ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1241,7 +1241,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.14.1 +oralb-ble==0.14.2 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5afffcd0b3e..281ffd05ae3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -886,7 +886,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.14.1 +oralb-ble==0.14.2 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 533efa288024f12048bbed22fbb8e63bf9032069 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 13 Nov 2022 06:58:59 -0800 Subject: [PATCH 087/114] Bump gcal_sync 4.0.2 (#82017) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 2bc84827cd6..7de3a735b96 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", - "requirements": ["gcal-sync==4.0.1", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==4.0.2", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 3ba9c7941ab..43037eddc37 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -725,7 +725,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==4.0.1 +gcal-sync==4.0.2 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 281ffd05ae3..9e73316565e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -541,7 +541,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==4.0.1 +gcal-sync==4.0.2 # homeassistant.components.geocaching geocachingapi==0.2.1 From 76cc26ad17095edb5a385b292e999a49789d1caf Mon Sep 17 00:00:00 2001 From: Vincent Giorgi Date: Sun, 13 Nov 2022 16:11:07 +0100 Subject: [PATCH 088/114] Bump airthings-ble to 0.5.3 (#82033) Bump airthings-ble --- homeassistant/components/airthings_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airthings_ble/manifest.json b/homeassistant/components/airthings_ble/manifest.json index dca2dbbb562..422a51c7187 100644 --- a/homeassistant/components/airthings_ble/manifest.json +++ b/homeassistant/components/airthings_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Airthings BLE", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airthings_ble", - "requirements": ["airthings-ble==0.5.2"], + "requirements": ["airthings-ble==0.5.3"], "dependencies": ["bluetooth"], "codeowners": ["@vincegio"], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 43037eddc37..221ca65c7d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -294,7 +294,7 @@ aioymaps==1.2.2 airly==1.1.0 # homeassistant.components.airthings_ble -airthings-ble==0.5.2 +airthings-ble==0.5.3 # homeassistant.components.airthings airthings_cloud==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e73316565e..95f1983f464 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -269,7 +269,7 @@ aioymaps==1.2.2 airly==1.1.0 # homeassistant.components.airthings_ble -airthings-ble==0.5.2 +airthings-ble==0.5.3 # homeassistant.components.airthings airthings_cloud==0.1.0 From 609438d929662e9921a04f723f14b863f2f2e0d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Nov 2022 11:27:59 -0600 Subject: [PATCH 089/114] Make sure the config_flow key is set for brands (#82038) Fixes https://github.com/home-assistant/frontend/issues/14376 --- homeassistant/generated/integrations.json | 85 +++++++++++++++++++++++ script/hassfest/config_flow.py | 5 +- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 08317d06a5c..b917153203b 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -159,21 +159,25 @@ "integrations": { "alexa": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Amazon Alexa" }, "amazon_polly": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Amazon Polly" }, "aws": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Amazon Web Services (AWS)" }, "route53": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "AWS Route53" } @@ -284,6 +288,7 @@ }, "itunes": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Apple iTunes" } @@ -336,11 +341,13 @@ "integrations": { "aruba": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Aruba" }, "cppm_tracker": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Aruba ClearPass" } @@ -363,11 +370,13 @@ "integrations": { "asterisk_cdr": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Asterisk Call Detail Records" }, "asterisk_mbox": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "Asterisk Voicemail" } @@ -710,16 +719,19 @@ "integrations": { "cisco_ios": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Cisco IOS" }, "cisco_mobility_express": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Cisco Mobility Express" }, "cisco_webex_teams": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Cisco Webex Teams" } @@ -748,11 +760,13 @@ "integrations": { "clicksend": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "ClickSend SMS" }, "clicksend_tts": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "ClickSend TTS" } @@ -944,6 +958,7 @@ "integrations": { "denon": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Denon Network Receivers" }, @@ -1245,6 +1260,7 @@ "integrations": { "avea": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Elgato Avea" }, @@ -1291,11 +1307,13 @@ "integrations": { "emoncms": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Emoncms" }, "emoncms_history": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Emoncms History" } @@ -1377,6 +1395,7 @@ }, "epsonworkforce": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Epson Workforce" } @@ -1387,11 +1406,13 @@ "integrations": { "eq3btsmart": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "eQ-3 Bluetooth Smart Thermostats" }, "maxcube": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "eQ-3 MAX!" } @@ -1480,15 +1501,18 @@ "integrations": { "ffmpeg": { "integration_type": "hub", + "config_flow": false, "name": "FFmpeg" }, "ffmpeg_motion": { "integration_type": "hub", + "config_flow": false, "iot_class": "calculated", "name": "FFmpeg Motion" }, "ffmpeg_noise": { "integration_type": "hub", + "config_flow": false, "iot_class": "calculated", "name": "FFmpeg Noise" } @@ -1871,11 +1895,13 @@ "integrations": { "gc100": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Global Cach\u00e9 GC-100" }, "itach": { "integration_type": "hub", + "config_flow": false, "iot_class": "assumed_state", "name": "Global Cach\u00e9 iTach TCP/IP to IR" } @@ -1910,26 +1936,31 @@ "integrations": { "google_assistant": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Google Assistant" }, "google_cloud": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Google Cloud Platform" }, "google_domains": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_polling", "name": "Google Domains" }, "google_maps": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_polling", "name": "Google Maps" }, "google_pubsub": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Google Pub/Sub" }, @@ -1941,6 +1972,7 @@ }, "google_translate": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Google Translate Text-to-Speech" }, @@ -1951,6 +1983,7 @@ }, "google_wifi": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Google Wifi" }, @@ -2119,11 +2152,13 @@ "integrations": { "hikvision": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "Hikvision" }, "hikvisioncam": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Hikvision" } @@ -2176,6 +2211,7 @@ "integrations": { "homematic": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "Homematic" }, @@ -2204,6 +2240,7 @@ }, "evohome": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_polling", "name": "Honeywell Total Connect Comfort (Europe)" }, @@ -2297,11 +2334,13 @@ "integrations": { "watson_iot": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "IBM Watson IoT Platform" }, "watson_tts": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "IBM Watson TTS" } @@ -2342,6 +2381,7 @@ "integrations": { "symfonisk": { "integration_type": "virtual", + "config_flow": false, "supported_by": "sonos", "name": "IKEA SYMFONISK" }, @@ -2720,6 +2760,7 @@ "integrations": { "lg_netcast": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "LG Netcast" }, @@ -2855,6 +2896,7 @@ }, "ue_smart_radio": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_polling", "name": "Logitech UE Smart Radio" }, @@ -2901,6 +2943,7 @@ "integrations": { "lutron": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Lutron" }, @@ -2912,6 +2955,7 @@ }, "homeworks": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "Lutron Homeworks" } @@ -3021,6 +3065,7 @@ }, "raincloud": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_polling", "name": "Melnor RainCloud" } @@ -3097,31 +3142,37 @@ }, "azure_service_bus": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Azure Service Bus" }, "microsoft_face_detect": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Microsoft Face Detect" }, "microsoft_face_identify": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Microsoft Face Identify" }, "microsoft_face": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Microsoft Face" }, "microsoft": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Microsoft Text-to-Speech (TTS)" }, "msteams": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Microsoft Teams" }, @@ -3133,6 +3184,7 @@ }, "xbox_live": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_polling", "name": "Xbox Live" } @@ -3260,6 +3312,7 @@ "integrations": { "manual_mqtt": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "Manual MQTT Alarm Control Panel" }, @@ -3271,21 +3324,25 @@ }, "mqtt_eventstream": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "MQTT Eventstream" }, "mqtt_json": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "MQTT JSON" }, "mqtt_room": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "MQTT Room Presence" }, "mqtt_statestream": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "MQTT Statestream" } @@ -3404,6 +3461,7 @@ }, "netgear_lte": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "NETGEAR LTE" } @@ -3765,11 +3823,13 @@ "integrations": { "luci": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "OpenWrt (luci)" }, "ubus": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "OpenWrt (ubus)" } @@ -3846,6 +3906,7 @@ "integrations": { "panasonic_bluray": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Panasonic Blu-Ray Player" }, @@ -4140,6 +4201,7 @@ "integrations": { "qnap": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "QNAP" }, @@ -4228,6 +4290,7 @@ "integrations": { "rpi_camera": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Raspberry Pi Camera" }, @@ -4238,6 +4301,7 @@ }, "remote_rpi_gpio": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "Raspberry Pi Remote GPIO" } @@ -4437,11 +4501,13 @@ "integrations": { "russound_rio": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_push", "name": "Russound RIO" }, "russound_rnet": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Russound RNET" } @@ -4464,6 +4530,7 @@ "integrations": { "familyhub": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Samsung Family Hub" }, @@ -4845,6 +4912,7 @@ }, "solaredge_local": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "SolarEdge Local" } @@ -4908,6 +4976,7 @@ }, "sony_projector": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Sony Projector" }, @@ -5121,6 +5190,7 @@ "integrations": { "synology_chat": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Synology Chat" }, @@ -5132,6 +5202,7 @@ }, "synology_srm": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Synology SRM" } @@ -5218,11 +5289,13 @@ "integrations": { "telegram": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_polling", "name": "Telegram" }, "telegram_bot": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Telegram bot" } @@ -5239,6 +5312,7 @@ }, "tellstick": { "integration_type": "hub", + "config_flow": false, "iot_class": "assumed_state", "name": "TellStick" } @@ -5522,11 +5596,13 @@ }, "twilio_call": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Twilio Call" }, "twilio_sms": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Twilio SMS" } @@ -5555,6 +5631,7 @@ "integrations": { "ultraloq": { "integration_type": "virtual", + "config_flow": false, "iot_standards": [ "zwave" ], @@ -5573,11 +5650,13 @@ }, "unifi_direct": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "UniFi AP" }, "unifiled": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "UniFi LED" }, @@ -5754,6 +5833,7 @@ "integrations": { "vlc": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "VLC media player" }, @@ -5978,11 +6058,13 @@ }, "xiaomi_tv": { "integration_type": "hub", + "config_flow": false, "iot_class": "assumed_state", "name": "Xiaomi TV" }, "xiaomi": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Xiaomi" } @@ -6040,11 +6122,13 @@ "integrations": { "yandex_transport": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_polling", "name": "Yandex Transport" }, "yandextts": { "integration_type": "hub", + "config_flow": false, "iot_class": "cloud_push", "name": "Yandex TTS" } @@ -6061,6 +6145,7 @@ }, "yeelightsunflower": { "integration_type": "hub", + "config_flow": false, "iot_class": "local_polling", "name": "Yeelight Sunflower" } diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index 9cebb37d371..84347697147 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -113,8 +113,9 @@ def _populate_brand_integrations( metadata = { "integration_type": integration.integration_type, } - if integration.config_flow: - metadata["config_flow"] = integration.config_flow + # Always set the config_flow key to avoid breaking the frontend + # https://github.com/home-assistant/frontend/issues/14376 + metadata["config_flow"] = bool(integration.config_flow) if integration.iot_class: metadata["iot_class"] = integration.iot_class if integration.supported_by: From f8b5a97e72f283787189746eb0bcb28073633dab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Nov 2022 17:12:52 -0600 Subject: [PATCH 090/114] Bump pySwitchbot to 0.20.4 (#82055) Fixes for curtain firmware v6 changelog: https://github.com/Danielhiversen/pySwitchbot/compare/0.20.3...0.20.4 --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 2c95327beef..d586a328ed7 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.20.3"], + "requirements": ["PySwitchbot==0.20.4"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 221ca65c7d7..c313121c620 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.20.3 +PySwitchbot==0.20.4 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 95f1983f464..38794ee865c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.20.3 +PySwitchbot==0.20.4 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 2a641d1d19bf0d6d64c5d740cec581df487e7d24 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Mon, 14 Nov 2022 12:43:45 +1100 Subject: [PATCH 091/114] Restore color_temp handling for lifx.set_state (#82067) fixes undefined --- homeassistant/components/lifx/util.py | 7 +++++++ tests/components/lifx/test_light.py | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/homeassistant/components/lifx/util.py b/homeassistant/components/lifx/util.py index 6a9bff465ee..fde36d714d5 100644 --- a/homeassistant/components/lifx/util.py +++ b/homeassistant/components/lifx/util.py @@ -16,6 +16,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, + ATTR_COLOR_TEMP, ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, ATTR_KELVIN, @@ -114,6 +115,12 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] | kelvin = kwargs.pop(ATTR_KELVIN) saturation = 0 + if ATTR_COLOR_TEMP in kwargs: + kelvin = color_util.color_temperature_mired_to_kelvin( + kwargs.pop(ATTR_COLOR_TEMP) + ) + saturation = 0 + if ATTR_COLOR_TEMP_KELVIN in kwargs: kelvin = kwargs.pop(ATTR_COLOR_TEMP_KELVIN) saturation = 0 diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index 5a9b250034a..3bf82fce3fc 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -1524,6 +1524,15 @@ async def test_lifx_set_state_kelvin(hass: HomeAssistant) -> None: assert bulb.set_color.calls[0][0][0] == [32000, 0, 25700, 2700] bulb.set_color.reset_mock() + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255, ATTR_COLOR_TEMP: 400}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, 0, 65535, 2500] + bulb.set_color.reset_mock() + async def test_infrared_color_bulb(hass: HomeAssistant) -> None: """Test setting infrared with a color bulb.""" From 66d3891a376527f26f352a42480df24340cf27e3 Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Mon, 14 Nov 2022 09:41:25 +0000 Subject: [PATCH 092/114] Bump growattServer to 1.2.4 (#82071) Growatt - Library bump to workaround for #81951 --- homeassistant/components/growatt_server/manifest.json | 2 +- homeassistant/components/growatt_server/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index f3f17804fc1..e3b63f7c8b3 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -3,7 +3,7 @@ "name": "Growatt", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/growatt_server/", - "requirements": ["growattServer==1.2.3"], + "requirements": ["growattServer==1.2.4"], "codeowners": ["@indykoning", "@muppet3000", "@JasperPlant"], "iot_class": "cloud_polling", "loggers": ["growattServer"] diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index eceba2f7bce..ac19d91b24d 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -32,7 +32,7 @@ from .sensor_types.total import TOTAL_SENSOR_TYPES _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = datetime.timedelta(minutes=1) +SCAN_INTERVAL = datetime.timedelta(minutes=5) def get_device_list(api, config): diff --git a/requirements_all.txt b/requirements_all.txt index c313121c620..d96b3422382 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -804,7 +804,7 @@ greenwavereality==0.5.1 gridnet==4.0.0 # homeassistant.components.growatt_server -growattServer==1.2.3 +growattServer==1.2.4 # homeassistant.components.google_sheets gspread==5.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 38794ee865c..cd003264740 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -599,7 +599,7 @@ greeneye_monitor==3.0.3 gridnet==4.0.0 # homeassistant.components.growatt_server -growattServer==1.2.3 +growattServer==1.2.4 # homeassistant.components.google_sheets gspread==5.5.0 From 431f93e1d3378d1987caec5e12708a6972852479 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Nov 2022 11:30:03 -0600 Subject: [PATCH 093/114] Bump PySwitchbot to 0.20.5 (#82099) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index d586a328ed7..274c5784b2f 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.20.4"], + "requirements": ["PySwitchbot==0.20.5"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index d96b3422382..d41703e1c7d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.20.4 +PySwitchbot==0.20.5 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd003264740..075254c5ad8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.20.4 +PySwitchbot==0.20.5 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 57c868e615cd5e84f78884c0286b93ee0e94380b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 15 Nov 2022 15:51:13 +0100 Subject: [PATCH 094/114] Update sqlalchemy to 1.4.44 (#82129) --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index afdabfd6d01..3fb873bfc90 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.42", "fnvhash==0.1.0"], + "requirements": ["sqlalchemy==1.4.44", "fnvhash==0.1.0"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 7484ca0feb7..4ee1683a357 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.4.42"], + "requirements": ["sqlalchemy==1.4.44"], "codeowners": ["@dgomes", "@gjohansson-ST"], "config_flow": true, "iot_class": "local_polling" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 69eb6f4976c..4641c32d562 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -37,7 +37,7 @@ pyudev==0.23.2 pyyaml==6.0 requests==2.28.1 scapy==2.4.5 -sqlalchemy==1.4.42 +sqlalchemy==1.4.44 typing-extensions>=4.4.0,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index d41703e1c7d..bbe5c347870 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2314,7 +2314,7 @@ spotipy==2.20.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.42 +sqlalchemy==1.4.44 # homeassistant.components.srp_energy srpenergy==1.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 075254c5ad8..1415f9b4cb6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1599,7 +1599,7 @@ spotipy==2.20.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.42 +sqlalchemy==1.4.44 # homeassistant.components.srp_energy srpenergy==1.3.6 From 228fa9f5a00812f9773a331672252415a7a61902 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 15 Nov 2022 18:43:01 +0100 Subject: [PATCH 095/114] Always update attributes on an update for MQTT update (#82139) --- homeassistant/components/mqtt/update.py | 8 ++++---- tests/components/mqtt/test_update.py | 16 ++++++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index 5536d16d1c7..abad1cdb2ff 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -196,19 +196,19 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity): self._attr_latest_version = json_payload["latest_version"] get_mqtt_data(self.hass).state_write_requests.write_state_request(self) - if CONF_TITLE in json_payload and not self._attr_title: + if CONF_TITLE in json_payload: self._attr_title = json_payload[CONF_TITLE] get_mqtt_data(self.hass).state_write_requests.write_state_request(self) - if CONF_RELEASE_SUMMARY in json_payload and not self._attr_release_summary: + if CONF_RELEASE_SUMMARY in json_payload: self._attr_release_summary = json_payload[CONF_RELEASE_SUMMARY] get_mqtt_data(self.hass).state_write_requests.write_state_request(self) - if CONF_RELEASE_URL in json_payload and not self._attr_release_url: + if CONF_RELEASE_URL in json_payload: self._attr_release_url = json_payload[CONF_RELEASE_URL] get_mqtt_data(self.hass).state_write_requests.write_state_request(self) - if CONF_ENTITY_PICTURE in json_payload and not self._entity_picture: + if CONF_ENTITY_PICTURE in json_payload: self._entity_picture = json_payload[CONF_ENTITY_PICTURE] get_mqtt_data(self.hass).state_write_requests.write_state_request(self) diff --git a/tests/components/mqtt/test_update.py b/tests/components/mqtt/test_update.py index e7d75ee7cc8..a8f925bf4a6 100644 --- a/tests/components/mqtt/test_update.py +++ b/tests/components/mqtt/test_update.py @@ -203,8 +203,9 @@ async def test_json_state_message(hass, mqtt_mock_entry_with_yaml_config): hass, state_topic, '{"installed_version":"1.9.0","latest_version":"1.9.0",' - '"title":"Test Update Title","release_url":"https://example.com/release",' - '"release_summary":"Test release summary"}', + '"title":"Test Update 1 Title","release_url":"https://example.com/release1",' + '"release_summary":"Test release summary 1",' + '"entity_picture": "https://example.com/icon1.png"}', ) await hass.async_block_till_done() @@ -213,14 +214,16 @@ async def test_json_state_message(hass, mqtt_mock_entry_with_yaml_config): assert state.state == STATE_OFF assert state.attributes.get("installed_version") == "1.9.0" assert state.attributes.get("latest_version") == "1.9.0" - assert state.attributes.get("release_summary") == "Test release summary" - assert state.attributes.get("release_url") == "https://example.com/release" - assert state.attributes.get("title") == "Test Update Title" + assert state.attributes.get("release_summary") == "Test release summary 1" + assert state.attributes.get("release_url") == "https://example.com/release1" + assert state.attributes.get("title") == "Test Update 1 Title" + assert state.attributes.get("entity_picture") == "https://example.com/icon1.png" async_fire_mqtt_message( hass, state_topic, - '{"installed_version":"1.9.0","latest_version":"2.0.0","title":"Test Update Title"}', + '{"installed_version":"1.9.0","latest_version":"2.0.0",' + '"title":"Test Update 2 Title","entity_picture":"https://example.com/icon2.png"}', ) await hass.async_block_till_done() @@ -229,6 +232,7 @@ async def test_json_state_message(hass, mqtt_mock_entry_with_yaml_config): assert state.state == STATE_ON assert state.attributes.get("installed_version") == "1.9.0" assert state.attributes.get("latest_version") == "2.0.0" + assert state.attributes.get("entity_picture") == "https://example.com/icon2.png" async def test_json_state_message_with_template(hass, mqtt_mock_entry_with_yaml_config): From aade51248db3ac353201091822c6fa4259cbd494 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 16 Nov 2022 07:04:34 -0500 Subject: [PATCH 096/114] Add missing strings in Onvif (#82141) --- homeassistant/components/onvif/strings.json | 3 ++- homeassistant/components/onvif/translations/en.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onvif/strings.json b/homeassistant/components/onvif/strings.json index 4cf1bd4bad0..210027e96e5 100644 --- a/homeassistant/components/onvif/strings.json +++ b/homeassistant/components/onvif/strings.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "Extra FFMPEG arguments", - "rtsp_transport": "RTSP transport mechanism" + "rtsp_transport": "RTSP transport mechanism", + "use_wallclock_as_timestamps": "Use wall clock as timestamps" }, "title": "ONVIF Device Options" } diff --git a/homeassistant/components/onvif/translations/en.json b/homeassistant/components/onvif/translations/en.json index c3b328646ee..473e2af9ba8 100644 --- a/homeassistant/components/onvif/translations/en.json +++ b/homeassistant/components/onvif/translations/en.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "Extra FFMPEG arguments", - "rtsp_transport": "RTSP transport mechanism" + "rtsp_transport": "RTSP transport mechanism", + "use_wallclock_as_timestamps": "Use wall clock as timestamps" }, "title": "ONVIF Device Options" } From 83db9a3335d3ece9b9157f71de11202aa9d2fd87 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 15 Nov 2022 17:07:43 -0500 Subject: [PATCH 097/114] Fix Google Sheets formula input (#82157) --- homeassistant/components/google_sheets/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_sheets/__init__.py b/homeassistant/components/google_sheets/__init__.py index e211693bf21..19f5ce81f5c 100644 --- a/homeassistant/components/google_sheets/__init__.py +++ b/homeassistant/components/google_sheets/__init__.py @@ -7,6 +7,7 @@ import aiohttp from google.auth.exceptions import RefreshError from google.oauth2.credentials import Credentials from gspread import Client +from gspread.utils import ValueInputOption import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigEntryState @@ -100,7 +101,7 @@ async def async_setup_service(hass: HomeAssistant) -> None: columns.append(key) worksheet.update_cell(1, len(columns), key) row.append(value) - worksheet.append_row(row) + worksheet.append_row(row, value_input_option=ValueInputOption.user_entered) async def append_to_sheet(call: ServiceCall) -> None: """Append new line of data to a Google Sheets document.""" From 987add50cbf7e94df320abe485fa534ea4d3bceb Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Wed, 16 Nov 2022 09:41:14 +0000 Subject: [PATCH 098/114] Fix Growatt incorrect energy dashboard values for grid import (#82163) * Fix Growatt incorrect energy dashboard values for grid import (#80905) * Growatt - addressing review comments (#80905) * Growatt - addressing more review comments (#80905) --- .../components/growatt_server/sensor.py | 61 +++++++++++++++++-- .../growatt_server/sensor_types/mix.py | 1 + .../sensor_types/sensor_entity_description.py | 1 + 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index ac19d91b24d..d6b74b78475 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -159,7 +159,7 @@ class GrowattInverter(SensorEntity): @property def native_value(self): """Return the state of the sensor.""" - result = self.probe.get_data(self.entity_description.api_key) + result = self.probe.get_data(self.entity_description) if self.entity_description.precision is not None: result = round(result, self.entity_description.precision) return result @@ -168,7 +168,7 @@ class GrowattInverter(SensorEntity): def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of the sensor, if any.""" if self.entity_description.currency: - return self.probe.get_data("currency") + return self.probe.get_currency() return super().native_unit_of_measurement def update(self) -> None: @@ -187,6 +187,7 @@ class GrowattData: self.device_id = device_id self.plant_id = None self.data = {} + self.previous_values = {} self.username = username self.password = password @@ -254,9 +255,61 @@ class GrowattData: **mix_detail, **dashboard_values_for_mix, } + _LOGGER.debug( + "Finished updating data for %s (%s)", + self.device_id, + self.growatt_type, + ) except json.decoder.JSONDecodeError: _LOGGER.error("Unable to fetch data from Growatt server") - def get_data(self, variable): + def get_currency(self): + """Get the currency.""" + return self.data.get("currency") + + def get_data(self, entity_description): """Get the data.""" - return self.data.get(variable) + _LOGGER.debug( + "Data request for: %s", + entity_description.name, + ) + variable = entity_description.api_key + api_value = self.data.get(variable) + previous_value = self.previous_values.get(variable) + return_value = api_value + + # If we have a 'drop threshold' specified, then check it and correct if needed + if ( + entity_description.previous_value_drop_threshold is not None + and previous_value is not None + and api_value is not None + ): + _LOGGER.debug( + "%s - Drop threshold specified (%s), checking for drop... API Value: %s, Previous Value: %s", + entity_description.name, + entity_description.previous_value_drop_threshold, + api_value, + previous_value, + ) + diff = float(api_value) - float(previous_value) + + # Check if the value has dropped (negative value i.e. < 0) and it has only dropped by a + # small amount, if so, use the previous value. + # Note - The energy dashboard takes care of drops within 10% of the current value, + # however if the value is low e.g. 0.2 and drops by 0.1 it classes as a reset. + if -(entity_description.previous_value_drop_threshold) <= diff < 0: + _LOGGER.debug( + "Diff is negative, but only by a small amount therefore not a nightly reset, " + "using previous value (%s) instead of api value (%s)", + previous_value, + api_value, + ) + return_value = previous_value + else: + _LOGGER.debug( + "%s - No drop detected, using API value", entity_description.name + ) + + self.previous_values[variable] = return_value + + return return_value diff --git a/homeassistant/components/growatt_server/sensor_types/mix.py b/homeassistant/components/growatt_server/sensor_types/mix.py index 6cb61ea2e08..75d816fdf60 100644 --- a/homeassistant/components/growatt_server/sensor_types/mix.py +++ b/homeassistant/components/growatt_server/sensor_types/mix.py @@ -241,5 +241,6 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + previous_value_drop_threshold=0.2, ), ) diff --git a/homeassistant/components/growatt_server/sensor_types/sensor_entity_description.py b/homeassistant/components/growatt_server/sensor_types/sensor_entity_description.py index 04822fca35b..08a20209098 100644 --- a/homeassistant/components/growatt_server/sensor_types/sensor_entity_description.py +++ b/homeassistant/components/growatt_server/sensor_types/sensor_entity_description.py @@ -19,3 +19,4 @@ class GrowattSensorEntityDescription(SensorEntityDescription, GrowattRequiredKey precision: int | None = None currency: bool = False + previous_value_drop_threshold: float | None = None From 6371cb4ebd4ad38e401c08846031959d987b0cc0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Nov 2022 15:43:18 +0100 Subject: [PATCH 099/114] Bumped version to 2022.11.3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f3b51bd5d3a..7b4230c79f1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __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/pyproject.toml b/pyproject.toml index 5058c46641f..d60943ab098 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.2" +version = "2022.11.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From b0714e32b1218c50f41fa64354e60e00d67561fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Nov 2022 09:37:00 -0600 Subject: [PATCH 100/114] Fix static version in homekit tests (#82201) --- tests/components/homekit/test_diagnostics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py index 30fe5f2d8fc..48b1b84580e 100644 --- a/tests/components/homekit/test_diagnostics.py +++ b/tests/components/homekit/test_diagnostics.py @@ -379,7 +379,7 @@ async def test_config_entry_with_trigger_accessory( "iid": 7, "perms": ["pr"], "type": "52", - "value": "2022.12.0", + "value": ANY, }, ], "iid": 1, From 74c2639495d461cba48ab0f649c2cef557dc43b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Nov 2022 12:02:09 -0600 Subject: [PATCH 101/114] Fix missing await in nexia emergency heat (#82207) fixes undefined --- homeassistant/components/nexia/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 66c325d2fc3..f8c08b4efd6 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -378,7 +378,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): async def async_turn_aux_heat_on(self) -> None: """Turn Aux Heat on.""" - self._thermostat.set_emergency_heat(True) + await self._thermostat.set_emergency_heat(True) self._signal_thermostat_update() async def async_turn_off(self) -> None: From 5ab7c8e9bab6f8cc9a18d461db065bc9e689ad2c Mon Sep 17 00:00:00 2001 From: Daan Beverdam Date: Thu, 17 Nov 2022 21:57:26 +0100 Subject: [PATCH 102/114] Fix moving average for 0 values (#80476) --- homeassistant/components/filter/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 4c9c83a8787..ce70ed14d19 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -612,7 +612,7 @@ class TimeSMAFilter(Filter, SensorEntity): moving_sum = 0 start = new_state.timestamp - self._time_window - prev_state = self.last_leak or self.queue[0] + prev_state = self.last_leak if self.last_leak is not None else self.queue[0] for state in self.queue: moving_sum += (state.timestamp - start).total_seconds() * prev_state.state start = state.timestamp From 6a73406e9f5857c7d263e2d78959c0f7d3b5052a Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 18 Nov 2022 09:32:51 +0100 Subject: [PATCH 103/114] Solve Modbus reload issue (#82253) fixes undefined --- homeassistant/components/modbus/__init__.py | 1 - homeassistant/components/modbus/modbus.py | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 8aa2903506f..ec04d5f147d 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -375,4 +375,3 @@ async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> No hubs = hass.data[DOMAIN] for name in hubs: await hubs[name].async_close() - del hass.data[DOMAIN] diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 5e15f65035a..e2240f530c6 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -132,6 +132,12 @@ async def async_modbus_setup( await async_setup_reload_service(hass, DOMAIN, [DOMAIN]) + if DOMAIN in hass.data and config[DOMAIN] == []: + hubs = hass.data[DOMAIN] + for name in hubs: + if not await hubs[name].async_setup(): + return False + hass.data[DOMAIN] = hub_collect = {} for conf_hub in config[DOMAIN]: my_hub = ModbusHub(hass, conf_hub) From 1c06c6c1e6d3d9dd23c72256c7a067f29deadd40 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 18 Nov 2022 09:56:34 +0100 Subject: [PATCH 104/114] Add kilo watts unit mapping for nibe_heatpump (#82284) --- homeassistant/components/nibe_heatpump/sensor.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/nibe_heatpump/sensor.py b/homeassistant/components/nibe_heatpump/sensor.py index 66c66aaabe1..0b12afd9e05 100644 --- a/homeassistant/components/nibe_heatpump/sensor.py +++ b/homeassistant/components/nibe_heatpump/sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, + POWER_KILO_WATT, POWER_WATT, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -80,6 +81,13 @@ UNIT_DESCRIPTIONS = { state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), + "kW": SensorEntityDescription( + key="kW", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=POWER_KILO_WATT, + ), "Wh": SensorEntityDescription( key="Wh", entity_category=EntityCategory.DIAGNOSTIC, From df2953403ed1604bb4da90d1fa9ba0fc155b12a2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 18 Nov 2022 17:37:12 -0700 Subject: [PATCH 105/114] Bump `regenmaschine` to 2022.11.0 (#82337) fixes undefined --- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index a41db1d18f9..a8f6f316d3a 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.10.0"], + "requirements": ["regenmaschine==2022.11.0"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index bbe5c347870..cfd2b640142 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2153,7 +2153,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.10.0 +regenmaschine==2022.11.0 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1415f9b4cb6..d8aa5615e61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1489,7 +1489,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.10.0 +regenmaschine==2022.11.0 # homeassistant.components.renault renault-api==0.1.11 From 23280268c81b9e881ff5cfc0cddaf3534ab421a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Sat, 19 Nov 2022 16:05:38 +0100 Subject: [PATCH 106/114] Bump pysma to version 0.7.3 (#82343) fixes undefined --- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 83bf4258a95..f4e82a550e3 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.7.2"], + "requirements": ["pysma==0.7.3"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling", "loggers": ["pysma"] diff --git a/requirements_all.txt b/requirements_all.txt index cfd2b640142..f0be7b4d44c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1893,7 +1893,7 @@ pysignalclirestapi==0.3.18 pyskyqhub==0.1.4 # homeassistant.components.sma -pysma==0.7.2 +pysma==0.7.3 # homeassistant.components.smappee pysmappee==0.2.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d8aa5615e61..51e6f17d852 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1334,7 +1334,7 @@ pysiaalarm==3.0.2 pysignalclirestapi==0.3.18 # homeassistant.components.sma -pysma==0.7.2 +pysma==0.7.3 # homeassistant.components.smappee pysmappee==0.2.29 From f490119fed010f58d2f357620e2c0b61d1435981 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 19 Nov 2022 06:49:54 -0600 Subject: [PATCH 107/114] Bump flux_led to 0.28.34 (#82347) * Bump flux_led to 0.28.33 fixes #75832 changelog: https://github.com/Danielhiversen/flux_led/compare/0.28.32...0.28.33 * more more bump for legacy turn on --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 632ef04e456..66aa9fe0b92 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.28.32"], + "requirements": ["flux_led==0.28.34"], "quality_scale": "platinum", "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index f0be7b4d44c..2218f2d77a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -693,7 +693,7 @@ fjaraskupan==2.2.0 flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.32 +flux_led==0.28.34 # homeassistant.components.homekit # homeassistant.components.recorder diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51e6f17d852..6fa04d7a578 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ fjaraskupan==2.2.0 flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.32 +flux_led==0.28.34 # homeassistant.components.homekit # homeassistant.components.recorder From d45a0cc41e790ca8ad938f8eaacf61c63a276ea1 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 19 Nov 2022 15:02:07 +0100 Subject: [PATCH 108/114] Fix invalid configuration_url in Netatmo (#82372) --- homeassistant/components/netatmo/netatmo_entity_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index c434d370e27..78352e486e0 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -29,7 +29,7 @@ class NetatmoBase(Entity): self._device_name: str = "" self._id: str = "" self._model: str = "" - self._config_url: str = "" + self._config_url: str | None = None self._attr_name = None self._attr_unique_id = None self._attr_extra_state_attributes = {} From 5ec7cff3ffddb698d681379138a411ea440b54b8 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Sun, 20 Nov 2022 15:13:19 +0100 Subject: [PATCH 109/114] Bump PyViCare to 2.19.0 (#82381) fixes undefined --- homeassistant/components/vicare/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index c770f1b2382..0294022137c 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -3,7 +3,7 @@ "name": "Viessmann ViCare", "documentation": "https://www.home-assistant.io/integrations/vicare", "codeowners": ["@oischinger"], - "requirements": ["PyViCare==2.17.0"], + "requirements": ["PyViCare==2.19.0"], "iot_class": "cloud_polling", "config_flow": true, "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 2218f2d77a4..20c5490d335 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -47,7 +47,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.7 # homeassistant.components.vicare -PyViCare==2.17.0 +PyViCare==2.19.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.14.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6fa04d7a578..5d7f9bb0598 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -43,7 +43,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.7 # homeassistant.components.vicare -PyViCare==2.17.0 +PyViCare==2.19.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.14.3 From 018300858fc4c61f83d111c6941973fe73e93c2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 19 Nov 2022 18:46:06 -0600 Subject: [PATCH 110/114] Bump bleak-retry-connector to 2.8.5 (#82387) changelog: https://github.com/Bluetooth-Devices/bleak-retry-connector/compare/v2.8.4...v2.8.5 from: https://github.com/esphome/esphome/pull/4049 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 2b6847b65b8..5e56d125f44 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.2", - "bleak-retry-connector==2.8.4", + "bleak-retry-connector==2.8.5", "bluetooth-adapters==0.7.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.61.1" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4641c32d562..b83c9c8db01 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.8.4 +bleak-retry-connector==2.8.5 bleak==0.19.2 bluetooth-adapters==0.7.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index 20c5490d335..bc363d39203 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.4 +bleak-retry-connector==2.8.5 # homeassistant.components.bluetooth bleak==0.19.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d7f9bb0598..f7c09893df4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.4 +bleak-retry-connector==2.8.5 # homeassistant.components.bluetooth bleak==0.19.2 From 243941f3fd6b2964aac3023ec933bb8454e27e5c Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sun, 20 Nov 2022 15:12:38 +0100 Subject: [PATCH 111/114] Update xknx to 1.2.1 (#82404) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index b29e44490e9..a7436ef1ae3 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==1.2.0"], + "requirements": ["xknx==1.2.1"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index bc363d39203..4b9242fe2a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2563,7 +2563,7 @@ xboxapi==2.0.1 xiaomi-ble==0.10.0 # homeassistant.components.knx -xknx==1.2.0 +xknx==1.2.1 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7c09893df4..506a740b2aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1776,7 +1776,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.10.0 # homeassistant.components.knx -xknx==1.2.0 +xknx==1.2.1 # homeassistant.components.bluesound # homeassistant.components.fritz From b578a76cb94572862a0e2029e24e1a04e4521160 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 20 Nov 2022 10:39:47 -0500 Subject: [PATCH 112/114] Attempt to fix occasional Flo timeouts (#82408) * Attempt to fix occasional Flo timeouts * remove gather --- homeassistant/components/flo/device.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index ca4b6aa6234..d6e05c17136 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -1,7 +1,6 @@ """Flo device object.""" from __future__ import annotations -import asyncio from datetime import datetime, timedelta from typing import Any @@ -40,14 +39,10 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self): """Update data via library.""" try: - async with timeout(10): - await asyncio.gather( - *[ - self.send_presence_ping(), - self._update_device(), - self._update_consumption_data(), - ] - ) + async with timeout(20): + await self.send_presence_ping() + await self._update_device() + await self._update_consumption_data() except (RequestError) as error: raise UpdateFailed(error) from error From 3ab1df4660c0de42edace3139b078ee96584764c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Nov 2022 09:38:30 -0600 Subject: [PATCH 113/114] Prevent powerwall from switching addresses if its online (#82410) * Prevent powerwall from switching addresses if its online If the wifi interface was discovered we would switch the ip address in the entry to the wifi ip even if it was connected via ethernet * cover * more cover --- .../components/powerwall/__init__.py | 13 +- .../components/powerwall/config_flow.py | 49 ++++++- .../components/powerwall/test_config_flow.py | 136 +++++++++++++++++- 3 files changed, 194 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 2fdf3d61d20..d8550e6f46b 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -18,7 +18,7 @@ from tesla_powerwall import ( from homeassistant.components import persistent_notification from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -221,6 +221,17 @@ def _fetch_powerwall_data(power_wall: Powerwall) -> PowerwallData: ) +@callback +def async_last_update_was_successful(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Return True if the last update was successful.""" + return bool( + (domain_data := hass.data.get(DOMAIN)) + and (entry_data := domain_data.get(entry.entry_id)) + and (coordinator := entry_data.get(POWERWALL_COORDINATOR)) + and coordinator.last_update_success + ) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index b9f6f3969fd..6e4f40bf01b 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -20,11 +20,18 @@ from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.data_entry_flow import FlowResult from homeassistant.util.network import is_ip_address +from . import async_last_update_was_successful from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +ENTRY_FAILURE_STATES = { + config_entries.ConfigEntryState.SETUP_ERROR, + config_entries.ConfigEntryState.SETUP_RETRY, +} + + def _login_and_fetch_site_info( power_wall: Powerwall, password: str ) -> tuple[SiteInfo, str]: @@ -34,6 +41,17 @@ def _login_and_fetch_site_info( return power_wall.get_site_info(), power_wall.get_gateway_din() +def _powerwall_is_reachable(ip_address: str, password: str) -> bool: + """Check if the powerwall is reachable.""" + try: + Powerwall(ip_address).login(password) + except AccessDeniedError: + return True + except PowerwallUnreachableError: + return False + return True + + async def validate_input( hass: core.HomeAssistant, data: dict[str, str] ) -> dict[str, str]: @@ -69,13 +87,31 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.title: str | None = None self.reauth_entry: config_entries.ConfigEntry | None = None + async def _async_powerwall_is_offline( + self, entry: config_entries.ConfigEntry + ) -> bool: + """Check if the power wall is offline. + + We define offline by the config entry + is in a failure/retry state or the updates + are failing and the powerwall is unreachable + since device may be updating. + """ + ip_address = entry.data[CONF_IP_ADDRESS] + password = entry.data[CONF_PASSWORD] + return bool( + entry.state in ENTRY_FAILURE_STATES + or not async_last_update_was_successful(self.hass, entry) + ) and not await self.hass.async_add_executor_job( + _powerwall_is_reachable, ip_address, password + ) + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" self.ip_address = discovery_info.ip gateway_din = discovery_info.hostname.upper() # The hostname is the gateway_din (unique_id) await self.async_set_unique_id(gateway_din) - self._abort_if_unique_id_configured(updates={CONF_IP_ADDRESS: self.ip_address}) for entry in self._async_current_entries(include_ignore=False): if entry.data[CONF_IP_ADDRESS] == discovery_info.ip: if entry.unique_id is not None and is_ip_address(entry.unique_id): @@ -86,6 +122,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_reload(entry.entry_id) ) return self.async_abort(reason="already_configured") + if entry.unique_id == gateway_din: + if await self._async_powerwall_is_offline(entry): + if self.hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_IP_ADDRESS: self.ip_address} + ): + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") + # Still need to abort for ignored entries + self._abort_if_unique_id_configured() self.context["title_placeholders"] = { "name": gateway_din, "ip_address": self.ip_address, diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index f4dcfd87b8b..11861a8238c 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -1,6 +1,6 @@ """Test the Powerwall config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch from tesla_powerwall import ( AccessDeniedError, @@ -18,6 +18,7 @@ from .mocks import ( MOCK_GATEWAY_DIN, _mock_powerwall_side_effect, _mock_powerwall_site_name, + _mock_powerwall_with_fixtures, ) from tests.common import MockConfigEntry @@ -351,7 +352,7 @@ async def test_dhcp_discovery_update_ip_address(hass): unique_id=MOCK_GATEWAY_DIN, ) entry.add_to_hass(hass) - mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") + mock_powerwall = MagicMock(login=MagicMock(side_effect=PowerwallUnreachableError)) with patch( "homeassistant.components.powerwall.config_flow.Powerwall", @@ -375,6 +376,70 @@ async def test_dhcp_discovery_update_ip_address(hass): assert entry.data[CONF_IP_ADDRESS] == "1.1.1.1" +async def test_dhcp_discovery_does_not_update_ip_when_auth_fails(hass): + """Test we do not switch to another interface when auth is failing.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + unique_id=MOCK_GATEWAY_DIN, + ) + entry.add_to_hass(hass) + mock_powerwall = MagicMock(login=MagicMock(side_effect=AccessDeniedError("any"))) + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname=MOCK_GATEWAY_DIN.lower(), + ), + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" + + +async def test_dhcp_discovery_does_not_update_ip_when_auth_successful(hass): + """Test we do not switch to another interface when auth is successful.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + unique_id=MOCK_GATEWAY_DIN, + ) + entry.add_to_hass(hass) + mock_powerwall = MagicMock(login=MagicMock(return_value=True)) + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname=MOCK_GATEWAY_DIN.lower(), + ), + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" + + async def test_dhcp_discovery_updates_unique_id(hass): """Test we can update the unique id from dhcp.""" entry = MockConfigEntry( @@ -406,3 +471,70 @@ async def test_dhcp_discovery_updates_unique_id(hass): assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" assert entry.unique_id == MOCK_GATEWAY_DIN + + +async def test_dhcp_discovery_updates_unique_id_when_entry_is_failed(hass): + """Test we can update the unique id from dhcp in a failed state.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + unique_id="1.2.3.4", + ) + entry.add_to_hass(hass) + entry.state = config_entries.ConfigEntryState.SETUP_ERROR + mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.2.3.4", + macaddress="AA:BB:CC:DD:EE:FF", + hostname=MOCK_GATEWAY_DIN.lower(), + ), + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" + assert entry.unique_id == MOCK_GATEWAY_DIN + + +async def test_discovered_wifi_does_not_update_ip_if_is_still_online(hass) -> None: + """Test a discovery does not update the ip unless the powerwall at the old ip is offline.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + unique_id=MOCK_GATEWAY_DIN, + ) + entry.add_to_hass(hass) + mock_powerwall = await _mock_powerwall_with_fixtures(hass) + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.2.3.5", + macaddress="AA:BB:CC:DD:EE:FF", + hostname=MOCK_GATEWAY_DIN.lower(), + ), + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" From 2a384987e7b5d7f94f520972e8da287a8f8bc49a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 20 Nov 2022 15:27:43 -0500 Subject: [PATCH 114/114] Bumped version to 2022.11.4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7b4230c79f1..0fe06355342 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __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/pyproject.toml b/pyproject.toml index d60943ab098..0562e18b5a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.3" +version = "2022.11.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"