From 1018e82725bd222f49094956f22fb343d1e6d642 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Aug 2023 18:17:32 +0200 Subject: [PATCH 001/202] Bump version to 2023.9.0b0 --- 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 66d05f0bd4f..30b6e4a29cb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 375aa7e5088..575c96234bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.0.dev0" +version = "2023.9.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From cb33d82c249735db5831abb998b2ed15cfd1a225 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 31 Aug 2023 17:45:44 +1000 Subject: [PATCH 002/202] Patch service validation in Aussie Broadband (#99077) * Bump pyAussieBB * rolling back to previous version * patching the pydantic 2.x issue in aussie_broadband integration * adding test for validate_service_type * adding test for validate_service_type * fixing tests, again * adding additional test * doing fixes for live tests * Implement Feedback * Add test to detect pydantic2 * Update test_init.py * Update docstring --------- Co-authored-by: James Hodgkinson --- .../components/aussie_broadband/__init__.py | 19 +++++++++++++++- .../components/aussie_broadband/test_init.py | 22 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/aussie_broadband/__init__.py b/homeassistant/components/aussie_broadband/__init__.py index ae4bc78580c..1bdb0579976 100644 --- a/homeassistant/components/aussie_broadband/__init__.py +++ b/homeassistant/components/aussie_broadband/__init__.py @@ -3,10 +3,11 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any from aiohttp import ClientError from aussiebb.asyncio import AussieBB -from aussiebb.const import FETCH_TYPES +from aussiebb.const import FETCH_TYPES, NBN_TYPES, PHONE_TYPES from aussiebb.exceptions import AuthenticationException, UnrecognisedServiceType from homeassistant.config_entries import ConfigEntry @@ -22,6 +23,19 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR] +# Backport for the pyaussiebb=0.0.15 validate_service_type method +def validate_service_type(service: dict[str, Any]) -> None: + """Check the service types against known types.""" + + if "type" not in service: + raise ValueError("Field 'type' not found in service data") + if service["type"] not in NBN_TYPES + PHONE_TYPES + ["Hardware"]: + raise UnrecognisedServiceType( + f"Service type {service['type']=} {service['name']=} - not recognised - ", + "please report this at https://github.com/yaleman/aussiebb/issues/new", + ) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Aussie Broadband from a config entry.""" # Login to the Aussie Broadband API and retrieve the current service list @@ -30,6 +44,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_PASSWORD], async_get_clientsession(hass), ) + # Overwrite the pyaussiebb=0.0.15 validate_service_type method with backport + # Required until pydantic 2.x is supported + client.validate_service_type = validate_service_type try: await client.login() services = await client.get_services(drop_types=FETCH_TYPES) diff --git a/tests/components/aussie_broadband/test_init.py b/tests/components/aussie_broadband/test_init.py index 3eb1972011c..dc32212ee87 100644 --- a/tests/components/aussie_broadband/test_init.py +++ b/tests/components/aussie_broadband/test_init.py @@ -3,8 +3,11 @@ from unittest.mock import patch from aiohttp import ClientConnectionError from aussiebb.exceptions import AuthenticationException, UnrecognisedServiceType +import pydantic +import pytest from homeassistant import data_entry_flow +from homeassistant.components.aussie_broadband import validate_service_type from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -19,6 +22,19 @@ async def test_unload(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.NOT_LOADED +async def test_validate_service_type() -> None: + """Testing the validation function.""" + test_service = {"type": "Hardware", "name": "test service"} + validate_service_type(test_service) + + with pytest.raises(ValueError): + test_service = {"name": "test service"} + validate_service_type(test_service) + with pytest.raises(UnrecognisedServiceType): + test_service = {"type": "FunkyBob", "name": "test service"} + validate_service_type(test_service) + + async def test_auth_failure(hass: HomeAssistant) -> None: """Test init with an authentication failure.""" with patch( @@ -39,3 +55,9 @@ async def test_service_failure(hass: HomeAssistant) -> None: """Test init with a invalid service.""" entry = await setup_platform(hass, usage_effect=UnrecognisedServiceType()) assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_not_pydantic2() -> None: + """Test that Home Assistant still does not support Pydantic 2.""" + """For PR#99077 and validate_service_type backport""" + assert pydantic.__version__ < "2" From 794071449abe949ae99221d90c8003c5f06ca098 Mon Sep 17 00:00:00 2001 From: tronikos Date: Wed, 30 Aug 2023 21:36:07 -0700 Subject: [PATCH 003/202] Opower MFA fixes (#99317) opower mfa fixes --- .../components/opower/config_flow.py | 24 +++++++++---------- .../components/opower/coordinator.py | 2 +- homeassistant/components/opower/manifest.json | 2 +- homeassistant/components/opower/strings.json | 11 ++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/opower/test_config_flow.py | 4 ++-- 7 files changed, 25 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/opower/config_flow.py b/homeassistant/components/opower/config_flow.py index 9f2ec56423d..d456fc536e5 100644 --- a/homeassistant/components/opower/config_flow.py +++ b/homeassistant/components/opower/config_flow.py @@ -5,7 +5,13 @@ from collections.abc import Mapping import logging from typing import Any -from opower import CannotConnect, InvalidAuth, Opower, get_supported_utility_names +from opower import ( + CannotConnect, + InvalidAuth, + Opower, + get_supported_utility_names, + select_utility, +) import voluptuous as vol from homeassistant import config_entries @@ -20,9 +26,7 @@ _LOGGER = logging.getLogger(__name__) STEP_USER_DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_UTILITY): vol.In( - get_supported_utility_names(supports_mfa=True) - ), + vol.Required(CONF_UTILITY): vol.In(get_supported_utility_names()), vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, } @@ -38,7 +42,7 @@ async def _validate_login( login_data[CONF_UTILITY], login_data[CONF_USERNAME], login_data[CONF_PASSWORD], - login_data.get(CONF_TOTP_SECRET, None), + login_data.get(CONF_TOTP_SECRET), ) errors: dict[str, str] = {} try: @@ -50,12 +54,6 @@ async def _validate_login( return errors -@callback -def _supports_mfa(utility: str) -> bool: - """Return whether the utility supports MFA.""" - return utility not in get_supported_utility_names(supports_mfa=False) - - class OpowerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Opower.""" @@ -78,7 +76,7 @@ class OpowerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_USERNAME: user_input[CONF_USERNAME], } ) - if _supports_mfa(user_input[CONF_UTILITY]): + if select_utility(user_input[CONF_UTILITY]).accepts_mfa(): self.utility_info = user_input return await self.async_step_mfa() @@ -154,7 +152,7 @@ class OpowerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): vol.Required(CONF_USERNAME): self.reauth_entry.data[CONF_USERNAME], vol.Required(CONF_PASSWORD): str, } - if _supports_mfa(self.reauth_entry.data[CONF_UTILITY]): + if select_utility(self.reauth_entry.data[CONF_UTILITY]).accepts_mfa(): schema[vol.Optional(CONF_TOTP_SECRET)] = str return self.async_show_form( step_id="reauth_confirm", diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index 1410b62b7b6..5ce35e949af 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -55,7 +55,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): entry_data[CONF_UTILITY], entry_data[CONF_USERNAME], entry_data[CONF_PASSWORD], - entry_data.get(CONF_TOTP_SECRET, None), + entry_data.get(CONF_TOTP_SECRET), ) async def _async_update_data( diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index fb4ff5153ec..05e89ea96d4 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.0.32"] + "requirements": ["opower==0.0.33"] } diff --git a/homeassistant/components/opower/strings.json b/homeassistant/components/opower/strings.json index ac931bf9308..362e6cd7596 100644 --- a/homeassistant/components/opower/strings.json +++ b/homeassistant/components/opower/strings.json @@ -5,8 +5,13 @@ "data": { "utility": "Utility name", "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]", - "totp_secret": "TOTP Secret (only for some utilities, see documentation)" + "password": "[%key:common::config_flow::data::password%]" + } + }, + "mfa": { + "description": "The TOTP secret below is not one of the 6 digit time-based numeric codes. It is a string of around 16 characters containing the shared secret that enables your authenticator app to generate the correct time-based code at the appropriate time. See the documentation.", + "data": { + "totp_secret": "TOTP Secret" } }, "reauth_confirm": { @@ -14,7 +19,7 @@ "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "totp_secret": "TOTP Secret (only for some utilities, see documentation)" + "totp_secret": "TOTP Secret" } } }, diff --git a/requirements_all.txt b/requirements_all.txt index 35394fafa95..56f75109423 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1374,7 +1374,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.32 +opower==0.0.33 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40b6c4ace56..69e6a9043a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1040,7 +1040,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.32 +opower==0.0.33 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/tests/components/opower/test_config_flow.py b/tests/components/opower/test_config_flow.py index 0391e42ca16..f9ae457a80e 100644 --- a/tests/components/opower/test_config_flow.py +++ b/tests/components/opower/test_config_flow.py @@ -300,7 +300,7 @@ async def test_form_valid_reauth( assert result["reason"] == "reauth_successful" await hass.async_block_till_done() - assert hass.config_entries.async_entries(DOMAIN)[0].data == { + assert mock_config_entry.data == { "utility": "Pacific Gas and Electric Company (PG&E)", "username": "test-username", "password": "test-password2", @@ -350,7 +350,7 @@ async def test_form_valid_reauth_with_mfa( assert result["reason"] == "reauth_successful" await hass.async_block_till_done() - assert hass.config_entries.async_entries(DOMAIN)[0].data == { + assert mock_config_entry.data == { "utility": "Consolidated Edison (ConEd)", "username": "test-username", "password": "test-password2", From 52f8dbf25bc3d698b5a163e17a8777cda8553a70 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 31 Aug 2023 16:50:53 +0200 Subject: [PATCH 004/202] Add documentation URL for homeassistant_yellow (#99336) * Add documentation URL for homeassistant_yellow * Fix test * Tweak --- homeassistant/components/homeassistant_yellow/hardware.py | 3 ++- tests/components/homeassistant_yellow/test_hardware.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homeassistant_yellow/hardware.py b/homeassistant/components/homeassistant_yellow/hardware.py index b67eb50ff2c..0749ca8edc6 100644 --- a/homeassistant/components/homeassistant_yellow/hardware.py +++ b/homeassistant/components/homeassistant_yellow/hardware.py @@ -9,6 +9,7 @@ from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN BOARD_NAME = "Home Assistant Yellow" +DOCUMENTATION_URL = "https://yellow.home-assistant.io/documentation/" MANUFACTURER = "homeassistant" MODEL = "yellow" @@ -39,6 +40,6 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: config_entries=config_entries, dongle=None, name=BOARD_NAME, - url=None, + url=DOCUMENTATION_URL, ) ] diff --git a/tests/components/homeassistant_yellow/test_hardware.py b/tests/components/homeassistant_yellow/test_hardware.py index 5fa0e73d82c..5fb662471aa 100644 --- a/tests/components/homeassistant_yellow/test_hardware.py +++ b/tests/components/homeassistant_yellow/test_hardware.py @@ -54,7 +54,7 @@ async def test_hardware_info( "config_entries": [config_entry.entry_id], "dongle": None, "name": "Home Assistant Yellow", - "url": None, + "url": "https://yellow.home-assistant.io/documentation/", } ] } From 3066d70809ed0918dfa215fd7770760b5f87281c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 30 Aug 2023 14:56:19 -0400 Subject: [PATCH 005/202] Bump ZHA dependencies (#99341) * Bump ZHA dependencies * Include bellows as well --- homeassistant/components/zha/manifest.json | 6 +++--- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 4f23945b105..cd0dc2db5ae 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -20,12 +20,12 @@ "zigpy_znp" ], "requirements": [ - "bellows==0.35.9", + "bellows==0.36.1", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.102", + "zha-quirks==0.0.103", "zigpy-deconz==0.21.0", - "zigpy==0.56.4", + "zigpy==0.57.0", "zigpy-xbee==0.18.1", "zigpy-zigate==0.11.0", "zigpy-znp==0.11.4" diff --git a/requirements_all.txt b/requirements_all.txt index 56f75109423..264404dd225 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -509,7 +509,7 @@ beautifulsoup4==4.12.2 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.35.9 +bellows==0.36.1 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.0 @@ -2769,7 +2769,7 @@ zeroconf==0.88.0 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.102 +zha-quirks==0.0.103 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.9 @@ -2790,7 +2790,7 @@ zigpy-zigate==0.11.0 zigpy-znp==0.11.4 # homeassistant.components.zha -zigpy==0.56.4 +zigpy==0.57.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69e6a9043a1..49b8ad76018 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -430,7 +430,7 @@ base36==0.1.1 beautifulsoup4==4.12.2 # homeassistant.components.zha -bellows==0.35.9 +bellows==0.36.1 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.0 @@ -2039,7 +2039,7 @@ zeroconf==0.88.0 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.102 +zha-quirks==0.0.103 # homeassistant.components.zha zigpy-deconz==0.21.0 @@ -2054,7 +2054,7 @@ zigpy-zigate==0.11.0 zigpy-znp==0.11.4 # homeassistant.components.zha -zigpy==0.56.4 +zigpy==0.57.0 # homeassistant.components.zwave_js zwave-js-server-python==0.51.0 From 316f89beadbde976e6106c3a45f24303bae614dc Mon Sep 17 00:00:00 2001 From: Austin Brunkhorst Date: Thu, 31 Aug 2023 02:15:45 -0700 Subject: [PATCH 006/202] Update pysnooz to 0.8.6 (#99368) --- homeassistant/components/snooz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/snooz/manifest.json b/homeassistant/components/snooz/manifest.json index cd132d5a175..5b43aa7e92d 100644 --- a/homeassistant/components/snooz/manifest.json +++ b/homeassistant/components/snooz/manifest.json @@ -14,5 +14,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/snooz", "iot_class": "local_push", - "requirements": ["pysnooz==0.8.3"] + "requirements": ["pysnooz==0.8.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 264404dd225..9cf0a7abba1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2035,7 +2035,7 @@ pysml==0.0.12 pysnmplib==5.0.21 # homeassistant.components.snooz -pysnooz==0.8.3 +pysnooz==0.8.6 # homeassistant.components.soma pysoma==0.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49b8ad76018..864a661e72d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1518,7 +1518,7 @@ pysml==0.0.12 pysnmplib==5.0.21 # homeassistant.components.snooz -pysnooz==0.8.3 +pysnooz==0.8.6 # homeassistant.components.soma pysoma==0.0.12 From eb423c39b63f7abdcb8de2b3a9b158c86c37364c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 31 Aug 2023 15:16:32 +0200 Subject: [PATCH 007/202] Improve template sensor config flow validation (#99373) --- .../components/template/config_flow.py | 27 +++++++--- tests/components/template/test_config_flow.py | 54 +++++++++++++++++-- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index b89b3cbc91d..b2ccddedad8 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -160,32 +160,43 @@ def _validate_unit(options: dict[str, Any]) -> None: and (units := DEVICE_CLASS_UNITS.get(device_class)) is not None and (unit := options.get(CONF_UNIT_OF_MEASUREMENT)) not in units ): - units_string = sorted( - [str(unit) if unit else "no unit of measurement" for unit in units], + sorted_units = sorted( + [f"'{str(unit)}'" if unit else "no unit of measurement" for unit in units], key=str.casefold, ) + if len(sorted_units) == 1: + units_string = sorted_units[0] + else: + units_string = f"one of {', '.join(sorted_units)}" raise vol.Invalid( f"'{unit}' is not a valid unit for device class '{device_class}'; " - f"expected one of {', '.join(units_string)}" + f"expected {units_string}" ) def _validate_state_class(options: dict[str, Any]) -> None: """Validate state class.""" if ( - (device_class := options.get(CONF_DEVICE_CLASS)) + (state_class := options.get(CONF_STATE_CLASS)) + and (device_class := options.get(CONF_DEVICE_CLASS)) and (state_classes := DEVICE_CLASS_STATE_CLASSES.get(device_class)) is not None - and (state_class := options.get(CONF_STATE_CLASS)) not in state_classes + and state_class not in state_classes ): - state_classes_string = sorted( - [str(state_class) for state_class in state_classes], + sorted_state_classes = sorted( + [f"'{str(state_class)}'" for state_class in state_classes], key=str.casefold, ) + if len(sorted_state_classes) == 0: + state_classes_string = "no state class" + elif len(sorted_state_classes) == 1: + state_classes_string = sorted_state_classes[0] + else: + state_classes_string = f"one of {', '.join(sorted_state_classes)}" raise vol.Invalid( f"'{state_class}' is not a valid state class for device class " - f"'{device_class}'; expected one of {', '.join(state_classes_string)}" + f"'{device_class}'; expected {state_classes_string}" ) diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index dd283ff9214..ba939f3b8d1 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -349,18 +349,62 @@ EARLY_END_ERROR = "invalid template (TemplateSyntaxError: unexpected 'end of tem [ ("binary_sensor", "{{", {}, {"state": EARLY_END_ERROR}), ("sensor", "{{", {}, {"state": EARLY_END_ERROR}), + ( + "sensor", + "", + {"device_class": "aqi", "unit_of_measurement": "cats"}, + { + "unit_of_measurement": ( + "'cats' is not a valid unit for device class 'aqi'; " + "expected no unit of measurement" + ), + }, + ), ( "sensor", "", {"device_class": "temperature", "unit_of_measurement": "cats"}, { - "state_class": ( - "'None' is not a valid state class for device class 'temperature'; " - "expected one of measurement" - ), "unit_of_measurement": ( "'cats' is not a valid unit for device class 'temperature'; " - "expected one of K, °C, °F" + "expected one of 'K', '°C', '°F'" + ), + }, + ), + ( + "sensor", + "", + {"device_class": "timestamp", "state_class": "measurement"}, + { + "state_class": ( + "'measurement' is not a valid state class for device class " + "'timestamp'; expected no state class" + ), + }, + ), + ( + "sensor", + "", + {"device_class": "aqi", "state_class": "total"}, + { + "state_class": ( + "'total' is not a valid state class for device class " + "'aqi'; expected 'measurement'" + ), + }, + ), + ( + "sensor", + "", + {"device_class": "energy", "state_class": "measurement"}, + { + "state_class": ( + "'measurement' is not a valid state class for device class " + "'energy'; expected one of 'total', 'total_increasing'" + ), + "unit_of_measurement": ( + "'None' is not a valid unit for device class 'energy'; " + "expected one of 'GJ', 'kWh', 'MJ', 'MWh', 'Wh'" ), }, ), From a0d03d6bb1100362505ce1aeee9a769b09d3e23e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 31 Aug 2023 03:33:57 -0400 Subject: [PATCH 008/202] Revert orjson to 3.9.2 (#99374) * Revert "Update orjson to 3.9.4 (#98108)" This reverts commit 3dd377cb2a0b60593a18767a5e4b032f5630fd78. * Revert "Update orjson to 3.9.3 (#97930)" This reverts commit d993aa59ea097b25084a5fde2730a576eb13b7b5. --- 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 949181c7ddd..0994ca657ba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ janus==1.0.0 Jinja2==3.1.2 lru-dict==1.2.0 mutagen==1.46.0 -orjson==3.9.4 +orjson==3.9.2 packaging>=23.1 paho-mqtt==1.6.1 Pillow==10.0.0 diff --git a/pyproject.toml b/pyproject.toml index 575c96234bb..c8aa4f7566f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ "cryptography==41.0.3", # pyOpenSSL 23.2.0 is required to work with cryptography 41+ "pyOpenSSL==23.2.0", - "orjson==3.9.4", + "orjson==3.9.2", "packaging>=23.1", "pip>=21.3.1", "python-slugify==4.0.1", diff --git a/requirements.txt b/requirements.txt index 10220697390..e7a3b0fc4c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ lru-dict==1.2.0 PyJWT==2.8.0 cryptography==41.0.3 pyOpenSSL==23.2.0 -orjson==3.9.4 +orjson==3.9.2 packaging>=23.1 pip>=21.3.1 python-slugify==4.0.1 From 97b0815122976a6c54895fc3135f2d09e000b6ad Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 31 Aug 2023 10:39:24 +0200 Subject: [PATCH 009/202] Add documentation URL for homeassistant_sky_connect (#99377) --- .../components/homeassistant_sky_connect/hardware.py | 3 ++- tests/components/homeassistant_sky_connect/test_hardware.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homeassistant_sky_connect/hardware.py b/homeassistant/components/homeassistant_sky_connect/hardware.py index 217a6e57543..bd752278397 100644 --- a/homeassistant/components/homeassistant_sky_connect/hardware.py +++ b/homeassistant/components/homeassistant_sky_connect/hardware.py @@ -6,6 +6,7 @@ from homeassistant.core import HomeAssistant, callback from .const import DOMAIN +DOCUMENTATION_URL = "https://skyconnect.home-assistant.io/documentation/" DONGLE_NAME = "Home Assistant SkyConnect" @@ -26,7 +27,7 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: description=entry.data["description"], ), name=DONGLE_NAME, - url=None, + url=DOCUMENTATION_URL, ) for entry in entries ] diff --git a/tests/components/homeassistant_sky_connect/test_hardware.py b/tests/components/homeassistant_sky_connect/test_hardware.py index 5ddddfc637b..ca9a7887040 100644 --- a/tests/components/homeassistant_sky_connect/test_hardware.py +++ b/tests/components/homeassistant_sky_connect/test_hardware.py @@ -78,7 +78,7 @@ async def test_hardware_info( "description": "bla_description", }, "name": "Home Assistant SkyConnect", - "url": None, + "url": "https://skyconnect.home-assistant.io/documentation/", }, { "board": None, @@ -91,7 +91,7 @@ async def test_hardware_info( "description": "bla_description_2", }, "name": "Home Assistant SkyConnect", - "url": None, + "url": "https://skyconnect.home-assistant.io/documentation/", }, ] } From d1c154fc0dd3fa412754aeb5ee510710a9ae456f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 31 Aug 2023 10:24:03 +0200 Subject: [PATCH 010/202] Revert "Sonos add yaml config issue" (#99379) Revert "Sonos add yaml config issue (#97365)" This reverts commit 2299430dbeb470ff8b5a62fae1fa80fbfc3f014f. --- homeassistant/components/sonos/__init__.py | 23 +--------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 259a9f54044..e6b328cbcb0 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -25,13 +25,7 @@ from homeassistant.components import ssdp from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import ( - CALLBACK_TYPE, - DOMAIN as HOMEASSISTANT_DOMAIN, - Event, - HomeAssistant, - callback, -) +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -39,7 +33,6 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later, async_track_time_interval -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from .alarms import SonosAlarms @@ -132,20 +125,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) ) - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.2.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Sonos", - }, - ) return True From db8980246bf4ca04b96fc8af0245b254fa5a3ffe Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 31 Aug 2023 14:29:24 +0200 Subject: [PATCH 011/202] Add entity component translation for water heater away mode attribute (#99394) --- homeassistant/components/water_heater/strings.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/water_heater/strings.json b/homeassistant/components/water_heater/strings.json index 5ddb61d28b0..6991d371bd3 100644 --- a/homeassistant/components/water_heater/strings.json +++ b/homeassistant/components/water_heater/strings.json @@ -16,6 +16,15 @@ "high_demand": "High Demand", "heat_pump": "Heat Pump", "performance": "Performance" + }, + "state_attributes": { + "away_mode": { + "name": "Away mode", + "state": { + "off": "Off", + "on": "On" + } + } } } }, From 9836d17c92f8c86cf539e9547cab2a44d8a737dd Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 31 Aug 2023 15:32:37 +0200 Subject: [PATCH 012/202] Update frontend to 20230831.0 (#99405) --- 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 06b6da85e19..a31faaf362e 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230830.0"] + "requirements": ["home-assistant-frontend==20230831.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0994ca657ba..3dccb80d11e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.70.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 -home-assistant-frontend==20230830.0 +home-assistant-frontend==20230831.0 home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9cf0a7abba1..9084b181383 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -994,7 +994,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230830.0 +home-assistant-frontend==20230831.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 864a661e72d..88a25dffed0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230830.0 +home-assistant-frontend==20230831.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 From a603a99bd6e6a3accdb65d9e355348d8fa3a8c30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 31 Aug 2023 16:43:32 +0200 Subject: [PATCH 013/202] Add remote alias to connection info response (#99410) --- homeassistant/components/cloud/client.py | 1 + tests/components/cloud/test_client.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 6fbcfc30f69..c216ec85c5c 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -221,6 +221,7 @@ class CloudClient(Interface): "connected": self.cloud.remote.is_connected, "enabled": self._prefs.remote_enabled, "instance_domain": self.cloud.remote.instance_domain, + "alias": self.cloud.remote.alias, }, "version": HA_VERSION, } diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 50cfce3f9a9..e205ba5f6e8 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -365,6 +365,11 @@ async def test_cloud_connection_info(hass: HomeAssistant) -> None: response = await cloud.client.async_cloud_connection_info({}) assert response == { - "remote": {"connected": False, "enabled": False, "instance_domain": None}, + "remote": { + "connected": False, + "enabled": False, + "instance_domain": None, + "alias": None, + }, "version": HA_VERSION, } From 8284c288bf637c94ffa38c8dcdb9862c55061868 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 31 Aug 2023 17:19:10 +0200 Subject: [PATCH 014/202] Bump version to 2023.9.0b1 --- 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 30b6e4a29cb..0b1cc1c5ea0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index c8aa4f7566f..210c2973d4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.0b0" +version = "2023.9.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 0bae0824b42ee4f5e18cff36ce3b3ddc99487bc0 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 31 Aug 2023 12:09:46 -0400 Subject: [PATCH 015/202] Initialize ZHA device database before connecting to the radio (#98082) * Create ZHA entities before attempting to connect to the coordinator * Delete the ZHA gateway object when unloading the config entry * Only load ZHA groups if the coordinator device info is known offline * Do not create a coordinator ZHA device until it is ready * [WIP] begin fixing unit tests * [WIP] Fix existing unit tests (one failure left) * Fix remaining unit test --- homeassistant/components/zha/__init__.py | 20 +---- homeassistant/components/zha/core/const.py | 1 - homeassistant/components/zha/core/gateway.py | 53 ++++++++++--- homeassistant/components/zha/core/helpers.py | 6 +- tests/components/zha/common.py | 10 +-- tests/components/zha/conftest.py | 26 ++++++- tests/components/zha/test_api.py | 2 +- tests/components/zha/test_gateway.py | 79 +++++++++++++------- tests/components/zha/test_websocket_api.py | 11 ++- 9 files changed, 133 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index a51d6f387e1..e48f8ce2096 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -10,7 +10,7 @@ from zhaquirks import setup as setup_quirks from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_TYPE, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv @@ -33,7 +33,6 @@ from .core.const import ( DATA_ZHA, DATA_ZHA_CONFIG, DATA_ZHA_GATEWAY, - DATA_ZHA_SHUTDOWN_TASK, DOMAIN, PLATFORMS, SIGNAL_ADD_ENTITIES, @@ -137,6 +136,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() + config_entry.async_on_unload(zha_gateway.shutdown) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -149,15 +150,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b websocket_api.async_load_api(hass) - async def async_zha_shutdown(event): - """Handle shutdown tasks.""" - zha_gateway: ZHAGateway = zha_data[DATA_ZHA_GATEWAY] - await zha_gateway.shutdown() - - zha_data[DATA_ZHA_SHUTDOWN_TASK] = hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, async_zha_shutdown - ) - await zha_gateway.async_initialize_devices_and_entities() await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) async_dispatcher_send(hass, SIGNAL_ADD_ENTITIES) @@ -167,12 +159,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload ZHA config entry.""" try: - zha_gateway: ZHAGateway = hass.data[DATA_ZHA].pop(DATA_ZHA_GATEWAY) + del hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] except KeyError: return False - await zha_gateway.shutdown() - GROUP_PROBE.cleanup() websocket_api.async_unload_api(hass) @@ -184,8 +174,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> ) ) - hass.data[DATA_ZHA][DATA_ZHA_SHUTDOWN_TASK]() - return True diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 7aab6112ab0..63b59e9d8d4 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -187,7 +187,6 @@ DATA_ZHA_CONFIG = "config" DATA_ZHA_BRIDGE_ID = "zha_bridge_id" DATA_ZHA_CORE_EVENTS = "zha_core_events" DATA_ZHA_GATEWAY = "zha_gateway" -DATA_ZHA_SHUTDOWN_TASK = "zha_shutdown_task" DEBUG_COMP_BELLOWS = "bellows" DEBUG_COMP_ZHA = "homeassistant.components.zha" diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 1320e77ba3c..3abf1274f98 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -148,7 +148,6 @@ class ZHAGateway: self._log_relay_handler = LogRelayHandler(hass, self) self.config_entry = config_entry self._unsubs: list[Callable[[], None]] = [] - self.initialized: bool = False def get_application_controller_data(self) -> tuple[ControllerApplication, dict]: """Get an uninitialized instance of a zigpy `ControllerApplication`.""" @@ -199,12 +198,32 @@ class ZHAGateway: self.ha_entity_registry = er.async_get(self._hass) app_controller_cls, app_config = self.get_application_controller_data() + self.application_controller = await app_controller_cls.new( + config=app_config, + auto_form=False, + start_radio=False, + ) + + self._hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self + + self.async_load_devices() + + # Groups are attached to the coordinator device so we need to load it early + coordinator = self._find_coordinator_device() + loaded_groups = False + + # We can only load groups early if the coordinator's model info has been stored + # in the zigpy database + if coordinator.model is not None: + self.coordinator_zha_device = self._async_get_or_create_device( + coordinator, restored=True + ) + self.async_load_groups() + loaded_groups = True for attempt in range(STARTUP_RETRIES): try: - self.application_controller = await app_controller_cls.new( - app_config, auto_form=True, start_radio=True - ) + await self.application_controller.startup(auto_form=True) except zigpy.exceptions.TransientConnectionError as exc: raise ConfigEntryNotReady from exc except Exception as exc: # pylint: disable=broad-except @@ -223,21 +242,33 @@ class ZHAGateway: else: break + self.coordinator_zha_device = self._async_get_or_create_device( + self._find_coordinator_device(), restored=True + ) + self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(self.coordinator_ieee) + + # If ZHA groups could not load early, we can safely load them now + if not loaded_groups: + self.async_load_groups() + self.application_controller.add_listener(self) self.application_controller.groups.add_listener(self) - self._hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self - self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(self.coordinator_ieee) - self.async_load_devices() - self.async_load_groups() - self.initialized = True + + def _find_coordinator_device(self) -> zigpy.device.Device: + if last_backup := self.application_controller.backups.most_recent_backup(): + zigpy_coordinator = self.application_controller.get_device( + ieee=last_backup.node_info.ieee + ) + else: + zigpy_coordinator = self.application_controller.get_device(nwk=0x0000) + + return zigpy_coordinator @callback def async_load_devices(self) -> None: """Restore ZHA devices from zigpy application state.""" for zigpy_device in self.application_controller.devices.values(): zha_device = self._async_get_or_create_device(zigpy_device, restored=True) - if zha_device.ieee == self.coordinator_ieee: - self.coordinator_zha_device = zha_device delta_msg = "not known" if zha_device.last_seen is not None: delta = round(time.time() - zha_device.last_seen) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index ac7c15d3ecd..7b0d062738b 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -27,7 +27,6 @@ import zigpy.zdo.types as zdo_types from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, State, callback -from homeassistant.exceptions import IntegrationError from homeassistant.helpers import config_validation as cv, device_registry as dr from .const import ( @@ -246,11 +245,8 @@ def async_get_zha_device(hass: HomeAssistant, device_id: str) -> ZHADevice: _LOGGER.error("Device id `%s` not found in registry", device_id) raise KeyError(f"Device id `{device_id}` not found in registry.") zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - if not zha_gateway.initialized: - _LOGGER.error("Attempting to get a ZHA device when ZHA is not initialized") - raise IntegrationError("ZHA is not initialized yet") try: - ieee_address = list(list(registry_device.identifiers)[0])[1] + ieee_address = list(registry_device.identifiers)[0][1] ieee = zigpy.types.EUI64.convert(ieee_address) except (IndexError, ValueError) as ex: _LOGGER.error( diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 01206c432e6..db1da3721ee 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -87,10 +87,7 @@ def update_attribute_cache(cluster): def get_zha_gateway(hass): """Return ZHA gateway from hass.data.""" - try: - return hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] - except KeyError: - return None + return hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] def make_attribute(attrid, value, status=0): @@ -167,12 +164,9 @@ def find_entity_ids(domain, zha_device, hass): def async_find_group_entity_id(hass, domain, group): """Find the group entity id under test.""" - entity_id = ( - f"{domain}.fakemanufacturer_fakemodel_{group.name.lower().replace(' ', '_')}" - ) + entity_id = f"{domain}.coordinator_manufacturer_coordinator_model_{group.name.lower().replace(' ', '_')}" entity_ids = hass.states.async_entity_ids(domain) - assert entity_id in entity_ids return entity_id diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index dd2c200973c..f690a5152fc 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -16,6 +16,8 @@ import zigpy.profiles import zigpy.quirks import zigpy.types import zigpy.util +from zigpy.zcl.clusters.general import Basic, Groups +from zigpy.zcl.foundation import Status import zigpy.zdo.types as zdo_t import homeassistant.components.zha.core.const as zha_const @@ -116,6 +118,9 @@ def zigpy_app_controller(): { zigpy.config.CONF_DATABASE: None, zigpy.config.CONF_DEVICE: {zigpy.config.CONF_DEVICE_PATH: "/dev/null"}, + zigpy.config.CONF_STARTUP_ENERGY_SCAN: False, + zigpy.config.CONF_NWK_BACKUP_ENABLED: False, + zigpy.config.CONF_TOPO_SCAN_ENABLED: False, } ) @@ -128,9 +133,24 @@ def zigpy_app_controller(): app.state.network_info.channel = 15 app.state.network_info.network_key.key = zigpy.types.KeyData(range(16)) - with patch("zigpy.device.Device.request"), patch.object( - app, "permit", autospec=True - ), patch.object(app, "permit_with_key", autospec=True): + # Create a fake coordinator device + dev = app.add_device(nwk=app.state.node_info.nwk, ieee=app.state.node_info.ieee) + dev.node_desc = zdo_t.NodeDescriptor() + dev.node_desc.logical_type = zdo_t.LogicalType.Coordinator + dev.manufacturer = "Coordinator Manufacturer" + dev.model = "Coordinator Model" + + ep = dev.add_endpoint(1) + ep.add_input_cluster(Basic.cluster_id) + ep.add_input_cluster(Groups.cluster_id) + + with patch( + "zigpy.device.Device.request", return_value=[Status.SUCCESS] + ), patch.object(app, "permit", autospec=True), patch.object( + app, "startup", wraps=app.startup + ), patch.object( + app, "permit_with_key", autospec=True + ): yield app diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index 85f85cc0437..c2cb16efcc8 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -71,7 +71,7 @@ async def test_async_get_network_settings_missing( await setup_zha() gateway = api._get_gateway(hass) - await zha.async_unload_entry(hass, gateway.config_entry) + await gateway.config_entry.async_unload(hass) # Network settings were never loaded for whatever reason zigpy_app_controller.state.network_info = zigpy.state.NetworkInfo() diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index b9fcd4b6932..0f791a08955 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,9 +1,9 @@ """Test ZHA Gateway.""" import asyncio -from typing import Any -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import MagicMock, patch import pytest +from zigpy.application import ControllerApplication import zigpy.exceptions import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general @@ -232,68 +232,89 @@ async def test_gateway_create_group_with_id( ) @patch("homeassistant.components.zha.core.gateway.STARTUP_FAILURE_DELAY_S", 0.01) @pytest.mark.parametrize( - "startup", + "startup_effect", [ - [asyncio.TimeoutError(), FileNotFoundError(), MagicMock()], - [asyncio.TimeoutError(), MagicMock()], - [MagicMock()], + [asyncio.TimeoutError(), FileNotFoundError(), None], + [asyncio.TimeoutError(), None], + [None], ], ) async def test_gateway_initialize_success( - startup: list[Any], + startup_effect: list[Exception | None], hass: HomeAssistant, device_light_1: ZHADevice, coordinator: ZHADevice, + zigpy_app_controller: ControllerApplication, ) -> None: """Test ZHA initializing the gateway successfully.""" zha_gateway = get_zha_gateway(hass) assert zha_gateway is not None - zha_gateway.shutdown = AsyncMock() + zigpy_app_controller.startup.side_effect = startup_effect + zigpy_app_controller.startup.reset_mock() with patch( - "bellows.zigbee.application.ControllerApplication.new", side_effect=startup - ) as mock_new: + "bellows.zigbee.application.ControllerApplication.new", + return_value=zigpy_app_controller, + ): await zha_gateway.async_initialize() - assert mock_new.call_count == len(startup) - + assert zigpy_app_controller.startup.call_count == len(startup_effect) device_light_1.async_cleanup_handles() @patch("homeassistant.components.zha.core.gateway.STARTUP_FAILURE_DELAY_S", 0.01) async def test_gateway_initialize_failure( - hass: HomeAssistant, device_light_1, coordinator + hass: HomeAssistant, + device_light_1: ZHADevice, + coordinator: ZHADevice, + zigpy_app_controller: ControllerApplication, ) -> None: """Test ZHA failing to initialize the gateway.""" zha_gateway = get_zha_gateway(hass) assert zha_gateway is not None + zigpy_app_controller.startup.side_effect = [ + asyncio.TimeoutError(), + RuntimeError(), + FileNotFoundError(), + ] + zigpy_app_controller.startup.reset_mock() + with patch( "bellows.zigbee.application.ControllerApplication.new", - side_effect=[asyncio.TimeoutError(), FileNotFoundError(), RuntimeError()], - ) as mock_new, pytest.raises(RuntimeError): + return_value=zigpy_app_controller, + ), pytest.raises(FileNotFoundError): await zha_gateway.async_initialize() - assert mock_new.call_count == 3 + assert zigpy_app_controller.startup.call_count == 3 @patch("homeassistant.components.zha.core.gateway.STARTUP_FAILURE_DELAY_S", 0.01) async def test_gateway_initialize_failure_transient( - hass: HomeAssistant, device_light_1, coordinator + hass: HomeAssistant, + device_light_1: ZHADevice, + coordinator: ZHADevice, + zigpy_app_controller: ControllerApplication, ) -> None: """Test ZHA failing to initialize the gateway but with a transient error.""" zha_gateway = get_zha_gateway(hass) assert zha_gateway is not None + zigpy_app_controller.startup.side_effect = [ + RuntimeError(), + zigpy.exceptions.TransientConnectionError(), + ] + zigpy_app_controller.startup.reset_mock() + with patch( "bellows.zigbee.application.ControllerApplication.new", - side_effect=[RuntimeError(), zigpy.exceptions.TransientConnectionError()], - ) as mock_new, pytest.raises(ConfigEntryNotReady): + return_value=zigpy_app_controller, + ), pytest.raises(ConfigEntryNotReady): await zha_gateway.async_initialize() # Initialization immediately stops and is retried after TransientConnectionError - assert mock_new.call_count == 2 + assert zigpy_app_controller.startup.call_count == 2 @patch( @@ -313,7 +334,12 @@ async def test_gateway_initialize_failure_transient( ], ) async def test_gateway_initialize_bellows_thread( - device_path, thread_state, config_override, hass: HomeAssistant, coordinator + device_path: str, + thread_state: bool, + config_override: dict, + hass: HomeAssistant, + coordinator: ZHADevice, + zigpy_app_controller: ControllerApplication, ) -> None: """Test ZHA disabling the UART thread when connecting to a TCP coordinator.""" zha_gateway = get_zha_gateway(hass) @@ -324,15 +350,12 @@ async def test_gateway_initialize_bellows_thread( zha_gateway._config.setdefault("zigpy_config", {}).update(config_override) with patch( - "bellows.zigbee.application.ControllerApplication.new" - ) as controller_app_mock: - mock = AsyncMock() - mock.add_listener = MagicMock() - mock.groups = MagicMock() - controller_app_mock.return_value = mock + "bellows.zigbee.application.ControllerApplication.new", + return_value=zigpy_app_controller, + ) as mock_new: await zha_gateway.async_initialize() - assert controller_app_mock.mock_calls[0].args[0]["use_thread"] is thread_state + assert mock_new.mock_calls[0].kwargs["config"]["use_thread"] is thread_state @pytest.mark.parametrize( diff --git a/tests/components/zha/test_websocket_api.py b/tests/components/zha/test_websocket_api.py index 0904fc1f685..740ffd6c06c 100644 --- a/tests/components/zha/test_websocket_api.py +++ b/tests/components/zha/test_websocket_api.py @@ -13,6 +13,7 @@ import zigpy.profiles.zha import zigpy.types from zigpy.types.named import EUI64 import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters.general import Groups import zigpy.zcl.clusters.security as security import zigpy.zdo.types as zdo_types @@ -233,7 +234,7 @@ async def test_list_devices(zha_client) -> None: msg = await zha_client.receive_json() devices = msg["result"] - assert len(devices) == 2 + assert len(devices) == 2 + 1 # the coordinator is included as well msg_id = 100 for device in devices: @@ -371,8 +372,13 @@ async def test_get_group_not_found(zha_client) -> None: assert msg["error"]["code"] == const.ERR_NOT_FOUND -async def test_list_groupable_devices(zha_client, device_groupable) -> None: +async def test_list_groupable_devices( + zha_client, device_groupable, zigpy_app_controller +) -> None: """Test getting ZHA devices that have a group cluster.""" + # Ensure the coordinator doesn't have a group cluster + coordinator = zigpy_app_controller.get_device(nwk=0x0000) + del coordinator.endpoints[1].in_clusters[Groups.cluster_id] await zha_client.send_json({ID: 10, TYPE: "zha/devices/groupable"}) @@ -479,6 +485,7 @@ async def app_controller( ) -> ControllerApplication: """Fixture for zigpy Application Controller.""" await setup_zha() + zigpy_app_controller.permit.reset_mock() return zigpy_app_controller From 2ec9abfd24eaf4f9047d8901356a5ca5e054dbbc Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 1 Sep 2023 09:05:45 -0400 Subject: [PATCH 016/202] Create a ZHA repair when directly accessing a radio with multi-PAN firmware (#98275) * Add the SiLabs flasher as a dependency * Create a repair if the wrong firmware is detected on an EZSP device * Update homeassistant/components/zha/strings.json Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> * Provide the ZHA config entry as a reusable fixture * Create a separate repair when using non-Nabu Casa hardware * Add unit tests * Drop extraneous `config_entry.add_to_hass` added in 021def44 * Fully unit test all edge cases * Move `socket://`-ignoring logic into repair function * Open a repair from ZHA flows when the wrong firmware is running * Fix existing unit tests * Link to the flashing section in the documentation * Reduce repair severity to `ERROR` * Make issue persistent * Add unit tests for new radio probing states * Add unit tests for new config flow steps * Handle probing failure raising an exception * Implement review suggestions * Address review comments --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/zha/__init__.py | 21 +- homeassistant/components/zha/config_flow.py | 26 +- homeassistant/components/zha/manifest.json | 6 +- homeassistant/components/zha/radio_manager.py | 23 +- homeassistant/components/zha/repairs.py | 126 ++++++++++ homeassistant/components/zha/strings.json | 16 +- requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/hassfest/dependencies.py | 1 + tests/components/zha/conftest.py | 33 ++- tests/components/zha/test_config_flow.py | 75 ++++-- tests/components/zha/test_diagnostics.py | 6 +- tests/components/zha/test_radio_manager.py | 63 ++++- tests/components/zha/test_repairs.py | 235 ++++++++++++++++++ 14 files changed, 587 insertions(+), 50 deletions(-) create mode 100644 homeassistant/components/zha/repairs.py create mode 100644 tests/components/zha/test_repairs.py diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index e48f8ce2096..1c4c3e776d0 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -12,13 +12,14 @@ from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TYPE from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import ConfigType -from . import websocket_api +from . import repairs, websocket_api from .core import ZHAGateway from .core.const import ( BAUD_RATES, @@ -134,7 +135,23 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b _LOGGER.debug("ZHA storage file does not exist or was already removed") zha_gateway = ZHAGateway(hass, config, config_entry) - await zha_gateway.async_initialize() + + try: + await zha_gateway.async_initialize() + except Exception: # pylint: disable=broad-except + if RadioType[config_entry.data[CONF_RADIO_TYPE]] == RadioType.ezsp: + try: + await repairs.warn_on_wrong_silabs_firmware( + hass, config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] + ) + except repairs.AlreadyRunningEZSP as exc: + # If connecting fails but we somehow probe EZSP (e.g. stuck in the + # bootloader), reconnect, it should work + raise ConfigEntryNotReady from exc + + raise + + repairs.async_delete_blocking_issues(hass) config_entry.async_on_unload(zha_gateway.shutdown) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 6ac3a155ed9..1b6bbee5159 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -35,6 +35,7 @@ from .core.const import ( from .radio_manager import ( HARDWARE_DISCOVERY_SCHEMA, RECOMMENDED_RADIOS, + ProbeResult, ZhaRadioManager, ) @@ -60,6 +61,8 @@ OPTIONS_INTENT_RECONFIGURE = "intent_reconfigure" UPLOADED_BACKUP_FILE = "uploaded_backup_file" +REPAIR_MY_URL = "https://my.home-assistant.io/redirect/repairs/" + DEFAULT_ZHA_ZEROCONF_PORT = 6638 ESPHOME_API_PORT = 6053 @@ -187,7 +190,13 @@ class BaseZhaFlow(FlowHandler): port = ports[list_of_ports.index(user_selection)] self._radio_mgr.device_path = port.device - if not await self._radio_mgr.detect_radio_type(): + probe_result = await self._radio_mgr.detect_radio_type() + if probe_result == ProbeResult.WRONG_FIRMWARE_INSTALLED: + return self.async_abort( + reason="wrong_firmware_installed", + description_placeholders={"repair_url": REPAIR_MY_URL}, + ) + if probe_result == ProbeResult.PROBING_FAILED: # Did not autodetect anything, proceed to manual selection return await self.async_step_manual_pick_radio_type() @@ -530,10 +539,17 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN # config flow logic that interacts with hardware. if user_input is not None or not onboarding.async_is_onboarded(self.hass): # Probe the radio type if we don't have one yet - if ( - self._radio_mgr.radio_type is None - and not await self._radio_mgr.detect_radio_type() - ): + if self._radio_mgr.radio_type is None: + probe_result = await self._radio_mgr.detect_radio_type() + else: + probe_result = ProbeResult.RADIO_TYPE_DETECTED + + if probe_result == ProbeResult.WRONG_FIRMWARE_INSTALLED: + return self.async_abort( + reason="wrong_firmware_installed", + description_placeholders={"repair_url": REPAIR_MY_URL}, + ) + if probe_result == ProbeResult.PROBING_FAILED: # This path probably will not happen now that we have # more precise USB matching unless there is a problem # with the device diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index cd0dc2db5ae..809b576defa 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -17,7 +17,8 @@ "zigpy_deconz", "zigpy_xbee", "zigpy_zigate", - "zigpy_znp" + "zigpy_znp", + "universal_silabs_flasher" ], "requirements": [ "bellows==0.36.1", @@ -28,7 +29,8 @@ "zigpy==0.57.0", "zigpy-xbee==0.18.1", "zigpy-zigate==0.11.0", - "zigpy-znp==0.11.4" + "zigpy-znp==0.11.4", + "universal-silabs-flasher==0.0.13" ], "usb": [ { diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index 4e70fc2247f..751fea99847 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -5,6 +5,7 @@ import asyncio import contextlib from contextlib import suppress import copy +import enum import logging import os from typing import Any @@ -20,6 +21,7 @@ from homeassistant import config_entries from homeassistant.components import usb from homeassistant.core import HomeAssistant +from . import repairs from .core.const import ( CONF_DATABASE, CONF_RADIO_TYPE, @@ -76,6 +78,14 @@ HARDWARE_MIGRATION_SCHEMA = vol.Schema( _LOGGER = logging.getLogger(__name__) +class ProbeResult(enum.StrEnum): + """Radio firmware probing result.""" + + RADIO_TYPE_DETECTED = "radio_type_detected" + WRONG_FIRMWARE_INSTALLED = "wrong_firmware_installed" + PROBING_FAILED = "probing_failed" + + def _allow_overwrite_ezsp_ieee( backup: zigpy.backups.NetworkBackup, ) -> zigpy.backups.NetworkBackup: @@ -171,8 +181,10 @@ class ZhaRadioManager: return RadioType[radio_type] - async def detect_radio_type(self) -> bool: + async def detect_radio_type(self) -> ProbeResult: """Probe all radio types on the current port.""" + assert self.device_path is not None + for radio in AUTOPROBE_RADIOS: _LOGGER.debug("Attempting to probe radio type %s", radio) @@ -191,9 +203,14 @@ class ZhaRadioManager: self.radio_type = radio self.device_settings = dev_config - return True + repairs.async_delete_blocking_issues(self.hass) + return ProbeResult.RADIO_TYPE_DETECTED - return False + with suppress(repairs.AlreadyRunningEZSP): + if await repairs.warn_on_wrong_silabs_firmware(self.hass, self.device_path): + return ProbeResult.WRONG_FIRMWARE_INSTALLED + + return ProbeResult.PROBING_FAILED async def async_load_network_settings( self, *, create_backup: bool = False diff --git a/homeassistant/components/zha/repairs.py b/homeassistant/components/zha/repairs.py new file mode 100644 index 00000000000..ac523f37aa0 --- /dev/null +++ b/homeassistant/components/zha/repairs.py @@ -0,0 +1,126 @@ +"""ZHA repairs for common environmental and device problems.""" +from __future__ import annotations + +import enum +import logging + +from universal_silabs_flasher.const import ApplicationType +from universal_silabs_flasher.flasher import Flasher + +from homeassistant.components.homeassistant_sky_connect import ( + hardware as skyconnect_hardware, +) +from homeassistant.components.homeassistant_yellow import ( + RADIO_DEVICE as YELLOW_RADIO_DEVICE, + hardware as yellow_hardware, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import issue_registry as ir + +from .core.const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class AlreadyRunningEZSP(Exception): + """The device is already running EZSP firmware.""" + + +class HardwareType(enum.StrEnum): + """Detected Zigbee hardware type.""" + + SKYCONNECT = "skyconnect" + YELLOW = "yellow" + OTHER = "other" + + +DISABLE_MULTIPAN_URL = { + HardwareType.YELLOW: ( + "https://yellow.home-assistant.io/guides/disable-multiprotocol/#flash-the-silicon-labs-radio-firmware" + ), + HardwareType.SKYCONNECT: ( + "https://skyconnect.home-assistant.io/procedures/disable-multiprotocol/#step-flash-the-silicon-labs-radio-firmware" + ), + HardwareType.OTHER: None, +} + +ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED = "wrong_silabs_firmware_installed" + + +def _detect_radio_hardware(hass: HomeAssistant, device: str) -> HardwareType: + """Identify the radio hardware with the given serial port.""" + try: + yellow_hardware.async_info(hass) + except HomeAssistantError: + pass + else: + if device == YELLOW_RADIO_DEVICE: + return HardwareType.YELLOW + + try: + info = skyconnect_hardware.async_info(hass) + except HomeAssistantError: + pass + else: + for hardware_info in info: + for entry_id in hardware_info.config_entries or []: + entry = hass.config_entries.async_get_entry(entry_id) + + if entry is not None and entry.data["device"] == device: + return HardwareType.SKYCONNECT + + return HardwareType.OTHER + + +async def probe_silabs_firmware_type(device: str) -> ApplicationType | None: + """Probe the running firmware on a Silabs device.""" + flasher = Flasher(device=device) + + try: + await flasher.probe_app_type() + except Exception: # pylint: disable=broad-except + _LOGGER.debug("Failed to probe application type", exc_info=True) + + return flasher.app_type + + +async def warn_on_wrong_silabs_firmware(hass: HomeAssistant, device: str) -> bool: + """Create a repair issue if the wrong type of SiLabs firmware is detected.""" + # Only consider actual serial ports + if device.startswith("socket://"): + return False + + app_type = await probe_silabs_firmware_type(device) + + if app_type is None: + # Failed to probe, we can't tell if the wrong firmware is installed + return False + + if app_type == ApplicationType.EZSP: + # If connecting fails but we somehow probe EZSP (e.g. stuck in bootloader), + # reconnect, it should work + raise AlreadyRunningEZSP() + + hardware_type = _detect_radio_hardware(hass, device) + ir.async_create_issue( + hass, + domain=DOMAIN, + issue_id=ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED, + is_fixable=False, + is_persistent=True, + learn_more_url=DISABLE_MULTIPAN_URL[hardware_type], + severity=ir.IssueSeverity.ERROR, + translation_key=( + ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED + + ("_nabucasa" if hardware_type != HardwareType.OTHER else "_other") + ), + translation_placeholders={"firmware_type": app_type.name}, + ) + + return True + + +def async_delete_blocking_issues(hass: HomeAssistant) -> None: + """Delete repair issues that should disappear on a successful startup.""" + ir.async_delete_issue(hass, DOMAIN, ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED) diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 3829ee68bb5..87738e821ea 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -75,7 +75,8 @@ "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "not_zha_device": "This device is not a zha device", - "usb_probe_failed": "Failed to probe the usb device" + "usb_probe_failed": "Failed to probe the usb device", + "wrong_firmware_installed": "Your device is running the wrong firmware and cannot be used with ZHA until the correct firmware is installed. [A repair has been created]({repair_url}) with more information and instructions for how to fix this." } }, "options": { @@ -168,7 +169,8 @@ "abort": { "single_instance_allowed": "[%key:component::zha::config::abort::single_instance_allowed%]", "not_zha_device": "[%key:component::zha::config::abort::not_zha_device%]", - "usb_probe_failed": "[%key:component::zha::config::abort::usb_probe_failed%]" + "usb_probe_failed": "[%key:component::zha::config::abort::usb_probe_failed%]", + "wrong_firmware_installed": "[%key:component::zha::config::abort::wrong_firmware_installed%]" } }, "config_panel": { @@ -502,5 +504,15 @@ } } } + }, + "issues": { + "wrong_silabs_firmware_installed_nabucasa": { + "title": "Zigbee radio with multiprotocol firmware detected", + "description": "Your Zigbee radio was previously used with multiprotocol (Zigbee and Thread) and still has multiprotocol firmware installed: ({firmware_type}). \n Option 1: To run your radio exclusively with ZHA, you need to install the Zigbee firmware:\n - Open the documentation by selecting the link under \"Learn More\".\n -. Follow the instructions described in the step to flash the Zigbee firmware.\n Option 2: To run your radio with multiprotocol, follow these steps: \n - Go to Settings > System > Hardware, select the device and select Configure. \n - Select the Configure IEEE 802.15.4 radio multiprotocol support option. \n - Select the checkbox and select Submit. \n - Once installed, configure the newly discovered ZHA integration." + }, + "wrong_silabs_firmware_installed_other": { + "title": "[%key:component::zha::issues::wrong_silabs_firmware_installed_nabucasa::title%]", + "description": "Your Zigbee radio was previously used with multiprotocol (Zigbee and Thread) and still has multiprotocol firmware installed: ({firmware_type}). To run your radio exclusively with ZHA, you need to install Zigbee firmware. Follow your Zigbee radio manufacturer's instructions for how to do this." + } } } diff --git a/requirements_all.txt b/requirements_all.txt index 9084b181383..bd372977b95 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2611,6 +2611,9 @@ unifi-discovery==1.1.7 # homeassistant.components.unifiled unifiled==0.11 +# homeassistant.components.zha +universal-silabs-flasher==0.0.13 + # homeassistant.components.upb upb-lib==0.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88a25dffed0..7cc452889b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1908,6 +1908,9 @@ ultraheat-api==0.5.1 # homeassistant.components.unifiprotect unifi-discovery==1.1.7 +# homeassistant.components.zha +universal-silabs-flasher==0.0.13 + # homeassistant.components.upb upb-lib==0.5.4 diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index c0733841ed5..31fd31dfc96 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -149,6 +149,7 @@ IGNORE_VIOLATIONS = { ("http", "network"), # This would be a circular dep ("zha", "homeassistant_hardware"), + ("zha", "homeassistant_sky_connect"), ("zha", "homeassistant_yellow"), # This should become a helper method that integrations can submit data to ("websocket_api", "lovelace"), diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index f690a5152fc..4778f3216da 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -1,5 +1,5 @@ """Test configuration for the ZHA component.""" -from collections.abc import Callable +from collections.abc import Callable, Generator import itertools import time from unittest.mock import AsyncMock, MagicMock, patch @@ -155,10 +155,10 @@ def zigpy_app_controller(): @pytest.fixture(name="config_entry") -async def config_entry_fixture(hass): +async def config_entry_fixture(hass) -> MockConfigEntry: """Fixture representing a config entry.""" - entry = MockConfigEntry( - version=2, + return MockConfigEntry( + version=3, domain=zha_const.DOMAIN, data={ zigpy.config.CONF_DEVICE: {zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB0"}, @@ -178,23 +178,30 @@ async def config_entry_fixture(hass): } }, ) - entry.add_to_hass(hass) - return entry @pytest.fixture -def setup_zha(hass, config_entry, zigpy_app_controller): +def mock_zigpy_connect( + zigpy_app_controller: ControllerApplication, +) -> Generator[ControllerApplication, None, None]: + """Patch the zigpy radio connection with our mock application.""" + with patch( + "bellows.zigbee.application.ControllerApplication.new", + return_value=zigpy_app_controller, + ) as mock_app: + yield mock_app + + +@pytest.fixture +def setup_zha(hass, config_entry: MockConfigEntry, mock_zigpy_connect): """Set up ZHA component.""" zha_config = {zha_const.CONF_ENABLE_QUIRKS: False} - p1 = patch( - "bellows.zigbee.application.ControllerApplication.new", - return_value=zigpy_app_controller, - ) - async def _setup(config=None): + config_entry.add_to_hass(hass) config = config or {} - with p1: + + with mock_zigpy_connect: status = await async_setup_component( hass, zha_const.DOMAIN, {zha_const.DOMAIN: {**zha_config, **config}} ) diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 8e071247872..77d8a615c72 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -26,6 +26,7 @@ from homeassistant.components.zha.core.const import ( EZSP_OVERWRITE_EUI64, RadioType, ) +from homeassistant.components.zha.radio_manager import ProbeResult from homeassistant.config_entries import ( SOURCE_SSDP, SOURCE_USB, @@ -114,7 +115,10 @@ def backup(make_backup): return make_backup() -def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): +def mock_detect_radio_type( + radio_type: RadioType = RadioType.ezsp, + ret: ProbeResult = ProbeResult.RADIO_TYPE_DETECTED, +): """Mock `detect_radio_type` that just sets the appropriate attributes.""" async def detect(self): @@ -489,8 +493,11 @@ async def test_zigate_discovery_via_usb(probe_mock, hass: HomeAssistant) -> None } -@patch(f"bellows.{PROBE_FUNCTION_PATH}", return_value=False) -async def test_discovery_via_usb_no_radio(probe_mock, hass: HomeAssistant) -> None: +@patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", + AsyncMock(return_value=ProbeResult.PROBING_FAILED), +) +async def test_discovery_via_usb_no_radio(hass: HomeAssistant) -> None: """Test usb flow -- no radio detected.""" discovery_info = usb.UsbServiceInfo( device="/dev/null", @@ -759,7 +766,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: @patch( "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", - mock_detect_radio_type(ret=False), + AsyncMock(return_value=ProbeResult.PROBING_FAILED), ) @patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) async def test_user_flow_not_detected(hass: HomeAssistant) -> None: @@ -851,6 +858,7 @@ async def test_detect_radio_type_success( handler = config_flow.ZhaConfigFlowHandler() handler._radio_mgr.device_path = "/dev/null" + handler.hass = hass await handler._radio_mgr.detect_radio_type() @@ -879,6 +887,8 @@ async def test_detect_radio_type_success_with_settings( handler = config_flow.ZhaConfigFlowHandler() handler._radio_mgr.device_path = "/dev/null" + handler.hass = hass + await handler._radio_mgr.detect_radio_type() assert handler._radio_mgr.radio_type == RadioType.ezsp @@ -956,22 +966,10 @@ async def test_user_port_config(probe_mock, hass: HomeAssistant) -> None: ], ) async def test_migration_ti_cc_to_znp( - old_type, new_type, hass: HomeAssistant, config_entry + old_type, new_type, hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test zigpy-cc to zigpy-znp config migration.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id=old_type + new_type, - data={ - CONF_RADIO_TYPE: old_type, - CONF_DEVICE: { - CONF_DEVICE_PATH: "/dev/ttyUSB1", - CONF_BAUDRATE: 115200, - CONF_FLOWCONTROL: None, - }, - }, - ) - + config_entry.data = {**config_entry.data, CONF_RADIO_TYPE: old_type} config_entry.version = 2 config_entry.add_to_hass(hass) @@ -1919,3 +1917,44 @@ async def test_config_flow_port_multiprotocol_port_name(hass: HomeAssistant) -> result["data_schema"].schema["path"].container[0] == "socket://core-silabs-multiprotocol:9999 - Multiprotocol add-on - Nabu Casa" ) + + +@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) +async def test_probe_wrong_firmware_installed(hass: HomeAssistant) -> None: + """Test auto-probing failing because the wrong firmware is installed.""" + + with patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", + return_value=ProbeResult.WRONG_FIRMWARE_INSTALLED, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: "choose_serial_port"}, + data={ + CONF_DEVICE_PATH: ( + "/dev/ttyUSB1234 - Some serial port, s/n: 1234 - Virtual serial port" + ) + }, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "wrong_firmware_installed" + + +async def test_discovery_wrong_firmware_installed(hass: HomeAssistant) -> None: + """Test auto-probing failing because the wrong firmware is installed.""" + + with patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", + return_value=ProbeResult.WRONG_FIRMWARE_INSTALLED, + ), patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: "confirm"}, + data={}, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "wrong_firmware_installed" diff --git a/tests/components/zha/test_diagnostics.py b/tests/components/zha/test_diagnostics.py index 0bb06ea723b..6bcb321ab14 100644 --- a/tests/components/zha/test_diagnostics.py +++ b/tests/components/zha/test_diagnostics.py @@ -15,6 +15,7 @@ from homeassistant.helpers.device_registry import async_get from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE +from tests.common import MockConfigEntry from tests.components.diagnostics import ( get_diagnostics_for_config_entry, get_diagnostics_for_device, @@ -57,7 +58,7 @@ def zigpy_device(zigpy_device_mock): async def test_diagnostics_for_config_entry( hass: HomeAssistant, hass_client: ClientSessionGenerator, - config_entry, + config_entry: MockConfigEntry, zha_device_joined, zigpy_device, ) -> None: @@ -86,12 +87,11 @@ async def test_diagnostics_for_config_entry( async def test_diagnostics_for_device( hass: HomeAssistant, hass_client: ClientSessionGenerator, - config_entry, + config_entry: MockConfigEntry, zha_device_joined, zigpy_device, ) -> None: """Test diagnostics for device.""" - zha_device: ZHADevice = await zha_device_joined(zigpy_device) dev_reg = async_get(hass) device = dev_reg.async_get_device(identifiers={("zha", str(zha_device.ieee))}) diff --git a/tests/components/zha/test_radio_manager.py b/tests/components/zha/test_radio_manager.py index c507db3e6ab..7acf9219d67 100644 --- a/tests/components/zha/test_radio_manager.py +++ b/tests/components/zha/test_radio_manager.py @@ -14,6 +14,7 @@ from homeassistant import config_entries from homeassistant.components.usb import UsbServiceInfo from homeassistant.components.zha import radio_manager from homeassistant.components.zha.core.const import DOMAIN, RadioType +from homeassistant.components.zha.radio_manager import ProbeResult, ZhaRadioManager from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -59,10 +60,13 @@ def backup(): return backup -def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): +def mock_detect_radio_type( + radio_type: RadioType = RadioType.ezsp, + ret: ProbeResult = ProbeResult.RADIO_TYPE_DETECTED, +): """Mock `detect_radio_type` that just sets the appropriate attributes.""" - async def detect(self): + async def detect(self) -> ProbeResult: self.radio_type = radio_type self.device_settings = radio_type.controller.SCHEMA_DEVICE( {CONF_DEVICE_PATH: self.device_path} @@ -421,3 +425,58 @@ async def test_migrate_initiate_failure( await migration_helper.async_initiate_migration(migration_data) assert len(mock_load_info.mock_calls) == radio_manager.BACKUP_RETRIES + + +@pytest.fixture(name="radio_manager") +def zha_radio_manager(hass: HomeAssistant) -> ZhaRadioManager: + """Fixture for an instance of `ZhaRadioManager`.""" + radio_manager = ZhaRadioManager() + radio_manager.hass = hass + radio_manager.device_path = "/dev/ttyZigbee" + return radio_manager + + +async def test_detect_radio_type_success(radio_manager: ZhaRadioManager) -> None: + """Test radio type detection, success.""" + with patch( + "bellows.zigbee.application.ControllerApplication.probe", return_value=False + ), patch( + # Intentionally probe only the second radio type + "zigpy_znp.zigbee.application.ControllerApplication.probe", + return_value=True, + ): + assert ( + await radio_manager.detect_radio_type() == ProbeResult.RADIO_TYPE_DETECTED + ) + assert radio_manager.radio_type == RadioType.znp + + +async def test_detect_radio_type_failure_wrong_firmware( + radio_manager: ZhaRadioManager, +) -> None: + """Test radio type detection, wrong firmware.""" + with patch( + "homeassistant.components.zha.radio_manager.AUTOPROBE_RADIOS", () + ), patch( + "homeassistant.components.zha.radio_manager.repairs.warn_on_wrong_silabs_firmware", + return_value=True, + ): + assert ( + await radio_manager.detect_radio_type() + == ProbeResult.WRONG_FIRMWARE_INSTALLED + ) + assert radio_manager.radio_type is None + + +async def test_detect_radio_type_failure_no_detect( + radio_manager: ZhaRadioManager, +) -> None: + """Test radio type detection, no firmware detected.""" + with patch( + "homeassistant.components.zha.radio_manager.AUTOPROBE_RADIOS", () + ), patch( + "homeassistant.components.zha.radio_manager.repairs.warn_on_wrong_silabs_firmware", + return_value=False, + ): + assert await radio_manager.detect_radio_type() == ProbeResult.PROBING_FAILED + assert radio_manager.radio_type is None diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py new file mode 100644 index 00000000000..18705168a3f --- /dev/null +++ b/tests/components/zha/test_repairs.py @@ -0,0 +1,235 @@ +"""Test ZHA repairs.""" +from collections.abc import Callable +import logging +from unittest.mock import patch + +import pytest +from universal_silabs_flasher.const import ApplicationType +from universal_silabs_flasher.flasher import Flasher + +from homeassistant.components.homeassistant_sky_connect import ( + DOMAIN as SKYCONNECT_DOMAIN, +) +from homeassistant.components.zha.core.const import DOMAIN +from homeassistant.components.zha.repairs import ( + DISABLE_MULTIPAN_URL, + ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED, + HardwareType, + _detect_radio_hardware, + probe_silabs_firmware_type, + warn_on_wrong_silabs_firmware, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import issue_registry as ir + +from tests.common import MockConfigEntry + +SKYCONNECT_DEVICE = "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0" + + +def set_flasher_app_type(app_type: ApplicationType) -> Callable[[Flasher], None]: + """Set the app type on the flasher.""" + + def replacement(self: Flasher) -> None: + self.app_type = app_type + + return replacement + + +def test_detect_radio_hardware(hass: HomeAssistant) -> None: + """Test logic to detect radio hardware.""" + skyconnect_config_entry = MockConfigEntry( + data={ + "device": SKYCONNECT_DEVICE, + "vid": "10C4", + "pid": "EA60", + "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", + "manufacturer": "Nabu Casa", + "description": "SkyConnect v1.0", + }, + domain=SKYCONNECT_DOMAIN, + options={}, + title="Home Assistant SkyConnect", + ) + skyconnect_config_entry.add_to_hass(hass) + + assert _detect_radio_hardware(hass, SKYCONNECT_DEVICE) == HardwareType.SKYCONNECT + assert ( + _detect_radio_hardware(hass, SKYCONNECT_DEVICE + "_foo") == HardwareType.OTHER + ) + assert _detect_radio_hardware(hass, "/dev/ttyAMA1") == HardwareType.OTHER + + with patch( + "homeassistant.components.homeassistant_yellow.hardware.get_os_info", + return_value={"board": "yellow"}, + ): + assert _detect_radio_hardware(hass, "/dev/ttyAMA1") == HardwareType.YELLOW + assert _detect_radio_hardware(hass, "/dev/ttyAMA2") == HardwareType.OTHER + assert ( + _detect_radio_hardware(hass, SKYCONNECT_DEVICE) == HardwareType.SKYCONNECT + ) + + +def test_detect_radio_hardware_failure(hass: HomeAssistant) -> None: + """Test radio hardware detection failure.""" + + with patch( + "homeassistant.components.homeassistant_yellow.hardware.async_info", + side_effect=HomeAssistantError(), + ), patch( + "homeassistant.components.homeassistant_sky_connect.hardware.async_info", + side_effect=HomeAssistantError(), + ): + assert _detect_radio_hardware(hass, SKYCONNECT_DEVICE) == HardwareType.OTHER + + +@pytest.mark.parametrize( + ("detected_hardware", "expected_learn_more_url"), + [ + (HardwareType.SKYCONNECT, DISABLE_MULTIPAN_URL[HardwareType.SKYCONNECT]), + (HardwareType.YELLOW, DISABLE_MULTIPAN_URL[HardwareType.YELLOW]), + (HardwareType.OTHER, None), + ], +) +async def test_multipan_firmware_repair( + hass: HomeAssistant, + detected_hardware: HardwareType, + expected_learn_more_url: str, + config_entry: MockConfigEntry, + mock_zigpy_connect, +) -> None: + """Test creating a repair when multi-PAN firmware is installed and probed.""" + + config_entry.add_to_hass(hass) + + # ZHA fails to set up + with patch( + "homeassistant.components.zha.repairs.Flasher.probe_app_type", + side_effect=set_flasher_app_type(ApplicationType.CPC), + autospec=True, + ), patch( + "homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize", + side_effect=RuntimeError(), + ), patch( + "homeassistant.components.zha.repairs._detect_radio_hardware", + return_value=detected_hardware, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.SETUP_ERROR + + await hass.config_entries.async_unload(config_entry.entry_id) + + issue_registry = ir.async_get(hass) + + issue = issue_registry.async_get_issue( + domain=DOMAIN, + issue_id=ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED, + ) + + # The issue is created when we fail to probe + assert issue is not None + assert issue.translation_placeholders["firmware_type"] == "CPC" + assert issue.learn_more_url == expected_learn_more_url + + # If ZHA manages to start up normally after this, the issue will be deleted + with mock_zigpy_connect: + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + issue = issue_registry.async_get_issue( + domain=DOMAIN, + issue_id=ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED, + ) + assert issue is None + + +async def test_multipan_firmware_no_repair_on_probe_failure( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test that a repair is not created when multi-PAN firmware cannot be probed.""" + + config_entry.add_to_hass(hass) + + # ZHA fails to set up + with patch( + "homeassistant.components.zha.repairs.Flasher.probe_app_type", + side_effect=set_flasher_app_type(None), + autospec=True, + ), patch( + "homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize", + side_effect=RuntimeError(), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.SETUP_ERROR + + await hass.config_entries.async_unload(config_entry.entry_id) + + # No repair is created + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + domain=DOMAIN, + issue_id=ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED, + ) + assert issue is None + + +async def test_multipan_firmware_retry_on_probe_ezsp( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_zigpy_connect, +) -> None: + """Test that ZHA is reloaded when EZSP firmware is probed.""" + + config_entry.add_to_hass(hass) + + # ZHA fails to set up + with patch( + "homeassistant.components.zha.repairs.Flasher.probe_app_type", + side_effect=set_flasher_app_type(ApplicationType.EZSP), + autospec=True, + ), patch( + "homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize", + side_effect=RuntimeError(), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # The config entry state is `SETUP_RETRY`, not `SETUP_ERROR`! + assert config_entry.state == ConfigEntryState.SETUP_RETRY + + await hass.config_entries.async_unload(config_entry.entry_id) + + # No repair is created + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + domain=DOMAIN, + issue_id=ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED, + ) + assert issue is None + + +async def test_no_warn_on_socket(hass: HomeAssistant) -> None: + """Test that no warning is issued when the device is a socket.""" + with patch( + "homeassistant.components.zha.repairs.probe_silabs_firmware_type", autospec=True + ) as mock_probe: + await warn_on_wrong_silabs_firmware(hass, device="socket://1.2.3.4:5678") + + mock_probe.assert_not_called() + + +async def test_probe_failure_exception_handling(caplog) -> None: + """Test that probe failures are handled gracefully.""" + with patch( + "homeassistant.components.zha.repairs.Flasher.probe_app_type", + side_effect=RuntimeError(), + ), caplog.at_level(logging.DEBUG): + await probe_silabs_firmware_type("/dev/ttyZigbee") + + assert "Failed to probe application type" in caplog.text From 057daa5fdbc8321c5a7ed737a5f33ffe5d04e5d3 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 31 Aug 2023 20:35:11 +0200 Subject: [PATCH 017/202] Address late review for Nextcloud (#99226) --- .../components/nextcloud/__init__.py | 2 +- homeassistant/components/nextcloud/entity.py | 2 +- homeassistant/components/nextcloud/sensor.py | 196 ++++++++++-------- 3 files changed, 106 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py index 27c9b8b6078..9cfe4aa7f70 100644 --- a/homeassistant/components/nextcloud/__init__.py +++ b/homeassistant/components/nextcloud/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) for entity in entities: old_uid_start = f"{entry.data[CONF_URL]}#nextcloud_" - new_uid_start = f"{entry.data[CONF_URL]}#" + new_uid_start = f"{entry.entry_id}#" if entity.unique_id.startswith(old_uid_start): new_uid = entity.unique_id.replace(old_uid_start, new_uid_start) _LOGGER.debug("migrate unique id '%s' to '%s'", entity.unique_id, new_uid) diff --git a/homeassistant/components/nextcloud/entity.py b/homeassistant/components/nextcloud/entity.py index 92ba65a134b..b9dab9179c1 100644 --- a/homeassistant/components/nextcloud/entity.py +++ b/homeassistant/components/nextcloud/entity.py @@ -23,7 +23,7 @@ class NextcloudEntity(CoordinatorEntity[NextcloudDataUpdateCoordinator]): ) -> None: """Initialize the Nextcloud sensor.""" super().__init__(coordinator) - self._attr_unique_id = f"{coordinator.url}#{description.key}" + self._attr_unique_id = f"{entry.entry_id}#{description.key}" self._attr_device_info = DeviceInfo( configuration_url=coordinator.url, identifiers={(DOMAIN, entry.entry_id)}, diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py index 0cf30cee000..0133a9e7f76 100644 --- a/homeassistant/components/nextcloud/sensor.py +++ b/homeassistant/components/nextcloud/sensor.py @@ -1,8 +1,10 @@ """Summary data from Nextcoud.""" from __future__ import annotations -from datetime import UTC, datetime -from typing import Final, cast +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime +from typing import Final from homeassistant.components.sensor import ( SensorDeviceClass, @@ -18,7 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType +from homeassistant.util.dt import utc_from_timestamp from .const import DOMAIN from .coordinator import NextcloudDataUpdateCoordinator @@ -26,32 +28,42 @@ from .entity import NextcloudEntity UNIT_OF_LOAD: Final[str] = "load" -SENSORS: Final[list[SensorEntityDescription]] = [ - SensorEntityDescription( + +@dataclass +class NextcloudSensorEntityDescription(SensorEntityDescription): + """Describes Nextcloud sensor entity.""" + + value_fn: Callable[ + [str | int | float], str | int | float | datetime + ] = lambda value: value + + +SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ + NextcloudSensorEntityDescription( key="activeUsers_last1hour", translation_key="nextcloud_activeusers_last1hour", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:account-multiple", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="activeUsers_last24hours", translation_key="nextcloud_activeusers_last24hours", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:account-multiple", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="activeUsers_last5minutes", translation_key="nextcloud_activeusers_last5minutes", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:account-multiple", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_expunges", translation_key="nextcloud_cache_expunges", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_mem_size", translation_key="nextcloud_cache_mem_size", device_class=SensorDeviceClass.DATA_SIZE, @@ -60,56 +72,57 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_memory_type", translation_key="nextcloud_cache_memory_type", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_num_entries", translation_key="nextcloud_cache_num_entries", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_num_hits", translation_key="nextcloud_cache_num_hits", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_num_inserts", translation_key="nextcloud_cache_num_inserts", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_num_misses", translation_key="nextcloud_cache_num_misses", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_num_slots", translation_key="nextcloud_cache_num_slots", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_start_time", translation_key="nextcloud_cache_start_time", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, + value_fn=lambda val: utc_from_timestamp(float(val)), ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_ttl", translation_key="nextcloud_cache_ttl", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="database_size", translation_key="nextcloud_database_size", device_class=SensorDeviceClass.DATA_SIZE, @@ -118,19 +131,19 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="database_type", translation_key="nextcloud_database_type", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:database", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="database_version", translation_key="nextcloud_database_version", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:database", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="interned_strings_usage_buffer_size", translation_key="nextcloud_interned_strings_usage_buffer_size", device_class=SensorDeviceClass.DATA_SIZE, @@ -140,7 +153,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="interned_strings_usage_free_memory", translation_key="nextcloud_interned_strings_usage_free_memory", device_class=SensorDeviceClass.DATA_SIZE, @@ -150,13 +163,13 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="interned_strings_usage_number_of_strings", translation_key="nextcloud_interned_strings_usage_number_of_strings", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="interned_strings_usage_used_memory", translation_key="nextcloud_interned_strings_usage_used_memory", device_class=SensorDeviceClass.DATA_SIZE, @@ -166,7 +179,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="jit_buffer_free", translation_key="nextcloud_jit_buffer_free", device_class=SensorDeviceClass.DATA_SIZE, @@ -176,7 +189,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="jit_buffer_size", translation_key="nextcloud_jit_buffer_size", device_class=SensorDeviceClass.DATA_SIZE, @@ -186,93 +199,94 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="jit_kind", translation_key="nextcloud_jit_kind", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="jit_opt_flags", translation_key="nextcloud_jit_opt_flags", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="jit_opt_level", translation_key="nextcloud_jit_opt_level", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_blacklist_miss_ratio", translation_key="nextcloud_opcache_statistics_blacklist_miss_ratio", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, native_unit_of_measurement=PERCENTAGE, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_blacklist_misses", translation_key="nextcloud_opcache_statistics_blacklist_misses", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_hash_restarts", translation_key="nextcloud_opcache_statistics_hash_restarts", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_hits", translation_key="nextcloud_opcache_statistics_hits", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_last_restart_time", translation_key="nextcloud_opcache_statistics_last_restart_time", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, + value_fn=lambda val: utc_from_timestamp(float(val)), ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_manual_restarts", translation_key="nextcloud_opcache_statistics_manual_restarts", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_max_cached_keys", translation_key="nextcloud_opcache_statistics_max_cached_keys", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_misses", translation_key="nextcloud_opcache_statistics_misses", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_num_cached_keys", translation_key="nextcloud_opcache_statistics_num_cached_keys", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_num_cached_scripts", translation_key="nextcloud_opcache_statistics_num_cached_scripts", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_oom_restarts", translation_key="nextcloud_opcache_statistics_oom_restarts", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_opcache_hit_rate", translation_key="nextcloud_opcache_statistics_opcache_hit_rate", entity_category=EntityCategory.DIAGNOSTIC, @@ -280,14 +294,15 @@ SENSORS: Final[list[SensorEntityDescription]] = [ native_unit_of_measurement=PERCENTAGE, suggested_display_precision=1, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_start_time", translation_key="nextcloud_opcache_statistics_start_time", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, + value_fn=lambda val: utc_from_timestamp(float(val)), ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="server_php_opcache_memory_usage_current_wasted_percentage", translation_key="nextcloud_server_php_opcache_memory_usage_current_wasted_percentage", entity_category=EntityCategory.DIAGNOSTIC, @@ -296,7 +311,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ native_unit_of_measurement=PERCENTAGE, suggested_display_precision=1, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="server_php_opcache_memory_usage_free_memory", translation_key="nextcloud_server_php_opcache_memory_usage_free_memory", device_class=SensorDeviceClass.DATA_SIZE, @@ -307,7 +322,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="server_php_opcache_memory_usage_used_memory", translation_key="nextcloud_server_php_opcache_memory_usage_used_memory", device_class=SensorDeviceClass.DATA_SIZE, @@ -318,7 +333,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="server_php_opcache_memory_usage_wasted_memory", translation_key="nextcloud_server_php_opcache_memory_usage_wasted_memory", device_class=SensorDeviceClass.DATA_SIZE, @@ -329,7 +344,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="server_php_max_execution_time", translation_key="nextcloud_server_php_max_execution_time", device_class=SensorDeviceClass.DURATION, @@ -337,7 +352,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ icon="mdi:language-php", native_unit_of_measurement=UnitOfTime.SECONDS, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="server_php_memory_limit", translation_key="nextcloud_server_php_memory_limit", device_class=SensorDeviceClass.DATA_SIZE, @@ -347,7 +362,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="server_php_upload_max_filesize", translation_key="nextcloud_server_php_upload_max_filesize", device_class=SensorDeviceClass.DATA_SIZE, @@ -357,62 +372,62 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="server_php_version", translation_key="nextcloud_server_php_version", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:language-php", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="server_webserver", translation_key="nextcloud_server_webserver", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="shares_num_fed_shares_sent", translation_key="nextcloud_shares_num_fed_shares_sent", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="shares_num_fed_shares_received", translation_key="nextcloud_shares_num_fed_shares_received", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="shares_num_shares", translation_key="nextcloud_shares_num_shares", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="shares_num_shares_groups", translation_key="nextcloud_shares_num_shares_groups", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="shares_num_shares_link", translation_key="nextcloud_shares_num_shares_link", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="shares_num_shares_link_no_password", translation_key="nextcloud_shares_num_shares_link_no_password", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="shares_num_shares_mail", translation_key="nextcloud_shares_num_shares_mail", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="shares_num_shares_room", translation_key="nextcloud_shares_num_shares_room", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( - key="server_num_shares_user", + NextcloudSensorEntityDescription( + key="shares_num_shares_user", translation_key="nextcloud_shares_num_shares_user", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="sma_avail_mem", translation_key="nextcloud_sma_avail_mem", device_class=SensorDeviceClass.DATA_SIZE, @@ -422,13 +437,13 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="sma_num_seg", translation_key="nextcloud_sma_num_seg", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="sma_seg_size", translation_key="nextcloud_sma_seg_size", device_class=SensorDeviceClass.DATA_SIZE, @@ -438,64 +453,64 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="storage_num_files", translation_key="nextcloud_storage_num_files", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="storage_num_storages", translation_key="nextcloud_storage_num_storages", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="storage_num_storages_home", translation_key="nextcloud_storage_num_storages_home", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="storage_num_storages_local", translation_key="nextcloud_storage_num_storages_local", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="storage_num_storages_other", translation_key="nextcloud_storage_num_storages_other", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="storage_num_users", translation_key="nextcloud_storage_num_users", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_apps_num_installed", translation_key="nextcloud_system_apps_num_installed", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_apps_num_updates_available", translation_key="nextcloud_system_apps_num_updates_available", icon="mdi:update", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_cpuload_1", translation_key="nextcloud_system_cpuload_1", native_unit_of_measurement=UNIT_OF_LOAD, icon="mdi:chip", suggested_display_precision=2, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_cpuload_5", translation_key="nextcloud_system_cpuload_5", native_unit_of_measurement=UNIT_OF_LOAD, icon="mdi:chip", suggested_display_precision=2, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_cpuload_15", translation_key="nextcloud_system_cpuload_15", native_unit_of_measurement=UNIT_OF_LOAD, icon="mdi:chip", suggested_display_precision=2, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_freespace", translation_key="nextcloud_system_freespace", device_class=SensorDeviceClass.DATA_SIZE, @@ -504,7 +519,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_mem_free", translation_key="nextcloud_system_mem_free", device_class=SensorDeviceClass.DATA_SIZE, @@ -513,7 +528,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_mem_total", translation_key="nextcloud_system_mem_total", device_class=SensorDeviceClass.DATA_SIZE, @@ -522,25 +537,25 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_memcache.distributed", translation_key="nextcloud_system_memcache_distributed", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_memcache.local", translation_key="nextcloud_system_memcache_local", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_memcache.locking", translation_key="nextcloud_system_memcache_locking", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_swap_total", translation_key="nextcloud_system_swap_total", device_class=SensorDeviceClass.DATA_SIZE, @@ -549,7 +564,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_swap_free", translation_key="nextcloud_system_swap_free", device_class=SensorDeviceClass.DATA_SIZE, @@ -558,11 +573,11 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_theme", translation_key="nextcloud_system_theme", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_version", translation_key="nextcloud_system_version", ), @@ -586,13 +601,10 @@ async def async_setup_entry( class NextcloudSensor(NextcloudEntity, SensorEntity): """Represents a Nextcloud sensor.""" + entity_description: NextcloudSensorEntityDescription + @property - def native_value(self) -> StateType | datetime: + def native_value(self) -> str | int | float | datetime: """Return the state for this sensor.""" val = self.coordinator.data.get(self.entity_description.key) - if ( - getattr(self.entity_description, "device_class", None) - == SensorDeviceClass.TIMESTAMP - ): - return datetime.fromtimestamp(cast(int, val), tz=UTC) - return val + return self.entity_description.value_fn(val) # type: ignore[arg-type] From ebf42ad342309439500e3863a1993b6ba2e7fe8b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 31 Aug 2023 13:47:01 -0400 Subject: [PATCH 018/202] Significantly reduce overhead to filter event triggers (#99376) * fast * cleanups * cleanups * cleanups * comment * comment * add more cover * comment * pull more examples from forums to validate cover --- .../homeassistant/triggers/event.py | 66 +++++++++---- .../homeassistant/triggers/test_event.py | 93 ++++++++++++++++++- 2 files changed, 138 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index d0e74d5b04e..a4266a70add 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -1,6 +1,7 @@ """Offer event listening automation rules.""" from __future__ import annotations +from collections.abc import ItemsView from typing import Any import voluptuous as vol @@ -47,9 +48,8 @@ async def async_attach_trigger( event_types = template.render_complex( config[CONF_EVENT_TYPE], variables, limited=True ) - removes = [] - - event_data_schema = None + event_data_schema: vol.Schema | None = None + event_data_items: ItemsView | None = None if CONF_EVENT_DATA in config: # Render the schema input template.attach(hass, config[CONF_EVENT_DATA]) @@ -57,13 +57,21 @@ async def async_attach_trigger( event_data.update( template.render_complex(config[CONF_EVENT_DATA], variables, limited=True) ) - # Build the schema - event_data_schema = vol.Schema( - {vol.Required(key): value for key, value in event_data.items()}, - extra=vol.ALLOW_EXTRA, - ) + # Build the schema or a an items view if the schema is simple + # and does not contain sub-dicts. We explicitly do not check for + # list like the context data below since lists are a special case + # only for context data. (see test test_event_data_with_list) + if any(isinstance(value, dict) for value in event_data.values()): + event_data_schema = vol.Schema( + {vol.Required(key): value for key, value in event_data.items()}, + extra=vol.ALLOW_EXTRA, + ) + else: + # Use a simple items comparison if possible + event_data_items = event_data.items() - event_context_schema = None + event_context_schema: vol.Schema | None = None + event_context_items: ItemsView | None = None if CONF_EVENT_CONTEXT in config: # Render the schema input template.attach(hass, config[CONF_EVENT_CONTEXT]) @@ -71,14 +79,23 @@ async def async_attach_trigger( event_context.update( template.render_complex(config[CONF_EVENT_CONTEXT], variables, limited=True) ) - # Build the schema - event_context_schema = vol.Schema( - { - vol.Required(key): _schema_value(value) - for key, value in event_context.items() - }, - extra=vol.ALLOW_EXTRA, - ) + # Build the schema or a an items view if the schema is simple + # and does not contain lists. Lists are a special case to support + # matching events by user_id. (see test test_if_fires_on_multiple_user_ids) + # This can likely be optimized further in the future to handle the + # multiple user_id case without requiring expensive schema + # validation. + if any(isinstance(value, list) for value in event_context.values()): + event_context_schema = vol.Schema( + { + vol.Required(key): _schema_value(value) + for key, value in event_context.items() + }, + extra=vol.ALLOW_EXTRA, + ) + else: + # Use a simple items comparison if possible + event_context_items = event_context.items() job = HassJob(action, f"event trigger {trigger_info}") @@ -88,9 +105,20 @@ async def async_attach_trigger( try: # Check that the event data and context match the configured # schema if one was provided - if event_data_schema: + if event_data_items: + # Fast path for simple items comparison + if not (event.data.items() >= event_data_items): + return False + elif event_data_schema: + # Slow path for schema validation event_data_schema(event.data) - if event_context_schema: + + if event_context_items: + # Fast path for simple items comparison + if not (event.context.as_dict().items() >= event_context_items): + return False + elif event_context_schema: + # Slow path for schema validation event_context_schema(dict(event.context.as_dict())) except vol.Invalid: # If event doesn't match, skip event diff --git a/tests/components/homeassistant/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py index 6fc7e5055ed..d996cd74da7 100644 --- a/tests/components/homeassistant/triggers/test_event.py +++ b/tests/components/homeassistant/triggers/test_event.py @@ -288,7 +288,11 @@ async def test_if_fires_on_event_with_empty_data_and_context_config( async def test_if_fires_on_event_with_nested_data(hass: HomeAssistant, calls) -> None: - """Test the firing of events with nested data.""" + """Test the firing of events with nested data. + + This test exercises the slow path of using vol.Schema to validate + matching event data. + """ assert await async_setup_component( hass, automation.DOMAIN, @@ -311,6 +315,87 @@ async def test_if_fires_on_event_with_nested_data(hass: HomeAssistant, calls) -> assert len(calls) == 1 +async def test_if_fires_on_event_with_empty_data(hass: HomeAssistant, calls) -> None: + """Test the firing of events with empty data. + + This test exercises the fast path to validate matching event data. + """ + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "event", + "event_type": "test_event", + "event_data": {}, + }, + "action": {"service": "test.automation"}, + } + }, + ) + hass.bus.async_fire("test_event", {"any_attr": {}}) + await hass.async_block_till_done() + assert len(calls) == 1 + + +async def test_if_fires_on_sample_zha_event(hass: HomeAssistant, calls) -> None: + """Test the firing of events with a sample zha event. + + This test exercises the fast path to validate matching event data. + """ + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "event", + "event_type": "zha_event", + "event_data": { + "device_ieee": "00:15:8d:00:02:93:04:11", + "command": "attribute_updated", + "args": { + "attribute_id": 0, + "attribute_name": "on_off", + "value": True, + }, + }, + }, + "action": {"service": "test.automation"}, + } + }, + ) + + hass.bus.async_fire( + "zha_event", + { + "device_ieee": "00:15:8d:00:02:93:04:11", + "unique_id": "00:15:8d:00:02:93:04:11:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + "command": "attribute_updated", + "args": {"attribute_id": 0, "attribute_name": "on_off", "value": True}, + }, + ) + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.bus.async_fire( + "zha_event", + { + "device_ieee": "00:15:8d:00:02:93:04:11", + "unique_id": "00:15:8d:00:02:93:04:11:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + "command": "attribute_updated", + "args": {"attribute_id": 0, "attribute_name": "on_off", "value": False}, + }, + ) + await hass.async_block_till_done() + assert len(calls) == 1 + + async def test_if_not_fires_if_event_data_not_matches( hass: HomeAssistant, calls ) -> None: @@ -362,7 +447,11 @@ async def test_if_not_fires_if_event_context_not_matches( async def test_if_fires_on_multiple_user_ids( hass: HomeAssistant, calls, context_with_user ) -> None: - """Test the firing of event when the trigger has multiple user ids.""" + """Test the firing of event when the trigger has multiple user ids. + + This test exercises the slow path of using vol.Schema to validate + matching event context. + """ assert await async_setup_component( hass, automation.DOMAIN, From a95691f30645850d4df265532ce2da4e9db74dc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 31 Aug 2023 21:59:01 +0200 Subject: [PATCH 019/202] Update AEMET-OpenData to v0.4.4 (#99418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/aemet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index c43e7a0b402..1c65572a64e 100644 --- a/homeassistant/components/aemet/manifest.json +++ b/homeassistant/components/aemet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/aemet", "iot_class": "cloud_polling", "loggers": ["aemet_opendata"], - "requirements": ["AEMET-OpenData==0.4.3"] + "requirements": ["AEMET-OpenData==0.4.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index bd372977b95..8b22f051591 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2,7 +2,7 @@ -r requirements.txt # homeassistant.components.aemet -AEMET-OpenData==0.4.3 +AEMET-OpenData==0.4.4 # homeassistant.components.aladdin_connect AIOAladdinConnect==0.1.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7cc452889b7..03a52733d51 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.aemet -AEMET-OpenData==0.4.3 +AEMET-OpenData==0.4.4 # homeassistant.components.aladdin_connect AIOAladdinConnect==0.1.57 From 469a72a5f90e703a0b3740dcdab5b14f56386573 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 1 Sep 2023 15:58:01 +0200 Subject: [PATCH 020/202] Use common key for away mode state translations (#99425) --- homeassistant/components/water_heater/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/water_heater/strings.json b/homeassistant/components/water_heater/strings.json index 6991d371bd3..1b3af02610c 100644 --- a/homeassistant/components/water_heater/strings.json +++ b/homeassistant/components/water_heater/strings.json @@ -21,8 +21,8 @@ "away_mode": { "name": "Away mode", "state": { - "off": "Off", - "on": "On" + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" } } } From 987a959b1943c879eb3d2094d56f95b82fdf1c81 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Fri, 1 Sep 2023 02:24:13 -0400 Subject: [PATCH 021/202] Update asynsleepiq library to 1.3.7 (#99431) --- homeassistant/components/sleepiq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index 3d757e2328d..874ae90ec4a 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/sleepiq", "iot_class": "cloud_polling", "loggers": ["asyncsleepiq"], - "requirements": ["asyncsleepiq==1.3.5"] + "requirements": ["asyncsleepiq==1.3.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8b22f051591..3fc82ea3316 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,7 +464,7 @@ asyncinotify==4.0.2 asyncpysupla==0.0.5 # homeassistant.components.sleepiq -asyncsleepiq==1.3.5 +asyncsleepiq==1.3.7 # homeassistant.components.aten_pe # atenpdu==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03a52733d51..191a845eb94 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -409,7 +409,7 @@ async-upnp-client==0.35.0 async_interrupt==1.1.1 # homeassistant.components.sleepiq -asyncsleepiq==1.3.5 +asyncsleepiq==1.3.7 # homeassistant.components.aurora auroranoaa==0.0.3 From eb8d375e359186c0841b1d88a8c91d66ea69c37d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Sep 2023 17:14:42 +0200 Subject: [PATCH 022/202] Fix template helper strings (#99456) --- homeassistant/components/template/strings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 482682d0ce1..7e5e56a26d6 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -5,7 +5,7 @@ "data": { "device_class": "[%key:component::template::config::step::sensor::data::device_class%]", "name": "[%key:common::config_flow::data::name%]", - "state_template": "[%key:component::template::config::step::sensor::data::state_template%]" + "state": "[%key:component::template::config::step::sensor::data::state%]" }, "title": "Template binary sensor" }, @@ -14,7 +14,7 @@ "device_class": "Device class", "name": "[%key:common::config_flow::data::name%]", "state_class": "[%key:component::sensor::entity_component::_::state_attributes::state_class::name%]", - "state_template": "State template", + "state": "State template", "unit_of_measurement": "Unit of measurement" }, "title": "Template sensor" @@ -34,7 +34,7 @@ "binary_sensor": { "data": { "device_class": "[%key:component::template::config::step::sensor::data::device_class%]", - "state_template": "[%key:component::template::config::step::sensor::data::state_template%]" + "state": "[%key:component::template::config::step::sensor::data::state%]" }, "title": "[%key:component::template::config::step::binary_sensor::title%]" }, @@ -42,7 +42,7 @@ "data": { "device_class": "[%key:component::template::config::step::sensor::data::device_class%]", "state_class": "[%key:component::template::config::step::sensor::data::state_class%]", - "state_template": "[%key:component::template::config::step::sensor::data::state_template%]", + "state": "[%key:component::template::config::step::sensor::data::state%]", "unit_of_measurement": "[%key:component::template::config::step::sensor::data::unit_of_measurement%]" }, "title": "[%key:component::template::config::step::sensor::title%]" From 528e8c0fe75bd9a47c6f9f40ffe55fb9cac1abbb Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 1 Sep 2023 17:28:52 +0200 Subject: [PATCH 023/202] Update frontend to 20230901.0 (#99464) --- 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 a31faaf362e..3b46f568d3e 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230831.0"] + "requirements": ["home-assistant-frontend==20230901.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3dccb80d11e..19169de83f6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.70.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 -home-assistant-frontend==20230831.0 +home-assistant-frontend==20230901.0 home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3fc82ea3316..ae8066dd6d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -994,7 +994,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230831.0 +home-assistant-frontend==20230901.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 191a845eb94..dde4bf062f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230831.0 +home-assistant-frontend==20230901.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 From 1f0b3c4e339d09d765f11fb96217d1444ddaf94d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 1 Sep 2023 18:01:07 +0200 Subject: [PATCH 024/202] Bump version to 2023.9.0b2 --- 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 0b1cc1c5ea0..19370552139 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 210c2973d4b..0248bd43af1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.0b1" +version = "2023.9.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f573c1e27c3a4252b6da18f3254c302fc56df5a2 Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Sat, 2 Sep 2023 01:20:36 -0700 Subject: [PATCH 025/202] Handle timestamp sensors in Prometheus integration (#98001) --- homeassistant/components/prometheus/__init__.py | 16 +++++++++++++++- tests/components/prometheus/test_init.py | 16 ++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index e5d7f6cb060..adc5225b286 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -19,6 +19,7 @@ from homeassistant.components.climate import ( from homeassistant.components.cover import ATTR_POSITION, ATTR_TILT_POSITION from homeassistant.components.http import HomeAssistantView from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES, ATTR_HUMIDITY +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_DEVICE_CLASS, @@ -44,6 +45,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.helpers.entity_values import EntityValues from homeassistant.helpers.typing import ConfigType +from homeassistant.util.dt import as_timestamp from homeassistant.util.unit_conversion import TemperatureConverter _LOGGER = logging.getLogger(__name__) @@ -147,6 +149,7 @@ class PrometheusMetrics: self._sensor_metric_handlers = [ self._sensor_override_component_metric, self._sensor_override_metric, + self._sensor_timestamp_metric, self._sensor_attribute_metric, self._sensor_default_metric, self._sensor_fallback_metric, @@ -292,7 +295,10 @@ class PrometheusMetrics: def state_as_number(state): """Return a state casted to a float.""" try: - value = state_helper.state_as_number(state) + if state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP: + value = as_timestamp(state.state) + else: + value = state_helper.state_as_number(state) except ValueError: _LOGGER.debug("Could not convert %s to float", state) value = 0 @@ -576,6 +582,14 @@ class PrometheusMetrics: return f"sensor_{metric}_{unit}" return None + @staticmethod + def _sensor_timestamp_metric(state, unit): + """Get metric for timestamp sensors, which have no unit of measurement attribute.""" + metric = state.attributes.get(ATTR_DEVICE_CLASS) + if metric == SensorDeviceClass.TIMESTAMP: + return f"sensor_{metric}_seconds" + return None + def _sensor_override_metric(self, state, unit): """Get metric from override in configuration.""" if self._override_metric: diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 446666c4a6a..82a205eb259 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -232,6 +232,12 @@ async def test_sensor_device_class(client, sensor_entities) -> None: 'friendly_name="Radio Energy"} 14.0' in body ) + assert ( + 'sensor_timestamp_seconds{domain="sensor",' + 'entity="sensor.timestamp",' + 'friendly_name="Timestamp"} 1.691445808136036e+09' in body + ) + @pytest.mark.parametrize("namespace", [""]) async def test_input_number(client, input_number_entities) -> None: @@ -1049,6 +1055,16 @@ async def sensor_fixture( set_state_with_entry(hass, sensor_11, 50) data["sensor_11"] = sensor_11 + sensor_12 = entity_registry.async_get_or_create( + domain=sensor.DOMAIN, + platform="test", + unique_id="sensor_12", + original_device_class=SensorDeviceClass.TIMESTAMP, + suggested_object_id="Timestamp", + original_name="Timestamp", + ) + set_state_with_entry(hass, sensor_12, "2023-08-07T15:03:28.136036-0700") + data["sensor_12"] = sensor_12 await hass.async_block_till_done() return data From f0878addcaa43ec3ef8c7dbe990db56744ea0f83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sat, 2 Sep 2023 15:08:49 +0200 Subject: [PATCH 026/202] Update Tibber library to 0.28.2 (#99115) --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index c668430914f..1d8120a4321 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["tibber"], "quality_scale": "silver", - "requirements": ["pyTibber==0.28.0"] + "requirements": ["pyTibber==0.28.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index ae8066dd6d9..6909ae3b924 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1547,7 +1547,7 @@ pyRFXtrx==0.30.1 pySDCP==1 # homeassistant.components.tibber -pyTibber==0.28.0 +pyTibber==0.28.2 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dde4bf062f3..f4570ea0a32 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1159,7 +1159,7 @@ pyElectra==1.2.0 pyRFXtrx==0.30.1 # homeassistant.components.tibber -pyTibber==0.28.0 +pyTibber==0.28.2 # homeassistant.components.dlink pyW215==0.7.0 From 270be19e1a44ca960f530643187ccd78e8523ab1 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 2 Sep 2023 21:00:33 +0200 Subject: [PATCH 027/202] Check new IP of Reolink camera from DHCP (#99381) Co-authored-by: J. Nick Koston --- .../components/reolink/config_flow.py | 44 +++++++++- homeassistant/components/reolink/util.py | 23 +++++ tests/components/reolink/test_config_flow.py | 85 ++++++++++++++++--- 3 files changed, 137 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/reolink/util.py diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index d24fd8d1f14..d924f395c50 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -12,13 +12,14 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import config_validation as cv from homeassistant.helpers.device_registry import format_mac from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DOMAIN from .exceptions import ReolinkException, ReolinkWebhookException, UserNotAdmin from .host import ReolinkHost +from .util import has_connection_problem _LOGGER = logging.getLogger(__name__) @@ -96,7 +97,46 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle discovery via dhcp.""" mac_address = format_mac(discovery_info.macaddress) - await self.async_set_unique_id(mac_address) + existing_entry = await self.async_set_unique_id(mac_address) + if ( + existing_entry + and CONF_PASSWORD in existing_entry.data + and existing_entry.data[CONF_HOST] != discovery_info.ip + ): + if has_connection_problem(self.hass, existing_entry): + _LOGGER.debug( + "Reolink DHCP reported new IP '%s', " + "but connection to camera seems to be okay, so sticking to IP '%s'", + discovery_info.ip, + existing_entry.data[CONF_HOST], + ) + raise AbortFlow("already_configured") + + # check if the camera is reachable at the new IP + host = ReolinkHost(self.hass, existing_entry.data, existing_entry.options) + try: + await host.api.get_state("GetLocalLink") + await host.api.logout() + except ReolinkError as err: + _LOGGER.debug( + "Reolink DHCP reported new IP '%s', " + "but got error '%s' trying to connect, so sticking to IP '%s'", + discovery_info.ip, + str(err), + existing_entry.data[CONF_HOST], + ) + raise AbortFlow("already_configured") from err + if format_mac(host.api.mac_address) != mac_address: + _LOGGER.debug( + "Reolink mac address '%s' at new IP '%s' from DHCP, " + "does not match mac '%s' of config entry, so sticking to IP '%s'", + format_mac(host.api.mac_address), + discovery_info.ip, + mac_address, + existing_entry.data[CONF_HOST], + ) + raise AbortFlow("already_configured") + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) self.context["title_placeholders"] = { diff --git a/homeassistant/components/reolink/util.py b/homeassistant/components/reolink/util.py new file mode 100644 index 00000000000..2ab625647a7 --- /dev/null +++ b/homeassistant/components/reolink/util.py @@ -0,0 +1,23 @@ +"""Utility functions for the Reolink component.""" +from __future__ import annotations + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant + +from . import ReolinkData +from .const import DOMAIN + + +def has_connection_problem( + hass: HomeAssistant, config_entry: config_entries.ConfigEntry +) -> bool: + """Check if a existing entry has a connection problem.""" + reolink_data: ReolinkData | None = hass.data.get(DOMAIN, {}).get( + config_entry.entry_id + ) + connection_problem = ( + reolink_data is not None + and config_entry.state == config_entries.ConfigEntryState.LOADED + and reolink_data.device_coordinator.last_update_success + ) + return connection_problem diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index 048b48d9576..1a4bf999cce 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -1,18 +1,22 @@ """Test the Reolink config flow.""" +from datetime import timedelta import json -from unittest.mock import MagicMock +from typing import Any +from unittest.mock import AsyncMock, MagicMock import pytest from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp -from homeassistant.components.reolink import const +from homeassistant.components.reolink import DEVICE_UPDATE_INTERVAL, const from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL from homeassistant.components.reolink.exceptions import ReolinkWebhookException +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import format_mac +from homeassistant.util.dt import utcnow from .conftest import ( TEST_HOST, @@ -27,12 +31,14 @@ from .conftest import ( TEST_USERNAME2, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed -pytestmark = pytest.mark.usefixtures("mock_setup_entry", "reolink_connect") +pytestmark = pytest.mark.usefixtures("reolink_connect") -async def test_config_flow_manual_success(hass: HomeAssistant) -> None: +async def test_config_flow_manual_success( + hass: HomeAssistant, mock_setup_entry: MagicMock +) -> None: """Successful flow manually initialized by the user.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -66,7 +72,7 @@ async def test_config_flow_manual_success(hass: HomeAssistant) -> None: async def test_config_flow_errors( - hass: HomeAssistant, reolink_connect: MagicMock + hass: HomeAssistant, reolink_connect: MagicMock, mock_setup_entry: MagicMock ) -> None: """Successful flow manually initialized by the user after some errors.""" result = await hass.config_entries.flow.async_init( @@ -192,7 +198,7 @@ async def test_config_flow_errors( } -async def test_options_flow(hass: HomeAssistant) -> None: +async def test_options_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None: """Test specifying non default settings using options flow.""" config_entry = MockConfigEntry( domain=const.DOMAIN, @@ -230,7 +236,9 @@ async def test_options_flow(hass: HomeAssistant) -> None: } -async def test_change_connection_settings(hass: HomeAssistant) -> None: +async def test_change_connection_settings( + hass: HomeAssistant, mock_setup_entry: MagicMock +) -> None: """Test changing connection settings by issuing a second user config flow.""" config_entry = MockConfigEntry( domain=const.DOMAIN, @@ -273,7 +281,7 @@ async def test_change_connection_settings(hass: HomeAssistant) -> None: assert config_entry.data[CONF_PASSWORD] == TEST_PASSWORD2 -async def test_reauth(hass: HomeAssistant) -> None: +async def test_reauth(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None: """Test a reauth flow.""" config_entry = MockConfigEntry( domain=const.DOMAIN, @@ -333,7 +341,7 @@ async def test_reauth(hass: HomeAssistant) -> None: assert config_entry.data[CONF_PASSWORD] == TEST_PASSWORD2 -async def test_dhcp_flow(hass: HomeAssistant) -> None: +async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None: """Successful flow from DHCP discovery.""" dhcp_data = dhcp.DhcpServiceInfo( ip=TEST_HOST, @@ -371,8 +379,44 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: } -async def test_dhcp_abort_flow(hass: HomeAssistant) -> None: - """Test dhcp discovery aborts if already configured.""" +@pytest.mark.parametrize( + ("last_update_success", "attr", "value", "expected"), + [ + ( + False, + None, + None, + TEST_HOST2, + ), + ( + True, + None, + None, + TEST_HOST, + ), + ( + False, + "get_state", + AsyncMock(side_effect=ReolinkError("Test error")), + TEST_HOST, + ), + ( + False, + "mac_address", + "aa:aa:aa:aa:aa:aa", + TEST_HOST, + ), + ], +) +async def test_dhcp_ip_update( + hass: HomeAssistant, + reolink_connect: MagicMock, + last_update_success: bool, + attr: str, + value: Any, + expected: str, +) -> None: + """Test dhcp discovery aborts if already configured where the IP is updated if appropriate.""" config_entry = MockConfigEntry( domain=const.DOMAIN, unique_id=format_mac(TEST_MAC), @@ -392,16 +436,31 @@ async def test_dhcp_abort_flow(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + + if not last_update_success: + # ensure the last_update_succes is False for the device_coordinator. + reolink_connect.get_states = AsyncMock(side_effect=ReolinkError("Test error")) + async_fire_time_changed( + hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(minutes=1) + ) + await hass.async_block_till_done() dhcp_data = dhcp.DhcpServiceInfo( - ip=TEST_HOST, + ip=TEST_HOST2, hostname="Reolink", macaddress=TEST_MAC, ) + if attr is not None: + setattr(reolink_connect, attr, value) + result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data ) assert result["type"] is data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" + + await hass.async_block_till_done() + assert config_entry.data[CONF_HOST] == expected From 8dcc96c083a35079c03eae6b018933f52e6cd174 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:06:37 -0400 Subject: [PATCH 028/202] Fix device name in zwave_js repair flow (#99414) --- homeassistant/components/zwave_js/__init__.py | 9 +++------ homeassistant/components/zwave_js/repairs.py | 17 +++++++++++------ tests/components/zwave_js/test_repairs.py | 1 + 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 2d158f47e44..b56298e36ba 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -600,19 +600,16 @@ class NodeEvents: # device config has changed, and if so, issue a repair registry entry for a # possible reinterview if not node.is_controller_node and await node.async_has_device_config_changed(): + device_name = device.name_by_user or device.name or "Unnamed device" async_create_issue( self.hass, DOMAIN, f"device_config_file_changed.{device.id}", - data={"device_id": device.id}, + data={"device_id": device.id, "device_name": device_name}, is_fixable=True, is_persistent=False, translation_key="device_config_file_changed", - translation_placeholders={ - "device_name": device.name_by_user - or device.name - or "Unnamed device" - }, + translation_placeholders={"device_name": device_name}, severity=IssueSeverity.WARNING, ) diff --git a/homeassistant/components/zwave_js/repairs.py b/homeassistant/components/zwave_js/repairs.py index 58781941b09..89f51dddb88 100644 --- a/homeassistant/components/zwave_js/repairs.py +++ b/homeassistant/components/zwave_js/repairs.py @@ -1,8 +1,6 @@ """Repairs for Z-Wave JS.""" from __future__ import annotations -from typing import cast - import voluptuous as vol from zwave_js_server.model.node import Node @@ -16,9 +14,10 @@ from .helpers import async_get_node_from_device_id class DeviceConfigFileChangedFlow(RepairsFlow): """Handler for an issue fixing flow.""" - def __init__(self, node: Node) -> None: + def __init__(self, node: Node, device_name: str) -> None: """Initialize.""" self.node = node + self.device_name = device_name async def async_step_init( self, user_input: dict[str, str] | None = None @@ -34,17 +33,23 @@ class DeviceConfigFileChangedFlow(RepairsFlow): self.hass.async_create_task(self.node.async_refresh_info()) return self.async_create_entry(title="", data={}) - return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) + return self.async_show_form( + step_id="confirm", + data_schema=vol.Schema({}), + description_placeholders={"device_name": self.device_name}, + ) async def async_create_fix_flow( hass: HomeAssistant, issue_id: str, - data: dict[str, str | int | float | None] | None, + data: dict[str, str] | None, ) -> RepairsFlow: """Create flow.""" + if issue_id.split(".")[0] == "device_config_file_changed": + assert data return DeviceConfigFileChangedFlow( - async_get_node_from_device_id(hass, cast(dict, data)["device_id"]) + async_get_node_from_device_id(hass, data["device_id"]), data["device_name"] ) return ConfirmRepairFlow() diff --git a/tests/components/zwave_js/test_repairs.py b/tests/components/zwave_js/test_repairs.py index b1702900d7c..07371a299ef 100644 --- a/tests/components/zwave_js/test_repairs.py +++ b/tests/components/zwave_js/test_repairs.py @@ -77,6 +77,7 @@ async def test_device_config_file_changed( flow_id = data["flow_id"] assert data["step_id"] == "confirm" + assert data["description_placeholders"] == {"device_name": device.name} # Apply fix url = RepairsFlowResourceView.url.format(flow_id=flow_id) From 9fec0be17328d7243e6b2ba12eb57e21e67f8a46 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 2 Sep 2023 16:51:06 +0200 Subject: [PATCH 029/202] Log unexpected exceptions causing recorder shutdown (#99445) --- homeassistant/components/recorder/core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index ffdc3807039..bbaff24ff77 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -692,6 +692,10 @@ class Recorder(threading.Thread): """Run the recorder thread.""" try: self._run() + except Exception: # pylint: disable=broad-exception-caught + _LOGGER.exception( + "Recorder._run threw unexpected exception, recorder shutting down" + ) finally: # Ensure shutdown happens cleanly if # anything goes wrong in the run loop From caaba5d422fba47a87d56c1e637845ecbfb98227 Mon Sep 17 00:00:00 2001 From: jimmyd-be <34766203+jimmyd-be@users.noreply.github.com> Date: Sat, 2 Sep 2023 10:55:12 +0200 Subject: [PATCH 030/202] Fix translation bug Renson sensors (#99461) * Fix translation bug * Revert "Fix translation bug" This reverts commit 84b5e90dac1e75a4c9f6d890865ac42044858682. * Fixed translation of Renson sensor --- homeassistant/components/renson/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/renson/sensor.py b/homeassistant/components/renson/sensor.py index c8a355a0f7c..661ab82f373 100644 --- a/homeassistant/components/renson/sensor.py +++ b/homeassistant/components/renson/sensor.py @@ -266,6 +266,8 @@ SENSORS: tuple[RensonSensorEntityDescription, ...] = ( class RensonSensor(RensonEntity, SensorEntity): """Get a sensor data from the Renson API and store it in the state of the class.""" + _attr_has_entity_name = True + def __init__( self, description: RensonSensorEntityDescription, From cb1267477b7ab8e392ced0cf61347a1025165ceb Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 2 Sep 2023 09:47:59 +0200 Subject: [PATCH 031/202] Fix default language in Workday (#99463) Workday fix default language --- homeassistant/components/workday/binary_sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 6b6dfbffa5d..ad18c8863d6 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -129,7 +129,13 @@ async def async_setup_entry( workdays: list[str] = entry.options[CONF_WORKDAYS] year: int = (dt_util.now() + timedelta(days=days_offset)).year - obj_holidays: HolidayBase = country_holidays(country, subdiv=province, years=year) + cls: HolidayBase = country_holidays(country, subdiv=province, years=year) + obj_holidays: HolidayBase = country_holidays( + country, + subdiv=province, + years=year, + language=cls.default_language, + ) # Add custom holidays try: From e726b49adb3643ea399044fdae8449e26eab7e87 Mon Sep 17 00:00:00 2001 From: Andrew Onyshchuk Date: Fri, 1 Sep 2023 13:28:53 -0700 Subject: [PATCH 032/202] Update aiotractive to 0.5.6 (#99477) --- homeassistant/components/tractive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tractive/manifest.json b/homeassistant/components/tractive/manifest.json index 9e448d1fd26..75ddf065bd7 100644 --- a/homeassistant/components/tractive/manifest.json +++ b/homeassistant/components/tractive/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "cloud_push", "loggers": ["aiotractive"], - "requirements": ["aiotractive==0.5.5"] + "requirements": ["aiotractive==0.5.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6909ae3b924..942fa631902 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -360,7 +360,7 @@ aioswitcher==3.3.0 aiosyncthing==0.5.1 # homeassistant.components.tractive -aiotractive==0.5.5 +aiotractive==0.5.6 # homeassistant.components.unifi aiounifi==58 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4570ea0a32..2802d0a37f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ aioswitcher==3.3.0 aiosyncthing==0.5.1 # homeassistant.components.tractive -aiotractive==0.5.5 +aiotractive==0.5.6 # homeassistant.components.unifi aiounifi==58 From 46343bc261f6849876f72a2f9d704fd0587af019 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 2 Sep 2023 14:04:13 -0500 Subject: [PATCH 033/202] Bump zeroconf to 0.91.1 (#99490) --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 79b7e514f51..26577bd0bbe 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.88.0"] + "requirements": ["zeroconf==0.91.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 19169de83f6..cb114b1504d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.88.0 +zeroconf==0.91.1 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 942fa631902..e53d9d8fd0f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2766,7 +2766,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.88.0 +zeroconf==0.91.1 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2802d0a37f5..ec3b4a62052 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2036,7 +2036,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.88.0 +zeroconf==0.91.1 # homeassistant.components.zeversolar zeversolar==0.3.1 From 7b50316b3e4e61e4fa5b73c66888256b059e2625 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 2 Sep 2023 21:09:52 +0200 Subject: [PATCH 034/202] Bump version to 2023.9.0b3 --- 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 19370552139..12a12aea631 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 0248bd43af1..dc9d314fe4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.0b2" +version = "2023.9.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 269bcac982db1d58da21b1c8fc0dca4761ed3351 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 2 Sep 2023 16:19:45 -0700 Subject: [PATCH 035/202] Extend template entities with a script section (#96175) * Extend template entities with a script section This allows making a trigger entity that triggers a few times a day, and allows collecting data from a service resopnse which can be fed into a template entity. The current alternatives are to publish and subscribe to events or to store data in input entities. * Make variables set in actions accessible to templates * Format code --------- Co-authored-by: Erik --- homeassistant/components/script/__init__.py | 3 +- homeassistant/components/template/__init__.py | 19 ++++++-- homeassistant/components/template/config.py | 3 +- homeassistant/components/template/const.py | 1 + .../components/websocket_api/commands.py | 4 +- homeassistant/helpers/script.py | 15 +++++-- tests/components/template/test_sensor.py | 44 +++++++++++++++++++ 7 files changed, 79 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 8530aa3b04c..13b25a00053 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -563,7 +563,8 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity): ) coro = self._async_run(variables, context) if wait: - return await coro + script_result = await coro + return script_result.service_response if script_result else None # Caller does not want to wait for called script to finish so let script run in # separate Task. Make a new empty script stack; scripts are allowed to diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index e9ced060491..c4ba7081f5a 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -20,11 +20,12 @@ from homeassistant.helpers import ( update_coordinator, ) from homeassistant.helpers.reload import async_reload_integration_platforms +from homeassistant.helpers.script import Script from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration -from .const import CONF_TRIGGER, DOMAIN, PLATFORMS +from .const import CONF_ACTION, CONF_TRIGGER, DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -133,6 +134,7 @@ class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator): self.config = config self._unsub_start: Callable[[], None] | None = None self._unsub_trigger: Callable[[], None] | None = None + self._script: Script | None = None @property def unique_id(self) -> str | None: @@ -170,6 +172,14 @@ class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator): async def _attach_triggers(self, start_event=None) -> None: """Attach the triggers.""" + if CONF_ACTION in self.config: + self._script = Script( + self.hass, + self.config[CONF_ACTION], + self.name, + DOMAIN, + ) + if start_event is not None: self._unsub_start = None @@ -183,8 +193,11 @@ class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator): start_event is not None, ) - @callback - def _handle_triggered(self, run_variables, context=None): + async def _handle_triggered(self, run_variables, context=None): + if self._script: + script_result = await self._script.async_run(run_variables, context) + if script_result: + run_variables = script_result.variables self.async_set_updated_data( {"run_variables": run_variables, "context": context} ) diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index 2261bde2659..54c82d88c74 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -22,7 +22,7 @@ from . import ( select as select_platform, sensor as sensor_platform, ) -from .const import CONF_TRIGGER, DOMAIN +from .const import CONF_ACTION, CONF_TRIGGER, DOMAIN PACKAGE_MERGE_HINT = "list" @@ -30,6 +30,7 @@ CONFIG_SECTION_SCHEMA = vol.Schema( { vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_TRIGGER): cv.TRIGGER_SCHEMA, + vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(NUMBER_DOMAIN): vol.All( cv.ensure_list, [number_platform.NUMBER_SCHEMA] ), diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py index 9b371125750..6805c0ad812 100644 --- a/homeassistant/components/template/const.py +++ b/homeassistant/components/template/const.py @@ -2,6 +2,7 @@ from homeassistant.const import Platform +CONF_ACTION = "action" CONF_AVAILABILITY_TEMPLATE = "availability_template" CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" CONF_TRIGGER = "trigger" diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index bbcbfa6ecb8..c6564967a39 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -713,12 +713,12 @@ async def handle_execute_script( context = connection.context(msg) script_obj = Script(hass, script_config, f"{const.DOMAIN} script", const.DOMAIN) - response = await script_obj.async_run(msg.get("variables"), context=context) + script_result = await script_obj.async_run(msg.get("variables"), context=context) connection.send_result( msg["id"], { "context": context, - "response": response, + "response": script_result.service_response if script_result else None, }, ) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 4035d55b325..c9d8de23b96 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -6,6 +6,7 @@ from collections.abc import Callable, Mapping, Sequence from contextlib import asynccontextmanager, suppress from contextvars import ContextVar from copy import copy +from dataclasses import dataclass from datetime import datetime, timedelta from functools import partial import itertools @@ -401,7 +402,7 @@ class _ScriptRun: ) self._log("Executing step %s%s", self._script.last_action, _timeout) - async def async_run(self) -> ServiceResponse: + async def async_run(self) -> ScriptRunResult | None: """Run script.""" # Push the script to the script execution stack if (script_stack := script_stack_cv.get()) is None: @@ -443,7 +444,7 @@ class _ScriptRun: script_stack.pop() self._finish() - return response + return ScriptRunResult(response, self._variables) async def _async_step(self, log_exceptions): continue_on_error = self._action.get(CONF_CONTINUE_ON_ERROR, False) @@ -1189,6 +1190,14 @@ class _IfData(TypedDict): if_else: Script | None +@dataclass +class ScriptRunResult: + """Container with the result of a script run.""" + + service_response: ServiceResponse + variables: dict + + class Script: """Representation of a script.""" @@ -1480,7 +1489,7 @@ class Script: run_variables: _VarsType | None = None, context: Context | None = None, started_action: Callable[..., Any] | None = None, - ) -> ServiceResponse: + ) -> ScriptRunResult | None: """Run script.""" if context is None: self._log( diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 5eca8330789..1bd1e797c05 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -1530,3 +1530,47 @@ async def test_trigger_entity_restore_state( assert state.attributes["entity_picture"] == "/local/dogs.png" assert state.attributes["plus_one"] == 3 assert state.attributes["another"] == 1 + + +@pytest.mark.parametrize(("count", "domain"), [(1, "template")]) +@pytest.mark.parametrize( + "config", + [ + { + "template": [ + { + "unique_id": "listening-test-event", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": [ + { + "variables": { + "my_variable": "{{ trigger.event.data.beer + 1 }}" + }, + }, + ], + "sensor": [ + { + "name": "Hello Name", + "state": "{{ my_variable + 1 }}", + } + ], + }, + ], + }, + ], +) +async def test_trigger_action( + hass: HomeAssistant, start_ha, entity_registry: er.EntityRegistry +) -> None: + """Test trigger entity with an action works.""" + state = hass.states.get("sensor.hello_name") + assert state is not None + assert state.state == STATE_UNKNOWN + + context = Context() + hass.bus.async_fire("test_event", {"beer": 1}, context=context) + await hass.async_block_till_done() + + state = hass.states.get("sensor.hello_name") + assert state.state == "3" + assert state.context is context From 4524b38b8043f58d7cbff7382c1746fda70000db Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 4 Sep 2023 20:23:46 +0200 Subject: [PATCH 036/202] Mark AVM Fritz!Smarthome as Gold integration (#97086) set quality scale to gold --- homeassistant/components/fritzbox/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 35b78e91f81..fdf38d88439 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -7,6 +7,7 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["pyfritzhome"], + "quality_scale": "gold", "requirements": ["pyfritzhome==0.6.9"], "ssdp": [ { From e0594bffa14242d0d31da5424f7bff9ee1d743a6 Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Mon, 4 Sep 2023 00:30:56 -0700 Subject: [PATCH 037/202] Enumerate available states in Prometheus startup (#97993) --- .../components/prometheus/__init__.py | 24 +++++++++++----- tests/components/prometheus/test_init.py | 28 +++++++++++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index adc5225b286..1818f308239 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -120,10 +120,15 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: default_metric, ) - hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_state_changed) + hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_state_changed_event) hass.bus.listen( EVENT_ENTITY_REGISTRY_UPDATED, metrics.handle_entity_registry_updated ) + + for state in hass.states.all(): + if entity_filter(state.entity_id): + metrics.handle_state(state) + return True @@ -162,16 +167,13 @@ class PrometheusMetrics: self._metrics = {} self._climate_units = climate_units - def handle_state_changed(self, event): - """Listen for new messages on the bus, and add them to Prometheus.""" + def handle_state_changed_event(self, event): + """Handle new messages from the bus.""" if (state := event.data.get("new_state")) is None: return - entity_id = state.entity_id - _LOGGER.debug("Handling state update for %s", entity_id) - domain, _ = hacore.split_entity_id(entity_id) - if not self._filter(state.entity_id): + _LOGGER.debug("Filtered out entity %s", state.entity_id) return if (old_state := event.data.get("old_state")) is not None and ( @@ -179,6 +181,14 @@ class PrometheusMetrics: ) != state.attributes.get(ATTR_FRIENDLY_NAME): self._remove_labelsets(old_state.entity_id, old_friendly_name) + self.handle_state(state) + + def handle_state(self, state): + """Add/update a state in Prometheus.""" + entity_id = state.entity_id + _LOGGER.debug("Handling state update for %s", entity_id) + domain, _ = hacore.split_entity_id(entity_id) + ignored_states = (STATE_UNAVAILABLE, STATE_UNKNOWN) handler = f"_handle_{domain}" diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 82a205eb259..07a666946fb 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -107,6 +107,34 @@ async def generate_latest_metrics(client): return body +@pytest.mark.parametrize("namespace", [""]) +async def test_setup_enumeration(hass, hass_client, entity_registry, namespace): + """Test that setup enumerates existing states/entities.""" + + # The order of when things are created must be carefully controlled in + # this test, so we don't use fixtures. + + sensor_1 = entity_registry.async_get_or_create( + domain=sensor.DOMAIN, + platform="test", + unique_id="sensor_1", + unit_of_measurement=UnitOfTemperature.CELSIUS, + original_device_class=SensorDeviceClass.TEMPERATURE, + suggested_object_id="outside_temperature", + original_name="Outside Temperature", + ) + set_state_with_entry(hass, sensor_1, 12.3, {}) + assert await async_setup_component(hass, prometheus.DOMAIN, {prometheus.DOMAIN: {}}) + + client = await hass_client() + body = await generate_latest_metrics(client) + assert ( + 'homeassistant_sensor_temperature_celsius{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 12.3' in body + ) + + @pytest.mark.parametrize("namespace", [""]) async def test_view_empty_namespace(client, sensor_entities) -> None: """Test prometheus metrics view.""" From 1c2c13c9380481269679a25e54b5860bed1c2721 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Sep 2023 14:08:38 +0200 Subject: [PATCH 038/202] Don't set assumed_state in cover groups (#99391) --- homeassistant/components/group/cover.py | 20 +------------------- tests/components/group/test_cover.py | 13 +++++++------ 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index dbb49222bb0..d22184c0922 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -17,7 +17,6 @@ from homeassistant.components.cover import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_ENTITIES, @@ -44,7 +43,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import GroupEntity -from .util import attribute_equal, reduce_attribute +from .util import reduce_attribute KEY_OPEN_CLOSE = "open_close" KEY_STOP = "stop" @@ -116,7 +115,6 @@ class CoverGroup(GroupEntity, CoverEntity): _attr_is_opening: bool | None = False _attr_is_closing: bool | None = False _attr_current_cover_position: int | None = 100 - _attr_assumed_state: bool = True def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None: """Initialize a CoverGroup entity.""" @@ -251,8 +249,6 @@ class CoverGroup(GroupEntity, CoverEntity): @callback def async_update_group_state(self) -> None: """Update state and attributes.""" - self._attr_assumed_state = False - states = [ state.state for entity_id in self._entity_ids @@ -293,9 +289,6 @@ class CoverGroup(GroupEntity, CoverEntity): self._attr_current_cover_position = reduce_attribute( position_states, ATTR_CURRENT_POSITION ) - self._attr_assumed_state |= not attribute_equal( - position_states, ATTR_CURRENT_POSITION - ) tilt_covers = self._tilts[KEY_POSITION] all_tilt_states = [self.hass.states.get(x) for x in tilt_covers] @@ -303,9 +296,6 @@ class CoverGroup(GroupEntity, CoverEntity): self._attr_current_cover_tilt_position = reduce_attribute( tilt_states, ATTR_CURRENT_TILT_POSITION ) - self._attr_assumed_state |= not attribute_equal( - tilt_states, ATTR_CURRENT_TILT_POSITION - ) supported_features = CoverEntityFeature(0) if self._covers[KEY_OPEN_CLOSE]: @@ -322,11 +312,3 @@ class CoverGroup(GroupEntity, CoverEntity): if self._tilts[KEY_POSITION]: supported_features |= CoverEntityFeature.SET_TILT_POSITION self._attr_supported_features = supported_features - - if not self._attr_assumed_state: - for entity_id in self._entity_ids: - if (state := self.hass.states.get(entity_id)) is None: - continue - if state and state.attributes.get(ATTR_ASSUMED_STATE): - self._attr_assumed_state = True - break diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index 84ccba2ff66..4e0ddc19a31 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -346,10 +346,10 @@ async def test_attributes(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_POSITION] == 70 assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 60 - # ### Test assumed state ### + # ### Test state when group members have different states ### # ########################## - # For covers - assumed state set true if position differ + # Covers hass.states.async_set( DEMO_COVER, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 100} ) @@ -357,7 +357,7 @@ async def test_attributes(hass: HomeAssistant, setup_comp) -> None: state = hass.states.get(COVER_GROUP) assert state.state == STATE_OPEN - assert state.attributes[ATTR_ASSUMED_STATE] is True + assert ATTR_ASSUMED_STATE not in state.attributes assert state.attributes[ATTR_SUPPORTED_FEATURES] == 244 assert state.attributes[ATTR_CURRENT_POSITION] == 85 # (70 + 100) / 2 assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 60 @@ -373,7 +373,7 @@ async def test_attributes(hass: HomeAssistant, setup_comp) -> None: assert ATTR_CURRENT_POSITION not in state.attributes assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 60 - # For tilts - assumed state set true if tilt position differ + # Tilts hass.states.async_set( DEMO_TILT, STATE_OPEN, @@ -383,7 +383,7 @@ async def test_attributes(hass: HomeAssistant, setup_comp) -> None: state = hass.states.get(COVER_GROUP) assert state.state == STATE_OPEN - assert state.attributes[ATTR_ASSUMED_STATE] is True + assert ATTR_ASSUMED_STATE not in state.attributes assert state.attributes[ATTR_SUPPORTED_FEATURES] == 128 assert ATTR_CURRENT_POSITION not in state.attributes assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 80 # (60 + 100) / 2 @@ -399,11 +399,12 @@ async def test_attributes(hass: HomeAssistant, setup_comp) -> None: assert ATTR_CURRENT_POSITION not in state.attributes assert ATTR_CURRENT_TILT_POSITION not in state.attributes + # Group member has set assumed_state hass.states.async_set(DEMO_TILT, STATE_CLOSED, {ATTR_ASSUMED_STATE: True}) await hass.async_block_till_done() state = hass.states.get(COVER_GROUP) - assert state.attributes[ATTR_ASSUMED_STATE] is True + assert ATTR_ASSUMED_STATE not in state.attributes # Test entity registry integration entity_registry = er.async_get(hass) From 2a5f8ee4a7aa0e24de80101d6b9a57127bb968a5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Sep 2023 14:08:50 +0200 Subject: [PATCH 039/202] Don't set assumed_state in fan groups (#99399) --- homeassistant/components/group/fan.py | 18 +----------------- tests/components/group/test_config_flow.py | 2 +- tests/components/group/test_fan.py | 17 ++++------------- 3 files changed, 6 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index 4ee788c8402..4e3bb824266 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -25,7 +25,6 @@ from homeassistant.components.fan import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_ENTITIES, @@ -41,12 +40,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import GroupEntity -from .util import ( - attribute_equal, - most_frequent_attribute, - reduce_attribute, - states_equal, -) +from .util import attribute_equal, most_frequent_attribute, reduce_attribute SUPPORTED_FLAGS = { FanEntityFeature.SET_SPEED, @@ -110,7 +104,6 @@ class FanGroup(GroupEntity, FanEntity): """Representation of a FanGroup.""" _attr_available: bool = False - _attr_assumed_state: bool = True def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None: """Initialize a FanGroup entity.""" @@ -243,19 +236,16 @@ class FanGroup(GroupEntity, FanEntity): """Set an attribute based on most frequent supported entities attributes.""" states = self._async_states_by_support_flag(flag) setattr(self, attr, most_frequent_attribute(states, entity_attr)) - self._attr_assumed_state |= not attribute_equal(states, entity_attr) @callback def async_update_group_state(self) -> None: """Update state and attributes.""" - self._attr_assumed_state = False states = [ state for entity_id in self._entity_ids if (state := self.hass.states.get(entity_id)) is not None ] - self._attr_assumed_state |= not states_equal(states) # Set group as unavailable if all members are unavailable or missing self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states) @@ -274,9 +264,6 @@ class FanGroup(GroupEntity, FanEntity): FanEntityFeature.SET_SPEED ) self._percentage = reduce_attribute(percentage_states, ATTR_PERCENTAGE) - self._attr_assumed_state |= not attribute_equal( - percentage_states, ATTR_PERCENTAGE - ) if ( percentage_states and percentage_states[0].attributes.get(ATTR_PERCENTAGE_STEP) @@ -301,6 +288,3 @@ class FanGroup(GroupEntity, FanEntity): ior, [feature for feature in SUPPORTED_FLAGS if self._fans[feature]], 0 ) ) - self._attr_assumed_state |= any( - state.attributes.get(ATTR_ASSUMED_STATE) for state in states - ) diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index d0e90fe61bd..1c8275c7f2d 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -468,7 +468,7 @@ async def test_options_flow_hides_members( COVER_ATTRS = [{"supported_features": 0}, {}] EVENT_ATTRS = [{"event_types": []}, {"event_type": None}] -FAN_ATTRS = [{"supported_features": 0}, {"assumed_state": True}] +FAN_ATTRS = [{"supported_features": 0}, {}] LIGHT_ATTRS = [ { "icon": "mdi:lightbulb-group", diff --git a/tests/components/group/test_fan.py b/tests/components/group/test_fan.py index 6269df3fed7..2272a29f6ed 100644 --- a/tests/components/group/test_fan.py +++ b/tests/components/group/test_fan.py @@ -247,11 +247,7 @@ async def test_attributes(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_PERCENTAGE] == 50 assert ATTR_ASSUMED_STATE not in state.attributes - # Add Entity that supports - # ### Test assumed state ### - # ########################## - - # Add Entity with a different speed should set assumed state + # Add Entity with a different speed should not set assumed state hass.states.async_set( PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_ON, @@ -264,7 +260,7 @@ async def test_attributes(hass: HomeAssistant, setup_comp) -> None: state = hass.states.get(FAN_GROUP) assert state.state == STATE_ON - assert state.attributes[ATTR_ASSUMED_STATE] is True + assert ATTR_ASSUMED_STATE not in state.attributes assert state.attributes[ATTR_PERCENTAGE] == int((50 + 75) / 2) @@ -306,11 +302,7 @@ async def test_direction_oscillating(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_DIRECTION] == DIRECTION_FORWARD assert ATTR_ASSUMED_STATE not in state.attributes - # Add Entity that supports - # ### Test assumed state ### - # ########################## - - # Add Entity with a different direction should set assumed state + # Add Entity with a different direction should not set assumed state hass.states.async_set( PERCENTAGE_FULL_FAN_ENTITY_ID, STATE_ON, @@ -325,11 +317,10 @@ async def test_direction_oscillating(hass: HomeAssistant, setup_comp) -> None: state = hass.states.get(FAN_GROUP) assert state.state == STATE_ON - assert state.attributes[ATTR_ASSUMED_STATE] is True + assert ATTR_ASSUMED_STATE not in state.attributes assert ATTR_PERCENTAGE in state.attributes assert state.attributes[ATTR_PERCENTAGE] == 50 assert state.attributes[ATTR_OSCILLATING] is True - assert ATTR_ASSUMED_STATE in state.attributes # Now that everything is the same, no longer assumed state From 852589f02549853d512e88a0eb330c5bbdd2856c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiit=20R=C3=A4tsep?= Date: Mon, 4 Sep 2023 11:03:58 +0300 Subject: [PATCH 040/202] Fix battery reading in SOMA API (#99403) Co-authored-by: Robert Resch --- homeassistant/components/soma/sensor.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/soma/sensor.py b/homeassistant/components/soma/sensor.py index 6472f6934e0..d1c0de188a0 100644 --- a/homeassistant/components/soma/sensor.py +++ b/homeassistant/components/soma/sensor.py @@ -43,11 +43,12 @@ class SomaSensor(SomaEntity, SensorEntity): async def async_update(self) -> None: """Update the sensor with the latest data.""" response = await self.get_battery_level_from_api() - - # https://support.somasmarthome.com/hc/en-us/articles/360026064234-HTTP-API - # battery_level response is expected to be min = 360, max 410 for - # 0-100% levels above 410 are consider 100% and below 360, 0% as the - # device considers 360 the minimum to move the motor. - _battery = round(2 * (response["battery_level"] - 360)) + _battery = response.get("battery_percentage") + if _battery is None: + # https://support.somasmarthome.com/hc/en-us/articles/360026064234-HTTP-API + # battery_level response is expected to be min = 360, max 410 for + # 0-100% levels above 410 are consider 100% and below 360, 0% as the + # device considers 360 the minimum to move the motor. + _battery = round(2 * (response["battery_level"] - 360)) battery = max(min(100, _battery), 0) self.battery_state = battery From bdc39e1d52aa73a23a7c6d68e3f5653bb0d4a439 Mon Sep 17 00:00:00 2001 From: Mike O'Driscoll Date: Sun, 3 Sep 2023 11:08:17 -0400 Subject: [PATCH 041/202] Fix recollect_waste month time boundary issue (#99429) --- .../components/recollect_waste/__init__.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index 21cf574d548..076067312eb 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -1,7 +1,7 @@ """The ReCollect Waste integration.""" from __future__ import annotations -from datetime import timedelta +from datetime import date, timedelta from typing import Any from aiorecollect.client import Client, PickupEvent @@ -31,7 +31,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_get_pickup_events() -> list[PickupEvent]: """Get the next pickup.""" try: - return await client.async_get_pickup_events() + # Retrieve today through to 35 days in the future, to get + # coverage across a full two months boundary so that no + # upcoming pickups are missed. The api.recollect.net base API + # call returns only the current month when no dates are passed. + # This ensures that data about when the next pickup is will be + # returned when the next pickup is the first day of the next month. + # Ex: Today is August 31st, tomorrow is a pickup on September 1st. + today = date.today() + return await client.async_get_pickup_events( + start_date=today, + end_date=today + timedelta(days=35), + ) except RecollectError as err: raise UpdateFailed( f"Error while requesting data from ReCollect: {err}" From e5fd6961a8ef10a2c656c891a625f11dc253e54d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Sep 2023 14:09:51 +0200 Subject: [PATCH 042/202] Set state of entity with invalid state to unknown (#99452) * Set state of entity with invalid state to unknown * Add test * Apply suggestions from code review Co-authored-by: Robert Resch * Update test_entity.py --------- Co-authored-by: Robert Resch --- homeassistant/helpers/entity.py | 16 ++++++++++++++-- tests/helpers/test_entity.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 29a944874ab..e946c41d3b8 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -35,7 +35,11 @@ from homeassistant.const import ( EntityCategory, ) from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError, NoEntitySpecifiedError +from homeassistant.exceptions import ( + HomeAssistantError, + InvalidStateError, + NoEntitySpecifiedError, +) from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util, ensure_unique_string, slugify @@ -848,7 +852,15 @@ class Entity(ABC): self._context = None self._context_set = None - hass.states.async_set(entity_id, state, attr, self.force_update, self._context) + try: + hass.states.async_set( + entity_id, state, attr, self.force_update, self._context + ) + except InvalidStateError: + _LOGGER.exception("Failed to set state, fall back to %s", STATE_UNKNOWN) + hass.states.async_set( + entity_id, STATE_UNKNOWN, {}, self.force_update, self._context + ) def schedule_update_ha_state(self, force_refresh: bool = False) -> None: """Schedule an update ha state change task. diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 200b0230adb..20bea6a98eb 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -3,6 +3,7 @@ import asyncio from collections.abc import Iterable import dataclasses from datetime import timedelta +import logging import threading from typing import Any from unittest.mock import MagicMock, PropertyMock, patch @@ -1477,3 +1478,30 @@ async def test_warn_no_platform( caplog.clear() ent.async_write_ha_state() assert error_message not in caplog.text + + +async def test_invalid_state( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test the entity helper catches InvalidState and sets state to unknown.""" + ent = entity.Entity() + ent.entity_id = "test.test" + ent.hass = hass + + ent._attr_state = "x" * 255 + ent.async_write_ha_state() + assert hass.states.get("test.test").state == "x" * 255 + + caplog.clear() + ent._attr_state = "x" * 256 + ent.async_write_ha_state() + assert hass.states.get("test.test").state == STATE_UNKNOWN + assert ( + "homeassistant.helpers.entity", + logging.ERROR, + f"Failed to set state, fall back to {STATE_UNKNOWN}", + ) in caplog.record_tuples + + ent._attr_state = "x" * 255 + ent.async_write_ha_state() + assert hass.states.get("test.test").state == "x" * 255 From 423e8fbde3e5130ff4efa132a16ed1d9d2b5be52 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Sep 2023 14:10:43 +0200 Subject: [PATCH 043/202] Validate state in template helper preview (#99455) * Validate state in template helper preview * Deduplicate state validation --- .../components/template/template_entity.py | 13 ++++++++++--- homeassistant/core.py | 16 +++++++++++----- tests/test_core.py | 7 +++++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index ac06e2c8734..c33674fa86f 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -25,6 +25,7 @@ from homeassistant.core import ( HomeAssistant, State, callback, + validate_state, ) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv @@ -413,8 +414,8 @@ class TemplateEntity(Entity): return for update in updates: - for attr in self._template_attrs[update.template]: - attr.handle_result( + for template_attr in self._template_attrs[update.template]: + template_attr.handle_result( event, update.template, update.last_result, update.result ) @@ -422,7 +423,13 @@ class TemplateEntity(Entity): self.async_write_ha_state() return - self._preview_callback(*self._async_generate_attributes(), None) + try: + state, attrs = self._async_generate_attributes() + validate_state(state) + except Exception as err: # pylint: disable=broad-exception-caught + self._preview_callback(None, None, str(err)) + else: + self._preview_callback(state, attrs, None) @callback def _async_template_startup(self, *_: Any) -> None: diff --git a/homeassistant/core.py b/homeassistant/core.py index 18c5c355ae9..f2921e244ab 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -174,6 +174,16 @@ def valid_entity_id(entity_id: str) -> bool: return VALID_ENTITY_ID.match(entity_id) is not None +def validate_state(state: str) -> str: + """Validate a state, raise if it not valid.""" + if len(state) > MAX_LENGTH_STATE_STATE: + raise InvalidStateError( + f"Invalid state with length {len(state)}. " + "State max length is 255 characters." + ) + return state + + def callback(func: _CallableT) -> _CallableT: """Annotation to mark method as safe to call from within the event loop.""" setattr(func, "_hass_callback", True) @@ -1251,11 +1261,7 @@ class State: "Format should be ." ) - if len(state) > MAX_LENGTH_STATE_STATE: - raise InvalidStateError( - f"Invalid state encountered for entity ID: {entity_id}. " - "State max length is 255 characters." - ) + validate_state(state) self.entity_id = entity_id.lower() self.state = state diff --git a/tests/test_core.py b/tests/test_core.py index 4f7916e757b..8ec4dad2ebd 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -2464,3 +2464,10 @@ async def test_cancellable_hassjob(hass: HomeAssistant) -> None: # Cleanup timer2.cancel() + + +async def test_validate_state(hass: HomeAssistant) -> None: + """Test validate_state.""" + assert ha.validate_state("test") == "test" + with pytest.raises(InvalidStateError): + ha.validate_state("t" * 256) From 2bc08d1fccac71a6b230d8c3c7d874feb95a25a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Sep 2023 13:19:10 -0500 Subject: [PATCH 044/202] Fix module check in _async_get_flow_handler (#99509) We should have been checking for the module in hass.data[DATA_COMPONENTS] and not hass.config.components as the check was ineffective if there were no existing integrations instances for the domain which is the case for discovery or when the integration is ignored --- homeassistant/config_entries.py | 4 +++- homeassistant/loader.py | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a3b03407a14..02117c3ac5a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2055,7 +2055,9 @@ async def _async_get_flow_handler( """Get a flow handler for specified domain.""" # First check if there is a handler registered for the domain - if domain in hass.config.components and (handler := HANDLERS.get(domain)): + if loader.is_component_module_loaded(hass, f"{domain}.config_flow") and ( + handler := HANDLERS.get(domain) + ): return handler await _load_integration(hass, domain, hass_config) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 40161bd3be9..37e470c1178 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -1162,3 +1162,8 @@ def _lookup_path(hass: HomeAssistant) -> list[str]: if hass.config.safe_mode: return [PACKAGE_BUILTIN] return [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN] + + +def is_component_module_loaded(hass: HomeAssistant, module: str) -> bool: + """Test if a component module is loaded.""" + return module in hass.data[DATA_COMPONENTS] From 70b460f52bc86168b28f59a7d0fd2ddcf34d20fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 2 Sep 2023 15:25:42 -0500 Subject: [PATCH 045/202] Bump aiohomekit to 3.0.2 (#99514) --- 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 83852f38d52..9567ff83cea 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.0.1"], + "requirements": ["aiohomekit==3.0.2"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index e53d9d8fd0f..06f5cea0169 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -249,7 +249,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.0.1 +aiohomekit==3.0.2 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec3b4a62052..93779c21efd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -227,7 +227,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.0.1 +aiohomekit==3.0.2 # homeassistant.components.emulated_hue # homeassistant.components.http From 453c5f90f8fa823119715a18f9784989dc6c11a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Sep 2023 08:22:03 -0500 Subject: [PATCH 046/202] Bump bleak to 0.21.0 (#99520) Co-authored-by: Martin Hjelmare --- .../components/bluetooth/manifest.json | 2 +- homeassistant/components/bluetooth/wrappers.py | 6 ++++-- .../components/esphome/bluetooth/client.py | 18 +++++++++++------- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 19 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 59a87f4dfbb..8ddf0a38c1d 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -14,7 +14,7 @@ ], "quality_scale": "internal", "requirements": [ - "bleak==0.20.2", + "bleak==0.21.0", "bleak-retry-connector==3.1.1", "bluetooth-adapters==0.16.0", "bluetooth-auto-recovery==1.2.1", diff --git a/homeassistant/components/bluetooth/wrappers.py b/homeassistant/components/bluetooth/wrappers.py index 3a0abc855b5..97f253f8825 100644 --- a/homeassistant/components/bluetooth/wrappers.py +++ b/homeassistant/components/bluetooth/wrappers.py @@ -120,15 +120,17 @@ class HaBleakScannerWrapper(BaseBleakScanner): def register_detection_callback( self, callback: AdvertisementDataCallback | None - ) -> None: + ) -> Callable[[], None]: """Register a detection callback. The callback is called when a device is discovered or has a property changed. - This method takes the callback and registers it with the long running sscanner. + This method takes the callback and registers it with the long running scanner. """ self._advertisement_data_callback = callback self._setup_detection_callback() + assert self._detection_cancel is not None + return self._detection_cancel def _setup_detection_callback(self) -> None: """Set up the detection callback.""" diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index ad43ca5df7d..411a5b989a3 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -7,9 +7,15 @@ import contextlib from dataclasses import dataclass, field from functools import partial import logging +import sys from typing import Any, TypeVar, cast import uuid +if sys.version_info < (3, 12): + from typing_extensions import Buffer +else: + from collections.abc import Buffer + from aioesphomeapi import ( ESP_CONNECTION_ERROR_DESCRIPTION, ESPHOME_GATT_ERRORS, @@ -620,14 +626,14 @@ class ESPHomeClient(BaseBleakClient): @api_error_as_bleak_error async def write_gatt_char( self, - char_specifier: BleakGATTCharacteristic | int | str | uuid.UUID, - data: bytes | bytearray | memoryview, + characteristic: BleakGATTCharacteristic | int | str | uuid.UUID, + data: Buffer, response: bool = False, ) -> None: """Perform a write operation of the specified GATT characteristic. Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): + characteristic (BleakGATTCharacteristic, int, str or UUID): The characteristic to write to, specified by either integer handle, UUID or directly by the BleakGATTCharacteristic object representing it. @@ -635,16 +641,14 @@ class ESPHomeClient(BaseBleakClient): response (bool): If write-with-response operation should be done. Defaults to `False`. """ - characteristic = self._resolve_characteristic(char_specifier) + characteristic = self._resolve_characteristic(characteristic) await self._client.bluetooth_gatt_write( self._address_as_int, characteristic.handle, bytes(data), response ) @verify_connected @api_error_as_bleak_error - async def write_gatt_descriptor( - self, handle: int, data: bytes | bytearray | memoryview - ) -> None: + async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: """Perform a write operation on the specified GATT descriptor. Args: diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cb114b1504d..c169c2ab3b1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -9,7 +9,7 @@ attrs==23.1.0 awesomeversion==22.9.0 bcrypt==4.0.1 bleak-retry-connector==3.1.1 -bleak==0.20.2 +bleak==0.21.0 bluetooth-adapters==0.16.0 bluetooth-auto-recovery==1.2.1 bluetooth-data-tools==1.9.1 diff --git a/requirements_all.txt b/requirements_all.txt index 06f5cea0169..719e9d6f0ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -521,7 +521,7 @@ bizkaibus==0.1.1 bleak-retry-connector==3.1.1 # homeassistant.components.bluetooth -bleak==0.20.2 +bleak==0.21.0 # homeassistant.components.blebox blebox-uniapi==2.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93779c21efd..e06beb16ed1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -439,7 +439,7 @@ bimmer-connected==0.14.0 bleak-retry-connector==3.1.1 # homeassistant.components.bluetooth -bleak==0.20.2 +bleak==0.21.0 # homeassistant.components.blebox blebox-uniapi==2.1.4 From c677dae9c1b5bca91fabfc271ffa8feb04f910fe Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 3 Sep 2023 17:48:25 +0200 Subject: [PATCH 047/202] Modbus switch, allow restore "unknown" (#99533) --- homeassistant/components/modbus/base_platform.py | 6 +++++- tests/components/modbus/test_switch.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 7c3fcd78b05..65cfa1b49ba 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -21,6 +21,7 @@ from homeassistant.const import ( CONF_SLAVE, CONF_STRUCTURE, CONF_UNIQUE_ID, + STATE_OFF, STATE_ON, ) from homeassistant.core import callback @@ -311,7 +312,10 @@ class BaseSwitch(BasePlatform, ToggleEntity, RestoreEntity): """Handle entity which will be added.""" await self.async_base_added_to_hass() if state := await self.async_get_last_state(): - self._attr_is_on = state.state == STATE_ON + if state.state == STATE_ON: + self._attr_is_on = True + elif state.state == STATE_OFF: + self._attr_is_on = False async def async_turn(self, command: int) -> None: """Evaluate switch result.""" diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index dce4588d606..7a79e19869a 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -250,7 +250,7 @@ async def test_lazy_error_switch( @pytest.mark.parametrize( "mock_test_state", - [(State(ENTITY_ID, STATE_ON),)], + [(State(ENTITY_ID, STATE_ON),), (State(ENTITY_ID, STATE_OFF),)], indirect=True, ) @pytest.mark.parametrize( From a60e23caf2b5f0385a5c37fea7b14428a7757e3e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Sep 2023 09:13:21 -0500 Subject: [PATCH 048/202] Bump bleak-retry-connector to 3.1.2 (#99540) --- 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 8ddf0a38c1d..c643df542d8 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -15,7 +15,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.21.0", - "bleak-retry-connector==3.1.1", + "bleak-retry-connector==3.1.2", "bluetooth-adapters==0.16.0", "bluetooth-auto-recovery==1.2.1", "bluetooth-data-tools==1.9.1", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c169c2ab3b1..4e6338aa30d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ atomicwrites-homeassistant==1.4.1 attrs==23.1.0 awesomeversion==22.9.0 bcrypt==4.0.1 -bleak-retry-connector==3.1.1 +bleak-retry-connector==3.1.2 bleak==0.21.0 bluetooth-adapters==0.16.0 bluetooth-auto-recovery==1.2.1 diff --git a/requirements_all.txt b/requirements_all.txt index 719e9d6f0ed..04ffad53cbf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -518,7 +518,7 @@ bimmer-connected==0.14.0 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==3.1.1 +bleak-retry-connector==3.1.2 # homeassistant.components.bluetooth bleak==0.21.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e06beb16ed1..a99e9bbb765 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -436,7 +436,7 @@ bellows==0.36.1 bimmer-connected==0.14.0 # homeassistant.components.bluetooth -bleak-retry-connector==3.1.1 +bleak-retry-connector==3.1.2 # homeassistant.components.bluetooth bleak==0.21.0 From 8ccd2b6457dccee7608e1134decaae471ab3febd Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 1 Sep 2023 23:33:19 +0100 Subject: [PATCH 049/202] Update bluetooth-data-tools to 1.11.0 (#99485) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/ld2410_ble/manifest.json | 2 +- homeassistant/components/led_ble/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index c643df542d8..e1a5ee41324 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -18,7 +18,7 @@ "bleak-retry-connector==3.1.2", "bluetooth-adapters==0.16.0", "bluetooth-auto-recovery==1.2.1", - "bluetooth-data-tools==1.9.1", + "bluetooth-data-tools==1.11.0", "dbus-fast==1.94.1" ] } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index d0ab27656c2..7d552f340f0 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "requirements": [ "async_interrupt==1.1.1", "aioesphomeapi==16.0.3", - "bluetooth-data-tools==1.9.1", + "bluetooth-data-tools==1.11.0", "esphome-dashboard-api==1.2.3" ], "zeroconf": ["_esphomelib._tcp.local."] diff --git a/homeassistant/components/ld2410_ble/manifest.json b/homeassistant/components/ld2410_ble/manifest.json index 0c77e0e2ef5..798a80147de 100644 --- a/homeassistant/components/ld2410_ble/manifest.json +++ b/homeassistant/components/ld2410_ble/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/ld2410_ble", "integration_type": "device", "iot_class": "local_push", - "requirements": ["bluetooth-data-tools==1.9.1", "ld2410-ble==0.1.1"] + "requirements": ["bluetooth-data-tools==1.11.0", "ld2410-ble==0.1.1"] } diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 36e3b7355ff..da5b4b0a4ee 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -32,5 +32,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/led_ble", "iot_class": "local_polling", - "requirements": ["bluetooth-data-tools==1.9.1", "led-ble==1.0.0"] + "requirements": ["bluetooth-data-tools==1.11.0", "led-ble==1.0.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4e6338aa30d..598c2625f78 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ bleak-retry-connector==3.1.2 bleak==0.21.0 bluetooth-adapters==0.16.0 bluetooth-auto-recovery==1.2.1 -bluetooth-data-tools==1.9.1 +bluetooth-data-tools==1.11.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 04ffad53cbf..4dc721b679b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -549,7 +549,7 @@ bluetooth-auto-recovery==1.2.1 # homeassistant.components.esphome # homeassistant.components.ld2410_ble # homeassistant.components.led_ble -bluetooth-data-tools==1.9.1 +bluetooth-data-tools==1.11.0 # homeassistant.components.bond bond-async==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a99e9bbb765..19d4295649d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -460,7 +460,7 @@ bluetooth-auto-recovery==1.2.1 # homeassistant.components.esphome # homeassistant.components.ld2410_ble # homeassistant.components.led_ble -bluetooth-data-tools==1.9.1 +bluetooth-data-tools==1.11.0 # homeassistant.components.bond bond-async==0.2.1 From c791ddc937b828977643f4afba441746424e1651 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 4 Sep 2023 07:33:46 +0200 Subject: [PATCH 050/202] Fix loading filesize coordinator from wrong place (#99547) * Fix loading filesize coordinator from wrong place * aboslute in executor * combine into executor --- homeassistant/components/filesize/__init__.py | 14 ++++-- .../components/filesize/coordinator.py | 48 +++++++++++++++++++ homeassistant/components/filesize/sensor.py | 45 ++--------------- 3 files changed, 62 insertions(+), 45 deletions(-) create mode 100644 homeassistant/components/filesize/coordinator.py diff --git a/homeassistant/components/filesize/__init__.py b/homeassistant/components/filesize/__init__.py index 73f060e79b7..9d7cc99421f 100644 --- a/homeassistant/components/filesize/__init__.py +++ b/homeassistant/components/filesize/__init__.py @@ -9,10 +9,11 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from .const import PLATFORMS +from .coordinator import FileSizeCoordinator -def _check_path(hass: HomeAssistant, path: str) -> None: - """Check if path is valid and allowed.""" +def _get_full_path(hass: HomeAssistant, path: str) -> str: + """Check if path is valid, allowed and return full path.""" get_path = pathlib.Path(path) if not get_path.exists() or not get_path.is_file(): raise ConfigEntryNotReady(f"Can not access file {path}") @@ -20,10 +21,17 @@ def _check_path(hass: HomeAssistant, path: str) -> None: if not hass.config.is_allowed_path(path): raise ConfigEntryNotReady(f"Filepath {path} is not valid or allowed") + return str(get_path.absolute()) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" - await hass.async_add_executor_job(_check_path, hass, entry.data[CONF_FILE_PATH]) + full_path = await hass.async_add_executor_job( + _get_full_path, hass, entry.data[CONF_FILE_PATH] + ) + coordinator = FileSizeCoordinator(hass, full_path) + await coordinator.async_config_entry_first_refresh() + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/filesize/coordinator.py b/homeassistant/components/filesize/coordinator.py new file mode 100644 index 00000000000..75411f84975 --- /dev/null +++ b/homeassistant/components/filesize/coordinator.py @@ -0,0 +1,48 @@ +"""Coordinator for monitoring the size of a file.""" +from __future__ import annotations + +from datetime import datetime, timedelta +import logging +import os + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +import homeassistant.util.dt as dt_util + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class FileSizeCoordinator(DataUpdateCoordinator[dict[str, int | float | datetime]]): + """Filesize coordinator.""" + + def __init__(self, hass: HomeAssistant, path: str) -> None: + """Initialize filesize coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=60), + always_update=False, + ) + self._path = path + + async def _async_update_data(self) -> dict[str, float | int | datetime]: + """Fetch file information.""" + try: + statinfo = await self.hass.async_add_executor_job(os.stat, self._path) + except OSError as error: + raise UpdateFailed(f"Can not retrieve file statistics {error}") from error + + size = statinfo.st_size + last_updated = dt_util.utc_from_timestamp(statinfo.st_mtime) + + _LOGGER.debug("size %s, last updated %s", size, last_updated) + data: dict[str, int | float | datetime] = { + "file": round(size / 1e6, 2), + "bytes": size, + "last_updated": last_updated, + } + + return data diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 0e600363640..c8e5dae5892 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -1,9 +1,8 @@ """Sensor for monitoring the size of a file.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime import logging -import os import pathlib from homeassistant.components.sensor import ( @@ -17,14 +16,10 @@ from homeassistant.const import CONF_FILE_PATH, EntityCategory, UnitOfInformatio from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) -import homeassistant.util.dt as dt_util +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN +from .coordinator import FileSizeCoordinator _LOGGER = logging.getLogger(__name__) @@ -80,40 +75,6 @@ async def async_setup_entry( ) -class FileSizeCoordinator(DataUpdateCoordinator): - """Filesize coordinator.""" - - def __init__(self, hass: HomeAssistant, path: str) -> None: - """Initialize filesize coordinator.""" - super().__init__( - hass, - _LOGGER, - name=DOMAIN, - update_interval=timedelta(seconds=60), - always_update=False, - ) - self._path = path - - async def _async_update_data(self) -> dict[str, float | int | datetime]: - """Fetch file information.""" - try: - statinfo = await self.hass.async_add_executor_job(os.stat, self._path) - except OSError as error: - raise UpdateFailed(f"Can not retrieve file statistics {error}") from error - - size = statinfo.st_size - last_updated = dt_util.utc_from_timestamp(statinfo.st_mtime) - - _LOGGER.debug("size %s, last updated %s", size, last_updated) - data: dict[str, int | float | datetime] = { - "file": round(size / 1e6, 2), - "bytes": size, - "last_updated": last_updated, - } - - return data - - class FilesizeEntity(CoordinatorEntity[FileSizeCoordinator], SensorEntity): """Filesize sensor.""" From 5c42ea57b3cba7e0ceb1dee1cf2c193ea8d65e23 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 3 Sep 2023 21:31:25 +0200 Subject: [PATCH 051/202] Bump aiounifi to v60 (#99548) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 363313bf878..cb1c8f1c0dc 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==58"], + "requirements": ["aiounifi==60"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 4dc721b679b..988413b7f8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -363,7 +363,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==58 +aiounifi==60 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 19d4295649d..db79ab36403 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,7 +338,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==58 +aiounifi==60 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From a7efeb2c878f5e7e49606689bc364c37a2ce2c8e Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 4 Sep 2023 09:26:14 -0400 Subject: [PATCH 052/202] Bump ZHA dependencies (#99561) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 809b576defa..7352487a318 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,12 +21,12 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.36.1", + "bellows==0.36.2", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.103", "zigpy-deconz==0.21.0", - "zigpy==0.57.0", + "zigpy==0.57.1", "zigpy-xbee==0.18.1", "zigpy-zigate==0.11.0", "zigpy-znp==0.11.4", diff --git a/requirements_all.txt b/requirements_all.txt index 988413b7f8e..1c634186c3c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -509,7 +509,7 @@ beautifulsoup4==4.12.2 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.36.1 +bellows==0.36.2 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.0 @@ -2793,7 +2793,7 @@ zigpy-zigate==0.11.0 zigpy-znp==0.11.4 # homeassistant.components.zha -zigpy==0.57.0 +zigpy==0.57.1 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index db79ab36403..c63c5ca7783 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -430,7 +430,7 @@ base36==0.1.1 beautifulsoup4==4.12.2 # homeassistant.components.zha -bellows==0.36.1 +bellows==0.36.2 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.0 @@ -2057,7 +2057,7 @@ zigpy-zigate==0.11.0 zigpy-znp==0.11.4 # homeassistant.components.zha -zigpy==0.57.0 +zigpy==0.57.1 # homeassistant.components.zwave_js zwave-js-server-python==0.51.0 From 37f8f911712c9999b61edb4f2b15f56db7b5a3ec Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Sep 2023 20:17:30 +0200 Subject: [PATCH 053/202] Small cleanup of WS command render_template (#99562) --- homeassistant/components/websocket_api/commands.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index c6564967a39..84c7567a40e 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -516,7 +516,6 @@ async def handle_render_template( template_obj = _cached_template(template_str, hass) variables = msg.get("variables") timeout = msg.get("timeout") - info = None if timeout: try: @@ -540,7 +539,6 @@ async def handle_render_template( event: EventType[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: - nonlocal info track_template_result = updates.pop() result = track_template_result.result if isinstance(result, TemplateError): @@ -549,7 +547,7 @@ async def handle_render_template( connection.send_message( messages.event_message( - msg["id"], {"result": result, "listeners": info.listeners} # type: ignore[attr-defined] + msg["id"], {"result": result, "listeners": info.listeners} ) ) From 30c65652058ed41180446bfb933b059e32e9f138 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Sep 2023 13:39:24 +0200 Subject: [PATCH 054/202] Bump pyenphase to 1.9.1 (#99574) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 540c121bb17..a45f4f01e49 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.8.1"], + "requirements": ["pyenphase==1.9.1"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 1c634186c3c..c180b2db67d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1671,7 +1671,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.8.1 +pyenphase==1.9.1 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c63c5ca7783..7965cea85c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1235,7 +1235,7 @@ pyeconet==0.1.20 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.8.1 +pyenphase==1.9.1 # homeassistant.components.everlights pyeverlights==0.1.0 From 89e280e1367c9225e5808569477d756db1ba8ca4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Sep 2023 15:29:30 +0200 Subject: [PATCH 055/202] Remove unneeded name property from Logi Circle (#99604) --- homeassistant/components/logi_circle/camera.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index 77c0f2f24c8..5c27d2a08ae 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -122,11 +122,6 @@ class LogiCam(Camera): """Return a unique ID.""" return self._id - @property - def name(self): - """Return the name of this camera.""" - return self._name - @property def device_info(self) -> DeviceInfo: """Return information about the device.""" From 0520cadfa6e8665563db4feee760af31591d5049 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Sep 2023 20:10:16 +0200 Subject: [PATCH 056/202] Revert "Deprecate timer start optional duration parameter" (#99613) Revert "Deprecate timer start optional duration parameter (#93471)" This reverts commit 2ce5b08fc36e77a2594a39040e5440d2ca01dff8. --- homeassistant/components/timer/__init__.py | 13 ------------- homeassistant/components/timer/strings.json | 13 ------------- tests/components/timer/test_init.py | 16 ++-------------- 3 files changed, 2 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 1bc8eb8fd5e..228e2071b4a 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -22,7 +22,6 @@ from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store @@ -304,18 +303,6 @@ class Timer(collection.CollectionEntity, RestoreEntity): @callback def async_start(self, duration: timedelta | None = None): """Start a timer.""" - if duration: - async_create_issue( - self.hass, - DOMAIN, - "deprecated_duration_in_start", - breaks_in_ha_version="2024.3.0", - is_fixable=True, - is_persistent=True, - severity=IssueSeverity.WARNING, - translation_key="deprecated_duration_in_start", - ) - if self._listener: self._listener() self._listener = None diff --git a/homeassistant/components/timer/strings.json b/homeassistant/components/timer/strings.json index c85a9f4c55e..56cb46d26b4 100644 --- a/homeassistant/components/timer/strings.json +++ b/homeassistant/components/timer/strings.json @@ -63,18 +63,5 @@ } } } - }, - "issues": { - "deprecated_duration_in_start": { - "title": "The timer start service duration parameter is being removed", - "fix_flow": { - "step": { - "confirm": { - "title": "[%key:component::timer::issues::deprecated_duration_in_start::title%]", - "description": "The timer service `timer.start` optional duration parameter is being removed and use of it has been detected. To change the duration please create a new timer.\n\nPlease remove the use of the `duration` parameter in the `timer.start` service in your automations and scripts and select **submit** to close this issue." - } - } - } - } } } diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 7bc2df87f35..eabc5e04e0b 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -46,11 +46,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, CoreState, HomeAssistant, State from homeassistant.exceptions import HomeAssistantError, Unauthorized -from homeassistant.helpers import ( - config_validation as cv, - entity_registry as er, - issue_registry as ir, -) +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.restore_state import StoredState, async_get from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -270,9 +266,7 @@ async def test_methods_and_events(hass: HomeAssistant) -> None: @pytest.mark.freeze_time("2023-06-05 17:47:50") -async def test_start_service( - hass: HomeAssistant, issue_registry: ir.IssueRegistry -) -> None: +async def test_start_service(hass: HomeAssistant) -> None: """Test the start/stop service.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}}) @@ -317,12 +311,6 @@ async def test_start_service( blocking=True, ) await hass.async_block_till_done() - - # Ensure an issue is raised for the use of this deprecated service - assert issue_registry.async_get_issue( - domain=DOMAIN, issue_id="deprecated_duration_in_start" - ) - state = hass.states.get("timer.test1") assert state assert state.state == STATUS_ACTIVE From 6b988a99d5d395186e2efbbb3f390da57c0fb84c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 4 Sep 2023 20:44:20 +0200 Subject: [PATCH 057/202] Update frontend to 20230904.0 (#99636) --- 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 3b46f568d3e..156adfa73d2 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230901.0"] + "requirements": ["home-assistant-frontend==20230904.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 598c2625f78..87ed1537205 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.70.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 -home-assistant-frontend==20230901.0 +home-assistant-frontend==20230904.0 home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index c180b2db67d..0960f774960 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -994,7 +994,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230901.0 +home-assistant-frontend==20230904.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7965cea85c7..8a28b186f40 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230901.0 +home-assistant-frontend==20230904.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 From 4fcb0a840a1b470bb4627dd86648a1123b166539 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 4 Sep 2023 20:50:17 +0200 Subject: [PATCH 058/202] Bump version to 2023.9.0b4 --- 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 12a12aea631..397b6ea1ab8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index dc9d314fe4d..1e7899afd52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.0b3" +version = "2023.9.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From ba5822bed4775b51d2a84f8187fe2146345f9137 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 3 Sep 2023 10:25:00 +0200 Subject: [PATCH 059/202] Bump gardena_bluetooth to 1.4.0 (#99530) --- homeassistant/components/gardena_bluetooth/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gardena_bluetooth/manifest.json b/homeassistant/components/gardena_bluetooth/manifest.json index 5d1c1888586..3e07eb1ad42 100644 --- a/homeassistant/components/gardena_bluetooth/manifest.json +++ b/homeassistant/components/gardena_bluetooth/manifest.json @@ -13,5 +13,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth", "iot_class": "local_polling", - "requirements": ["gardena_bluetooth==1.3.0"] + "requirements": ["gardena_bluetooth==1.4.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0960f774960..1572d523409 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -835,7 +835,7 @@ fritzconnection[qr]==1.12.2 gTTS==2.2.4 # homeassistant.components.gardena_bluetooth -gardena_bluetooth==1.3.0 +gardena_bluetooth==1.4.0 # homeassistant.components.google_assistant_sdk gassist-text==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a28b186f40..37970b023d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -654,7 +654,7 @@ fritzconnection[qr]==1.12.2 gTTS==2.2.4 # homeassistant.components.gardena_bluetooth -gardena_bluetooth==1.3.0 +gardena_bluetooth==1.4.0 # homeassistant.components.google_assistant_sdk gassist-text==0.0.10 From cab9c9759831f394f66f3223aebc2991c8cb2b28 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Sep 2023 09:13:34 -0500 Subject: [PATCH 060/202] Bump aioesphomeapi to 16.0.4 (#99541) --- 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 7d552f340f0..fd6fde0cb05 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async_interrupt==1.1.1", - "aioesphomeapi==16.0.3", + "aioesphomeapi==16.0.4", "bluetooth-data-tools==1.11.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 1572d523409..45972fad83b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==16.0.3 +aioesphomeapi==16.0.4 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37970b023d3..cae833a3b5b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==16.0.3 +aioesphomeapi==16.0.4 # homeassistant.components.flo aioflo==2021.11.0 From 4c0e4fe74584daafec8af846032a7d7c12cc8b77 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Sep 2023 22:19:40 +0200 Subject: [PATCH 061/202] Small cleanup of TemplateEnvironment (#99571) * Small cleanup of TemplateEnvironment * Fix typo --- homeassistant/helpers/template.py | 57 +++++++++++++++++-------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 40d64ba37ae..b5a6a45e97f 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -492,7 +492,7 @@ class Template: if ret is None: ret = self.hass.data[wanted_env] = TemplateEnvironment( self.hass, - self._limited, # type: ignore[no-untyped-call] + self._limited, self._strict, ) return ret @@ -2276,7 +2276,12 @@ class HassLoader(jinja2.BaseLoader): class TemplateEnvironment(ImmutableSandboxedEnvironment): """The Home Assistant template environment.""" - def __init__(self, hass, limited=False, strict=False): + def __init__( + self, + hass: HomeAssistant | None, + limited: bool | None = False, + strict: bool | None = False, + ) -> None: """Initialise template environment.""" undefined: type[LoggingUndefined] | type[jinja2.StrictUndefined] if not strict: @@ -2381,6 +2386,10 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): # can be discarded, we only need to get at the hass object. def hassfunction( func: Callable[Concatenate[HomeAssistant, _P], _R], + jinja_context: Callable[ + [Callable[Concatenate[Any, _P], _R]], + Callable[Concatenate[Any, _P], _R], + ] = pass_context, ) -> Callable[Concatenate[Any, _P], _R]: """Wrap function that depend on hass.""" @@ -2388,42 +2397,40 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): def wrapper(_: Any, *args: _P.args, **kwargs: _P.kwargs) -> _R: return func(hass, *args, **kwargs) - return pass_context(wrapper) + return jinja_context(wrapper) self.globals["device_entities"] = hassfunction(device_entities) - self.filters["device_entities"] = pass_context(self.globals["device_entities"]) + self.filters["device_entities"] = self.globals["device_entities"] self.globals["device_attr"] = hassfunction(device_attr) - self.filters["device_attr"] = pass_context(self.globals["device_attr"]) + self.filters["device_attr"] = self.globals["device_attr"] self.globals["is_device_attr"] = hassfunction(is_device_attr) - self.tests["is_device_attr"] = pass_eval_context(self.globals["is_device_attr"]) + self.tests["is_device_attr"] = hassfunction(is_device_attr, pass_eval_context) self.globals["config_entry_id"] = hassfunction(config_entry_id) - self.filters["config_entry_id"] = pass_context(self.globals["config_entry_id"]) + self.filters["config_entry_id"] = self.globals["config_entry_id"] self.globals["device_id"] = hassfunction(device_id) - self.filters["device_id"] = pass_context(self.globals["device_id"]) + self.filters["device_id"] = self.globals["device_id"] self.globals["areas"] = hassfunction(areas) - self.filters["areas"] = pass_context(self.globals["areas"]) + self.filters["areas"] = self.globals["areas"] self.globals["area_id"] = hassfunction(area_id) - self.filters["area_id"] = pass_context(self.globals["area_id"]) + self.filters["area_id"] = self.globals["area_id"] self.globals["area_name"] = hassfunction(area_name) - self.filters["area_name"] = pass_context(self.globals["area_name"]) + self.filters["area_name"] = self.globals["area_name"] self.globals["area_entities"] = hassfunction(area_entities) - self.filters["area_entities"] = pass_context(self.globals["area_entities"]) + self.filters["area_entities"] = self.globals["area_entities"] self.globals["area_devices"] = hassfunction(area_devices) - self.filters["area_devices"] = pass_context(self.globals["area_devices"]) + self.filters["area_devices"] = self.globals["area_devices"] self.globals["integration_entities"] = hassfunction(integration_entities) - self.filters["integration_entities"] = pass_context( - self.globals["integration_entities"] - ) + self.filters["integration_entities"] = self.globals["integration_entities"] if limited: # Only device_entities is available to limited templates, mark other @@ -2479,25 +2486,25 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): return self.globals["expand"] = hassfunction(expand) - self.filters["expand"] = pass_context(self.globals["expand"]) + self.filters["expand"] = self.globals["expand"] self.globals["closest"] = hassfunction(closest) - self.filters["closest"] = pass_context(hassfunction(closest_filter)) + self.filters["closest"] = hassfunction(closest_filter) self.globals["distance"] = hassfunction(distance) self.globals["is_hidden_entity"] = hassfunction(is_hidden_entity) - self.tests["is_hidden_entity"] = pass_eval_context( - self.globals["is_hidden_entity"] + self.tests["is_hidden_entity"] = hassfunction( + is_hidden_entity, pass_eval_context ) self.globals["is_state"] = hassfunction(is_state) - self.tests["is_state"] = pass_eval_context(self.globals["is_state"]) + self.tests["is_state"] = hassfunction(is_state, pass_eval_context) self.globals["is_state_attr"] = hassfunction(is_state_attr) - self.tests["is_state_attr"] = pass_eval_context(self.globals["is_state_attr"]) + self.tests["is_state_attr"] = hassfunction(is_state_attr, pass_eval_context) self.globals["state_attr"] = hassfunction(state_attr) self.filters["state_attr"] = self.globals["state_attr"] self.globals["states"] = AllStates(hass) self.filters["states"] = self.globals["states"] self.globals["has_value"] = hassfunction(has_value) - self.filters["has_value"] = pass_context(self.globals["has_value"]) - self.tests["has_value"] = pass_eval_context(self.globals["has_value"]) + self.filters["has_value"] = self.globals["has_value"] + self.tests["has_value"] = hassfunction(has_value, pass_eval_context) self.globals["utcnow"] = hassfunction(utcnow) self.globals["now"] = hassfunction(now) self.globals["relative_time"] = hassfunction(relative_time) @@ -2575,4 +2582,4 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): return cached -_NO_HASS_ENV = TemplateEnvironment(None) # type: ignore[no-untyped-call] +_NO_HASS_ENV = TemplateEnvironment(None) From 2a7c12013fc87d979747f39768f4abd66c70ca2f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 4 Sep 2023 22:46:19 +0200 Subject: [PATCH 062/202] Fix not stripping no device class in template helper binary sensor (#99640) Strip none template helper binary sensor --- homeassistant/components/template/config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index b2ccddedad8..ccc06989c71 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -208,6 +208,7 @@ def validate_user_input( ]: """Do post validation of user input. + For binary sensors: Strip none-sentinels. For sensors: Strip none-sentinels and validate unit of measurement. For all domaines: Set template type. """ @@ -217,8 +218,9 @@ def validate_user_input( user_input: dict[str, Any], ) -> dict[str, Any]: """Add template type to user input.""" - if template_type == Platform.SENSOR: + if template_type in (Platform.BINARY_SENSOR, Platform.SENSOR): _strip_sentinel(user_input) + if template_type == Platform.SENSOR: _validate_unit(user_input) _validate_state_class(user_input) return {"template_type": template_type} | user_input From f0cf539e15c517755f1047afac7b9cd7b42b8474 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 4 Sep 2023 22:31:53 +0200 Subject: [PATCH 063/202] Fix missing unique id in SQL (#99641) --- homeassistant/components/sql/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index f4f44d4f9a4..3fdc6b2c079 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -123,7 +123,7 @@ async def async_setup_entry( value_template.hass = hass name_template = Template(name, hass) - trigger_entity_config = {CONF_NAME: name_template} + trigger_entity_config = {CONF_NAME: name_template, CONF_UNIQUE_ID: entry.entry_id} for key in TRIGGER_ENTITY_OPTIONS: if key not in entry.options: continue From c040672cabb580763990ca383618e7f6291a918f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 5 Sep 2023 19:42:19 +0900 Subject: [PATCH 064/202] Update aioairzone to v0.6.8 (#99644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update aioairzone to v0.6.8 Signed-off-by: Álvaro Fernández Rojas * Trigger CI --------- Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index bb1e448c8eb..c0b24b2cc3e 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone", "iot_class": "local_polling", "loggers": ["aioairzone"], - "requirements": ["aioairzone==0.6.7"] + "requirements": ["aioairzone==0.6.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 45972fad83b..ca0f713e423 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -188,7 +188,7 @@ aioairq==0.2.4 aioairzone-cloud==0.2.1 # homeassistant.components.airzone -aioairzone==0.6.7 +aioairzone==0.6.8 # homeassistant.components.ambient_station aioambient==2023.04.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cae833a3b5b..b1c8207303f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -169,7 +169,7 @@ aioairq==0.2.4 aioairzone-cloud==0.2.1 # homeassistant.components.airzone -aioairzone==0.6.7 +aioairzone==0.6.8 # homeassistant.components.ambient_station aioambient==2023.04.0 From 9e03f8a8d6cf56e93e5145c41ef5588487182846 Mon Sep 17 00:00:00 2001 From: itpeters <59966384+itpeters@users.noreply.github.com> Date: Tue, 5 Sep 2023 05:10:14 -0600 Subject: [PATCH 065/202] Fix long press event for matter generic switch (#99645) --- homeassistant/components/matter/event.py | 2 +- tests/components/matter/test_event.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/event.py b/homeassistant/components/matter/event.py index 3a1faa6dcbe..84049301296 100644 --- a/homeassistant/components/matter/event.py +++ b/homeassistant/components/matter/event.py @@ -65,7 +65,7 @@ class MatterEventEntity(MatterEntity, EventEntity): if feature_map & SwitchFeature.kMomentarySwitchRelease: event_types.append("short_release") if feature_map & SwitchFeature.kMomentarySwitchLongPress: - event_types.append("long_press_ongoing") + event_types.append("long_press") event_types.append("long_release") if feature_map & SwitchFeature.kMomentarySwitchMultiPress: event_types.append("multi_press_ongoing") diff --git a/tests/components/matter/test_event.py b/tests/components/matter/test_event.py index 0d5891a7778..0aa9385a74c 100644 --- a/tests/components/matter/test_event.py +++ b/tests/components/matter/test_event.py @@ -48,7 +48,7 @@ async def test_generic_switch_node( assert state.attributes[ATTR_EVENT_TYPES] == [ "initial_press", "short_release", - "long_press_ongoing", + "long_press", "long_release", "multi_press_ongoing", "multi_press_complete", @@ -111,7 +111,7 @@ async def test_generic_switch_multi_node( assert state_button_1.attributes[ATTR_EVENT_TYPES] == [ "initial_press", "short_release", - "long_press_ongoing", + "long_press", "long_release", ] # check button 2 From 5d1fe0eb00bfd3bae2d1b7c3a3efaa699f0f5f87 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Sep 2023 19:56:34 -0500 Subject: [PATCH 066/202] Fix mobile app dispatcher performance (#99647) Fix mobile app thundering heard The mobile_app would setup a dispatcher to listener for updates on every entity and reject the ones that were not for the unique id that it was intrested in. Instead we now register for a signal per unique id since we were previously generating O(entities*sensors*devices) callbacks which was causing the event loop to stall when there were a large number of mobile app users. --- homeassistant/components/mobile_app/entity.py | 13 +++++++------ homeassistant/components/mobile_app/webhook.py | 5 ++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 3a2f038a0af..120014d1d52 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -1,6 +1,8 @@ """A entity class for mobile_app.""" from __future__ import annotations +from typing import Any + from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ICON, CONF_NAME, CONF_UNIQUE_ID, STATE_UNAVAILABLE from homeassistant.core import callback @@ -36,7 +38,9 @@ class MobileAppEntity(RestoreEntity): """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( - self.hass, SIGNAL_SENSOR_UPDATE, self._handle_update + self.hass, + f"{SIGNAL_SENSOR_UPDATE}-{self._attr_unique_id}", + self._handle_update, ) ) @@ -96,10 +100,7 @@ class MobileAppEntity(RestoreEntity): return self._config.get(ATTR_SENSOR_STATE) != STATE_UNAVAILABLE @callback - def _handle_update(self, incoming_id, data): + def _handle_update(self, data: dict[str, Any]) -> None: """Handle async event updates.""" - if incoming_id != self._attr_unique_id: - return - - self._config = {**self._config, **data} + self._config.update(data) self.async_write_ha_state() diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 62417b0873a..1a56b13ddc5 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -607,7 +607,7 @@ async def webhook_register_sensor( if changes: entity_registry.async_update_entity(existing_sensor, **changes) - async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, unique_store_key, data) + async_dispatcher_send(hass, f"{SIGNAL_SENSOR_UPDATE}-{unique_store_key}", data) else: data[CONF_UNIQUE_ID] = unique_store_key data[ @@ -693,8 +693,7 @@ async def webhook_update_sensor_states( sensor[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID] async_dispatcher_send( hass, - SIGNAL_SENSOR_UPDATE, - unique_store_key, + f"{SIGNAL_SENSOR_UPDATE}-{unique_store_key}", sensor, ) From bd0fd9db77b8a798cdf2edb1cb33200c81355b8f Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 5 Sep 2023 09:58:32 -0400 Subject: [PATCH 067/202] Bump zwave-js-server-python to 0.51.1 (#99652) * Bump zwave-js-server-python to 0.51.1 * Update test --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_api.py | 5 ++--- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 73fa41a8cca..080074451bd 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["zwave_js_server"], "quality_scale": "platinum", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.51.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.51.1"], "usb": [ { "vid": "0658", diff --git a/requirements_all.txt b/requirements_all.txt index ca0f713e423..1cf89dd95fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2799,7 +2799,7 @@ zigpy==0.57.1 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.51.0 +zwave-js-server-python==0.51.1 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1c8207303f..774d70d08e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2060,7 +2060,7 @@ zigpy-znp==0.11.4 zigpy==0.57.1 # homeassistant.components.zwave_js -zwave-js-server-python==0.51.0 +zwave-js-server-python==0.51.1 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index e686def8883..02ed507cabe 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -3679,7 +3679,6 @@ async def test_abort_firmware_update( ws_client = await hass_ws_client(hass) device = get_device(hass, multisensor_6) - client.async_send_command.return_value = {} await ws_client.send_json( { ID: 1, @@ -3690,8 +3689,8 @@ async def test_abort_firmware_update( msg = await ws_client.receive_json() assert msg["success"] - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.abort_firmware_update" assert args["nodeId"] == multisensor_6.node_id From fed84ebc4010a2de5b0164c30ec84ed96f2cba8c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 5 Sep 2023 20:12:40 +0200 Subject: [PATCH 068/202] Update frontend to 20230905.0 (#99677) --- 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 156adfa73d2..627b36a59b8 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230904.0"] + "requirements": ["home-assistant-frontend==20230905.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 87ed1537205..5f939ea9763 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.70.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 -home-assistant-frontend==20230904.0 +home-assistant-frontend==20230905.0 home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1cf89dd95fe..11f404b6394 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -994,7 +994,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230904.0 +home-assistant-frontend==20230905.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 774d70d08e4..581e8f3d325 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230904.0 +home-assistant-frontend==20230905.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 From 6b8027019bb0f57b2c63a390fc46d2df723734e7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 5 Sep 2023 20:25:02 +0200 Subject: [PATCH 069/202] Bump version to 2023.9.0b5 --- 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 397b6ea1ab8..a851c041398 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 1e7899afd52..d4a71cd6abe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.0b4" +version = "2023.9.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 9d87e8d02b68b96f16d56a9b5673ef9790147cec Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Sep 2023 10:03:35 +0200 Subject: [PATCH 070/202] Allow specifying a custom log function for template render (#99572) * Allow specifying a custom log function for template render * Bypass template cache when reporting errors + fix tests * Send errors as events * Fix logic for creating new TemplateEnvironment * Add strict mode back * Only send error events if report_errors is True * Force test of websocket_api only * Debug test * Run pytest with higher verbosity * Timeout after 1 minute, enable syslog output * Adjust timeout * Add debug logs * Fix unsafe call to WebSocketHandler._send_message * Remove debug code * Improve test coverage * Revert accidental change * Include severity in error events * Remove redundant information from error events --- .../components/websocket_api/commands.py | 32 +- homeassistant/helpers/event.py | 17 +- homeassistant/helpers/template.py | 113 ++++--- .../components/websocket_api/test_commands.py | 278 ++++++++++++++++-- tests/helpers/test_template.py | 18 +- 5 files changed, 374 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 84c7567a40e..7772bef66f9 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -5,6 +5,7 @@ from collections.abc import Callable import datetime as dt from functools import lru_cache, partial import json +import logging from typing import Any, cast import voluptuous as vol @@ -505,6 +506,7 @@ def _cached_template(template_str: str, hass: HomeAssistant) -> template.Templat vol.Optional("variables"): dict, vol.Optional("timeout"): vol.Coerce(float), vol.Optional("strict", default=False): bool, + vol.Optional("report_errors", default=False): bool, } ) @decorators.async_response @@ -513,14 +515,32 @@ async def handle_render_template( ) -> None: """Handle render_template command.""" template_str = msg["template"] - template_obj = _cached_template(template_str, hass) + report_errors: bool = msg["report_errors"] + if report_errors: + template_obj = template.Template(template_str, hass) + else: + template_obj = _cached_template(template_str, hass) variables = msg.get("variables") timeout = msg.get("timeout") + @callback + def _error_listener(level: int, template_error: str) -> None: + connection.send_message( + messages.event_message( + msg["id"], + {"error": template_error, "level": logging.getLevelName(level)}, + ) + ) + + @callback + def _thread_safe_error_listener(level: int, template_error: str) -> None: + hass.loop.call_soon_threadsafe(_error_listener, level, template_error) + if timeout: try: + log_fn = _thread_safe_error_listener if report_errors else None timed_out = await template_obj.async_render_will_timeout( - timeout, variables, strict=msg["strict"] + timeout, variables, strict=msg["strict"], log_fn=log_fn ) except TemplateError as ex: connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex)) @@ -542,7 +562,11 @@ async def handle_render_template( track_template_result = updates.pop() result = track_template_result.result if isinstance(result, TemplateError): - connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(result)) + if not report_errors: + return + connection.send_message( + messages.event_message(msg["id"], {"error": str(result)}) + ) return connection.send_message( @@ -552,12 +576,14 @@ async def handle_render_template( ) try: + log_fn = _error_listener if report_errors else None info = async_track_template_result( hass, [TrackTemplate(template_obj, variables)], _template_listener, raise_on_template_error=True, strict=msg["strict"], + log_fn=log_fn, ) except TemplateError as ex: connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex)) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 62a3b91991d..173dd057f96 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -915,7 +915,12 @@ class TrackTemplateResultInfo: """Return the representation.""" return f"" - def async_setup(self, raise_on_template_error: bool, strict: bool = False) -> None: + def async_setup( + self, + raise_on_template_error: bool, + strict: bool = False, + log_fn: Callable[[int, str], None] | None = None, + ) -> None: """Activation of template tracking.""" block_render = False super_template = self._track_templates[0] if self._has_super_template else None @@ -925,7 +930,7 @@ class TrackTemplateResultInfo: template = super_template.template variables = super_template.variables self._info[template] = info = template.async_render_to_info( - variables, strict=strict + variables, strict=strict, log_fn=log_fn ) # If the super template did not render to True, don't update other templates @@ -946,7 +951,7 @@ class TrackTemplateResultInfo: template = track_template_.template variables = track_template_.variables self._info[template] = info = template.async_render_to_info( - variables, strict=strict + variables, strict=strict, log_fn=log_fn ) if info.exception: @@ -1233,6 +1238,7 @@ def async_track_template_result( action: TrackTemplateResultListener, raise_on_template_error: bool = False, strict: bool = False, + log_fn: Callable[[int, str], None] | None = None, has_super_template: bool = False, ) -> TrackTemplateResultInfo: """Add a listener that fires when the result of a template changes. @@ -1264,6 +1270,9 @@ def async_track_template_result( tracking. strict When set to True, raise on undefined variables. + log_fn + If not None, template error messages will logging by calling log_fn + instead of the normal logging facility. has_super_template When set to True, the first template will block rendering of other templates if it doesn't render as True. @@ -1274,7 +1283,7 @@ def async_track_template_result( """ tracker = TrackTemplateResultInfo(hass, track_templates, action, has_super_template) - tracker.async_setup(raise_on_template_error, strict=strict) + tracker.async_setup(raise_on_template_error, strict=strict, log_fn=log_fn) return tracker diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index b5a6a45e97f..9f280db6c98 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -458,6 +458,7 @@ class Template: "_exc_info", "_limited", "_strict", + "_log_fn", "_hash_cache", "_renders", ) @@ -475,6 +476,7 @@ class Template: self._exc_info: sys._OptExcInfo | None = None self._limited: bool | None = None self._strict: bool | None = None + self._log_fn: Callable[[int, str], None] | None = None self._hash_cache: int = hash(self.template) self._renders: int = 0 @@ -482,6 +484,11 @@ class Template: def _env(self) -> TemplateEnvironment: if self.hass is None: return _NO_HASS_ENV + # Bypass cache if a custom log function is specified + if self._log_fn is not None: + return TemplateEnvironment( + self.hass, self._limited, self._strict, self._log_fn + ) if self._limited: wanted_env = _ENVIRONMENT_LIMITED elif self._strict: @@ -491,9 +498,7 @@ class Template: ret: TemplateEnvironment | None = self.hass.data.get(wanted_env) if ret is None: ret = self.hass.data[wanted_env] = TemplateEnvironment( - self.hass, - self._limited, - self._strict, + self.hass, self._limited, self._strict, self._log_fn ) return ret @@ -537,6 +542,7 @@ class Template: parse_result: bool = True, limited: bool = False, strict: bool = False, + log_fn: Callable[[int, str], None] | None = None, **kwargs: Any, ) -> Any: """Render given template. @@ -553,7 +559,7 @@ class Template: return self.template return self._parse_result(self.template) - compiled = self._compiled or self._ensure_compiled(limited, strict) + compiled = self._compiled or self._ensure_compiled(limited, strict, log_fn) if variables is not None: kwargs.update(variables) @@ -608,6 +614,7 @@ class Template: timeout: float, variables: TemplateVarsType = None, strict: bool = False, + log_fn: Callable[[int, str], None] | None = None, **kwargs: Any, ) -> bool: """Check to see if rendering a template will timeout during render. @@ -628,7 +635,7 @@ class Template: if self.is_static: return False - compiled = self._compiled or self._ensure_compiled(strict=strict) + compiled = self._compiled or self._ensure_compiled(strict=strict, log_fn=log_fn) if variables is not None: kwargs.update(variables) @@ -664,7 +671,11 @@ class Template: @callback def async_render_to_info( - self, variables: TemplateVarsType = None, strict: bool = False, **kwargs: Any + self, + variables: TemplateVarsType = None, + strict: bool = False, + log_fn: Callable[[int, str], None] | None = None, + **kwargs: Any, ) -> RenderInfo: """Render the template and collect an entity filter.""" self._renders += 1 @@ -680,7 +691,9 @@ class Template: token = _render_info.set(render_info) try: - render_info._result = self.async_render(variables, strict=strict, **kwargs) + render_info._result = self.async_render( + variables, strict=strict, log_fn=log_fn, **kwargs + ) except TemplateError as ex: render_info.exception = ex finally: @@ -743,7 +756,10 @@ class Template: return value if error_value is _SENTINEL else error_value def _ensure_compiled( - self, limited: bool = False, strict: bool = False + self, + limited: bool = False, + strict: bool = False, + log_fn: Callable[[int, str], None] | None = None, ) -> jinja2.Template: """Bind a template to a specific hass instance.""" self.ensure_valid() @@ -756,10 +772,14 @@ class Template: self._strict is None or self._strict == strict ), "can't change between strict and non strict template" assert not (strict and limited), "can't combine strict and limited template" + assert ( + self._log_fn is None or self._log_fn == log_fn + ), "can't change custom log function" assert self._compiled_code is not None, "template code was not compiled" self._limited = limited self._strict = strict + self._log_fn = log_fn env = self._env self._compiled = jinja2.Template.from_code( @@ -2178,45 +2198,56 @@ def _render_with_context( return template.render(**kwargs) -class LoggingUndefined(jinja2.Undefined): +def make_logging_undefined( + strict: bool | None, log_fn: Callable[[int, str], None] | None +) -> type[jinja2.Undefined]: """Log on undefined variables.""" - def _log_message(self) -> None: + if strict: + return jinja2.StrictUndefined + + def _log_with_logger(level: int, msg: str) -> None: template, action = template_cv.get() or ("", "rendering or compiling") - _LOGGER.warning( - "Template variable warning: %s when %s '%s'", - self._undefined_message, + _LOGGER.log( + level, + "Template variable %s: %s when %s '%s'", + logging.getLevelName(level).lower(), + msg, action, template, ) - def _fail_with_undefined_error(self, *args, **kwargs): - try: - return super()._fail_with_undefined_error(*args, **kwargs) - except self._undefined_exception as ex: - template, action = template_cv.get() or ("", "rendering or compiling") - _LOGGER.error( - "Template variable error: %s when %s '%s'", - self._undefined_message, - action, - template, - ) - raise ex + _log_fn = log_fn or _log_with_logger - def __str__(self) -> str: - """Log undefined __str___.""" - self._log_message() - return super().__str__() + class LoggingUndefined(jinja2.Undefined): + """Log on undefined variables.""" - def __iter__(self): - """Log undefined __iter___.""" - self._log_message() - return super().__iter__() + def _log_message(self) -> None: + _log_fn(logging.WARNING, self._undefined_message) - def __bool__(self) -> bool: - """Log undefined __bool___.""" - self._log_message() - return super().__bool__() + def _fail_with_undefined_error(self, *args, **kwargs): + try: + return super()._fail_with_undefined_error(*args, **kwargs) + except self._undefined_exception as ex: + _log_fn(logging.ERROR, self._undefined_message) + raise ex + + def __str__(self) -> str: + """Log undefined __str___.""" + self._log_message() + return super().__str__() + + def __iter__(self): + """Log undefined __iter___.""" + self._log_message() + return super().__iter__() + + def __bool__(self) -> bool: + """Log undefined __bool___.""" + self._log_message() + return super().__bool__() + + return LoggingUndefined async def async_load_custom_templates(hass: HomeAssistant) -> None: @@ -2281,14 +2312,10 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): hass: HomeAssistant | None, limited: bool | None = False, strict: bool | None = False, + log_fn: Callable[[int, str], None] | None = None, ) -> None: """Initialise template environment.""" - undefined: type[LoggingUndefined] | type[jinja2.StrictUndefined] - if not strict: - undefined = LoggingUndefined - else: - undefined = jinja2.StrictUndefined - super().__init__(undefined=undefined) + super().__init__(undefined=make_logging_undefined(strict, log_fn)) self.hass = hass self.template_cache: weakref.WeakValueDictionary[ str | jinja2.nodes.Template, CodeType | str | None diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 73baa968ab6..96e79a81716 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -2,6 +2,7 @@ import asyncio from copy import deepcopy import datetime +import logging from unittest.mock import ANY, AsyncMock, Mock, patch import pytest @@ -33,7 +34,11 @@ from tests.common import ( async_mock_service, mock_platform, ) -from tests.typing import ClientSessionGenerator, WebSocketGenerator +from tests.typing import ( + ClientSessionGenerator, + MockHAClientWebSocket, + WebSocketGenerator, +) STATE_KEY_SHORT_NAMES = { "entity_id": "e", @@ -1225,46 +1230,187 @@ async def test_render_template_manual_entity_ids_no_longer_needed( } +EMPTY_LISTENERS = {"all": False, "entities": [], "domains": [], "time": False} + +ERR_MSG = {"type": "result", "success": False} + +VARIABLE_ERROR_UNDEFINED_FUNC = { + "error": "'my_unknown_func' is undefined", + "level": "ERROR", +} +TEMPLATE_ERROR_UNDEFINED_FUNC = { + "code": "template_error", + "message": "UndefinedError: 'my_unknown_func' is undefined", +} + +VARIABLE_WARNING_UNDEFINED_VAR = { + "error": "'my_unknown_var' is undefined", + "level": "WARNING", +} +TEMPLATE_ERROR_UNDEFINED_VAR = { + "code": "template_error", + "message": "UndefinedError: 'my_unknown_var' is undefined", +} + +TEMPLATE_ERROR_UNDEFINED_FILTER = { + "code": "template_error", + "message": "TemplateAssertionError: No filter named 'unknown_filter'.", +} + + @pytest.mark.parametrize( - "template", + ("template", "expected_events"), [ - "{{ my_unknown_func() + 1 }}", - "{{ my_unknown_var }}", - "{{ my_unknown_var + 1 }}", - "{{ now() | unknown_filter }}", + ( + "{{ my_unknown_func() + 1 }}", + [ + {"type": "event", "event": VARIABLE_ERROR_UNDEFINED_FUNC}, + ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC}, + ], + ), + ( + "{{ my_unknown_var }}", + [ + {"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR}, + {"type": "result", "success": True, "result": None}, + {"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR}, + { + "type": "event", + "event": {"result": "", "listeners": EMPTY_LISTENERS}, + }, + ], + ), + ( + "{{ my_unknown_var + 1 }}", + [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}], + ), + ( + "{{ now() | unknown_filter }}", + [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}], + ), ], ) async def test_render_template_with_error( - hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture, template + hass: HomeAssistant, + websocket_client: MockHAClientWebSocket, + caplog: pytest.LogCaptureFixture, + template: str, + expected_events: list[dict[str, str]], ) -> None: """Test a template with an error.""" + caplog.set_level(logging.INFO) await websocket_client.send_json( - {"id": 5, "type": "render_template", "template": template, "strict": True} + { + "id": 5, + "type": "render_template", + "template": template, + "report_errors": True, + } ) - msg = await websocket_client.receive_json() - assert msg["id"] == 5 - assert msg["type"] == const.TYPE_RESULT - assert not msg["success"] - assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR + for expected_event in expected_events: + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + for key, value in expected_event.items(): + assert msg[key] == value assert "Template variable error" not in caplog.text + assert "Template variable warning" not in caplog.text assert "TemplateError" not in caplog.text @pytest.mark.parametrize( - "template", + ("template", "expected_events"), [ - "{{ my_unknown_func() + 1 }}", - "{{ my_unknown_var }}", - "{{ my_unknown_var + 1 }}", - "{{ now() | unknown_filter }}", + ( + "{{ my_unknown_func() + 1 }}", + [ + {"type": "event", "event": VARIABLE_ERROR_UNDEFINED_FUNC}, + ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC}, + ], + ), + ( + "{{ my_unknown_var }}", + [ + {"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR}, + {"type": "result", "success": True, "result": None}, + {"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR}, + { + "type": "event", + "event": {"result": "", "listeners": EMPTY_LISTENERS}, + }, + ], + ), + ( + "{{ my_unknown_var + 1 }}", + [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}], + ), + ( + "{{ now() | unknown_filter }}", + [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}], + ), ], ) async def test_render_template_with_timeout_and_error( - hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture, template + hass: HomeAssistant, + websocket_client, + caplog: pytest.LogCaptureFixture, + template: str, + expected_events: list[dict[str, str]], ) -> None: """Test a template with an error with a timeout.""" + caplog.set_level(logging.INFO) + await websocket_client.send_json( + { + "id": 5, + "type": "render_template", + "template": template, + "timeout": 5, + "report_errors": True, + } + ) + + for expected_event in expected_events: + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + for key, value in expected_event.items(): + assert msg[key] == value + + assert "Template variable error" not in caplog.text + assert "Template variable warning" not in caplog.text + assert "TemplateError" not in caplog.text + + +@pytest.mark.parametrize( + ("template", "expected_events"), + [ + ( + "{{ my_unknown_func() + 1 }}", + [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC}], + ), + ( + "{{ my_unknown_var }}", + [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}], + ), + ( + "{{ my_unknown_var + 1 }}", + [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}], + ), + ( + "{{ now() | unknown_filter }}", + [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}], + ), + ], +) +async def test_render_template_strict_with_timeout_and_error( + hass: HomeAssistant, + websocket_client, + caplog: pytest.LogCaptureFixture, + template: str, + expected_events: list[dict[str, str]], +) -> None: + """Test a template with an error with a timeout.""" + caplog.set_level(logging.INFO) await websocket_client.send_json( { "id": 5, @@ -1275,13 +1421,14 @@ async def test_render_template_with_timeout_and_error( } ) - msg = await websocket_client.receive_json() - assert msg["id"] == 5 - assert msg["type"] == const.TYPE_RESULT - assert not msg["success"] - assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR + for expected_event in expected_events: + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + for key, value in expected_event.items(): + assert msg[key] == value assert "Template variable error" not in caplog.text + assert "Template variable warning" not in caplog.text assert "TemplateError" not in caplog.text @@ -1299,13 +1446,19 @@ async def test_render_template_error_in_template_code( assert not msg["success"] assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR + assert "Template variable error" not in caplog.text + assert "Template variable warning" not in caplog.text assert "TemplateError" not in caplog.text async def test_render_template_with_delayed_error( hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture ) -> None: - """Test a template with an error that only happens after a state change.""" + """Test a template with an error that only happens after a state change. + + In this test report_errors is enabled. + """ + caplog.set_level(logging.INFO) hass.states.async_set("sensor.test", "on") await hass.async_block_till_done() @@ -1318,12 +1471,16 @@ async def test_render_template_with_delayed_error( """ await websocket_client.send_json( - {"id": 5, "type": "render_template", "template": template_str} + { + "id": 5, + "type": "render_template", + "template": template_str, + "report_errors": True, + } ) await hass.async_block_till_done() msg = await websocket_client.receive_json() - assert msg["id"] == 5 assert msg["type"] == const.TYPE_RESULT assert msg["success"] @@ -1347,13 +1504,74 @@ async def test_render_template_with_delayed_error( msg = await websocket_client.receive_json() assert msg["id"] == 5 - assert msg["type"] == const.TYPE_RESULT - assert not msg["success"] - assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR + assert msg["type"] == "event" + event = msg["event"] + assert event["error"] == "'None' has no attribute 'state'" + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + event = msg["event"] + assert event == {"error": "UndefinedError: 'explode' is undefined"} + + assert "Template variable error" not in caplog.text + assert "Template variable warning" not in caplog.text assert "TemplateError" not in caplog.text +async def test_render_template_with_delayed_error_2( + hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture +) -> None: + """Test a template with an error that only happens after a state change. + + In this test report_errors is disabled. + """ + hass.states.async_set("sensor.test", "on") + await hass.async_block_till_done() + + template_str = """ +{% if states.sensor.test.state %} + on +{% else %} + {{ explode + 1 }} +{% endif %} + """ + + await websocket_client.send_json( + { + "id": 5, + "type": "render_template", + "template": template_str, + "report_errors": False, + } + ) + await hass.async_block_till_done() + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + hass.states.async_remove("sensor.test") + await hass.async_block_till_done() + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + event = msg["event"] + assert event == { + "result": "on", + "listeners": { + "all": False, + "domains": [], + "entities": ["sensor.test"], + "time": False, + }, + } + + assert "Template variable warning" in caplog.text + + async def test_render_template_with_timeout( hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture ) -> None: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index d14496d321e..58e0c730165 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -4466,15 +4466,25 @@ async def test_parse_result(hass: HomeAssistant) -> None: assert template.Template(tpl, hass).async_render() == result -async def test_undefined_variable( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture +@pytest.mark.parametrize( + "template_string", + [ + "{{ no_such_variable }}", + "{{ no_such_variable and True }}", + "{{ no_such_variable | join(', ') }}", + ], +) +async def test_undefined_symbol_warnings( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + template_string: str, ) -> None: """Test a warning is logged on undefined variables.""" - tpl = template.Template("{{ no_such_variable }}", hass) + tpl = template.Template(template_string, hass) assert tpl.async_render() == "" assert ( "Template variable warning: 'no_such_variable' is undefined when rendering " - "'{{ no_such_variable }}'" in caplog.text + f"'{template_string}'" in caplog.text ) From 0cbcacbbf5439f69a37574ca7ae5ab58fb867e1d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Sep 2023 09:49:42 +0200 Subject: [PATCH 071/202] Include template listener info in template preview (#99669) --- .../components/template/config_flow.py | 3 +- .../components/template/template_entity.py | 34 ++++++++++++++----- .../helpers/trigger_template_entity.py | 6 ++-- tests/components/template/test_config_flow.py | 27 +++++++++++++++ 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index ccc06989c71..093cbf14098 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -349,6 +349,7 @@ def ws_start_preview( def async_preview_updated( state: str | None, attributes: Mapping[str, Any] | None, + listeners: dict[str, bool | set[str]] | None, error: str | None, ) -> None: """Forward config entry state events to websocket.""" @@ -363,7 +364,7 @@ def ws_start_preview( connection.send_message( websocket_api.event_message( msg["id"], - {"attributes": attributes, "state": state}, + {"attributes": attributes, "listeners": listeners, "state": state}, ) ) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index c33674fa86f..2ce42083117 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -34,6 +34,7 @@ from homeassistant.helpers.event import ( EventStateChangedData, TrackTemplate, TrackTemplateResult, + TrackTemplateResultInfo, async_track_template_result, ) from homeassistant.helpers.script import Script, _VarsType @@ -260,12 +261,18 @@ class TemplateEntity(Entity): ) -> None: """Template Entity.""" self._template_attrs: dict[Template, list[_TemplateAttribute]] = {} - self._async_update: Callable[[], None] | None = None + self._template_result_info: TrackTemplateResultInfo | None = None self._attr_extra_state_attributes = {} self._self_ref_update_count = 0 self._attr_unique_id = unique_id self._preview_callback: Callable[ - [str | None, dict[str, Any] | None, str | None], None + [ + str | None, + dict[str, Any] | None, + dict[str, bool | set[str]] | None, + str | None, + ], + None, ] | None = None if config is None: self._attribute_templates = attribute_templates @@ -427,9 +434,12 @@ class TemplateEntity(Entity): state, attrs = self._async_generate_attributes() validate_state(state) except Exception as err: # pylint: disable=broad-exception-caught - self._preview_callback(None, None, str(err)) + self._preview_callback(None, None, None, str(err)) else: - self._preview_callback(state, attrs, None) + assert self._template_result_info + self._preview_callback( + state, attrs, self._template_result_info.listeners, None + ) @callback def _async_template_startup(self, *_: Any) -> None: @@ -460,7 +470,7 @@ class TemplateEntity(Entity): has_super_template=has_availability_template, ) self.async_on_remove(result_info.async_remove) - self._async_update = result_info.async_refresh + self._template_result_info = result_info result_info.async_refresh() @callback @@ -494,7 +504,13 @@ class TemplateEntity(Entity): def async_start_preview( self, preview_callback: Callable[ - [str | None, Mapping[str, Any] | None, str | None], None + [ + str | None, + Mapping[str, Any] | None, + dict[str, bool | set[str]] | None, + str | None, + ], + None, ], ) -> CALLBACK_TYPE: """Render a preview.""" @@ -504,7 +520,7 @@ class TemplateEntity(Entity): try: self._async_template_startup() except Exception as err: # pylint: disable=broad-exception-caught - preview_callback(None, None, str(err)) + preview_callback(None, None, None, str(err)) return self._call_on_remove_callbacks async def async_added_to_hass(self) -> None: @@ -521,8 +537,8 @@ class TemplateEntity(Entity): async def async_update(self) -> None: """Call for forced update.""" - assert self._async_update - self._async_update() + assert self._template_result_info + self._template_result_info.async_refresh() async def async_run_script( self, diff --git a/homeassistant/helpers/trigger_template_entity.py b/homeassistant/helpers/trigger_template_entity.py index 8fc99f5cb52..0ee653b42bd 100644 --- a/homeassistant/helpers/trigger_template_entity.py +++ b/homeassistant/helpers/trigger_template_entity.py @@ -77,8 +77,8 @@ class TriggerBaseEntity(Entity): """Template Base entity based on trigger data.""" domain: str - extra_template_keys: tuple | None = None - extra_template_keys_complex: tuple | None = None + extra_template_keys: tuple[str, ...] | None = None + extra_template_keys_complex: tuple[str, ...] | None = None _unique_id: str | None def __init__( @@ -94,7 +94,7 @@ class TriggerBaseEntity(Entity): self._config = config self._static_rendered = {} - self._to_render_simple = [] + self._to_render_simple: list[str] = [] self._to_render_complex: list[str] = [] for itm in ( diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index ba939f3b8d1..b8634b68b1c 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -3,6 +3,7 @@ from typing import Any from unittest.mock import patch import pytest +from pytest_unordered import unordered from homeassistant import config_entries from homeassistant.components.template import DOMAIN, async_setup_entry @@ -257,6 +258,7 @@ async def test_options( "input_states", "template_states", "extra_attributes", + "listeners", ), ( ( @@ -266,6 +268,7 @@ async def test_options( {"one": "on", "two": "off"}, ["off", "on"], [{}, {}], + [["one", "two"], ["one"]], ), ( "sensor", @@ -274,6 +277,7 @@ async def test_options( {"one": "30.0", "two": "20.0"}, ["unavailable", "50.0"], [{}, {}], + [["one"], ["one", "two"]], ), ), ) @@ -286,6 +290,7 @@ async def test_config_flow_preview( input_states: list[str], template_states: str, extra_attributes: list[dict[str, Any]], + listeners: list[list[str]], ) -> None: """Test the config flow preview.""" client = await hass_ws_client(hass) @@ -323,6 +328,12 @@ async def test_config_flow_preview( msg = await client.receive_json() assert msg["event"] == { "attributes": {"friendly_name": "My template"} | extra_attributes[0], + "listeners": { + "all": False, + "domains": [], + "entities": unordered([f"{template_type}.{_id}" for _id in listeners[0]]), + "time": False, + }, "state": template_states[0], } @@ -336,6 +347,12 @@ async def test_config_flow_preview( "attributes": {"friendly_name": "My template"} | extra_attributes[0] | extra_attributes[1], + "listeners": { + "all": False, + "domains": [], + "entities": unordered([f"{template_type}.{_id}" for _id in listeners[1]]), + "time": False, + }, "state": template_states[1], } assert len(hass.states.async_all()) == 2 @@ -526,6 +543,7 @@ async def test_config_flow_preview_bad_state( "input_states", "template_state", "extra_attributes", + "listeners", ), [ ( @@ -537,6 +555,7 @@ async def test_config_flow_preview_bad_state( {"one": "on", "two": "off"}, "off", {}, + ["one", "two"], ), ( "sensor", @@ -547,6 +566,7 @@ async def test_config_flow_preview_bad_state( {"one": "30.0", "two": "20.0"}, "10.0", {}, + ["one", "two"], ), ], ) @@ -561,6 +581,7 @@ async def test_option_flow_preview( input_states: list[str], template_state: str, extra_attributes: dict[str, Any], + listeners: list[str], ) -> None: """Test the option flow preview.""" client = await hass_ws_client(hass) @@ -608,6 +629,12 @@ async def test_option_flow_preview( msg = await client.receive_json() assert msg["event"] == { "attributes": {"friendly_name": "My template"} | extra_attributes, + "listeners": { + "all": False, + "domains": [], + "entities": unordered([f"{template_type}.{_id}" for _id in listeners]), + "time": False, + }, "state": template_state, } assert len(hass.states.async_all()) == 3 From aa32b658b2d39c42f6bf5899cf655ff42b02af8f Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:30:28 -0400 Subject: [PATCH 072/202] Fix ZHA startup creating entities with non-unique IDs (#99679) * Make the ZHAGateway initialization restartable so entities are unique * Add a unit test --- homeassistant/components/zha/__init__.py | 4 +- homeassistant/components/zha/core/gateway.py | 12 ++--- tests/components/zha/test_init.py | 50 +++++++++++++++++++- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 1c4c3e776d0..f9113ebaa90 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -134,7 +134,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b else: _LOGGER.debug("ZHA storage file does not exist or was already removed") - zha_gateway = ZHAGateway(hass, config, config_entry) + # Re-use the gateway object between ZHA reloads + if (zha_gateway := zha_data.get(DATA_ZHA_GATEWAY)) is None: + zha_gateway = ZHAGateway(hass, config, config_entry) try: await zha_gateway.async_initialize() diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 3abf1274f98..353bc6904d7 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -149,6 +149,12 @@ class ZHAGateway: self.config_entry = config_entry self._unsubs: list[Callable[[], None]] = [] + discovery.PROBE.initialize(self._hass) + discovery.GROUP_PROBE.initialize(self._hass) + + self.ha_device_registry = dr.async_get(self._hass) + self.ha_entity_registry = er.async_get(self._hass) + def get_application_controller_data(self) -> tuple[ControllerApplication, dict]: """Get an uninitialized instance of a zigpy `ControllerApplication`.""" radio_type = self.config_entry.data[CONF_RADIO_TYPE] @@ -191,12 +197,6 @@ class ZHAGateway: async def async_initialize(self) -> None: """Initialize controller and connect radio.""" - discovery.PROBE.initialize(self._hass) - discovery.GROUP_PROBE.initialize(self._hass) - - self.ha_device_registry = dr.async_get(self._hass) - self.ha_entity_registry = er.async_get(self._hass) - app_controller_cls, app_config = self.get_application_controller_data() self.application_controller = await app_controller_cls.new( config=app_config, diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index 24ee63fb3d5..63ca10bbf91 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -1,8 +1,10 @@ """Tests for ZHA integration init.""" +import asyncio from unittest.mock import AsyncMock, Mock, patch import pytest from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH +from zigpy.exceptions import TransientConnectionError from homeassistant.components.zha import async_setup_entry from homeassistant.components.zha.core.const import ( @@ -11,10 +13,13 @@ from homeassistant.components.zha.core.const import ( CONF_USB_PATH, DOMAIN, ) -from homeassistant.const import MAJOR_VERSION, MINOR_VERSION +from homeassistant.const import MAJOR_VERSION, MINOR_VERSION, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.event import async_call_later from homeassistant.setup import async_setup_component +from .test_light import LIGHT_ON_OFF + from tests.common import MockConfigEntry DATA_RADIO_TYPE = "deconz" @@ -157,3 +162,46 @@ async def test_setup_with_v3_cleaning_uri( assert config_entry_v3.data[CONF_RADIO_TYPE] == DATA_RADIO_TYPE assert config_entry_v3.data[CONF_DEVICE][CONF_DEVICE_PATH] == cleaned_path assert config_entry_v3.version == 3 + + +@patch( + "homeassistant.components.zha.PLATFORMS", + [Platform.LIGHT, Platform.BUTTON, Platform.SENSOR, Platform.SELECT], +) +async def test_zha_retry_unique_ids( + hass: HomeAssistant, + config_entry: MockConfigEntry, + zigpy_device_mock, + mock_zigpy_connect, + caplog, +) -> None: + """Test that ZHA retrying creates unique entity IDs.""" + + config_entry.add_to_hass(hass) + + # Ensure we have some device to try to load + app = mock_zigpy_connect.return_value + light = zigpy_device_mock(LIGHT_ON_OFF) + app.devices[light.ieee] = light + + # Re-try setup but have it fail once, so entities have two chances to be created + with patch.object( + app, + "startup", + side_effect=[TransientConnectionError(), None], + ) as mock_connect: + with patch( + "homeassistant.config_entries.async_call_later", + lambda hass, delay, action: async_call_later(hass, 0, action), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Wait for the config entry setup to retry + await asyncio.sleep(0.1) + + assert len(mock_connect.mock_calls) == 2 + + await hass.config_entries.async_unload(config_entry.entry_id) + + assert "does not generate unique IDs" not in caplog.text From 96e932dad6b41579fa89a426e7c0ec112a7ff45a Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 5 Sep 2023 21:21:27 +0200 Subject: [PATCH 073/202] Bump reolink_aio to 0.7.9 (#99680) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 3ff25d1e7a0..060490c6e56 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.7.8"] + "requirements": ["reolink-aio==0.7.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index 11f404b6394..06d081594db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2294,7 +2294,7 @@ renault-api==0.2.0 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.8 +reolink-aio==0.7.9 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 581e8f3d325..45775e4374e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1684,7 +1684,7 @@ renault-api==0.2.0 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.8 +reolink-aio==0.7.9 # homeassistant.components.rflink rflink==0.0.65 From cea1109e25d34455922607a987194f62354ed948 Mon Sep 17 00:00:00 2001 From: Daniel Gangl <31815106+killer0071234@users.noreply.github.com> Date: Tue, 5 Sep 2023 21:18:06 +0200 Subject: [PATCH 074/202] Bump zamg to 0.3.0 (#99685) --- homeassistant/components/zamg/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zamg/manifest.json b/homeassistant/components/zamg/manifest.json index 3ff7612d47e..df17672231e 100644 --- a/homeassistant/components/zamg/manifest.json +++ b/homeassistant/components/zamg/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zamg", "iot_class": "cloud_polling", - "requirements": ["zamg==0.2.4"] + "requirements": ["zamg==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 06d081594db..a9568704d71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2760,7 +2760,7 @@ youtubeaio==1.1.5 yt-dlp==2023.7.6 # homeassistant.components.zamg -zamg==0.2.4 +zamg==0.3.0 # homeassistant.components.zengge zengge==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45775e4374e..cec152acba5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2033,7 +2033,7 @@ youless-api==1.0.1 youtubeaio==1.1.5 # homeassistant.components.zamg -zamg==0.2.4 +zamg==0.3.0 # homeassistant.components.zeroconf zeroconf==0.91.1 From e486ad735d41b9a6e4720dd5f8739950128e62b3 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 5 Sep 2023 21:13:28 +0200 Subject: [PATCH 075/202] Bump aiounifi to v61 (#99686) * Bump aiounifi to v61 * Alter a test to cover the upstream change --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifi/test_sensor.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index cb1c8f1c0dc..f20e5f9e4ac 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==60"], + "requirements": ["aiounifi==61"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index a9568704d71..56a34af1fbb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -363,7 +363,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==60 +aiounifi==61 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cec152acba5..bb4d90e93ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,7 +338,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==60 +aiounifi==61 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index da2c0b46f76..7ed87512f2b 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -336,8 +336,8 @@ async def test_bandwidth_sensors( "mac": "00:00:00:00:00:02", "name": "Wireless client", "oui": "Producer", - "rx_bytes-r": 2345000000, - "tx_bytes-r": 6789000000, + "rx_bytes-r": 2345000000.0, + "tx_bytes-r": 6789000000.0, } options = { CONF_ALLOW_BANDWIDTH_SENSORS: True, From f9ee18352d60c9984782a651e0524bde0503e72f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Sep 2023 20:42:50 -0500 Subject: [PATCH 076/202] Bump aioesphomeapi to 16.0.5 (#99698) changelog: https://github.com/esphome/aioesphomeapi/compare/v16.0.4...v16.0.5 fixes `RuntimeError: set changed size during iteration` https://github.com/esphome/aioesphomeapi/pull/538 some added debug logging which may help with https://github.com/home-assistant/core/issues/98221 --- 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 fd6fde0cb05..1c8da971168 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async_interrupt==1.1.1", - "aioesphomeapi==16.0.4", + "aioesphomeapi==16.0.5", "bluetooth-data-tools==1.11.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 56a34af1fbb..6345387e525 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==16.0.4 +aioesphomeapi==16.0.5 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb4d90e93ab..2afdc1c2f10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==16.0.4 +aioesphomeapi==16.0.5 # homeassistant.components.flo aioflo==2021.11.0 From 7b7fd35af29ae46593097f6baf66daec43a13c7f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Sep 2023 10:35:04 +0200 Subject: [PATCH 077/202] Fix unit conversion for gas cost sensor (#99708) --- homeassistant/components/energy/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index ae92ee2de58..e9760a96aa4 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -377,11 +377,10 @@ class EnergyCostSensor(SensorEntity): if energy_price_unit is None: converted_energy_price = energy_price else: - if self._adapter.source_type == "grid": - converter: Callable[ - [float, str, str], float - ] = unit_conversion.EnergyConverter.convert - elif self._adapter.source_type in ("gas", "water"): + converter: Callable[[float, str, str], float] + if energy_unit in VALID_ENERGY_UNITS: + converter = unit_conversion.EnergyConverter.convert + else: converter = unit_conversion.VolumeConverter.convert converted_energy_price = converter( From 800ff489b09da542384310f3d77c4ce332cba10e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 6 Sep 2023 10:40:05 +0200 Subject: [PATCH 078/202] Update frontend to 20230906.0 (#99715) --- 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 627b36a59b8..9e0bd3e5de9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230905.0"] + "requirements": ["home-assistant-frontend==20230906.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5f939ea9763..a0736bb427b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.70.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 -home-assistant-frontend==20230905.0 +home-assistant-frontend==20230906.0 home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6345387e525..0a8ecc21ae4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -994,7 +994,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230905.0 +home-assistant-frontend==20230906.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2afdc1c2f10..b7aa90642e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230905.0 +home-assistant-frontend==20230906.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 From 98896834cdac922e3fc60a20c225f1bd03eff98d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 6 Sep 2023 11:00:01 +0200 Subject: [PATCH 079/202] Bump version to 2023.9.0b6 --- 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 a851c041398..e4d6d55a523 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index d4a71cd6abe..63027052edd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.0b5" +version = "2023.9.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 107ca83d4284460f5da92df7e86db3403ba46986 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 6 Sep 2023 14:46:24 +0200 Subject: [PATCH 080/202] Reolink onvif not supported fix (#99714) * only subscibe to ONVIF if supported * Catch NotSupportedError when ONVIF is not supported * fix styling --- homeassistant/components/reolink/host.py | 47 +++++++++++++++++------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index a679cb34f4b..a43dbce9a7c 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -10,7 +10,7 @@ import aiohttp from aiohttp.web import Request from reolink_aio.api import Host from reolink_aio.enums import SubType -from reolink_aio.exceptions import ReolinkError, SubscriptionError +from reolink_aio.exceptions import NotSupportedError, ReolinkError, SubscriptionError from homeassistant.components import webhook from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME @@ -61,6 +61,7 @@ class ReolinkHost: ) self.webhook_id: str | None = None + self._onvif_supported: bool = True self._base_url: str = "" self._webhook_url: str = "" self._webhook_reachable: bool = False @@ -96,6 +97,8 @@ class ReolinkHost: f"'{self._api.user_level}', only admin users can change camera settings" ) + self._onvif_supported = self._api.supported(None, "ONVIF") + enable_rtsp = None enable_onvif = None enable_rtmp = None @@ -106,7 +109,7 @@ class ReolinkHost: ) enable_rtsp = True - if not self._api.onvif_enabled: + if not self._api.onvif_enabled and self._onvif_supported: _LOGGER.debug( "ONVIF is disabled on %s, trying to enable it", self._api.nvr_name ) @@ -154,21 +157,34 @@ class ReolinkHost: self._unique_id = format_mac(self._api.mac_address) - await self.subscribe() - - if self._api.supported(None, "initial_ONVIF_state"): + if self._onvif_supported: + try: + await self.subscribe() + except NotSupportedError: + self._onvif_supported = False + self.unregister_webhook() + await self._api.unsubscribe() + else: + if self._api.supported(None, "initial_ONVIF_state"): + _LOGGER.debug( + "Waiting for initial ONVIF state on webhook '%s'", + self._webhook_url, + ) + else: + _LOGGER.debug( + "Camera model %s most likely does not push its initial state" + " upon ONVIF subscription, do not check", + self._api.model, + ) + self._cancel_onvif_check = async_call_later( + self._hass, FIRST_ONVIF_TIMEOUT, self._async_check_onvif + ) + if not self._onvif_supported: _LOGGER.debug( - "Waiting for initial ONVIF state on webhook '%s'", self._webhook_url - ) - else: - _LOGGER.debug( - "Camera model %s most likely does not push its initial state" - " upon ONVIF subscription, do not check", + "Camera model %s does not support ONVIF, using fast polling instead", self._api.model, ) - self._cancel_onvif_check = async_call_later( - self._hass, FIRST_ONVIF_TIMEOUT, self._async_check_onvif - ) + await self._async_poll_all_motion() if self._api.sw_version_update_required: ir.async_create_issue( @@ -365,6 +381,9 @@ class ReolinkHost: async def renew(self) -> None: """Renew the subscription of motion events (lease time is 15 minutes).""" + if not self._onvif_supported: + return + try: await self._renew(SubType.push) if self._long_poll_task is not None: From 067f9461297e57a000c0f522096a3bd7c55b6024 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Sep 2023 16:07:05 +0200 Subject: [PATCH 081/202] Send template render errors to template helper preview (#99716) --- .../components/template/template_entity.py | 23 +-- homeassistant/helpers/event.py | 13 +- tests/components/template/test_config_flow.py | 173 +++++++++++++++++- 3 files changed, 190 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 2ce42083117..8c3554c067e 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -15,13 +15,11 @@ from homeassistant.const import ( CONF_ICON, CONF_ICON_TEMPLATE, CONF_NAME, - EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, ) from homeassistant.core import ( CALLBACK_TYPE, Context, - CoreState, HomeAssistant, State, callback, @@ -38,6 +36,7 @@ from homeassistant.helpers.event import ( async_track_template_result, ) from homeassistant.helpers.script import Script, _VarsType +from homeassistant.helpers.start import async_at_start from homeassistant.helpers.template import ( Template, TemplateStateFromEntityId, @@ -442,7 +441,11 @@ class TemplateEntity(Entity): ) @callback - def _async_template_startup(self, *_: Any) -> None: + def _async_template_startup( + self, + _hass: HomeAssistant | None, + log_fn: Callable[[int, str], None] | None = None, + ) -> None: template_var_tups: list[TrackTemplate] = [] has_availability_template = False @@ -467,6 +470,7 @@ class TemplateEntity(Entity): self.hass, template_var_tups, self._handle_results, + log_fn=log_fn, has_super_template=has_availability_template, ) self.async_on_remove(result_info.async_remove) @@ -515,10 +519,13 @@ class TemplateEntity(Entity): ) -> CALLBACK_TYPE: """Render a preview.""" + def log_template_error(level: int, msg: str) -> None: + preview_callback(None, None, None, msg) + self._preview_callback = preview_callback self._async_setup_templates() try: - self._async_template_startup() + self._async_template_startup(None, log_template_error) except Exception as err: # pylint: disable=broad-exception-caught preview_callback(None, None, None, str(err)) return self._call_on_remove_callbacks @@ -527,13 +534,7 @@ class TemplateEntity(Entity): """Run when entity about to be added to hass.""" self._async_setup_templates() - if self.hass.state == CoreState.running: - self._async_template_startup() - return - - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, self._async_template_startup - ) + async_at_start(self.hass, self._async_template_startup) async def async_update(self) -> None: """Call for forced update.""" diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 173dd057f96..51a8f1f1982 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -957,11 +957,14 @@ class TrackTemplateResultInfo: if info.exception: if raise_on_template_error: raise info.exception - _LOGGER.error( - "Error while processing template: %s", - track_template_.template, - exc_info=info.exception, - ) + if not log_fn: + _LOGGER.error( + "Error while processing template: %s", + track_template_.template, + exc_info=info.exception, + ) + else: + log_fn(logging.ERROR, str(info.exception)) self._track_state_changes = async_track_state_change_filtered( self.hass, _render_infos_to_track_states(self._info.values()), self._refresh diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index b8634b68b1c..f4cfe90b9f0 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -272,12 +272,12 @@ async def test_options( ), ( "sensor", - "{{ float(states('sensor.one')) + float(states('sensor.two')) }}", + "{{ float(states('sensor.one'), default='') + float(states('sensor.two'), default='') }}", {}, {"one": "30.0", "two": "20.0"}, - ["unavailable", "50.0"], + ["", "50.0"], [{}, {}], - [["one"], ["one", "two"]], + [["one", "two"], ["one", "two"]], ), ), ) @@ -470,6 +470,173 @@ async def test_config_flow_preview_bad_input( } +@pytest.mark.parametrize( + ( + "template_type", + "state_template", + "input_states", + "template_states", + "error_events", + ), + [ + ( + "sensor", + "{{ float(states('sensor.one')) + float(states('sensor.two')) }}", + {"one": "30.0", "two": "20.0"}, + ["unavailable", "50.0"], + [ + ( + "ValueError: Template error: float got invalid input 'unknown' " + "when rendering template '{{ float(states('sensor.one')) + " + "float(states('sensor.two')) }}' but no default was specified" + ) + ], + ), + ], +) +async def test_config_flow_preview_template_startup_error( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + template_type: str, + state_template: str, + input_states: dict[str, str], + template_states: list[str], + error_events: list[str], +) -> None: + """Test the config flow preview.""" + client = await hass_ws_client(hass) + + input_entities = ["one", "two"] + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": template_type}, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == template_type + assert result["errors"] is None + assert result["preview"] == "template" + + await client.send_json_auto_id( + { + "type": "template/start_preview", + "flow_id": result["flow_id"], + "flow_type": "config_flow", + "user_input": {"name": "My template", "state": state_template}, + } + ) + msg = await client.receive_json() + assert msg["type"] == "result" + assert msg["success"] + + for error_event in error_events: + msg = await client.receive_json() + assert msg["type"] == "event" + assert msg["event"] == {"error": error_event} + + msg = await client.receive_json() + assert msg["type"] == "event" + assert msg["event"]["state"] == template_states[0] + + for input_entity in input_entities: + hass.states.async_set( + f"{template_type}.{input_entity}", input_states[input_entity], {} + ) + + msg = await client.receive_json() + assert msg["type"] == "event" + assert msg["event"]["state"] == template_states[1] + + +@pytest.mark.parametrize( + ( + "template_type", + "state_template", + "input_states", + "template_states", + "error_events", + ), + [ + ( + "sensor", + "{{ float(states('sensor.one')) > 30 and undefined_function() }}", + [{"one": "30.0", "two": "20.0"}, {"one": "35.0", "two": "20.0"}], + ["False", "unavailable"], + ["'undefined_function' is undefined"], + ), + ], +) +async def test_config_flow_preview_template_error( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + template_type: str, + state_template: str, + input_states: list[dict[str, str]], + template_states: list[str], + error_events: list[str], +) -> None: + """Test the config flow preview.""" + client = await hass_ws_client(hass) + + input_entities = ["one", "two"] + + for input_entity in input_entities: + hass.states.async_set( + f"{template_type}.{input_entity}", input_states[0][input_entity], {} + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": template_type}, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == template_type + assert result["errors"] is None + assert result["preview"] == "template" + + await client.send_json_auto_id( + { + "type": "template/start_preview", + "flow_id": result["flow_id"], + "flow_type": "config_flow", + "user_input": {"name": "My template", "state": state_template}, + } + ) + msg = await client.receive_json() + assert msg["type"] == "result" + assert msg["success"] + + msg = await client.receive_json() + assert msg["type"] == "event" + assert msg["event"]["state"] == template_states[0] + + for input_entity in input_entities: + hass.states.async_set( + f"{template_type}.{input_entity}", input_states[1][input_entity], {} + ) + + for error_event in error_events: + msg = await client.receive_json() + assert msg["type"] == "event" + assert msg["event"] == {"error": error_event} + + msg = await client.receive_json() + assert msg["type"] == "event" + assert msg["event"]["state"] == template_states[1] + + @pytest.mark.parametrize( ( "template_type", From 6f6306b39b7d0d9898dffa8176f8ee76f1be9b23 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Sep 2023 15:59:30 +0200 Subject: [PATCH 082/202] Don't allow changing device class in template binary sensor options (#99720) --- homeassistant/components/template/config_flow.py | 8 ++++---- homeassistant/components/template/strings.json | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 093cbf14098..15be2c52d91 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -40,11 +40,11 @@ from .template_entity import TemplateEntity NONE_SENTINEL = "none" -def generate_schema(domain: str) -> dict[vol.Marker, Any]: +def generate_schema(domain: str, flow_type: str) -> dict[vol.Marker, Any]: """Generate schema.""" schema: dict[vol.Marker, Any] = {} - if domain == Platform.BINARY_SENSOR: + if domain == Platform.BINARY_SENSOR and flow_type == "config": schema = { vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector( selector.SelectSelectorConfig( @@ -124,7 +124,7 @@ def options_schema(domain: str) -> vol.Schema: """Generate options schema.""" return vol.Schema( {vol.Required(CONF_STATE): selector.TemplateSelector()} - | generate_schema(domain), + | generate_schema(domain, "option"), ) @@ -135,7 +135,7 @@ def config_schema(domain: str) -> vol.Schema: vol.Required(CONF_NAME): selector.TextSelector(), vol.Required(CONF_STATE): selector.TemplateSelector(), } - | generate_schema(domain), + | generate_schema(domain, "config"), ) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 7e5e56a26d6..a0ee31126cd 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -33,7 +33,6 @@ "step": { "binary_sensor": { "data": { - "device_class": "[%key:component::template::config::step::sensor::data::device_class%]", "state": "[%key:component::template::config::step::sensor::data::state%]" }, "title": "[%key:component::template::config::step::binary_sensor::title%]" From d99e5e0c4f293f6edd30e48a05150ecfd376e9ec Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Sep 2023 16:06:33 +0200 Subject: [PATCH 083/202] Correct state attributes in template helper preview (#99722) --- homeassistant/components/template/config_flow.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 15be2c52d91..c361b4c42cc 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -24,7 +24,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import selector +from homeassistant.helpers import entity_registry as er, selector from homeassistant.helpers.schema_config_entry_flow import ( SchemaCommonFlowHandler, SchemaConfigFlowHandler, @@ -328,6 +328,7 @@ def ws_start_preview( return errors + entity_registry_entry: er.RegistryEntry | None = None if msg["flow_type"] == "config_flow": flow_status = hass.config_entries.flow.async_get(msg["flow_id"]) template_type = flow_status["step_id"] @@ -342,6 +343,12 @@ def ws_start_preview( template_type = config_entry.options["template_type"] name = config_entry.options["name"] schema = cast(vol.Schema, OPTIONS_FLOW[template_type].schema) + entity_registry = er.async_get(hass) + entries = er.async_entries_for_config_entry( + entity_registry, flow_status["handler"] + ) + if entries: + entity_registry_entry = entries[0] errors = _validate(schema, template_type, msg["user_input"]) @@ -382,6 +389,7 @@ def ws_start_preview( _strip_sentinel(msg["user_input"]) preview_entity = CREATE_PREVIEW_ENTITY[template_type](hass, name, msg["user_input"]) preview_entity.hass = hass + preview_entity.registry_entry = entity_registry_entry connection.send_result(msg["id"]) connection.subscriptions[msg["id"]] = preview_entity.async_start_preview( From b5d9014bbd608e0454120e3d68e06cefd9a783a2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Sep 2023 15:59:03 +0200 Subject: [PATCH 084/202] Correct state attributes in group helper preview (#99723) --- homeassistant/components/group/config_flow.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 9eb973b9609..93160b0db5b 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -361,6 +361,7 @@ def ws_start_preview( msg: dict[str, Any], ) -> None: """Generate a preview.""" + entity_registry_entry: er.RegistryEntry | None = None if msg["flow_type"] == "config_flow": flow_status = hass.config_entries.flow.async_get(msg["flow_id"]) group_type = flow_status["step_id"] @@ -370,12 +371,17 @@ def ws_start_preview( name = validated["name"] else: flow_status = hass.config_entries.options.async_get(msg["flow_id"]) - config_entry = hass.config_entries.async_get_entry(flow_status["handler"]) + config_entry_id = flow_status["handler"] + config_entry = hass.config_entries.async_get_entry(config_entry_id) if not config_entry: raise HomeAssistantError group_type = config_entry.options["group_type"] name = config_entry.options["name"] validated = PREVIEW_OPTIONS_SCHEMA[group_type](msg["user_input"]) + entity_registry = er.async_get(hass) + entries = er.async_entries_for_config_entry(entity_registry, config_entry_id) + if entries: + entity_registry_entry = entries[0] @callback def async_preview_updated(state: str, attributes: Mapping[str, Any]) -> None: @@ -388,6 +394,7 @@ def ws_start_preview( preview_entity = CREATE_PREVIEW_ENTITY[group_type](name, validated) preview_entity.hass = hass + preview_entity.registry_entry = entity_registry_entry connection.send_result(msg["id"]) connection.subscriptions[msg["id"]] = preview_entity.async_start_preview( From 02fc735c080e05fb3c7a99df7b2eaecb75fd7e88 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 6 Sep 2023 15:00:26 +0200 Subject: [PATCH 085/202] Update frontend to 20230906.1 (#99733) --- 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 9e0bd3e5de9..50c557eae89 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230906.0"] + "requirements": ["home-assistant-frontend==20230906.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a0736bb427b..7c48166172f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.70.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 -home-assistant-frontend==20230906.0 +home-assistant-frontend==20230906.1 home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0a8ecc21ae4..d656b0dbb48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -994,7 +994,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230906.0 +home-assistant-frontend==20230906.1 # homeassistant.components.conversation home-assistant-intents==2023.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b7aa90642e4..308759108ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230906.0 +home-assistant-frontend==20230906.1 # homeassistant.components.conversation home-assistant-intents==2023.8.2 From d369a700923292b819ff960af1bf129c3efbf4a9 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 6 Sep 2023 16:09:56 +0200 Subject: [PATCH 086/202] Bumped version to 2023.9.0 --- 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 e4d6d55a523..cfdb5095128 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 63027052edd..e4403bd7c30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.0b6" +version = "2023.9.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 39669bc788c75ca1d50b31c4133dac2844372c82 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Wed, 6 Sep 2023 10:51:27 -0400 Subject: [PATCH 087/202] Fix the Hydrawise status sensor (#99271) --- homeassistant/components/hydrawise/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index 63fe28cd400..9298e605791 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -90,7 +90,7 @@ class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity): """Get the latest data and updates the state.""" LOGGER.debug("Updating Hydrawise binary sensor: %s", self.name) if self.entity_description.key == "status": - self._attr_is_on = self.coordinator.api.status == "All good!" + self._attr_is_on = self.coordinator.last_update_success elif self.entity_description.key == "is_watering": relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]] self._attr_is_on = relay_data["timestr"] == "Now" From 8b69f9fda83e16e8e10f08ead240b858380b0944 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 6 Sep 2023 16:57:13 +0200 Subject: [PATCH 088/202] Fix tradfri asyncio.wait (#99730) --- homeassistant/components/tradfri/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 2a3052c1f7b..a383cc2bbee 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -122,7 +122,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if same_hub_entries: await asyncio.wait( [ - self.hass.config_entries.async_remove(entry_id) + asyncio.create_task(self.hass.config_entries.async_remove(entry_id)) for entry_id in same_hub_entries ] ) From 64408cab10dfede169db947fa5294d1bd8f4637a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 6 Sep 2023 18:54:16 +0200 Subject: [PATCH 089/202] Handle alexa invalid climate temp adjustment (#99740) * Handle temp adjust when target state not set * Update homeassistant/components/alexa/errors.py Co-authored-by: Robert Resch * black --------- Co-authored-by: Robert Resch --- homeassistant/components/alexa/errors.py | 7 +++ homeassistant/components/alexa/handlers.py | 9 ++- tests/components/alexa/test_smart_home.py | 69 ++++++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py index 2c5ced62403..f8e3720e160 100644 --- a/homeassistant/components/alexa/errors.py +++ b/homeassistant/components/alexa/errors.py @@ -90,6 +90,13 @@ class AlexaUnsupportedThermostatModeError(AlexaError): error_type = "UNSUPPORTED_THERMOSTAT_MODE" +class AlexaUnsupportedThermostatTargetStateError(AlexaError): + """Class to represent unsupported climate target state error.""" + + namespace = "Alexa.ThermostatController" + error_type = "INVALID_TARGET_STATE" + + class AlexaTempRangeError(AlexaError): """Class to represent TempRange errors.""" diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 3e995e9ffe2..f99b0231e4d 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -73,6 +73,7 @@ from .errors import ( AlexaSecurityPanelAuthorizationRequired, AlexaTempRangeError, AlexaUnsupportedThermostatModeError, + AlexaUnsupportedThermostatTargetStateError, AlexaVideoActionNotPermittedForContentError, ) from .state_report import AlexaDirective, AlexaResponse, async_enable_proactive_mode @@ -911,7 +912,13 @@ async def async_api_adjust_target_temp( } ) else: - target_temp = float(entity.attributes[ATTR_TEMPERATURE]) + temp_delta + current_target_temp: str | None = entity.attributes.get(ATTR_TEMPERATURE) + if current_target_temp is None: + raise AlexaUnsupportedThermostatTargetStateError( + "The current target temperature is not set, " + "cannot adjust target temperature" + ) + target_temp = float(current_target_temp) + temp_delta if target_temp < min_temp or target_temp > max_temp: raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index c42ea0a0f6a..bbdf3efeb5f 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -2471,6 +2471,75 @@ async def test_thermostat(hass: HomeAssistant) -> None: assert call.data["preset_mode"] == "eco" +async def test_no_current_target_temp_adjusting_temp(hass: HomeAssistant) -> None: + """Test thermostat adjusting temp with no initial target temperature.""" + hass.config.units = US_CUSTOMARY_SYSTEM + device = ( + "climate.test_thermostat", + "cool", + { + "temperature": None, + "target_temp_high": None, + "target_temp_low": None, + "current_temperature": 75.0, + "friendly_name": "Test Thermostat", + "supported_features": 1 | 2 | 4 | 128, + "hvac_modes": ["off", "heat", "cool", "auto", "dry", "fan_only"], + "preset_mode": None, + "preset_modes": ["eco"], + "min_temp": 50, + "max_temp": 90, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "climate#test_thermostat" + assert appliance["displayCategories"][0] == "THERMOSTAT" + assert appliance["friendlyName"] == "Test Thermostat" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PowerController", + "Alexa.ThermostatController", + "Alexa.TemperatureSensor", + "Alexa.EndpointHealth", + "Alexa", + ) + + properties = await reported_properties(hass, "climate#test_thermostat") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "COOL") + properties.assert_not_has_property( + "Alexa.ThermostatController", + "targetSetpoint", + ) + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 75.0, "scale": "FAHRENHEIT"} + ) + + thermostat_capability = get_capability(capabilities, "Alexa.ThermostatController") + assert thermostat_capability is not None + configuration = thermostat_capability["configuration"] + assert configuration["supportsScheduling"] is False + + supported_modes = ["OFF", "HEAT", "COOL", "AUTO", "ECO", "CUSTOM"] + for mode in supported_modes: + assert mode in configuration["supportedModes"] + + # Adjust temperature where target temp is not set + msg = await assert_request_fails( + "Alexa.ThermostatController", + "AdjustTargetTemperature", + "climate#test_thermostat", + "climate.set_temperature", + hass, + payload={"targetSetpointDelta": {"value": -5.0, "scale": "KELVIN"}}, + ) + assert msg["event"]["payload"]["type"] == "INVALID_TARGET_STATE" + assert msg["event"]["payload"]["message"] == ( + "The current target temperature is not set, cannot adjust target temperature" + ) + + async def test_thermostat_dual(hass: HomeAssistant) -> None: """Test thermostat discovery with auto mode, with upper and lower target temperatures.""" hass.config.units = US_CUSTOMARY_SYSTEM From c741214ab5cbf7278180570b98c6b097d3cfd50d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 6 Sep 2023 17:16:40 +0200 Subject: [PATCH 090/202] Revert "Bump pyoverkiz to 1.10.1 (#97916)" (#99742) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 8cf029adb54..d88996c7e02 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -13,7 +13,7 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"], - "requirements": ["pyoverkiz==1.10.1"], + "requirements": ["pyoverkiz==1.9.0"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index d656b0dbb48..01c29698411 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1916,7 +1916,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.10.1 +pyoverkiz==1.9.0 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 308759108ca..a86febaabfb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1423,7 +1423,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.10.1 +pyoverkiz==1.9.0 # homeassistant.components.openweathermap pyowm==3.2.0 From 718f1a6673aac434e7949ece9f4fa5898792d559 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 7 Sep 2023 10:28:08 +0200 Subject: [PATCH 091/202] Fix Freebox disk free space sensor (#99757) * Fix Freebox disk free space sensor * Add initial value assert to check results --- .coveragerc | 1 - homeassistant/components/freebox/router.py | 7 +++- homeassistant/components/freebox/sensor.py | 13 ++++--- tests/components/freebox/common.py | 27 +++++++++++++ tests/components/freebox/test_sensor.py | 45 ++++++++++++++++++++++ 5 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 tests/components/freebox/common.py create mode 100644 tests/components/freebox/test_sensor.py diff --git a/.coveragerc b/.coveragerc index 97ed97ef293..66c47e35f37 100644 --- a/.coveragerc +++ b/.coveragerc @@ -416,7 +416,6 @@ omit = homeassistant/components/freebox/device_tracker.py homeassistant/components/freebox/home_base.py homeassistant/components/freebox/router.py - homeassistant/components/freebox/sensor.py homeassistant/components/freebox/switch.py homeassistant/components/fritz/common.py homeassistant/components/fritz/device_tracker.py diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 7c83e980540..cd5862a2f80 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -156,7 +156,12 @@ class FreeboxRouter: fbx_disks: list[dict[str, Any]] = await self._api.storage.get_disks() or [] for fbx_disk in fbx_disks: - self.disks[fbx_disk["id"]] = fbx_disk + disk: dict[str, Any] = {**fbx_disk} + disk_part: dict[int, dict[str, Any]] = {} + for fbx_disk_part in fbx_disk["partitions"]: + disk_part[fbx_disk_part["id"]] = fbx_disk_part + disk["partitions"] = disk_part + self.disks[fbx_disk["id"]] = disk async def _update_raids_sensors(self) -> None: """Update Freebox raids.""" diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 901bfc63199..4e7c3910c54 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -95,7 +95,7 @@ async def async_setup_entry( entities.extend( FreeboxDiskSensor(router, disk, partition, description) for disk in router.disks.values() - for partition in disk["partitions"] + for partition in disk["partitions"].values() for description in DISK_PARTITION_SENSORS ) @@ -197,7 +197,8 @@ class FreeboxDiskSensor(FreeboxSensor): ) -> None: """Initialize a Freebox disk sensor.""" super().__init__(router, description) - self._partition = partition + self._disk_id = disk["id"] + self._partition_id = partition["id"] self._attr_name = f"{partition['label']} {description.name}" self._attr_unique_id = ( f"{router.mac} {description.key} {disk['id']} {partition['id']}" @@ -218,10 +219,10 @@ class FreeboxDiskSensor(FreeboxSensor): def async_update_state(self) -> None: """Update the Freebox disk sensor.""" value = None - if self._partition.get("total_bytes"): - value = round( - self._partition["free_bytes"] * 100 / self._partition["total_bytes"], 2 - ) + disk: dict[str, Any] = self._router.disks[self._disk_id] + partition: dict[str, Any] = disk["partitions"][self._partition_id] + if partition.get("total_bytes"): + value = round(partition["free_bytes"] * 100 / partition["total_bytes"], 2) self._attr_native_value = value diff --git a/tests/components/freebox/common.py b/tests/components/freebox/common.py new file mode 100644 index 00000000000..9f7dfd8f92a --- /dev/null +++ b/tests/components/freebox/common.py @@ -0,0 +1,27 @@ +"""Common methods used across tests for Freebox.""" +from unittest.mock import patch + +from homeassistant.components.freebox.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import MOCK_HOST, MOCK_PORT + +from tests.common import MockConfigEntry + + +async def setup_platform(hass: HomeAssistant, platform: str) -> MockConfigEntry: + """Set up the Freebox platform.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, + unique_id=MOCK_HOST, + ) + mock_entry.add_to_hass(hass) + + with patch("homeassistant.components.freebox.PLATFORMS", [platform]): + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + return mock_entry diff --git a/tests/components/freebox/test_sensor.py b/tests/components/freebox/test_sensor.py new file mode 100644 index 00000000000..2ebcf8baa04 --- /dev/null +++ b/tests/components/freebox/test_sensor.py @@ -0,0 +1,45 @@ +"""Tests for the Freebox sensors.""" +from copy import deepcopy +from unittest.mock import Mock + +from freezegun.api import FrozenDateTimeFactory + +from homeassistant.components.freebox import SCAN_INTERVAL +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.core import HomeAssistant + +from .common import setup_platform +from .const import DATA_STORAGE_GET_DISKS + +from tests.common import async_fire_time_changed + + +async def test_disk( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, router: Mock +) -> None: + """Test disk sensor.""" + await setup_platform(hass, SENSOR_DOMAIN) + + # Initial state + assert ( + router().storage.get_disks.return_value[2]["partitions"][0]["total_bytes"] + == 1960000000000 + ) + + assert ( + router().storage.get_disks.return_value[2]["partitions"][0]["free_bytes"] + == 1730000000000 + ) + + assert hass.states.get("sensor.freebox_free_space").state == "88.27" + + # Simulate a changed storage size + data_storage_get_disks_changed = deepcopy(DATA_STORAGE_GET_DISKS) + data_storage_get_disks_changed[2]["partitions"][0]["free_bytes"] = 880000000000 + router().storage.get_disks.return_value = data_storage_get_disks_changed + # Simulate an update + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + # To execute the save + await hass.async_block_till_done() + assert hass.states.get("sensor.freebox_free_space").state == "44.9" From fb56e48e07bdc790d2dd3dabed3ea69e3c7827dc Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 7 Sep 2023 17:28:50 +0200 Subject: [PATCH 092/202] Fix Freebox Home battery sensor (#99756) --- homeassistant/components/freebox/const.py | 3 +++ tests/components/freebox/const.py | 6 ++--- tests/components/freebox/test_sensor.py | 28 ++++++++++++++++++++++- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index 59dce75649b..5bed7b3456a 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -85,4 +85,7 @@ CATEGORY_TO_MODEL = { HOME_COMPATIBLE_CATEGORIES = [ FreeboxHomeCategory.CAMERA, + FreeboxHomeCategory.DWS, + FreeboxHomeCategory.KFB, + FreeboxHomeCategory.PIR, ] diff --git a/tests/components/freebox/const.py b/tests/components/freebox/const.py index a6253dbf315..0b58348a5df 100644 --- a/tests/components/freebox/const.py +++ b/tests/components/freebox/const.py @@ -1986,7 +1986,7 @@ DATA_HOME_GET_NODES = [ "category": "kfb", "group": {"label": ""}, "id": 9, - "label": "Télécommande I", + "label": "Télécommande", "name": "node_9", "props": { "Address": 5, @@ -2067,7 +2067,7 @@ DATA_HOME_GET_NODES = [ "category": "dws", "group": {"label": "Entrée"}, "id": 11, - "label": "dws i", + "label": "Ouverture porte", "name": "node_11", "props": { "Address": 6, @@ -2259,7 +2259,7 @@ DATA_HOME_GET_NODES = [ "category": "pir", "group": {"label": "Salon"}, "id": 26, - "label": "Salon Détecteur s", + "label": "Détecteur", "name": "node_26", "props": { "Address": 9, diff --git a/tests/components/freebox/test_sensor.py b/tests/components/freebox/test_sensor.py index 2ebcf8baa04..41daa79fe4e 100644 --- a/tests/components/freebox/test_sensor.py +++ b/tests/components/freebox/test_sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant from .common import setup_platform -from .const import DATA_STORAGE_GET_DISKS +from .const import DATA_HOME_GET_NODES, DATA_STORAGE_GET_DISKS from tests.common import async_fire_time_changed @@ -43,3 +43,29 @@ async def test_disk( # To execute the save await hass.async_block_till_done() assert hass.states.get("sensor.freebox_free_space").state == "44.9" + + +async def test_battery( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, router: Mock +) -> None: + """Test battery sensor.""" + await setup_platform(hass, SENSOR_DOMAIN) + + assert hass.states.get("sensor.telecommande_niveau_de_batterie").state == "100" + assert hass.states.get("sensor.ouverture_porte_niveau_de_batterie").state == "100" + assert hass.states.get("sensor.detecteur_niveau_de_batterie").state == "100" + + # Simulate a changed battery + data_home_get_nodes_changed = deepcopy(DATA_HOME_GET_NODES) + data_home_get_nodes_changed[2]["show_endpoints"][3]["value"] = 25 + data_home_get_nodes_changed[3]["show_endpoints"][3]["value"] = 50 + data_home_get_nodes_changed[4]["show_endpoints"][3]["value"] = 75 + router().home.get_home_nodes.return_value = data_home_get_nodes_changed + # Simulate an update + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + # To execute the save + await hass.async_block_till_done() + assert hass.states.get("sensor.telecommande_niveau_de_batterie").state == "25" + assert hass.states.get("sensor.ouverture_porte_niveau_de_batterie").state == "50" + assert hass.states.get("sensor.detecteur_niveau_de_batterie").state == "75" From a25f6bbd0995d9bbf85484426ccf00738c096052 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 6 Sep 2023 20:01:22 -0500 Subject: [PATCH 093/202] Bump sense_energy to 0.12.1 (#99763) --- homeassistant/components/emulated_kasa/manifest.json | 2 +- homeassistant/components/sense/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index 324279db7d9..d39d530eccc 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_push", "loggers": ["sense_energy"], "quality_scale": "internal", - "requirements": ["sense_energy==0.12.0"] + "requirements": ["sense_energy==0.12.1"] } diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 8c20db2e422..8a89d6d8531 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/sense", "iot_class": "cloud_polling", "loggers": ["sense_energy"], - "requirements": ["sense-energy==0.12.0"] + "requirements": ["sense-energy==0.12.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 01c29698411..5c116d88868 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2369,10 +2369,10 @@ securetar==2023.3.0 sendgrid==6.8.2 # homeassistant.components.sense -sense-energy==0.12.0 +sense-energy==0.12.1 # homeassistant.components.emulated_kasa -sense_energy==0.12.0 +sense_energy==0.12.1 # homeassistant.components.sensirion_ble sensirion-ble==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a86febaabfb..8a33575909b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1729,10 +1729,10 @@ screenlogicpy==0.8.2 securetar==2023.3.0 # homeassistant.components.sense -sense-energy==0.12.0 +sense-energy==0.12.1 # homeassistant.components.emulated_kasa -sense_energy==0.12.0 +sense_energy==0.12.1 # homeassistant.components.sensirion_ble sensirion-ble==0.1.0 From 7c7fed032223d328afb3ef915e1e540d762f107a Mon Sep 17 00:00:00 2001 From: Pawel Date: Thu, 7 Sep 2023 12:45:31 +0200 Subject: [PATCH 094/202] Add support for more busy codes for Epson (#99771) add support for more busy codes --- homeassistant/components/epson/manifest.json | 2 +- homeassistant/components/epson/media_player.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/epson/manifest.json b/homeassistant/components/epson/manifest.json index 77a1a89b686..7b8f8d8a4a2 100644 --- a/homeassistant/components/epson/manifest.json +++ b/homeassistant/components/epson/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/epson", "iot_class": "local_polling", "loggers": ["epson_projector"], - "requirements": ["epson-projector==0.5.0"] + "requirements": ["epson-projector==0.5.1"] } diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 5c49f566bb5..1f80be9fe06 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -6,7 +6,7 @@ import logging from epson_projector import Projector, ProjectorUnavailableError from epson_projector.const import ( BACK, - BUSY, + BUSY_CODES, CMODE, CMODE_LIST, CMODE_LIST_SET, @@ -147,7 +147,7 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): self._attr_volume_level = float(volume) except ValueError: self._attr_volume_level = None - elif power_state == BUSY: + elif power_state in BUSY_CODES: self._attr_state = MediaPlayerState.ON else: self._attr_state = MediaPlayerState.OFF diff --git a/requirements_all.txt b/requirements_all.txt index 5c116d88868..314dd122129 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -751,7 +751,7 @@ env-canada==0.5.36 ephem==4.1.2 # homeassistant.components.epson -epson-projector==0.5.0 +epson-projector==0.5.1 # homeassistant.components.epsonworkforce epsonprinter==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a33575909b..205abc1cc7f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -604,7 +604,7 @@ env-canada==0.5.36 ephem==4.1.2 # homeassistant.components.epson -epson-projector==0.5.0 +epson-projector==0.5.1 # homeassistant.components.esphome esphome-dashboard-api==1.2.3 From c740b79e1e71e67d35518895ba07180dea3d8feb Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 6 Sep 2023 17:26:14 -0600 Subject: [PATCH 095/202] Bump `aiorecollect` to 2023.09.0 (#99780) --- homeassistant/components/recollect_waste/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json index dc31adddb78..e1ad3f98950 100644 --- a/homeassistant/components/recollect_waste/manifest.json +++ b/homeassistant/components/recollect_waste/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["aiorecollect"], - "requirements": ["aiorecollect==1.0.8"] + "requirements": ["aiorecollect==2023.09.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 314dd122129..afa46ccf2f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -327,7 +327,7 @@ aiopyarr==23.4.0 aioqsw==0.3.4 # homeassistant.components.recollect_waste -aiorecollect==1.0.8 +aiorecollect==2023.09.0 # homeassistant.components.ridwell aioridwell==2023.07.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 205abc1cc7f..b5c02fa72bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -302,7 +302,7 @@ aiopyarr==23.4.0 aioqsw==0.3.4 # homeassistant.components.recollect_waste -aiorecollect==1.0.8 +aiorecollect==2023.09.0 # homeassistant.components.ridwell aioridwell==2023.07.0 From c217ea7b388a4a2e0320a2e5f1509620842f9a37 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Sep 2023 05:17:04 -0500 Subject: [PATCH 096/202] Bump pyenphase to 1.9.3 (#99787) * Bump pyenphase to 1.9.2 changelog: https://github.com/pyenphase/pyenphase/compare/v1.9.1...v1.9.2 Handle the case where the user has manually specified a password for local auth with firmware < 7.x but its incorrect. The integration previously accepted any wrong password and would reduce functionality down to what works without a password. We now preserve that behavior to avoid breaking existing installs. * bump --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index a45f4f01e49..d3a36b16b60 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.9.1"], + "requirements": ["pyenphase==1.9.3"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index afa46ccf2f6..5052906c9a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1671,7 +1671,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.9.1 +pyenphase==1.9.3 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b5c02fa72bb..b83cbe6146e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1235,7 +1235,7 @@ pyeconet==0.1.20 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.9.1 +pyenphase==1.9.3 # homeassistant.components.everlights pyeverlights==0.1.0 From 45e723135c1216709cc23d0b52796055919a9c50 Mon Sep 17 00:00:00 2001 From: swamplynx Date: Thu, 7 Sep 2023 06:17:38 -0400 Subject: [PATCH 097/202] Bump pylutron-caseta to v0.18.2 (#99789) * Bump pylutron-caseta to v0.18.2 Minor bump to pylutron-caseta requirement to support wall mounted occupancy sensor device type in latest RA3 firmware. * Update requirements_all.txt for pylutron-caseta 0.18.2 * Update requirements_test_all.txt for pylutron-caseta 0.18.2 --- homeassistant/components/lutron_caseta/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index feab9744df0..bf6ed32c668 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -9,7 +9,7 @@ }, "iot_class": "local_push", "loggers": ["pylutron_caseta"], - "requirements": ["pylutron-caseta==0.18.1"], + "requirements": ["pylutron-caseta==0.18.2"], "zeroconf": [ { "type": "_lutron._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 5052906c9a9..7086fb20ec2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1821,7 +1821,7 @@ pylitejet==0.5.0 pylitterbot==2023.4.5 # homeassistant.components.lutron_caseta -pylutron-caseta==0.18.1 +pylutron-caseta==0.18.2 # homeassistant.components.lutron pylutron==0.2.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b83cbe6146e..6f98821badb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1349,7 +1349,7 @@ pylitejet==0.5.0 pylitterbot==2023.4.5 # homeassistant.components.lutron_caseta -pylutron-caseta==0.18.1 +pylutron-caseta==0.18.2 # homeassistant.components.mailgun pymailgunner==1.4 From 590ac173edfeb1ec32b77577613891869ea4636a Mon Sep 17 00:00:00 2001 From: lymanepp <4195527+lymanepp@users.noreply.github.com> Date: Thu, 7 Sep 2023 22:12:18 -0400 Subject: [PATCH 098/202] Fix missing dew point and humidity in tomorrowio forecasts (#99793) * Fix missing dew point and humidity in tomorrowio forecasts * Add assertion for correct parameters to realtime_and_all_forecasts method --- .../components/tomorrowio/__init__.py | 2 + tests/components/tomorrowio/test_weather.py | 59 ++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tomorrowio/__init__.py b/homeassistant/components/tomorrowio/__init__.py index 41fa8158624..77675e3f2ec 100644 --- a/homeassistant/components/tomorrowio/__init__.py +++ b/homeassistant/components/tomorrowio/__init__.py @@ -302,6 +302,8 @@ class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): [ TMRW_ATTR_TEMPERATURE_LOW, TMRW_ATTR_TEMPERATURE_HIGH, + TMRW_ATTR_DEW_POINT, + TMRW_ATTR_HUMIDITY, TMRW_ATTR_WIND_SPEED, TMRW_ATTR_WIND_DIRECTION, TMRW_ATTR_CONDITION, diff --git a/tests/components/tomorrowio/test_weather.py b/tests/components/tomorrowio/test_weather.py index a6a5e935614..229e62065a6 100644 --- a/tests/components/tomorrowio/test_weather.py +++ b/tests/components/tomorrowio/test_weather.py @@ -153,9 +153,66 @@ async def test_legacy_config_entry(hass: HomeAssistant) -> None: assert len(er.async_entries_for_config_entry(registry, entry.entry_id)) == 30 -async def test_v4_weather(hass: HomeAssistant) -> None: +async def test_v4_weather(hass: HomeAssistant, tomorrowio_config_entry_update) -> None: """Test v4 weather data.""" weather_state = await _setup(hass, API_V4_ENTRY_DATA) + + tomorrowio_config_entry_update.assert_called_with( + [ + "temperature", + "humidity", + "pressureSeaLevel", + "windSpeed", + "windDirection", + "weatherCode", + "visibility", + "pollutantO3", + "windGust", + "cloudCover", + "precipitationType", + "pollutantCO", + "mepIndex", + "mepHealthConcern", + "mepPrimaryPollutant", + "cloudBase", + "cloudCeiling", + "cloudCover", + "dewPoint", + "epaIndex", + "epaHealthConcern", + "epaPrimaryPollutant", + "temperatureApparent", + "fireIndex", + "pollutantNO2", + "pollutantO3", + "particulateMatter10", + "particulateMatter25", + "grassIndex", + "treeIndex", + "weedIndex", + "precipitationType", + "pressureSurfaceLevel", + "solarGHI", + "pollutantSO2", + "uvIndex", + "uvHealthConcern", + "windGust", + ], + [ + "temperatureMin", + "temperatureMax", + "dewPoint", + "humidity", + "windSpeed", + "windDirection", + "weatherCode", + "precipitationIntensityAvg", + "precipitationProbability", + ], + nowcast_timestep=60, + location="80.0,80.0", + ) + assert weather_state.state == ATTR_CONDITION_SUNNY assert weather_state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION assert len(weather_state.attributes[ATTR_FORECAST]) == 14 From 31fb9d12ea25eb9436a2962f1138c8e0b5a8aa2c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Sep 2023 12:00:19 +0200 Subject: [PATCH 099/202] Always set severity level flag on render_template error events (#99804) --- homeassistant/components/websocket_api/commands.py | 4 +++- tests/components/websocket_api/test_commands.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 7772bef66f9..a05f2aa8e3f 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -565,7 +565,9 @@ async def handle_render_template( if not report_errors: return connection.send_message( - messages.event_message(msg["id"], {"error": str(result)}) + messages.event_message( + msg["id"], {"error": str(result), "level": "ERROR"} + ) ) return diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 96e79a81716..70f08477a72 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1512,7 +1512,10 @@ async def test_render_template_with_delayed_error( assert msg["id"] == 5 assert msg["type"] == "event" event = msg["event"] - assert event == {"error": "UndefinedError: 'explode' is undefined"} + assert event == { + "error": "UndefinedError: 'explode' is undefined", + "level": "ERROR", + } assert "Template variable error" not in caplog.text assert "Template variable warning" not in caplog.text From 850c0959883694eea7a3bc5c70ba353c4139ecc6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Sep 2023 13:33:38 +0200 Subject: [PATCH 100/202] Improve error handling in /api/states POST (#99810) --- homeassistant/components/api/__init__.py | 23 ++++++++++++++++++----- tests/components/api/test_init.py | 22 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 7b13833ccab..d968784b5b9 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -28,7 +28,13 @@ from homeassistant.const import ( ) import homeassistant.core as ha from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ServiceNotFound, TemplateError, Unauthorized +from homeassistant.exceptions import ( + InvalidEntityFormatError, + InvalidStateError, + ServiceNotFound, + TemplateError, + Unauthorized, +) from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.json import json_dumps from homeassistant.helpers.service import async_get_all_descriptions @@ -222,7 +228,7 @@ class APIEntityStateView(HomeAssistantView): """Update state of entity.""" if not request["hass_user"].is_admin: raise Unauthorized(entity_id=entity_id) - hass = request.app["hass"] + hass: HomeAssistant = request.app["hass"] try: data = await request.json() except ValueError: @@ -237,9 +243,16 @@ class APIEntityStateView(HomeAssistantView): is_new_state = hass.states.get(entity_id) is None # Write state - hass.states.async_set( - entity_id, new_state, attributes, force_update, self.context(request) - ) + try: + hass.states.async_set( + entity_id, new_state, attributes, force_update, self.context(request) + ) + except InvalidEntityFormatError: + return self.json_message( + "Invalid entity ID specified.", HTTPStatus.BAD_REQUEST + ) + except InvalidStateError: + return self.json_message("Invalid state specified.", HTTPStatus.BAD_REQUEST) # Read the state back for our response status_code = HTTPStatus.CREATED if is_new_state else HTTPStatus.OK diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index 116529b02a4..b3bb6de49cf 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -96,6 +96,28 @@ async def test_api_state_change_of_non_existing_entity( assert hass.states.get("test_entity.that_does_not_exist").state == new_state +async def test_api_state_change_with_bad_entity_id( + hass: HomeAssistant, mock_api_client: TestClient +) -> None: + """Test if API sends appropriate error if we omit state.""" + resp = await mock_api_client.post( + "/api/states/bad.entity.id", json={"state": "new_state"} + ) + + assert resp.status == HTTPStatus.BAD_REQUEST + + +async def test_api_state_change_with_bad_state( + hass: HomeAssistant, mock_api_client: TestClient +) -> None: + """Test if API sends appropriate error if we omit state.""" + resp = await mock_api_client.post( + "/api/states/test.test", json={"state": "x" * 256} + ) + + assert resp.status == HTTPStatus.BAD_REQUEST + + async def test_api_state_change_with_bad_data( hass: HomeAssistant, mock_api_client: TestClient ) -> None: From 0b4fedccff561712c1eb36631901101487814bcb Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 7 Sep 2023 12:45:47 +0200 Subject: [PATCH 101/202] Use correct config entry id in Livisi (#99812) --- homeassistant/components/livisi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/livisi/__init__.py b/homeassistant/components/livisi/__init__.py index b0387c6dcc9..e638c84a917 100644 --- a/homeassistant/components/livisi/__init__.py +++ b/homeassistant/components/livisi/__init__.py @@ -33,7 +33,7 @@ async def async_setup_entry(hass: core.HomeAssistant, entry: ConfigEntry) -> boo hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator device_registry = dr.async_get(hass) device_registry.async_get_or_create( - config_entry_id=coordinator.serial_number, + config_entry_id=entry.entry_id, identifiers={(DOMAIN, entry.entry_id)}, manufacturer="Livisi", name=f"SHC {coordinator.controller_type} {coordinator.serial_number}", From 7dc7060825c9d350e002cd942e2a60ee1a6a1cd3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 8 Sep 2023 06:32:21 -0500 Subject: [PATCH 102/202] Fix missing name and identifiers for ELKM1 connected devices (#99828) --- homeassistant/components/elkm1/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 352c8419106..14046b7079b 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -518,6 +518,8 @@ class ElkEntity(Entity): def device_info(self) -> DeviceInfo: """Device info connecting via the ElkM1 system.""" return DeviceInfo( + name=self._element.name, + identifiers={(DOMAIN, self._unique_id)}, via_device=(DOMAIN, f"{self._prefix}_system"), ) From d50f9f4e514c781039f9109306447fd232fbce72 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 8 Sep 2023 00:32:15 +0200 Subject: [PATCH 103/202] Bump aiovodafone to 0.1.0 (#99851) * bump aiovodafone to 0.1.0 * fix tests --- homeassistant/components/vodafone_station/coordinator.py | 4 ++-- homeassistant/components/vodafone_station/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/vodafone_station/test_config_flow.py | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/vodafone_station/coordinator.py b/homeassistant/components/vodafone_station/coordinator.py index b79acac9ce9..58079180bf8 100644 --- a/homeassistant/components/vodafone_station/coordinator.py +++ b/homeassistant/components/vodafone_station/coordinator.py @@ -112,9 +112,9 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]): dev_info, utc_point_in_time ), ) - for dev_info in (await self.api.get_all_devices()).values() + for dev_info in (await self.api.get_devices_data()).values() } - data_sensors = await self.api.get_user_data() + data_sensors = await self.api.get_sensor_data() await self.api.logout() return UpdateCoordinatorDataType(data_devices, data_sensors) diff --git a/homeassistant/components/vodafone_station/manifest.json b/homeassistant/components/vodafone_station/manifest.json index 7069629ca2e..5470cdd684c 100644 --- a/homeassistant/components/vodafone_station/manifest.json +++ b/homeassistant/components/vodafone_station/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/vodafone_station", "iot_class": "local_polling", "loggers": ["aiovodafone"], - "requirements": ["aiovodafone==0.0.6"] + "requirements": ["aiovodafone==0.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7086fb20ec2..026caea7f8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -369,7 +369,7 @@ aiounifi==61 aiovlc==0.1.0 # homeassistant.components.vodafone_station -aiovodafone==0.0.6 +aiovodafone==0.1.0 # homeassistant.components.waqi aiowaqi==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f98821badb..6953dfd0de5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -344,7 +344,7 @@ aiounifi==61 aiovlc==0.1.0 # homeassistant.components.vodafone_station -aiovodafone==0.0.6 +aiovodafone==0.1.0 # homeassistant.components.watttime aiowatttime==0.1.1 diff --git a/tests/components/vodafone_station/test_config_flow.py b/tests/components/vodafone_station/test_config_flow.py index 03a1198288d..3d2ef0cf568 100644 --- a/tests/components/vodafone_station/test_config_flow.py +++ b/tests/components/vodafone_station/test_config_flow.py @@ -78,7 +78,7 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> # Should be recoverable after hits error with patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.get_all_devices", + "homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.get_devices_data", return_value={ "wifi_user": "on|laptop|device-1|xx:xx:xx:xx:xx:xx|192.168.100.1||2.4G", "ethernet": "laptop|device-2|yy:yy:yy:yy:yy:yy|192.168.100.2|;", @@ -191,7 +191,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> # Should be recoverable after hits error with patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.get_all_devices", + "homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.get_devices_data", return_value={ "wifi_user": "on|laptop|device-1|xx:xx:xx:xx:xx:xx|192.168.100.1||2.4G", "ethernet": "laptop|device-2|yy:yy:yy:yy:yy:yy|192.168.100.2|;", From cadf56c98983452877268aa72f73d70b550ecf34 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 6 Sep 2023 13:21:21 -0500 Subject: [PATCH 104/202] Bump dbus-fast to 1.95.0 (#99749) --- 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 e1a5ee41324..bcb371971a6 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -19,6 +19,6 @@ "bluetooth-adapters==0.16.0", "bluetooth-auto-recovery==1.2.1", "bluetooth-data-tools==1.11.0", - "dbus-fast==1.94.1" + "dbus-fast==1.95.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7c48166172f..1e18d99a890 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ bluetooth-data-tools==1.11.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.3 -dbus-fast==1.94.1 +dbus-fast==1.95.0 fnv-hash-fast==0.4.1 ha-av==10.1.1 hass-nabucasa==0.70.0 diff --git a/requirements_all.txt b/requirements_all.txt index 026caea7f8e..9928c2e3ebe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -641,7 +641,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.94.1 +dbus-fast==1.95.0 # homeassistant.components.debugpy debugpy==1.6.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6953dfd0de5..8fd79ba6347 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -521,7 +521,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.94.1 +dbus-fast==1.95.0 # homeassistant.components.debugpy debugpy==1.6.7 From 02b6bbdcc696805f794d70452e2fa6f2427addfb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Sep 2023 13:27:29 -0500 Subject: [PATCH 105/202] Bump dbus-fast to 1.95.2 (#99852) --- 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 bcb371971a6..4231e03c2ef 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -19,6 +19,6 @@ "bluetooth-adapters==0.16.0", "bluetooth-auto-recovery==1.2.1", "bluetooth-data-tools==1.11.0", - "dbus-fast==1.95.0" + "dbus-fast==1.95.2" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1e18d99a890..25e9c20c0d8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ bluetooth-data-tools==1.11.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.3 -dbus-fast==1.95.0 +dbus-fast==1.95.2 fnv-hash-fast==0.4.1 ha-av==10.1.1 hass-nabucasa==0.70.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9928c2e3ebe..867d7fa74d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -641,7 +641,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.95.0 +dbus-fast==1.95.2 # homeassistant.components.debugpy debugpy==1.6.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8fd79ba6347..514385544b2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -521,7 +521,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.95.0 +dbus-fast==1.95.2 # homeassistant.components.debugpy debugpy==1.6.7 From dccccda5025b596488be0eb79442551a45e93e2b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 7 Sep 2023 13:22:24 -0400 Subject: [PATCH 106/202] Bump ZHA dependencies (#99855) --- 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 7352487a318..cce223fac11 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.36.2", + "bellows==0.36.3", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.103", diff --git a/requirements_all.txt b/requirements_all.txt index 867d7fa74d0..b3e74520957 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -509,7 +509,7 @@ beautifulsoup4==4.12.2 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.36.2 +bellows==0.36.3 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 514385544b2..5552d6fe132 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -430,7 +430,7 @@ base36==0.1.1 beautifulsoup4==4.12.2 # homeassistant.components.zha -bellows==0.36.2 +bellows==0.36.3 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.0 From 110a17ad6d74c26e01efb21d277cf3eb4f077072 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 7 Sep 2023 21:49:03 +0200 Subject: [PATCH 107/202] Fix NOAA tides warnings (#99856) --- homeassistant/components/noaa_tides/sensor.py | 49 ++++++++++++++----- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py index 7f3260c7635..a83f18fd6ca 100644 --- a/homeassistant/components/noaa_tides/sensor.py +++ b/homeassistant/components/noaa_tides/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import datetime, timedelta import logging +from typing import TYPE_CHECKING, Any, Literal, TypedDict import noaa_coops as coops import requests @@ -17,6 +18,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.unit_system import METRIC_SYSTEM +if TYPE_CHECKING: + from pandas import Timestamp + _LOGGER = logging.getLogger(__name__) CONF_STATION_ID = "station_id" @@ -76,40 +80,56 @@ def setup_platform( add_entities([noaa_sensor], True) +class NOAATidesData(TypedDict): + """Representation of a single tide.""" + + time_stamp: list[Timestamp] + hi_lo: list[Literal["L"] | Literal["H"]] + predicted_wl: list[float] + + class NOAATidesAndCurrentsSensor(SensorEntity): """Representation of a NOAA Tides and Currents sensor.""" _attr_attribution = "Data provided by NOAA" - def __init__(self, name, station_id, timezone, unit_system, station): + def __init__(self, name, station_id, timezone, unit_system, station) -> None: """Initialize the sensor.""" self._name = name self._station_id = station_id self._timezone = timezone self._unit_system = unit_system self._station = station - self.data = None + self.data: NOAATidesData | None = None @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self._name @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of this device.""" - attr = {} + attr: dict[str, Any] = {} if self.data is None: return attr if self.data["hi_lo"][1] == "H": - attr["high_tide_time"] = self.data.index[1].strftime("%Y-%m-%dT%H:%M") + attr["high_tide_time"] = self.data["time_stamp"][1].strftime( + "%Y-%m-%dT%H:%M" + ) attr["high_tide_height"] = self.data["predicted_wl"][1] - attr["low_tide_time"] = self.data.index[2].strftime("%Y-%m-%dT%H:%M") + attr["low_tide_time"] = self.data["time_stamp"][2].strftime( + "%Y-%m-%dT%H:%M" + ) attr["low_tide_height"] = self.data["predicted_wl"][2] elif self.data["hi_lo"][1] == "L": - attr["low_tide_time"] = self.data.index[1].strftime("%Y-%m-%dT%H:%M") + attr["low_tide_time"] = self.data["time_stamp"][1].strftime( + "%Y-%m-%dT%H:%M" + ) attr["low_tide_height"] = self.data["predicted_wl"][1] - attr["high_tide_time"] = self.data.index[2].strftime("%Y-%m-%dT%H:%M") + attr["high_tide_time"] = self.data["time_stamp"][2].strftime( + "%Y-%m-%dT%H:%M" + ) attr["high_tide_height"] = self.data["predicted_wl"][2] return attr @@ -118,7 +138,7 @@ class NOAATidesAndCurrentsSensor(SensorEntity): """Return the state of the device.""" if self.data is None: return None - api_time = self.data.index[0] + api_time = self.data["time_stamp"][0] if self.data["hi_lo"][0] == "H": tidetime = api_time.strftime("%-I:%M %p") return f"High tide at {tidetime}" @@ -142,8 +162,13 @@ class NOAATidesAndCurrentsSensor(SensorEntity): units=self._unit_system, time_zone=self._timezone, ) - self.data = df_predictions.head() - _LOGGER.debug("Data = %s", self.data) + api_data = df_predictions.head() + self.data = NOAATidesData( + time_stamp=list(api_data.index), + hi_lo=list(api_data["hi_lo"].values), + predicted_wl=list(api_data["predicted_wl"].values), + ) + _LOGGER.debug("Data = %s", api_data) _LOGGER.debug( "Recent Tide data queried with start time set to %s", begin.strftime("%m-%d-%Y %H:%M"), From 330e527560927e360c0c9c5f032b250e47820c97 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 8 Sep 2023 06:25:25 -0500 Subject: [PATCH 108/202] Upgrade bluetooth deps to fix timeout behavior on py3.11 (#99879) --- homeassistant/components/bluetooth/manifest.json | 6 +++--- homeassistant/package_constraints.txt | 6 +++--- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 4231e03c2ef..a3c40f739aa 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -15,9 +15,9 @@ "quality_scale": "internal", "requirements": [ "bleak==0.21.0", - "bleak-retry-connector==3.1.2", - "bluetooth-adapters==0.16.0", - "bluetooth-auto-recovery==1.2.1", + "bleak-retry-connector==3.1.3", + "bluetooth-adapters==0.16.1", + "bluetooth-auto-recovery==1.2.2", "bluetooth-data-tools==1.11.0", "dbus-fast==1.95.2" ] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 25e9c20c0d8..27818898990 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,10 +8,10 @@ atomicwrites-homeassistant==1.4.1 attrs==23.1.0 awesomeversion==22.9.0 bcrypt==4.0.1 -bleak-retry-connector==3.1.2 +bleak-retry-connector==3.1.3 bleak==0.21.0 -bluetooth-adapters==0.16.0 -bluetooth-auto-recovery==1.2.1 +bluetooth-adapters==0.16.1 +bluetooth-auto-recovery==1.2.2 bluetooth-data-tools==1.11.0 certifi>=2021.5.30 ciso8601==2.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index b3e74520957..51232296a15 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -518,7 +518,7 @@ bimmer-connected==0.14.0 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==3.1.2 +bleak-retry-connector==3.1.3 # homeassistant.components.bluetooth bleak==0.21.0 @@ -540,10 +540,10 @@ bluemaestro-ble==0.2.3 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.16.0 +bluetooth-adapters==0.16.1 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.2.1 +bluetooth-auto-recovery==1.2.2 # homeassistant.components.bluetooth # homeassistant.components.esphome diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5552d6fe132..5f54e05315a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -436,7 +436,7 @@ bellows==0.36.3 bimmer-connected==0.14.0 # homeassistant.components.bluetooth -bleak-retry-connector==3.1.2 +bleak-retry-connector==3.1.3 # homeassistant.components.bluetooth bleak==0.21.0 @@ -451,10 +451,10 @@ blinkpy==0.21.0 bluemaestro-ble==0.2.3 # homeassistant.components.bluetooth -bluetooth-adapters==0.16.0 +bluetooth-adapters==0.16.1 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.2.1 +bluetooth-auto-recovery==1.2.2 # homeassistant.components.bluetooth # homeassistant.components.esphome From fed061f69cf36ec2a46c1b56429d1bf4f229aac6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 8 Sep 2023 18:59:08 +0200 Subject: [PATCH 109/202] Update frontend to 20230908.0 (#99939) --- 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 50c557eae89..58de25fc03d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230906.1"] + "requirements": ["home-assistant-frontend==20230908.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 27818898990..e1aeaeb5c20 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.70.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 -home-assistant-frontend==20230906.1 +home-assistant-frontend==20230908.0 home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 51232296a15..598f3bf396b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -994,7 +994,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230906.1 +home-assistant-frontend==20230908.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5f54e05315a..f9a62ee593b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230906.1 +home-assistant-frontend==20230908.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 From 50c34ab41981f1569735482d6102b0c86a763bb1 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 30 Aug 2023 20:26:13 +0200 Subject: [PATCH 110/202] Bump pymodbus v3.5.0 (#99343) Bump pymodbus v3.5.0. --- homeassistant/components/modbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index d0d573227d8..a4187de77eb 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_polling", "loggers": ["pymodbus"], "quality_scale": "gold", - "requirements": ["pymodbus==3.4.1"] + "requirements": ["pymodbus==3.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 598f3bf396b..e50cdb37af3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1851,7 +1851,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.4.1 +pymodbus==3.5.0 # homeassistant.components.monoprice pymonoprice==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f9a62ee593b..b7b227fc1c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1370,7 +1370,7 @@ pymeteoclimatic==0.0.6 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.4.1 +pymodbus==3.5.0 # homeassistant.components.monoprice pymonoprice==0.4 From 0e4bf58736f2e891d401d65e0ef9721be1ac9b72 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 8 Sep 2023 19:20:06 +0200 Subject: [PATCH 111/202] Bump pymodbus v.3.5.1 (#99940) --- homeassistant/components/modbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index a4187de77eb..bef85f1d20d 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_polling", "loggers": ["pymodbus"], "quality_scale": "gold", - "requirements": ["pymodbus==3.5.0"] + "requirements": ["pymodbus==3.5.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index e50cdb37af3..6fd67a64038 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1851,7 +1851,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.5.0 +pymodbus==3.5.1 # homeassistant.components.monoprice pymonoprice==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b7b227fc1c0..cc9eeaa2b77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1370,7 +1370,7 @@ pymeteoclimatic==0.0.6 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.5.0 +pymodbus==3.5.1 # homeassistant.components.monoprice pymonoprice==0.4 From f1bae7d37bbaea8afa4edc902b2535ec267ad021 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 8 Sep 2023 19:08:32 +0200 Subject: [PATCH 112/202] Bump pyenphase to v1.11.0 (#99941) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index d3a36b16b60..c6d127a3f6e 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.9.3"], + "requirements": ["pyenphase==1.11.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 6fd67a64038..b7789648b8f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1671,7 +1671,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.9.3 +pyenphase==1.11.0 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc9eeaa2b77..b68ea0b6144 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1235,7 +1235,7 @@ pyeconet==0.1.20 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.9.3 +pyenphase==1.11.0 # homeassistant.components.everlights pyeverlights==0.1.0 From 06109d22fd16aac13ae39256b52eafc7133de0ae Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 8 Sep 2023 19:10:17 +0200 Subject: [PATCH 113/202] Fix key error MQTT binary_sensor when no name is set (#99943) Log entitty ID when instead of name --- homeassistant/components/mqtt/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 0d4b2c4a7b4..83bca91f4e1 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -215,7 +215,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): "Empty template output for entity: %s with state topic: %s." " Payload: '%s', with value template '%s'" ), - self._config[CONF_NAME], + self.entity_id, self._config[CONF_STATE_TOPIC], msg.payload, self._config.get(CONF_VALUE_TEMPLATE), @@ -240,7 +240,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): "No matching payload found for entity: %s with state topic: %s." " Payload: '%s'%s" ), - self._config[CONF_NAME], + self.entity_id, self._config[CONF_STATE_TOPIC], msg.payload, template_info, From 55533350d2a76e890b95a98de59eac9ec562b093 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Sep 2023 04:16:26 -0500 Subject: [PATCH 114/202] Bump zeroconf to 0.93.1 (#99516) * Bump zeroconf to 0.92.0 changelog: https://github.com/python-zeroconf/python-zeroconf/compare/0.91.1...0.92.0 * drop unused argument * Update tests/components/thread/test_diagnostics.py * lint * again * bump again since actions failed to release the wheels --- .../components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/thread/test_diagnostics.py | 44 +++++++++---------- 5 files changed, 25 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 26577bd0bbe..718f3047a07 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.91.1"] + "requirements": ["zeroconf==0.93.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e1aeaeb5c20..726227381df 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.91.1 +zeroconf==0.93.1 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index b7789648b8f..1ed53d0b136 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2766,7 +2766,7 @@ zamg==0.3.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.91.1 +zeroconf==0.93.1 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b68ea0b6144..a4e6630b76c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2036,7 +2036,7 @@ youtubeaio==1.1.5 zamg==0.3.0 # homeassistant.components.zeroconf -zeroconf==0.91.1 +zeroconf==0.93.1 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/tests/components/thread/test_diagnostics.py b/tests/components/thread/test_diagnostics.py index 94ca4373715..15ab0750316 100644 --- a/tests/components/thread/test_diagnostics.py +++ b/tests/components/thread/test_diagnostics.py @@ -1,7 +1,6 @@ """Test the thread websocket API.""" import dataclasses -import time from unittest.mock import Mock, patch import pytest @@ -191,50 +190,49 @@ async def test_diagnostics( """Test diagnostics for thread routers.""" cache = mock_async_zeroconf.zeroconf.cache = DNSCache() - now = time.monotonic() * 1000 cache.async_add_records( [ - *TEST_ZEROCONF_RECORD_1.dns_addresses(created=now), - TEST_ZEROCONF_RECORD_1.dns_service(created=now), - TEST_ZEROCONF_RECORD_1.dns_text(created=now), - TEST_ZEROCONF_RECORD_1.dns_pointer(created=now), + *TEST_ZEROCONF_RECORD_1.dns_addresses(), + TEST_ZEROCONF_RECORD_1.dns_service(), + TEST_ZEROCONF_RECORD_1.dns_text(), + TEST_ZEROCONF_RECORD_1.dns_pointer(), ] ) cache.async_add_records( [ - *TEST_ZEROCONF_RECORD_2.dns_addresses(created=now), - TEST_ZEROCONF_RECORD_2.dns_service(created=now), - TEST_ZEROCONF_RECORD_2.dns_text(created=now), - TEST_ZEROCONF_RECORD_2.dns_pointer(created=now), + *TEST_ZEROCONF_RECORD_2.dns_addresses(), + TEST_ZEROCONF_RECORD_2.dns_service(), + TEST_ZEROCONF_RECORD_2.dns_text(), + TEST_ZEROCONF_RECORD_2.dns_pointer(), ] ) # Test for invalid cache - cache.async_add_records([TEST_ZEROCONF_RECORD_3.dns_pointer(created=now)]) + cache.async_add_records([TEST_ZEROCONF_RECORD_3.dns_pointer()]) # Test for invalid record cache.async_add_records( [ - *TEST_ZEROCONF_RECORD_4.dns_addresses(created=now), - TEST_ZEROCONF_RECORD_4.dns_service(created=now), - TEST_ZEROCONF_RECORD_4.dns_text(created=now), - TEST_ZEROCONF_RECORD_4.dns_pointer(created=now), + *TEST_ZEROCONF_RECORD_4.dns_addresses(), + TEST_ZEROCONF_RECORD_4.dns_service(), + TEST_ZEROCONF_RECORD_4.dns_text(), + TEST_ZEROCONF_RECORD_4.dns_pointer(), ] ) # Test for record without xa cache.async_add_records( [ - *TEST_ZEROCONF_RECORD_5.dns_addresses(created=now), - TEST_ZEROCONF_RECORD_5.dns_service(created=now), - TEST_ZEROCONF_RECORD_5.dns_text(created=now), - TEST_ZEROCONF_RECORD_5.dns_pointer(created=now), + *TEST_ZEROCONF_RECORD_5.dns_addresses(), + TEST_ZEROCONF_RECORD_5.dns_service(), + TEST_ZEROCONF_RECORD_5.dns_text(), + TEST_ZEROCONF_RECORD_5.dns_pointer(), ] ) # Test for record without xp cache.async_add_records( [ - *TEST_ZEROCONF_RECORD_6.dns_addresses(created=now), - TEST_ZEROCONF_RECORD_6.dns_service(created=now), - TEST_ZEROCONF_RECORD_6.dns_text(created=now), - TEST_ZEROCONF_RECORD_6.dns_pointer(created=now), + *TEST_ZEROCONF_RECORD_6.dns_addresses(), + TEST_ZEROCONF_RECORD_6.dns_service(), + TEST_ZEROCONF_RECORD_6.dns_text(), + TEST_ZEROCONF_RECORD_6.dns_pointer(), ] ) assert await async_setup_component(hass, DOMAIN, {}) From c1447d5ce47188fedb8649199e5f83bfd8729e4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Sep 2023 17:06:21 -0500 Subject: [PATCH 115/202] Bump zeroconf to 0.96.0 (#99549) --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 718f3047a07..53b0dd5f5b5 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.93.1"] + "requirements": ["zeroconf==0.96.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 726227381df..2b1e73a5900 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.93.1 +zeroconf==0.96.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 1ed53d0b136..19a447d43a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2766,7 +2766,7 @@ zamg==0.3.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.93.1 +zeroconf==0.96.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4e6630b76c..615a305e50a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2036,7 +2036,7 @@ youtubeaio==1.1.5 zamg==0.3.0 # homeassistant.components.zeroconf -zeroconf==0.93.1 +zeroconf==0.96.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From 7ded08464f7074f30ec4e0b0066ca5147ffa9d22 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Sep 2023 02:52:21 -0500 Subject: [PATCH 116/202] Bump zeroconf to 0.97.0 (#99554) changelog: https://github.com/python-zeroconf/python-zeroconf/compare/0.96.0...0.97.0 --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 53b0dd5f5b5..4969b2a5a65 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.96.0"] + "requirements": ["zeroconf==0.97.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2b1e73a5900..84c6eba445f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.96.0 +zeroconf==0.97.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 19a447d43a3..8da4c2d662c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2766,7 +2766,7 @@ zamg==0.3.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.96.0 +zeroconf==0.97.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 615a305e50a..45e04e054b1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2036,7 +2036,7 @@ youtubeaio==1.1.5 zamg==0.3.0 # homeassistant.components.zeroconf -zeroconf==0.96.0 +zeroconf==0.97.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From 19c2bbcd939e966350d1586cb7df49fba9790031 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 6 Sep 2023 12:37:42 -0500 Subject: [PATCH 117/202] Bump zeroconf to 0.98.0 (#99748) --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 4969b2a5a65..117744a2775 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.97.0"] + "requirements": ["zeroconf==0.98.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 84c6eba445f..d03dc72863d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.97.0 +zeroconf==0.98.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 8da4c2d662c..cc51a8e0497 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2766,7 +2766,7 @@ zamg==0.3.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.97.0 +zeroconf==0.98.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45e04e054b1..07c7c2a7ba7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2036,7 +2036,7 @@ youtubeaio==1.1.5 zamg==0.3.0 # homeassistant.components.zeroconf -zeroconf==0.97.0 +zeroconf==0.98.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From f753f5af6ee96dbb5b59338605580590b462ace9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 8 Sep 2023 21:02:06 +0200 Subject: [PATCH 118/202] Make WS command render_template not give up if initial render raises (#99808) --- .../components/websocket_api/commands.py | 6 +- homeassistant/helpers/event.py | 11 +- .../components/websocket_api/test_commands.py | 317 +++++++++++++++--- tests/helpers/test_event.py | 21 -- 4 files changed, 281 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index a05f2aa8e3f..ea21b7b5eba 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -542,9 +542,8 @@ async def handle_render_template( timed_out = await template_obj.async_render_will_timeout( timeout, variables, strict=msg["strict"], log_fn=log_fn ) - except TemplateError as ex: - connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex)) - return + except TemplateError: + timed_out = False if timed_out: connection.send_error( @@ -583,7 +582,6 @@ async def handle_render_template( hass, [TrackTemplate(template_obj, variables)], _template_listener, - raise_on_template_error=True, strict=msg["strict"], log_fn=log_fn, ) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 51a8f1f1982..40364b7b367 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -917,7 +917,6 @@ class TrackTemplateResultInfo: def async_setup( self, - raise_on_template_error: bool, strict: bool = False, log_fn: Callable[[int, str], None] | None = None, ) -> None: @@ -955,8 +954,6 @@ class TrackTemplateResultInfo: ) if info.exception: - if raise_on_template_error: - raise info.exception if not log_fn: _LOGGER.error( "Error while processing template: %s", @@ -1239,7 +1236,6 @@ def async_track_template_result( hass: HomeAssistant, track_templates: Sequence[TrackTemplate], action: TrackTemplateResultListener, - raise_on_template_error: bool = False, strict: bool = False, log_fn: Callable[[int, str], None] | None = None, has_super_template: bool = False, @@ -1266,11 +1262,6 @@ def async_track_template_result( An iterable of TrackTemplate. action Callable to call with results. - raise_on_template_error - When set to True, if there is an exception - processing the template during setup, the system - will raise the exception instead of setting up - tracking. strict When set to True, raise on undefined variables. log_fn @@ -1286,7 +1277,7 @@ def async_track_template_result( """ tracker = TrackTemplateResultInfo(hass, track_templates, action, has_super_template) - tracker.async_setup(raise_on_template_error, strict=strict, log_fn=log_fn) + tracker.async_setup(strict=strict, log_fn=log_fn) return tracker diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 70f08477a72..b1b2027c65d 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1234,27 +1234,27 @@ EMPTY_LISTENERS = {"all": False, "entities": [], "domains": [], "time": False} ERR_MSG = {"type": "result", "success": False} -VARIABLE_ERROR_UNDEFINED_FUNC = { +EVENT_UNDEFINED_FUNC_1 = { "error": "'my_unknown_func' is undefined", "level": "ERROR", } -TEMPLATE_ERROR_UNDEFINED_FUNC = { - "code": "template_error", - "message": "UndefinedError: 'my_unknown_func' is undefined", +EVENT_UNDEFINED_FUNC_2 = { + "error": "UndefinedError: 'my_unknown_func' is undefined", + "level": "ERROR", } -VARIABLE_WARNING_UNDEFINED_VAR = { +EVENT_UNDEFINED_VAR_WARN = { "error": "'my_unknown_var' is undefined", "level": "WARNING", } -TEMPLATE_ERROR_UNDEFINED_VAR = { - "code": "template_error", - "message": "UndefinedError: 'my_unknown_var' is undefined", +EVENT_UNDEFINED_VAR_ERR = { + "error": "UndefinedError: 'my_unknown_var' is undefined", + "level": "ERROR", } -TEMPLATE_ERROR_UNDEFINED_FILTER = { - "code": "template_error", - "message": "TemplateAssertionError: No filter named 'unknown_filter'.", +EVENT_UNDEFINED_FILTER = { + "error": "TemplateAssertionError: No filter named 'unknown_filter'.", + "level": "ERROR", } @@ -1264,16 +1264,19 @@ TEMPLATE_ERROR_UNDEFINED_FILTER = { ( "{{ my_unknown_func() + 1 }}", [ - {"type": "event", "event": VARIABLE_ERROR_UNDEFINED_FUNC}, - ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC}, + {"type": "event", "event": EVENT_UNDEFINED_FUNC_1}, + {"type": "event", "event": EVENT_UNDEFINED_FUNC_2}, + {"type": "result", "success": True, "result": None}, + {"type": "event", "event": EVENT_UNDEFINED_FUNC_1}, + {"type": "event", "event": EVENT_UNDEFINED_FUNC_2}, ], ), ( "{{ my_unknown_var }}", [ - {"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR}, + {"type": "event", "event": EVENT_UNDEFINED_VAR_WARN}, {"type": "result", "success": True, "result": None}, - {"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR}, + {"type": "event", "event": EVENT_UNDEFINED_VAR_WARN}, { "type": "event", "event": {"result": "", "listeners": EMPTY_LISTENERS}, @@ -1282,11 +1285,19 @@ TEMPLATE_ERROR_UNDEFINED_FILTER = { ), ( "{{ my_unknown_var + 1 }}", - [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}], + [ + {"type": "event", "event": EVENT_UNDEFINED_VAR_ERR}, + {"type": "result", "success": True, "result": None}, + {"type": "event", "event": EVENT_UNDEFINED_VAR_ERR}, + ], ), ( "{{ now() | unknown_filter }}", - [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}], + [ + {"type": "event", "event": EVENT_UNDEFINED_FILTER}, + {"type": "result", "success": True, "result": None}, + {"type": "event", "event": EVENT_UNDEFINED_FILTER}, + ], ), ], ) @@ -1325,16 +1336,20 @@ async def test_render_template_with_error( ( "{{ my_unknown_func() + 1 }}", [ - {"type": "event", "event": VARIABLE_ERROR_UNDEFINED_FUNC}, - ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC}, + {"type": "event", "event": EVENT_UNDEFINED_FUNC_1}, + {"type": "event", "event": EVENT_UNDEFINED_FUNC_2}, + {"type": "result", "success": True, "result": None}, + {"type": "event", "event": EVENT_UNDEFINED_FUNC_1}, + {"type": "event", "event": EVENT_UNDEFINED_FUNC_2}, + {"type": "event", "event": EVENT_UNDEFINED_FUNC_1}, ], ), ( "{{ my_unknown_var }}", [ - {"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR}, + {"type": "event", "event": EVENT_UNDEFINED_VAR_WARN}, {"type": "result", "success": True, "result": None}, - {"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR}, + {"type": "event", "event": EVENT_UNDEFINED_VAR_WARN}, { "type": "event", "event": {"result": "", "listeners": EMPTY_LISTENERS}, @@ -1343,11 +1358,19 @@ async def test_render_template_with_error( ), ( "{{ my_unknown_var + 1 }}", - [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}], + [ + {"type": "event", "event": EVENT_UNDEFINED_VAR_ERR}, + {"type": "result", "success": True, "result": None}, + {"type": "event", "event": EVENT_UNDEFINED_VAR_ERR}, + ], ), ( "{{ now() | unknown_filter }}", - [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}], + [ + {"type": "event", "event": EVENT_UNDEFINED_FILTER}, + {"type": "result", "success": True, "result": None}, + {"type": "event", "event": EVENT_UNDEFINED_FILTER}, + ], ), ], ) @@ -1386,19 +1409,35 @@ async def test_render_template_with_timeout_and_error( [ ( "{{ my_unknown_func() + 1 }}", - [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC}], + [ + {"type": "event", "event": EVENT_UNDEFINED_FUNC_2}, + {"type": "result", "success": True, "result": None}, + {"type": "event", "event": EVENT_UNDEFINED_FUNC_2}, + ], ), ( "{{ my_unknown_var }}", - [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}], + [ + {"type": "event", "event": EVENT_UNDEFINED_VAR_ERR}, + {"type": "result", "success": True, "result": None}, + {"type": "event", "event": EVENT_UNDEFINED_VAR_ERR}, + ], ), ( "{{ my_unknown_var + 1 }}", - [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}], + [ + {"type": "event", "event": EVENT_UNDEFINED_VAR_ERR}, + {"type": "result", "success": True, "result": None}, + {"type": "event", "event": EVENT_UNDEFINED_VAR_ERR}, + ], ), ( "{{ now() | unknown_filter }}", - [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}], + [ + {"type": "event", "event": EVENT_UNDEFINED_FILTER}, + {"type": "result", "success": True, "result": None}, + {"type": "event", "event": EVENT_UNDEFINED_FILTER}, + ], ), ], ) @@ -1409,7 +1448,73 @@ async def test_render_template_strict_with_timeout_and_error( template: str, expected_events: list[dict[str, str]], ) -> None: - """Test a template with an error with a timeout.""" + """Test a template with an error with a timeout. + + In this test report_errors is enabled. + """ + caplog.set_level(logging.INFO) + await websocket_client.send_json( + { + "id": 5, + "type": "render_template", + "template": template, + "timeout": 5, + "strict": True, + "report_errors": True, + } + ) + + for expected_event in expected_events: + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + for key, value in expected_event.items(): + assert msg[key] == value + + assert "Template variable error" not in caplog.text + assert "Template variable warning" not in caplog.text + assert "TemplateError" not in caplog.text + + +@pytest.mark.parametrize( + ("template", "expected_events"), + [ + ( + "{{ my_unknown_func() + 1 }}", + [ + {"type": "result", "success": True, "result": None}, + ], + ), + ( + "{{ my_unknown_var }}", + [ + {"type": "result", "success": True, "result": None}, + ], + ), + ( + "{{ my_unknown_var + 1 }}", + [ + {"type": "result", "success": True, "result": None}, + ], + ), + ( + "{{ now() | unknown_filter }}", + [ + {"type": "result", "success": True, "result": None}, + ], + ), + ], +) +async def test_render_template_strict_with_timeout_and_error_2( + hass: HomeAssistant, + websocket_client, + caplog: pytest.LogCaptureFixture, + template: str, + expected_events: list[dict[str, str]], +) -> None: + """Test a template with an error with a timeout. + + In this test report_errors is disabled. + """ caplog.set_level(logging.INFO) await websocket_client.send_json( { @@ -1427,30 +1532,164 @@ async def test_render_template_strict_with_timeout_and_error( for key, value in expected_event.items(): assert msg[key] == value - assert "Template variable error" not in caplog.text - assert "Template variable warning" not in caplog.text - assert "TemplateError" not in caplog.text + assert "TemplateError" in caplog.text +@pytest.mark.parametrize( + ("template", "expected_events_1", "expected_events_2"), + [ + ( + "{{ now() | random }}", + [ + { + "type": "event", + "event": { + "error": "TypeError: object of type 'datetime.datetime' has no len()", + "level": "ERROR", + }, + }, + {"type": "result", "success": True, "result": None}, + { + "type": "event", + "event": { + "error": "TypeError: object of type 'datetime.datetime' has no len()", + "level": "ERROR", + }, + }, + ], + [], + ), + ( + "{{ float(states.sensor.foo.state) + 1 }}", + [ + { + "type": "event", + "event": { + "error": "UndefinedError: 'None' has no attribute 'state'", + "level": "ERROR", + }, + }, + {"type": "result", "success": True, "result": None}, + { + "type": "event", + "event": { + "error": "UndefinedError: 'None' has no attribute 'state'", + "level": "ERROR", + }, + }, + ], + [ + { + "type": "event", + "event": { + "result": 3.0, + "listeners": EMPTY_LISTENERS | {"entities": ["sensor.foo"]}, + }, + }, + ], + ), + ], +) async def test_render_template_error_in_template_code( - hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + websocket_client, + caplog: pytest.LogCaptureFixture, + template: str, + expected_events_1: list[dict[str, str]], + expected_events_2: list[dict[str, str]], ) -> None: - """Test a template that will throw in template.py.""" + """Test a template that will throw in template.py. + + In this test report_errors is enabled. + """ await websocket_client.send_json( - {"id": 5, "type": "render_template", "template": "{{ now() | random }}"} + { + "id": 5, + "type": "render_template", + "template": template, + "report_errors": True, + } ) - msg = await websocket_client.receive_json() - assert msg["id"] == 5 - assert msg["type"] == const.TYPE_RESULT - assert not msg["success"] - assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR + for expected_event in expected_events_1: + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + for key, value in expected_event.items(): + assert msg[key] == value + + hass.states.async_set("sensor.foo", "2") + + for expected_event in expected_events_2: + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + for key, value in expected_event.items(): + assert msg[key] == value assert "Template variable error" not in caplog.text assert "Template variable warning" not in caplog.text assert "TemplateError" not in caplog.text +@pytest.mark.parametrize( + ("template", "expected_events_1", "expected_events_2"), + [ + ( + "{{ now() | random }}", + [ + {"type": "result", "success": True, "result": None}, + ], + [], + ), + ( + "{{ float(states.sensor.foo.state) + 1 }}", + [ + {"type": "result", "success": True, "result": None}, + ], + [ + { + "type": "event", + "event": { + "result": 3.0, + "listeners": EMPTY_LISTENERS | {"entities": ["sensor.foo"]}, + }, + }, + ], + ), + ], +) +async def test_render_template_error_in_template_code_2( + hass: HomeAssistant, + websocket_client, + caplog: pytest.LogCaptureFixture, + template: str, + expected_events_1: list[dict[str, str]], + expected_events_2: list[dict[str, str]], +) -> None: + """Test a template that will throw in template.py. + + In this test report_errors is disabled. + """ + await websocket_client.send_json( + {"id": 5, "type": "render_template", "template": template} + ) + + for expected_event in expected_events_1: + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + for key, value in expected_event.items(): + assert msg[key] == value + + hass.states.async_set("sensor.foo", "2") + + for expected_event in expected_events_2: + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + for key, value in expected_event.items(): + assert msg[key] == value + + assert "TemplateError" in caplog.text + + async def test_render_template_with_delayed_error( hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture ) -> None: diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index dc06b9d94c8..00ad580693e 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -3239,27 +3239,6 @@ async def test_async_track_template_result_multiple_templates_mixing_domain( ] -async def test_async_track_template_result_raise_on_template_error( - hass: HomeAssistant, -) -> None: - """Test that we raise as soon as we encounter a failed template.""" - - with pytest.raises(TemplateError): - async_track_template_result( - hass, - [ - TrackTemplate( - Template( - "{{ states.switch | function_that_does_not_exist | list }}" - ), - None, - ), - ], - ha.callback(lambda event, updates: None), - raise_on_template_error=True, - ) - - async def test_track_template_with_time(hass: HomeAssistant) -> None: """Test tracking template with time.""" From ff393f6b86064a680b58de429ea187f92b37e527 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 8 Sep 2023 21:01:34 +0200 Subject: [PATCH 119/202] Bump hatasmota to 0.7.1 (#99818) --- homeassistant/components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_sensor.py | 10 ++++++++++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 220bc4e31fb..9843f64fc25 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["hatasmota"], "mqtt": ["tasmota/discovery/#"], - "requirements": ["HATasmota==0.7.0"] + "requirements": ["HATasmota==0.7.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index cc51a8e0497..bd1541b0821 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -29,7 +29,7 @@ DoorBirdPy==2.1.0 HAP-python==4.7.1 # homeassistant.components.tasmota -HATasmota==0.7.0 +HATasmota==0.7.1 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07c7c2a7ba7..623c45a5946 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -28,7 +28,7 @@ DoorBirdPy==2.1.0 HAP-python==4.7.1 # homeassistant.components.tasmota -HATasmota==0.7.0 +HATasmota==0.7.1 # homeassistant.components.doods # homeassistant.components.generic diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 4e79b8ad0d5..c14c7ffe53c 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -626,6 +626,16 @@ async def test_battery_sensor_state_via_mqtt( "unit_of_measurement": "%", } + # Test polled state update + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/STATUS11", + '{"StatusSTS":{"BatteryPercentage":50}}', + ) + await hass.async_block_till_done() + state = hass.states.get("sensor.tasmota_battery_level") + assert state.state == "50" + @pytest.mark.parametrize("status_sensor_disabled", [False]) async def test_single_shot_status_sensor_state_via_mqtt( From 6e952134e7d0f036aad5fc1e8c206e6995e964b0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 8 Sep 2023 21:03:27 +0200 Subject: [PATCH 120/202] Bumped version to 2023.9.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 cfdb5095128..cac54748211 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index e4403bd7c30..b74e7914fd7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.0" +version = "2023.9.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2dcf6a6f5bd07b6294aa0a84ea5161f49a4e50a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 5 Sep 2023 21:14:39 +0200 Subject: [PATCH 121/202] Bump millheater to 0.11.2 (#99683) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update Mill lib Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index b2dbf993dae..a4c824b3674 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mill", "iot_class": "local_polling", "loggers": ["mill", "mill_local"], - "requirements": ["millheater==0.11.1", "mill-local==0.2.0"] + "requirements": ["millheater==0.11.2", "mill-local==0.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index bd1541b0821..9a704bd2b25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1213,7 +1213,7 @@ micloud==0.5 mill-local==0.2.0 # homeassistant.components.mill -millheater==0.11.1 +millheater==0.11.2 # homeassistant.components.minio minio==7.1.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 623c45a5946..7beceb953a8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -927,7 +927,7 @@ micloud==0.5 mill-local==0.2.0 # homeassistant.components.mill -millheater==0.11.1 +millheater==0.11.2 # homeassistant.components.minio minio==7.1.12 From d5ff05bdf54a0acc78edfca2f1bd0c11163b64bc Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 12 Sep 2023 16:05:59 +0200 Subject: [PATCH 122/202] Remove modbus pragma no cover and solve nan (#99221) * Remove pragma no cover. * Ruff ! * Review comments. * update test. * Review. * review. * Add slave test. --- .../components/modbus/base_platform.py | 25 ++-- tests/components/modbus/test_sensor.py | 119 +++++++++++++++++- 2 files changed, 131 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 65cfa1b49ba..cb8cd1cdc75 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -188,10 +188,14 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): registers.reverse() return registers - def __process_raw_value(self, entry: float | int | str) -> float | int | str | None: + def __process_raw_value( + self, entry: float | int | str | bytes + ) -> float | int | str | bytes | None: """Process value from sensor with NaN handling, scaling, offset, min/max etc.""" if self._nan_value and entry in (self._nan_value, -self._nan_value): return None + if isinstance(entry, bytes): + return entry val: float | int = self._scale * entry + self._offset if self._min_value is not None and val < self._min_value: return self._min_value @@ -232,14 +236,20 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): if isinstance(v_temp, int) and self._precision == 0: v_result.append(str(v_temp)) elif v_temp is None: - v_result.append("") # pragma: no cover + v_result.append("0") elif v_temp != v_temp: # noqa: PLR0124 # NaN float detection replace with None - v_result.append("nan") # pragma: no cover + v_result.append("0") else: v_result.append(f"{float(v_temp):.{self._precision}f}") return ",".join(map(str, v_result)) + # NaN float detection replace with None + if val[0] != val[0]: # noqa: PLR0124 + return None + if byte_string == b"nan\x00": + return None + # Apply scale, precision, limits to floats and ints val_result = self.__process_raw_value(val[0]) @@ -249,15 +259,10 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): if val_result is None: return None - # NaN float detection replace with None - if val_result != val_result: # noqa: PLR0124 - return None # pragma: no cover if isinstance(val_result, int) and self._precision == 0: return str(val_result) - if isinstance(val_result, str): - if val_result == "nan": - val_result = None # pragma: no cover - return val_result + if isinstance(val_result, bytes): + return val_result.decode() return f"{float(val_result):.{self._precision}f}" diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index f72371ed42e..9cc55e5ab51 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -1,4 +1,6 @@ """The tests for the Modbus sensor component.""" +import struct + from freezegun.api import FrozenDateTimeFactory import pytest @@ -625,6 +627,21 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: @pytest.mark.parametrize( ("config_addon", "register_words", "do_exception", "expected"), [ + ( + { + CONF_SLAVE_COUNT: 1, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_DATA_TYPE: DataType.FLOAT32, + }, + [ + 0x5102, + 0x0304, + int.from_bytes(struct.pack(">f", float("nan"))[0:2]), + int.from_bytes(struct.pack(">f", float("nan"))[2:4]), + ], + False, + ["34899771392", "0"], + ), ( { CONF_SLAVE_COUNT: 0, @@ -902,6 +919,65 @@ async def test_wrong_unpack(hass: HomeAssistant, mock_do_cycle) -> None: assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_SCAN_INTERVAL: 1, + }, + ], + }, + ], +) +@pytest.mark.parametrize( + ("config_addon", "register_words", "expected"), + [ + ( + { + CONF_DATA_TYPE: DataType.FLOAT32, + }, + [ + int.from_bytes(struct.pack(">f", float("nan"))[0:2]), + int.from_bytes(struct.pack(">f", float("nan"))[2:4]), + ], + STATE_UNAVAILABLE, + ), + ( + { + CONF_DATA_TYPE: DataType.FLOAT32, + }, + [0x6E61, 0x6E00], + STATE_UNAVAILABLE, + ), + ( + { + CONF_DATA_TYPE: DataType.CUSTOM, + CONF_COUNT: 2, + CONF_STRUCTURE: "4s", + }, + [0x6E61, 0x6E00], + STATE_UNAVAILABLE, + ), + ( + { + CONF_DATA_TYPE: DataType.CUSTOM, + CONF_COUNT: 2, + CONF_STRUCTURE: "4s", + }, + [0x6161, 0x6100], + "aaa\x00", + ), + ], +) +async def test_unpack_ok(hass: HomeAssistant, mock_do_cycle, expected) -> None: + """Run test for sensor.""" + assert hass.states.get(ENTITY_ID).state == expected + + @pytest.mark.parametrize( "do_config", [ @@ -965,10 +1041,35 @@ async def test_lazy_error_sensor( CONF_DATA_TYPE: DataType.CUSTOM, CONF_STRUCTURE: ">4f", }, - # floats: 7.931250095367432, 10.600000381469727, + # floats: nan, 10.600000381469727, # 1.000879611487865e-28, 10.566553115844727 - [0x40FD, 0xCCCD, 0x4129, 0x999A, 0x10FD, 0xC0CD, 0x4129, 0x109A], - "7.93,10.60,0.00,10.57", + [ + int.from_bytes(struct.pack(">f", float("nan"))[0:2]), + int.from_bytes(struct.pack(">f", float("nan"))[2:4]), + 0x4129, + 0x999A, + 0x10FD, + 0xC0CD, + 0x4129, + 0x109A, + ], + "0,10.60,0.00,10.57", + ), + ( + { + CONF_COUNT: 4, + CONF_DATA_TYPE: DataType.CUSTOM, + CONF_STRUCTURE: ">2i", + CONF_NAN_VALUE: 0x0000000F, + }, + # int: nan, 10, + [ + 0x0000, + 0x000F, + 0x0000, + 0x000A, + ], + "0,10", ), ( { @@ -988,6 +1089,18 @@ async def test_lazy_error_sensor( [0x0101], "257", ), + ( + { + CONF_COUNT: 8, + CONF_PRECISION: 2, + CONF_DATA_TYPE: DataType.CUSTOM, + CONF_STRUCTURE: ">4f", + }, + # floats: 7.931250095367432, 10.600000381469727, + # 1.000879611487865e-28, 10.566553115844727 + [0x40FD, 0xCCCD, 0x4129, 0x999A, 0x10FD, 0xC0CD, 0x4129, 0x109A], + "7.93,10.60,0.00,10.57", + ), ], ) async def test_struct_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: From 367d893fc890f2701133c96193f384b0207b4590 Mon Sep 17 00:00:00 2001 From: Michael Arthur Date: Mon, 4 Sep 2023 01:53:23 +1200 Subject: [PATCH 123/202] Bugfix: Electric Kiwi reduce interval so oauth doesn't expire (#99489) decrease interval time as EK have broken/changed their oauth again --- homeassistant/components/electric_kiwi/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/electric_kiwi/coordinator.py b/homeassistant/components/electric_kiwi/coordinator.py index 49611f9febd..b084f4656d5 100644 --- a/homeassistant/components/electric_kiwi/coordinator.py +++ b/homeassistant/components/electric_kiwi/coordinator.py @@ -14,7 +14,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda _LOGGER = logging.getLogger(__name__) -HOP_SCAN_INTERVAL = timedelta(hours=2) +HOP_SCAN_INTERVAL = timedelta(minutes=20) class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]): From d399ebb8e143de9ecb7e8a59652cf0fbec47d255 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 3 Sep 2023 17:13:49 +0200 Subject: [PATCH 124/202] Read modbus data before scan_interval (#99243) Read before scan_interval. --- homeassistant/components/modbus/base_platform.py | 4 +--- tests/components/modbus/conftest.py | 2 +- tests/components/modbus/test_sensor.py | 5 +---- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index cb8cd1cdc75..0f5a10d3679 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -31,7 +31,6 @@ from homeassistant.helpers.event import async_call_later, async_track_time_inter from homeassistant.helpers.restore_state import RestoreEntity from .const import ( - ACTIVE_SCAN_INTERVAL, CALL_TYPE_COIL, CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_HOLDING, @@ -116,8 +115,7 @@ class BasePlatform(Entity): def async_run(self) -> None: """Remote start entity.""" self.async_hold(update=False) - if self._scan_interval == 0 or self._scan_interval > ACTIVE_SCAN_INTERVAL: - self._cancel_call = async_call_later(self.hass, 1, self.async_update) + self._cancel_call = async_call_later(self.hass, 1, self.async_update) if self._scan_interval > 0: self._cancel_timer = async_track_time_interval( self.hass, self.async_update, timedelta(seconds=self._scan_interval) diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 23d3ee522bb..d4c7dfa5e10 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -149,7 +149,7 @@ async def mock_do_cycle_fixture( mock_pymodbus_return, ) -> FrozenDateTimeFactory: """Trigger update call with time_changed event.""" - freezer.tick(timedelta(seconds=90)) + freezer.tick(timedelta(seconds=1)) async_fire_time_changed(hass) await hass.async_block_till_done() return freezer diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 9cc55e5ab51..c9507298ab2 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -269,7 +269,6 @@ async def test_config_wrong_struct_sensor( { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, - CONF_SCAN_INTERVAL: 1, }, ], }, @@ -727,7 +726,6 @@ async def test_slave_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> Non CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_SCAN_INTERVAL: 1, }, ], }, @@ -1011,7 +1009,7 @@ async def test_lazy_error_sensor( hass.states.async_set(ENTITY_ID, 17) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == start_expect - await do_next_cycle(hass, mock_do_cycle, 11) + await do_next_cycle(hass, mock_do_cycle, 5) assert hass.states.get(ENTITY_ID).state == start_expect await do_next_cycle(hass, mock_do_cycle, 11) assert hass.states.get(ENTITY_ID).state == end_expect @@ -1116,7 +1114,6 @@ async def test_struct_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> No { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 201, - CONF_SCAN_INTERVAL: 1, }, ], }, From 7235de1a0ccbd1f92639b46445501de428b09adb Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 12 Sep 2023 16:01:15 +0200 Subject: [PATCH 125/202] Make modbus retry fast on read errors (#99576) * Fast retry on read errors. * Review comments. --- homeassistant/components/modbus/base_platform.py | 4 +++- homeassistant/components/modbus/sensor.py | 7 ++++++- tests/components/modbus/test_sensor.py | 12 ++++-------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 0f5a10d3679..f1a48728814 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -115,7 +115,9 @@ class BasePlatform(Entity): def async_run(self) -> None: """Remote start entity.""" self.async_hold(update=False) - self._cancel_call = async_call_later(self.hass, 1, self.async_update) + self._cancel_call = async_call_later( + self.hass, timedelta(milliseconds=100), self.async_update + ) if self._scan_interval > 0: self._cancel_timer = async_track_time_interval( self.hass, self.async_update, timedelta(seconds=self._scan_interval) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index fe2d4bc415d..f2ed504b41b 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -1,7 +1,7 @@ """Support for Modbus Register sensors.""" from __future__ import annotations -from datetime import datetime +from datetime import datetime, timedelta import logging from typing import Any @@ -19,6 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -106,12 +107,16 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity): """Update the state of the sensor.""" # remark "now" is a dummy parameter to avoid problems with # async_track_time_interval + self._cancel_call = None raw_result = await self._hub.async_pb_call( self._slave, self._address, self._count, self._input_type ) if raw_result is None: if self._lazy_errors: self._lazy_errors -= 1 + self._cancel_call = async_call_later( + self.hass, timedelta(seconds=1), self.async_update + ) return self._lazy_errors = self._lazy_error_count self._attr_available = False diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index c9507298ab2..de390d126fe 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -992,27 +992,23 @@ async def test_unpack_ok(hass: HomeAssistant, mock_do_cycle, expected) -> None: ], ) @pytest.mark.parametrize( - ("register_words", "do_exception", "start_expect", "end_expect"), + ("register_words", "do_exception"), [ ( [0x8000], True, - "17", - STATE_UNAVAILABLE, ), ], ) async def test_lazy_error_sensor( - hass: HomeAssistant, mock_do_cycle: FrozenDateTimeFactory, start_expect, end_expect + hass: HomeAssistant, mock_do_cycle: FrozenDateTimeFactory ) -> None: """Run test for sensor.""" hass.states.async_set(ENTITY_ID, 17) await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == start_expect + assert hass.states.get(ENTITY_ID).state == "17" await do_next_cycle(hass, mock_do_cycle, 5) - assert hass.states.get(ENTITY_ID).state == start_expect - await do_next_cycle(hass, mock_do_cycle, 11) - assert hass.states.get(ENTITY_ID).state == end_expect + assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE @pytest.mark.parametrize( From 4c125fda9d1e3684029896557a37eb7e67499e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiit=20R=C3=A4tsep?= Date: Mon, 11 Sep 2023 12:15:46 +0300 Subject: [PATCH 126/202] Fix Soma cover tilt (#99717) --- homeassistant/components/soma/cover.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/soma/cover.py b/homeassistant/components/soma/cover.py index 26487756a44..4aa2559b140 100644 --- a/homeassistant/components/soma/cover.py +++ b/homeassistant/components/soma/cover.py @@ -51,6 +51,8 @@ class SomaTilt(SomaEntity, CoverEntity): | CoverEntityFeature.STOP_TILT | CoverEntityFeature.SET_TILT_POSITION ) + CLOSED_UP_THRESHOLD = 80 + CLOSED_DOWN_THRESHOLD = 20 @property def current_cover_tilt_position(self) -> int: @@ -60,7 +62,12 @@ class SomaTilt(SomaEntity, CoverEntity): @property def is_closed(self) -> bool: """Return if the cover tilt is closed.""" - return self.current_position == 0 + if ( + self.current_position < self.CLOSED_DOWN_THRESHOLD + or self.current_position > self.CLOSED_UP_THRESHOLD + ): + return True + return False def close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" From 8e6ec01bfb3e2baffe952a8e8840f0cb36eb5d99 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 11 Sep 2023 14:36:01 +0200 Subject: [PATCH 127/202] Cache device trigger info during ZHA startup (#99764) * Do not connect to the radio hardware within `_connect_zigpy_app` * Make `connect_zigpy_app` public * Create radio manager instances from config entries * Cache device triggers on startup * reorg zha init * don't reuse gateway * don't nuke yaml configuration * review comments * Fix existing unit tests * Ensure `app.shutdown` is called, not just `app.disconnect` * Revert creating group entities and device registry entries early * Add unit tests --------- Co-authored-by: David F. Mulcahey --- homeassistant/components/zha/__init__.py | 47 ++++++- homeassistant/components/zha/core/const.py | 1 + homeassistant/components/zha/core/device.py | 21 +-- homeassistant/components/zha/core/gateway.py | 48 +++---- .../components/zha/device_trigger.py | 91 ++++++------ homeassistant/components/zha/radio_manager.py | 33 +++-- .../homeassistant_hardware/conftest.py | 2 +- .../homeassistant_sky_connect/conftest.py | 2 +- .../homeassistant_sky_connect/test_init.py | 2 +- .../homeassistant_yellow/conftest.py | 2 +- tests/components/zha/conftest.py | 25 +++- tests/components/zha/test_config_flow.py | 7 - tests/components/zha/test_device_trigger.py | 130 ++++++++++++++++-- tests/components/zha/test_init.py | 16 +-- tests/components/zha/test_radio_manager.py | 6 +- 15 files changed, 299 insertions(+), 134 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index f9113ebaa90..662ddd080e0 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -1,5 +1,6 @@ """Support for Zigbee Home Automation devices.""" import asyncio +import contextlib import copy import logging import os @@ -33,13 +34,16 @@ from .core.const import ( CONF_ZIGPY, DATA_ZHA, DATA_ZHA_CONFIG, + DATA_ZHA_DEVICE_TRIGGER_CACHE, DATA_ZHA_GATEWAY, DOMAIN, PLATFORMS, SIGNAL_ADD_ENTITIES, RadioType, ) +from .core.device import get_device_automation_triggers from .core.discovery import GROUP_PROBE +from .radio_manager import ZhaRadioManager DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(CONF_TYPE): cv.string}) ZHA_CONFIG_SCHEMA = { @@ -134,9 +138,43 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b else: _LOGGER.debug("ZHA storage file does not exist or was already removed") - # Re-use the gateway object between ZHA reloads - if (zha_gateway := zha_data.get(DATA_ZHA_GATEWAY)) is None: - zha_gateway = ZHAGateway(hass, config, config_entry) + # Load and cache device trigger information early + zha_data.setdefault(DATA_ZHA_DEVICE_TRIGGER_CACHE, {}) + + device_registry = dr.async_get(hass) + radio_mgr = ZhaRadioManager.from_config_entry(hass, config_entry) + + async with radio_mgr.connect_zigpy_app() as app: + for dev in app.devices.values(): + dev_entry = device_registry.async_get_device( + identifiers={(DOMAIN, str(dev.ieee))}, + connections={(dr.CONNECTION_ZIGBEE, str(dev.ieee))}, + ) + + if dev_entry is None: + continue + + zha_data[DATA_ZHA_DEVICE_TRIGGER_CACHE][dev_entry.id] = ( + str(dev.ieee), + get_device_automation_triggers(dev), + ) + + _LOGGER.debug("Trigger cache: %s", zha_data[DATA_ZHA_DEVICE_TRIGGER_CACHE]) + + zha_gateway = ZHAGateway(hass, config, config_entry) + + async def async_zha_shutdown(): + """Handle shutdown tasks.""" + await zha_gateway.shutdown() + # clean up any remaining entity metadata + # (entities that have been discovered but not yet added to HA) + # suppress KeyError because we don't know what state we may + # be in when we get here in failure cases + with contextlib.suppress(KeyError): + for platform in PLATFORMS: + del hass.data[DATA_ZHA][platform] + + config_entry.async_on_unload(async_zha_shutdown) try: await zha_gateway.async_initialize() @@ -155,9 +193,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b repairs.async_delete_blocking_issues(hass) - config_entry.async_on_unload(zha_gateway.shutdown) - - device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(dr.CONNECTION_ZIGBEE, str(zha_gateway.coordinator_ieee))}, diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 63b59e9d8d4..9569fc49659 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -186,6 +186,7 @@ DATA_ZHA = "zha" DATA_ZHA_CONFIG = "config" DATA_ZHA_BRIDGE_ID = "zha_bridge_id" DATA_ZHA_CORE_EVENTS = "zha_core_events" +DATA_ZHA_DEVICE_TRIGGER_CACHE = "zha_device_trigger_cache" DATA_ZHA_GATEWAY = "zha_gateway" DEBUG_COMP_BELLOWS = "bellows" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 1455173b27c..60bf78e516c 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -93,6 +93,16 @@ _UPDATE_ALIVE_INTERVAL = (60, 90) _CHECKIN_GRACE_PERIODS = 2 +def get_device_automation_triggers( + device: zigpy.device.Device, +) -> dict[tuple[str, str], dict[str, str]]: + """Get the supported device automation triggers for a zigpy device.""" + return { + ("device_offline", "device_offline"): {"device_event_type": "device_offline"}, + **getattr(device, "device_automation_triggers", {}), + } + + class DeviceStatus(Enum): """Status of a device.""" @@ -311,16 +321,7 @@ class ZHADevice(LogMixin): @cached_property def device_automation_triggers(self) -> dict[tuple[str, str], dict[str, str]]: """Return the device automation triggers for this device.""" - triggers = { - ("device_offline", "device_offline"): { - "device_event_type": "device_offline" - } - } - - if hasattr(self._zigpy_device, "device_automation_triggers"): - triggers.update(self._zigpy_device.device_automation_triggers) - - return triggers + return get_device_automation_triggers(self._zigpy_device) @property def available_signal(self) -> str: diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 353bc6904d7..5cc2cd9a4b9 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -149,12 +149,6 @@ class ZHAGateway: self.config_entry = config_entry self._unsubs: list[Callable[[], None]] = [] - discovery.PROBE.initialize(self._hass) - discovery.GROUP_PROBE.initialize(self._hass) - - self.ha_device_registry = dr.async_get(self._hass) - self.ha_entity_registry = er.async_get(self._hass) - def get_application_controller_data(self) -> tuple[ControllerApplication, dict]: """Get an uninitialized instance of a zigpy `ControllerApplication`.""" radio_type = self.config_entry.data[CONF_RADIO_TYPE] @@ -197,6 +191,12 @@ class ZHAGateway: async def async_initialize(self) -> None: """Initialize controller and connect radio.""" + discovery.PROBE.initialize(self._hass) + discovery.GROUP_PROBE.initialize(self._hass) + + self.ha_device_registry = dr.async_get(self._hass) + self.ha_entity_registry = er.async_get(self._hass) + app_controller_cls, app_config = self.get_application_controller_data() self.application_controller = await app_controller_cls.new( config=app_config, @@ -204,23 +204,6 @@ class ZHAGateway: start_radio=False, ) - self._hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self - - self.async_load_devices() - - # Groups are attached to the coordinator device so we need to load it early - coordinator = self._find_coordinator_device() - loaded_groups = False - - # We can only load groups early if the coordinator's model info has been stored - # in the zigpy database - if coordinator.model is not None: - self.coordinator_zha_device = self._async_get_or_create_device( - coordinator, restored=True - ) - self.async_load_groups() - loaded_groups = True - for attempt in range(STARTUP_RETRIES): try: await self.application_controller.startup(auto_form=True) @@ -242,14 +225,15 @@ class ZHAGateway: else: break + self._hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self + self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(self.coordinator_ieee) + self.coordinator_zha_device = self._async_get_or_create_device( self._find_coordinator_device(), restored=True ) - self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(self.coordinator_ieee) - # If ZHA groups could not load early, we can safely load them now - if not loaded_groups: - self.async_load_groups() + self.async_load_devices() + self.async_load_groups() self.application_controller.add_listener(self) self.application_controller.groups.add_listener(self) @@ -766,7 +750,15 @@ class ZHAGateway: unsubscribe() for device in self.devices.values(): device.async_cleanup_handles() - await self.application_controller.shutdown() + # shutdown is called when the config entry unloads are processed + # there are cases where unloads are processed because of a failure of + # some sort and the application controller may not have been + # created yet + if ( + hasattr(self, "application_controller") + and self.application_controller is not None + ): + await self.application_controller.shutdown() def handle_message( self, diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 9e33e3fa615..7a479443377 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -9,12 +9,12 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant -from homeassistant.exceptions import HomeAssistantError, IntegrationError +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN as ZHA_DOMAIN -from .core.const import ZHA_EVENT +from .core.const import DATA_ZHA, DATA_ZHA_DEVICE_TRIGGER_CACHE, ZHA_EVENT from .core.helpers import async_get_zha_device CONF_SUBTYPE = "subtype" @@ -26,21 +26,32 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( ) +def _get_device_trigger_data(hass: HomeAssistant, device_id: str) -> tuple[str, dict]: + """Get device trigger data for a device, falling back to the cache if possible.""" + + # First, try checking to see if the device itself is accessible + try: + zha_device = async_get_zha_device(hass, device_id) + except KeyError: + pass + else: + return str(zha_device.ieee), zha_device.device_automation_triggers + + # If not, check the trigger cache but allow any `KeyError`s to propagate + return hass.data[DATA_ZHA][DATA_ZHA_DEVICE_TRIGGER_CACHE][device_id] + + async def async_validate_trigger_config( hass: HomeAssistant, config: ConfigType ) -> ConfigType: """Validate config.""" config = TRIGGER_SCHEMA(config) + # Trigger validation will not occur if the config entry is not loaded + _, triggers = _get_device_trigger_data(hass, config[CONF_DEVICE_ID]) + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) - try: - zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) - except (KeyError, AttributeError, IntegrationError) as err: - raise InvalidDeviceAutomationConfig from err - if ( - zha_device.device_automation_triggers is None - or trigger not in zha_device.device_automation_triggers - ): + if trigger not in triggers: raise InvalidDeviceAutomationConfig(f"device does not have trigger {trigger}") return config @@ -53,26 +64,26 @@ async def async_attach_trigger( trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger_key: tuple[str, str] = (config[CONF_TYPE], config[CONF_SUBTYPE]) + try: - zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) - except (KeyError, AttributeError) as err: + ieee, triggers = _get_device_trigger_data(hass, config[CONF_DEVICE_ID]) + except KeyError as err: raise HomeAssistantError( f"Unable to get zha device {config[CONF_DEVICE_ID]}" ) from err - if trigger_key not in zha_device.device_automation_triggers: + trigger_key: tuple[str, str] = (config[CONF_TYPE], config[CONF_SUBTYPE]) + + if trigger_key not in triggers: raise HomeAssistantError(f"Unable to find trigger {trigger_key}") - trigger = zha_device.device_automation_triggers[trigger_key] - - event_config = { - event_trigger.CONF_PLATFORM: "event", - event_trigger.CONF_EVENT_TYPE: ZHA_EVENT, - event_trigger.CONF_EVENT_DATA: {DEVICE_IEEE: str(zha_device.ieee), **trigger}, - } - - event_config = event_trigger.TRIGGER_SCHEMA(event_config) + event_config = event_trigger.TRIGGER_SCHEMA( + { + event_trigger.CONF_PLATFORM: "event", + event_trigger.CONF_EVENT_TYPE: ZHA_EVENT, + event_trigger.CONF_EVENT_DATA: {DEVICE_IEEE: ieee, **triggers[trigger_key]}, + } + ) return await event_trigger.async_attach_trigger( hass, event_config, action, trigger_info, platform_type="device" ) @@ -83,24 +94,20 @@ async def async_get_triggers( ) -> list[dict[str, str]]: """List device triggers. - Make sure the device supports device automations and - if it does return the trigger list. + Make sure the device supports device automations and return the trigger list. """ - zha_device = async_get_zha_device(hass, device_id) + try: + _, triggers = _get_device_trigger_data(hass, device_id) + except KeyError as err: + raise InvalidDeviceAutomationConfig from err - if not zha_device.device_automation_triggers: - return [] - - triggers = [] - for trigger, subtype in zha_device.device_automation_triggers: - triggers.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: ZHA_DOMAIN, - CONF_PLATFORM: DEVICE, - CONF_TYPE: trigger, - CONF_SUBTYPE: subtype, - } - ) - - return triggers + return [ + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: ZHA_DOMAIN, + CONF_PLATFORM: DEVICE, + CONF_TYPE: trigger, + CONF_SUBTYPE: subtype, + } + for trigger, subtype in triggers + ] diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index 751fea99847..df30a85cd7b 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -8,7 +8,7 @@ import copy import enum import logging import os -from typing import Any +from typing import Any, Self from bellows.config import CONF_USE_THREAD import voluptuous as vol @@ -127,8 +127,21 @@ class ZhaRadioManager: self.backups: list[zigpy.backups.NetworkBackup] = [] self.chosen_backup: zigpy.backups.NetworkBackup | None = None + @classmethod + def from_config_entry( + cls, hass: HomeAssistant, config_entry: config_entries.ConfigEntry + ) -> Self: + """Create an instance from a config entry.""" + mgr = cls() + mgr.hass = hass + mgr.device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] + mgr.device_settings = config_entry.data[CONF_DEVICE] + mgr.radio_type = RadioType[config_entry.data[CONF_RADIO_TYPE]] + + return mgr + @contextlib.asynccontextmanager - async def _connect_zigpy_app(self) -> ControllerApplication: + async def connect_zigpy_app(self) -> ControllerApplication: """Connect to the radio with the current config and then clean up.""" assert self.radio_type is not None @@ -155,10 +168,9 @@ class ZhaRadioManager: ) try: - await app.connect() yield app finally: - await app.disconnect() + await app.shutdown() await asyncio.sleep(CONNECT_DELAY_S) async def restore_backup( @@ -170,7 +182,8 @@ class ZhaRadioManager: ): return - async with self._connect_zigpy_app() as app: + async with self.connect_zigpy_app() as app: + await app.connect() await app.backups.restore_backup(backup, **kwargs) @staticmethod @@ -218,7 +231,9 @@ class ZhaRadioManager: """Connect to the radio and load its current network settings.""" backup = None - async with self._connect_zigpy_app() as app: + async with self.connect_zigpy_app() as app: + await app.connect() + # Check if the stick has any settings and load them try: await app.load_network_info() @@ -241,12 +256,14 @@ class ZhaRadioManager: async def async_form_network(self) -> None: """Form a brand-new network.""" - async with self._connect_zigpy_app() as app: + async with self.connect_zigpy_app() as app: + await app.connect() await app.form_network() async def async_reset_adapter(self) -> None: """Reset the current adapter.""" - async with self._connect_zigpy_app() as app: + async with self.connect_zigpy_app() as app: + await app.connect() await app.reset_network_info() async def async_restore_backup_step_1(self) -> bool: diff --git a/tests/components/homeassistant_hardware/conftest.py b/tests/components/homeassistant_hardware/conftest.py index 60083c2de94..02b468e558e 100644 --- a/tests/components/homeassistant_hardware/conftest.py +++ b/tests/components/homeassistant_hardware/conftest.py @@ -23,7 +23,7 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]: with patch( "bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe ), patch( - "homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app", + "homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app", return_value=mock_connect_app, ), patch( "homeassistant.components.zha.async_setup_entry", diff --git a/tests/components/homeassistant_sky_connect/conftest.py b/tests/components/homeassistant_sky_connect/conftest.py index 85017866db9..90dbe5af384 100644 --- a/tests/components/homeassistant_sky_connect/conftest.py +++ b/tests/components/homeassistant_sky_connect/conftest.py @@ -25,7 +25,7 @@ def mock_zha(): ) with patch( - "homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app", + "homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app", return_value=mock_connect_app, ), patch( "homeassistant.components.zha.async_setup_entry", diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index cbf1cfa7d36..3afc8c24774 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -45,7 +45,7 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]: with patch( "bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe ), patch( - "homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app", + "homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app", return_value=mock_connect_app, ): yield diff --git a/tests/components/homeassistant_yellow/conftest.py b/tests/components/homeassistant_yellow/conftest.py index e4a666f9f04..a7d66d659f0 100644 --- a/tests/components/homeassistant_yellow/conftest.py +++ b/tests/components/homeassistant_yellow/conftest.py @@ -23,7 +23,7 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]: with patch( "bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe ), patch( - "homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app", + "homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app", return_value=mock_connect_app, ), patch( "homeassistant.components.zha.async_setup_entry", diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 4778f3216da..7d391872a77 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -293,14 +293,20 @@ def zigpy_device_mock(zigpy_app_controller): return _mock_dev +@patch("homeassistant.components.zha.setup_quirks", MagicMock(return_value=True)) @pytest.fixture def zha_device_joined(hass, setup_zha): """Return a newly joined ZHA device.""" + setup_zha_fixture = setup_zha - async def _zha_device(zigpy_dev): + async def _zha_device(zigpy_dev, *, setup_zha: bool = True): zigpy_dev.last_seen = time.time() - await setup_zha() + + if setup_zha: + await setup_zha_fixture() + zha_gateway = common.get_zha_gateway(hass) + zha_gateway.application_controller.devices[zigpy_dev.ieee] = zigpy_dev await zha_gateway.async_device_initialized(zigpy_dev) await hass.async_block_till_done() return zha_gateway.get_device(zigpy_dev.ieee) @@ -308,17 +314,21 @@ def zha_device_joined(hass, setup_zha): return _zha_device +@patch("homeassistant.components.zha.setup_quirks", MagicMock(return_value=True)) @pytest.fixture def zha_device_restored(hass, zigpy_app_controller, setup_zha): """Return a restored ZHA device.""" + setup_zha_fixture = setup_zha - async def _zha_device(zigpy_dev, last_seen=None): + async def _zha_device(zigpy_dev, *, last_seen=None, setup_zha: bool = True): zigpy_app_controller.devices[zigpy_dev.ieee] = zigpy_dev if last_seen is not None: zigpy_dev.last_seen = last_seen - await setup_zha() + if setup_zha: + await setup_zha_fixture() + zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] return zha_gateway.get_device(zigpy_dev.ieee) @@ -376,3 +386,10 @@ def hass_disable_services(hass): hass, "services", MagicMock(has_service=MagicMock(return_value=True)) ): yield hass + + +@pytest.fixture(autouse=True) +def speed_up_radio_mgr(): + """Speed up the radio manager connection time by removing delays.""" + with patch("homeassistant.components.zha.radio_manager.CONNECT_DELAY_S", 0.00001): + yield diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 77d8a615c72..d50d43da675 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -62,13 +62,6 @@ def mock_multipan_platform(): yield -@pytest.fixture(autouse=True) -def reduce_reconnect_timeout(): - """Reduces reconnect timeout to speed up tests.""" - with patch("homeassistant.components.zha.radio_manager.CONNECT_DELAY_S", 0.01): - yield - - @pytest.fixture(autouse=True) def mock_app(): """Mock zigpy app interface.""" diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 22f62cb977a..491e2d96d4f 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -9,6 +9,9 @@ import zigpy.zcl.clusters.general as general import homeassistant.components.automation as automation from homeassistant.components.device_automation import DeviceAutomationType +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) from homeassistant.components.zha.core.const import ATTR_ENDPOINT_ID from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -20,6 +23,7 @@ from .common import async_enable_traffic from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from tests.common import ( + MockConfigEntry, async_fire_time_changed, async_get_device_automations, async_mock_service, @@ -45,6 +49,16 @@ LONG_PRESS = "remote_button_long_press" LONG_RELEASE = "remote_button_long_release" +SWITCH_SIGNATURE = { + 1: { + SIG_EP_INPUT: [general.Basic.cluster_id], + SIG_EP_OUTPUT: [general.OnOff.cluster_id], + SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, + SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, + } +} + + @pytest.fixture(autouse=True) def sensor_platforms_only(): """Only set up the sensor platform and required base platforms to speed up tests.""" @@ -72,16 +86,7 @@ def calls(hass): async def mock_devices(hass, zigpy_device_mock, zha_device_joined_restored): """IAS device fixture.""" - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [general.Basic.cluster_id], - SIG_EP_OUTPUT: [general.OnOff.cluster_id], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - } - } - ) + zigpy_device = zigpy_device_mock(SWITCH_SIGNATURE) zha_device = await zha_device_joined_restored(zigpy_device) zha_device.update_available(True) @@ -397,3 +402,108 @@ async def test_exception_bad_trigger( "Unnamed automation failed to setup triggers and has been disabled: " "device does not have trigger ('junk', 'junk')" in caplog.text ) + + +async def test_validate_trigger_config_missing_info( + hass: HomeAssistant, + config_entry: MockConfigEntry, + zigpy_device_mock, + mock_zigpy_connect, + zha_device_joined, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test device triggers referring to a missing device.""" + + # Join a device + switch = zigpy_device_mock(SWITCH_SIGNATURE) + await zha_device_joined(switch) + + # After we unload the config entry, trigger info was not cached on startup, nor can + # it be pulled from the current device, making it impossible to validate triggers + await hass.config_entries.async_unload(config_entry.entry_id) + + ha_device_registry = dr.async_get(hass) + reg_device = ha_device_registry.async_get_device( + identifiers={("zha", str(switch.ieee))} + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": "junk", + "subtype": "junk", + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + + assert "Unable to get zha device" in caplog.text + + with pytest.raises(InvalidDeviceAutomationConfig): + await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, reg_device.id + ) + + +async def test_validate_trigger_config_unloaded_bad_info( + hass: HomeAssistant, + config_entry: MockConfigEntry, + zigpy_device_mock, + mock_zigpy_connect, + zha_device_joined, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test device triggers referring to a missing device.""" + + # Join a device + switch = zigpy_device_mock(SWITCH_SIGNATURE) + await zha_device_joined(switch) + + # After we unload the config entry, trigger info was not cached on startup, nor can + # it be pulled from the current device, making it impossible to validate triggers + await hass.config_entries.async_unload(config_entry.entry_id) + + # Reload ZHA to persist the device info in the cache + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.config_entries.async_unload(config_entry.entry_id) + + ha_device_registry = dr.async_get(hass) + reg_device = ha_device_registry.async_get_device( + identifiers={("zha", str(switch.ieee))} + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": "junk", + "subtype": "junk", + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + + assert "Unable to find trigger" in caplog.text diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index 63ca10bbf91..6bac012d667 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -6,7 +6,6 @@ import pytest from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from zigpy.exceptions import TransientConnectionError -from homeassistant.components.zha import async_setup_entry from homeassistant.components.zha.core.const import ( CONF_BAUDRATE, CONF_RADIO_TYPE, @@ -22,7 +21,7 @@ from .test_light import LIGHT_ON_OFF from tests.common import MockConfigEntry -DATA_RADIO_TYPE = "deconz" +DATA_RADIO_TYPE = "ezsp" DATA_PORT_PATH = "/dev/serial/by-id/FTDI_USB__-__Serial_Cable_12345678-if00-port0" @@ -137,7 +136,7 @@ async def test_config_depreciation(hass: HomeAssistant, zha_config) -> None: "homeassistant.components.zha.websocket_api.async_load_api", Mock(return_value=True) ) async def test_setup_with_v3_cleaning_uri( - hass: HomeAssistant, path: str, cleaned_path: str + hass: HomeAssistant, path: str, cleaned_path: str, mock_zigpy_connect ) -> None: """Test migration of config entry from v3, applying corrections to the port path.""" config_entry_v3 = MockConfigEntry( @@ -150,14 +149,9 @@ async def test_setup_with_v3_cleaning_uri( ) config_entry_v3.add_to_hass(hass) - with patch( - "homeassistant.components.zha.ZHAGateway", return_value=AsyncMock() - ) as mock_gateway: - mock_gateway.return_value.coordinator_ieee = "mock_ieee" - mock_gateway.return_value.radio_description = "mock_radio" - - assert await async_setup_entry(hass, config_entry_v3) - hass.data[DOMAIN]["zha_gateway"] = mock_gateway.return_value + await hass.config_entries.async_setup(config_entry_v3.entry_id) + await hass.async_block_till_done() + await hass.config_entries.async_unload(config_entry_v3.entry_id) assert config_entry_v3.data[CONF_RADIO_TYPE] == DATA_RADIO_TYPE assert config_entry_v3.data[CONF_DEVICE][CONF_DEVICE_PATH] == cleaned_path diff --git a/tests/components/zha/test_radio_manager.py b/tests/components/zha/test_radio_manager.py index 7acf9219d67..1467e2e2951 100644 --- a/tests/components/zha/test_radio_manager.py +++ b/tests/components/zha/test_radio_manager.py @@ -32,9 +32,7 @@ def disable_platform_only(): @pytest.fixture(autouse=True) def reduce_reconnect_timeout(): """Reduces reconnect timeout to speed up tests.""" - with patch( - "homeassistant.components.zha.radio_manager.CONNECT_DELAY_S", 0.0001 - ), patch("homeassistant.components.zha.radio_manager.RETRY_DELAY_S", 0.0001): + with patch("homeassistant.components.zha.radio_manager.RETRY_DELAY_S", 0.0001): yield @@ -99,7 +97,7 @@ def mock_connect_zigpy_app() -> Generator[MagicMock, None, None]: ) with patch( - "homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app", + "homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app", return_value=mock_connect_app, ): yield mock_connect_app From f0b6367444fd59feed50a58033d4774fdbfbbb5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Tue, 12 Sep 2023 15:59:54 +0200 Subject: [PATCH 128/202] Airthings BLE unique id migration (#99832) * Fix sensor unique id * Add sensor identifiers * Migrate entities to new unique id * Fix linting issues * Fix crash when migrating entity fails * Change how entities are migrated * Remve debug logging * Remove unneeded async * Remove migration code from init file * Add migration code to sensor.py * Adjust for loops to improve speed * Bugfixes, improve documentation * Remove old comment * Remove unused function parameter * Address PR feedback * Add tests * Improve tests and test data * Refactor test * Update logger level Co-authored-by: J. Nick Koston * Adjust PR comments * Address more PR comments * Address PR comments and adjust tests * Fix PR comment --------- Co-authored-by: J. Nick Koston --- .../components/airthings_ble/sensor.py | 55 ++++- tests/components/airthings_ble/__init__.py | 108 ++++++++- tests/components/airthings_ble/test_sensor.py | 213 ++++++++++++++++++ 3 files changed, 365 insertions(+), 11 deletions(-) create mode 100644 tests/components/airthings_ble/test_sensor.py diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py index 4783f3e3b35..b66d6b8f810 100644 --- a/homeassistant/components/airthings_ble/sensor.py +++ b/homeassistant/components/airthings_ble/sensor.py @@ -5,25 +5,35 @@ import logging from airthings_ble import AirthingsDevice -from homeassistant import config_entries from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, LIGHT_LUX, PERCENTAGE, EntityCategory, + Platform, UnitOfPressure, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import ( + CONNECTION_BLUETOOTH, + DeviceInfo, + async_get as device_async_get, +) from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_registry import ( + RegistryEntry, + async_entries_for_device, + async_get as entity_async_get, +) from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -107,9 +117,43 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = { } +@callback +def async_migrate(hass: HomeAssistant, address: str, sensor_name: str) -> None: + """Migrate entities to new unique ids (with BLE Address).""" + ent_reg = entity_async_get(hass) + unique_id_trailer = f"_{sensor_name}" + new_unique_id = f"{address}{unique_id_trailer}" + if ent_reg.async_get_entity_id(DOMAIN, Platform.SENSOR, new_unique_id): + # New unique id already exists + return + dev_reg = device_async_get(hass) + if not ( + device := dev_reg.async_get_device( + connections={(CONNECTION_BLUETOOTH, address)} + ) + ): + return + entities = async_entries_for_device( + ent_reg, + device_id=device.id, + include_disabled_entities=True, + ) + matching_reg_entry: RegistryEntry | None = None + for entry in entities: + if entry.unique_id.endswith(unique_id_trailer) and ( + not matching_reg_entry or "(" not in entry.unique_id + ): + matching_reg_entry = entry + if not matching_reg_entry: + return + entity_id = matching_reg_entry.entity_id + ent_reg.async_update_entity(entity_id=entity_id, new_unique_id=new_unique_id) + _LOGGER.debug("Migrated entity '%s' to unique id '%s'", entity_id, new_unique_id) + + async def async_setup_entry( hass: HomeAssistant, - entry: config_entries.ConfigEntry, + entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Airthings BLE sensors.""" @@ -137,6 +181,7 @@ async def async_setup_entry( sensor_value, ) continue + async_migrate(hass, coordinator.data.address, sensor_type) entities.append( AirthingsSensor(coordinator, coordinator.data, sensors_mapping[sensor_type]) ) @@ -165,7 +210,7 @@ class AirthingsSensor( if identifier := airthings_device.identifier: name += f" ({identifier})" - self._attr_unique_id = f"{name}_{entity_description.key}" + self._attr_unique_id = f"{airthings_device.address}_{entity_description.key}" self._attr_device_info = DeviceInfo( connections={ ( diff --git a/tests/components/airthings_ble/__init__.py b/tests/components/airthings_ble/__init__.py index 0dd78718a30..da0c312bf28 100644 --- a/tests/components/airthings_ble/__init__.py +++ b/tests/components/airthings_ble/__init__.py @@ -5,8 +5,11 @@ from unittest.mock import patch from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice +from homeassistant.components.airthings_ble.const import DOMAIN from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak +from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH +from tests.common import MockConfigEntry, MockEntity from tests.components.bluetooth import generate_advertisement_data, generate_ble_device @@ -36,18 +39,52 @@ def patch_airthings_ble(return_value=AirthingsDevice, side_effect=None): ) +def patch_airthings_device_update(): + """Patch airthings-ble device.""" + return patch( + "homeassistant.components.airthings_ble.AirthingsBluetoothDeviceData.update_device", + return_value=WAVE_DEVICE_INFO, + ) + + WAVE_SERVICE_INFO = BluetoothServiceInfoBleak( name="cc-cc-cc-cc-cc-cc", address="cc:cc:cc:cc:cc:cc", + device=generate_ble_device( + address="cc:cc:cc:cc:cc:cc", + name="Airthings Wave+", + ), rssi=-61, manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"}, - service_data={}, - service_uuids=["b42e1c08-ade7-11e4-89d3-123b93f75cba"], + service_data={ + # Sensor data + "b42e2a68-ade7-11e4-89d3-123b93f75cba": bytearray( + b"\x01\x02\x03\x04\x00\x05\x00\x06\x00\x07\x00\x08\x00\x09\x00\x0A" + ), + # Manufacturer + "00002a29-0000-1000-8000-00805f9b34fb": bytearray(b"Airthings AS"), + # Model + "00002a24-0000-1000-8000-00805f9b34fb": bytearray(b"2930"), + # Identifier + "00002a25-0000-1000-8000-00805f9b34fb": bytearray(b"123456"), + # SW Version + "00002a26-0000-1000-8000-00805f9b34fb": bytearray(b"G-BLE-1.5.3-master+0"), + # HW Version + "00002a27-0000-1000-8000-00805f9b34fb": bytearray(b"REV A"), + # Command + "b42e2d06-ade7-11e4-89d3-123b93f75cba": bytearray(b"\x00"), + }, + service_uuids=[ + "b42e1c08-ade7-11e4-89d3-123b93f75cba", + "b42e2a68-ade7-11e4-89d3-123b93f75cba", + "00002a29-0000-1000-8000-00805f9b34fb", + "00002a24-0000-1000-8000-00805f9b34fb", + "00002a25-0000-1000-8000-00805f9b34fb", + "00002a26-0000-1000-8000-00805f9b34fb", + "00002a27-0000-1000-8000-00805f9b34fb", + "b42e2d06-ade7-11e4-89d3-123b93f75cba", + ], source="local", - device=generate_ble_device( - "cc:cc:cc:cc:cc:cc", - "cc-cc-cc-cc-cc-cc", - ), advertisement=generate_advertisement_data( manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"}, service_uuids=["b42e1c08-ade7-11e4-89d3-123b93f75cba"], @@ -99,3 +136,62 @@ WAVE_DEVICE_INFO = AirthingsDevice( }, address="cc:cc:cc:cc:cc:cc", ) + +TEMPERATURE_V1 = MockEntity( + unique_id="Airthings Wave Plus 123456_temperature", + name="Airthings Wave Plus 123456 Temperature", +) + +HUMIDITY_V2 = MockEntity( + unique_id="Airthings Wave Plus (123456)_humidity", + name="Airthings Wave Plus (123456) Humidity", +) + +CO2_V1 = MockEntity( + unique_id="Airthings Wave Plus 123456_co2", + name="Airthings Wave Plus 123456 CO2", +) + +CO2_V2 = MockEntity( + unique_id="Airthings Wave Plus (123456)_co2", + name="Airthings Wave Plus (123456) CO2", +) + +VOC_V1 = MockEntity( + unique_id="Airthings Wave Plus 123456_voc", + name="Airthings Wave Plus 123456 CO2", +) + +VOC_V2 = MockEntity( + unique_id="Airthings Wave Plus (123456)_voc", + name="Airthings Wave Plus (123456) VOC", +) + +VOC_V3 = MockEntity( + unique_id="cc:cc:cc:cc:cc:cc_voc", + name="Airthings Wave Plus (123456) VOC", +) + + +def create_entry(hass): + """Create a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=WAVE_SERVICE_INFO.address, + title="Airthings Wave Plus (123456)", + ) + entry.add_to_hass(hass) + return entry + + +def create_device(hass, entry): + """Create a device for the given entry.""" + device_registry = hass.helpers.device_registry.async_get(hass) + device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(CONNECTION_BLUETOOTH, WAVE_SERVICE_INFO.address)}, + manufacturer="Airthings AS", + name="Airthings Wave Plus (123456)", + model="Wave Plus", + ) + return device diff --git a/tests/components/airthings_ble/test_sensor.py b/tests/components/airthings_ble/test_sensor.py new file mode 100644 index 00000000000..68efd4d25f6 --- /dev/null +++ b/tests/components/airthings_ble/test_sensor.py @@ -0,0 +1,213 @@ +"""Test the Airthings Wave sensor.""" +import logging + +from homeassistant.components.airthings_ble.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.components.airthings_ble import ( + CO2_V1, + CO2_V2, + HUMIDITY_V2, + TEMPERATURE_V1, + VOC_V1, + VOC_V2, + VOC_V3, + WAVE_DEVICE_INFO, + WAVE_SERVICE_INFO, + create_device, + create_entry, + patch_airthings_device_update, +) +from tests.components.bluetooth import inject_bluetooth_service_info + +_LOGGER = logging.getLogger(__name__) + + +async def test_migration_from_v1_to_v3_unique_id(hass: HomeAssistant): + """Verify that we can migrate from v1 (pre 2023.9.0) to the latest unique id format.""" + entry = create_entry(hass) + device = create_device(hass, entry) + + assert entry is not None + assert device is not None + + entity_registry = hass.helpers.entity_registry.async_get(hass) + + sensor = entity_registry.async_get_or_create( + domain=DOMAIN, + platform="sensor", + unique_id=TEMPERATURE_V1.unique_id, + config_entry=entry, + device_id=device.id, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + inject_bluetooth_service_info( + hass, + WAVE_SERVICE_INFO, + ) + + await hass.async_block_till_done() + + with patch_airthings_device_update(): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) > 0 + + assert ( + entity_registry.async_get(sensor.entity_id).unique_id + == WAVE_DEVICE_INFO.address + "_temperature" + ) + + +async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant): + """Verify that we can migrate from v2 (introduced in 2023.9.0) to the latest unique id format.""" + entry = create_entry(hass) + device = create_device(hass, entry) + + assert entry is not None + assert device is not None + + entity_registry = hass.helpers.entity_registry.async_get(hass) + + await hass.async_block_till_done() + + sensor = entity_registry.async_get_or_create( + domain=DOMAIN, + platform="sensor", + unique_id=HUMIDITY_V2.unique_id, + config_entry=entry, + device_id=device.id, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + inject_bluetooth_service_info( + hass, + WAVE_SERVICE_INFO, + ) + + await hass.async_block_till_done() + + with patch_airthings_device_update(): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) > 0 + + assert ( + entity_registry.async_get(sensor.entity_id).unique_id + == WAVE_DEVICE_INFO.address + "_humidity" + ) + + +async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant): + """Test if migration works when we have both v1 (pre 2023.9.0) and v2 (introduced in 2023.9.0) unique ids.""" + entry = create_entry(hass) + device = create_device(hass, entry) + + assert entry is not None + assert device is not None + + entity_registry = hass.helpers.entity_registry.async_get(hass) + + await hass.async_block_till_done() + + v2 = entity_registry.async_get_or_create( + domain=DOMAIN, + platform="sensor", + unique_id=CO2_V2.unique_id, + config_entry=entry, + device_id=device.id, + ) + + v1 = entity_registry.async_get_or_create( + domain=DOMAIN, + platform="sensor", + unique_id=CO2_V1.unique_id, + config_entry=entry, + device_id=device.id, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + inject_bluetooth_service_info( + hass, + WAVE_SERVICE_INFO, + ) + + await hass.async_block_till_done() + + with patch_airthings_device_update(): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) > 0 + + assert ( + entity_registry.async_get(v1.entity_id).unique_id + == WAVE_DEVICE_INFO.address + "_co2" + ) + assert entity_registry.async_get(v2.entity_id).unique_id == v2.unique_id + + +async def test_migration_with_all_unique_ids(hass: HomeAssistant): + """Test if migration works when we have all unique ids.""" + entry = create_entry(hass) + device = create_device(hass, entry) + + assert entry is not None + assert device is not None + + entity_registry = hass.helpers.entity_registry.async_get(hass) + + await hass.async_block_till_done() + + v1 = entity_registry.async_get_or_create( + domain=DOMAIN, + platform="sensor", + unique_id=VOC_V1.unique_id, + config_entry=entry, + device_id=device.id, + ) + + v2 = entity_registry.async_get_or_create( + domain=DOMAIN, + platform="sensor", + unique_id=VOC_V2.unique_id, + config_entry=entry, + device_id=device.id, + ) + + v3 = entity_registry.async_get_or_create( + domain=DOMAIN, + platform="sensor", + unique_id=VOC_V3.unique_id, + config_entry=entry, + device_id=device.id, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + inject_bluetooth_service_info( + hass, + WAVE_SERVICE_INFO, + ) + + await hass.async_block_till_done() + + with patch_airthings_device_update(): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) > 0 + + assert entity_registry.async_get(v1.entity_id).unique_id == v1.unique_id + assert entity_registry.async_get(v2.entity_id).unique_id == v2.unique_id + assert entity_registry.async_get(v3.entity_id).unique_id == v3.unique_id From e597a6b640104518af25e5849fd21f31becac301 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 9 Sep 2023 01:51:26 +0200 Subject: [PATCH 129/202] Update RestrictedPython to 6.2 (#99955) --- homeassistant/components/python_script/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json index ea153be11cf..80ed6164e74 100644 --- a/homeassistant/components/python_script/manifest.json +++ b/homeassistant/components/python_script/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/python_script", "loggers": ["RestrictedPython"], "quality_scale": "internal", - "requirements": ["RestrictedPython==6.1"] + "requirements": ["RestrictedPython==6.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9a704bd2b25..18728ce1802 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -121,7 +121,7 @@ PyXiaomiGateway==0.14.3 RachioPy==1.0.3 # homeassistant.components.python_script -RestrictedPython==6.1 +RestrictedPython==6.2 # homeassistant.components.remember_the_milk RtmAPI==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7beceb953a8..09d17d43302 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -108,7 +108,7 @@ PyXiaomiGateway==0.14.3 RachioPy==1.0.3 # homeassistant.components.python_script -RestrictedPython==6.1 +RestrictedPython==6.2 # homeassistant.components.remember_the_milk RtmAPI==0.7.2 From 02831ad94f62e85954397ecbe75b28142857de8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 8 Sep 2023 19:39:30 -0500 Subject: [PATCH 130/202] Bump bleak to 0.21.1 (#99960) --- 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 a3c40f739aa..393326d2687 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -14,7 +14,7 @@ ], "quality_scale": "internal", "requirements": [ - "bleak==0.21.0", + "bleak==0.21.1", "bleak-retry-connector==3.1.3", "bluetooth-adapters==0.16.1", "bluetooth-auto-recovery==1.2.2", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d03dc72863d..423c7a1ca94 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -9,7 +9,7 @@ attrs==23.1.0 awesomeversion==22.9.0 bcrypt==4.0.1 bleak-retry-connector==3.1.3 -bleak==0.21.0 +bleak==0.21.1 bluetooth-adapters==0.16.1 bluetooth-auto-recovery==1.2.2 bluetooth-data-tools==1.11.0 diff --git a/requirements_all.txt b/requirements_all.txt index 18728ce1802..dee20a57f11 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -521,7 +521,7 @@ bizkaibus==0.1.1 bleak-retry-connector==3.1.3 # homeassistant.components.bluetooth -bleak==0.21.0 +bleak==0.21.1 # homeassistant.components.blebox blebox-uniapi==2.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 09d17d43302..27e3119c99f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -439,7 +439,7 @@ bimmer-connected==0.14.0 bleak-retry-connector==3.1.3 # homeassistant.components.bluetooth -bleak==0.21.0 +bleak==0.21.1 # homeassistant.components.blebox blebox-uniapi==2.1.4 From 0f9d00e4aa4628dd811f420fabd7c1b9a9ef54a8 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Sat, 9 Sep 2023 08:15:28 -0400 Subject: [PATCH 131/202] Bump python-roborock to 33.2 (#99962) bump to 33.2 --- homeassistant/components/roborock/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 01548a6334c..dfcac67d2b0 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", "loggers": ["roborock"], - "requirements": ["python-roborock==0.32.3"] + "requirements": ["python-roborock==0.33.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index dee20a57f11..f64b36b18d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2159,7 +2159,7 @@ python-qbittorrent==0.4.3 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.32.3 +python-roborock==0.33.2 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27e3119c99f..d6a01686d1a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1585,7 +1585,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.3 # homeassistant.components.roborock -python-roborock==0.32.3 +python-roborock==0.33.2 # homeassistant.components.smarttub python-smarttub==0.0.33 From e6c2833032fdc1d8029b563f5d8b6ec75178d886 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:21:44 -0400 Subject: [PATCH 132/202] Handle disconnects in zwave_js repair flow (#99964) * Handle disconnects in zwave_js repair flow * Combine logic to reduce LoC * only check once --- homeassistant/components/zwave_js/repairs.py | 24 +++--- .../components/zwave_js/strings.json | 3 + tests/components/zwave_js/test_repairs.py | 74 ++++++++++++++++--- 3 files changed, 80 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/zwave_js/repairs.py b/homeassistant/components/zwave_js/repairs.py index 89f51dddb88..83ee0523a3b 100644 --- a/homeassistant/components/zwave_js/repairs.py +++ b/homeassistant/components/zwave_js/repairs.py @@ -2,7 +2,6 @@ from __future__ import annotations import voluptuous as vol -from zwave_js_server.model.node import Node from homeassistant import data_entry_flow from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow @@ -14,10 +13,10 @@ from .helpers import async_get_node_from_device_id class DeviceConfigFileChangedFlow(RepairsFlow): """Handler for an issue fixing flow.""" - def __init__(self, node: Node, device_name: str) -> None: + def __init__(self, data: dict[str, str]) -> None: """Initialize.""" - self.node = node - self.device_name = device_name + self.device_name: str = data["device_name"] + self.device_id: str = data["device_id"] async def async_step_init( self, user_input: dict[str, str] | None = None @@ -30,7 +29,14 @@ class DeviceConfigFileChangedFlow(RepairsFlow): ) -> data_entry_flow.FlowResult: """Handle the confirm step of a fix flow.""" if user_input is not None: - self.hass.async_create_task(self.node.async_refresh_info()) + try: + node = async_get_node_from_device_id(self.hass, self.device_id) + except ValueError: + return self.async_abort( + reason="cannot_connect", + description_placeholders={"device_name": self.device_name}, + ) + self.hass.async_create_task(node.async_refresh_info()) return self.async_create_entry(title="", data={}) return self.async_show_form( @@ -41,15 +47,11 @@ class DeviceConfigFileChangedFlow(RepairsFlow): async def async_create_fix_flow( - hass: HomeAssistant, - issue_id: str, - data: dict[str, str] | None, + hass: HomeAssistant, issue_id: str, data: dict[str, str] | None ) -> RepairsFlow: """Create flow.""" if issue_id.split(".")[0] == "device_config_file_changed": assert data - return DeviceConfigFileChangedFlow( - async_get_node_from_device_id(hass, data["device_id"]), data["device_name"] - ) + return DeviceConfigFileChangedFlow(data) return ConfirmRepairFlow() diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 6435c6b7a54..6994ce15a0c 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -170,6 +170,9 @@ "title": "Z-Wave device configuration file changed: {device_name}", "description": "Z-Wave JS discovers a lot of device metadata by interviewing the device. However, some of the information has to be loaded from a configuration file. Some of this information is only evaluated once, during the device interview.\n\nWhen a device config file is updated, this information may be stale and and the device must be re-interviewed to pick up the changes.\n\n This is not a required operation and device functionality will be impacted during the re-interview process, but you may see improvements for your device once it is complete.\n\nIf you'd like to proceed, click on SUBMIT below. The re-interview will take place in the background." } + }, + "abort": { + "cannot_connect": "Cannot connect to {device_name}. Please try again later after confirming that your Z-Wave network is up and connected to Home Assistant." } } } diff --git a/tests/components/zwave_js/test_repairs.py b/tests/components/zwave_js/test_repairs.py index 07371a299ef..d18bcfa09aa 100644 --- a/tests/components/zwave_js/test_repairs.py +++ b/tests/components/zwave_js/test_repairs.py @@ -22,16 +22,10 @@ import homeassistant.helpers.issue_registry as ir from tests.typing import ClientSessionGenerator, WebSocketGenerator -async def test_device_config_file_changed( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - hass_ws_client: WebSocketGenerator, - client, - multisensor_6_state, - integration, -) -> None: - """Test the device_config_file_changed issue.""" - dev_reg = dr.async_get(hass) +async def _trigger_repair_issue( + hass: HomeAssistant, client, multisensor_6_state +) -> Node: + """Trigger repair issue.""" # Create a node node_state = deepcopy(multisensor_6_state) node = Node(client, node_state) @@ -53,6 +47,23 @@ async def test_device_config_file_changed( client.async_send_command_no_wait.reset_mock() + return node + + +async def test_device_config_file_changed( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, + client, + multisensor_6_state, + integration, +) -> None: + """Test the device_config_file_changed issue.""" + dev_reg = dr.async_get(hass) + node = await _trigger_repair_issue(hass, client, multisensor_6_state) + + client.async_send_command_no_wait.reset_mock() + device = dev_reg.async_get_device(identifiers={get_device_id(client.driver, node)}) assert device issue_id = f"device_config_file_changed.{device.id}" @@ -157,3 +168,46 @@ async def test_invalid_issue( msg = await ws_client.receive_json() assert msg["success"] assert len(msg["result"]["issues"]) == 0 + + +async def test_abort_confirm( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, + client, + multisensor_6_state, + integration, +) -> None: + """Test aborting device_config_file_changed issue in confirm step.""" + dev_reg = dr.async_get(hass) + node = await _trigger_repair_issue(hass, client, multisensor_6_state) + + device = dev_reg.async_get_device(identifiers={get_device_id(client.driver, node)}) + assert device + issue_id = f"device_config_file_changed.{device.id}" + + await async_process_repairs_platforms(hass) + await hass_ws_client(hass) + http_client = await hass_client() + + url = RepairsFlowIndexView.url + resp = await http_client.post(url, json={"handler": DOMAIN, "issue_id": issue_id}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["step_id"] == "confirm" + + # Unload config entry so we can't connect to the node + await hass.config_entries.async_unload(integration.entry_id) + + # Apply fix + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await http_client.post(url) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "abort" + assert data["reason"] == "cannot_connect" + assert data["description_placeholders"] == {"device_name": device.name} From 7cdb4ec8523a59ba25e6ad89bfbc07d20cb2fd00 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sat, 9 Sep 2023 23:50:26 +0200 Subject: [PATCH 133/202] Bump plugwise to v0.32.2 (#99973) * Bump plugwise to v0.32.2 * Adapt number.py to the backend updates * Update related test-cases * Update plugwise test-fixtures * Update test_diagnostics.py --- .../components/plugwise/manifest.json | 2 +- homeassistant/components/plugwise/number.py | 13 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../plugwise/fixtures/adam_jip/all_data.json | 48 ++ .../all_data.json | 54 ++ .../anna_heatpump_heating/all_data.json | 6 + .../fixtures/m_adam_cooling/all_data.json | 12 + .../fixtures/m_adam_heating/all_data.json | 12 + .../m_anna_heatpump_cooling/all_data.json | 6 + .../m_anna_heatpump_idle/all_data.json | 6 + .../fixtures/p1v4_442_triple/all_data.json | 8 +- .../p1v4_442_triple/notifications.json | 6 +- .../fixtures/stretch_v31/all_data.json | 9 - tests/components/plugwise/test_diagnostics.py | 594 ++++++++++-------- tests/components/plugwise/test_number.py | 4 +- 16 files changed, 493 insertions(+), 291 deletions(-) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index ef0f01b38f7..e87e1f0c281 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["crcmod", "plugwise"], - "requirements": ["plugwise==0.31.9"], + "requirements": ["plugwise==0.32.2"], "zeroconf": ["_plugwise._tcp.local."] } diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index 5979480d90f..6fd3f7f92da 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -27,7 +27,7 @@ from .entity import PlugwiseEntity class PlugwiseEntityDescriptionMixin: """Mixin values for Plugwise entities.""" - command: Callable[[Smile, str, float], Awaitable[None]] + command: Callable[[Smile, str, str, float], Awaitable[None]] @dataclass @@ -43,7 +43,9 @@ NUMBER_TYPES = ( PlugwiseNumberEntityDescription( key="maximum_boiler_temperature", translation_key="maximum_boiler_temperature", - command=lambda api, number, value: api.set_number_setpoint(number, value), + command=lambda api, number, dev_id, value: api.set_number_setpoint( + number, dev_id, value + ), device_class=NumberDeviceClass.TEMPERATURE, entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -51,7 +53,9 @@ NUMBER_TYPES = ( PlugwiseNumberEntityDescription( key="max_dhw_temperature", translation_key="max_dhw_temperature", - command=lambda api, number, value: api.set_number_setpoint(number, value), + command=lambda api, number, dev_id, value: api.set_number_setpoint( + number, dev_id, value + ), device_class=NumberDeviceClass.TEMPERATURE, entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -94,6 +98,7 @@ class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): ) -> None: """Initiate Plugwise Number.""" super().__init__(coordinator, device_id) + self.device_id = device_id self.entity_description = description self._attr_unique_id = f"{device_id}-{description.key}" self._attr_mode = NumberMode.BOX @@ -109,6 +114,6 @@ class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Change to the new setpoint value.""" await self.entity_description.command( - self.coordinator.api, self.entity_description.key, value + self.coordinator.api, self.entity_description.key, self.device_id, value ) await self.coordinator.async_request_refresh() diff --git a/requirements_all.txt b/requirements_all.txt index f64b36b18d6..8c3336b5899 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1438,7 +1438,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.31.9 +plugwise==0.32.2 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d6a01686d1a..77214c21835 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1086,7 +1086,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.31.9 +plugwise==0.32.2 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/fixtures/adam_jip/all_data.json b/tests/components/plugwise/fixtures/adam_jip/all_data.json index 177478f0fff..4dda9af3b54 100644 --- a/tests/components/plugwise/fixtures/adam_jip/all_data.json +++ b/tests/components/plugwise/fixtures/adam_jip/all_data.json @@ -20,6 +20,12 @@ "setpoint": 13.0, "temperature": 24.2 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, "thermostat": { "lower_bound": 0.0, "resolution": 0.01, @@ -43,6 +49,12 @@ "temperature_difference": 2.0, "valve_position": 0.0 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A07" }, @@ -60,6 +72,12 @@ "temperature_difference": 1.7, "valve_position": 0.0 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A05" }, @@ -99,6 +117,12 @@ "setpoint": 13.0, "temperature": 30.0 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, "thermostat": { "lower_bound": 0.0, "resolution": 0.01, @@ -122,6 +146,12 @@ "temperature_difference": 1.8, "valve_position": 100 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A09" }, @@ -145,6 +175,12 @@ "setpoint": 13.0, "temperature": 30.0 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, "thermostat": { "lower_bound": 0.0, "resolution": 0.01, @@ -187,6 +223,12 @@ "temperature_difference": 1.9, "valve_position": 0.0 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A04" }, @@ -246,6 +288,12 @@ "setpoint": 9.0, "temperature": 27.4 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, "thermostat": { "lower_bound": 4.0, "resolution": 0.01, diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json index 63f0012ea92..0cc28731ff4 100644 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json +++ b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json @@ -95,6 +95,12 @@ "temperature_difference": -0.4, "valve_position": 0.0 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A17" }, @@ -123,6 +129,12 @@ "setpoint": 15.0, "temperature": 17.2 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, "thermostat": { "lower_bound": 0.0, "resolution": 0.01, @@ -200,6 +212,12 @@ "temperature_difference": -0.2, "valve_position": 0.0 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A09" }, @@ -217,6 +235,12 @@ "temperature_difference": 3.5, "valve_position": 100 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A02" }, @@ -245,6 +269,12 @@ "setpoint": 21.5, "temperature": 20.9 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, "thermostat": { "lower_bound": 0.0, "resolution": 0.01, @@ -289,6 +319,12 @@ "temperature_difference": 0.1, "valve_position": 0.0 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A10" }, @@ -317,6 +353,12 @@ "setpoint": 13.0, "temperature": 16.5 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, "thermostat": { "lower_bound": 0.0, "resolution": 0.01, @@ -353,6 +395,12 @@ "temperature_difference": 0.0, "valve_position": 0.0 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, "thermostat": { "lower_bound": 0.0, "resolution": 0.01, @@ -387,6 +435,12 @@ "setpoint": 14.0, "temperature": 18.9 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, "thermostat": { "lower_bound": 0.0, "resolution": 0.01, diff --git a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json index 49b5221233f..cdddfdb3439 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json @@ -76,6 +76,12 @@ "setpoint": 20.5, "temperature": 19.3 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": -0.5, + "upper_bound": 2.0 + }, "thermostat": { "lower_bound": 4.0, "resolution": 0.1, diff --git a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json index 92618a90189..ac7e602821e 100644 --- a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json @@ -40,6 +40,12 @@ "temperature_difference": 2.3, "valve_position": 0.0 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A01" }, @@ -118,6 +124,12 @@ "setpoint_low": 20.0, "temperature": 239 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, "thermostat": { "lower_bound": 0.0, "resolution": 0.01, diff --git a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json index 4345cf76a3a..a4923b1c549 100644 --- a/tests/components/plugwise/fixtures/m_adam_heating/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_heating/all_data.json @@ -45,6 +45,12 @@ "temperature_difference": 2.3, "valve_position": 0.0 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A01" }, @@ -114,6 +120,12 @@ "setpoint": 15.0, "temperature": 17.9 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, "thermostat": { "lower_bound": 0.0, "resolution": 0.01, diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json index 20f2db213bd..f98f253e938 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/all_data.json @@ -78,6 +78,12 @@ "setpoint_low": 20.5, "temperature": 26.3 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": -0.5, + "upper_bound": 2.0 + }, "thermostat": { "lower_bound": 4.0, "resolution": 0.1, diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json index 3a7bd2dae89..56d26f67acb 100644 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/all_data.json @@ -78,6 +78,12 @@ "setpoint_low": 20.5, "temperature": 23.0 }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": -0.5, + "upper_bound": 2.0 + }, "thermostat": { "lower_bound": 4.0, "resolution": 0.1, diff --git a/tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json b/tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json index e9a3b4c68b9..d503bd3a59d 100644 --- a/tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json +++ b/tests/components/plugwise/fixtures/p1v4_442_triple/all_data.json @@ -2,7 +2,7 @@ "devices": { "03e65b16e4b247a29ae0d75a78cb492e": { "binary_sensors": { - "plugwise_notification": false + "plugwise_notification": true }, "dev_class": "gateway", "firmware": "4.4.2", @@ -51,7 +51,11 @@ }, "gateway": { "gateway_id": "03e65b16e4b247a29ae0d75a78cb492e", - "notifications": {}, + "notifications": { + "97a04c0c263049b29350a660b4cdd01e": { + "warning": "The Smile P1 is not connected to a smart meter." + } + }, "smile_name": "Smile P1" } } diff --git a/tests/components/plugwise/fixtures/p1v4_442_triple/notifications.json b/tests/components/plugwise/fixtures/p1v4_442_triple/notifications.json index 0967ef424bc..49db062035a 100644 --- a/tests/components/plugwise/fixtures/p1v4_442_triple/notifications.json +++ b/tests/components/plugwise/fixtures/p1v4_442_triple/notifications.json @@ -1 +1,5 @@ -{} +{ + "97a04c0c263049b29350a660b4cdd01e": { + "warning": "The Smile P1 is not connected to a smart meter." + } +} diff --git a/tests/components/plugwise/fixtures/stretch_v31/all_data.json b/tests/components/plugwise/fixtures/stretch_v31/all_data.json index c336a9cb9c2..8604aaae10e 100644 --- a/tests/components/plugwise/fixtures/stretch_v31/all_data.json +++ b/tests/components/plugwise/fixtures/stretch_v31/all_data.json @@ -48,15 +48,6 @@ "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A07" }, - "71e1944f2a944b26ad73323e399efef0": { - "dev_class": "switching", - "members": ["5ca521ac179d468e91d772eeeb8a2117"], - "model": "Switchgroup", - "name": "Test", - "switches": { - "relay": true - } - }, "aac7b735042c4832ac9ff33aae4f453b": { "dev_class": "dishwasher", "firmware": "2011-06-27T10:52:18+02:00", diff --git a/tests/components/plugwise/test_diagnostics.py b/tests/components/plugwise/test_diagnostics.py index 5dde8a0e09e..69f180692e2 100644 --- a/tests/components/plugwise/test_diagnostics.py +++ b/tests/components/plugwise/test_diagnostics.py @@ -31,159 +31,141 @@ async def test_diagnostics( }, }, "devices": { - "df4a4a8169904cdb9c03d61a21f42140": { - "dev_class": "zone_thermostat", - "firmware": "2016-10-27T02:00:00+02:00", - "hardware": "255", - "location": "12493538af164a409c6a1c79e38afe1c", - "model": "Lisa", - "name": "Zone Lisa Bios", - "zigbee_mac_address": "ABCD012345670A06", - "vendor": "Plugwise", - "thermostat": { - "setpoint": 13.0, - "lower_bound": 0.0, - "upper_bound": 99.9, - "resolution": 0.01, - }, - "available": True, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "active_preset": "away", - "available_schedules": [ - "CV Roan", - "Bios Schema met Film Avond", - "GF7 Woonkamer", - "Badkamer Schema", - "CV Jessie", - ], - "select_schedule": "None", - "last_used": "Badkamer Schema", - "mode": "heat", - "sensors": {"temperature": 16.5, "setpoint": 13.0, "battery": 67}, - }, - "b310b72a0e354bfab43089919b9a88bf": { - "dev_class": "thermo_sensor", - "firmware": "2019-03-27T01:00:00+01:00", - "hardware": "1", - "location": "c50f167537524366a5af7aa3942feb1e", - "model": "Tom/Floor", - "name": "Floor kraan", - "zigbee_mac_address": "ABCD012345670A02", - "vendor": "Plugwise", + "02cf28bfec924855854c544690a609ef": { "available": True, + "dev_class": "vcr", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "cd143c07248f491493cea0533bc3d669", + "model": "Plug", + "name": "NVR", "sensors": { - "temperature": 26.0, - "setpoint": 21.5, - "temperature_difference": 3.5, - "valve_position": 100, + "electricity_consumed": 34.0, + "electricity_consumed_interval": 9.15, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0, }, - }, - "a2c3583e0a6349358998b760cea82d2a": { - "dev_class": "thermo_sensor", - "firmware": "2019-03-27T01:00:00+01:00", - "hardware": "1", - "location": "12493538af164a409c6a1c79e38afe1c", - "model": "Tom/Floor", - "name": "Bios Cv Thermostatic Radiator ", - "zigbee_mac_address": "ABCD012345670A09", + "switches": {"lock": True, "relay": True}, "vendor": "Plugwise", - "available": True, - "sensors": { - "temperature": 17.2, - "setpoint": 13.0, - "battery": 62, - "temperature_difference": -0.2, - "valve_position": 0.0, - }, - }, - "b59bcebaf94b499ea7d46e4a66fb62d8": { - "dev_class": "zone_thermostat", - "firmware": "2016-08-02T02:00:00+02:00", - "hardware": "255", - "location": "c50f167537524366a5af7aa3942feb1e", - "model": "Lisa", - "name": "Zone Lisa WK", - "zigbee_mac_address": "ABCD012345670A07", - "vendor": "Plugwise", - "thermostat": { - "setpoint": 21.5, - "lower_bound": 0.0, - "upper_bound": 99.9, - "resolution": 0.01, - }, - "available": True, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "active_preset": "home", - "available_schedules": [ - "CV Roan", - "Bios Schema met Film Avond", - "GF7 Woonkamer", - "Badkamer Schema", - "CV Jessie", - ], - "select_schedule": "GF7 Woonkamer", - "last_used": "GF7 Woonkamer", - "mode": "auto", - "sensors": {"temperature": 20.9, "setpoint": 21.5, "battery": 34}, - }, - "fe799307f1624099878210aa0b9f1475": { - "dev_class": "gateway", - "firmware": "3.0.15", - "hardware": "AME Smile 2.0 board", - "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", - "mac_address": "012345670001", - "model": "Gateway", - "name": "Adam", - "zigbee_mac_address": "ABCD012345670101", - "vendor": "Plugwise", - "select_regulation_mode": "heating", - "binary_sensors": {"plugwise_notification": True}, - "sensors": {"outdoor_temperature": 7.81}, - }, - "d3da73bde12a47d5a6b8f9dad971f2ec": { - "dev_class": "thermo_sensor", - "firmware": "2019-03-27T01:00:00+01:00", - "hardware": "1", - "location": "82fa13f017d240daa0d0ea1775420f24", - "model": "Tom/Floor", - "name": "Thermostatic Radiator Jessie", - "zigbee_mac_address": "ABCD012345670A10", - "vendor": "Plugwise", - "available": True, - "sensors": { - "temperature": 17.1, - "setpoint": 15.0, - "battery": 62, - "temperature_difference": 0.1, - "valve_position": 0.0, - }, + "zigbee_mac_address": "ABCD012345670A15", }, "21f2b542c49845e6bb416884c55778d6": { + "available": True, "dev_class": "game_console", "firmware": "2019-06-21T02:00:00+02:00", "location": "cd143c07248f491493cea0533bc3d669", "model": "Plug", "name": "Playstation Smart Plug", - "zigbee_mac_address": "ABCD012345670A12", - "vendor": "Plugwise", - "available": True, "sensors": { "electricity_consumed": 82.6, "electricity_consumed_interval": 8.6, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, }, - "switches": {"relay": True, "lock": False}, + "switches": {"lock": False, "relay": True}, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A12", + }, + "4a810418d5394b3f82727340b91ba740": { + "available": True, + "dev_class": "router", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "cd143c07248f491493cea0533bc3d669", + "model": "Plug", + "name": "USG Smart Plug", + "sensors": { + "electricity_consumed": 8.5, + "electricity_consumed_interval": 0.0, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0, + }, + "switches": {"lock": True, "relay": True}, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A16", + }, + "675416a629f343c495449970e2ca37b5": { + "available": True, + "dev_class": "router", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "cd143c07248f491493cea0533bc3d669", + "model": "Plug", + "name": "Ziggo Modem", + "sensors": { + "electricity_consumed": 12.2, + "electricity_consumed_interval": 2.97, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0, + }, + "switches": {"lock": True, "relay": True}, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A01", + }, + "680423ff840043738f42cc7f1ff97a36": { + "available": True, + "dev_class": "thermo_sensor", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", + "location": "08963fec7c53423ca5680aa4cb502c63", + "model": "Tom/Floor", + "name": "Thermostatic Radiator Badkamer", + "sensors": { + "battery": 51, + "setpoint": 14.0, + "temperature": 19.1, + "temperature_difference": -0.4, + "valve_position": 0.0, + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0, + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A17", + }, + "6a3bf693d05e48e0b460c815a4fdd09d": { + "active_preset": "asleep", + "available": True, + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie", + ], + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "last_used": "CV Jessie", + "location": "82fa13f017d240daa0d0ea1775420f24", + "mode": "auto", + "model": "Lisa", + "name": "Zone Thermostat Jessie", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "CV Jessie", + "sensors": {"battery": 37, "setpoint": 15.0, "temperature": 17.2}, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0, + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 15.0, + "upper_bound": 99.9, + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A03", }, "78d1126fc4c743db81b61c20e88342a7": { + "available": True, "dev_class": "central_heating_pump", "firmware": "2019-06-21T02:00:00+02:00", "location": "c50f167537524366a5af7aa3942feb1e", "model": "Plug", "name": "CV Pomp", - "zigbee_mac_address": "ABCD012345670A05", - "vendor": "Plugwise", - "available": True, "sensors": { "electricity_consumed": 35.6, "electricity_consumed_interval": 7.37, @@ -191,153 +173,88 @@ async def test_diagnostics( "electricity_produced_interval": 0.0, }, "switches": {"relay": True}, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A05", }, "90986d591dcd426cae3ec3e8111ff730": { + "binary_sensors": {"heating_state": True}, "dev_class": "heater_central", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", "model": "Unknown", "name": "OnOff", - "binary_sensors": {"heating_state": True}, "sensors": { - "water_temperature": 70.0, "intended_boiler_temperature": 70.0, "modulation_level": 1, + "water_temperature": 70.0, }, }, - "cd0ddb54ef694e11ac18ed1cbce5dbbd": { - "dev_class": "vcr", - "firmware": "2019-06-21T02:00:00+02:00", - "location": "cd143c07248f491493cea0533bc3d669", - "model": "Plug", - "name": "NAS", - "zigbee_mac_address": "ABCD012345670A14", - "vendor": "Plugwise", - "available": True, - "sensors": { - "electricity_consumed": 16.5, - "electricity_consumed_interval": 0.5, - "electricity_produced": 0.0, - "electricity_produced_interval": 0.0, - }, - "switches": {"relay": True, "lock": True}, - }, - "4a810418d5394b3f82727340b91ba740": { - "dev_class": "router", - "firmware": "2019-06-21T02:00:00+02:00", - "location": "cd143c07248f491493cea0533bc3d669", - "model": "Plug", - "name": "USG Smart Plug", - "zigbee_mac_address": "ABCD012345670A16", - "vendor": "Plugwise", - "available": True, - "sensors": { - "electricity_consumed": 8.5, - "electricity_consumed_interval": 0.0, - "electricity_produced": 0.0, - "electricity_produced_interval": 0.0, - }, - "switches": {"relay": True, "lock": True}, - }, - "02cf28bfec924855854c544690a609ef": { - "dev_class": "vcr", - "firmware": "2019-06-21T02:00:00+02:00", - "location": "cd143c07248f491493cea0533bc3d669", - "model": "Plug", - "name": "NVR", - "zigbee_mac_address": "ABCD012345670A15", - "vendor": "Plugwise", - "available": True, - "sensors": { - "electricity_consumed": 34.0, - "electricity_consumed_interval": 9.15, - "electricity_produced": 0.0, - "electricity_produced_interval": 0.0, - }, - "switches": {"relay": True, "lock": True}, - }, "a28f588dc4a049a483fd03a30361ad3a": { + "available": True, "dev_class": "settop", "firmware": "2019-06-21T02:00:00+02:00", "location": "cd143c07248f491493cea0533bc3d669", "model": "Plug", "name": "Fibaro HC2", - "zigbee_mac_address": "ABCD012345670A13", - "vendor": "Plugwise", - "available": True, "sensors": { "electricity_consumed": 12.5, "electricity_consumed_interval": 3.8, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, }, - "switches": {"relay": True, "lock": True}, - }, - "6a3bf693d05e48e0b460c815a4fdd09d": { - "dev_class": "zone_thermostat", - "firmware": "2016-10-27T02:00:00+02:00", - "hardware": "255", - "location": "82fa13f017d240daa0d0ea1775420f24", - "model": "Lisa", - "name": "Zone Thermostat Jessie", - "zigbee_mac_address": "ABCD012345670A03", + "switches": {"lock": True, "relay": True}, "vendor": "Plugwise", - "thermostat": { - "setpoint": 15.0, - "lower_bound": 0.0, - "upper_bound": 99.9, - "resolution": 0.01, - }, - "available": True, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "active_preset": "asleep", - "available_schedules": [ - "CV Roan", - "Bios Schema met Film Avond", - "GF7 Woonkamer", - "Badkamer Schema", - "CV Jessie", - ], - "select_schedule": "CV Jessie", - "last_used": "CV Jessie", - "mode": "auto", - "sensors": {"temperature": 17.2, "setpoint": 15.0, "battery": 37}, + "zigbee_mac_address": "ABCD012345670A13", }, - "680423ff840043738f42cc7f1ff97a36": { + "a2c3583e0a6349358998b760cea82d2a": { + "available": True, "dev_class": "thermo_sensor", "firmware": "2019-03-27T01:00:00+01:00", "hardware": "1", - "location": "08963fec7c53423ca5680aa4cb502c63", + "location": "12493538af164a409c6a1c79e38afe1c", "model": "Tom/Floor", - "name": "Thermostatic Radiator Badkamer", - "zigbee_mac_address": "ABCD012345670A17", - "vendor": "Plugwise", - "available": True, + "name": "Bios Cv Thermostatic Radiator ", "sensors": { - "temperature": 19.1, - "setpoint": 14.0, - "battery": 51, - "temperature_difference": -0.4, + "battery": 62, + "setpoint": 13.0, + "temperature": 17.2, + "temperature_difference": -0.2, "valve_position": 0.0, }, - }, - "f1fee6043d3642a9b0a65297455f008e": { - "dev_class": "zone_thermostat", - "firmware": "2016-10-27T02:00:00+02:00", - "hardware": "255", - "location": "08963fec7c53423ca5680aa4cb502c63", - "model": "Lisa", - "name": "Zone Thermostat Badkamer", - "zigbee_mac_address": "ABCD012345670A08", - "vendor": "Plugwise", - "thermostat": { - "setpoint": 14.0, - "lower_bound": 0.0, - "upper_bound": 99.9, - "resolution": 0.01, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0, }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A09", + }, + "b310b72a0e354bfab43089919b9a88bf": { + "available": True, + "dev_class": "thermo_sensor", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", + "location": "c50f167537524366a5af7aa3942feb1e", + "model": "Tom/Floor", + "name": "Floor kraan", + "sensors": { + "setpoint": 21.5, + "temperature": 26.0, + "temperature_difference": 3.5, + "valve_position": 100, + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0, + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A02", + }, + "b59bcebaf94b499ea7d46e4a66fb62d8": { + "active_preset": "home", "available": True, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "active_preset": "away", "available_schedules": [ "CV Roan", "Bios Schema met Film Avond", @@ -345,46 +262,76 @@ async def test_diagnostics( "Badkamer Schema", "CV Jessie", ], - "select_schedule": "Badkamer Schema", - "last_used": "Badkamer Schema", + "dev_class": "zone_thermostat", + "firmware": "2016-08-02T02:00:00+02:00", + "hardware": "255", + "last_used": "GF7 Woonkamer", + "location": "c50f167537524366a5af7aa3942feb1e", "mode": "auto", - "sensors": {"temperature": 18.9, "setpoint": 14.0, "battery": 92}, + "model": "Lisa", + "name": "Zone Lisa WK", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "GF7 Woonkamer", + "sensors": {"battery": 34, "setpoint": 21.5, "temperature": 20.9}, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0, + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 21.5, + "upper_bound": 99.9, + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A07", }, - "675416a629f343c495449970e2ca37b5": { - "dev_class": "router", + "cd0ddb54ef694e11ac18ed1cbce5dbbd": { + "available": True, + "dev_class": "vcr", "firmware": "2019-06-21T02:00:00+02:00", "location": "cd143c07248f491493cea0533bc3d669", "model": "Plug", - "name": "Ziggo Modem", - "zigbee_mac_address": "ABCD012345670A01", - "vendor": "Plugwise", - "available": True, + "name": "NAS", "sensors": { - "electricity_consumed": 12.2, - "electricity_consumed_interval": 2.97, + "electricity_consumed": 16.5, + "electricity_consumed_interval": 0.5, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, }, - "switches": {"relay": True, "lock": True}, + "switches": {"lock": True, "relay": True}, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A14", }, - "e7693eb9582644e5b865dba8d4447cf1": { - "dev_class": "thermostatic_radiator_valve", + "d3da73bde12a47d5a6b8f9dad971f2ec": { + "available": True, + "dev_class": "thermo_sensor", "firmware": "2019-03-27T01:00:00+01:00", "hardware": "1", - "location": "446ac08dd04d4eff8ac57489757b7314", + "location": "82fa13f017d240daa0d0ea1775420f24", "model": "Tom/Floor", - "name": "CV Kraan Garage", - "zigbee_mac_address": "ABCD012345670A11", - "vendor": "Plugwise", - "thermostat": { - "setpoint": 5.5, - "lower_bound": 0.0, - "upper_bound": 100.0, - "resolution": 0.01, + "name": "Thermostatic Radiator Jessie", + "sensors": { + "battery": 62, + "setpoint": 15.0, + "temperature": 17.1, + "temperature_difference": 0.1, + "valve_position": 0.0, }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0, + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A10", + }, + "df4a4a8169904cdb9c03d61a21f42140": { + "active_preset": "away", "available": True, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], - "active_preset": "no_frost", "available_schedules": [ "CV Roan", "Bios Schema met Film Avond", @@ -392,16 +339,123 @@ async def test_diagnostics( "Badkamer Schema", "CV Jessie", ], - "select_schedule": "None", + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", "last_used": "Badkamer Schema", + "location": "12493538af164a409c6a1c79e38afe1c", "mode": "heat", + "model": "Lisa", + "name": "Zone Lisa Bios", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "None", + "sensors": {"battery": 67, "setpoint": 13.0, "temperature": 16.5}, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0, + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 13.0, + "upper_bound": 99.9, + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A06", + }, + "e7693eb9582644e5b865dba8d4447cf1": { + "active_preset": "no_frost", + "available": True, + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie", + ], + "dev_class": "thermostatic_radiator_valve", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", + "last_used": "Badkamer Schema", + "location": "446ac08dd04d4eff8ac57489757b7314", + "mode": "heat", + "model": "Tom/Floor", + "name": "CV Kraan Garage", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "None", "sensors": { - "temperature": 15.6, - "setpoint": 5.5, "battery": 68, + "setpoint": 5.5, + "temperature": 15.6, "temperature_difference": 0.0, "valve_position": 0.0, }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0, + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 5.5, + "upper_bound": 100.0, + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A11", + }, + "f1fee6043d3642a9b0a65297455f008e": { + "active_preset": "away", + "available": True, + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie", + ], + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "last_used": "Badkamer Schema", + "location": "08963fec7c53423ca5680aa4cb502c63", + "mode": "auto", + "model": "Lisa", + "name": "Zone Thermostat Badkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "Badkamer Schema", + "sensors": {"battery": 92, "setpoint": 14.0, "temperature": 18.9}, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0, + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 14.0, + "upper_bound": 99.9, + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A08", + }, + "fe799307f1624099878210aa0b9f1475": { + "binary_sensors": {"plugwise_notification": True}, + "dev_class": "gateway", + "firmware": "3.0.15", + "hardware": "AME Smile 2.0 board", + "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", + "mac_address": "012345670001", + "model": "Gateway", + "name": "Adam", + "select_regulation_mode": "heating", + "sensors": {"outdoor_temperature": 7.81}, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670101", }, }, } diff --git a/tests/components/plugwise/test_number.py b/tests/components/plugwise/test_number.py index 9ca64e104d3..bccf257a433 100644 --- a/tests/components/plugwise/test_number.py +++ b/tests/components/plugwise/test_number.py @@ -38,7 +38,7 @@ async def test_anna_max_boiler_temp_change( assert mock_smile_anna.set_number_setpoint.call_count == 1 mock_smile_anna.set_number_setpoint.assert_called_with( - "maximum_boiler_temperature", 65.0 + "maximum_boiler_temperature", "1cbf783bb11e4a7c8a6843dee3a86927", 65.0 ) @@ -67,5 +67,5 @@ async def test_adam_dhw_setpoint_change( assert mock_smile_adam_2.set_number_setpoint.call_count == 1 mock_smile_adam_2.set_number_setpoint.assert_called_with( - "max_dhw_temperature", 55.0 + "max_dhw_temperature", "056ee145a816487eaa69243c3280f8bf", 55.0 ) From f200ba7a864ec93995695d7e33f257289c8bdad5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 9 Sep 2023 12:23:15 -0500 Subject: [PATCH 134/202] Bump bluetooth-auto-recovery to 1.2.3 (#99979) fixes #99977 --- 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 393326d2687..33ec71065db 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -17,7 +17,7 @@ "bleak==0.21.1", "bleak-retry-connector==3.1.3", "bluetooth-adapters==0.16.1", - "bluetooth-auto-recovery==1.2.2", + "bluetooth-auto-recovery==1.2.3", "bluetooth-data-tools==1.11.0", "dbus-fast==1.95.2" ] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 423c7a1ca94..7bd0c843e6a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ bcrypt==4.0.1 bleak-retry-connector==3.1.3 bleak==0.21.1 bluetooth-adapters==0.16.1 -bluetooth-auto-recovery==1.2.2 +bluetooth-auto-recovery==1.2.3 bluetooth-data-tools==1.11.0 certifi>=2021.5.30 ciso8601==2.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8c3336b5899..3086c5398a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -543,7 +543,7 @@ bluemaestro-ble==0.2.3 bluetooth-adapters==0.16.1 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.2.2 +bluetooth-auto-recovery==1.2.3 # homeassistant.components.bluetooth # homeassistant.components.esphome diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77214c21835..5c770f5936a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -454,7 +454,7 @@ bluemaestro-ble==0.2.3 bluetooth-adapters==0.16.1 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.2.2 +bluetooth-auto-recovery==1.2.3 # homeassistant.components.bluetooth # homeassistant.components.esphome From 3d09e859fcd39257367fab3b89bbac447a1aef00 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Sep 2023 21:34:23 -0500 Subject: [PATCH 135/202] Avoid probing ipp printers for unique_id when it is available via mdns (#99982) * Avoid probing ipp printers for unique_id when it is available via mdns We would always probe the device in the ipp flow and than abort if it was already configured. We avoid the probe for most printers. * dry * coverage * fix test * add test for updating host --- homeassistant/components/ipp/config_flow.py | 36 ++++-- .../ipp/fixtures/printer_without_uuid.json | 35 ++++++ tests/components/ipp/test_config_flow.py | 103 +++++++++++++++++- 3 files changed, 160 insertions(+), 14 deletions(-) create mode 100644 tests/components/ipp/fixtures/printer_without_uuid.json diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index 8d1da6eca91..dfe6c0b2127 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -116,8 +116,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): name = discovery_info.name.replace(f".{zctype}", "") tls = zctype == "_ipps._tcp.local." base_path = discovery_info.properties.get("rp", "ipp/print") - - self.context.update({"title_placeholders": {"name": name}}) + unique_id = discovery_info.properties.get("UUID") self.discovery_info.update( { @@ -127,10 +126,18 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): CONF_VERIFY_SSL: False, CONF_BASE_PATH: f"/{base_path}", CONF_NAME: name, - CONF_UUID: discovery_info.properties.get("UUID"), + CONF_UUID: unique_id, } ) + if unique_id: + # If we already have the unique id, try to set it now + # so we can avoid probing the device if its already + # configured or ignored + await self._async_set_unique_id_and_abort_if_already_configured(unique_id) + + self.context.update({"title_placeholders": {"name": name}}) + try: info = await validate_input(self.hass, self.discovery_info) except IPPConnectionUpgradeRequired: @@ -147,7 +154,6 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): _LOGGER.debug("IPP Error", exc_info=True) return self.async_abort(reason="ipp_error") - unique_id = self.discovery_info[CONF_UUID] if not unique_id and info[CONF_UUID]: _LOGGER.debug( "Printer UUID is missing from discovery info. Falling back to IPP UUID" @@ -164,18 +170,24 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): "Unable to determine unique id from discovery info and IPP response" ) - if unique_id: - await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured( - updates={ - CONF_HOST: self.discovery_info[CONF_HOST], - CONF_NAME: self.discovery_info[CONF_NAME], - }, - ) + if unique_id and self.unique_id != unique_id: + await self._async_set_unique_id_and_abort_if_already_configured(unique_id) await self._async_handle_discovery_without_unique_id() return await self.async_step_zeroconf_confirm() + async def _async_set_unique_id_and_abort_if_already_configured( + self, unique_id: str + ) -> None: + """Set the unique ID and abort if already configured.""" + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: self.discovery_info[CONF_HOST], + CONF_NAME: self.discovery_info[CONF_NAME], + }, + ) + async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/tests/components/ipp/fixtures/printer_without_uuid.json b/tests/components/ipp/fixtures/printer_without_uuid.json new file mode 100644 index 00000000000..21f1eb93a32 --- /dev/null +++ b/tests/components/ipp/fixtures/printer_without_uuid.json @@ -0,0 +1,35 @@ +{ + "printer-state": "idle", + "printer-name": "Test Printer", + "printer-location": null, + "printer-make-and-model": "Test HA-1000 Series", + "printer-device-id": "MFG:TEST;CMD:ESCPL2,BDC,D4,D4PX,ESCPR7,END4,GENEP,URF;MDL:HA-1000 Series;CLS:PRINTER;DES:TEST HA-1000 Series;CID:EpsonRGB;FID:FXN,DPA,WFA,ETN,AFN,DAN,WRA;RID:20;DDS:022500;ELG:1000;SN:555534593035345555;URF:CP1,PQ4-5,OB9,OFU0,RS360,SRGB24,W8,DM3,IS1-7-6,V1.4,MT1-3-7-8-10-11-12;", + "printer-uri-supported": [ + "ipps://192.168.1.31:631/ipp/print", + "ipp://192.168.1.31:631/ipp/print" + ], + "uri-authentication-supported": ["none", "none"], + "uri-security-supported": ["tls", "none"], + "printer-info": "Test HA-1000 Series", + "printer-up-time": 30, + "printer-firmware-string-version": "20.23.06HA", + "printer-more-info": "http://192.168.1.31:80/PRESENTATION/BONJOUR", + "marker-names": [ + "Black ink", + "Photo black ink", + "Cyan ink", + "Yellow ink", + "Magenta ink" + ], + "marker-types": [ + "ink-cartridge", + "ink-cartridge", + "ink-cartridge", + "ink-cartridge", + "ink-cartridge" + ], + "marker-colors": ["#000000", "#000000", "#00FFFF", "#FFFF00", "#FF00FF"], + "marker-levels": [58, 98, 91, 95, 73], + "marker-low-levels": [10, 10, 10, 10, 10], + "marker-high-levels": [100, 100, 100, 100, 100] +} diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 69a2bb9287a..0daf8a0f7e0 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for the IPP config flow.""" import dataclasses -from unittest.mock import MagicMock +import json +from unittest.mock import MagicMock, patch from pyipp import ( IPPConnectionError, @@ -8,6 +9,7 @@ from pyipp import ( IPPError, IPPParseError, IPPVersionNotSupportedError, + Printer, ) import pytest @@ -23,7 +25,7 @@ from . import ( MOCK_ZEROCONF_IPPS_SERVICE_INFO, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture pytestmark = pytest.mark.usefixtures("mock_setup_entry") @@ -316,6 +318,31 @@ async def test_zeroconf_with_uuid_device_exists_abort( assert result["reason"] == "already_configured" +async def test_zeroconf_with_uuid_device_exists_abort_new_host( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_ipp_config_flow: MagicMock, +) -> None: + """Test we abort zeroconf flow if printer already configured.""" + mock_config_entry.add_to_hass(hass) + + discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO, host="1.2.3.9") + discovery_info.properties = { + **MOCK_ZEROCONF_IPP_SERVICE_INFO.properties, + "UUID": "cfe92100-67c4-11d4-a45f-f8d027761251", + } + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=discovery_info, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert mock_config_entry.data[CONF_HOST] == "1.2.3.9" + + async def test_zeroconf_empty_unique_id( hass: HomeAssistant, mock_ipp_config_flow: MagicMock, @@ -337,6 +364,21 @@ async def test_zeroconf_empty_unique_id( assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"}, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "EPSON XP-6000 Series" + + assert result["data"] + assert result["data"][CONF_HOST] == "192.168.1.31" + assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251" + + assert result["result"] + assert result["result"].unique_id == "cfe92100-67c4-11d4-a45f-f8d027761251" + async def test_zeroconf_no_unique_id( hass: HomeAssistant, @@ -355,6 +397,21 @@ async def test_zeroconf_no_unique_id( assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"}, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "EPSON XP-6000 Series" + + assert result["data"] + assert result["data"][CONF_HOST] == "192.168.1.31" + assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251" + + assert result["result"] + assert result["result"].unique_id == "cfe92100-67c4-11d4-a45f-f8d027761251" + async def test_full_user_flow_implementation( hass: HomeAssistant, @@ -448,3 +505,45 @@ async def test_full_zeroconf_tls_flow_implementation( assert result["result"] assert result["result"].unique_id == "cfe92100-67c4-11d4-a45f-f8d027761251" + + +async def test_zeroconf_empty_unique_id_uses_serial(hass: HomeAssistant) -> None: + """Test zeroconf flow if printer lacks (empty) unique identification with serial fallback.""" + fixture = await hass.async_add_executor_job( + load_fixture, "ipp/printer_without_uuid.json" + ) + mock_printer_without_uuid = Printer.from_dict(json.loads(fixture)) + mock_printer_without_uuid.unique_id = None + + discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) + discovery_info.properties = { + **MOCK_ZEROCONF_IPP_SERVICE_INFO.properties, + "UUID": "", + } + with patch( + "homeassistant.components.ipp.config_flow.IPP", autospec=True + ) as ipp_mock: + client = ipp_mock.return_value + client.printer.return_value = mock_printer_without_uuid + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=discovery_info, + ) + + assert result["type"] == FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"}, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "EPSON XP-6000 Series" + + assert result["data"] + assert result["data"][CONF_HOST] == "192.168.1.31" + assert result["data"][CONF_UUID] == "" + + assert result["result"] + assert result["result"].unique_id == "555534593035345555" From 42f62485cfc762f3457874bbe7972078522fd21c Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 9 Sep 2023 17:45:19 +0200 Subject: [PATCH 136/202] Bump pymodbus to v3.5.2 (#99988) --- homeassistant/components/modbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index bef85f1d20d..b70055e5fbe 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_polling", "loggers": ["pymodbus"], "quality_scale": "gold", - "requirements": ["pymodbus==3.5.1"] + "requirements": ["pymodbus==3.5.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3086c5398a7..ea0c146ae25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1851,7 +1851,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.5.1 +pymodbus==3.5.2 # homeassistant.components.monoprice pymonoprice==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c770f5936a..7339d2a6ba4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1370,7 +1370,7 @@ pymeteoclimatic==0.0.6 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.5.1 +pymodbus==3.5.2 # homeassistant.components.monoprice pymonoprice==0.4 From 82a5615d7ddadcae13b157ddd4f7cda10de92d63 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Sat, 9 Sep 2023 19:16:27 +0200 Subject: [PATCH 137/202] Bump pywaze to 0.4.0 (#99995) bump pywaze from 0.3.0 to 0.4.0 --- homeassistant/components/waze_travel_time/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index 3f1f8c6d67b..c72d9b1dbad 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", "iot_class": "cloud_polling", "loggers": ["pywaze", "homeassistant.helpers.location"], - "requirements": ["pywaze==0.3.0"] + "requirements": ["pywaze==0.4.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index ea0c146ae25..7f7424431cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2231,7 +2231,7 @@ pyvlx==0.2.20 pyvolumio==0.1.5 # homeassistant.components.waze_travel_time -pywaze==0.3.0 +pywaze==0.4.0 # homeassistant.components.html5 pywebpush==1.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7339d2a6ba4..3f03459b0a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1639,7 +1639,7 @@ pyvizio==0.1.61 pyvolumio==0.1.5 # homeassistant.components.waze_travel_time -pywaze==0.3.0 +pywaze==0.4.0 # homeassistant.components.html5 pywebpush==1.9.2 From a19bc71300d703f052159361a59d35a7753edbcb Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 10 Sep 2023 16:02:42 +0100 Subject: [PATCH 138/202] Bump systembridgeconnector to 3.8.2 (#100051) Update systembridgeconnector to 3.8.2 --- homeassistant/components/system_bridge/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index c0f89c16339..bcc6189c8ef 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -10,6 +10,6 @@ "iot_class": "local_push", "loggers": ["systembridgeconnector"], "quality_scale": "silver", - "requirements": ["systembridgeconnector==3.4.9"], + "requirements": ["systembridgeconnector==3.8.2"], "zeroconf": ["_system-bridge._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 7f7424431cf..727fa06629c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2504,7 +2504,7 @@ swisshydrodata==0.1.0 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridgeconnector==3.4.9 +systembridgeconnector==3.8.2 # homeassistant.components.tailscale tailscale==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f03459b0a1..a6e5369dcba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1837,7 +1837,7 @@ sunwatcher==0.2.1 surepy==0.8.0 # homeassistant.components.system_bridge -systembridgeconnector==3.4.9 +systembridgeconnector==3.8.2 # homeassistant.components.tailscale tailscale==0.2.0 From a651e9df1d3d59047240db840f6154afccde6ebc Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 10 Sep 2023 19:09:21 +0200 Subject: [PATCH 139/202] Bump aiovodafone to 0.2.0 (#100062) bump aiovodafone to 0.2.0 --- homeassistant/components/vodafone_station/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vodafone_station/manifest.json b/homeassistant/components/vodafone_station/manifest.json index 5470cdd684c..68e7665b5ac 100644 --- a/homeassistant/components/vodafone_station/manifest.json +++ b/homeassistant/components/vodafone_station/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/vodafone_station", "iot_class": "local_polling", "loggers": ["aiovodafone"], - "requirements": ["aiovodafone==0.1.0"] + "requirements": ["aiovodafone==0.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 727fa06629c..ef2b818bc79 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -369,7 +369,7 @@ aiounifi==61 aiovlc==0.1.0 # homeassistant.components.vodafone_station -aiovodafone==0.1.0 +aiovodafone==0.2.0 # homeassistant.components.waqi aiowaqi==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6e5369dcba..a644b9cf97a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -344,7 +344,7 @@ aiounifi==61 aiovlc==0.1.0 # homeassistant.components.vodafone_station -aiovodafone==0.1.0 +aiovodafone==0.2.0 # homeassistant.components.watttime aiowatttime==0.1.1 From 55a4346460126be6b0ff7d9260e3a3c49bbea2bc Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 11 Sep 2023 08:49:10 +0200 Subject: [PATCH 140/202] Remove Comelit alarm data retrieval (#100067) fix: remove alarm data retrieval --- homeassistant/components/comelit/coordinator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/comelit/coordinator.py b/homeassistant/components/comelit/coordinator.py index beb7266c403..1affd5046fe 100644 --- a/homeassistant/components/comelit/coordinator.py +++ b/homeassistant/components/comelit/coordinator.py @@ -44,7 +44,6 @@ class ComelitSerialBridge(DataUpdateCoordinator): raise ConfigEntryAuthFailed devices_data = await self.api.get_all_devices() - alarm_data = await self.api.get_alarm_config() await self.api.logout() - return devices_data | alarm_data + return devices_data From 2cb84274ec04c0ce2e5df48ca629dee433222c5f Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Tue, 12 Sep 2023 09:59:12 -0400 Subject: [PATCH 141/202] Fix addon slug validation (#100070) * Fix addon slug validation * Don't redefine compile --- homeassistant/components/hassio/__init__.py | 5 ++- tests/components/hassio/test_init.py | 35 +++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 72fb5ce5110..270309149ef 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -6,6 +6,7 @@ from contextlib import suppress from datetime import datetime, timedelta import logging import os +import re from typing import Any, NamedTuple import voluptuous as vol @@ -149,10 +150,12 @@ SERVICE_BACKUP_PARTIAL = "backup_partial" SERVICE_RESTORE_FULL = "restore_full" SERVICE_RESTORE_PARTIAL = "restore_partial" +VALID_ADDON_SLUG = vol.Match(re.compile(r"^[-_.A-Za-z0-9]+$")) + def valid_addon(value: Any) -> str: """Validate value is a valid addon slug.""" - value = cv.slug(value) + value = VALID_ADDON_SLUG(value) hass: HomeAssistant | None = None with suppress(HomeAssistantError): diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 31ee73013da..48f52ee7c24 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -633,6 +633,41 @@ async def test_invalid_service_calls( ) +async def test_addon_service_call_with_complex_slug( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Addon slugs can have ., - and _, confirm that passes validation.""" + supervisor_mock_data = { + "version_latest": "1.0.0", + "version": "1.0.0", + "auto_update": True, + "addons": [ + { + "name": "test.a_1-2", + "slug": "test.a_1-2", + "state": "stopped", + "update_available": False, + "version": "1.0.0", + "version_latest": "1.0.0", + "repository": "core", + "icon": False, + }, + ], + } + with patch.dict(os.environ, MOCK_ENVIRON), patch( + "homeassistant.components.hassio.HassIO.is_connected", + return_value=None, + ), patch( + "homeassistant.components.hassio.HassIO.get_supervisor_info", + return_value=supervisor_mock_data, + ): + assert await async_setup_component(hass, "hassio", {}) + await hass.async_block_till_done() + + await hass.services.async_call("hassio", "addon_start", {"addon": "test.a_1-2"}) + + async def test_service_calls_core( hassio_env, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: From df718ca509d00dc6ffcb38cf7cf8a079e4236fa4 Mon Sep 17 00:00:00 2001 From: Greig Sheridan Date: Mon, 11 Sep 2023 19:16:21 +1200 Subject: [PATCH 142/202] Remove duplicated word in enphase description text (#100098) --- homeassistant/components/enphase_envoy/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index ae0ac31413c..92eca38ef20 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -3,7 +3,7 @@ "flow_title": "{serial} ({host})", "step": { "user": { - "description": "For firmware version 7.0 and later, enter the Enphase cloud credentials, for older models models, enter username `installer` without a password.", + "description": "For firmware version 7.0 and later, enter the Enphase cloud credentials, for older models, enter username `installer` without a password.", "data": { "host": "[%key:common::config_flow::data::host%]", "username": "[%key:common::config_flow::data::username%]", From 7a4a792b7bf23e42ec332cf17cebc140c8163c8c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Sep 2023 14:33:43 +0200 Subject: [PATCH 143/202] Fix TriggerEntity.async_added_to_hass (#100119) --- .../components/template/trigger_entity.py | 3 +- tests/components/template/test_sensor.py | 44 ++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/template/trigger_entity.py b/homeassistant/components/template/trigger_entity.py index ca2f7240086..5f5fbe5b99a 100644 --- a/homeassistant/components/template/trigger_entity.py +++ b/homeassistant/components/template/trigger_entity.py @@ -23,8 +23,7 @@ class TriggerEntity(TriggerBaseEntity, CoordinatorEntity[TriggerUpdateCoordinato async def async_added_to_hass(self) -> None: """Handle being added to Home Assistant.""" - await TriggerBaseEntity.async_added_to_hass(self) - await CoordinatorEntity.async_added_to_hass(self) # type: ignore[arg-type] + await super().async_added_to_hass() if self.coordinator.data is not None: self._process_data() diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 1bd1e797c05..4010bb34d2d 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -1,7 +1,7 @@ """The test for the Template sensor platform.""" from asyncio import Event from datetime import timedelta -from unittest.mock import patch +from unittest.mock import ANY, patch import pytest @@ -1140,6 +1140,48 @@ async def test_trigger_entity( assert state.context is context +@pytest.mark.parametrize(("count", "domain"), [(1, "template")]) +@pytest.mark.parametrize( + "config", + [ + { + "template": [ + { + "trigger": {"platform": "event", "event_type": "test_event"}, + "sensors": { + "hello": { + "friendly_name": "Hello Name", + "value_template": "{{ trigger.event.data.beer }}", + "entity_picture_template": "{{ '/local/dogs.png' }}", + "icon_template": "{{ 'mdi:pirate' }}", + "attribute_templates": { + "last": "{{now().strftime('%D %X')}}", + "history_1": "{{this.attributes.last|default('Not yet set')}}", + }, + }, + }, + }, + ], + }, + ], +) +async def test_trigger_entity_runs_once( + hass: HomeAssistant, start_ha, entity_registry: er.EntityRegistry +) -> None: + """Test trigger entity handles a trigger once.""" + state = hass.states.get("sensor.hello_name") + assert state is not None + assert state.state == STATE_UNKNOWN + + hass.bus.async_fire("test_event", {"beer": 2}) + await hass.async_block_till_done() + + state = hass.states.get("sensor.hello_name") + assert state.state == "2" + assert state.attributes.get("last") == ANY + assert state.attributes.get("history_1") == "Not yet set" + + @pytest.mark.parametrize(("count", "domain"), [(1, "template")]) @pytest.mark.parametrize( "config", From 8e2fa67cfd385788e6ce970f328f25dcf9abb647 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Sep 2023 18:35:48 +0200 Subject: [PATCH 144/202] Bump hatasmota to 0.7.2 (#100129) --- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_light.py | 32 +++++++++++++++++++ tests/components/tasmota/test_switch.py | 32 +++++++++++++++++++ 5 files changed, 67 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 9843f64fc25..fa34665cd73 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["hatasmota"], "mqtt": ["tasmota/discovery/#"], - "requirements": ["HATasmota==0.7.1"] + "requirements": ["HATasmota==0.7.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index ef2b818bc79..2ca4a3b85ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -29,7 +29,7 @@ DoorBirdPy==2.1.0 HAP-python==4.7.1 # homeassistant.components.tasmota -HATasmota==0.7.1 +HATasmota==0.7.2 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a644b9cf97a..7818564c784 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -28,7 +28,7 @@ DoorBirdPy==2.1.0 HAP-python==4.7.1 # homeassistant.components.tasmota -HATasmota==0.7.1 +HATasmota==0.7.2 # homeassistant.components.doods # homeassistant.components.generic diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 5c8339a6f89..82fa89c5280 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -1835,3 +1835,35 @@ async def test_entity_id_update_discovery_update( await help_test_entity_id_update_discovery_update( hass, mqtt_mock, Platform.LIGHT, config ) + + +async def test_no_device_name( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test name of lights when no device name is set. + + When the device name is not set, Tasmota uses friendly name 1 as device naem. + This test ensures that case is handled correctly. + """ + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Light 1" + config["fn"][0] = "Light 1" + config["fn"][1] = "Light 2" + config["rl"][0] = 2 + config["rl"][1] = 2 + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + state = hass.states.get("light.light_1") + assert state is not None + assert state.attributes["friendly_name"] == "Light 1" + + state = hass.states.get("light.light_1_light_2") + assert state is not None + assert state.attributes["friendly_name"] == "Light 1 Light 2" diff --git a/tests/components/tasmota/test_switch.py b/tests/components/tasmota/test_switch.py index b8d0ed2d060..54d94b46fe8 100644 --- a/tests/components/tasmota/test_switch.py +++ b/tests/components/tasmota/test_switch.py @@ -283,3 +283,35 @@ async def test_entity_id_update_discovery_update( await help_test_entity_id_update_discovery_update( hass, mqtt_mock, Platform.SWITCH, config ) + + +async def test_no_device_name( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test name of switches when no device name is set. + + When the device name is not set, Tasmota uses friendly name 1 as device naem. + This test ensures that case is handled correctly. + """ + config = copy.deepcopy(DEFAULT_CONFIG) + config["dn"] = "Relay 1" + config["fn"][0] = "Relay 1" + config["fn"][1] = "Relay 2" + config["rl"][0] = 1 + config["rl"][1] = 1 + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.relay_1") + assert state is not None + assert state.attributes["friendly_name"] == "Relay 1" + + state = hass.states.get("switch.relay_1_relay_2") + assert state is not None + assert state.attributes["friendly_name"] == "Relay 1 Relay 2" From 04549925c296365bfed4966274b0efbeb0f49021 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 11 Sep 2023 23:06:21 +0200 Subject: [PATCH 145/202] Update frontend to 20230911.0 (#100139) --- 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 58de25fc03d..6291e3a237e 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230908.0"] + "requirements": ["home-assistant-frontend==20230911.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7bd0c843e6a..bd47d696230 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.70.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 -home-assistant-frontend==20230908.0 +home-assistant-frontend==20230911.0 home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2ca4a3b85ae..30112fe9b59 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -994,7 +994,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230908.0 +home-assistant-frontend==20230911.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7818564c784..cef0f2493e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230908.0 +home-assistant-frontend==20230911.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 From 5068846fc996bad06808cfc80f11438b90ade067 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 11 Sep 2023 21:50:29 +0200 Subject: [PATCH 146/202] Fix devices not always reporting IP - bump aiounifi to v62 (#100149) --- homeassistant/components/unifi/device_tracker.py | 4 ++-- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index fcfe71a2858..71b0a9869a9 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -139,7 +139,7 @@ class UnifiEntityTrackerDescriptionMixin(Generic[HandlerT, ApiItemT]): """Device tracker local functions.""" heartbeat_timedelta_fn: Callable[[UniFiController, str], timedelta] - ip_address_fn: Callable[[aiounifi.Controller, str], str] + ip_address_fn: Callable[[aiounifi.Controller, str], str | None] is_connected_fn: Callable[[UniFiController, str], bool] hostname_fn: Callable[[aiounifi.Controller, str], str | None] @@ -249,7 +249,7 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): return self.entity_description.hostname_fn(self.controller.api, self._obj_id) @property - def ip_address(self) -> str: + def ip_address(self) -> str | None: """Return the primary ip address of the device.""" return self.entity_description.ip_address_fn(self.controller.api, self._obj_id) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index f20e5f9e4ac..8734fd7dce5 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==61"], + "requirements": ["aiounifi==62"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 30112fe9b59..b9b43688779 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -363,7 +363,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==61 +aiounifi==62 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cef0f2493e9..bcc92a956eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,7 +338,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==61 +aiounifi==62 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From d1bc6df14f81a89725a3157b131530240991a22e Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 12 Sep 2023 04:30:50 +0200 Subject: [PATCH 147/202] Fix AVM Fritz!Tools update entity (#100151) * move update entity to coordinator * fix tests --- homeassistant/components/fritz/common.py | 11 ++++-- homeassistant/components/fritz/update.py | 38 ++++++++++++------ tests/components/fritz/test_update.py | 49 +++++++++++++++--------- 3 files changed, 63 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 69773778121..76368175ca0 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -1096,7 +1096,7 @@ class FritzBoxBaseEntity: class FritzRequireKeysMixin: """Fritz entity description mix in.""" - value_fn: Callable[[FritzStatus, Any], Any] + value_fn: Callable[[FritzStatus, Any], Any] | None @dataclass @@ -1118,9 +1118,12 @@ class FritzBoxBaseCoordinatorEntity(update_coordinator.CoordinatorEntity[AvmWrap ) -> None: """Init device info class.""" super().__init__(avm_wrapper) - self.async_on_remove( - avm_wrapper.register_entity_updates(description.key, description.value_fn) - ) + if description.value_fn is not None: + self.async_on_remove( + avm_wrapper.register_entity_updates( + description.key, description.value_fn + ) + ) self.entity_description = description self._device_name = device_name self._attr_unique_id = f"{avm_wrapper.unique_id}-{description.key}" diff --git a/homeassistant/components/fritz/update.py b/homeassistant/components/fritz/update.py index 03cffc3cae6..80cbe1f4c5c 100644 --- a/homeassistant/components/fritz/update.py +++ b/homeassistant/components/fritz/update.py @@ -1,20 +1,31 @@ """Support for AVM FRITZ!Box update platform.""" from __future__ import annotations +from dataclasses import dataclass import logging from typing import Any -from homeassistant.components.update import UpdateEntity, UpdateEntityFeature +from homeassistant.components.update import ( + UpdateEntity, + UpdateEntityDescription, + UpdateEntityFeature, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .common import AvmWrapper, FritzBoxBaseEntity +from .common import AvmWrapper, FritzBoxBaseCoordinatorEntity, FritzEntityDescription from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +@dataclass +class FritzUpdateEntityDescription(UpdateEntityDescription, FritzEntityDescription): + """Describes Fritz update entity.""" + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -27,11 +38,13 @@ async def async_setup_entry( async_add_entities(entities) -class FritzBoxUpdateEntity(FritzBoxBaseEntity, UpdateEntity): +class FritzBoxUpdateEntity(FritzBoxBaseCoordinatorEntity, UpdateEntity): """Mixin for update entity specific attributes.""" + _attr_entity_category = EntityCategory.CONFIG _attr_supported_features = UpdateEntityFeature.INSTALL _attr_title = "FRITZ!OS" + entity_description: FritzUpdateEntityDescription def __init__( self, @@ -39,29 +52,30 @@ class FritzBoxUpdateEntity(FritzBoxBaseEntity, UpdateEntity): device_friendly_name: str, ) -> None: """Init FRITZ!Box connectivity class.""" - self._attr_name = f"{device_friendly_name} FRITZ!OS" - self._attr_unique_id = f"{avm_wrapper.unique_id}-update" - super().__init__(avm_wrapper, device_friendly_name) + description = FritzUpdateEntityDescription( + key="update", name="FRITZ!OS", value_fn=None + ) + super().__init__(avm_wrapper, device_friendly_name, description) @property def installed_version(self) -> str | None: """Version currently in use.""" - return self._avm_wrapper.current_firmware + return self.coordinator.current_firmware @property def latest_version(self) -> str | None: """Latest version available for install.""" - if self._avm_wrapper.update_available: - return self._avm_wrapper.latest_firmware - return self._avm_wrapper.current_firmware + if self.coordinator.update_available: + return self.coordinator.latest_firmware + return self.coordinator.current_firmware @property def release_url(self) -> str | None: """URL to the full release notes of the latest version available.""" - return self._avm_wrapper.release_url + return self.coordinator.release_url async def async_install( self, version: str | None, backup: bool, **kwargs: Any ) -> None: """Install an update.""" - await self._avm_wrapper.async_trigger_firmware_update() + await self.coordinator.async_trigger_firmware_update() diff --git a/tests/components/fritz/test_update.py b/tests/components/fritz/test_update.py index dbff4713553..bc677e28ebe 100644 --- a/tests/components/fritz/test_update.py +++ b/tests/components/fritz/test_update.py @@ -8,11 +8,25 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from .const import MOCK_FIRMWARE_AVAILABLE, MOCK_FIRMWARE_RELEASE_URL, MOCK_USER_DATA +from .const import ( + MOCK_FB_SERVICES, + MOCK_FIRMWARE_AVAILABLE, + MOCK_FIRMWARE_RELEASE_URL, + MOCK_USER_DATA, +) from tests.common import MockConfigEntry from tests.typing import ClientSessionGenerator +AVAILABLE_UPDATE = { + "UserInterface1": { + "GetInfo": { + "NewX_AVM-DE_Version": MOCK_FIRMWARE_AVAILABLE, + "NewX_AVM-DE_InfoURL": MOCK_FIRMWARE_RELEASE_URL, + }, + } +} + async def test_update_entities_initialized( hass: HomeAssistant, @@ -41,23 +55,21 @@ async def test_update_available( ) -> None: """Test update entities.""" - with patch( - "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", - return_value=(True, MOCK_FIRMWARE_AVAILABLE, MOCK_FIRMWARE_RELEASE_URL), - ): - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) - entry.add_to_hass(hass) + fc_class_mock().override_services({**MOCK_FB_SERVICES, **AVAILABLE_UPDATE}) - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) - update = hass.states.get("update.mock_title_fritz_os") - assert update is not None - assert update.state == "on" - assert update.attributes.get("installed_version") == "7.29" - assert update.attributes.get("latest_version") == MOCK_FIRMWARE_AVAILABLE - assert update.attributes.get("release_url") == MOCK_FIRMWARE_RELEASE_URL + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + update = hass.states.get("update.mock_title_fritz_os") + assert update is not None + assert update.state == "on" + assert update.attributes.get("installed_version") == "7.29" + assert update.attributes.get("latest_version") == MOCK_FIRMWARE_AVAILABLE + assert update.attributes.get("release_url") == MOCK_FIRMWARE_RELEASE_URL async def test_no_update_available( @@ -90,10 +102,9 @@ async def test_available_update_can_be_installed( ) -> None: """Test update entities.""" + fc_class_mock().override_services({**MOCK_FB_SERVICES, **AVAILABLE_UPDATE}) + with patch( - "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", - return_value=(True, MOCK_FIRMWARE_AVAILABLE, MOCK_FIRMWARE_RELEASE_URL), - ), patch( "homeassistant.components.fritz.common.FritzBoxTools.async_trigger_firmware_update", return_value=True, ) as mocked_update_call: From 68b0f05758fb286f4c170dab4ce872d7a6ccabef Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 12 Sep 2023 04:23:55 +0200 Subject: [PATCH 148/202] Bump ZHA dependencies (#100156) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index cce223fac11..c3fa6b1ff01 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -25,9 +25,9 @@ "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.103", - "zigpy-deconz==0.21.0", + "zigpy-deconz==0.21.1", "zigpy==0.57.1", - "zigpy-xbee==0.18.1", + "zigpy-xbee==0.18.2", "zigpy-zigate==0.11.0", "zigpy-znp==0.11.4", "universal-silabs-flasher==0.0.13" diff --git a/requirements_all.txt b/requirements_all.txt index b9b43688779..8d7ed7bff24 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2781,10 +2781,10 @@ zhong-hong-hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.21.0 +zigpy-deconz==0.21.1 # homeassistant.components.zha -zigpy-xbee==0.18.1 +zigpy-xbee==0.18.2 # homeassistant.components.zha zigpy-zigate==0.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bcc92a956eb..a6b521e5c1e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2045,10 +2045,10 @@ zeversolar==0.3.1 zha-quirks==0.0.103 # homeassistant.components.zha -zigpy-deconz==0.21.0 +zigpy-deconz==0.21.1 # homeassistant.components.zha -zigpy-xbee==0.18.1 +zigpy-xbee==0.18.2 # homeassistant.components.zha zigpy-zigate==0.11.0 From 1cd80c5b78ccca3b030b3120ddc976f3d562d6d2 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 11 Sep 2023 23:03:47 -0400 Subject: [PATCH 149/202] Bump zwave-js-server-python to 0.51.2 (#100159) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 080074451bd..4ea46099f14 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["zwave_js_server"], "quality_scale": "platinum", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.51.1"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.51.2"], "usb": [ { "vid": "0658", diff --git a/requirements_all.txt b/requirements_all.txt index 8d7ed7bff24..e48adf70f51 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2799,7 +2799,7 @@ zigpy==0.57.1 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.51.1 +zwave-js-server-python==0.51.2 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6b521e5c1e..74485ef47ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2060,7 +2060,7 @@ zigpy-znp==0.11.4 zigpy==0.57.1 # homeassistant.components.zwave_js -zwave-js-server-python==0.51.1 +zwave-js-server-python==0.51.2 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 From 3c27283fc1252a68272b55e2d7d3d5eab2b2ff36 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 12 Sep 2023 08:59:39 +0200 Subject: [PATCH 150/202] Adjust tasmota sensor device class and icon mapping (#100168) --- homeassistant/components/tasmota/sensor.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index ddcdb3e8c26..8365fd97ca4 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -88,12 +88,10 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { hc.SENSOR_COLOR_GREEN: {ICON: "mdi:palette"}, hc.SENSOR_COLOR_RED: {ICON: "mdi:palette"}, hc.SENSOR_CURRENT: { - ICON: "mdi:alpha-a-circle-outline", DEVICE_CLASS: SensorDeviceClass.CURRENT, STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_CURRENTNEUTRAL: { - ICON: "mdi:alpha-a-circle-outline", DEVICE_CLASS: SensorDeviceClass.CURRENT, STATE_CLASS: SensorStateClass.MEASUREMENT, }, @@ -103,11 +101,14 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_DISTANCE: { - ICON: "mdi:leak", DEVICE_CLASS: SensorDeviceClass.DISTANCE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_ECO2: {ICON: "mdi:molecule-co2"}, + hc.SENSOR_ENERGY: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, hc.SENSOR_FREQUENCY: { DEVICE_CLASS: SensorDeviceClass.FREQUENCY, STATE_CLASS: SensorStateClass.MEASUREMENT, @@ -122,10 +123,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { }, hc.SENSOR_STATUS_IP: {ICON: "mdi:ip-network"}, hc.SENSOR_STATUS_LINK_COUNT: {ICON: "mdi:counter"}, - hc.SENSOR_MOISTURE: { - DEVICE_CLASS: SensorDeviceClass.MOISTURE, - ICON: "mdi:cup-water", - }, + hc.SENSOR_MOISTURE: {DEVICE_CLASS: SensorDeviceClass.MOISTURE}, hc.SENSOR_STATUS_MQTT_COUNT: {ICON: "mdi:counter"}, hc.SENSOR_PB0_3: {ICON: "mdi:flask"}, hc.SENSOR_PB0_5: {ICON: "mdi:flask"}, @@ -146,7 +144,6 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_POWERFACTOR: { - ICON: "mdi:alpha-f-circle-outline", DEVICE_CLASS: SensorDeviceClass.POWER_FACTOR, STATE_CLASS: SensorStateClass.MEASUREMENT, }, @@ -162,7 +159,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { DEVICE_CLASS: SensorDeviceClass.PRESSURE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, - hc.SENSOR_PROXIMITY: {DEVICE_CLASS: SensorDeviceClass.DISTANCE, ICON: "mdi:ruler"}, + hc.SENSOR_PROXIMITY: {ICON: "mdi:ruler"}, hc.SENSOR_REACTIVE_ENERGYEXPORT: {STATE_CLASS: SensorStateClass.TOTAL}, hc.SENSOR_REACTIVE_ENERGYIMPORT: {STATE_CLASS: SensorStateClass.TOTAL}, hc.SENSOR_REACTIVE_POWERUSAGE: { @@ -195,11 +192,10 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_TVOC: {ICON: "mdi:air-filter"}, hc.SENSOR_VOLTAGE: { - ICON: "mdi:alpha-v-circle-outline", + DEVICE_CLASS: SensorDeviceClass.VOLTAGE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_WEIGHT: { - ICON: "mdi:scale", DEVICE_CLASS: SensorDeviceClass.WEIGHT, STATE_CLASS: SensorStateClass.MEASUREMENT, }, From 1b494fb4bae44cfa322a85e1510dfd51ee1992ee Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 12 Sep 2023 09:58:05 +0200 Subject: [PATCH 151/202] Bump hatasmota to 0.7.3 (#100169) --- .../components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/sensor.py | 1 - requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_sensor.py | 214 ++++++++++++++++++ 5 files changed, 217 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index fa34665cd73..42fc849a2cf 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["hatasmota"], "mqtt": ["tasmota/discovery/#"], - "requirements": ["HATasmota==0.7.2"] + "requirements": ["HATasmota==0.7.3"] } diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 8365fd97ca4..e718c0fdcf4 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -216,7 +216,6 @@ SENSOR_UNIT_MAP = { hc.LIGHT_LUX: LIGHT_LUX, hc.MASS_KILOGRAMS: UnitOfMass.KILOGRAMS, hc.PERCENTAGE: PERCENTAGE, - hc.POWER_FACTOR: None, hc.POWER_WATT: UnitOfPower.WATT, hc.PRESSURE_HPA: UnitOfPressure.HPA, hc.REACTIVE_POWER: POWER_VOLT_AMPERE_REACTIVE, diff --git a/requirements_all.txt b/requirements_all.txt index e48adf70f51..f035cbe863b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -29,7 +29,7 @@ DoorBirdPy==2.1.0 HAP-python==4.7.1 # homeassistant.components.tasmota -HATasmota==0.7.2 +HATasmota==0.7.3 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 74485ef47ec..fdd5656c8c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -28,7 +28,7 @@ DoorBirdPy==2.1.0 HAP-python==4.7.1 # homeassistant.components.tasmota -HATasmota==0.7.2 +HATasmota==0.7.3 # homeassistant.components.doods # homeassistant.components.generic diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index c14c7ffe53c..2f50a84ffdd 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -137,6 +137,27 @@ DICT_SENSOR_CONFIG_2 = { } } +NUMBERED_SENSOR_CONFIG = { + "sn": { + "Time": "2020-09-25T12:47:15", + "ANALOG": { + "Temperature1": 2.4, + "Temperature2": 2.4, + "Illuminance3": 2.4, + }, + "TempUnit": "C", + } +} + +NUMBERED_SENSOR_CONFIG_2 = { + "sn": { + "Time": "2020-09-25T12:47:15", + "ANALOG": { + "CTEnergy1": {"Energy": 0.5, "Power": 2300, "Voltage": 230, "Current": 10}, + }, + "TempUnit": "C", + } +} TEMPERATURE_SENSOR_CONFIG = { "sn": { @@ -343,6 +364,118 @@ TEMPERATURE_SENSOR_CONFIG = { }, ), ), + ( + NUMBERED_SENSOR_CONFIG, + [ + "sensor.tasmota_analog_temperature1", + "sensor.tasmota_analog_temperature2", + "sensor.tasmota_analog_illuminance3", + ], + ( + ( + '{"ANALOG":{"Temperature1":1.2,"Temperature2":3.4,' + '"Illuminance3": 5.6}}' + ), + ( + '{"StatusSNS":{"ANALOG":{"Temperature1": 7.8,"Temperature2": 9.0,' + '"Illuminance3":1.2}}}' + ), + ), + ( + { + "sensor.tasmota_analog_temperature1": { + "state": "1.2", + "attributes": { + "device_class": "temperature", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + "unit_of_measurement": "°C", + }, + }, + "sensor.tasmota_analog_temperature2": { + "state": "3.4", + "attributes": { + "device_class": "temperature", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + "unit_of_measurement": "°C", + }, + }, + "sensor.tasmota_analog_illuminance3": { + "state": "5.6", + "attributes": { + "device_class": "illuminance", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + "unit_of_measurement": "lx", + }, + }, + }, + { + "sensor.tasmota_analog_temperature1": {"state": "7.8"}, + "sensor.tasmota_analog_temperature2": {"state": "9.0"}, + "sensor.tasmota_analog_illuminance3": {"state": "1.2"}, + }, + ), + ), + ( + NUMBERED_SENSOR_CONFIG_2, + [ + "sensor.tasmota_analog_ctenergy1_energy", + "sensor.tasmota_analog_ctenergy1_power", + "sensor.tasmota_analog_ctenergy1_voltage", + "sensor.tasmota_analog_ctenergy1_current", + ], + ( + ( + '{"ANALOG":{"CTEnergy1":' + '{"Energy":0.5,"Power":2300,"Voltage":230,"Current":10}}}' + ), + ( + '{"StatusSNS":{"ANALOG":{"CTEnergy1":' + '{"Energy":1.0,"Power":1150,"Voltage":230,"Current":5}}}}' + ), + ), + ( + { + "sensor.tasmota_analog_ctenergy1_energy": { + "state": "0.5", + "attributes": { + "device_class": "energy", + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + "unit_of_measurement": "kWh", + }, + }, + "sensor.tasmota_analog_ctenergy1_power": { + "state": "2300", + "attributes": { + "device_class": "power", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + "unit_of_measurement": "W", + }, + }, + "sensor.tasmota_analog_ctenergy1_voltage": { + "state": "230", + "attributes": { + "device_class": "voltage", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + "unit_of_measurement": "V", + }, + }, + "sensor.tasmota_analog_ctenergy1_current": { + "state": "10", + "attributes": { + "device_class": "current", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + "unit_of_measurement": "A", + }, + }, + }, + { + "sensor.tasmota_analog_ctenergy1_energy": {"state": "1.0"}, + "sensor.tasmota_analog_ctenergy1_power": {"state": "1150"}, + "sensor.tasmota_analog_ctenergy1_voltage": {"state": "230"}, + "sensor.tasmota_analog_ctenergy1_current": {"state": "5"}, + }, + ), + ), ], ) async def test_controlling_state_via_mqtt( @@ -409,6 +542,87 @@ async def test_controlling_state_via_mqtt( assert state.attributes.get(attribute) == expected +@pytest.mark.parametrize( + ("sensor_config", "entity_ids", "states"), + [ + ( + # The AS33935 energy sensor is not reporting energy in W + {"sn": {"Time": "2020-09-25T12:47:15", "AS3935": {"Energy": None}}}, + ["sensor.tasmota_as3935_energy"], + { + "sensor.tasmota_as3935_energy": { + "device_class": None, + "state_class": None, + "unit_of_measurement": None, + }, + }, + ), + ( + # The AS33935 energy sensor is not reporting energy in W + {"sn": {"Time": "2020-09-25T12:47:15", "LD2410": {"Energy": None}}}, + ["sensor.tasmota_ld2410_energy"], + { + "sensor.tasmota_ld2410_energy": { + "device_class": None, + "state_class": None, + "unit_of_measurement": None, + }, + }, + ), + ( + # Check other energy sensors work + {"sn": {"Time": "2020-09-25T12:47:15", "Other": {"Energy": None}}}, + ["sensor.tasmota_other_energy"], + { + "sensor.tasmota_other_energy": { + "device_class": "energy", + "state_class": "total", + "unit_of_measurement": "kWh", + }, + }, + ), + ], +) +async def test_quantity_override( + hass: HomeAssistant, + mqtt_mock: MqttMockHAClient, + setup_tasmota, + sensor_config, + entity_ids, + states, +) -> None: + """Test quantity override for certain sensors.""" + entity_reg = er.async_get(hass) + config = copy.deepcopy(DEFAULT_CONFIG) + sensor_config = copy.deepcopy(sensor_config) + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/sensors", + json.dumps(sensor_config), + ) + await hass.async_block_till_done() + + for entity_id in entity_ids: + state = hass.states.get(entity_id) + assert state.state == "unavailable" + expected_state = states[entity_id] + for attribute, expected in expected_state.get("attributes", {}).items(): + assert state.attributes.get(attribute) == expected + + entry = entity_reg.async_get(entity_id) + assert entry.disabled is False + assert entry.disabled_by is None + assert entry.entity_category is None + + async def test_bad_indexed_sensor_state_via_mqtt( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota ) -> None: From f13ce5daffc9d62b4ac5453717c814ca87b5fb95 Mon Sep 17 00:00:00 2001 From: Vincent Knoop Pathuis <48653141+vpathuis@users.noreply.github.com> Date: Tue, 12 Sep 2023 10:45:35 +0200 Subject: [PATCH 152/202] Bump Ultraheat to version 0.5.7 (#100172) --- homeassistant/components/landisgyr_heat_meter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/landisgyr_heat_meter/manifest.json b/homeassistant/components/landisgyr_heat_meter/manifest.json index a056f1f6564..1bf77d7ab51 100644 --- a/homeassistant/components/landisgyr_heat_meter/manifest.json +++ b/homeassistant/components/landisgyr_heat_meter/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["usb"], "documentation": "https://www.home-assistant.io/integrations/landisgyr_heat_meter", "iot_class": "local_polling", - "requirements": ["ultraheat-api==0.5.1"] + "requirements": ["ultraheat-api==0.5.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index f035cbe863b..e264f5f52d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2603,7 +2603,7 @@ twitchAPI==3.10.0 uasiren==0.0.1 # homeassistant.components.landisgyr_heat_meter -ultraheat-api==0.5.1 +ultraheat-api==0.5.7 # homeassistant.components.unifiprotect unifi-discovery==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fdd5656c8c0..960a25bc321 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1903,7 +1903,7 @@ twitchAPI==3.10.0 uasiren==0.0.1 # homeassistant.components.landisgyr_heat_meter -ultraheat-api==0.5.1 +ultraheat-api==0.5.7 # homeassistant.components.unifiprotect unifi-discovery==1.1.7 From 63647d96ddb87de3d781122b2d4acd163a63e3de Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 12 Sep 2023 15:22:37 +0200 Subject: [PATCH 153/202] Fix entity name attribute on mqtt entity is not removed on update (#100187) Fix entity name attribute is not remove on update --- homeassistant/components/mqtt/mixins.py | 5 +++ tests/components/mqtt/test_mixins.py | 60 ++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 3b28bc8804f..4eae1fae30c 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -1135,6 +1135,11 @@ class MqttEntity( elif not self._default_to_device_class_name(): # Assign the default name self._attr_name = self._default_name + elif hasattr(self, "_attr_name"): + # An entity name was not set in the config + # don't set the name attribute and derive + # the name from the device_class + delattr(self, "_attr_name") if CONF_DEVICE in config: device_name: str if CONF_NAME not in config[CONF_DEVICE]: diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py index 0647721b4d0..1ca9bf07d72 100644 --- a/tests/components/mqtt/test_mixins.py +++ b/tests/components/mqtt/test_mixins.py @@ -7,6 +7,7 @@ import pytest from homeassistant.components import mqtt, sensor from homeassistant.components.mqtt.sensor import DEFAULT_NAME as DEFAULT_SENSOR_NAME from homeassistant.const import ( + ATTR_FRIENDLY_NAME, EVENT_HOMEASSISTANT_STARTED, EVENT_STATE_CHANGED, Platform, @@ -324,7 +325,6 @@ async def test_default_entity_and_device_name( This is a test helper for the _setup_common_attributes_from_config mixin. """ - # mqtt_mock = await mqtt_mock_entry() events = async_capture_events(hass, ir.EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED) hass.state = CoreState.starting @@ -352,3 +352,61 @@ async def test_default_entity_and_device_name( # Assert that an issues ware registered assert len(events) == issue_events + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) +async def test_name_attribute_is_set_or_not( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test frendly name with device_class set. + + This is a test helper for the _setup_common_attributes_from_config mixin. + """ + await mqtt_mock_entry() + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Gate", "state_topic": "test-topic", "device_class": "door", ' + '"object_id": "gate",' + '"device": {"identifiers": "very_unique", "name": "xyz_door_sensor"}' + "}", + ) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.gate") + + assert state is not None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Gate" + + # Remove the name in a discovery update + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "state_topic": "test-topic", "device_class": "door", ' + '"object_id": "gate",' + '"device": {"identifiers": "very_unique", "name": "xyz_door_sensor"}' + "}", + ) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.gate") + + assert state is not None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Door" + + # Set the name to `null` in a discovery update + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": null, "state_topic": "test-topic", "device_class": "door", ' + '"object_id": "gate",' + '"device": {"identifiers": "very_unique", "name": "xyz_door_sensor"}' + "}", + ) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.gate") + + assert state is not None + assert state.attributes.get(ATTR_FRIENDLY_NAME) is None From 7b9ae6755ac4f2cd641591dcc64d83d2dfce2b3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 12 Sep 2023 15:58:25 +0200 Subject: [PATCH 154/202] Bump hass-nabucasa from 0.70.0 to 0.71.0 (#100193) Bump hass-nabucasa from 0.70.0 to 0.71.1 --- homeassistant/components/cloud/__init__.py | 2 -- homeassistant/components/cloud/const.py | 1 - homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cloud/test_init.py | 1 - 7 files changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 40e5f264caf..4dc242376d9 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -47,7 +47,6 @@ from .const import ( CONF_FILTER, CONF_GOOGLE_ACTIONS, CONF_RELAYER_SERVER, - CONF_REMOTE_SNI_SERVER, CONF_REMOTESTATE_SERVER, CONF_SERVICEHANDLERS_SERVER, CONF_THINGTALK_SERVER, @@ -115,7 +114,6 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_ALEXA_SERVER): str, vol.Optional(CONF_CLOUDHOOK_SERVER): str, vol.Optional(CONF_RELAYER_SERVER): str, - vol.Optional(CONF_REMOTE_SNI_SERVER): str, vol.Optional(CONF_REMOTESTATE_SERVER): str, vol.Optional(CONF_THINGTALK_SERVER): str, vol.Optional(CONF_SERVICEHANDLERS_SERVER): str, diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 7aa39efbf07..bd9d61cde16 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -55,7 +55,6 @@ CONF_ACME_SERVER = "acme_server" CONF_ALEXA_SERVER = "alexa_server" CONF_CLOUDHOOK_SERVER = "cloudhook_server" CONF_RELAYER_SERVER = "relayer_server" -CONF_REMOTE_SNI_SERVER = "remote_sni_server" CONF_REMOTESTATE_SERVER = "remotestate_server" CONF_THINGTALK_SERVER = "thingtalk_server" CONF_SERVICEHANDLERS_SERVER = "servicehandlers_server" diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index a8e28d66291..fe0628f1886 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -8,5 +8,5 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["hass_nabucasa"], - "requirements": ["hass-nabucasa==0.70.0"] + "requirements": ["hass-nabucasa==0.71.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bd47d696230..343c3be1481 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ cryptography==41.0.3 dbus-fast==1.95.2 fnv-hash-fast==0.4.1 ha-av==10.1.1 -hass-nabucasa==0.70.0 +hass-nabucasa==0.71.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 home-assistant-frontend==20230911.0 diff --git a/requirements_all.txt b/requirements_all.txt index e264f5f52d7..f4315943472 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -958,7 +958,7 @@ ha-philipsjs==3.1.0 habitipy==0.2.0 # homeassistant.components.cloud -hass-nabucasa==0.70.0 +hass-nabucasa==0.71.0 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 960a25bc321..cdfdefdb989 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -753,7 +753,7 @@ ha-philipsjs==3.1.0 habitipy==0.2.0 # homeassistant.components.cloud -hass-nabucasa==0.70.0 +hass-nabucasa==0.71.0 # homeassistant.components.conversation hassil==1.2.5 diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 28b531b608c..e12775d5a4a 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -32,7 +32,6 @@ async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None: "relayer_server": "test-relayer-server", "accounts_server": "test-acounts-server", "cloudhook_server": "test-cloudhook-server", - "remote_sni_server": "test-remote-sni-server", "alexa_server": "test-alexa-server", "acme_server": "test-acme-server", "remotestate_server": "test-remotestate-server", From 583ea2fed40e0313be50f5bdde685b4472134377 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Sep 2023 12:05:06 -0400 Subject: [PATCH 155/202] Bumped version to 2023.9.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 cac54748211..08e12ce58ff 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index b74e7914fd7..53f9bf38a32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.1" +version = "2023.9.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f50d47121e31935dc1f7c116f22b1ca9dd6b0b1c Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 10 Sep 2023 14:29:38 +0100 Subject: [PATCH 156/202] Always update unit of measurement of the utility_meter on state change (#99102) --- .../components/utility_meter/sensor.py | 8 +++++ tests/components/utility_meter/test_sensor.py | 33 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index f3e86136f5d..cd581d8c37f 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -17,6 +17,7 @@ from homeassistant.components.sensor import ( SensorExtraStoredData, SensorStateClass, ) +from homeassistant.components.sensor.recorder import _suggest_report_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, @@ -484,6 +485,12 @@ class UtilityMeterSensor(RestoreSensor): DATA_TARIFF_SENSORS ]: sensor.start(new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)) + if self._unit_of_measurement is None: + _LOGGER.warning( + "Source sensor %s has no unit of measurement. Please %s", + self._sensor_source_id, + _suggest_report_issue(self.hass, self._sensor_source_id), + ) if ( adjustment := self.calculate_adjustment(old_state, new_state) @@ -491,6 +498,7 @@ class UtilityMeterSensor(RestoreSensor): # If net_consumption is off, the adjustment must be non-negative self._state += adjustment # type: ignore[operator] # self._state will be set to by the start function if it is None, therefore it always has a valid Decimal value at this line + self._unit_of_measurement = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._last_valid_state = new_state_val self.async_write_ha_state() diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index b8f197a4dee..43d68d87362 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -1460,6 +1460,39 @@ def test_calculate_adjustment_invalid_new_state( assert "Invalid state unknown" in caplog.text +async def test_unit_of_measurement_missing_invalid_new_state( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that a suggestion is created when new_state is missing unit_of_measurement.""" + yaml_config = { + "utility_meter": { + "energy_bill": { + "source": "sensor.energy", + } + } + } + source_entity_id = yaml_config[DOMAIN]["energy_bill"]["source"] + + assert await async_setup_component(hass, DOMAIN, yaml_config) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.states.async_set(source_entity_id, 4, {ATTR_UNIT_OF_MEASUREMENT: None}) + + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_bill") + assert state is not None + assert state.state == "0" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert ( + f"Source sensor {source_entity_id} has no unit of measurement." in caplog.text + ) + + async def test_device_id(hass: HomeAssistant) -> None: """Test for source entity device for Utility Meter.""" device_registry = dr.async_get(hass) From d2b5ffc9fc5cedf07090d50b3a82d568c775c726 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 13 Sep 2023 15:32:03 +0200 Subject: [PATCH 157/202] Netgear catch no info error (#100212) --- .../components/netgear/config_flow.py | 6 +- homeassistant/components/netgear/strings.json | 3 +- tests/components/netgear/test_config_flow.py | 64 ++++++++----------- 3 files changed, 32 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py index da260a2559e..7b74880d011 100644 --- a/homeassistant/components/netgear/config_flow.py +++ b/homeassistant/components/netgear/config_flow.py @@ -190,8 +190,6 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) except CannotLoginException: errors["base"] = "config" - - if errors: return await self._show_setup_form(user_input, errors) config_data = { @@ -204,6 +202,10 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Check if already configured info = await self.hass.async_add_executor_job(api.get_info) + if info is None: + errors["base"] = "info" + return await self._show_setup_form(user_input, errors) + await self.async_set_unique_id(info["SerialNumber"], raise_on_progress=False) self._abort_if_unique_id_configured(updates=config_data) diff --git a/homeassistant/components/netgear/strings.json b/homeassistant/components/netgear/strings.json index 7941d1fe0a7..15766874bc5 100644 --- a/homeassistant/components/netgear/strings.json +++ b/homeassistant/components/netgear/strings.json @@ -11,7 +11,8 @@ } }, "error": { - "config": "Connection or login error: please check your configuration" + "config": "Connection or login error: please check your configuration", + "info": "Failed to get info from router" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/tests/components/netgear/test_config_flow.py b/tests/components/netgear/test_config_flow.py index 248ad3a69ea..37787024fb6 100644 --- a/tests/components/netgear/test_config_flow.py +++ b/tests/components/netgear/test_config_flow.py @@ -76,41 +76,6 @@ def mock_controller_service(): yield service_mock -@pytest.fixture(name="service_5555") -def mock_controller_service_5555(): - """Mock a successful service.""" - with patch( - "homeassistant.components.netgear.async_setup_entry", return_value=True - ), patch("homeassistant.components.netgear.router.Netgear") as service_mock: - service_mock.return_value.get_info = Mock(return_value=ROUTER_INFOS) - service_mock.return_value.port = 5555 - service_mock.return_value.ssl = True - yield service_mock - - -@pytest.fixture(name="service_incomplete") -def mock_controller_service_incomplete(): - """Mock a successful service.""" - router_infos = ROUTER_INFOS.copy() - router_infos.pop("DeviceName") - with patch( - "homeassistant.components.netgear.async_setup_entry", return_value=True - ), patch("homeassistant.components.netgear.router.Netgear") as service_mock: - service_mock.return_value.get_info = Mock(return_value=router_infos) - service_mock.return_value.port = 80 - service_mock.return_value.ssl = False - yield service_mock - - -@pytest.fixture(name="service_failed") -def mock_controller_service_failed(): - """Mock a failed service.""" - with patch("homeassistant.components.netgear.router.Netgear") as service_mock: - service_mock.return_value.login_try_port = Mock(return_value=None) - service_mock.return_value.get_info = Mock(return_value=None) - yield service_mock - - async def test_user(hass: HomeAssistant, service) -> None: """Test user step.""" result = await hass.config_entries.flow.async_init( @@ -138,7 +103,7 @@ async def test_user(hass: HomeAssistant, service) -> None: assert result["data"][CONF_PASSWORD] == PASSWORD -async def test_user_connect_error(hass: HomeAssistant, service_failed) -> None: +async def test_user_connect_error(hass: HomeAssistant, service) -> None: """Test user step with connection failure.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -146,7 +111,23 @@ async def test_user_connect_error(hass: HomeAssistant, service_failed) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" + service.return_value.get_info = Mock(return_value=None) + # Have to provide all config + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + }, + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "info"} + + service.return_value.login_try_port = Mock(return_value=None) + result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -160,7 +141,7 @@ async def test_user_connect_error(hass: HomeAssistant, service_failed) -> None: assert result["errors"] == {"base": "config"} -async def test_user_incomplete_info(hass: HomeAssistant, service_incomplete) -> None: +async def test_user_incomplete_info(hass: HomeAssistant, service) -> None: """Test user step with incomplete device info.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -168,6 +149,10 @@ async def test_user_incomplete_info(hass: HomeAssistant, service_incomplete) -> assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" + router_infos = ROUTER_INFOS.copy() + router_infos.pop("DeviceName") + service.return_value.get_info = Mock(return_value=router_infos) + # Have to provide all config result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -313,7 +298,7 @@ async def test_ssdp(hass: HomeAssistant, service) -> None: assert result["data"][CONF_PASSWORD] == PASSWORD -async def test_ssdp_port_5555(hass: HomeAssistant, service_5555) -> None: +async def test_ssdp_port_5555(hass: HomeAssistant, service) -> None: """Test ssdp step with port 5555.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -332,6 +317,9 @@ async def test_ssdp_port_5555(hass: HomeAssistant, service_5555) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" + service.return_value.port = 5555 + service.return_value.ssl = True + result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD} ) From 094666005ef7fa4b6afc1f47ccadd8b0c0f7fe91 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 13 Sep 2023 02:33:48 -0400 Subject: [PATCH 158/202] Bump python-roborock to 0.34.0 (#100236) --- homeassistant/components/roborock/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/roborock/snapshots/test_diagnostics.ambr | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index dfcac67d2b0..81bbd07d904 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", "loggers": ["roborock"], - "requirements": ["python-roborock==0.33.2"] + "requirements": ["python-roborock==0.34.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index f4315943472..07fad662602 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2159,7 +2159,7 @@ python-qbittorrent==0.4.3 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.33.2 +python-roborock==0.34.0 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cdfdefdb989..09338a91c9d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1585,7 +1585,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.3 # homeassistant.components.roborock -python-roborock==0.33.2 +python-roborock==0.34.0 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/tests/components/roborock/snapshots/test_diagnostics.ambr b/tests/components/roborock/snapshots/test_diagnostics.ambr index eb70e04110f..a766a6c2703 100644 --- a/tests/components/roborock/snapshots/test_diagnostics.ambr +++ b/tests/components/roborock/snapshots/test_diagnostics.ambr @@ -225,11 +225,13 @@ 'area': 20965000, 'avoidCount': 19, 'begin': 1672543330, + 'beginDatetime': '2023-01-01T03:22:10+00:00', 'cleanType': 3, 'complete': 1, 'duration': 1176, 'dustCollectionStatus': 1, 'end': 1672544638, + 'endDatetime': '2023-01-01T03:43:58+00:00', 'error': 0, 'finishReason': 56, 'mapFlag': 0, From c9bee8233e936ca471925ccfba6b95c1f75481ee Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 12 Sep 2023 23:36:44 +0200 Subject: [PATCH 159/202] Bump pynetgear to 0.10.10 (#100242) --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index be4dd0f2d9d..59a41542d7c 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/netgear", "iot_class": "local_polling", "loggers": ["pynetgear"], - "requirements": ["pynetgear==0.10.9"], + "requirements": ["pynetgear==0.10.10"], "ssdp": [ { "manufacturer": "NETGEAR, Inc.", diff --git a/requirements_all.txt b/requirements_all.txt index 07fad662602..6a456ce74d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1866,7 +1866,7 @@ pymyq==3.1.4 pymysensors==0.24.0 # homeassistant.components.netgear -pynetgear==0.10.9 +pynetgear==0.10.10 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 09338a91c9d..b65063513c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1382,7 +1382,7 @@ pymyq==3.1.4 pymysensors==0.24.0 # homeassistant.components.netgear -pynetgear==0.10.9 +pynetgear==0.10.10 # homeassistant.components.nobo_hub pynobo==1.6.0 From 64d599343482878680aead6adf67f39450b00997 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Tue, 12 Sep 2023 19:16:31 -0400 Subject: [PATCH 160/202] Fix incorrect off peak translation key for Roborock (#100246) fix incorrect translation key --- homeassistant/components/roborock/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 5ca2292f804..20e90488ad3 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -164,10 +164,10 @@ "dnd_end_time": { "name": "Do not disturb end" }, - "off_peak_start_time": { + "off_peak_start": { "name": "Off-peak start" }, - "off_peak_end_time": { + "off_peak_end": { "name": "Off-peak end" } }, From daf21d677a0245c85d418a92380f488d1212b353 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 12 Sep 2023 18:17:29 -0400 Subject: [PATCH 161/202] Bump pyenphase to 1.11.2 (#100249) * Bump pyenphase to 1.11.1 * Apply suggestions from code review --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index c6d127a3f6e..9fc6b63edfc 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.11.0"], + "requirements": ["pyenphase==1.11.2"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 6a456ce74d6..208b4073d79 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1671,7 +1671,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.11.0 +pyenphase==1.11.2 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b65063513c1..e7446b90275 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1235,7 +1235,7 @@ pyeconet==0.1.20 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.11.0 +pyenphase==1.11.2 # homeassistant.components.everlights pyeverlights==0.1.0 From 9ec6e63384e21a74f3dd3cb5e92ea4ebca9aad0a Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 13 Sep 2023 02:15:33 -0400 Subject: [PATCH 162/202] Bump pyenphase to 1.11.3 (#100255) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 9fc6b63edfc..aa801fea14e 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.11.2"], + "requirements": ["pyenphase==1.11.3"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 208b4073d79..bb875c31bb5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1671,7 +1671,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.11.2 +pyenphase==1.11.3 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e7446b90275..2670bd9e11f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1235,7 +1235,7 @@ pyeconet==0.1.20 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.11.2 +pyenphase==1.11.3 # homeassistant.components.everlights pyeverlights==0.1.0 From 9e0d23f9a8199625180b1e2d4ab4c63fef8a413d Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 13 Sep 2023 11:21:55 +0300 Subject: [PATCH 163/202] Bump sensirion-ble to 0.1.1 (#100271) Bump to sensirion-ble==0.1.1 Fixes akx/sensirion-ble#6 Refs https://github.com/home-assistant/core/issues/93678#issuecomment-1694522112 --- homeassistant/components/sensirion_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/sensirion_ble/manifest.json b/homeassistant/components/sensirion_ble/manifest.json index 38f66a88e8e..01ccc873f56 100644 --- a/homeassistant/components/sensirion_ble/manifest.json +++ b/homeassistant/components/sensirion_ble/manifest.json @@ -16,5 +16,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/sensirion_ble", "iot_class": "local_push", - "requirements": ["sensirion-ble==0.1.0"] + "requirements": ["sensirion-ble==0.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index bb875c31bb5..3f808112d5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2375,7 +2375,7 @@ sense-energy==0.12.1 sense_energy==0.12.1 # homeassistant.components.sensirion_ble -sensirion-ble==0.1.0 +sensirion-ble==0.1.1 # homeassistant.components.sensorpro sensorpro-ble==0.5.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2670bd9e11f..6623c1072d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1735,7 +1735,7 @@ sense-energy==0.12.1 sense_energy==0.12.1 # homeassistant.components.sensirion_ble -sensirion-ble==0.1.0 +sensirion-ble==0.1.1 # homeassistant.components.sensorpro sensorpro-ble==0.5.3 From 29ba5a42927494a62aaba50dac099537871e5b1f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 13 Sep 2023 12:41:28 +0200 Subject: [PATCH 164/202] Future proof assist_pipeline.Pipeline (#100277) --- .../components/assist_pipeline/pipeline.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 520daa9f5c2..f4d060ed7b8 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -298,6 +298,26 @@ class Pipeline: id: str = field(default_factory=ulid_util.ulid) + @classmethod + def from_json(cls, data: dict[str, Any]) -> Pipeline: + """Create an instance from a JSON serialization. + + This function was added in HA Core 2023.10, previous versions will raise + if there are unexpected items in the serialized data. + """ + return cls( + conversation_engine=data["conversation_engine"], + conversation_language=data["conversation_language"], + id=data["id"], + language=data["language"], + name=data["name"], + stt_engine=data["stt_engine"], + stt_language=data["stt_language"], + tts_engine=data["tts_engine"], + tts_language=data["tts_language"], + tts_voice=data["tts_voice"], + ) + def to_json(self) -> dict[str, Any]: """Return a JSON serializable representation for storage.""" return { @@ -1205,7 +1225,7 @@ class PipelineStorageCollection( def _deserialize_item(self, data: dict) -> Pipeline: """Create an item from its serialized representation.""" - return Pipeline(**data) + return Pipeline.from_json(data) def _serialize_item(self, item_id: str, item: Pipeline) -> dict: """Return the serialized representation of an item for storing.""" From ef0d8da4ce6611c68bd2cc650f397e5c6794dcbe Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 13 Sep 2023 10:04:34 -0400 Subject: [PATCH 165/202] Bump pyenphase to 1.11.4 (#100288) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index aa801fea14e..917e325be51 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.11.3"], + "requirements": ["pyenphase==1.11.4"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 3f808112d5b..9ade4b7d862 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1671,7 +1671,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.11.3 +pyenphase==1.11.4 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6623c1072d9..55b2f9771ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1235,7 +1235,7 @@ pyeconet==0.1.20 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.11.3 +pyenphase==1.11.4 # homeassistant.components.everlights pyeverlights==0.1.0 From fed7fc9597599e7d1544ed623e30ff3dd5e7ff33 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 9 Sep 2023 17:34:11 -0500 Subject: [PATCH 166/202] Bump yalexs-ble to 2.3.0 (#100007) --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index cd2737adca3..a2d460d12ec 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==1.8.0", "yalexs-ble==2.2.3"] + "requirements": ["yalexs==1.8.0", "yalexs-ble==2.3.0"] } diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 3aefeea048a..cbff581d296 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", "iot_class": "local_push", - "requirements": ["yalexs-ble==2.2.3"] + "requirements": ["yalexs-ble==2.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9ade4b7d862..fa77ebd0b20 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2736,7 +2736,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.2.3 +yalexs-ble==2.3.0 # homeassistant.components.august yalexs==1.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 55b2f9771ad..475f0095883 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2015,7 +2015,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.2.3 +yalexs-ble==2.3.0 # homeassistant.components.august yalexs==1.8.0 From 66dbcc04ebee60656ba4cbd15fde3d68bada2618 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 13 Sep 2023 11:25:10 -0500 Subject: [PATCH 167/202] Bump yalexs to 1.9.0 (#100305) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index a2d460d12ec..c5a0da71136 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==1.8.0", "yalexs-ble==2.3.0"] + "requirements": ["yalexs==1.9.0", "yalexs-ble==2.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index fa77ebd0b20..e42b5a26c47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2739,7 +2739,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.3.0 # homeassistant.components.august -yalexs==1.8.0 +yalexs==1.9.0 # homeassistant.components.yeelight yeelight==0.7.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 475f0095883..e806b26284f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2018,7 +2018,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.3.0 # homeassistant.components.august -yalexs==1.8.0 +yalexs==1.9.0 # homeassistant.components.yeelight yeelight==0.7.13 From 57cfd2ef032a6652cacf2017e1a658f66c71710a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 13 Sep 2023 21:09:29 +0200 Subject: [PATCH 168/202] Update Mill library to 0.11.5, handle rate limiting (#100315) --- homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index a4c824b3674..7e9416f6695 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mill", "iot_class": "local_polling", "loggers": ["mill", "mill_local"], - "requirements": ["millheater==0.11.2", "mill-local==0.2.0"] + "requirements": ["millheater==0.11.5", "mill-local==0.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index e42b5a26c47..9a2051736c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1213,7 +1213,7 @@ micloud==0.5 mill-local==0.2.0 # homeassistant.components.mill -millheater==0.11.2 +millheater==0.11.5 # homeassistant.components.minio minio==7.1.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e806b26284f..8366f70a051 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -927,7 +927,7 @@ micloud==0.5 mill-local==0.2.0 # homeassistant.components.mill -millheater==0.11.2 +millheater==0.11.5 # homeassistant.components.minio minio==7.1.12 From 4d1ca9397303e1b21771c3fae7c0f346d25d4914 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 14 Sep 2023 22:32:50 +0200 Subject: [PATCH 169/202] Remove _next_refresh variable in update coordinator (#100323) * Remove _next_refresh variable * Adjust tomorrowio --- homeassistant/components/tomorrowio/__init__.py | 1 - homeassistant/helpers/update_coordinator.py | 12 ++++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tomorrowio/__init__.py b/homeassistant/components/tomorrowio/__init__.py index 77675e3f2ec..626049276f5 100644 --- a/homeassistant/components/tomorrowio/__init__.py +++ b/homeassistant/components/tomorrowio/__init__.py @@ -221,7 +221,6 @@ class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): await self.async_refresh() self.update_interval = async_set_update_interval(self.hass, self._api) - self._next_refresh = None self._async_unsub_refresh() if self._listeners: self._schedule_refresh() diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 34651fcaf9d..2b570009a57 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -81,7 +81,6 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): self._shutdown_requested = False self.config_entry = config_entries.current_entry.get() self.always_update = always_update - self._next_refresh: float | None = None # It's None before the first successful update. # Components should call async_config_entry_first_refresh @@ -184,7 +183,6 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): """Unschedule any pending refresh since there is no longer any listeners.""" self._async_unsub_refresh() self._debounced_refresh.async_cancel() - self._next_refresh = None def async_contexts(self) -> Generator[Any, None, None]: """Return all registered contexts.""" @@ -220,13 +218,13 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): # We use event.async_call_at because DataUpdateCoordinator does # not need an exact update interval. now = self.hass.loop.time() - if self._next_refresh is None or self._next_refresh <= now: - self._next_refresh = int(now) + self._microsecond - self._next_refresh += self.update_interval.total_seconds() + + next_refresh = int(now) + self._microsecond + next_refresh += self.update_interval.total_seconds() self._unsub_refresh = event.async_call_at( self.hass, self._job, - self._next_refresh, + next_refresh, ) async def _handle_refresh_interval(self, _now: datetime) -> None: @@ -265,7 +263,6 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): async def async_refresh(self) -> None: """Refresh data and log errors.""" - self._next_refresh = None await self._async_refresh(log_failures=True) async def _async_refresh( # noqa: C901 @@ -405,7 +402,6 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): """Manually update data, notify listeners and reset refresh interval.""" self._async_unsub_refresh() self._debounced_refresh.async_cancel() - self._next_refresh = None self.data = data self.last_update_success = True From 94ef5f751f4b44694d4fdc425500d78abb8be760 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 14 Sep 2023 10:28:45 +0200 Subject: [PATCH 170/202] Fix timeout issue in devolo_home_network (#100350) --- homeassistant/components/devolo_home_network/__init__.py | 2 +- homeassistant/components/devolo_home_network/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index f54fddc9a86..627a121dcb4 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -74,7 +74,7 @@ async def async_setup_entry( # noqa: C901 """Fetch data from API endpoint.""" assert device.device try: - async with asyncio.timeout(10): + async with asyncio.timeout(30): return await device.device.async_check_firmware_available() except DeviceUnavailable as err: raise UpdateFailed(err) from err diff --git a/homeassistant/components/devolo_home_network/manifest.json b/homeassistant/components/devolo_home_network/manifest.json index a047437e980..27fd08898c0 100644 --- a/homeassistant/components/devolo_home_network/manifest.json +++ b/homeassistant/components/devolo_home_network/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["devolo_plc_api"], "quality_scale": "platinum", - "requirements": ["devolo-plc-api==1.4.0"], + "requirements": ["devolo-plc-api==1.4.1"], "zeroconf": [ { "type": "_dvl-deviceapi._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 9a2051736c7..0948b6f7973 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -670,7 +670,7 @@ denonavr==0.11.3 devolo-home-control-api==0.18.2 # homeassistant.components.devolo_home_network -devolo-plc-api==1.4.0 +devolo-plc-api==1.4.1 # homeassistant.components.directv directv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8366f70a051..90138cd1d54 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -544,7 +544,7 @@ denonavr==0.11.3 devolo-home-control-api==0.18.2 # homeassistant.components.devolo_home_network -devolo-plc-api==1.4.0 +devolo-plc-api==1.4.1 # homeassistant.components.directv directv==0.4.0 From 50a41f516de4bc39933584346b6d1bb4d712cecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Thu, 14 Sep 2023 12:51:06 +0200 Subject: [PATCH 171/202] Fix Airthings ble migration (#100362) * Import Platform for tests * Migration bugfix * Store new unique id as a variable in tests * Add comments to tests --- .../components/airthings_ble/sensor.py | 3 +- tests/components/airthings_ble/test_sensor.py | 45 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py index b66d6b8f810..28b5fa3a7a6 100644 --- a/homeassistant/components/airthings_ble/sensor.py +++ b/homeassistant/components/airthings_ble/sensor.py @@ -144,7 +144,8 @@ def async_migrate(hass: HomeAssistant, address: str, sensor_name: str) -> None: not matching_reg_entry or "(" not in entry.unique_id ): matching_reg_entry = entry - if not matching_reg_entry: + if not matching_reg_entry or matching_reg_entry.unique_id == new_unique_id: + # Already has the newest unique id format return entity_id = matching_reg_entry.entity_id ent_reg.async_update_entity(entity_id=entity_id, new_unique_id=new_unique_id) diff --git a/tests/components/airthings_ble/test_sensor.py b/tests/components/airthings_ble/test_sensor.py index 68efd4d25f6..1bf036b735d 100644 --- a/tests/components/airthings_ble/test_sensor.py +++ b/tests/components/airthings_ble/test_sensor.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.airthings_ble.const import DOMAIN +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from tests.components.airthings_ble import ( @@ -31,11 +32,13 @@ async def test_migration_from_v1_to_v3_unique_id(hass: HomeAssistant): assert entry is not None assert device is not None + new_unique_id = f"{WAVE_DEVICE_INFO.address}_temperature" + entity_registry = hass.helpers.entity_registry.async_get(hass) sensor = entity_registry.async_get_or_create( domain=DOMAIN, - platform="sensor", + platform=Platform.SENSOR, unique_id=TEMPERATURE_V1.unique_id, config_entry=entry, device_id=device.id, @@ -57,10 +60,7 @@ async def test_migration_from_v1_to_v3_unique_id(hass: HomeAssistant): assert len(hass.states.async_all()) > 0 - assert ( - entity_registry.async_get(sensor.entity_id).unique_id - == WAVE_DEVICE_INFO.address + "_temperature" - ) + assert entity_registry.async_get(sensor.entity_id).unique_id == new_unique_id async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant): @@ -77,7 +77,7 @@ async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant): sensor = entity_registry.async_get_or_create( domain=DOMAIN, - platform="sensor", + platform=Platform.SENSOR, unique_id=HUMIDITY_V2.unique_id, config_entry=entry, device_id=device.id, @@ -99,10 +99,9 @@ async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant): assert len(hass.states.async_all()) > 0 - assert ( - entity_registry.async_get(sensor.entity_id).unique_id - == WAVE_DEVICE_INFO.address + "_humidity" - ) + # Migration should happen, v2 unique id should be updated to the new format + new_unique_id = f"{WAVE_DEVICE_INFO.address}_humidity" + assert entity_registry.async_get(sensor.entity_id).unique_id == new_unique_id async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant): @@ -119,7 +118,7 @@ async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant): v2 = entity_registry.async_get_or_create( domain=DOMAIN, - platform="sensor", + platform=Platform.SENSOR, unique_id=CO2_V2.unique_id, config_entry=entry, device_id=device.id, @@ -127,7 +126,7 @@ async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant): v1 = entity_registry.async_get_or_create( domain=DOMAIN, - platform="sensor", + platform=Platform.SENSOR, unique_id=CO2_V1.unique_id, config_entry=entry, device_id=device.id, @@ -149,11 +148,10 @@ async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant): assert len(hass.states.async_all()) > 0 - assert ( - entity_registry.async_get(v1.entity_id).unique_id - == WAVE_DEVICE_INFO.address + "_co2" - ) - assert entity_registry.async_get(v2.entity_id).unique_id == v2.unique_id + # Migration should happen, v1 unique id should be updated to the new format + new_unique_id = f"{WAVE_DEVICE_INFO.address}_co2" + assert entity_registry.async_get(v1.entity_id).unique_id == new_unique_id + assert entity_registry.async_get(v2.entity_id).unique_id == CO2_V2.unique_id async def test_migration_with_all_unique_ids(hass: HomeAssistant): @@ -170,7 +168,7 @@ async def test_migration_with_all_unique_ids(hass: HomeAssistant): v1 = entity_registry.async_get_or_create( domain=DOMAIN, - platform="sensor", + platform=Platform.SENSOR, unique_id=VOC_V1.unique_id, config_entry=entry, device_id=device.id, @@ -178,7 +176,7 @@ async def test_migration_with_all_unique_ids(hass: HomeAssistant): v2 = entity_registry.async_get_or_create( domain=DOMAIN, - platform="sensor", + platform=Platform.SENSOR, unique_id=VOC_V2.unique_id, config_entry=entry, device_id=device.id, @@ -186,7 +184,7 @@ async def test_migration_with_all_unique_ids(hass: HomeAssistant): v3 = entity_registry.async_get_or_create( domain=DOMAIN, - platform="sensor", + platform=Platform.SENSOR, unique_id=VOC_V3.unique_id, config_entry=entry, device_id=device.id, @@ -208,6 +206,7 @@ async def test_migration_with_all_unique_ids(hass: HomeAssistant): assert len(hass.states.async_all()) > 0 - assert entity_registry.async_get(v1.entity_id).unique_id == v1.unique_id - assert entity_registry.async_get(v2.entity_id).unique_id == v2.unique_id - assert entity_registry.async_get(v3.entity_id).unique_id == v3.unique_id + # No migration should happen, unique id should be the same as before + assert entity_registry.async_get(v1.entity_id).unique_id == VOC_V1.unique_id + assert entity_registry.async_get(v2.entity_id).unique_id == VOC_V2.unique_id + assert entity_registry.async_get(v3.entity_id).unique_id == VOC_V3.unique_id From 66a1522d889d8fa404cef1ae04dad0be5aa643d4 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 17 Sep 2023 22:28:52 +0200 Subject: [PATCH 172/202] Try Reolink ONVIF long polling if ONVIF push not supported (#100375) --- homeassistant/components/reolink/host.py | 56 ++++++++++++++++++------ 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index a43dbce9a7c..2487013b032 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -61,7 +61,8 @@ class ReolinkHost: ) self.webhook_id: str | None = None - self._onvif_supported: bool = True + self._onvif_push_supported: bool = True + self._onvif_long_poll_supported: bool = True self._base_url: str = "" self._webhook_url: str = "" self._webhook_reachable: bool = False @@ -97,7 +98,9 @@ class ReolinkHost: f"'{self._api.user_level}', only admin users can change camera settings" ) - self._onvif_supported = self._api.supported(None, "ONVIF") + onvif_supported = self._api.supported(None, "ONVIF") + self._onvif_push_supported = onvif_supported + self._onvif_long_poll_supported = onvif_supported enable_rtsp = None enable_onvif = None @@ -109,7 +112,7 @@ class ReolinkHost: ) enable_rtsp = True - if not self._api.onvif_enabled and self._onvif_supported: + if not self._api.onvif_enabled and onvif_supported: _LOGGER.debug( "ONVIF is disabled on %s, trying to enable it", self._api.nvr_name ) @@ -157,11 +160,11 @@ class ReolinkHost: self._unique_id = format_mac(self._api.mac_address) - if self._onvif_supported: + if self._onvif_push_supported: try: await self.subscribe() except NotSupportedError: - self._onvif_supported = False + self._onvif_push_supported = False self.unregister_webhook() await self._api.unsubscribe() else: @@ -179,12 +182,27 @@ class ReolinkHost: self._cancel_onvif_check = async_call_later( self._hass, FIRST_ONVIF_TIMEOUT, self._async_check_onvif ) - if not self._onvif_supported: + if not self._onvif_push_supported: _LOGGER.debug( - "Camera model %s does not support ONVIF, using fast polling instead", + "Camera model %s does not support ONVIF push, using ONVIF long polling instead", self._api.model, ) - await self._async_poll_all_motion() + try: + await self._async_start_long_polling(initial=True) + except NotSupportedError: + _LOGGER.debug( + "Camera model %s does not support ONVIF long polling, using fast polling instead", + self._api.model, + ) + self._onvif_long_poll_supported = False + await self._api.unsubscribe() + await self._async_poll_all_motion() + else: + self._cancel_long_poll_check = async_call_later( + self._hass, + FIRST_ONVIF_LONG_POLL_TIMEOUT, + self._async_check_onvif_long_poll, + ) if self._api.sw_version_update_required: ir.async_create_issue( @@ -317,11 +335,22 @@ class ReolinkHost: str(err), ) - async def _async_start_long_polling(self): + async def _async_start_long_polling(self, initial=False): """Start ONVIF long polling task.""" if self._long_poll_task is None: try: await self._api.subscribe(sub_type=SubType.long_poll) + except NotSupportedError as err: + if initial: + raise err + # make sure the long_poll_task is always created to try again later + if not self._lost_subscription: + self._lost_subscription = True + _LOGGER.error( + "Reolink %s event long polling subscription lost: %s", + self._api.nvr_name, + str(err), + ) except ReolinkError as err: # make sure the long_poll_task is always created to try again later if not self._lost_subscription: @@ -381,12 +410,11 @@ class ReolinkHost: async def renew(self) -> None: """Renew the subscription of motion events (lease time is 15 minutes).""" - if not self._onvif_supported: - return - try: - await self._renew(SubType.push) - if self._long_poll_task is not None: + if self._onvif_push_supported: + await self._renew(SubType.push) + + if self._onvif_long_poll_supported and self._long_poll_task is not None: if not self._api.subscribed(SubType.long_poll): _LOGGER.debug("restarting long polling task") # To prevent 5 minute request timeout From e1ab0fe295238b558af9e5afbe08c5e178ec0c57 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 14 Sep 2023 21:24:23 +0200 Subject: [PATCH 173/202] Bump reolink-aio to 0.7.10 (#100376) --- homeassistant/components/reolink/manifest.json | 2 +- homeassistant/components/reolink/strings.json | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 060490c6e56..221a6b8b59d 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.7.9"] + "requirements": ["reolink-aio==0.7.10"] } diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 95aa26a1ff5..15ba4baed45 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -223,6 +223,7 @@ "state": { "off": "[%key:common::state::off%]", "auto": "Auto", + "onatnight": "On at night", "schedule": "Schedule", "adaptive": "Adaptive", "autoadaptive": "Auto adaptive" diff --git a/requirements_all.txt b/requirements_all.txt index 0948b6f7973..63ac667a580 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2294,7 +2294,7 @@ renault-api==0.2.0 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.9 +reolink-aio==0.7.10 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 90138cd1d54..3527d637a76 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1684,7 +1684,7 @@ renault-api==0.2.0 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.9 +reolink-aio==0.7.10 # homeassistant.components.rflink rflink==0.0.65 From 3e34fc3b82d7437e892b29f481fc6b448ee8e130 Mon Sep 17 00:00:00 2001 From: steffenrapp <88974099+steffenrapp@users.noreply.github.com> Date: Fri, 15 Sep 2023 14:42:27 +0200 Subject: [PATCH 174/202] Add missing timer service translation (#100388) --- homeassistant/components/timer/services.yaml | 2 ++ homeassistant/components/timer/strings.json | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/timer/services.yaml b/homeassistant/components/timer/services.yaml index 74eeae22b23..ac5453d38c9 100644 --- a/homeassistant/components/timer/services.yaml +++ b/homeassistant/components/timer/services.yaml @@ -36,3 +36,5 @@ change: example: "00:01:00, 60 or -60" selector: text: + +reload: diff --git a/homeassistant/components/timer/strings.json b/homeassistant/components/timer/strings.json index 56cb46d26b4..719cafe676a 100644 --- a/homeassistant/components/timer/strings.json +++ b/homeassistant/components/timer/strings.json @@ -62,6 +62,10 @@ "description": "Duration to add or subtract to the running timer." } } + }, + "reload": { + "name": "[%key:common::action::reload%]", + "description": "Reloads helpers from the YAML-configuration." } } } From ba30e6fb1cfc046ee9b16f2eb337c90e1b0e6e80 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 15 Sep 2023 06:52:50 +0100 Subject: [PATCH 175/202] Fix current condition in IPMA (#100412) always use hourly forecast to retrieve current weather condition. fix #100393 --- homeassistant/components/ipma/weather.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index a5bb3981575..f9b93cbe954 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -103,10 +103,7 @@ class IPMAWeather(WeatherEntity, IPMADevice): else: self._daily_forecast = None - if self._period == 1 or self._forecast_listeners["hourly"]: - await self._update_forecast("hourly", 1, True) - else: - self._hourly_forecast = None + await self._update_forecast("hourly", 1, True) _LOGGER.debug( "Updated location %s based on %s, current observation %s", @@ -139,8 +136,8 @@ class IPMAWeather(WeatherEntity, IPMADevice): @property def condition(self): - """Return the current condition.""" - forecast = self._hourly_forecast or self._daily_forecast + """Return the current condition which is only available on the hourly forecast data.""" + forecast = self._hourly_forecast if not forecast: return From d6c365014a2575e4f938446549c0f97123358742 Mon Sep 17 00:00:00 2001 From: Matrix Date: Fri, 15 Sep 2023 23:20:30 +0800 Subject: [PATCH 176/202] Bump yolink-api to 0.3.1 (#100426) --- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index ced0d527c7d..7322c58ae04 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["auth", "application_credentials"], "documentation": "https://www.home-assistant.io/integrations/yolink", "iot_class": "cloud_push", - "requirements": ["yolink-api==0.3.0"] + "requirements": ["yolink-api==0.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 63ac667a580..c8b4a3a376c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2748,7 +2748,7 @@ yeelight==0.7.13 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.3.0 +yolink-api==0.3.1 # homeassistant.components.youless youless-api==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3527d637a76..f7d9fd19ee4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2024,7 +2024,7 @@ yalexs==1.9.0 yeelight==0.7.13 # homeassistant.components.yolink -yolink-api==0.3.0 +yolink-api==0.3.1 # homeassistant.components.youless youless-api==1.0.1 From 74dbcae92f1c053c3545ce3450ff87b6dae3dee5 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 15 Sep 2023 16:05:56 +0200 Subject: [PATCH 177/202] Fix timer reload description (#100433) Fix copy/paste error of #100388 --- homeassistant/components/timer/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/timer/strings.json b/homeassistant/components/timer/strings.json index 719cafe676a..1ebf0c6f50a 100644 --- a/homeassistant/components/timer/strings.json +++ b/homeassistant/components/timer/strings.json @@ -65,7 +65,7 @@ }, "reload": { "name": "[%key:common::action::reload%]", - "description": "Reloads helpers from the YAML-configuration." + "description": "Reloads timers from the YAML-configuration." } } } From 13028e50eacaaa9e078c88e2b178dcbc5e8fb117 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Fri, 15 Sep 2023 19:03:04 +0200 Subject: [PATCH 178/202] bump pywaze to 0.5.0 (#100456) --- homeassistant/components/waze_travel_time/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index c72d9b1dbad..1a4be798367 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", "iot_class": "cloud_polling", "loggers": ["pywaze", "homeassistant.helpers.location"], - "requirements": ["pywaze==0.4.0"] + "requirements": ["pywaze==0.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index c8b4a3a376c..e2e9d45c98c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2231,7 +2231,7 @@ pyvlx==0.2.20 pyvolumio==0.1.5 # homeassistant.components.waze_travel_time -pywaze==0.4.0 +pywaze==0.5.0 # homeassistant.components.html5 pywebpush==1.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7d9fd19ee4..5aaac7b9265 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1639,7 +1639,7 @@ pyvizio==0.1.61 pyvolumio==0.1.5 # homeassistant.components.waze_travel_time -pywaze==0.4.0 +pywaze==0.5.0 # homeassistant.components.html5 pywebpush==1.9.2 From fcd9ae5a01495e7db174cc42307d44afc65188ec Mon Sep 17 00:00:00 2001 From: "J.P. Krauss" Date: Sat, 16 Sep 2023 09:16:15 -0700 Subject: [PATCH 179/202] Fix error is measurement is not sent by AirNow (#100477) --- homeassistant/components/airnow/sensor.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index 09393741d63..c83232c273a 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -58,6 +58,16 @@ class AirNowEntityDescription(SensorEntityDescription, AirNowEntityDescriptionMi """Describes Airnow sensor entity.""" +def station_extra_attrs(data: dict[str, Any]) -> dict[str, Any]: + """Process extra attributes for station location (if available).""" + if ATTR_API_STATION in data: + return { + "lat": data.get(ATTR_API_STATION_LATITUDE), + "long": data.get(ATTR_API_STATION_LONGITUDE), + } + return {} + + SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = ( AirNowEntityDescription( key=ATTR_API_AQI, @@ -93,10 +103,7 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = ( translation_key="station", icon="mdi:blur", value_fn=lambda data: data.get(ATTR_API_STATION), - extra_state_attributes_fn=lambda data: { - "lat": data[ATTR_API_STATION_LATITUDE], - "long": data[ATTR_API_STATION_LONGITUDE], - }, + extra_state_attributes_fn=station_extra_attrs, ), ) From 93081bcbea3be99211d3f27932f790f7ca04a3a5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 16 Sep 2023 18:48:41 +0200 Subject: [PATCH 180/202] Only get meteo france alert coordinator if it exists (#100493) Only get meteo france coordinator if it exists --- homeassistant/components/meteo_france/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 98cb4665614..dd8fd4af83b 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -196,9 +196,9 @@ async def async_setup_entry( data = hass.data[DOMAIN][entry.entry_id] coordinator_forecast: DataUpdateCoordinator[Forecast] = data[COORDINATOR_FORECAST] coordinator_rain: DataUpdateCoordinator[Rain] | None = data[COORDINATOR_RAIN] - coordinator_alert: DataUpdateCoordinator[CurrentPhenomenons] | None = data[ + coordinator_alert: DataUpdateCoordinator[CurrentPhenomenons] | None = data.get( COORDINATOR_ALERT - ] + ) entities: list[MeteoFranceSensor[Any]] = [ MeteoFranceSensor(coordinator_forecast, description) From 0a343037a7b8c524b3fdb3e2f93c7a512024451e Mon Sep 17 00:00:00 2001 From: Markus Friedli Date: Sun, 17 Sep 2023 20:00:09 +0200 Subject: [PATCH 181/202] Fix broken reconnect capability of fritzbox_callmonitor (#100526) --- homeassistant/components/fritz/manifest.json | 2 +- homeassistant/components/fritzbox_callmonitor/manifest.json | 2 +- homeassistant/components/fritzbox_callmonitor/sensor.py | 6 +++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index 8d52115d49b..d8d8f6b94bf 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/fritz", "iot_class": "local_polling", "loggers": ["fritzconnection"], - "requirements": ["fritzconnection[qr]==1.12.2", "xmltodict==0.13.0"], + "requirements": ["fritzconnection[qr]==1.13.2", "xmltodict==0.13.0"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index c3c305ab07e..4e5c60091c9 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["fritzconnection"], - "requirements": ["fritzconnection[qr]==1.12.2"] + "requirements": ["fritzconnection[qr]==1.13.2"] } diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index 43cdb29f85f..c8902622f85 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -189,7 +189,11 @@ class FritzBoxCallMonitor: _LOGGER.debug("Setting up socket connection") try: self.connection = FritzMonitor(address=self.host, port=self.port) - kwargs: dict[str, Any] = {"event_queue": self.connection.start()} + kwargs: dict[str, Any] = { + "event_queue": self.connection.start( + reconnect_tries=50, reconnect_delay=120 + ) + } Thread(target=self._process_events, kwargs=kwargs).start() except OSError as err: self.connection = None diff --git a/requirements_all.txt b/requirements_all.txt index e2e9d45c98c..32289b6b5bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -829,7 +829,7 @@ freesms==0.2.0 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -fritzconnection[qr]==1.12.2 +fritzconnection[qr]==1.13.2 # homeassistant.components.google_translate gTTS==2.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5aaac7b9265..cb890bd4fde 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,7 +648,7 @@ freebox-api==1.1.0 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -fritzconnection[qr]==1.12.2 +fritzconnection[qr]==1.13.2 # homeassistant.components.google_translate gTTS==2.2.4 From c059b1960e92f75011d48223e5a8be2475337a53 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 19 Sep 2023 17:15:43 +0200 Subject: [PATCH 182/202] Fix xiaomi_miio button platform regression (#100527) --- homeassistant/components/xiaomi_miio/button.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/button.py b/homeassistant/components/xiaomi_miio/button.py index 9ed9b780911..e5e11b85e58 100644 --- a/homeassistant/components/xiaomi_miio/button.py +++ b/homeassistant/components/xiaomi_miio/button.py @@ -169,8 +169,12 @@ class XiaomiGenericCoordinatedButton(XiaomiCoordinatedMiioEntity, ButtonEntity): async def async_press(self) -> None: """Press the button.""" method = getattr(self._device, self.entity_description.method_press) - await self._try_command( - self.entity_description.method_press_error_message, - method, - self.entity_description.method_press_params, - ) + params = self.entity_description.method_press_params + if params is not None: + await self._try_command( + self.entity_description.method_press_error_message, method, params + ) + else: + await self._try_command( + self.entity_description.method_press_error_message, method + ) From 9d25ca33bb9fcb2ba8d39c96fefbfe609940a88c Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Tue, 19 Sep 2023 10:35:23 -0400 Subject: [PATCH 183/202] Fix Roborock send command service calling not being enum (#100574) --- homeassistant/components/roborock/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roborock/device.py b/homeassistant/components/roborock/device.py index 27f25208a4e..2b005ecade6 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -40,7 +40,7 @@ class RoborockEntity(Entity): async def send( self, - command: RoborockCommand, + command: RoborockCommand | str, params: dict[str, Any] | list[Any] | int | None = None, ) -> dict: """Send a command to a vacuum cleaner.""" @@ -48,7 +48,7 @@ class RoborockEntity(Entity): response = await self._api.send_command(command, params) except RoborockException as err: raise HomeAssistantError( - f"Error while calling {command.name} with {params}" + f"Error while calling {command.name if isinstance(command, RoborockCommand) else command} with {params}" ) from err return response From 08f46ad61f38a97b9fa1cf51820065c61461c60a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 19 Sep 2023 11:30:38 -0400 Subject: [PATCH 184/202] Adjust hassfest.manifest based on config.action (#100577) --- script/hassfest/manifest.py | 14 +++++++++----- script/hassfest/model.py | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 9323b8e86c0..acdea23444d 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -366,15 +366,19 @@ def _sort_manifest_keys(key: str) -> str: return _SORT_KEYS.get(key, key) -def sort_manifest(integration: Integration) -> bool: +def sort_manifest(integration: Integration, config: Config) -> bool: """Sort manifest.""" keys = list(integration.manifest.keys()) if (keys_sorted := sorted(keys, key=_sort_manifest_keys)) != keys: manifest = {key: integration.manifest[key] for key in keys_sorted} - integration.manifest_path.write_text(json.dumps(manifest, indent=2)) + if config.action == "generate": + integration.manifest_path.write_text(json.dumps(manifest, indent=2)) + text = "have been sorted" + else: + text = "are not sorted correctly" integration.add_error( "manifest", - "Manifest keys have been sorted: domain, name, then alphabetical order", + f"Manifest keys {text}: domain, name, then alphabetical order", ) return True return False @@ -387,9 +391,9 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: for integration in integrations.values(): validate_manifest(integration, core_components_dir) if not integration.errors: - if sort_manifest(integration): + if sort_manifest(integration, config): manifests_resorted.append(integration.manifest_path) - if manifests_resorted: + if config.action == "generate" and manifests_resorted: subprocess.run( ["pre-commit", "run", "--hook-stage", "manual", "prettier", "--files"] + manifests_resorted, diff --git a/script/hassfest/model.py b/script/hassfest/model.py index e4f93c80e81..7df65b8221e 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass, field import json import pathlib -from typing import Any +from typing import Any, Literal @dataclass @@ -26,7 +26,7 @@ class Config: specific_integrations: list[pathlib.Path] | None root: pathlib.Path - action: str + action: Literal["validate", "generate"] requirements: bool errors: list[Error] = field(default_factory=list) cache: dict[str, Any] = field(default_factory=dict) From 4f4f6c92d3dd93e43e1ce60c8aadf22de8968af6 Mon Sep 17 00:00:00 2001 From: Robin Li Date: Wed, 20 Sep 2023 07:53:05 -0400 Subject: [PATCH 185/202] Fix ecobee aux_heat_off always returns to HEAT (#100630) --- homeassistant/components/ecobee/climate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index b18f646add7..e1253b585ac 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -326,6 +326,7 @@ class Thermostat(ClimateEntity): self._attr_unique_id = self.thermostat["identifier"] self.vacation = None self._last_active_hvac_mode = HVACMode.HEAT_COOL + self._last_hvac_mode_before_aux_heat = HVACMode.HEAT_COOL self._attr_hvac_modes = [] if self.settings["heatStages"] or self.settings["hasHeatPump"]: @@ -541,13 +542,14 @@ class Thermostat(ClimateEntity): def turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" _LOGGER.debug("Setting HVAC mode to auxHeatOnly to turn on aux heat") + self._last_hvac_mode_before_aux_heat = self.hvac_mode self.data.ecobee.set_hvac_mode(self.thermostat_index, ECOBEE_AUX_HEAT_ONLY) self.update_without_throttle = True def turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" _LOGGER.debug("Setting HVAC mode to last mode to disable aux heat") - self.set_hvac_mode(self._last_active_hvac_mode) + self.set_hvac_mode(self._last_hvac_mode_before_aux_heat) self.update_without_throttle = True def set_preset_mode(self, preset_mode: str) -> None: From fae063086c3d17be6fc530d23205b4d1c720698e Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 20 Sep 2023 16:02:00 -0400 Subject: [PATCH 186/202] Bump python-roborock to 0.34.1 (#100652) bump to 34.1 --- homeassistant/components/roborock/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 81bbd07d904..dfd5a9ee1c7 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", "loggers": ["roborock"], - "requirements": ["python-roborock==0.34.0"] + "requirements": ["python-roborock==0.34.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 32289b6b5bc..74ec9fc01d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2159,7 +2159,7 @@ python-qbittorrent==0.4.3 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.34.0 +python-roborock==0.34.1 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb890bd4fde..8d8158179ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1585,7 +1585,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.3 # homeassistant.components.roborock -python-roborock==0.34.0 +python-roborock==0.34.1 # homeassistant.components.smarttub python-smarttub==0.0.33 From 161e9d10bd42a4ce78978fe146ae0d48c605d52c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 21 Sep 2023 02:13:48 -0400 Subject: [PATCH 187/202] Bump zwave-js-server-python to 0.51.3 (#100665) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 4ea46099f14..cfb2c239d8e 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["zwave_js_server"], "quality_scale": "platinum", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.51.2"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.51.3"], "usb": [ { "vid": "0658", diff --git a/requirements_all.txt b/requirements_all.txt index 74ec9fc01d6..5d9534c6634 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2799,7 +2799,7 @@ zigpy==0.57.1 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.51.2 +zwave-js-server-python==0.51.3 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d8158179ee..f8fe2e654fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2060,7 +2060,7 @@ zigpy-znp==0.11.4 zigpy==0.57.1 # homeassistant.components.zwave_js -zwave-js-server-python==0.51.2 +zwave-js-server-python==0.51.3 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 From 556e40add58cb3fd009e4b976870ee804204f895 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 22 Sep 2023 13:31:29 +0200 Subject: [PATCH 188/202] Fix mqtt light rgbww update without state topic (#100707) * Fix mqtt light rgbww update without state topic * Add @callback decprator and correct mired conv --- .../components/mqtt/light/schema_basic.py | 18 +- tests/components/mqtt/test_light.py | 171 ++++++++++++++++++ 2 files changed, 188 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 2a726075bb0..9a1600f5865 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -462,6 +462,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): add_topic(CONF_BRIGHTNESS_STATE_TOPIC, brightness_received) + @callback def _rgbx_received( msg: ReceiveMessage, template: str, @@ -532,11 +533,26 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): @log_messages(self.hass, self.entity_id) def rgbww_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages for RGBWW.""" + + @callback + def _converter( + r: int, g: int, b: int, cw: int, ww: int + ) -> tuple[int, int, int]: + min_kelvin = color_util.color_temperature_mired_to_kelvin( + self.max_mireds + ) + max_kelvin = color_util.color_temperature_mired_to_kelvin( + self.min_mireds + ) + return color_util.color_rgbww_to_rgb( + r, g, b, cw, ww, min_kelvin, max_kelvin + ) + rgbww = _rgbx_received( msg, CONF_RGBWW_VALUE_TEMPLATE, ColorMode.RGBWW, - color_util.color_rgbww_to_rgb, + _converter, ) if rgbww is None: return diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 08def9a923e..133f38c1a56 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -198,6 +198,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, State from .test_common import ( + help_custom_config, help_test_availability_when_connection_lost, help_test_availability_without_topic, help_test_custom_availability_payload, @@ -441,6 +442,176 @@ async def test_controlling_state_via_topic( assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + light.DOMAIN, + DEFAULT_CONFIG, + ( + { + "state_topic": "test-topic", + "optimistic": True, + "brightness_command_topic": "test_light_rgb/brightness/set", + "color_mode_state_topic": "color-mode-state-topic", + "rgb_command_topic": "test_light_rgb/rgb/set", + "rgb_state_topic": "rgb-state-topic", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbw_state_topic": "rgbw-state-topic", + "rgbww_command_topic": "test_light_rgb/rgbww/set", + "rgbww_state_topic": "rgbww-state-topic", + }, + ), + ) + ], +) +async def test_received_rgbx_values_set_state_optimistic( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the state is set correctly when an rgbx update is received.""" + await mqtt_mock_entry() + state = hass.states.get("light.test") + assert state and state.state is not None + async_fire_mqtt_message(hass, "test-topic", "ON") + ## Test rgb processing + async_fire_mqtt_message(hass, "rgb-state-topic", "255,255,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgb" + assert state.attributes["rgb_color"] == (255, 255, 255) + + # Only update color mode + async_fire_mqtt_message(hass, "color-mode-state-topic", "rgbww") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgbww" + + # Resending same rgb value should restore color mode + async_fire_mqtt_message(hass, "rgb-state-topic", "255,255,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgb" + assert state.attributes["rgb_color"] == (255, 255, 255) + + # Only update brightness + await common.async_turn_on(hass, "light.test", brightness=128) + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 128 + assert state.attributes["color_mode"] == "rgb" + assert state.attributes["rgb_color"] == (255, 255, 255) + + # Resending same rgb value should restore brightness + async_fire_mqtt_message(hass, "rgb-state-topic", "255,255,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgb" + assert state.attributes["rgb_color"] == (255, 255, 255) + + # Only change rgb value + async_fire_mqtt_message(hass, "rgb-state-topic", "255,255,0") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgb" + assert state.attributes["rgb_color"] == (255, 255, 0) + + ## Test rgbw processing + async_fire_mqtt_message(hass, "rgbw-state-topic", "255,255,255,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgbw" + assert state.attributes["rgbw_color"] == (255, 255, 255, 255) + + # Only update color mode + async_fire_mqtt_message(hass, "color-mode-state-topic", "rgb") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgb" + + # Resending same rgbw value should restore color mode + async_fire_mqtt_message(hass, "rgbw-state-topic", "255,255,255,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgbw" + assert state.attributes["rgbw_color"] == (255, 255, 255, 255) + + # Only update brightness + await common.async_turn_on(hass, "light.test", brightness=128) + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 128 + assert state.attributes["color_mode"] == "rgbw" + assert state.attributes["rgbw_color"] == (255, 255, 255, 255) + + # Resending same rgbw value should restore brightness + async_fire_mqtt_message(hass, "rgbw-state-topic", "255,255,255,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgbw" + assert state.attributes["rgbw_color"] == (255, 255, 255, 255) + + # Only change rgbw value + async_fire_mqtt_message(hass, "rgbw-state-topic", "255,255,128,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgbw" + assert state.attributes["rgbw_color"] == (255, 255, 128, 255) + + ## Test rgbww processing + async_fire_mqtt_message(hass, "rgbww-state-topic", "255,255,255,32,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgbww" + assert state.attributes["rgbww_color"] == (255, 255, 255, 32, 255) + + # Only update color mode + async_fire_mqtt_message(hass, "color-mode-state-topic", "rgb") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgb" + + # Resending same rgbw value should restore color mode + async_fire_mqtt_message(hass, "rgbww-state-topic", "255,255,255,32,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgbww" + assert state.attributes["rgbww_color"] == (255, 255, 255, 32, 255) + + # Only update brightness + await common.async_turn_on(hass, "light.test", brightness=128) + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 128 + assert state.attributes["color_mode"] == "rgbww" + assert state.attributes["rgbww_color"] == (255, 255, 255, 32, 255) + + # Resending same rgbww value should restore brightness + async_fire_mqtt_message(hass, "rgbww-state-topic", "255,255,255,32,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgbww" + assert state.attributes["rgbww_color"] == (255, 255, 255, 32, 255) + + # Only change rgbww value + async_fire_mqtt_message(hass, "rgbww-state-topic", "255,255,128,32,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgbww" + assert state.attributes["rgbww_color"] == (255, 255, 128, 32, 255) + + @pytest.mark.parametrize( "hass_config", [ From bb8850e8cf139d94336a288525e2b10d03dc4d4a Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 22 Sep 2023 22:38:33 +0200 Subject: [PATCH 189/202] Bump aiocomelit to 0.0.8 (#100714) * Bump aiocomelit to 0.0.8 * fix import * fix tests --- homeassistant/components/comelit/config_flow.py | 4 ++-- homeassistant/components/comelit/coordinator.py | 4 ++-- homeassistant/components/comelit/manifest.json | 2 +- homeassistant/components/comelit/strings.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/comelit/test_config_flow.py | 14 +++++++------- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/comelit/config_flow.py b/homeassistant/components/comelit/config_flow.py index dd6227a6583..b0c8e5aabe5 100644 --- a/homeassistant/components/comelit/config_flow.py +++ b/homeassistant/components/comelit/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any -from aiocomelit import ComeliteSerialBridgeAPi, exceptions as aiocomelit_exceptions +from aiocomelit import ComeliteSerialBridgeApi, exceptions as aiocomelit_exceptions import voluptuous as vol from homeassistant import core, exceptions @@ -37,7 +37,7 @@ async def validate_input( ) -> dict[str, str]: """Validate the user input allows us to connect.""" - api = ComeliteSerialBridgeAPi(data[CONF_HOST], data[CONF_PIN]) + api = ComeliteSerialBridgeApi(data[CONF_HOST], data[CONF_PIN]) try: await api.login() diff --git a/homeassistant/components/comelit/coordinator.py b/homeassistant/components/comelit/coordinator.py index 1affd5046fe..1fcbd7c0d37 100644 --- a/homeassistant/components/comelit/coordinator.py +++ b/homeassistant/components/comelit/coordinator.py @@ -3,7 +3,7 @@ import asyncio from datetime import timedelta from typing import Any -from aiocomelit import ComeliteSerialBridgeAPi +from aiocomelit import ComeliteSerialBridgeApi import aiohttp from homeassistant.core import HomeAssistant @@ -22,7 +22,7 @@ class ComelitSerialBridge(DataUpdateCoordinator): self._host = host self._pin = pin - self.api = ComeliteSerialBridgeAPi(host, pin) + self.api = ComeliteSerialBridgeApi(host, pin) super().__init__( hass=hass, diff --git a/homeassistant/components/comelit/manifest.json b/homeassistant/components/comelit/manifest.json index fc7f2a3fc12..ee876434825 100644 --- a/homeassistant/components/comelit/manifest.json +++ b/homeassistant/components/comelit/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/comelit", "iot_class": "local_polling", "loggers": ["aiocomelit"], - "requirements": ["aiocomelit==0.0.5"] + "requirements": ["aiocomelit==0.0.8"] } diff --git a/homeassistant/components/comelit/strings.json b/homeassistant/components/comelit/strings.json index 6508f58412e..436fbfd5aec 100644 --- a/homeassistant/components/comelit/strings.json +++ b/homeassistant/components/comelit/strings.json @@ -3,7 +3,7 @@ "flow_title": "{host}", "step": { "reauth_confirm": { - "description": "Please enter the correct PIN for VEDO system: {host}", + "description": "Please enter the correct PIN for {host}", "data": { "pin": "[%key:common::config_flow::data::pin%]" } diff --git a/requirements_all.txt b/requirements_all.txt index 5d9534c6634..464a0dfde4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -209,7 +209,7 @@ aiobafi6==0.9.0 aiobotocore==2.6.0 # homeassistant.components.comelit -aiocomelit==0.0.5 +aiocomelit==0.0.8 # homeassistant.components.dhcp aiodiscover==1.4.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8fe2e654fe..8256ffc491c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -190,7 +190,7 @@ aiobafi6==0.9.0 aiobotocore==2.6.0 # homeassistant.components.comelit -aiocomelit==0.0.5 +aiocomelit==0.0.8 # homeassistant.components.dhcp aiodiscover==1.4.16 diff --git a/tests/components/comelit/test_config_flow.py b/tests/components/comelit/test_config_flow.py index 2fb9e836efb..10f68f4d7c1 100644 --- a/tests/components/comelit/test_config_flow.py +++ b/tests/components/comelit/test_config_flow.py @@ -18,9 +18,9 @@ from tests.common import MockConfigEntry async def test_user(hass: HomeAssistant) -> None: """Test starting a flow by user.""" with patch( - "aiocomelit.api.ComeliteSerialBridgeAPi.login", + "aiocomelit.api.ComeliteSerialBridgeApi.login", ), patch( - "aiocomelit.api.ComeliteSerialBridgeAPi.logout", + "aiocomelit.api.ComeliteSerialBridgeApi.logout", ), patch( "homeassistant.components.comelit.async_setup_entry" ) as mock_setup_entry, patch( @@ -64,7 +64,7 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> assert result["step_id"] == "user" with patch( - "aiocomelit.api.ComeliteSerialBridgeAPi.login", + "aiocomelit.api.ComeliteSerialBridgeApi.login", side_effect=side_effect, ): result = await hass.config_entries.flow.async_configure( @@ -83,9 +83,9 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: mock_config.add_to_hass(hass) with patch( - "aiocomelit.api.ComeliteSerialBridgeAPi.login", + "aiocomelit.api.ComeliteSerialBridgeApi.login", ), patch( - "aiocomelit.api.ComeliteSerialBridgeAPi.logout", + "aiocomelit.api.ComeliteSerialBridgeApi.logout", ), patch("homeassistant.components.comelit.async_setup_entry"), patch( "requests.get" ) as mock_request_get: @@ -127,9 +127,9 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> mock_config.add_to_hass(hass) with patch( - "aiocomelit.api.ComeliteSerialBridgeAPi.login", side_effect=side_effect + "aiocomelit.api.ComeliteSerialBridgeApi.login", side_effect=side_effect ), patch( - "aiocomelit.api.ComeliteSerialBridgeAPi.logout", + "aiocomelit.api.ComeliteSerialBridgeApi.logout", ), patch( "homeassistant.components.comelit.async_setup_entry" ): From 88f379d08fac6be621e1c950e456b8a2b7264d9d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 23 Sep 2023 13:28:14 +0200 Subject: [PATCH 190/202] Fix handling of unit system change in sensor (#100715) --- homeassistant/components/sensor/__init__.py | 4 +- tests/components/sensor/test_init.py | 60 +++++++++++++++++---- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index b8151256519..446dd60fd92 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -345,7 +345,7 @@ class SensorEntity(Entity): """Return initial entity options. These will be stored in the entity registry the first time the entity is seen, - and then never updated. + and then only updated if the unit system is changed. """ suggested_unit_of_measurement = self._get_initial_suggested_unit() @@ -783,7 +783,7 @@ class SensorEntity(Entity): registry = er.async_get(self.hass) initial_options = self.get_initial_entity_options() or {} registry.async_update_entity_options( - self.entity_id, + self.registry_entry.entity_id, f"{DOMAIN}.private", initial_options.get(f"{DOMAIN}.private"), ) diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 1f836ad9095..6ca26433056 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -44,6 +44,7 @@ from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM from tests.common import ( MockConfigEntry, + MockEntityPlatform, MockModule, MockPlatform, async_mock_restore_state_shutdown_restart, @@ -2177,27 +2178,24 @@ async def test_unit_conversion_update( entity_registry = er.async_get(hass) platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = platform.MockSensor( name="Test 0", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] - platform.ENTITIES["1"] = platform.MockSensor( + entity1 = platform.MockSensor( name="Test 1", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), unique_id="very_unique_1", ) - entity1 = platform.ENTITIES["1"] - platform.ENTITIES["2"] = platform.MockSensor( + entity2 = platform.MockSensor( name="Test 2", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2205,9 +2203,8 @@ async def test_unit_conversion_update( suggested_unit_of_measurement=suggested_unit, unique_id="very_unique_2", ) - entity2 = platform.ENTITIES["2"] - platform.ENTITIES["3"] = platform.MockSensor( + entity3 = platform.MockSensor( name="Test 3", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2215,9 +2212,33 @@ async def test_unit_conversion_update( suggested_unit_of_measurement=suggested_unit, unique_id="very_unique_3", ) - entity3 = platform.ENTITIES["3"] - assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + entity4 = platform.MockSensor( + name="Test 4", + device_class=device_class, + native_unit_of_measurement=native_unit, + native_value=str(native_value), + unique_id="very_unique_4", + ) + + entity_platform = MockEntityPlatform( + hass, domain="sensor", platform_name="test", platform=None + ) + await entity_platform.async_add_entities((entity0, entity1, entity2, entity3)) + + # Pre-register entity4 + entry = entity_registry.async_get_or_create( + "sensor", "test", entity4.unique_id, unit_of_measurement=automatic_unit_1 + ) + entity4_entity_id = entry.entity_id + entity_registry.async_update_entity_options( + entity4_entity_id, + "sensor.private", + { + "suggested_unit_of_measurement": automatic_unit_1, + }, + ) + await hass.async_block_till_done() # Registered entity -> Follow automatic unit conversion @@ -2320,6 +2341,25 @@ async def test_unit_conversion_update( assert state.state == suggested_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit + # Entity 4 still has a pending request to refresh entity options + entry = entity_registry.async_get(entity4_entity_id) + assert entry.options == { + "sensor.private": { + "refresh_initial_entity_options": True, + "suggested_unit_of_measurement": automatic_unit_1, + } + } + + # Add entity 4, the pending request to refresh entity options should be handled + await entity_platform.async_add_entities((entity4,)) + + state = hass.states.get(entity4_entity_id) + assert state.state == automatic_state_2 + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == automatic_unit_2 + + entry = entity_registry.async_get(entity4_entity_id) + assert entry.options == {} + class MockFlow(ConfigFlow): """Test flow.""" From c07a1126013bc121f8e35913306c3eb4c5843226 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 23 Sep 2023 03:53:56 -0400 Subject: [PATCH 191/202] Bump ZHA dependencies (#100732) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index c3fa6b1ff01..3610cd41425 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.36.3", + "bellows==0.36.4", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.103", @@ -30,7 +30,7 @@ "zigpy-xbee==0.18.2", "zigpy-zigate==0.11.0", "zigpy-znp==0.11.4", - "universal-silabs-flasher==0.0.13" + "universal-silabs-flasher==0.0.14" ], "usb": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 464a0dfde4d..614203898ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -509,7 +509,7 @@ beautifulsoup4==4.12.2 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.36.3 +bellows==0.36.4 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.0 @@ -2612,7 +2612,7 @@ unifi-discovery==1.1.7 unifiled==0.11 # homeassistant.components.zha -universal-silabs-flasher==0.0.13 +universal-silabs-flasher==0.0.14 # homeassistant.components.upb upb-lib==0.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8256ffc491c..1952948932a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -430,7 +430,7 @@ base36==0.1.1 beautifulsoup4==4.12.2 # homeassistant.components.zha -bellows==0.36.3 +bellows==0.36.4 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.0 @@ -1909,7 +1909,7 @@ ultraheat-api==0.5.7 unifi-discovery==1.1.7 # homeassistant.components.zha -universal-silabs-flasher==0.0.13 +universal-silabs-flasher==0.0.14 # homeassistant.components.upb upb-lib==0.5.4 From 862a26afad10240f69afb575abe73b01592c9adf Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 22 Sep 2023 19:29:00 -0500 Subject: [PATCH 192/202] Bump intents to 2023.9.22 (#100737) --- homeassistant/components/conversation/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/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 9e0909b6dfc..2f733ead486 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.2.5", "home-assistant-intents==2023.8.2"] + "requirements": ["hassil==1.2.5", "home-assistant-intents==2023.9.22"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 343c3be1481..714c11baf4a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ hass-nabucasa==0.71.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 home-assistant-frontend==20230911.0 -home-assistant-intents==2023.8.2 +home-assistant-intents==2023.9.22 httpx==0.24.1 ifaddr==0.2.0 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 614203898ae..0284414b6f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -997,7 +997,7 @@ holidays==0.28 home-assistant-frontend==20230911.0 # homeassistant.components.conversation -home-assistant-intents==2023.8.2 +home-assistant-intents==2023.9.22 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1952948932a..80bc2828150 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -780,7 +780,7 @@ holidays==0.28 home-assistant-frontend==20230911.0 # homeassistant.components.conversation -home-assistant-intents==2023.8.2 +home-assistant-intents==2023.9.22 # homeassistant.components.home_connect homeconnect==0.7.2 From ce8062041f3920ebf0a78c6e750f881461c81dc7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 23 Sep 2023 13:30:11 +0200 Subject: [PATCH 193/202] Fix weather template forecast attributes (#100748) --- homeassistant/components/template/weather.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index a04fc7a641d..d815655d775 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -44,7 +44,12 @@ from homeassistant.util.unit_conversion import ( from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_conf -CHECK_FORECAST_KEYS = set().union(Forecast.__annotations__.keys()) +CHECK_FORECAST_KEYS = ( + set().union(Forecast.__annotations__.keys()) + # Manually add the forecast resulting attributes that only exists + # as native_* in the Forecast definition + .union(("apparent_temperature", "wind_gust_speed", "dew_point")) +) CONDITION_CLASSES = { ATTR_CONDITION_CLEAR_NIGHT, @@ -434,7 +439,8 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): diff_result = set().union(forecast.keys()).difference(CHECK_FORECAST_KEYS) if diff_result: raise vol.Invalid( - "Only valid keys in Forecast are allowed, see Weather documentation https://www.home-assistant.io/integrations/weather/" + f"Only valid keys in Forecast are allowed, unallowed keys: ({diff_result}), " + "see Weather documentation https://www.home-assistant.io/integrations/weather/" ) if forecast_type == "twice_daily" and "is_daytime" not in forecast: raise vol.Invalid( From 527c7b21fdd56fb0f97575c685890283b499433d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 23 Sep 2023 13:47:05 +0200 Subject: [PATCH 194/202] Bumped version to 2023.9.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 08e12ce58ff..8ce4d434083 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 53f9bf38a32..de21d99335f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.2" +version = "2023.9.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From caacdabd3d3c11563e5fd7908cdfa3cf478c6b11 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 23 Sep 2023 14:14:57 -0700 Subject: [PATCH 195/202] Fix rainbird unique id (#99704) * Don't set a unique id for devices with no serial * Add additional check for the same config entry host/port when there is no serial * Update homeassistant/components/rainbird/config_flow.py Co-authored-by: Robert Resch * Update tests/components/rainbird/test_config_flow.py Co-authored-by: Robert Resch * Update tests/components/rainbird/test_config_flow.py Co-authored-by: Robert Resch --------- Co-authored-by: Robert Resch --- .../components/rainbird/config_flow.py | 9 +- tests/components/rainbird/conftest.py | 12 +- tests/components/rainbird/test_config_flow.py | 121 +++++++++++++++++- 3 files changed, 136 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rainbird/config_flow.py b/homeassistant/components/rainbird/config_flow.py index a784e4623d6..bf6682e7a6f 100644 --- a/homeassistant/components/rainbird/config_flow.py +++ b/homeassistant/components/rainbird/config_flow.py @@ -125,8 +125,13 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): options: dict[str, Any], ) -> FlowResult: """Create the config entry.""" - await self.async_set_unique_id(serial_number) - self._abort_if_unique_id_configured() + # Prevent devices with the same serial number. If the device does not have a serial number + # then we can at least prevent configuring the same host twice. + if serial_number: + await self.async_set_unique_id(serial_number) + self._abort_if_unique_id_configured() + else: + self._async_abort_entries_match(data) return self.async_create_entry( title=data[CONF_HOST], data=data, diff --git a/tests/components/rainbird/conftest.py b/tests/components/rainbird/conftest.py index 9e4e4e546cb..40b400210aa 100644 --- a/tests/components/rainbird/conftest.py +++ b/tests/components/rainbird/conftest.py @@ -35,6 +35,7 @@ SERIAL_NUMBER = 0x12635436566 # Get serial number Command 0x85. Serial is 0x12635436566 SERIAL_RESPONSE = "850000012635436566" +ZERO_SERIAL_RESPONSE = "850000000000000000" # Model and version command 0x82 MODEL_AND_VERSION_RESPONSE = "820006090C" # Get available stations command 0x83 @@ -84,6 +85,12 @@ def yaml_config() -> dict[str, Any]: return {} +@pytest.fixture +async def unique_id() -> str: + """Fixture for serial number used in the config entry.""" + return SERIAL_NUMBER + + @pytest.fixture async def config_entry_data() -> dict[str, Any]: """Fixture for MockConfigEntry data.""" @@ -92,13 +99,14 @@ async def config_entry_data() -> dict[str, Any]: @pytest.fixture async def config_entry( - config_entry_data: dict[str, Any] | None + config_entry_data: dict[str, Any] | None, + unique_id: str, ) -> MockConfigEntry | None: """Fixture for MockConfigEntry.""" if config_entry_data is None: return None return MockConfigEntry( - unique_id=SERIAL_NUMBER, + unique_id=unique_id, domain=DOMAIN, data=config_entry_data, options={ATTR_DURATION: DEFAULT_TRIGGER_TIME_MINUTES}, diff --git a/tests/components/rainbird/test_config_flow.py b/tests/components/rainbird/test_config_flow.py index f11eba4fed7..e7337ad6508 100644 --- a/tests/components/rainbird/test_config_flow.py +++ b/tests/components/rainbird/test_config_flow.py @@ -3,6 +3,7 @@ import asyncio from collections.abc import Generator from http import HTTPStatus +from typing import Any from unittest.mock import Mock, patch import pytest @@ -19,8 +20,11 @@ from .conftest import ( CONFIG_ENTRY_DATA, HOST, PASSWORD, + SERIAL_NUMBER, SERIAL_RESPONSE, URL, + ZERO_SERIAL_RESPONSE, + ComponentSetup, mock_response, ) @@ -66,19 +70,132 @@ async def complete_flow(hass: HomeAssistant) -> FlowResult: ) -async def test_controller_flow(hass: HomeAssistant, mock_setup: Mock) -> None: +@pytest.mark.parametrize( + ("responses", "expected_config_entry", "expected_unique_id"), + [ + ( + [mock_response(SERIAL_RESPONSE)], + CONFIG_ENTRY_DATA, + SERIAL_NUMBER, + ), + ( + [mock_response(ZERO_SERIAL_RESPONSE)], + {**CONFIG_ENTRY_DATA, "serial_number": 0}, + None, + ), + ], +) +async def test_controller_flow( + hass: HomeAssistant, + mock_setup: Mock, + expected_config_entry: dict[str, str], + expected_unique_id: int | None, +) -> None: """Test the controller is setup correctly.""" result = await complete_flow(hass) assert result.get("type") == "create_entry" assert result.get("title") == HOST assert "result" in result - assert result["result"].data == CONFIG_ENTRY_DATA + assert dict(result["result"].data) == expected_config_entry assert result["result"].options == {ATTR_DURATION: 6} + assert result["result"].unique_id == expected_unique_id assert len(mock_setup.mock_calls) == 1 +@pytest.mark.parametrize( + ( + "unique_id", + "config_entry_data", + "config_flow_responses", + "expected_config_entry", + ), + [ + ( + "other-serial-number", + {**CONFIG_ENTRY_DATA, "host": "other-host"}, + [mock_response(SERIAL_RESPONSE)], + CONFIG_ENTRY_DATA, + ), + ( + None, + {**CONFIG_ENTRY_DATA, "serial_number": 0, "host": "other-host"}, + [mock_response(ZERO_SERIAL_RESPONSE)], + {**CONFIG_ENTRY_DATA, "serial_number": 0}, + ), + ], + ids=["with-serial", "zero-serial"], +) +async def test_multiple_config_entries( + hass: HomeAssistant, + setup_integration: ComponentSetup, + responses: list[AiohttpClientMockResponse], + config_flow_responses: list[AiohttpClientMockResponse], + expected_config_entry: dict[str, Any] | None, +) -> None: + """Test setting up multiple config entries that refer to different devices.""" + assert await setup_integration() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + + responses.clear() + responses.extend(config_flow_responses) + + result = await complete_flow(hass) + assert result.get("type") == FlowResultType.CREATE_ENTRY + assert dict(result.get("result").data) == expected_config_entry + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 2 + + +@pytest.mark.parametrize( + ( + "unique_id", + "config_entry_data", + "config_flow_responses", + ), + [ + ( + SERIAL_NUMBER, + CONFIG_ENTRY_DATA, + [mock_response(SERIAL_RESPONSE)], + ), + ( + None, + {**CONFIG_ENTRY_DATA, "serial_number": 0}, + [mock_response(ZERO_SERIAL_RESPONSE)], + ), + ], + ids=[ + "duplicate-serial-number", + "duplicate-host-port-no-serial", + ], +) +async def test_duplicate_config_entries( + hass: HomeAssistant, + setup_integration: ComponentSetup, + responses: list[AiohttpClientMockResponse], + config_flow_responses: list[AiohttpClientMockResponse], +) -> None: + """Test that a device can not be registered twice.""" + assert await setup_integration() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + + responses.clear() + responses.extend(config_flow_responses) + + result = await complete_flow(hass) + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "already_configured" + + async def test_controller_cannot_connect( hass: HomeAssistant, mock_setup: Mock, From 1779222062c0d1c91ad3a5b6435a53909243a1df Mon Sep 17 00:00:00 2001 From: sdb9696 <51370195+sdb9696@users.noreply.github.com> Date: Sat, 23 Sep 2023 20:21:34 +0100 Subject: [PATCH 196/202] Bump ring-doorbell to 0.7.3 (#100688) Bump ring to 0.7.3 --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 355c630272e..0b5198f36d3 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/ring", "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], - "requirements": ["ring-doorbell==0.7.2"] + "requirements": ["ring-doorbell==0.7.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0284414b6f9..17c064e9aac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2303,7 +2303,7 @@ rfk101py==0.0.1 rflink==0.0.65 # homeassistant.components.ring -ring-doorbell==0.7.2 +ring-doorbell==0.7.3 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 80bc2828150..c3a9bc69cce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1690,7 +1690,7 @@ reolink-aio==0.7.10 rflink==0.0.65 # homeassistant.components.ring -ring-doorbell==0.7.2 +ring-doorbell==0.7.3 # homeassistant.components.roku rokuecp==0.18.1 From 4fb482610baaf0fa627bef1ca732c8a0dfc003f0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 23 Sep 2023 17:38:21 +0200 Subject: [PATCH 197/202] Update home-assistant/wheels to 2023.09.1 (#100758) * Update home-assistant/wheels to 2023.09.0 * Update home-assistant/wheels to 2023.09.1 --- .github/workflows/wheels.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 01823199c17..c6f819f9dfd 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -97,7 +97,7 @@ jobs: name: requirements_diff - name: Build wheels - uses: home-assistant/wheels@2023.04.0 + uses: home-assistant/wheels@2023.09.1 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 @@ -178,7 +178,7 @@ jobs: sed -i "/numpy/d" homeassistant/package_constraints.txt - name: Build wheels (part 1) - uses: home-assistant/wheels@2023.04.0 + uses: home-assistant/wheels@2023.09.1 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 @@ -192,7 +192,7 @@ jobs: requirements: "requirements_all.txtaa" - name: Build wheels (part 2) - uses: home-assistant/wheels@2023.04.0 + uses: home-assistant/wheels@2023.09.1 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 @@ -206,7 +206,7 @@ jobs: requirements: "requirements_all.txtab" - name: Build wheels (part 3) - uses: home-assistant/wheels@2023.04.0 + uses: home-assistant/wheels@2023.09.1 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 From 25cb835faf845d35beaf493ef1d2f6b850cea916 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Mon, 4 Sep 2023 13:51:33 -0400 Subject: [PATCH 198/202] Bump pyschlage to 2023.9.0 (#99624) --- homeassistant/components/schlage/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/schlage/manifest.json b/homeassistant/components/schlage/manifest.json index 25316004c58..fb4ccc81dee 100644 --- a/homeassistant/components/schlage/manifest.json +++ b/homeassistant/components/schlage/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/schlage", "iot_class": "cloud_polling", - "requirements": ["pyschlage==2023.8.1"] + "requirements": ["pyschlage==2023.9.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 17c064e9aac..1cf5f8198b4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1988,7 +1988,7 @@ pysabnzbd==1.1.1 pysaj==0.0.16 # homeassistant.components.schlage -pyschlage==2023.8.1 +pyschlage==2023.9.0 # homeassistant.components.sensibo pysensibo==1.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c3a9bc69cce..535686181ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1477,7 +1477,7 @@ pyrympro==0.0.7 pysabnzbd==1.1.1 # homeassistant.components.schlage -pyschlage==2023.8.1 +pyschlage==2023.9.0 # homeassistant.components.sensibo pysensibo==1.0.33 From 0eac0bb3c59429e52f282017fd9ce1aaf707fa89 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Sat, 23 Sep 2023 14:37:57 -0400 Subject: [PATCH 199/202] Bump pyschlage to 2023.9.1 (#100760) --- homeassistant/components/schlage/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/schlage/manifest.json b/homeassistant/components/schlage/manifest.json index fb4ccc81dee..3568692c6ca 100644 --- a/homeassistant/components/schlage/manifest.json +++ b/homeassistant/components/schlage/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/schlage", "iot_class": "cloud_polling", - "requirements": ["pyschlage==2023.9.0"] + "requirements": ["pyschlage==2023.9.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1cf5f8198b4..e9eaf4acdc9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1988,7 +1988,7 @@ pysabnzbd==1.1.1 pysaj==0.0.16 # homeassistant.components.schlage -pyschlage==2023.9.0 +pyschlage==2023.9.1 # homeassistant.components.sensibo pysensibo==1.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 535686181ae..e1c571d6472 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1477,7 +1477,7 @@ pyrympro==0.0.7 pysabnzbd==1.1.1 # homeassistant.components.schlage -pyschlage==2023.9.0 +pyschlage==2023.9.1 # homeassistant.components.sensibo pysensibo==1.0.33 From f22ecf2a09caffe5e00dfe05399e66916b099013 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 23 Sep 2023 23:08:07 +0200 Subject: [PATCH 200/202] Add strong to fan mode for Sensibo (#100773) --- homeassistant/components/sensibo/climate.py | 10 +++++++++- homeassistant/components/sensibo/strings.json | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index da86ba8fe24..2e2b92179f0 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -54,7 +54,15 @@ ATTR_HORIZONTAL_SWING_MODE = "horizontal_swing_mode" ATTR_LIGHT = "light" BOOST_INCLUSIVE = "boost_inclusive" -AVAILABLE_FAN_MODES = {"quiet", "low", "medium", "medium_high", "high", "auto"} +AVAILABLE_FAN_MODES = { + "quiet", + "low", + "medium", + "medium_high", + "high", + "strong", + "auto", +} AVAILABLE_SWING_MODES = { "stopped", "fixedtop", diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json index a6f14b73ace..ddd164225fc 100644 --- a/homeassistant/components/sensibo/strings.json +++ b/homeassistant/components/sensibo/strings.json @@ -127,6 +127,7 @@ "low": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::low%]", "medium": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::medium%]", "medium_high": "Medium high", + "strong": "Strong", "quiet": "Quiet" } }, @@ -211,6 +212,7 @@ "low": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::low%]", "medium": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::medium%]", "medium_high": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::fanlevel::state::medium_high%]", + "strong": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::fanlevel::state::strong%]", "quiet": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::fanlevel::state::quiet%]" } }, @@ -347,6 +349,7 @@ "fan_mode": { "state": { "quiet": "Quiet", + "strong": "Strong", "low": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::low%]", "medium": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::medium%]", "medium_high": "Medium high", From 0ae285c404b7050675e20354d229a99974796590 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 24 Sep 2023 14:36:02 +0200 Subject: [PATCH 201/202] Update home-assistant/builder to 2023.09.0 (#100797) --- .github/workflows/builder.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 3296f33f84c..1adcc269eb9 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -197,7 +197,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2023.08.0 + uses: home-assistant/builder@2023.09.0 with: args: | $BUILD_ARGS \ @@ -205,8 +205,6 @@ jobs: --cosign \ --target /data \ --generic ${{ needs.init.outputs.version }} - env: - CAS_API_KEY: ${{ secrets.CAS_TOKEN }} - name: Archive translations shell: bash @@ -275,15 +273,13 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2023.08.0 + uses: home-assistant/builder@2023.09.0 with: args: | $BUILD_ARGS \ --target /data/machine \ --cosign \ --machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}" - env: - CAS_API_KEY: ${{ secrets.CAS_TOKEN }} publish_ha: name: Publish version files From 49b8937bb3ef088c759e98773241e838d14f9c61 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 24 Sep 2023 14:51:56 +0200 Subject: [PATCH 202/202] Fix Comelit device info (#100587) --- .../components/comelit/coordinator.py | 39 ++++++++++++++++++- homeassistant/components/comelit/light.py | 14 ++----- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/comelit/coordinator.py b/homeassistant/components/comelit/coordinator.py index 1fcbd7c0d37..df1d745ce8a 100644 --- a/homeassistant/components/comelit/coordinator.py +++ b/homeassistant/components/comelit/coordinator.py @@ -3,11 +3,14 @@ import asyncio from datetime import timedelta from typing import Any -from aiocomelit import ComeliteSerialBridgeApi +from aiocomelit import ComeliteSerialBridgeApi, ComelitSerialBridgeObject +from aiocomelit.const import BRIDGE import aiohttp +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import _LOGGER, DOMAIN @@ -16,6 +19,8 @@ from .const import _LOGGER, DOMAIN class ComelitSerialBridge(DataUpdateCoordinator): """Queries Comelit Serial Bridge.""" + config_entry: ConfigEntry + def __init__(self, hass: HomeAssistant, host: str, pin: int) -> None: """Initialize the scanner.""" @@ -30,6 +35,38 @@ class ComelitSerialBridge(DataUpdateCoordinator): name=f"{DOMAIN}-{host}-coordinator", update_interval=timedelta(seconds=5), ) + device_registry = dr.async_get(self.hass) + device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + identifiers={(DOMAIN, self.config_entry.entry_id)}, + model=BRIDGE, + name=f"{BRIDGE} ({self.api.host})", + **self.basic_device_info, + ) + + @property + def basic_device_info(self) -> dict: + """Set basic device info.""" + + return { + "manufacturer": "Comelit", + "hw_version": "20003101", + } + + def platform_device_info( + self, device: ComelitSerialBridgeObject, platform: str + ) -> dr.DeviceInfo: + """Set platform device info.""" + + return dr.DeviceInfo( + identifiers={ + (DOMAIN, f"{self.config_entry.entry_id}-{platform}-{device.index}") + }, + via_device=(DOMAIN, self.config_entry.entry_id), + name=device.name, + model=f"{BRIDGE} {platform}", + **self.basic_device_info, + ) async def _async_update_data(self) -> dict[str, Any]: """Update router data.""" diff --git a/homeassistant/components/comelit/light.py b/homeassistant/components/comelit/light.py index 9a893bd929c..a4a534025f0 100644 --- a/homeassistant/components/comelit/light.py +++ b/homeassistant/components/comelit/light.py @@ -9,7 +9,6 @@ from aiocomelit.const import LIGHT, LIGHT_OFF, LIGHT_ON from homeassistant.components.light import LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -37,27 +36,20 @@ class ComelitLightEntity(CoordinatorEntity[ComelitSerialBridge], LightEntity): """Light device.""" _attr_has_entity_name = True - _attr_name = None def __init__( self, coordinator: ComelitSerialBridge, device: ComelitSerialBridgeObject, - config_entry_unique_id: str | None, + config_entry_unique_id: str, ) -> None: """Init light entity.""" self._api = coordinator.api self._device = device super().__init__(coordinator) + self._attr_name = device.name self._attr_unique_id = f"{config_entry_unique_id}-{device.index}" - self._attr_device_info = DeviceInfo( - identifiers={ - (DOMAIN, self._attr_unique_id), - }, - manufacturer="Comelit", - model="Serial Bridge", - name=device.name, - ) + self._attr_device_info = self.coordinator.platform_device_info(device, LIGHT) async def _light_set_state(self, state: int) -> None: """Set desired light state."""