From 2196bd3a13a277fd8ea84fa42728df740139ba28 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis <jbouwh@users.noreply.github.com> Date: Thu, 6 Jul 2023 17:14:09 +0200 Subject: [PATCH 01/18] Fix not including device_name in friendly name if it is None (#95485) * Omit device_name in friendly name if it is None * Fix test --- homeassistant/helpers/entity.py | 5 +++-- tests/helpers/test_entity.py | 23 ++++++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 3d22e2538a3322..e87eb15b9545ec 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -772,9 +772,10 @@ def _friendly_name_internal(self) -> str | None: ): return name + device_name = device_entry.name_by_user or device_entry.name if self.use_device_name: - return device_entry.name_by_user or device_entry.name - return f"{device_entry.name_by_user or device_entry.name} {name}" + return device_name + return f"{device_name} {name}" if device_name else name @callback def _async_write_ha_state(self) -> None: diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 85a7932aef89f1..7de6f70e793698 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -20,7 +20,7 @@ from homeassistant.core import Context, HomeAssistant, HomeAssistantError from homeassistant.helpers import device_registry as dr, entity, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity -from homeassistant.helpers.typing import UNDEFINED +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from tests.common import ( MockConfigEntry, @@ -989,12 +989,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @pytest.mark.parametrize( - ("has_entity_name", "entity_name", "expected_friendly_name", "warn_implicit_name"), ( - (False, "Entity Blu", "Entity Blu", False), - (False, None, None, False), - (True, "Entity Blu", "Device Bla Entity Blu", False), - (True, None, "Device Bla", False), + "has_entity_name", + "entity_name", + "device_name", + "expected_friendly_name", + "warn_implicit_name", + ), + ( + (False, "Entity Blu", "Device Bla", "Entity Blu", False), + (False, None, "Device Bla", None, False), + (True, "Entity Blu", "Device Bla", "Device Bla Entity Blu", False), + (True, None, "Device Bla", "Device Bla", False), + (True, "Entity Blu", UNDEFINED, "Entity Blu", False), + (True, "Entity Blu", None, "Mock Title Entity Blu", False), ), ) async def test_friendly_name_attr( @@ -1002,6 +1010,7 @@ async def test_friendly_name_attr( caplog: pytest.LogCaptureFixture, has_entity_name: bool, entity_name: str | None, + device_name: str | None | UndefinedType, expected_friendly_name: str | None, warn_implicit_name: bool, ) -> None: @@ -1012,7 +1021,7 @@ async def test_friendly_name_attr( device_info={ "identifiers": {("hue", "1234")}, "connections": {(dr.CONNECTION_NETWORK_MAC, "abcd")}, - "name": "Device Bla", + "name": device_name, }, ) ent._attr_has_entity_name = has_entity_name From 3e19fba7d359136bf21d3712ebda55df0593c70a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Thu, 6 Jul 2023 05:19:06 -1000 Subject: [PATCH 02/18] Handle integrations with empty services or failing to load during service description enumeration (#95911) * wip * tweaks * tweaks * add coverage * complain loudly as we never execpt this to happen * ensure not None * comment it --- homeassistant/helpers/service.py | 61 +++++++++--------- tests/helpers/test_service.py | 102 +++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 28 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index fa0e57d501c4e3..09c861421b019e 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -566,7 +566,9 @@ async def async_get_all_descriptions( hass: HomeAssistant, ) -> dict[str, dict[str, Any]]: """Return descriptions (i.e. user documentation) for all service calls.""" - descriptions_cache = hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {}) + descriptions_cache: dict[ + tuple[str, str], dict[str, Any] | None + ] = hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {}) services = hass.services.async_services() # See if there are new services not seen before. @@ -574,59 +576,60 @@ async def async_get_all_descriptions( missing = set() all_services = [] for domain in services: - for service in services[domain]: - cache_key = (domain, service) + for service_name in services[domain]: + cache_key = (domain, service_name) all_services.append(cache_key) if cache_key not in descriptions_cache: missing.add(domain) # If we have a complete cache, check if it is still valid - if ALL_SERVICE_DESCRIPTIONS_CACHE in hass.data: - previous_all_services, previous_descriptions_cache = hass.data[ - ALL_SERVICE_DESCRIPTIONS_CACHE - ] + if all_cache := hass.data.get(ALL_SERVICE_DESCRIPTIONS_CACHE): + previous_all_services, previous_descriptions_cache = all_cache # If the services are the same, we can return the cache if previous_all_services == all_services: return cast(dict[str, dict[str, Any]], previous_descriptions_cache) # Files we loaded for missing descriptions - loaded = {} + loaded: dict[str, JSON_TYPE] = {} if missing: ints_or_excs = await async_get_integrations(hass, missing) - integrations = [ - int_or_exc - for int_or_exc in ints_or_excs.values() - if isinstance(int_or_exc, Integration) - ] - + integrations: list[Integration] = [] + for domain, int_or_exc in ints_or_excs.items(): + if type(int_or_exc) is Integration: # pylint: disable=unidiomatic-typecheck + integrations.append(int_or_exc) + continue + if TYPE_CHECKING: + assert isinstance(int_or_exc, Exception) + _LOGGER.error("Failed to load integration: %s", domain, exc_info=int_or_exc) contents = await hass.async_add_executor_job( _load_services_files, hass, integrations ) - - for domain, content in zip(missing, contents): - loaded[domain] = content + loaded = dict(zip(missing, contents)) # Build response descriptions: dict[str, dict[str, Any]] = {} - for domain in services: + for domain, services_map in services.items(): descriptions[domain] = {} + domain_descriptions = descriptions[domain] - for service in services[domain]: - cache_key = (domain, service) + for service_name in services_map: + cache_key = (domain, service_name) description = descriptions_cache.get(cache_key) - # Cache missing descriptions if description is None: - domain_yaml = loaded[domain] + domain_yaml = loaded.get(domain) or {} + # The YAML may be empty for dynamically defined + # services (ie shell_command) that never call + # service.async_set_service_schema for the dynamic + # service yaml_description = domain_yaml.get( # type: ignore[union-attr] - service, {} + service_name, {} ) # Don't warn for missing services, because it triggers false # positives for things like scripts, that register as a service - description = { "name": yaml_description.get("name", ""), "description": yaml_description.get("description", ""), @@ -637,7 +640,7 @@ async def async_get_all_descriptions( description["target"] = yaml_description["target"] if ( - response := hass.services.supports_response(domain, service) + response := hass.services.supports_response(domain, service_name) ) != SupportsResponse.NONE: description["response"] = { "optional": response == SupportsResponse.OPTIONAL, @@ -645,7 +648,7 @@ async def async_get_all_descriptions( descriptions_cache[cache_key] = description - descriptions[domain][service] = description + domain_descriptions[service_name] = description hass.data[ALL_SERVICE_DESCRIPTIONS_CACHE] = (all_services, descriptions) return descriptions @@ -667,7 +670,9 @@ def async_set_service_schema( hass: HomeAssistant, domain: str, service: str, schema: dict[str, Any] ) -> None: """Register a description for a service.""" - hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {}) + descriptions_cache: dict[ + tuple[str, str], dict[str, Any] | None + ] = hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {}) description = { "name": schema.get("name", ""), @@ -679,7 +684,7 @@ def async_set_service_schema( description["target"] = schema["target"] hass.data.pop(ALL_SERVICE_DESCRIPTIONS_CACHE, None) - hass.data[SERVICE_DESCRIPTION_CACHE][(domain, service)] = description + descriptions_cache[(domain, service)] = description @bind_hass diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index f6299312b5391b..b062a3233055f2 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -605,6 +605,108 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: assert await service.async_get_all_descriptions(hass) is descriptions +async def test_async_get_all_descriptions_failing_integration( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test async_get_all_descriptions when async_get_integrations returns an exception.""" + group = hass.components.group + group_config = {group.DOMAIN: {}} + await async_setup_component(hass, group.DOMAIN, group_config) + descriptions = await service.async_get_all_descriptions(hass) + + assert len(descriptions) == 1 + + assert "description" in descriptions["group"]["reload"] + assert "fields" in descriptions["group"]["reload"] + + logger = hass.components.logger + logger_config = {logger.DOMAIN: {}} + await async_setup_component(hass, logger.DOMAIN, logger_config) + with patch( + "homeassistant.helpers.service.async_get_integrations", + return_value={"logger": ImportError}, + ): + descriptions = await service.async_get_all_descriptions(hass) + + assert len(descriptions) == 2 + assert "Failed to load integration: logger" in caplog.text + + # Services are empty defaults if the load fails but should + # not raise + assert descriptions[logger.DOMAIN]["set_level"] == { + "description": "", + "fields": {}, + "name": "", + } + + hass.services.async_register(logger.DOMAIN, "new_service", lambda x: None, None) + service.async_set_service_schema( + hass, logger.DOMAIN, "new_service", {"description": "new service"} + ) + descriptions = await service.async_get_all_descriptions(hass) + assert "description" in descriptions[logger.DOMAIN]["new_service"] + assert descriptions[logger.DOMAIN]["new_service"]["description"] == "new service" + + hass.services.async_register( + logger.DOMAIN, "another_new_service", lambda x: None, None + ) + hass.services.async_register( + logger.DOMAIN, + "service_with_optional_response", + lambda x: None, + None, + SupportsResponse.OPTIONAL, + ) + hass.services.async_register( + logger.DOMAIN, + "service_with_only_response", + lambda x: None, + None, + SupportsResponse.ONLY, + ) + + descriptions = await service.async_get_all_descriptions(hass) + assert "another_new_service" in descriptions[logger.DOMAIN] + assert "service_with_optional_response" in descriptions[logger.DOMAIN] + assert descriptions[logger.DOMAIN]["service_with_optional_response"][ + "response" + ] == {"optional": True} + assert "service_with_only_response" in descriptions[logger.DOMAIN] + assert descriptions[logger.DOMAIN]["service_with_only_response"]["response"] == { + "optional": False + } + + # Verify the cache returns the same object + assert await service.async_get_all_descriptions(hass) is descriptions + + +async def test_async_get_all_descriptions_dynamically_created_services( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test async_get_all_descriptions when async_get_integrations when services are dynamic.""" + group = hass.components.group + group_config = {group.DOMAIN: {}} + await async_setup_component(hass, group.DOMAIN, group_config) + descriptions = await service.async_get_all_descriptions(hass) + + assert len(descriptions) == 1 + + assert "description" in descriptions["group"]["reload"] + assert "fields" in descriptions["group"]["reload"] + + shell_command = hass.components.shell_command + shell_command_config = {shell_command.DOMAIN: {"test_service": "ls /bin"}} + await async_setup_component(hass, shell_command.DOMAIN, shell_command_config) + descriptions = await service.async_get_all_descriptions(hass) + + assert len(descriptions) == 2 + assert descriptions[shell_command.DOMAIN]["test_service"] == { + "description": "", + "fields": {}, + "name": "", + } + + async def test_call_with_required_features(hass: HomeAssistant, mock_entities) -> None: """Test service calls invoked only if entity has required features.""" test_service_mock = AsyncMock(return_value=None) From 57369be3229ceae47e10589171875372349a61c7 Mon Sep 17 00:00:00 2001 From: Bram Kragten <mail@bramkragten.nl> Date: Wed, 5 Jul 2023 18:20:10 +0200 Subject: [PATCH 03/18] Update frontend to 20230705.1 (#95913) --- 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 9f53aef8165836..07c5585833dd20 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==20230705.0"] + "requirements": ["home-assistant-frontend==20230705.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1686f91bac365d..71ca8fc4c3ea40 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.0 hass-nabucasa==0.69.0 hassil==1.0.6 home-assistant-bluetooth==1.10.0 -home-assistant-frontend==20230705.0 +home-assistant-frontend==20230705.1 home-assistant-intents==2023.6.28 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9ef98d4e402af5..c6294603728966 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -980,7 +980,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230705.0 +home-assistant-frontend==20230705.1 # homeassistant.components.conversation home-assistant-intents==2023.6.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c075eb142621a..c28835fee5530e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -763,7 +763,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230705.0 +home-assistant-frontend==20230705.1 # homeassistant.components.conversation home-assistant-intents==2023.6.28 From 4229778cf6ff473da194744e97a2b22ea801699f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@koston.org> Date: Wed, 5 Jul 2023 18:56:09 -0500 Subject: [PATCH 04/18] Make SwitchBot no_devices_found message more helpful (#95916) --- homeassistant/components/switchbot/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index 2d31a883e4b2d1..fb9f906527cd1e 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -44,7 +44,7 @@ }, "abort": { "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", - "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "no_devices_found": "No supported SwitchBot devices found in range; If the device is in range, ensure the scanner has active scanning enabled, as SwitchBot devices cannot be discovered with passive scans. Active scans can be disabled once the device is configured. If you need clarification on whether the device is in-range, download the diagnostics for the integration that provides your Bluetooth adapter or proxy and check if the MAC address of the SwitchBot device is present.", "unknown": "[%key:common::config_flow::error::unknown%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "switchbot_unsupported_type": "Unsupported Switchbot Type." From 6275932c293242dbaf5fdb46cb88fd3538ccf4e7 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 6 Jul 2023 11:47:51 -0400 Subject: [PATCH 05/18] Migrate bracketed IP addresses in ZHA config entry (#95917) * Automatically correct IP addresses surrounded by brackets * Simplify regex * Move pattern inline * Maintain old behavior of stripping whitespace --- homeassistant/components/zha/__init__.py | 24 ++++++++++++++++++++---- tests/components/zha/test_init.py | 12 ++++++++++-- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 5607cabffea19f..8a81648b580ae6 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -3,6 +3,7 @@ import copy import logging import os +import re import voluptuous as vol from zhaquirks import setup as setup_quirks @@ -85,19 +86,34 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True +def _clean_serial_port_path(path: str) -> str: + """Clean the serial port path, applying corrections where necessary.""" + + if path.startswith("socket://"): + path = path.strip() + + # Removes extraneous brackets from IP addresses (they don't parse in CPython 3.11.4) + if re.match(r"^socket://\[\d+\.\d+\.\d+\.\d+\]:\d+$", path): + path = path.replace("[", "").replace("]", "") + + return path + + async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up ZHA. Will automatically load components to support devices found on the network. """ - # Strip whitespace around `socket://` URIs, this is no longer accepted by zigpy - # This will be removed in 2023.7.0 + # Remove brackets around IP addresses, this no longer works in CPython 3.11.4 + # This will be removed in 2023.11.0 path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] + cleaned_path = _clean_serial_port_path(path) data = copy.deepcopy(dict(config_entry.data)) - if path.startswith("socket://") and path != path.strip(): - data[CONF_DEVICE][CONF_DEVICE_PATH] = path.strip() + if path != cleaned_path: + _LOGGER.debug("Cleaned serial port path %r -> %r", path, cleaned_path) + data[CONF_DEVICE][CONF_DEVICE_PATH] = cleaned_path hass.config_entries.async_update_entry(config_entry, data=data) zha_data = hass.data.setdefault(DATA_ZHA, {}) diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index 23a76de4c2504a..24ee63fb3d5064 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -114,19 +114,27 @@ async def test_config_depreciation(hass: HomeAssistant, zha_config) -> None: @pytest.mark.parametrize( ("path", "cleaned_path"), [ + # No corrections ("/dev/path1", "/dev/path1"), + ("/dev/path1[asd]", "/dev/path1[asd]"), ("/dev/path1 ", "/dev/path1 "), + ("socket://1.2.3.4:5678", "socket://1.2.3.4:5678"), + # Brackets around URI + ("socket://[1.2.3.4]:5678", "socket://1.2.3.4:5678"), + # Spaces ("socket://dev/path1 ", "socket://dev/path1"), + # Both + ("socket://[1.2.3.4]:5678 ", "socket://1.2.3.4:5678"), ], ) @patch("homeassistant.components.zha.setup_quirks", Mock(return_value=True)) @patch( "homeassistant.components.zha.websocket_api.async_load_api", Mock(return_value=True) ) -async def test_setup_with_v3_spaces_in_uri( +async def test_setup_with_v3_cleaning_uri( hass: HomeAssistant, path: str, cleaned_path: str ) -> None: - """Test migration of config entry from v3 with spaces after `socket://` URI.""" + """Test migration of config entry from v3, applying corrections to the port path.""" config_entry_v3 = MockConfigEntry( domain=DOMAIN, data={ From 4c10d186c06c6e87c7c6d25f0714556fe9ae27c5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker <joostlek@outlook.com> Date: Thu, 6 Jul 2023 16:17:59 +0200 Subject: [PATCH 06/18] Use device name for Nuki (#95941) --- homeassistant/components/nuki/lock.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 55560d3bf8c213..a1a75ef8260a82 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -72,6 +72,7 @@ class NukiDeviceEntity(NukiEntity[_NukiDeviceT], LockEntity): _attr_has_entity_name = True _attr_supported_features = LockEntityFeature.OPEN _attr_translation_key = "nuki_lock" + _attr_name = None @property def unique_id(self) -> str | None: From 95594a23dccce885880ffc9f18bb391be4448bd3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker <joostlek@outlook.com> Date: Thu, 6 Jul 2023 16:19:42 +0200 Subject: [PATCH 07/18] Add explicit device naming for Tuya sensors (#95944) --- homeassistant/components/tuya/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index a2cd2d5fc410b6..afa40f27afd9b4 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -511,6 +511,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "rqbj": ( TuyaSensorEntityDescription( key=DPCode.GAS_SENSOR_VALUE, + name=None, icon="mdi:gas-cylinder", state_class=SensorStateClass.MEASUREMENT, ), @@ -633,6 +634,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "ylcg": ( TuyaSensorEntityDescription( key=DPCode.PRESSURE_VALUE, + name=None, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), From 224886eb29731efae1d152a2e07b221de33e7eb2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker <joostlek@outlook.com> Date: Thu, 6 Jul 2023 16:21:15 +0200 Subject: [PATCH 08/18] Fix entity name for Flick Electric (#95947) Fix entity name --- homeassistant/components/flick_electric/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/flick_electric/sensor.py b/homeassistant/components/flick_electric/sensor.py index 2210f44bf7a8e1..a0844fe6cdb7c9 100644 --- a/homeassistant/components/flick_electric/sensor.py +++ b/homeassistant/components/flick_electric/sensor.py @@ -34,6 +34,7 @@ class FlickPricingSensor(SensorEntity): _attr_attribution = "Data provided by Flick Electric" _attr_native_unit_of_measurement = f"{CURRENCY_CENT}/{UnitOfEnergy.KILO_WATT_HOUR}" + _attr_has_entity_name = True _attr_translation_key = "power_price" _attributes: dict[str, Any] = {} From 7a21e858abff7a9e85835b05ef662835eed6ee9f Mon Sep 17 00:00:00 2001 From: neocolis <neocolis@gmail.com> Date: Thu, 6 Jul 2023 08:50:51 -0400 Subject: [PATCH 09/18] Fix matter exception NoneType in set_brightness for optional min/max level values (#95949) Fix exception NoneType in set_brightness for optional min/max level values --- homeassistant/components/matter/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index facdb6752d3b0a..02919baa8f1a4b 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -128,7 +128,7 @@ async def _set_brightness(self, brightness: int) -> None: renormalize( brightness, (0, 255), - (level_control.minLevel, level_control.maxLevel), + (level_control.minLevel or 1, level_control.maxLevel or 254), ) ) @@ -220,7 +220,7 @@ def _get_brightness(self) -> int: return round( renormalize( level_control.currentLevel, - (level_control.minLevel or 0, level_control.maxLevel or 254), + (level_control.minLevel or 1, level_control.maxLevel or 254), (0, 255), ) ) From bca5aae3bbd1c29563ccdbfea8a34d94a6f575f3 Mon Sep 17 00:00:00 2001 From: micha91 <michael.harbarth@gmx.de> Date: Thu, 6 Jul 2023 17:20:20 +0200 Subject: [PATCH 10/18] Fix grouping feature for MusicCast (#95958) check the current source for grouping using the source ID instead of the label --- .../yamaha_musiccast/media_player.py | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index cf6feb44fbd772..42549fb20d9164 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -130,14 +130,11 @@ def zone_id(self): @property def _is_netusb(self): - return ( - self.coordinator.data.netusb_input - == self.coordinator.data.zones[self._zone_id].input - ) + return self.coordinator.data.netusb_input == self.source_id @property def _is_tuner(self): - return self.coordinator.data.zones[self._zone_id].input == "tuner" + return self.source_id == "tuner" @property def media_content_id(self): @@ -516,10 +513,15 @@ async def async_select_source(self, source: str) -> None: self._zone_id, self.reverse_source_mapping.get(source, source) ) + @property + def source_id(self): + """ID of the current input source.""" + return self.coordinator.data.zones[self._zone_id].input + @property def source(self): """Name of the current input source.""" - return self.source_mapping.get(self.coordinator.data.zones[self._zone_id].input) + return self.source_mapping.get(self.source_id) @property def source_list(self): @@ -597,7 +599,7 @@ def is_network_client(self) -> bool: return ( self.coordinator.data.group_role == "client" and self.coordinator.data.group_id != NULL_GROUP - and self.source == ATTR_MC_LINK + and self.source_id == ATTR_MC_LINK ) @property @@ -606,7 +608,7 @@ def is_client(self) -> bool: If the media player is not part of a group, False is returned. """ - return self.is_network_client or self.source == ATTR_MAIN_SYNC + return self.is_network_client or self.source_id == ATTR_MAIN_SYNC def get_all_mc_entities(self) -> list[MusicCastMediaPlayer]: """Return all media player entities of the musiccast system.""" @@ -639,11 +641,11 @@ def is_part_of_group(self, group_server) -> bool: and self.coordinator.data.group_id == group_server.coordinator.data.group_id and self.ip_address != group_server.ip_address - and self.source == ATTR_MC_LINK + and self.source_id == ATTR_MC_LINK ) or ( self.ip_address == group_server.ip_address - and self.source == ATTR_MAIN_SYNC + and self.source_id == ATTR_MAIN_SYNC ) ) @@ -859,8 +861,12 @@ async def async_client_leave_group(self, force=False): """ _LOGGER.debug("%s client leave called", self.entity_id) if not force and ( - self.source == ATTR_MAIN_SYNC - or [entity for entity in self.other_zones if entity.source == ATTR_MC_LINK] + self.source_id == ATTR_MAIN_SYNC + or [ + entity + for entity in self.other_zones + if entity.source_id == ATTR_MC_LINK + ] ): await self.coordinator.musiccast.zone_unjoin(self._zone_id) else: From d969b89a12ac4492de165374414fa20434768d77 Mon Sep 17 00:00:00 2001 From: Allen Porter <allen@thebends.org> Date: Thu, 6 Jul 2023 01:26:10 -0700 Subject: [PATCH 11/18] Bump pyrainbird to 2.1.0 (#95968) --- homeassistant/components/rainbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 2216d060f29fc0..a44cfb3ce138ff 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/rainbird", "iot_class": "local_polling", "loggers": ["pyrainbird"], - "requirements": ["pyrainbird==2.0.0"] + "requirements": ["pyrainbird==2.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index c6294603728966..2edbba41b01089 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1938,7 +1938,7 @@ pyqwikswitch==0.93 pyrail==0.0.3 # homeassistant.components.rainbird -pyrainbird==2.0.0 +pyrainbird==2.1.0 # homeassistant.components.recswitch pyrecswitch==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c28835fee5530e..057a0e1d77dbaf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1439,7 +1439,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.rainbird -pyrainbird==2.0.0 +pyrainbird==2.1.0 # homeassistant.components.risco pyrisco==0.5.7 From 866e130967414308a063d3de53385e137242151f Mon Sep 17 00:00:00 2001 From: Erik Montnemery <erik@montnemery.com> Date: Thu, 6 Jul 2023 09:02:32 +0200 Subject: [PATCH 12/18] Add missing qnap translation (#95969) --- homeassistant/components/qnap/strings.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/qnap/strings.json b/homeassistant/components/qnap/strings.json index 26ca5dedd34422..36946b81c0ca18 100644 --- a/homeassistant/components/qnap/strings.json +++ b/homeassistant/components/qnap/strings.json @@ -19,5 +19,11 @@ "invalid_auth": "Bad authentication", "unknown": "Unknown error" } + }, + "issues": { + "deprecated_yaml": { + "title": "The QNAP YAML configuration is being removed", + "description": "Configuring QNAP using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the QNAP YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } From 3540c78fb98c65158be6a06b81edc9fa32454058 Mon Sep 17 00:00:00 2001 From: Bram Kragten <mail@bramkragten.nl> Date: Thu, 6 Jul 2023 16:24:34 +0200 Subject: [PATCH 13/18] Set correct `response` value in service description when `async_set_service_schema` is used (#95985) * Mark scripts as response optional, make it always return a response if return_response is set * Update test_init.py * Revert "Update test_init.py" This reverts commit 8e113e54dbf183db06e1d1f0fea95d6bc59e4e80. * Split + add test --- homeassistant/helpers/service.py | 7 +++++++ tests/helpers/test_service.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 09c861421b019e..1164c2d80155fd 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -683,6 +683,13 @@ def async_set_service_schema( if "target" in schema: description["target"] = schema["target"] + if ( + response := hass.services.supports_response(domain, service) + ) != SupportsResponse.NONE: + description["response"] = { + "optional": response == SupportsResponse.OPTIONAL, + } + hass.data.pop(ALL_SERVICE_DESCRIPTIONS_CACHE, None) descriptions_cache[(domain, service)] = description diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index b062a3233055f2..6adec334bb0aeb 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -589,6 +589,19 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: None, SupportsResponse.ONLY, ) + hass.services.async_register( + logger.DOMAIN, + "another_service_with_response", + lambda x: None, + None, + SupportsResponse.OPTIONAL, + ) + service.async_set_service_schema( + hass, + logger.DOMAIN, + "another_service_with_response", + {"description": "response service"}, + ) descriptions = await service.async_get_all_descriptions(hass) assert "another_new_service" in descriptions[logger.DOMAIN] @@ -600,6 +613,10 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: assert descriptions[logger.DOMAIN]["service_with_only_response"]["response"] == { "optional": False } + assert "another_service_with_response" in descriptions[logger.DOMAIN] + assert descriptions[logger.DOMAIN]["another_service_with_response"]["response"] == { + "optional": True + } # Verify the cache returns the same object assert await service.async_get_all_descriptions(hass) is descriptions From 10b97a77c6122cd95d4476c5a9b6500798de46b7 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke <jan-philipp@bnck.me> Date: Thu, 6 Jul 2023 13:25:34 +0200 Subject: [PATCH 14/18] Explicitly use device name as entity name for Xiaomi fan and humidifier (#95986) --- homeassistant/components/xiaomi_miio/fan.py | 2 ++ homeassistant/components/xiaomi_miio/humidifier.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 247b91d1b06401..a3bb28e7a8b7a1 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -292,6 +292,8 @@ async def async_service_handler(service: ServiceCall) -> None: class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): """Representation of a generic Xiaomi device.""" + _attr_name = None + def __init__(self, device, entry, unique_id, coordinator): """Initialize the generic Xiaomi device.""" super().__init__(device, entry, unique_id, coordinator) diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index 82ede87848e7eb..0438b606efd9c1 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -118,6 +118,7 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): _attr_device_class = HumidifierDeviceClass.HUMIDIFIER _attr_supported_features = HumidifierEntityFeature.MODES + _attr_name = None def __init__(self, device, entry, unique_id, coordinator): """Initialize the generic Xiaomi device.""" From 7408fa4ab6c89fd998d22023a5724fd8a329f598 Mon Sep 17 00:00:00 2001 From: Bram Kragten <mail@bramkragten.nl> Date: Thu, 6 Jul 2023 16:48:03 +0200 Subject: [PATCH 15/18] Make script services always respond when asked (#95991) * Make script services always respond when asked * Update test_init.py --- homeassistant/components/script/__init__.py | 2 +- tests/components/script/test_init.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index f8d41db0e11616..8530aa3b04c143 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -608,7 +608,7 @@ async def _service_handler(self, service: ServiceCall) -> ServiceResponse: variables=service.data, context=service.context, wait=True ) if service.return_response: - return response + return response or {} return None async def async_added_to_hass(self) -> None: diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 199c3e08942daf..cc41b6c404cd24 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -26,7 +26,7 @@ callback, split_entity_id, ) -from homeassistant.exceptions import HomeAssistantError, ServiceNotFound +from homeassistant.exceptions import ServiceNotFound from homeassistant.helpers import entity_registry as er, template from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import ( @@ -1625,7 +1625,7 @@ async def test_responses(hass: HomeAssistant, response: Any) -> None: ) -async def test_responses_error(hass: HomeAssistant) -> None: +async def test_responses_no_response(hass: HomeAssistant) -> None: """Test response variable not set.""" mock_restore_cache(hass, ()) assert await async_setup_component( @@ -1645,10 +1645,13 @@ async def test_responses_error(hass: HomeAssistant) -> None: }, ) - with pytest.raises(HomeAssistantError): - assert await hass.services.async_call( + # Validate we can call it with return_response + assert ( + await hass.services.async_call( DOMAIN, "test", {"greeting": "world"}, blocking=True, return_response=True ) + == {} + ) # Validate we can also call it without return_response assert ( await hass.services.async_call( From ef31608ce0a2dd17184d0f9b05fa8b7b56d5bf77 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt <m.vanderveldt@outlook.com> Date: Thu, 6 Jul 2023 16:28:20 +0200 Subject: [PATCH 16/18] Fix state of slimproto players (#96000) --- homeassistant/components/slimproto/media_player.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/slimproto/media_player.py b/homeassistant/components/slimproto/media_player.py index 641d3b8ae4d9f7..c7c6585e0023d5 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -27,8 +27,10 @@ from .const import DEFAULT_NAME, DOMAIN, PLAYER_EVENT STATE_MAPPING = { - PlayerState.IDLE: MediaPlayerState.IDLE, + PlayerState.STOPPED: MediaPlayerState.IDLE, PlayerState.PLAYING: MediaPlayerState.PLAYING, + PlayerState.BUFFER_READY: MediaPlayerState.PLAYING, + PlayerState.BUFFERING: MediaPlayerState.PLAYING, PlayerState.PAUSED: MediaPlayerState.PAUSED, } From 4096614ac0838b1bfc558e0d6b9fc25aed50d01e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen <balloob@gmail.com> Date: Thu, 6 Jul 2023 11:52:01 -0400 Subject: [PATCH 17/18] Bumped version to 2023.7.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 7b85163fba7266..cc04180a618556 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 7 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index cd0e6eb47f0460..2de9c9de5d11f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.7.0" +version = "2023.7.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From d18716e5f883de90ebdd8ac762d0ebc3f3707195 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen <balloob@gmail.com> Date: Thu, 6 Jul 2023 13:53:56 -0400 Subject: [PATCH 18/18] Disable test case for entity name (#96012) --- tests/helpers/test_entity.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 7de6f70e793698..60d47ca9a44b58 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -1002,7 +1002,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): (True, "Entity Blu", "Device Bla", "Device Bla Entity Blu", False), (True, None, "Device Bla", "Device Bla", False), (True, "Entity Blu", UNDEFINED, "Entity Blu", False), - (True, "Entity Blu", None, "Mock Title Entity Blu", False), + # Not valid on RC + # (True, "Entity Blu", None, "Mock Title Entity Blu", False), ), ) async def test_friendly_name_attr(