From 28a5e4735f24b34f197d153fd8b520e9915dcef4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 28 Jun 2023 20:45:45 +0200 Subject: [PATCH 001/184] Bumped version to 2023.7.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 4bc5e189cf2..1c12103be19 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 7 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 9c67835c544..6acf9d4ea94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.7.0.dev0" +version = "2023.7.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 1687ff1f28676e05d498031191074e804ed5be44 Mon Sep 17 00:00:00 2001 From: Barry Williams Date: Wed, 28 Jun 2023 21:06:24 +0100 Subject: [PATCH 002/184] Add Update Entity for Linn devices (#95217) * added update entity for Linn devices * Update homeassistant/components/openhome/update.py Co-authored-by: Paulus Schoutsen * use parent methods for version attributes * fixed issue with mocking openhome device * Update homeassistant/components/openhome/update.py Co-authored-by: Paulus Schoutsen * update entity name in tests --------- Co-authored-by: Paulus Schoutsen --- homeassistant/components/openhome/__init__.py | 2 +- .../components/openhome/manifest.json | 2 +- .../components/openhome/media_player.py | 6 +- homeassistant/components/openhome/update.py | 102 +++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/openhome/test_update.py | 172 ++++++++++++++++++ 7 files changed, 281 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/openhome/update.py create mode 100644 tests/components/openhome/test_update.py diff --git a/homeassistant/components/openhome/__init__.py b/homeassistant/components/openhome/__init__.py index d201646e81c..c7ee5a7d00c 100644 --- a/homeassistant/components/openhome/__init__.py +++ b/homeassistant/components/openhome/__init__.py @@ -17,7 +17,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.MEDIA_PLAYER] +PLATFORMS = [Platform.MEDIA_PLAYER, Platform.UPDATE] CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) diff --git a/homeassistant/components/openhome/manifest.json b/homeassistant/components/openhome/manifest.json index 61d425895bf..de6c56a01dd 100644 --- a/homeassistant/components/openhome/manifest.json +++ b/homeassistant/components/openhome/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/openhome", "iot_class": "local_polling", "loggers": ["async_upnp_client", "openhomedevice"], - "requirements": ["openhomedevice==2.0.2"], + "requirements": ["openhomedevice==2.2.0"], "ssdp": [ { "st": "urn:av-openhome-org:service:Product:1" diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index c0941906e40..77ab0ac0aaf 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -126,9 +126,9 @@ class OpenhomeDevice(MediaPlayerEntity): identifiers={ (DOMAIN, self._device.uuid()), }, - manufacturer=self._device.device.manufacturer, - model=self._device.device.model_name, - name=self._device.device.friendly_name, + manufacturer=self._device.manufacturer(), + model=self._device.model_name(), + name=self._device.friendly_name(), ) @property diff --git a/homeassistant/components/openhome/update.py b/homeassistant/components/openhome/update.py new file mode 100644 index 00000000000..22bffad44d8 --- /dev/null +++ b/homeassistant/components/openhome/update.py @@ -0,0 +1,102 @@ +"""Update entities for Linn devices.""" +from __future__ import annotations + +import asyncio +import logging +from typing import Any + +import aiohttp +from async_upnp_client.client import UpnpError + +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up update entities for Reolink component.""" + + _LOGGER.debug("Setting up config entry: %s", config_entry.unique_id) + + device = hass.data[DOMAIN][config_entry.entry_id] + + entity = OpenhomeUpdateEntity(device) + + await entity.async_update() + + async_add_entities([entity]) + + +class OpenhomeUpdateEntity(UpdateEntity): + """Update entity for a Linn DS device.""" + + _attr_device_class = UpdateDeviceClass.FIRMWARE + _attr_supported_features = UpdateEntityFeature.INSTALL + _attr_has_entity_name = True + + def __init__(self, device): + """Initialize a Linn DS update entity.""" + self._device = device + self._attr_unique_id = f"{device.uuid()}-update" + + @property + def device_info(self): + """Return a device description for device registry.""" + return DeviceInfo( + identifiers={ + (DOMAIN, self._device.uuid()), + }, + manufacturer=self._device.manufacturer(), + model=self._device.model_name(), + name=self._device.friendly_name(), + ) + + async def async_update(self) -> None: + """Update state of entity.""" + + software_status = await self._device.software_status() + + if not software_status: + self._attr_installed_version = None + self._attr_latest_version = None + self._attr_release_summary = None + self._attr_release_url = None + return + + self._attr_installed_version = software_status["current_software"]["version"] + + if software_status["status"] == "update_available": + self._attr_latest_version = software_status["update_info"]["updates"][0][ + "version" + ] + self._attr_release_summary = software_status["update_info"]["updates"][0][ + "description" + ] + self._attr_release_url = software_status["update_info"]["releasenotesuri"] + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install the latest firmware version.""" + try: + if self.latest_version: + await self._device.update_firmware() + except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError) as err: + raise HomeAssistantError( + f"Error updating {self._device.device.friendly_name}: {err}" + ) from err diff --git a/requirements_all.txt b/requirements_all.txt index 875c138aa12..b3cb0d2a6a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1348,7 +1348,7 @@ openerz-api==0.2.0 openevsewifi==1.1.2 # homeassistant.components.openhome -openhomedevice==2.0.2 +openhomedevice==2.2.0 # homeassistant.components.opensensemap opensensemap-api==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7af3d317830..4d309c5f18b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1026,7 +1026,7 @@ openai==0.27.2 openerz-api==0.2.0 # homeassistant.components.openhome -openhomedevice==2.0.2 +openhomedevice==2.2.0 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/tests/components/openhome/test_update.py b/tests/components/openhome/test_update.py new file mode 100644 index 00000000000..d975cc29af4 --- /dev/null +++ b/tests/components/openhome/test_update.py @@ -0,0 +1,172 @@ +"""Tests for the Openhome update platform.""" +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.components.openhome.const import DOMAIN +from homeassistant.components.update import ( + ATTR_INSTALLED_VERSION, + ATTR_LATEST_VERSION, + ATTR_RELEASE_SUMMARY, + ATTR_RELEASE_URL, + DOMAIN as PLATFORM_DOMAIN, + SERVICE_INSTALL, + UpdateDeviceClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + CONF_HOST, + STATE_ON, + STATE_UNKNOWN, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from tests.common import MockConfigEntry + +LATEST_FIRMWARE_INSTALLED = { + "status": "on_latest", + "current_software": {"version": "4.100.502", "topic": "main", "channel": "release"}, +} + +FIRMWARE_UPDATE_AVAILABLE = { + "status": "update_available", + "current_software": {"version": "4.99.491", "topic": "main", "channel": "release"}, + "update_info": { + "legal": { + "licenseurl": "http://products.linn.co.uk/VersionInfo/licenseV2.txt", + "privacyurl": "https://www.linn.co.uk/privacy", + "privacyuri": "https://products.linn.co.uk/VersionInfo/PrivacyV1.json", + "privacyversion": 1, + }, + "releasenotesuri": "http://docs.linn.co.uk/wiki/index.php/ReleaseNotes", + "updates": [ + { + "channel": "release", + "date": "07 Jun 2023 12:29:48", + "description": "Release build version 4.100.502 (07 Jun 2023 12:29:48)", + "exaktlink": "3", + "manifest": "https://cloud.linn.co.uk/update/components/836/4.100.502/manifest.json", + "topic": "main", + "variant": "836", + "version": "4.100.502", + } + ], + "exaktUpdates": [], + }, +} + + +async def setup_integration( + hass: HomeAssistant, + software_status: dict, + update_firmware: AsyncMock, +) -> None: + """Load an openhome platform with mocked device.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "http://localhost"}, + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.openhome.PLATFORMS", [Platform.UPDATE]), patch( + "homeassistant.components.openhome.Device", MagicMock() + ) as mock_device: + mock_device.return_value.init = AsyncMock() + mock_device.return_value.uuid = MagicMock(return_value="uuid") + mock_device.return_value.manufacturer = MagicMock(return_value="manufacturer") + mock_device.return_value.model_name = MagicMock(return_value="model_name") + mock_device.return_value.friendly_name = MagicMock(return_value="friendly_name") + mock_device.return_value.software_status = AsyncMock( + return_value=software_status + ) + mock_device.return_value.update_firmware = update_firmware + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + +async def test_not_supported(hass: HomeAssistant): + """Ensure update entity works if service not supported.""" + + update_firmware = AsyncMock() + await setup_integration(hass, None, update_firmware) + + state = hass.states.get("update.friendly_name") + + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + assert state.attributes[ATTR_INSTALLED_VERSION] is None + assert state.attributes[ATTR_LATEST_VERSION] is None + assert state.attributes[ATTR_RELEASE_URL] is None + assert state.attributes[ATTR_RELEASE_SUMMARY] is None + update_firmware.assert_not_called() + + +async def test_on_latest_firmware(hass: HomeAssistant): + """Test device on latest firmware.""" + + update_firmware = AsyncMock() + await setup_integration(hass, LATEST_FIRMWARE_INSTALLED, update_firmware) + + state = hass.states.get("update.friendly_name") + + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + assert state.attributes[ATTR_INSTALLED_VERSION] == "4.100.502" + assert state.attributes[ATTR_LATEST_VERSION] is None + assert state.attributes[ATTR_RELEASE_URL] is None + assert state.attributes[ATTR_RELEASE_SUMMARY] is None + update_firmware.assert_not_called() + + +async def test_update_available(hass: HomeAssistant): + """Test device has firmware update available.""" + + update_firmware = AsyncMock() + await setup_integration(hass, FIRMWARE_UPDATE_AVAILABLE, update_firmware) + + state = hass.states.get("update.friendly_name") + + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + assert state.attributes[ATTR_INSTALLED_VERSION] == "4.99.491" + assert state.attributes[ATTR_LATEST_VERSION] == "4.100.502" + assert ( + state.attributes[ATTR_RELEASE_URL] + == "http://docs.linn.co.uk/wiki/index.php/ReleaseNotes" + ) + assert ( + state.attributes[ATTR_RELEASE_SUMMARY] + == "Release build version 4.100.502 (07 Jun 2023 12:29:48)" + ) + + await hass.services.async_call( + PLATFORM_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.friendly_name"}, + blocking=True, + ) + await hass.async_block_till_done() + + update_firmware.assert_called_once() + + +async def test_firmware_update_not_required(hass: HomeAssistant): + """Ensure firmware install does nothing if up to date.""" + + update_firmware = AsyncMock() + await setup_integration(hass, LATEST_FIRMWARE_INSTALLED, update_firmware) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + PLATFORM_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.friendly_name"}, + blocking=True, + ) + update_firmware.assert_not_called() From a39ef03ff5ce51e050928537d311a79da80e7484 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 28 Jun 2023 20:41:11 -0500 Subject: [PATCH 003/184] Disconnect VoIP on RTCP bye message (#95452) * Support RTCP BYE message * Make RtcpState optional --- homeassistant/components/voip/manifest.json | 2 +- homeassistant/components/voip/voip.py | 26 +++++++++++++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/voip/manifest.json b/homeassistant/components/voip/manifest.json index 345480da363..594abc69c13 100644 --- a/homeassistant/components/voip/manifest.json +++ b/homeassistant/components/voip/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/voip", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["voip-utils==0.0.7"] + "requirements": ["voip-utils==0.1.0"] } diff --git a/homeassistant/components/voip/voip.py b/homeassistant/components/voip/voip.py index 32cfbd70337..3d0681a8475 100644 --- a/homeassistant/components/voip/voip.py +++ b/homeassistant/components/voip/voip.py @@ -11,7 +11,13 @@ import time from typing import TYPE_CHECKING import async_timeout -from voip_utils import CallInfo, RtpDatagramProtocol, SdpInfo, VoipDatagramProtocol +from voip_utils import ( + CallInfo, + RtcpState, + RtpDatagramProtocol, + SdpInfo, + VoipDatagramProtocol, +) from homeassistant.components import stt, tts from homeassistant.components.assist_pipeline import ( @@ -46,7 +52,10 @@ _LOGGER = logging.getLogger(__name__) def make_protocol( - hass: HomeAssistant, devices: VoIPDevices, call_info: CallInfo + hass: HomeAssistant, + devices: VoIPDevices, + call_info: CallInfo, + rtcp_state: RtcpState | None = None, ) -> VoipDatagramProtocol: """Plays a pre-recorded message if pipeline is misconfigured.""" voip_device = devices.async_get_or_create(call_info) @@ -70,6 +79,7 @@ def make_protocol( hass, "problem.pcm", opus_payload_type=call_info.opus_payload_type, + rtcp_state=rtcp_state, ) vad_sensitivity = pipeline_select.get_vad_sensitivity( @@ -86,6 +96,7 @@ def make_protocol( Context(user_id=devices.config_entry.data["user"]), opus_payload_type=call_info.opus_payload_type, silence_seconds=VadSensitivity.to_seconds(vad_sensitivity), + rtcp_state=rtcp_state, ) @@ -101,13 +112,14 @@ class HassVoipDatagramProtocol(VoipDatagramProtocol): session_name="voip_hass", version=__version__, ), - valid_protocol_factory=lambda call_info: make_protocol( - hass, devices, call_info + valid_protocol_factory=lambda call_info, rtcp_state: make_protocol( + hass, devices, call_info, rtcp_state ), - invalid_protocol_factory=lambda call_info: PreRecordMessageProtocol( + invalid_protocol_factory=lambda call_info, rtcp_state: PreRecordMessageProtocol( hass, "not_configured.pcm", opus_payload_type=call_info.opus_payload_type, + rtcp_state=rtcp_state, ), ) self.hass = hass @@ -147,6 +159,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): tone_delay: float = 0.2, tts_extra_timeout: float = 1.0, silence_seconds: float = 1.0, + rtcp_state: RtcpState | None = None, ) -> None: """Set up pipeline RTP server.""" super().__init__( @@ -154,6 +167,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): width=WIDTH, channels=CHANNELS, opus_payload_type=opus_payload_type, + rtcp_state=rtcp_state, ) self.hass = hass @@ -454,6 +468,7 @@ class PreRecordMessageProtocol(RtpDatagramProtocol): opus_payload_type: int, message_delay: float = 1.0, loop_delay: float = 2.0, + rtcp_state: RtcpState | None = None, ) -> None: """Set up RTP server.""" super().__init__( @@ -461,6 +476,7 @@ class PreRecordMessageProtocol(RtpDatagramProtocol): width=WIDTH, channels=CHANNELS, opus_payload_type=opus_payload_type, + rtcp_state=rtcp_state, ) self.hass = hass self.file_name = file_name diff --git a/requirements_all.txt b/requirements_all.txt index b3cb0d2a6a3..3975dc07513 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2614,7 +2614,7 @@ venstarcolortouch==0.19 vilfo-api-client==0.4.1 # homeassistant.components.voip -voip-utils==0.0.7 +voip-utils==0.1.0 # homeassistant.components.volkszaehler volkszaehler==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d309c5f18b..45f9223ad68 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1914,7 +1914,7 @@ venstarcolortouch==0.19 vilfo-api-client==0.4.1 # homeassistant.components.voip -voip-utils==0.0.7 +voip-utils==0.1.0 # homeassistant.components.volvooncall volvooncall==0.10.3 From be22195cf4e404df05a2f512c7245c051d5b62d3 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 29 Jun 2023 03:38:06 +0200 Subject: [PATCH 004/184] Add conversation agent selector, use in `conversation.process` service (#95462) --- .../components/conversation/services.yaml | 2 +- homeassistant/helpers/selector.py | 28 +++++++++++++++++++ tests/helpers/test_selector.py | 22 +++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/conversation/services.yaml b/homeassistant/components/conversation/services.yaml index 6b031ff7142..8ac929e13b6 100644 --- a/homeassistant/components/conversation/services.yaml +++ b/homeassistant/components/conversation/services.yaml @@ -20,4 +20,4 @@ process: description: Assist engine to process your request example: homeassistant selector: - text: + conversation_agent: diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index afd38bf7636..84c0f769c7c 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -500,6 +500,34 @@ class ConstantSelector(Selector[ConstantSelectorConfig]): return self.config["value"] +class ConversationAgentSelectorConfig(TypedDict, total=False): + """Class to represent a conversation agent selector config.""" + + language: str + + +@SELECTORS.register("conversation_agent") +class COnversationAgentSelector(Selector[ConversationAgentSelectorConfig]): + """Selector for a conversation agent.""" + + selector_type = "conversation_agent" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("language"): str, + } + ) + + def __init__(self, config: ConversationAgentSelectorConfig) -> None: + """Instantiate a selector.""" + super().__init__(config) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + agent: str = vol.Schema(str)(data) + return agent + + class DateSelectorConfig(TypedDict): """Class to represent a date selector config.""" diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index f95da5c6e66..c518ad227a7 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -979,3 +979,25 @@ def test_constant_selector_schema_error(schema) -> None: """Test constant selector.""" with pytest.raises(vol.Invalid): selector.validate_selector({"constant": schema}) + + +@pytest.mark.parametrize( + ("schema", "valid_selections", "invalid_selections"), + ( + ( + {}, + ("home_assistant", "2j4hp3uy4p87wyrpiuhk34"), + (None, True, 1), + ), + ( + {"language": "nl"}, + ("home_assistant", "2j4hp3uy4p87wyrpiuhk34"), + (None, True, 1), + ), + ), +) +def test_conversation_agent_selector_schema( + schema, valid_selections, invalid_selections +) -> None: + """Test conversation agent selector.""" + _test_selector("conversation_agent", schema, valid_selections, invalid_selections) From a23c0e12f191119d6536746d4e6be5bf1ce2c6b5 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 28 Jun 2023 23:40:12 +0200 Subject: [PATCH 005/184] Fix Matter entity names (#95477) --- homeassistant/components/matter/binary_sensor.py | 4 ---- homeassistant/components/matter/cover.py | 12 ++++++++---- homeassistant/components/matter/light.py | 16 +++++++++++----- homeassistant/components/matter/lock.py | 2 +- homeassistant/components/matter/sensor.py | 8 +------- homeassistant/components/matter/strings.json | 7 +++++++ homeassistant/components/matter/switch.py | 2 +- tests/components/matter/test_binary_sensor.py | 4 ++-- 8 files changed, 31 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/matter/binary_sensor.py b/homeassistant/components/matter/binary_sensor.py index bd65b3a0925..7c94c07c8cd 100644 --- a/homeassistant/components/matter/binary_sensor.py +++ b/homeassistant/components/matter/binary_sensor.py @@ -65,7 +65,6 @@ DISCOVERY_SCHEMAS = [ entity_description=MatterBinarySensorEntityDescription( key="HueMotionSensor", device_class=BinarySensorDeviceClass.MOTION, - name="Motion", measurement_to_ha=lambda x: (x & 1 == 1) if x is not None else None, ), entity_class=MatterBinarySensor, @@ -78,7 +77,6 @@ DISCOVERY_SCHEMAS = [ entity_description=MatterBinarySensorEntityDescription( key="ContactSensor", device_class=BinarySensorDeviceClass.DOOR, - name="Contact", # value is inverted on matter to what we expect measurement_to_ha=lambda x: not x, ), @@ -90,7 +88,6 @@ DISCOVERY_SCHEMAS = [ entity_description=MatterBinarySensorEntityDescription( key="OccupancySensor", device_class=BinarySensorDeviceClass.OCCUPANCY, - name="Occupancy", # The first bit = if occupied measurement_to_ha=lambda x: (x & 1 == 1) if x is not None else None, ), @@ -102,7 +99,6 @@ DISCOVERY_SCHEMAS = [ entity_description=MatterBinarySensorEntityDescription( key="BatteryChargeLevel", device_class=BinarySensorDeviceClass.BATTERY, - name="Battery Status", measurement_to_ha=lambda x: x != clusters.PowerSource.Enums.BatChargeLevelEnum.kOk, ), diff --git a/homeassistant/components/matter/cover.py b/homeassistant/components/matter/cover.py index 61c5d4cd2ff..590f325cf22 100644 --- a/homeassistant/components/matter/cover.py +++ b/homeassistant/components/matter/cover.py @@ -199,7 +199,7 @@ class MatterCover(MatterEntity, CoverEntity): DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.COVER, - entity_description=CoverEntityDescription(key="MatterCover"), + entity_description=CoverEntityDescription(key="MatterCover", name=None), entity_class=MatterCover, required_attributes=( clusters.WindowCovering.Attributes.OperationalStatus, @@ -212,7 +212,9 @@ DISCOVERY_SCHEMAS = [ ), MatterDiscoverySchema( platform=Platform.COVER, - entity_description=CoverEntityDescription(key="MatterCoverPositionAwareLift"), + entity_description=CoverEntityDescription( + key="MatterCoverPositionAwareLift", name=None + ), entity_class=MatterCover, required_attributes=( clusters.WindowCovering.Attributes.OperationalStatus, @@ -225,7 +227,9 @@ DISCOVERY_SCHEMAS = [ ), MatterDiscoverySchema( platform=Platform.COVER, - entity_description=CoverEntityDescription(key="MatterCoverPositionAwareTilt"), + entity_description=CoverEntityDescription( + key="MatterCoverPositionAwareTilt", name=None + ), entity_class=MatterCover, required_attributes=( clusters.WindowCovering.Attributes.OperationalStatus, @@ -239,7 +243,7 @@ DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.COVER, entity_description=CoverEntityDescription( - key="MatterCoverPositionAwareLiftAndTilt" + key="MatterCoverPositionAwareLiftAndTilt", name=None ), entity_class=MatterCover, required_attributes=( diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 4c220ab85ea..facdb6752d3 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -358,7 +358,7 @@ class MatterLight(MatterEntity, LightEntity): DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.LIGHT, - entity_description=LightEntityDescription(key="MatterLight"), + entity_description=LightEntityDescription(key="MatterLight", name=None), entity_class=MatterLight, required_attributes=(clusters.OnOff.Attributes.OnOff,), optional_attributes=( @@ -380,7 +380,9 @@ DISCOVERY_SCHEMAS = [ # Additional schema to match (HS Color) lights with incorrect/missing device type MatterDiscoverySchema( platform=Platform.LIGHT, - entity_description=LightEntityDescription(key="MatterHSColorLightFallback"), + entity_description=LightEntityDescription( + key="MatterHSColorLightFallback", name=None + ), entity_class=MatterLight, required_attributes=( clusters.OnOff.Attributes.OnOff, @@ -398,7 +400,9 @@ DISCOVERY_SCHEMAS = [ # Additional schema to match (XY Color) lights with incorrect/missing device type MatterDiscoverySchema( platform=Platform.LIGHT, - entity_description=LightEntityDescription(key="MatterXYColorLightFallback"), + entity_description=LightEntityDescription( + key="MatterXYColorLightFallback", name=None + ), entity_class=MatterLight, required_attributes=( clusters.OnOff.Attributes.OnOff, @@ -417,7 +421,7 @@ DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.LIGHT, entity_description=LightEntityDescription( - key="MatterColorTemperatureLightFallback" + key="MatterColorTemperatureLightFallback", name=None ), entity_class=MatterLight, required_attributes=( @@ -430,7 +434,9 @@ DISCOVERY_SCHEMAS = [ # Additional schema to match generic dimmable lights with incorrect/missing device type MatterDiscoverySchema( platform=Platform.LIGHT, - entity_description=LightEntityDescription(key="MatterDimmableLightFallback"), + entity_description=LightEntityDescription( + key="MatterDimmableLightFallback", name=None + ), entity_class=MatterLight, required_attributes=( clusters.OnOff.Attributes.OnOff, diff --git a/homeassistant/components/matter/lock.py b/homeassistant/components/matter/lock.py index f78529b7268..7df6d84c794 100644 --- a/homeassistant/components/matter/lock.py +++ b/homeassistant/components/matter/lock.py @@ -147,7 +147,7 @@ class DoorLockFeature(IntFlag): DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.LOCK, - entity_description=LockEntityDescription(key="MatterLock"), + entity_description=LockEntityDescription(key="MatterLock", name=None), entity_class=MatterLock, required_attributes=(clusters.DoorLock.Attributes.LockState,), optional_attributes=(clusters.DoorLock.Attributes.DoorState,), diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index 84e68695d63..027dcda65a7 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -68,7 +68,6 @@ DISCOVERY_SCHEMAS = [ platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( key="TemperatureSensor", - name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, measurement_to_ha=lambda x: x / 100, @@ -80,7 +79,6 @@ DISCOVERY_SCHEMAS = [ platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( key="PressureSensor", - name="Pressure", native_unit_of_measurement=UnitOfPressure.KPA, device_class=SensorDeviceClass.PRESSURE, measurement_to_ha=lambda x: x / 10, @@ -92,9 +90,8 @@ DISCOVERY_SCHEMAS = [ platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( key="FlowSensor", - name="Flow", native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, - device_class=SensorDeviceClass.WATER, # what is the device class here ? + translation_key="flow", measurement_to_ha=lambda x: x / 10, ), entity_class=MatterSensor, @@ -104,7 +101,6 @@ DISCOVERY_SCHEMAS = [ platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( key="HumiditySensor", - name="Humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, measurement_to_ha=lambda x: x / 100, @@ -118,7 +114,6 @@ DISCOVERY_SCHEMAS = [ platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( key="LightSensor", - name="Illuminance", native_unit_of_measurement=LIGHT_LUX, device_class=SensorDeviceClass.ILLUMINANCE, measurement_to_ha=lambda x: round(pow(10, ((x - 1) / 10000)), 1), @@ -130,7 +125,6 @@ DISCOVERY_SCHEMAS = [ platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( key="PowerSource", - name="Battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, # value has double precision diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index 594998c236f..dc5eb30df51 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -43,5 +43,12 @@ "install_addon": "Please wait while the Matter Server add-on installation finishes. This can take several minutes.", "start_addon": "Please wait while the Matter Server add-on starts. This add-on is what powers Matter in Home Assistant. This may take some seconds." } + }, + "entity": { + "sensor": { + "flow": { + "name": "Flow" + } + } } } diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index 2eb3c22c1f7..56c51d144d8 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -63,7 +63,7 @@ DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.SWITCH, entity_description=SwitchEntityDescription( - key="MatterPlug", device_class=SwitchDeviceClass.OUTLET + key="MatterPlug", device_class=SwitchDeviceClass.OUTLET, name=None ), entity_class=MatterSwitch, required_attributes=(clusters.OnOff.Attributes.OnOff,), diff --git a/tests/components/matter/test_binary_sensor.py b/tests/components/matter/test_binary_sensor.py index 743619ddde9..d7982e1d5ae 100644 --- a/tests/components/matter/test_binary_sensor.py +++ b/tests/components/matter/test_binary_sensor.py @@ -31,7 +31,7 @@ async def test_contact_sensor( contact_sensor_node: MatterNode, ) -> None: """Test contact sensor.""" - state = hass.states.get("binary_sensor.mock_contact_sensor_contact") + state = hass.states.get("binary_sensor.mock_contact_sensor_door") assert state assert state.state == "off" @@ -40,7 +40,7 @@ async def test_contact_sensor( hass, matter_client, data=(contact_sensor_node.node_id, "1/69/0", False) ) - state = hass.states.get("binary_sensor.mock_contact_sensor_contact") + state = hass.states.get("binary_sensor.mock_contact_sensor_door") assert state assert state.state == "on" From 4e940df224581b190d14ec60282e466e131818aa Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 28 Jun 2023 18:35:05 -0400 Subject: [PATCH 006/184] Bump ZHA dependencies (#95478) * Bump ZHA dependencies * Account for new EZSP metadata keys --- homeassistant/components/zha/manifest.json | 8 ++++---- homeassistant/components/zha/radio_manager.py | 7 ++++--- requirements_all.txt | 8 ++++---- requirements_test_all.txt | 8 ++++---- tests/components/zha/test_config_flow.py | 7 +++++++ 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index ae5718e108b..fa1c382926e 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -20,15 +20,15 @@ "zigpy_znp" ], "requirements": [ - "bellows==0.35.5", + "bellows==0.35.6", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.101", "zigpy-deconz==0.21.0", - "zigpy==0.56.0", - "zigpy-xbee==0.18.0", + "zigpy==0.56.1", + "zigpy-xbee==0.18.1", "zigpy-zigate==0.11.0", - "zigpy-znp==0.11.1" + "zigpy-znp==0.11.2" ], "usb": [ { diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index 9fbfa03b928..29214083d27 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -250,11 +250,12 @@ class ZhaRadioManager: assert self.current_settings is not None + metadata = self.current_settings.network_info.metadata["ezsp"] + if ( self.current_settings.node_info.ieee == self.chosen_backup.node_info.ieee - or not self.current_settings.network_info.metadata["ezsp"][ - "can_write_custom_eui64" - ] + or metadata["can_rewrite_custom_eui64"] + or not metadata["can_burn_userdata_custom_eui64"] ): # No point in prompting the user if the backup doesn't have a new IEEE # address or if there is no way to overwrite the IEEE address a second time diff --git a/requirements_all.txt b/requirements_all.txt index 3975dc07513..b0d6425d442 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -497,7 +497,7 @@ beautifulsoup4==4.11.1 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.35.5 +bellows==0.35.6 # homeassistant.components.bmw_connected_drive bimmer-connected==0.13.7 @@ -2753,16 +2753,16 @@ ziggo-mediabox-xl==1.1.0 zigpy-deconz==0.21.0 # homeassistant.components.zha -zigpy-xbee==0.18.0 +zigpy-xbee==0.18.1 # homeassistant.components.zha zigpy-zigate==0.11.0 # homeassistant.components.zha -zigpy-znp==0.11.1 +zigpy-znp==0.11.2 # homeassistant.components.zha -zigpy==0.56.0 +zigpy==0.56.1 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45f9223ad68..51f614a685f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -418,7 +418,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.35.5 +bellows==0.35.6 # homeassistant.components.bmw_connected_drive bimmer-connected==0.13.7 @@ -2017,16 +2017,16 @@ zha-quirks==0.0.101 zigpy-deconz==0.21.0 # homeassistant.components.zha -zigpy-xbee==0.18.0 +zigpy-xbee==0.18.1 # homeassistant.components.zha zigpy-zigate==0.11.0 # homeassistant.components.zha -zigpy-znp==0.11.1 +zigpy-znp==0.11.2 # homeassistant.components.zha -zigpy==0.56.0 +zigpy==0.56.1 # homeassistant.components.zwave_js zwave-js-server-python==0.49.0 diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 48c45dd241d..17665994806 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -74,6 +74,12 @@ def mock_app(): mock_app = AsyncMock() mock_app.backups = create_autospec(BackupManager, instance=True) mock_app.backups.backups = [] + mock_app.state.network_info.metadata = { + "ezsp": { + "can_burn_userdata_custom_eui64": True, + "can_rewrite_custom_eui64": False, + } + } with patch( "zigpy.application.ControllerApplication.new", AsyncMock(return_value=mock_app) @@ -1517,6 +1523,7 @@ async def test_ezsp_restore_without_settings_change_ieee( mock_app.state.node_info = backup.node_info mock_app.state.network_info = copy.deepcopy(backup.network_info) mock_app.state.network_info.network_key.tx_counter += 10000 + mock_app.state.network_info.metadata["ezsp"] = {} # Include the overwrite option, just in case someone uploads a backup with it backup.network_info.metadata["ezsp"] = {EZSP_OVERWRITE_EUI64: True} From 17c64ed791fc2baf9ea25f77d330c05683c16e4a Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 28 Jun 2023 17:34:43 -0500 Subject: [PATCH 007/184] Add targeted entities to sentence debug API (#95480) * Return targets with debug sentence API * Update test * Update homeassistant/components/conversation/__init__.py * Include area/domain in test sentences --------- Co-authored-by: Paulus Schoutsen --- .../components/conversation/__init__.py | 48 +++++++++++++++ .../conversation/snapshots/test_init.ambr | 59 +++++++++++++++++++ tests/components/conversation/test_init.py | 10 +++- 3 files changed, 116 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index f704a8baa33..5b82b5dae72 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -8,6 +8,7 @@ import logging import re from typing import Any, Literal +from hassil.recognize import RecognizeResult import voluptuous as vol from homeassistant import core @@ -353,6 +354,10 @@ async def websocket_hass_agent_debug( } for entity_key, entity in result.entities.items() }, + "targets": { + state.entity_id: {"matched": is_matched} + for state, is_matched in _get_debug_targets(hass, result) + }, } if result is not None else None @@ -362,6 +367,49 @@ async def websocket_hass_agent_debug( ) +def _get_debug_targets( + hass: HomeAssistant, + result: RecognizeResult, +) -> Iterable[tuple[core.State, bool]]: + """Yield state/is_matched pairs for a hassil recognition.""" + entities = result.entities + + name: str | None = None + area_name: str | None = None + domains: set[str] | None = None + device_classes: set[str] | None = None + state_names: set[str] | None = None + + if "name" in entities: + name = str(entities["name"].value) + + if "area" in entities: + area_name = str(entities["area"].value) + + if "domain" in entities: + domains = set(cv.ensure_list(entities["domain"].value)) + + if "device_class" in entities: + device_classes = set(cv.ensure_list(entities["device_class"].value)) + + if "state" in entities: + # HassGetState only + state_names = set(cv.ensure_list(entities["state"].value)) + + states = intent.async_match_states( + hass, + name=name, + area_name=area_name, + domains=domains, + device_classes=device_classes, + ) + + for state in states: + # For queries, a target is "matched" based on its state + is_matched = (state_names is None) or (state.state in state_names) + yield state, is_matched + + class ConversationProcessView(http.HomeAssistantView): """View to process text.""" diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index 23afe9ce3f7..8ef0cef52f9 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -382,6 +382,11 @@ 'intent': dict({ 'name': 'HassTurnOn', }), + 'targets': dict({ + 'light.kitchen': dict({ + 'matched': True, + }), + }), }), dict({ 'entities': dict({ @@ -394,6 +399,60 @@ 'intent': dict({ 'name': 'HassTurnOff', }), + 'targets': dict({ + 'light.kitchen': dict({ + 'matched': True, + }), + }), + }), + dict({ + 'entities': dict({ + 'area': dict({ + 'name': 'area', + 'text': 'kitchen', + 'value': 'kitchen', + }), + 'domain': dict({ + 'name': 'domain', + 'text': '', + 'value': 'light', + }), + }), + 'intent': dict({ + 'name': 'HassTurnOn', + }), + 'targets': dict({ + 'light.kitchen': dict({ + 'matched': True, + }), + }), + }), + dict({ + 'entities': dict({ + 'area': dict({ + 'name': 'area', + 'text': 'kitchen', + 'value': 'kitchen', + }), + 'domain': dict({ + 'name': 'domain', + 'text': 'lights', + 'value': 'light', + }), + 'state': dict({ + 'name': 'state', + 'text': 'on', + 'value': 'on', + }), + }), + 'intent': dict({ + 'name': 'HassGetState', + }), + 'targets': dict({ + 'light.kitchen': dict({ + 'matched': False, + }), + }), }), None, ]), diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index ec2128e3bd7..6ad9beb3362 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -1652,16 +1652,22 @@ async def test_ws_hass_agent_debug( hass: HomeAssistant, init_components, hass_ws_client: WebSocketGenerator, + area_registry: ar.AreaRegistry, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test homeassistant agent debug websocket command.""" client = await hass_ws_client(hass) + kitchen_area = area_registry.async_create("kitchen") entity_registry.async_get_or_create( "light", "demo", "1234", suggested_object_id="kitchen" ) - entity_registry.async_update_entity("light.kitchen", aliases={"my cool light"}) + entity_registry.async_update_entity( + "light.kitchen", + aliases={"my cool light"}, + area_id=kitchen_area.id, + ) hass.states.async_set("light.kitchen", "off") on_calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on") @@ -1673,6 +1679,8 @@ async def test_ws_hass_agent_debug( "sentences": [ "turn on my cool light", "turn my cool light off", + "turn on all lights in the kitchen", + "how many lights are on in the kitchen?", "this will not match anything", # null in results ], } From 71b192c072ba94b0c4732e8420caebd0c3c3d243 Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Thu, 29 Jun 2023 04:29:54 -0500 Subject: [PATCH 008/184] Update matter locks to support pin code validation (#95481) Update matter locks to support PINCode validation based on device attributes --- homeassistant/components/matter/lock.py | 20 ++++++++++ tests/components/matter/test_door_lock.py | 45 ++++++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/matter/lock.py b/homeassistant/components/matter/lock.py index 7df6d84c794..a5f625f9e73 100644 --- a/homeassistant/components/matter/lock.py +++ b/homeassistant/components/matter/lock.py @@ -33,6 +33,26 @@ class MatterLock(MatterEntity, LockEntity): features: int | None = None + @property + def code_format(self) -> str | None: + """Regex for code format or None if no code is required.""" + if self.get_matter_attribute_value( + clusters.DoorLock.Attributes.RequirePINforRemoteOperation + ): + min_pincode_length = int( + self.get_matter_attribute_value( + clusters.DoorLock.Attributes.MinPINCodeLength + ) + ) + max_pincode_length = int( + self.get_matter_attribute_value( + clusters.DoorLock.Attributes.MaxPINCodeLength + ) + ) + return f"^\\d{{{min_pincode_length},{max_pincode_length}}}$" + + return None + @property def supports_door_position_sensor(self) -> bool: """Return True if the lock supports door position sensor.""" diff --git a/tests/components/matter/test_door_lock.py b/tests/components/matter/test_door_lock.py index 072658044d8..003bfa3cf39 100644 --- a/tests/components/matter/test_door_lock.py +++ b/tests/components/matter/test_door_lock.py @@ -11,7 +11,7 @@ from homeassistant.components.lock import ( STATE_UNLOCKED, STATE_UNLOCKING, ) -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import ATTR_CODE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from .common import ( @@ -104,3 +104,46 @@ async def test_lock( state = hass.states.get("lock.mock_door_lock") assert state assert state.state == STATE_UNKNOWN + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_lock_requires_pin( + hass: HomeAssistant, + matter_client: MagicMock, + door_lock: MatterNode, +) -> None: + """Test door lock with PINCode.""" + + code = "1234567" + + # set RequirePINforRemoteOperation + set_node_attribute(door_lock, 1, 257, 51, True) + # set door state to unlocked + set_node_attribute(door_lock, 1, 257, 0, 2) + + with pytest.raises(ValueError): + # Lock door using invalid code format + await trigger_subscription_callback(hass, matter_client) + await hass.services.async_call( + "lock", + "lock", + {"entity_id": "lock.mock_door_lock", ATTR_CODE: "1234"}, + blocking=True, + ) + + # Lock door using valid code + await trigger_subscription_callback(hass, matter_client) + await hass.services.async_call( + "lock", + "lock", + {"entity_id": "lock.mock_door_lock", ATTR_CODE: code}, + blocking=True, + ) + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=door_lock.node_id, + endpoint_id=1, + command=clusters.DoorLock.Commands.LockDoor(code.encode()), + timed_request_timeout_ms=1000, + ) From 1f72a5b1fe520c60555756ec39208e81c9c74955 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Wed, 28 Jun 2023 21:46:08 -0400 Subject: [PATCH 009/184] Remove incompatible button entities for Mazda electric vehicles (#95486) * Remove incompatible button entities for Mazda electric vehicles * Update tests --- homeassistant/components/mazda/button.py | 4 ++ tests/components/mazda/test_button.py | 75 +++++++++++------------- 2 files changed, 37 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/mazda/button.py b/homeassistant/components/mazda/button.py index 99a1a4ac2ff..1b1e51db035 100644 --- a/homeassistant/components/mazda/button.py +++ b/homeassistant/components/mazda/button.py @@ -78,21 +78,25 @@ BUTTON_ENTITIES = [ key="start_engine", name="Start engine", icon="mdi:engine", + is_supported=lambda data: not data["isElectric"], ), MazdaButtonEntityDescription( key="stop_engine", name="Stop engine", icon="mdi:engine-off", + is_supported=lambda data: not data["isElectric"], ), MazdaButtonEntityDescription( key="turn_on_hazard_lights", name="Turn on hazard lights", icon="mdi:hazard-lights", + is_supported=lambda data: not data["isElectric"], ), MazdaButtonEntityDescription( key="turn_off_hazard_lights", name="Turn off hazard lights", icon="mdi:hazard-lights", + is_supported=lambda data: not data["isElectric"], ), MazdaButtonEntityDescription( key="refresh_vehicle_status", diff --git a/tests/components/mazda/test_button.py b/tests/components/mazda/test_button.py index 535ddcc963b..ba80c10b38d 100644 --- a/tests/components/mazda/test_button.py +++ b/tests/components/mazda/test_button.py @@ -65,40 +65,6 @@ async def test_button_setup_electric_vehicle(hass: HomeAssistant) -> None: entity_registry = er.async_get(hass) - entry = entity_registry.async_get("button.my_mazda3_start_engine") - assert entry - assert entry.unique_id == "JM000000000000000_start_engine" - state = hass.states.get("button.my_mazda3_start_engine") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start engine" - assert state.attributes.get(ATTR_ICON) == "mdi:engine" - - entry = entity_registry.async_get("button.my_mazda3_stop_engine") - assert entry - assert entry.unique_id == "JM000000000000000_stop_engine" - state = hass.states.get("button.my_mazda3_stop_engine") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop engine" - assert state.attributes.get(ATTR_ICON) == "mdi:engine-off" - - entry = entity_registry.async_get("button.my_mazda3_turn_on_hazard_lights") - assert entry - assert entry.unique_id == "JM000000000000000_turn_on_hazard_lights" - state = hass.states.get("button.my_mazda3_turn_on_hazard_lights") - assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn on hazard lights" - assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" - - entry = entity_registry.async_get("button.my_mazda3_turn_off_hazard_lights") - assert entry - assert entry.unique_id == "JM000000000000000_turn_off_hazard_lights" - state = hass.states.get("button.my_mazda3_turn_off_hazard_lights") - assert state - assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn off hazard lights" - ) - assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" - entry = entity_registry.async_get("button.my_mazda3_refresh_status") assert entry assert entry.unique_id == "JM000000000000000_refresh_vehicle_status" @@ -109,20 +75,45 @@ async def test_button_setup_electric_vehicle(hass: HomeAssistant) -> None: @pytest.mark.parametrize( - ("entity_id_suffix", "api_method_name"), + ("electric_vehicle", "entity_id_suffix"), [ - ("start_engine", "start_engine"), - ("stop_engine", "stop_engine"), - ("turn_on_hazard_lights", "turn_on_hazard_lights"), - ("turn_off_hazard_lights", "turn_off_hazard_lights"), - ("refresh_status", "refresh_vehicle_status"), + (True, "start_engine"), + (True, "stop_engine"), + (True, "turn_on_hazard_lights"), + (True, "turn_off_hazard_lights"), + (False, "refresh_status"), + ], +) +async def test_button_not_created( + hass: HomeAssistant, electric_vehicle, entity_id_suffix +) -> None: + """Test that button entities are not created when they should not be.""" + await init_integration(hass, electric_vehicle=electric_vehicle) + + entity_registry = er.async_get(hass) + + entity_id = f"button.my_mazda3_{entity_id_suffix}" + entry = entity_registry.async_get(entity_id) + assert entry is None + state = hass.states.get(entity_id) + assert state is None + + +@pytest.mark.parametrize( + ("electric_vehicle", "entity_id_suffix", "api_method_name"), + [ + (False, "start_engine", "start_engine"), + (False, "stop_engine", "stop_engine"), + (False, "turn_on_hazard_lights", "turn_on_hazard_lights"), + (False, "turn_off_hazard_lights", "turn_off_hazard_lights"), + (True, "refresh_status", "refresh_vehicle_status"), ], ) async def test_button_press( - hass: HomeAssistant, entity_id_suffix, api_method_name + hass: HomeAssistant, electric_vehicle, entity_id_suffix, api_method_name ) -> None: """Test pressing the button entities.""" - client_mock = await init_integration(hass, electric_vehicle=True) + client_mock = await init_integration(hass, electric_vehicle=electric_vehicle) await hass.services.async_call( BUTTON_DOMAIN, From f55ada5d61980e2f1bc36245574f41d4f98c253a Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 29 Jun 2023 03:45:17 +0200 Subject: [PATCH 010/184] Add reload service to KNX (#95489) --- homeassistant/components/knx/__init__.py | 8 ++++++++ homeassistant/components/knx/services.yaml | 3 +++ tests/components/knx/test_services.py | 24 ++++++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index cc713d1034c..e8c237114b5 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -30,6 +30,7 @@ from homeassistant.const import ( CONF_PORT, CONF_TYPE, EVENT_HOMEASSISTANT_STOP, + SERVICE_RELOAD, Platform, ) from homeassistant.core import Event, HomeAssistant, ServiceCall @@ -312,6 +313,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: schema=SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA, ) + async def _reload_integration(call: ServiceCall) -> None: + """Reload the integration.""" + await hass.config_entries.async_reload(entry.entry_id) + hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context) + + async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _reload_integration) + await register_panel(hass) return True diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index d95a1573872..0ad497a30a2 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -106,3 +106,6 @@ exposure_register: default: false selector: boolean: +reload: + name: Reload + description: Reload the KNX integration. diff --git a/tests/components/knx/test_services.py b/tests/components/knx/test_services.py index 5e6f856ee44..5796eae8393 100644 --- a/tests/components/knx/test_services.py +++ b/tests/components/knx/test_services.py @@ -1,7 +1,10 @@ """Test KNX services.""" +from unittest.mock import patch + import pytest from xknx.telegram.apci import GroupValueResponse, GroupValueWrite +from homeassistant.components.knx import async_unload_entry as knx_async_unload_entry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -250,3 +253,24 @@ async def test_exposure_register(hass: HomeAssistant, knx: KNXTestKit) -> None: hass.states.async_set(test_entity, STATE_OFF, {test_attribute: 25}) await knx.assert_telegram_count(1) await knx.assert_write(test_address, (25,)) + + +async def test_reload_service( + hass: HomeAssistant, + knx: KNXTestKit, +) -> None: + """Test reload service.""" + await knx.setup_integration({}) + + with patch( + "homeassistant.components.knx.async_unload_entry", wraps=knx_async_unload_entry + ) as mock_unload_entry, patch( + "homeassistant.components.knx.async_setup_entry" + ) as mock_setup_entry: + await hass.services.async_call( + "knx", + "reload", + blocking=True, + ) + mock_unload_entry.assert_called_once() + mock_setup_entry.assert_called_once() From 8dd2e21d0b28f87eb5c93b5cd745bb6a769bc783 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 03:43:42 +0200 Subject: [PATCH 011/184] Fix YouTube coordinator bug (#95492) Fix coordinator bug --- homeassistant/components/youtube/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/youtube/coordinator.py b/homeassistant/components/youtube/coordinator.py index 693d2550f53..72629544895 100644 --- a/homeassistant/components/youtube/coordinator.py +++ b/homeassistant/components/youtube/coordinator.py @@ -65,7 +65,7 @@ class YouTubeDataUpdateCoordinator(DataUpdateCoordinator): channels = self.config_entry.options[CONF_CHANNELS] while received_channels < len(channels): # We're slicing the channels in chunks of 50 to avoid making the URI too long - end = min(received_channels + 50, len(channels) - 1) + end = min(received_channels + 50, len(channels)) channel_request: HttpRequest = service.channels().list( part="snippet,statistics", id=",".join(channels[received_channels:end]), From d44ef0708275927ef135a80e4f55dd2e4dbe4d66 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 29 Jun 2023 12:29:27 -0400 Subject: [PATCH 012/184] Fix binary sensor device trigger for lock class (#95505) --- homeassistant/components/binary_sensor/device_trigger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index c1eac31886e..de6dbdbe075 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -112,8 +112,8 @@ ENTITY_TRIGGERS = { {CONF_TYPE: CONF_NO_LIGHT}, ], BinarySensorDeviceClass.LOCK: [ - {CONF_TYPE: CONF_LOCKED}, {CONF_TYPE: CONF_NOT_LOCKED}, + {CONF_TYPE: CONF_LOCKED}, ], BinarySensorDeviceClass.MOISTURE: [ {CONF_TYPE: CONF_MOIST}, From 286cff314aec472331af03777b246d92ed36f261 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 29 Jun 2023 10:25:25 -0700 Subject: [PATCH 013/184] Limit fields returned for the list events service (#95506) * Limit fields returned for the list events service * Update websocket tests and fix bugs in response fields * Omit 'None' fields in the list events response --- homeassistant/components/calendar/__init__.py | 20 +++++++++++++++---- homeassistant/components/calendar/const.py | 9 +++++++++ tests/components/calendar/test_init.py | 18 +++++++++++------ .../components/websocket_api/test_commands.py | 3 --- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 5d0d2526bf2..86f61f0ed87 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -60,6 +60,7 @@ from .const import ( EVENT_TIME_FIELDS, EVENT_TYPES, EVENT_UID, + LIST_EVENT_FIELDS, CalendarEntityFeature, ) @@ -415,6 +416,17 @@ def _api_event_dict_factory(obj: Iterable[tuple[str, Any]]) -> dict[str, Any]: return result +def _list_events_dict_factory( + obj: Iterable[tuple[str, Any]] +) -> dict[str, JsonValueType]: + """Convert CalendarEvent dataclass items to dictionary of attributes.""" + return { + name: value + for name, value in obj + if name in LIST_EVENT_FIELDS and value is not None + } + + def _get_datetime_local( dt_or_d: datetime.datetime | datetime.date, ) -> datetime.datetime: @@ -782,9 +794,9 @@ async def async_list_events_service( else: end = service_call.data[EVENT_END_DATETIME] calendar_event_list = await calendar.async_get_events(calendar.hass, start, end) - events: list[JsonValueType] = [ - dataclasses.asdict(event) for event in calendar_event_list - ] return { - "events": events, + "events": [ + dataclasses.asdict(event, dict_factory=_list_events_dict_factory) + for event in calendar_event_list + ] } diff --git a/homeassistant/components/calendar/const.py b/homeassistant/components/calendar/const.py index 2d4f0dfe0ba..e667510325b 100644 --- a/homeassistant/components/calendar/const.py +++ b/homeassistant/components/calendar/const.py @@ -41,3 +41,12 @@ EVENT_TIME_FIELDS = { } EVENT_TYPES = "event_types" EVENT_DURATION = "duration" + +# Fields for the list events service +LIST_EVENT_FIELDS = { + "start", + "end", + EVENT_SUMMARY, + EVENT_DESCRIPTION, + EVENT_LOCATION, +} diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 97292221819..9fdc76abe03 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta from http import HTTPStatus from typing import Any -from unittest.mock import patch +from unittest.mock import ANY, patch import pytest import voluptuous as vol @@ -405,11 +405,17 @@ async def test_list_events_service(hass: HomeAssistant) -> None: blocking=True, return_response=True, ) - assert response - assert "events" in response - events = response["events"] - assert len(events) == 1 - assert events[0]["summary"] == "Future Event" + assert response == { + "events": [ + { + "start": ANY, + "end": ANY, + "summary": "Future Event", + "description": "Future Description", + "location": "Future Location", + } + ] + } @pytest.mark.parametrize( diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index c8f494a0071..7e46dc0d0bd 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1769,9 +1769,6 @@ async def test_execute_script_complex_response( "summary": "Future Event", "description": "Future Description", "location": "Future Location", - "uid": None, - "recurrence_id": None, - "rrule": None, } ] } From b93ceca804393f1b0d09c99ca9440ad083ed634b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 11:09:45 +0200 Subject: [PATCH 014/184] Add explicit device name to Spotify (#95509) --- homeassistant/components/spotify/media_player.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index a738952d2ca..0145d6f0906 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -106,6 +106,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): _attr_has_entity_name = True _attr_icon = "mdi:spotify" _attr_media_image_remotely_accessible = False + _attr_name = None def __init__( self, From 698333f89490a00d23f7dccba2ce075f5139120b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 11:24:59 +0200 Subject: [PATCH 015/184] Add explicit device name to Tuya (#95511) Co-authored-by: Franck Nijhof --- .../components/tuya/alarm_control_panel.py | 1 + homeassistant/components/tuya/camera.py | 1 + homeassistant/components/tuya/climate.py | 1 + homeassistant/components/tuya/fan.py | 1 + homeassistant/components/tuya/humidifier.py | 1 + homeassistant/components/tuya/light.py | 14 ++++++++++++++ homeassistant/components/tuya/siren.py | 4 +--- homeassistant/components/tuya/vacuum.py | 1 + 8 files changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tuya/alarm_control_panel.py b/homeassistant/components/tuya/alarm_control_panel.py index d5122862e2c..c2c9c207c02 100644 --- a/homeassistant/components/tuya/alarm_control_panel.py +++ b/homeassistant/components/tuya/alarm_control_panel.py @@ -89,6 +89,7 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity): """Tuya Alarm Entity.""" _attr_icon = "mdi:security" + _attr_name = None def __init__( self, diff --git a/homeassistant/components/tuya/camera.py b/homeassistant/components/tuya/camera.py index 182d0dfd85d..72216057aff 100644 --- a/homeassistant/components/tuya/camera.py +++ b/homeassistant/components/tuya/camera.py @@ -52,6 +52,7 @@ class TuyaCameraEntity(TuyaEntity, CameraEntity): _attr_supported_features = CameraEntityFeature.STREAM _attr_brand = "Tuya" + _attr_name = None def __init__( self, diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index bcb97327006..6b3b84ba349 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -125,6 +125,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): _set_humidity: IntegerTypeData | None = None _set_temperature: IntegerTypeData | None = None entity_description: TuyaClimateEntityDescription + _attr_name = None def __init__( self, diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 59cfee3506c..210cc5c7518 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -65,6 +65,7 @@ class TuyaFanEntity(TuyaEntity, FanEntity): _speed: IntegerTypeData | None = None _speeds: EnumTypeData | None = None _switch: DPCode | None = None + _attr_name = None def __init__( self, diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py index 458a2681186..6d09ba4314c 100644 --- a/homeassistant/components/tuya/humidifier.py +++ b/homeassistant/components/tuya/humidifier.py @@ -86,6 +86,7 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity): _set_humidity: IntegerTypeData | None = None _switch_dpcode: DPCode | None = None entity_description: TuyaHumidifierEntityDescription + _attr_name = None def __init__( self, diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 44b3494ca77..3ab4c3568c4 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -79,6 +79,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "dc": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, @@ -90,6 +91,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "dd": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, @@ -102,6 +104,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "dj": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=(DPCode.BRIGHT_VALUE_V2, DPCode.BRIGHT_VALUE), color_temp=(DPCode.TEMP_VALUE_V2, DPCode.TEMP_VALUE), @@ -120,6 +123,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "fsd": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, @@ -128,6 +132,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { # Some ceiling fan lights use LIGHT for DPCode instead of SWITCH_LED TuyaLightEntityDescription( key=DPCode.LIGHT, + name=None, ), ), # Ambient Light @@ -135,6 +140,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "fwd": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, @@ -146,6 +152,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "gyd": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, @@ -157,6 +164,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "jsq": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_data=DPCode.COLOUR_DATA_HSV, @@ -195,6 +203,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "mbd": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_data=DPCode.COLOUR_DATA, @@ -206,6 +215,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "qjdcz": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_data=DPCode.COLOUR_DATA, @@ -296,6 +306,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "tyndj": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, @@ -307,6 +318,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "xdd": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, @@ -322,6 +334,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "ykq": ( TuyaLightEntityDescription( key=DPCode.SWITCH_CONTROLLER, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_CONTROLLER, color_temp=DPCode.TEMP_CONTROLLER, @@ -332,6 +345,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { "fs": ( TuyaLightEntityDescription( key=DPCode.LIGHT, + name=None, color_mode=DPCode.WORK_MODE, brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, diff --git a/homeassistant/components/tuya/siren.py b/homeassistant/components/tuya/siren.py index a60e24eca86..c2dc8cea99b 100644 --- a/homeassistant/components/tuya/siren.py +++ b/homeassistant/components/tuya/siren.py @@ -27,7 +27,6 @@ SIRENS: dict[str, tuple[SirenEntityDescription, ...]] = { "dgnbj": ( SirenEntityDescription( key=DPCode.ALARM_SWITCH, - name="Siren", ), ), # Siren Alarm @@ -35,7 +34,6 @@ SIRENS: dict[str, tuple[SirenEntityDescription, ...]] = { "sgbj": ( SirenEntityDescription( key=DPCode.ALARM_SWITCH, - name="Siren", ), ), # Smart Camera @@ -43,7 +41,6 @@ SIRENS: dict[str, tuple[SirenEntityDescription, ...]] = { "sp": ( SirenEntityDescription( key=DPCode.SIREN_SWITCH, - name="Siren", ), ), } @@ -83,6 +80,7 @@ class TuyaSirenEntity(TuyaEntity, SirenEntity): """Tuya Siren Entity.""" _attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF + _attr_name = None def __init__( self, diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index 7827fb061ea..a2961a55d78 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -78,6 +78,7 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): _fan_speed: EnumTypeData | None = None _battery_level: IntegerTypeData | None = None + _attr_name = None def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: """Init Tuya vacuum.""" From cda5ee5814350465489863f30f863c83078d8af0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 11:25:39 +0200 Subject: [PATCH 016/184] Add explicit device name to Switchbot (#95512) --- homeassistant/components/switchbot/cover.py | 1 + homeassistant/components/switchbot/humidifier.py | 1 + homeassistant/components/switchbot/light.py | 1 + homeassistant/components/switchbot/lock.py | 1 + homeassistant/components/switchbot/switch.py | 1 + 5 files changed, 5 insertions(+) diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index da6c710fed8..1da879cb02b 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -52,6 +52,7 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): | CoverEntityFeature.SET_POSITION ) _attr_translation_key = "cover" + _attr_name = None def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: """Initialize the Switchbot.""" diff --git a/homeassistant/components/switchbot/humidifier.py b/homeassistant/components/switchbot/humidifier.py index e5f1e3c46b4..5b53b410208 100644 --- a/homeassistant/components/switchbot/humidifier.py +++ b/homeassistant/components/switchbot/humidifier.py @@ -41,6 +41,7 @@ class SwitchBotHumidifier(SwitchbotSwitchedEntity, HumidifierEntity): _device: switchbot.SwitchbotHumidifier _attr_min_humidity = 1 _attr_translation_key = "humidifier" + _attr_name = None @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/switchbot/light.py b/homeassistant/components/switchbot/light.py index 5dc99c5d6f1..53b40bbf780 100644 --- a/homeassistant/components/switchbot/light.py +++ b/homeassistant/components/switchbot/light.py @@ -46,6 +46,7 @@ class SwitchbotLightEntity(SwitchbotEntity, LightEntity): """Representation of switchbot light bulb.""" _device: SwitchbotBaseLight + _attr_name = None def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: """Initialize the Switchbot light.""" diff --git a/homeassistant/components/switchbot/lock.py b/homeassistant/components/switchbot/lock.py index 69ce0aa4750..7710cde12a9 100644 --- a/homeassistant/components/switchbot/lock.py +++ b/homeassistant/components/switchbot/lock.py @@ -27,6 +27,7 @@ class SwitchBotLock(SwitchbotEntity, LockEntity): """Representation of a Switchbot lock.""" _attr_translation_key = "lock" + _attr_name = None _device: switchbot.SwitchbotLock def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 25b76ea24cb..f62e4d3f918 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -35,6 +35,7 @@ class SwitchBotSwitch(SwitchbotSwitchedEntity, SwitchEntity, RestoreEntity): _attr_device_class = SwitchDeviceClass.SWITCH _attr_translation_key = "bot" + _attr_name = None _device: switchbot.Switchbot def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: From c4a46294ccfe5c680b97cac7721928f20f77bccf Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 12:02:09 +0200 Subject: [PATCH 017/184] Add explicit device name to Roborock (#95513) --- homeassistant/components/roborock/vacuum.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index 932febd80f0..5f66338ecc1 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -83,6 +83,7 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): | VacuumEntityFeature.START ) _attr_translation_key = DOMAIN + _attr_name = None def __init__( self, From 2ac5bb46b1ed66a1f57d89ddf377a71d7461d7d4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 12:03:25 +0200 Subject: [PATCH 018/184] Add explicit device name to Broadlink (#95516) --- homeassistant/components/broadlink/remote.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index c116a1bb635..c0fb80971ca 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -107,6 +107,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): """Representation of a Broadlink remote.""" _attr_has_entity_name = True + _attr_name = None def __init__(self, device, codes, flags): """Initialize the remote.""" From 2b274b4e95f1de9d3cccff88513b51876ac9a634 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 29 Jun 2023 15:28:34 +0200 Subject: [PATCH 019/184] Bump Matter Server to 3.6.3 (#95519) --- homeassistant/components/matter/adapter.py | 12 ++++++++---- homeassistant/components/matter/entity.py | 4 ++-- homeassistant/components/matter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/matter/common.py | 2 +- tests/components/matter/test_adapter.py | 6 +++--- 7 files changed, 17 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index f3a764bc99f..8e76706b7fd 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -96,20 +96,24 @@ class MatterAdapter: ) self.config_entry.async_on_unload( - self.matter_client.subscribe( + self.matter_client.subscribe_events( endpoint_added_callback, EventType.ENDPOINT_ADDED ) ) self.config_entry.async_on_unload( - self.matter_client.subscribe( + self.matter_client.subscribe_events( endpoint_removed_callback, EventType.ENDPOINT_REMOVED ) ) self.config_entry.async_on_unload( - self.matter_client.subscribe(node_removed_callback, EventType.NODE_REMOVED) + self.matter_client.subscribe_events( + node_removed_callback, EventType.NODE_REMOVED + ) ) self.config_entry.async_on_unload( - self.matter_client.subscribe(node_added_callback, EventType.NODE_ADDED) + self.matter_client.subscribe_events( + node_added_callback, EventType.NODE_ADDED + ) ) def _setup_node(self, node: MatterNode) -> None: diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index d4e90508afa..0457cfaa810 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -81,7 +81,7 @@ class MatterEntity(Entity): self._attributes_map[attr_cls] = attr_path sub_paths.append(attr_path) self._unsubscribes.append( - self.matter_client.subscribe( + self.matter_client.subscribe_events( callback=self._on_matter_event, event_filter=EventType.ATTRIBUTE_UPDATED, node_filter=self._endpoint.node.node_id, @@ -93,7 +93,7 @@ class MatterEntity(Entity): ) # subscribe to node (availability changes) self._unsubscribes.append( - self.matter_client.subscribe( + self.matter_client.subscribe_events( callback=self._on_matter_event, event_filter=EventType.NODE_UPDATED, node_filter=self._endpoint.node.node_id, diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 0bf900c8812..85434407a10 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/matter", "iot_class": "local_push", - "requirements": ["python-matter-server==3.6.0"] + "requirements": ["python-matter-server==3.6.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index b0d6425d442..aab14224dc9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2105,7 +2105,7 @@ python-kasa==0.5.1 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==3.6.0 +python-matter-server==3.6.3 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51f614a685f..2863b4c90f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1543,7 +1543,7 @@ python-juicenet==1.1.0 python-kasa==0.5.1 # homeassistant.components.matter -python-matter-server==3.6.0 +python-matter-server==3.6.3 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/tests/components/matter/common.py b/tests/components/matter/common.py index 7582b9c415d..a0935154054 100644 --- a/tests/components/matter/common.py +++ b/tests/components/matter/common.py @@ -71,6 +71,6 @@ async def trigger_subscription_callback( data: Any = None, ) -> None: """Trigger a subscription callback.""" - callback = client.subscribe.call_args.kwargs["callback"] + callback = client.subscribe_events.call_args.kwargs["callback"] callback(event, data) await hass.async_block_till_done() diff --git a/tests/components/matter/test_adapter.py b/tests/components/matter/test_adapter.py index 0d7cfc9c2ed..62ed847bf28 100644 --- a/tests/components/matter/test_adapter.py +++ b/tests/components/matter/test_adapter.py @@ -136,10 +136,10 @@ async def test_node_added_subscription( integration: MagicMock, ) -> None: """Test subscription to new devices work.""" - assert matter_client.subscribe.call_count == 4 - assert matter_client.subscribe.call_args[0][1] == EventType.NODE_ADDED + assert matter_client.subscribe_events.call_count == 4 + assert matter_client.subscribe_events.call_args[0][1] == EventType.NODE_ADDED - node_added_callback = matter_client.subscribe.call_args[0][0] + node_added_callback = matter_client.subscribe_events.call_args[0][0] node_data = load_and_parse_node_fixture("onoff-light") node = MatterNode( dataclass_from_dict( From 612f33e3727d1f6f680031d434b5ab569d2534b4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jun 2023 17:39:08 +0200 Subject: [PATCH 020/184] Mark text input required for conversation.process service (#95520) --- homeassistant/components/conversation/services.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/conversation/services.yaml b/homeassistant/components/conversation/services.yaml index 8ac929e13b6..1a28044dcb5 100644 --- a/homeassistant/components/conversation/services.yaml +++ b/homeassistant/components/conversation/services.yaml @@ -7,6 +7,7 @@ process: name: Text description: Transcribed text example: Turn all lights on + required: true selector: text: language: From c156951925dcd501166fd4e56fd5fd7a0cc44270 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jun 2023 12:03:42 +0200 Subject: [PATCH 021/184] Revert "Remove Brottsplatskartan YAML configuration (#94101)" (#95521) --- .../brottsplatskartan/config_flow.py | 15 +++ .../components/brottsplatskartan/sensor.py | 51 ++++++++- .../components/brottsplatskartan/strings.json | 6 + .../brottsplatskartan/test_config_flow.py | 108 ++++++++++++++++++ 4 files changed, 176 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/brottsplatskartan/config_flow.py b/homeassistant/components/brottsplatskartan/config_flow.py index 09d6cd96087..1de24ffa76c 100644 --- a/homeassistant/components/brottsplatskartan/config_flow.py +++ b/homeassistant/components/brottsplatskartan/config_flow.py @@ -34,6 +34,21 @@ class BPKConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Import a configuration from config.yaml.""" + + if config.get(CONF_LATITUDE): + config[CONF_LOCATION] = { + CONF_LATITUDE: config[CONF_LATITUDE], + CONF_LONGITUDE: config[CONF_LONGITUDE], + } + if not config.get(CONF_AREA): + config[CONF_AREA] = "none" + else: + config[CONF_AREA] = config[CONF_AREA][0] + + return await self.async_step_user(user_input=config) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index 63af7530b79..a70b9c134d0 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -5,19 +5,62 @@ from collections import defaultdict from datetime import timedelta from brottsplatskartan import ATTRIBUTION, BrottsplatsKartan +import voluptuous as vol -from homeassistant.components.sensor import SensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SensorEntity, +) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import CONF_APP_ID, CONF_AREA, DOMAIN, LOGGER +from .const import AREAS, CONF_APP_ID, CONF_AREA, DEFAULT_NAME, DOMAIN, LOGGER SCAN_INTERVAL = timedelta(minutes=30) +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( + { + vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_AREA, default=[]): vol.All(cv.ensure_list, [vol.In(AREAS)]), + } +) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Brottsplatskartan platform.""" + + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.11.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/brottsplatskartan/strings.json b/homeassistant/components/brottsplatskartan/strings.json index f10120f7884..8d9677a0af4 100644 --- a/homeassistant/components/brottsplatskartan/strings.json +++ b/homeassistant/components/brottsplatskartan/strings.json @@ -16,6 +16,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "title": "The Brottsplatskartan YAML configuration is being removed", + "description": "Configuring Brottsplatskartan using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Brottsplatskartan YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + }, "selector": { "areas": { "options": { diff --git a/tests/components/brottsplatskartan/test_config_flow.py b/tests/components/brottsplatskartan/test_config_flow.py index efd259fa73c..dd3139dc2b9 100644 --- a/tests/components/brottsplatskartan/test_config_flow.py +++ b/tests/components/brottsplatskartan/test_config_flow.py @@ -1,6 +1,8 @@ """Test the Brottsplatskartan config flow.""" from __future__ import annotations +from unittest.mock import patch + import pytest from homeassistant import config_entries @@ -9,6 +11,8 @@ from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from tests.common import MockConfigEntry + pytestmark = pytest.mark.usefixtures("mock_setup_entry") @@ -99,3 +103,107 @@ async def test_form_area(hass: HomeAssistant) -> None: "area": "Stockholms län", "app_id": "ha-1234567890", } + + +async def test_import_flow_success(hass: HomeAssistant) -> None: + """Test a successful import of yaml.""" + + with patch( + "homeassistant.components.brottsplatskartan.sensor.BrottsplatsKartan", + ): + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Brottsplatskartan HOME" + assert result2["data"] == { + "latitude": hass.config.latitude, + "longitude": hass.config.longitude, + "area": None, + "app_id": "ha-1234567890", + } + + +async def test_import_flow_location_success(hass: HomeAssistant) -> None: + """Test a successful import of yaml with location.""" + + with patch( + "homeassistant.components.brottsplatskartan.sensor.BrottsplatsKartan", + ): + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_LATITUDE: 59.32, + CONF_LONGITUDE: 18.06, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Brottsplatskartan 59.32, 18.06" + assert result2["data"] == { + "latitude": 59.32, + "longitude": 18.06, + "area": None, + "app_id": "ha-1234567890", + } + + +async def test_import_flow_location_area_success(hass: HomeAssistant) -> None: + """Test a successful import of yaml with location and area.""" + + with patch( + "homeassistant.components.brottsplatskartan.sensor.BrottsplatsKartan", + ): + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_LATITUDE: 59.32, + CONF_LONGITUDE: 18.06, + CONF_AREA: ["Blekinge län"], + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Brottsplatskartan Blekinge län" + assert result2["data"] == { + "latitude": None, + "longitude": None, + "area": "Blekinge län", + "app_id": "ha-1234567890", + } + + +async def test_import_flow_already_exist(hass: HomeAssistant) -> None: + """Test import of yaml already exist.""" + + MockConfigEntry( + domain=DOMAIN, + data={ + "latitude": hass.config.latitude, + "longitude": hass.config.longitude, + "area": None, + "app_id": "ha-1234567890", + }, + unique_id="bpk-home", + ).add_to_hass(hass) + + with patch( + "homeassistant.components.brottsplatskartan.sensor.BrottsplatsKartan", + ): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={}, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.ABORT + assert result3["reason"] == "already_configured" From f165357772966dfa736b0570c2aabd98a165cc93 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jun 2023 12:01:26 +0200 Subject: [PATCH 022/184] Revert "Remove qbittorrent YAML configuration (#93548)" (#95522) --- .../components/qbittorrent/config_flow.py | 24 +++++++++++- .../components/qbittorrent/sensor.py | 27 ++++++++++++- .../components/qbittorrent/strings.json | 6 +++ .../qbittorrent/test_config_flow.py | 38 ++++++++++++++++++- 4 files changed, 92 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qbittorrent/config_flow.py b/homeassistant/components/qbittorrent/config_flow.py index ac41d03e998..54c47c53895 100644 --- a/homeassistant/components/qbittorrent/config_flow.py +++ b/homeassistant/components/qbittorrent/config_flow.py @@ -1,6 +1,7 @@ """Config flow for qBittorrent.""" from __future__ import annotations +import logging from typing import Any from qbittorrent.client import LoginRequired @@ -8,12 +9,20 @@ from requests.exceptions import RequestException import voluptuous as vol from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL +from homeassistant.const import ( + CONF_NAME, + CONF_PASSWORD, + CONF_URL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_NAME, DEFAULT_URL, DOMAIN from .helpers import setup_client +_LOGGER = logging.getLogger(__name__) + USER_DATA_SCHEMA = vol.Schema( { vol.Required(CONF_URL, default=DEFAULT_URL): str, @@ -52,3 +61,16 @@ class QbittorrentConfigFlow(ConfigFlow, domain=DOMAIN): schema = self.add_suggested_values_to_schema(USER_DATA_SCHEMA, user_input) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + self._async_abort_entries_match({CONF_URL: config[CONF_URL]}) + return self.async_create_entry( + title=config.get(CONF_NAME, DEFAULT_NAME), + data={ + CONF_URL: config[CONF_URL], + CONF_USERNAME: config[CONF_USERNAME], + CONF_PASSWORD: config[CONF_PASSWORD], + CONF_VERIFY_SSL: True, + }, + ) diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index b6d9fe63e4b..15a634cf7a9 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -14,7 +14,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -24,8 +24,10 @@ from homeassistant.const import ( UnitOfDataRate, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DEFAULT_NAME, DOMAIN @@ -68,6 +70,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the qBittorrent platform.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) + ir.async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.11.0", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, diff --git a/homeassistant/components/qbittorrent/strings.json b/homeassistant/components/qbittorrent/strings.json index 66c9430911e..24d1885a917 100644 --- a/homeassistant/components/qbittorrent/strings.json +++ b/homeassistant/components/qbittorrent/strings.json @@ -17,5 +17,11 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The qBittorrent YAML configuration is being removed", + "description": "Configuring qBittorrent using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the qBittorrent YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/tests/components/qbittorrent/test_config_flow.py b/tests/components/qbittorrent/test_config_flow.py index bbfeee20d8a..b7244ccef8d 100644 --- a/tests/components/qbittorrent/test_config_flow.py +++ b/tests/components/qbittorrent/test_config_flow.py @@ -4,7 +4,7 @@ from requests.exceptions import RequestException import requests_mock from homeassistant.components.qbittorrent.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import ( CONF_PASSWORD, CONF_SOURCE, @@ -26,6 +26,12 @@ USER_INPUT = { CONF_VERIFY_SSL: True, } +YAML_IMPORT = { + CONF_URL: "http://localhost:8080", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", +} + async def test_flow_user(hass: HomeAssistant, mock_api: requests_mock.Mocker) -> None: """Test the user flow.""" @@ -98,3 +104,33 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" + + +async def test_flow_import(hass: HomeAssistant) -> None: + """Test import step.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_IMPORT}, + data=YAML_IMPORT, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_URL: "http://localhost:8080", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_VERIFY_SSL: True, + } + + +async def test_flow_import_already_configured(hass: HomeAssistant) -> None: + """Test import step already configured.""" + entry = MockConfigEntry(domain=DOMAIN, data=USER_INPUT) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_IMPORT}, + data=YAML_IMPORT, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" From 4f99a71f6145180d10bce9259c4dfcce58b21107 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jun 2023 12:00:41 +0200 Subject: [PATCH 023/184] Revert "Remove snapcast YAML configuration (#93547)" (#95523) --- .../components/snapcast/config_flow.py | 10 +++++ .../components/snapcast/media_player.py | 41 ++++++++++++++++++- .../components/snapcast/strings.json | 6 +++ tests/components/snapcast/test_config_flow.py | 15 +++++++ 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/snapcast/config_flow.py b/homeassistant/components/snapcast/config_flow.py index 479d1d648b8..896d3f8b5a8 100644 --- a/homeassistant/components/snapcast/config_flow.py +++ b/homeassistant/components/snapcast/config_flow.py @@ -51,3 +51,13 @@ class SnapcastConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=SNAPCAST_SCHEMA, errors=errors ) + + async def async_step_import(self, import_config: dict[str, str]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + self._async_abort_entries_match( + { + CONF_HOST: (import_config[CONF_HOST]), + CONF_PORT: (import_config[CONF_PORT]), + } + ) + return self.async_create_entry(title=DEFAULT_TITLE, data=import_config) diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index 377a3d1e2b6..096e3829bc7 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -1,19 +1,24 @@ """Support for interacting with Snapcast clients.""" from __future__ import annotations -from snapcast.control.server import Snapserver +import logging + +from snapcast.control.server import CONTROL_PORT, Snapserver import voluptuous as vol from homeassistant.components.media_player import ( + PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( ATTR_LATENCY, @@ -30,6 +35,12 @@ from .const import ( SERVICE_UNJOIN, ) +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT): cv.port} +) + STREAM_STATUS = { "idle": MediaPlayerState.IDLE, "playing": MediaPlayerState.PLAYING, @@ -82,6 +93,32 @@ async def async_setup_entry( ].hass_async_add_entities = async_add_entities +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Snapcast platform.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.11.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + + config[CONF_PORT] = config.get(CONF_PORT, CONTROL_PORT) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) + + async def handle_async_join(entity, service_call): """Handle the entity service join.""" if not isinstance(entity, SnapcastClientDevice): diff --git a/homeassistant/components/snapcast/strings.json b/homeassistant/components/snapcast/strings.json index 766bca63495..0087b70d820 100644 --- a/homeassistant/components/snapcast/strings.json +++ b/homeassistant/components/snapcast/strings.json @@ -17,5 +17,11 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_host": "[%key:common::config_flow::error::invalid_host%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Snapcast YAML configuration is being removed", + "description": "Configuring Snapcast using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Snapcast YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/tests/components/snapcast/test_config_flow.py b/tests/components/snapcast/test_config_flow.py index bb07eae2140..b6ff43503a6 100644 --- a/tests/components/snapcast/test_config_flow.py +++ b/tests/components/snapcast/test_config_flow.py @@ -93,3 +93,18 @@ async def test_abort( assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" + + +async def test_import(hass: HomeAssistant) -> None: + """Test successful import.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=TEST_CONNECTION, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Snapcast" + assert result["data"] == {CONF_HOST: "snapserver.test", CONF_PORT: 1705} From 61ab84bf0462266241ec79c0f57ed9299c449deb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jun 2023 12:00:13 +0200 Subject: [PATCH 024/184] Revert "Remove Workday YAML configuration (#94102)" (#95524) --- .../components/workday/binary_sensor.py | 82 ++++++++++- .../components/workday/config_flow.py | 27 ++++ homeassistant/components/workday/strings.json | 6 + .../components/workday/test_binary_sensor.py | 45 ++++++ tests/components/workday/test_config_flow.py | 139 ++++++++++++++++++ 5 files changed, 297 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 7c0dc8ff0a6..4ea4da602e3 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -2,17 +2,25 @@ from __future__ import annotations from datetime import date, timedelta +from typing import Any import holidays from holidays import DateLike, HolidayBase +import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.config_entries import ConfigEntry +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + BinarySensorEntity, +) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util from .const import ( @@ -24,11 +32,81 @@ from .const import ( CONF_PROVINCE, CONF_REMOVE_HOLIDAYS, CONF_WORKDAYS, + DEFAULT_EXCLUDES, + DEFAULT_NAME, + DEFAULT_OFFSET, + DEFAULT_WORKDAYS, DOMAIN, LOGGER, ) +def valid_country(value: Any) -> str: + """Validate that the given country is supported.""" + value = cv.string(value) + all_supported_countries = holidays.list_supported_countries() + + try: + raw_value = value.encode("utf-8") + except UnicodeError as err: + raise vol.Invalid( + "The country name or the abbreviation must be a valid UTF-8 string." + ) from err + if not raw_value: + raise vol.Invalid("Country name or the abbreviation must not be empty.") + if value not in all_supported_countries: + raise vol.Invalid("Country is not supported.") + return value + + +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_COUNTRY): valid_country, + vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES): vol.All( + cv.ensure_list, [vol.In(ALLOWED_DAYS)] + ), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int), + vol.Optional(CONF_PROVINCE): cv.string, + vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS): vol.All( + cv.ensure_list, [vol.In(ALLOWED_DAYS)] + ), + vol.Optional(CONF_ADD_HOLIDAYS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_REMOVE_HOLIDAYS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + } +) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Workday sensor.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.11.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: diff --git a/homeassistant/components/workday/config_flow.py b/homeassistant/components/workday/config_flow.py index bfa6c299b57..7153dac1bcb 100644 --- a/homeassistant/components/workday/config_flow.py +++ b/homeassistant/components/workday/config_flow.py @@ -155,6 +155,33 @@ class WorkdayConfigFlow(ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return WorkdayOptionsFlowHandler(config_entry) + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Import a configuration from config.yaml.""" + + abort_match = { + CONF_COUNTRY: config[CONF_COUNTRY], + CONF_EXCLUDES: config[CONF_EXCLUDES], + CONF_OFFSET: config[CONF_OFFSET], + CONF_WORKDAYS: config[CONF_WORKDAYS], + CONF_ADD_HOLIDAYS: config[CONF_ADD_HOLIDAYS], + CONF_REMOVE_HOLIDAYS: config[CONF_REMOVE_HOLIDAYS], + CONF_PROVINCE: config.get(CONF_PROVINCE), + } + new_config = config.copy() + new_config[CONF_PROVINCE] = config.get(CONF_PROVINCE) + LOGGER.debug("Importing with %s", new_config) + + self._async_abort_entries_match(abort_match) + + self.data[CONF_NAME] = config.get(CONF_NAME, DEFAULT_NAME) + self.data[CONF_COUNTRY] = config[CONF_COUNTRY] + LOGGER.debug( + "No duplicate, next step with name %s for country %s", + self.data[CONF_NAME], + self.data[CONF_COUNTRY], + ) + return await self.async_step_options(user_input=new_config) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/workday/strings.json b/homeassistant/components/workday/strings.json index 5af69e29a8b..e6753b39dce 100644 --- a/homeassistant/components/workday/strings.json +++ b/homeassistant/components/workday/strings.json @@ -64,6 +64,12 @@ "already_configured": "Service with this configuration already exist" } }, + "issues": { + "deprecated_yaml": { + "title": "The Workday YAML configuration is being removed", + "description": "Configuring Workday using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Workday YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + }, "selector": { "province": { "options": { diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index d2ae9895544..71dd23c19a3 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -4,7 +4,9 @@ from typing import Any from freezegun.api import FrozenDateTimeFactory import pytest +import voluptuous as vol +from homeassistant.components.workday import binary_sensor from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import UTC @@ -28,6 +30,21 @@ from . import ( ) +async def test_valid_country_yaml() -> None: + """Test valid country from yaml.""" + # Invalid UTF-8, must not contain U+D800 to U+DFFF + with pytest.raises(vol.Invalid): + binary_sensor.valid_country("\ud800") + with pytest.raises(vol.Invalid): + binary_sensor.valid_country("\udfff") + # Country MUST NOT be empty + with pytest.raises(vol.Invalid): + binary_sensor.valid_country("") + # Country must be supported by holidays + with pytest.raises(vol.Invalid): + binary_sensor.valid_country("HomeAssistantLand") + + @pytest.mark.parametrize( ("config", "expected_state"), [ @@ -62,6 +79,34 @@ async def test_setup( } +async def test_setup_from_import( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test setup from various configs.""" + freezer.move_to(datetime(2022, 4, 15, 12, tzinfo=UTC)) # Monday + await async_setup_component( + hass, + "binary_sensor", + { + "binary_sensor": { + "platform": "workday", + "country": "DE", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.workday_sensor") + assert state.state == "off" + assert state.attributes == { + "friendly_name": "Workday Sensor", + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + } + + async def test_setup_with_invalid_province_from_yaml(hass: HomeAssistant) -> None: """Test setup invalid province with import.""" diff --git a/tests/components/workday/test_config_flow.py b/tests/components/workday/test_config_flow.py index ce4dd127778..7e28471c78c 100644 --- a/tests/components/workday/test_config_flow.py +++ b/tests/components/workday/test_config_flow.py @@ -13,6 +13,7 @@ from homeassistant.components.workday.const import ( CONF_REMOVE_HOLIDAYS, CONF_WORKDAYS, DEFAULT_EXCLUDES, + DEFAULT_NAME, DEFAULT_OFFSET, DEFAULT_WORKDAYS, DOMAIN, @@ -23,6 +24,8 @@ from homeassistant.data_entry_flow import FlowResultType from . import init_integration +from tests.common import MockConfigEntry + pytestmark = pytest.mark.usefixtures("mock_setup_entry") @@ -111,6 +114,142 @@ async def test_form_no_subdivision(hass: HomeAssistant) -> None: } +async def test_import_flow_success(hass: HomeAssistant) -> None: + """Test a successful import of yaml.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_NAME: DEFAULT_NAME, + CONF_COUNTRY: "DE", + CONF_EXCLUDES: DEFAULT_EXCLUDES, + CONF_OFFSET: DEFAULT_OFFSET, + CONF_WORKDAYS: DEFAULT_WORKDAYS, + CONF_ADD_HOLIDAYS: [], + CONF_REMOVE_HOLIDAYS: [], + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Workday Sensor" + assert result["options"] == { + "name": "Workday Sensor", + "country": "DE", + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "add_holidays": [], + "remove_holidays": [], + "province": None, + } + + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_NAME: "Workday Sensor 2", + CONF_COUNTRY: "DE", + CONF_PROVINCE: "BW", + CONF_EXCLUDES: DEFAULT_EXCLUDES, + CONF_OFFSET: DEFAULT_OFFSET, + CONF_WORKDAYS: DEFAULT_WORKDAYS, + CONF_ADD_HOLIDAYS: [], + CONF_REMOVE_HOLIDAYS: [], + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Workday Sensor 2" + assert result2["options"] == { + "name": "Workday Sensor 2", + "country": "DE", + "province": "BW", + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "add_holidays": [], + "remove_holidays": [], + } + + +async def test_import_flow_already_exist(hass: HomeAssistant) -> None: + """Test import of yaml already exist.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + "name": "Workday Sensor", + "country": "DE", + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "add_holidays": [], + "remove_holidays": [], + "province": None, + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_NAME: "Workday sensor 2", + CONF_COUNTRY: "DE", + CONF_EXCLUDES: ["sat", "sun", "holiday"], + CONF_OFFSET: 0, + CONF_WORKDAYS: ["mon", "tue", "wed", "thu", "fri"], + CONF_ADD_HOLIDAYS: [], + CONF_REMOVE_HOLIDAYS: [], + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_import_flow_province_no_conflict(hass: HomeAssistant) -> None: + """Test import of yaml with province.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + "name": "Workday Sensor", + "country": "DE", + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "add_holidays": [], + "remove_holidays": [], + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_NAME: "Workday sensor 2", + CONF_COUNTRY: "DE", + CONF_PROVINCE: "BW", + CONF_EXCLUDES: ["sat", "sun", "holiday"], + CONF_OFFSET: 0, + CONF_WORKDAYS: ["mon", "tue", "wed", "thu", "fri"], + CONF_ADD_HOLIDAYS: [], + CONF_REMOVE_HOLIDAYS: [], + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + + async def test_options_form(hass: HomeAssistant) -> None: """Test we get the form in options.""" From 129fee09d33d7645c1e6fb0378a22838285570f8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jun 2023 11:59:36 +0200 Subject: [PATCH 025/184] Bump breaking version for YAML features ADR-0021 (#95525) --- homeassistant/components/command_line/binary_sensor.py | 2 +- homeassistant/components/command_line/cover.py | 2 +- homeassistant/components/command_line/notify.py | 2 +- homeassistant/components/command_line/sensor.py | 2 +- homeassistant/components/command_line/switch.py | 2 +- homeassistant/components/counter/__init__.py | 2 +- homeassistant/components/dwd_weather_warnings/sensor.py | 2 +- homeassistant/components/ezviz/camera.py | 2 +- homeassistant/components/geo_json_events/geo_location.py | 2 +- homeassistant/components/lastfm/sensor.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index fb8f57b4d5a..f2097178a95 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -70,7 +70,7 @@ async def async_setup_platform( hass, DOMAIN, "deprecated_yaml_binary_sensor", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=False, severity=IssueSeverity.WARNING, translation_key="deprecated_platform_yaml", diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 90bc5b7d50e..553af2f0c86 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -73,7 +73,7 @@ async def async_setup_platform( hass, DOMAIN, "deprecated_yaml_cover", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=False, severity=IssueSeverity.WARNING, translation_key="deprecated_platform_yaml", diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index 2f4f20045d7..d00926eb0ee 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -43,7 +43,7 @@ def get_service( hass, DOMAIN, "deprecated_yaml_notify", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=False, severity=IssueSeverity.WARNING, translation_key="deprecated_platform_yaml", diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index c164c6636fa..1b865827e69 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -74,7 +74,7 @@ async def async_setup_platform( hass, DOMAIN, "deprecated_yaml_sensor", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=False, severity=IssueSeverity.WARNING, translation_key="deprecated_platform_yaml", diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 5beb06eea9d..8fbafd7a4d1 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -74,7 +74,7 @@ async def async_setup_platform( hass, DOMAIN, "deprecated_yaml_switch", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=False, severity=IssueSeverity.WARNING, translation_key="deprecated_platform_yaml", diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index d2834b8991b..6f3d48fc1bb 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -292,7 +292,7 @@ class Counter(collection.CollectionEntity, RestoreEntity): self.hass, DOMAIN, "deprecated_configure_service", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=True, is_persistent=True, severity=IssueSeverity.WARNING, diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 3e8ed2afbdc..f44d736b426 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -91,7 +91,7 @@ async def async_setup_platform( hass, DOMAIN, "deprecated_yaml", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=False, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 57be995a489..60a332446ce 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -312,7 +312,7 @@ class EzvizCamera(EzvizEntity, Camera): self.hass, DOMAIN, "service_depreciation_detection_sensibility", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=False, severity=ir.IssueSeverity.WARNING, translation_key="service_depreciation_detection_sensibility", diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py index def8f77994e..b922d98f25e 100644 --- a/homeassistant/components/geo_json_events/geo_location.py +++ b/homeassistant/components/geo_json_events/geo_location.py @@ -83,7 +83,7 @@ async def async_setup_platform( hass, DOMAIN, "deprecated_yaml", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=False, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index 08179df5b7e..b4776b19c50 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -53,7 +53,7 @@ async def async_setup_platform( hass, DOMAIN, "deprecated_yaml", - breaks_in_ha_version="2023.8.0", + breaks_in_ha_version="2023.12.0", is_fixable=False, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", From 1df12e8771add68ba992b9517d86bead55652c9f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 17:36:39 +0200 Subject: [PATCH 026/184] Use explicit naming in workday sensor (#95531) --- homeassistant/components/workday/binary_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 4ea4da602e3..51560161faa 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -178,6 +178,7 @@ class IsWorkdaySensor(BinarySensorEntity): """Implementation of a Workday sensor.""" _attr_has_entity_name = True + _attr_name = None def __init__( self, From 8f88b710f0e7d8167f98acf334a62957ae97fdce Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 14:49:46 +0200 Subject: [PATCH 027/184] Use explicit naming in Nest (#95532) --- homeassistant/components/nest/camera_sdm.py | 1 + homeassistant/components/nest/climate_sdm.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 834202ba58d..3eceb448fa4 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -61,6 +61,7 @@ class NestCamera(Camera): """Devices that support cameras.""" _attr_has_entity_name = True + _attr_name = None def __init__(self, device: Device) -> None: """Initialize the camera.""" diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index ab0ce20a9a1..ca975ed055d 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -99,6 +99,7 @@ class ThermostatEntity(ClimateEntity): _attr_max_temp = MAX_TEMP _attr_has_entity_name = True _attr_should_poll = False + _attr_name = None def __init__(self, device: Device) -> None: """Initialize ThermostatEntity.""" From 2a42622de9f859d6838832c88ccdd710d0e5305b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jun 2023 10:35:32 -0500 Subject: [PATCH 028/184] Fix manual specification of multiple advertise_ip with HomeKit (#95548) fixes #95508 --- homeassistant/components/homekit/__init__.py | 2 +- tests/components/homekit/test_homekit.py | 76 ++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 9a25a28aa1c..514c218b101 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -168,7 +168,7 @@ BRIDGE_SCHEMA = vol.All( vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_IP_ADDRESS): vol.All(ipaddress.ip_address, cv.string), vol.Optional(CONF_ADVERTISE_IP): vol.All( - cv.ensure_list, ipaddress.ip_address, cv.string + cv.ensure_list, [ipaddress.ip_address], [cv.string] ), vol.Optional(CONF_FILTER, default={}): BASE_FILTER_SCHEMA, vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 5c154a50bec..112c138a843 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from typing import Any from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch from uuid import uuid1 @@ -24,6 +25,7 @@ from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, + CONF_ADVERTISE_IP, DEFAULT_PORT, DOMAIN, HOMEKIT, @@ -322,6 +324,80 @@ async def test_homekit_setup_ip_address( ) +async def test_homekit_with_single_advertise_ips( + hass: HomeAssistant, + hk_driver, + mock_async_zeroconf: None, + hass_storage: dict[str, Any], +) -> None: + """Test setup with a single advertise ips.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345, CONF_ADVERTISE_IP: "1.3.4.4"}, + source=SOURCE_IMPORT, + ) + entry.add_to_hass(hass) + with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver: + mock_driver.async_start = AsyncMock() + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mock_driver.assert_called_with( + hass, + entry.entry_id, + ANY, + entry.title, + loop=hass.loop, + address=[None], + port=ANY, + persist_file=ANY, + advertised_address="1.3.4.4", + async_zeroconf_instance=mock_async_zeroconf, + zeroconf_server=ANY, + loader=ANY, + iid_storage=ANY, + ) + + +async def test_homekit_with_many_advertise_ips( + hass: HomeAssistant, + hk_driver, + mock_async_zeroconf: None, + hass_storage: dict[str, Any], +) -> None: + """Test setup with many advertise ips.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_NAME: "mock_name", + CONF_PORT: 12345, + CONF_ADVERTISE_IP: ["1.3.4.4", "4.3.2.2"], + }, + source=SOURCE_IMPORT, + ) + entry.add_to_hass(hass) + with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver: + mock_driver.async_start = AsyncMock() + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mock_driver.assert_called_with( + hass, + entry.entry_id, + ANY, + entry.title, + loop=hass.loop, + address=[None], + port=ANY, + persist_file=ANY, + advertised_address=["1.3.4.4", "4.3.2.2"], + async_zeroconf_instance=mock_async_zeroconf, + zeroconf_server=ANY, + loader=ANY, + iid_storage=ANY, + ) + + async def test_homekit_setup_advertise_ips( hass: HomeAssistant, hk_driver, mock_async_zeroconf: None ) -> None: From 22e32bc7370a9f4f7248de0a0de276d264096865 Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 29 Jun 2023 13:13:37 -0400 Subject: [PATCH 029/184] Bump Roborock to 0.29.2 (#95549) * init work * fix tests --- homeassistant/components/roborock/device.py | 11 + .../components/roborock/manifest.json | 2 +- homeassistant/components/roborock/switch.py | 191 ++++++------------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/roborock/conftest.py | 10 +- tests/components/roborock/mock_data.py | 7 +- .../roborock/snapshots/test_diagnostics.ambr | 15 -- tests/components/roborock/test_select.py | 2 + tests/components/roborock/test_switch.py | 23 +-- 10 files changed, 95 insertions(+), 170 deletions(-) diff --git a/homeassistant/components/roborock/device.py b/homeassistant/components/roborock/device.py index 3801ccbecc9..90ca13c5146 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -2,6 +2,8 @@ from typing import Any +from roborock.api import AttributeCache +from roborock.command_cache import CacheableAttribute from roborock.containers import Status from roborock.exceptions import RoborockException from roborock.local_api import RoborockLocalClient @@ -27,6 +29,15 @@ class RoborockEntity(Entity): self._attr_device_info = device_info self._api = api + @property + def api(self) -> RoborockLocalClient: + """Returns the api.""" + return self._api + + def get_cache(self, attribute: CacheableAttribute) -> AttributeCache: + """Get an item from the api cache.""" + return self._api.cache.get(attribute) + async def send( self, command: RoborockCommand, params: dict[str, Any] | list[Any] | None = None ) -> dict: diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 39e13412e90..baab687e64a 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.23.6"] + "requirements": ["python-roborock==0.29.2"] } diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py index d16d7437202..a0b3d5be295 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -1,23 +1,27 @@ """Support for Roborock switch.""" +from __future__ import annotations + import asyncio from collections.abc import Callable, Coroutine from dataclasses import dataclass import logging from typing import Any -from roborock.exceptions import RoborockException -from roborock.roborock_typing import RoborockCommand +from roborock.api import AttributeCache +from roborock.command_cache import CacheableAttribute +from roborock.local_api import RoborockLocalClient from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify from .const import DOMAIN from .coordinator import RoborockDataUpdateCoordinator -from .device import RoborockCoordinatedEntity, RoborockEntity +from .device import RoborockEntity _LOGGER = logging.getLogger(__name__) @@ -27,23 +31,11 @@ class RoborockSwitchDescriptionMixin: """Define an entity description mixin for switch entities.""" # Gets the status of the switch - get_value: Callable[[RoborockEntity], Coroutine[Any, Any, dict]] - # Evaluate the result of get_value to determine a bool - evaluate_value: Callable[[dict], bool] + cache_key: CacheableAttribute # Sets the status of the switch - set_command: Callable[[RoborockEntity, bool], Coroutine[Any, Any, dict]] - # Check support of this feature - check_support: Callable[[RoborockDataUpdateCoordinator], Coroutine[Any, Any, dict]] - - -@dataclass -class RoborockCoordinatedSwitchDescriptionMixIn: - """Define an entity description mixin for switch entities.""" - - get_value: Callable[[RoborockCoordinatedEntity], bool] - set_command: Callable[[RoborockCoordinatedEntity, bool], Coroutine[Any, Any, dict]] - # Check support of this feature - check_support: Callable[[RoborockDataUpdateCoordinator], dict] + update_value: Callable[[AttributeCache, bool], Coroutine[Any, Any, dict]] + # Attribute from cache + attribute: str @dataclass @@ -53,59 +45,42 @@ class RoborockSwitchDescription( """Class to describe an Roborock switch entity.""" -@dataclass -class RoborockCoordinatedSwitchDescription( - SwitchEntityDescription, RoborockCoordinatedSwitchDescriptionMixIn -): - """Class to describe an Roborock switch entity that needs a coordinator.""" - - SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [ RoborockSwitchDescription( - set_command=lambda entity, value: entity.send( - RoborockCommand.SET_CHILD_LOCK_STATUS, {"lock_status": 1 if value else 0} + cache_key=CacheableAttribute.child_lock_status, + update_value=lambda cache, value: cache.update_value( + {"lock_status": 1 if value else 0} ), - get_value=lambda data: data.send(RoborockCommand.GET_CHILD_LOCK_STATUS), - check_support=lambda data: data.api.send_command( - RoborockCommand.GET_CHILD_LOCK_STATUS - ), - evaluate_value=lambda data: data["lock_status"] == 1, + attribute="lock_status", key="child_lock", translation_key="child_lock", icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), RoborockSwitchDescription( - set_command=lambda entity, value: entity.send( - RoborockCommand.SET_FLOW_LED_STATUS, {"status": 1 if value else 0} + cache_key=CacheableAttribute.flow_led_status, + update_value=lambda cache, value: cache.update_value( + {"status": 1 if value else 0} ), - get_value=lambda data: data.send(RoborockCommand.GET_FLOW_LED_STATUS), - check_support=lambda data: data.api.send_command( - RoborockCommand.GET_FLOW_LED_STATUS - ), - evaluate_value=lambda data: data["status"] == 1, + attribute="status", key="status_indicator", translation_key="status_indicator", icon="mdi:alarm-light-outline", entity_category=EntityCategory.CONFIG, ), -] - -COORDINATED_SWITCH_DESCRIPTION = [ - RoborockCoordinatedSwitchDescription( - set_command=lambda entity, value: entity.send( - RoborockCommand.SET_DND_TIMER, + RoborockSwitchDescription( + cache_key=CacheableAttribute.dnd_timer, + update_value=lambda cache, value: cache.update_value( [ - entity.coordinator.roborock_device_info.props.dnd_timer.start_hour, - entity.coordinator.roborock_device_info.props.dnd_timer.start_minute, - entity.coordinator.roborock_device_info.props.dnd_timer.end_hour, - entity.coordinator.roborock_device_info.props.dnd_timer.end_minute, - ], + cache.value.get("start_hour"), + cache.value.get("start_minute"), + cache.value.get("end_hour"), + cache.value.get("end_minute"), + ] ) if value - else entity.send(RoborockCommand.CLOSE_DND_TIMER), - check_support=lambda data: data.roborock_device_info.props.dnd_timer, - get_value=lambda data: data.coordinator.roborock_device_info.props.dnd_timer.enabled, + else cache.close_value(), + attribute="enabled", key="dnd_switch", translation_key="dnd_switch", icon="mdi:bell-cancel", @@ -120,114 +95,74 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Roborock switch platform.""" - coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ config_entry.entry_id ] possible_entities: list[ - tuple[str, RoborockDataUpdateCoordinator, RoborockSwitchDescription] + tuple[RoborockDataUpdateCoordinator, RoborockSwitchDescription] ] = [ - (device_id, coordinator, description) - for device_id, coordinator in coordinators.items() + (coordinator, description) + for coordinator in coordinators.values() for description in SWITCH_DESCRIPTIONS ] # We need to check if this function is supported by the device. results = await asyncio.gather( *( - description.check_support(coordinator) - for _, coordinator, description in possible_entities + coordinator.api.cache.get(description.cache_key).async_value() + for coordinator, description in possible_entities ), return_exceptions=True, ) - valid_entities: list[RoborockNonCoordinatedSwitchEntity] = [] - for posible_entity, result in zip(possible_entities, results): - if isinstance(result, Exception): - if not isinstance(result, RoborockException): - raise result + valid_entities: list[RoborockSwitch] = [] + for (coordinator, description), result in zip(possible_entities, results): + if result is None or isinstance(result, Exception): _LOGGER.debug("Not adding entity because of %s", result) else: valid_entities.append( - RoborockNonCoordinatedSwitchEntity( - f"{posible_entity[2].key}_{slugify(posible_entity[0])}", - posible_entity[1], - posible_entity[2], - result, + RoborockSwitch( + f"{description.key}_{slugify(coordinator.roborock_device_info.device.duid)}", + coordinator.device_info, + description, + coordinator.api, ) ) - async_add_entities( - valid_entities, - True, - ) - async_add_entities( - ( - RoborockCoordinatedSwitchEntity( - f"{description.key}_{slugify(device_id)}", - coordinator, - description, - ) - for device_id, coordinator in coordinators.items() - for description in COORDINATED_SWITCH_DESCRIPTION - if description.check_support(coordinator) is not None - ) - ) + async_add_entities(valid_entities) -class RoborockNonCoordinatedSwitchEntity(RoborockEntity, SwitchEntity): - """A class to let you turn functionality on Roborock devices on and off that does not need a coordinator.""" +class RoborockSwitch(RoborockEntity, SwitchEntity): + """A class to let you turn functionality on Roborock devices on and off that does need a coordinator.""" entity_description: RoborockSwitchDescription def __init__( self, unique_id: str, - coordinator: RoborockDataUpdateCoordinator, - entity_description: RoborockSwitchDescription, - initial_value: bool, + device_info: DeviceInfo, + description: RoborockSwitchDescription, + api: RoborockLocalClient, ) -> None: - """Create a switch entity.""" - self.entity_description = entity_description - super().__init__(unique_id, coordinator.device_info, coordinator.api) - self._attr_is_on = initial_value + """Initialize the entity.""" + super().__init__(unique_id, device_info, api) + self.entity_description = description async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" - await self.entity_description.set_command(self, False) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn on the switch.""" - await self.entity_description.set_command(self, True) - - async def async_update(self) -> None: - """Update switch.""" - self._attr_is_on = self.entity_description.evaluate_value( - await self.entity_description.get_value(self) + await self.entity_description.update_value( + self.get_cache(self.entity_description.cache_key), False ) - -class RoborockCoordinatedSwitchEntity(RoborockCoordinatedEntity, SwitchEntity): - """A class to let you turn functionality on Roborock devices on and off that does need a coordinator.""" - - entity_description: RoborockCoordinatedSwitchDescription - - def __init__( - self, - unique_id: str, - coordinator: RoborockDataUpdateCoordinator, - entity_description: RoborockCoordinatedSwitchDescription, - ) -> None: - """Create a switch entity.""" - self.entity_description = entity_description - super().__init__(unique_id, coordinator) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn off the switch.""" - await self.entity_description.set_command(self, False) - async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" - await self.entity_description.set_command(self, True) + await self.entity_description.update_value( + self.get_cache(self.entity_description.cache_key), True + ) @property def is_on(self) -> bool | None: - """Use the coordinator to determine if the switch is on.""" - return self.entity_description.get_value(self) + """Return True if entity is on.""" + return ( + self.get_cache(self.entity_description.cache_key).value.get( + self.entity_description.attribute + ) + == 1 + ) diff --git a/requirements_all.txt b/requirements_all.txt index aab14224dc9..851359306ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2139,7 +2139,7 @@ python-qbittorrent==0.4.3 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.23.6 +python-roborock==0.29.2 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2863b4c90f7..74612b98385 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1565,7 +1565,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.3 # homeassistant.components.roborock -python-roborock==0.23.6 +python-roborock==0.29.2 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index d9c11bead74..eb281076825 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -20,11 +20,17 @@ from tests.common import MockConfigEntry @pytest.fixture(name="bypass_api_fixture") def bypass_api_fixture() -> None: """Skip calls to the API.""" - with patch("homeassistant.components.roborock.RoborockMqttClient.connect"), patch( - "homeassistant.components.roborock.RoborockMqttClient.send_command" + with patch( + "homeassistant.components.roborock.RoborockMqttClient.async_connect" + ), patch( + "homeassistant.components.roborock.RoborockMqttClient._send_command" ), patch( "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", return_value=PROP, + ), patch( + "roborock.api.AttributeCache.async_value" + ), patch( + "roborock.api.AttributeCache.value" ): yield diff --git a/tests/components/roborock/mock_data.py b/tests/components/roborock/mock_data.py index 15e69cee9d9..6a2e1f4b5f1 100644 --- a/tests/components/roborock/mock_data.py +++ b/tests/components/roborock/mock_data.py @@ -367,7 +367,12 @@ STATUS = S7Status.from_dict( "unsave_map_flag": 0, } ) -PROP = DeviceProp(STATUS, DND_TIMER, CLEAN_SUMMARY, CONSUMABLE, CLEAN_RECORD) +PROP = DeviceProp( + status=STATUS, + clean_summary=CLEAN_SUMMARY, + consumable=CONSUMABLE, + last_clean_record=CLEAN_RECORD, +) NETWORK_INFO = NetworkInfo( ip="123.232.12.1", ssid="wifi", mac="ac:cc:cc:cc:cc", bssid="bssid", rssi=90 diff --git a/tests/components/roborock/snapshots/test_diagnostics.ambr b/tests/components/roborock/snapshots/test_diagnostics.ambr index 432bad167cd..eb70e04110f 100644 --- a/tests/components/roborock/snapshots/test_diagnostics.ambr +++ b/tests/components/roborock/snapshots/test_diagnostics.ambr @@ -221,21 +221,6 @@ 'sideBrushWorkTime': 74382, 'strainerWorkTimes': 65, }), - 'dndTimer': dict({ - 'enabled': 1, - 'endHour': 7, - 'endMinute': 0, - 'endTime': dict({ - '__type': "", - 'isoformat': '07:00:00', - }), - 'startHour': 22, - 'startMinute': 0, - 'startTime': dict({ - '__type': "", - 'isoformat': '22:00:00', - }), - }), 'lastCleanRecord': dict({ 'area': 20965000, 'avoidCount': 19, diff --git a/tests/components/roborock/test_select.py b/tests/components/roborock/test_select.py index 3b0ba8183b3..bcea4e6246b 100644 --- a/tests/components/roborock/test_select.py +++ b/tests/components/roborock/test_select.py @@ -26,6 +26,8 @@ async def test_update_success( value: str, ) -> None: """Test allowed changing values for select entities.""" + # Ensure that the entity exist, as these test can pass even if there is no entity. + assert hass.states.get(entity_id) is not None with patch( "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" ) as mock_send_message: diff --git a/tests/components/roborock/test_switch.py b/tests/components/roborock/test_switch.py index 9c079ef85b6..40ecdc267ed 100644 --- a/tests/components/roborock/test_switch.py +++ b/tests/components/roborock/test_switch.py @@ -2,11 +2,9 @@ from unittest.mock import patch import pytest -from roborock.exceptions import RoborockException from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from tests.common import MockConfigEntry @@ -26,6 +24,8 @@ async def test_update_success( entity_id: str, ) -> None: """Test turning switch entities on and off.""" + # Ensure that the entity exist, as these test can pass even if there is no entity. + assert hass.states.get(entity_id) is not None with patch( "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" ) as mock_send_message: @@ -48,22 +48,3 @@ async def test_update_success( target={"entity_id": entity_id}, ) assert mock_send_message.assert_called_once - - -async def test_update_failure( - hass: HomeAssistant, - bypass_api_fixture, - setup_entry: MockConfigEntry, -) -> None: - """Test that changing a value will raise a homeassistanterror when it fails.""" - with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message", - side_effect=RoborockException(), - ), pytest.raises(HomeAssistantError): - await hass.services.async_call( - "switch", - SERVICE_TURN_ON, - service_data=None, - blocking=True, - target={"entity_id": "switch.roborock_s7_maxv_child_lock"}, - ) From ee72a952dea271a06b990853c9afbc677ed34ad2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 16:40:35 +0200 Subject: [PATCH 030/184] Philips.js explicit device naming (#95551) --- homeassistant/components/philips_js/media_player.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index c6ca70bdc84..bdd55bb2dad 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -70,6 +70,7 @@ class PhilipsTVMediaPlayer( _attr_device_class = MediaPlayerDeviceClass.TV _attr_has_entity_name = True + _attr_name = None def __init__( self, From 9cd7034dbd351d7e5b4f7cae46f52eddae870c8e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jun 2023 13:11:17 -0400 Subject: [PATCH 031/184] Fix some entity naming (#95562) --- homeassistant/components/sonos/media_player.py | 1 + homeassistant/components/wled/light.py | 4 +++- homeassistant/components/xiaomi_miio/vacuum.py | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 7e6c210a164..526ddd2bcc7 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -190,6 +190,7 @@ async def async_setup_entry( class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Representation of a Sonos entity.""" + _attr_name = None _attr_supported_features = ( MediaPlayerEntityFeature.BROWSE_MEDIA | MediaPlayerEntityFeature.CLEAR_PLAYLIST diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 2c684013765..1eb8074bbc1 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -118,7 +118,9 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): # Segment 0 uses a simpler name, which is more natural for when using # a single segment / using WLED with one big LED strip. - if segment != 0: + if segment == 0: + self._attr_name = None + else: self._attr_name = f"Segment {segment}" self._attr_unique_id = ( diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 10fd1f2406b..34a7b949646 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -187,6 +187,7 @@ class MiroboVacuum( ): """Representation of a Xiaomi Vacuum cleaner robot.""" + _attr_name = None _attr_supported_features = ( VacuumEntityFeature.STATE | VacuumEntityFeature.PAUSE From bcca1c91e68cc1323c64ad44235dcbd0e8c4b6a5 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Thu, 29 Jun 2023 19:53:50 +0300 Subject: [PATCH 032/184] Fix Android TV Remote entity naming (#95568) Return None as Android TV Remote entity name --- homeassistant/components/androidtv_remote/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/androidtv_remote/entity.py b/homeassistant/components/androidtv_remote/entity.py index 862f317ee82..5a99805da62 100644 --- a/homeassistant/components/androidtv_remote/entity.py +++ b/homeassistant/components/androidtv_remote/entity.py @@ -16,6 +16,7 @@ from .const import DOMAIN class AndroidTVRemoteBaseEntity(Entity): """Android TV Remote Base Entity.""" + _attr_name = None _attr_has_entity_name = True _attr_should_poll = False From c47a43c2b9e33fe9086133c6d012989d1ff14d03 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 29 Jun 2023 19:18:24 +0200 Subject: [PATCH 033/184] Update frontend to 20230629.0 (#95570) --- 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 509983c25f6..891a97d4d02 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==20230628.0"] + "requirements": ["home-assistant-frontend==20230629.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7b3e7fe7c4b..c850bb6790b 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==20230628.0 +home-assistant-frontend==20230629.0 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 851359306ad..8b905cbe408 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==20230628.0 +home-assistant-frontend==20230629.0 # homeassistant.components.conversation home-assistant-intents==2023.6.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 74612b98385..bb50d6f1b57 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==20230628.0 +home-assistant-frontend==20230629.0 # homeassistant.components.conversation home-assistant-intents==2023.6.28 From 4255a2af15252c39b6a96faeca0622857a5513c9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jun 2023 13:43:13 -0400 Subject: [PATCH 034/184] Fix entity name for iBeacon and Roku (#95574) * Fix entity nmae for iBeacon and Roku * Roku remote too --- homeassistant/components/ibeacon/device_tracker.py | 2 ++ homeassistant/components/roku/media_player.py | 1 + homeassistant/components/roku/remote.py | 2 ++ 3 files changed, 5 insertions(+) diff --git a/homeassistant/components/ibeacon/device_tracker.py b/homeassistant/components/ibeacon/device_tracker.py index 4c9337e54ce..8e194ac27b1 100644 --- a/homeassistant/components/ibeacon/device_tracker.py +++ b/homeassistant/components/ibeacon/device_tracker.py @@ -48,6 +48,8 @@ async def async_setup_entry( class IBeaconTrackerEntity(IBeaconEntity, BaseTrackerEntity): """An iBeacon Tracker entity.""" + _attr_name = None + def __init__( self, coordinator: IBeaconCoordinator, diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 877e58233d5..a8c1cf4698c 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -108,6 +108,7 @@ async def async_setup_entry( class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): """Representation of a Roku media player on the network.""" + _attr_name = None _attr_supported_features = ( MediaPlayerEntityFeature.PREVIOUS_TRACK | MediaPlayerEntityFeature.NEXT_TRACK diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index fceac67a477..0271e4a0f73 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -37,6 +37,8 @@ async def async_setup_entry( class RokuRemote(RokuEntity, RemoteEntity): """Device that sends commands to an Roku.""" + _attr_name = None + @property def is_on(self) -> bool: """Return true if device is on.""" From a04c98a703d8713417617a3fdccfb6d2066cabf9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jun 2023 13:57:56 -0400 Subject: [PATCH 035/184] Bumped version to 2023.7.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 1c12103be19..1380734ec5c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 7 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 6acf9d4ea94..39f468f28e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.7.0b0" +version = "2023.7.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From b8fc6e0c662b441c1aef8009176e07ce42c77414 Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Thu, 29 Jun 2023 20:20:14 +0200 Subject: [PATCH 036/184] Ezviz IR string align with depreciation. (#95563) --- homeassistant/components/ezviz/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index 6f00568cf2b..5711aff2a4a 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -62,7 +62,7 @@ "issues": { "service_depreciation_detection_sensibility": { "title": "Ezviz Detection sensitivity service is being removed", - "description": "Ezviz Detection sensitivity service is deprecated and will be removed in Home Assistant 2023.8; Please adjust the automation or script that uses the service and select submit below to mark this issue as resolved." + "description": "Ezviz Detection sensitivity service is deprecated and will be removed in Home Assistant 2023.12; Please adjust the automation or script that uses the service and select submit below to mark this issue as resolved." } } } From 1cf472f4e34cb0c11335243201d4c51e9271ffc7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jun 2023 18:03:59 -0500 Subject: [PATCH 037/184] Bump bluetooth-data-tools to 1.3.0 (#95576) --- 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 2d96897ef9d..dbe8ac3f1ab 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -18,7 +18,7 @@ "bleak-retry-connector==3.0.2", "bluetooth-adapters==0.15.3", "bluetooth-auto-recovery==1.2.0", - "bluetooth-data-tools==1.2.0", + "bluetooth-data-tools==1.3.0", "dbus-fast==1.86.0" ] } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index de47c904dd7..404339d4f30 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "aioesphomeapi==15.0.0", - "bluetooth-data-tools==1.2.0", + "bluetooth-data-tools==1.3.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 b15eb343c06..6eaf2885d89 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.2.0", "ld2410-ble==0.1.1"] + "requirements": ["bluetooth-data-tools==1.3.0", "ld2410-ble==0.1.1"] } diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index b788ec21052..cdc270f2e99 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.2.0", "led-ble==1.0.0"] + "requirements": ["bluetooth-data-tools==1.3.0", "led-ble==1.0.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c850bb6790b..e6e506273ad 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ bleak-retry-connector==3.0.2 bleak==0.20.2 bluetooth-adapters==0.15.3 bluetooth-auto-recovery==1.2.0 -bluetooth-data-tools==1.2.0 +bluetooth-data-tools==1.3.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 8b905cbe408..f4bd9865a77 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -537,7 +537,7 @@ bluetooth-auto-recovery==1.2.0 # homeassistant.components.esphome # homeassistant.components.ld2410_ble # homeassistant.components.led_ble -bluetooth-data-tools==1.2.0 +bluetooth-data-tools==1.3.0 # homeassistant.components.bond bond-async==0.1.23 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb50d6f1b57..5d216e6c844 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -448,7 +448,7 @@ bluetooth-auto-recovery==1.2.0 # homeassistant.components.esphome # homeassistant.components.ld2410_ble # homeassistant.components.led_ble -bluetooth-data-tools==1.2.0 +bluetooth-data-tools==1.3.0 # homeassistant.components.bond bond-async==0.1.23 From 6aba1a5af3ea97c4c7db24e200f56225c88586c1 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Thu, 29 Jun 2023 22:52:48 -0300 Subject: [PATCH 038/184] Fix device source for Utility Meter (#95585) * Fix Device Source * Remove debug --- homeassistant/components/utility_meter/sensor.py | 1 + tests/components/utility_meter/test_sensor.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 5f426fc49c5..f52b78b5a52 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -142,6 +142,7 @@ async def async_setup_entry( ): device_info = DeviceInfo( identifiers=device.identifiers, + connections=device.connections, ) else: device_info = None diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 1e26d5e211a..5cb9e594cb2 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -1469,6 +1469,7 @@ async def test_device_id(hass: HomeAssistant) -> None: source_device_entry = device_registry.async_get_or_create( config_entry_id=source_config_entry.entry_id, identifiers={("sensor", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, ) source_entity = entity_registry.async_get_or_create( "sensor", From c67f37d1bec51b8551ec3081e042e5d3dc50b0d9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jun 2023 22:20:53 -0400 Subject: [PATCH 039/184] Wiz set name explicitely to None (#95593) --- homeassistant/components/wiz/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/wiz/light.py b/homeassistant/components/wiz/light.py index 084acbe73e7..216c5d9335e 100644 --- a/homeassistant/components/wiz/light.py +++ b/homeassistant/components/wiz/light.py @@ -71,6 +71,8 @@ async def async_setup_entry( class WizBulbEntity(WizToggleEntity, LightEntity): """Representation of WiZ Light bulb.""" + _attr_name = None + def __init__(self, wiz_data: WizData, name: str) -> None: """Initialize an WiZLight.""" super().__init__(wiz_data, name) From 03e7170080ca40e7f7f66ee8c729d00096470820 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 29 Jun 2023 22:20:33 -0400 Subject: [PATCH 040/184] Fix ZHA multi-PAN startup issue (#95595) Bump ZHA dependencies --- 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 fa1c382926e..d7acc9788c4 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -20,7 +20,7 @@ "zigpy_znp" ], "requirements": [ - "bellows==0.35.6", + "bellows==0.35.7", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.101", diff --git a/requirements_all.txt b/requirements_all.txt index f4bd9865a77..749a064f952 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -497,7 +497,7 @@ beautifulsoup4==4.11.1 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.35.6 +bellows==0.35.7 # homeassistant.components.bmw_connected_drive bimmer-connected==0.13.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d216e6c844..5761609b4b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -418,7 +418,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.35.6 +bellows==0.35.7 # homeassistant.components.bmw_connected_drive bimmer-connected==0.13.7 From 0e89d0a26b2e70bb3fced70c8b9de651f2639a45 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jun 2023 22:21:39 -0400 Subject: [PATCH 041/184] Bumped version to 2023.7.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 1380734ec5c..ad465026058 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 7 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 39f468f28e4..4725c939693 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.7.0b1" +version = "2023.7.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 97d9876cf7f96d10ae48b096449a5eb8565e698b Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 29 Jun 2023 20:00:17 -0700 Subject: [PATCH 042/184] Google Assistant SDK: Always enable conversation agent and support multiple languages (#93201) * Enable agent and support multiple languages * fix test --- .../google_assistant_sdk/__init__.py | 39 ++------- .../google_assistant_sdk/config_flow.py | 14 +-- .../components/google_assistant_sdk/const.py | 1 - .../google_assistant_sdk/strings.json | 4 +- .../google_assistant_sdk/test_config_flow.py | 44 ++-------- .../google_assistant_sdk/test_init.py | 87 ++++++++++--------- 6 files changed, 67 insertions(+), 122 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index e2791f6000f..db2a8d9512e 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -18,18 +18,11 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( ) from homeassistant.helpers.typing import ConfigType -from .const import ( - CONF_ENABLE_CONVERSATION_AGENT, - CONF_LANGUAGE_CODE, - DATA_MEM_STORAGE, - DATA_SESSION, - DOMAIN, -) +from .const import DATA_MEM_STORAGE, DATA_SESSION, DOMAIN, SUPPORTED_LANGUAGE_CODES from .helpers import ( GoogleAssistantSDKAudioView, InMemoryStorage, async_send_text_commands, - default_language_code, ) SERVICE_SEND_TEXT_COMMAND = "send_text_command" @@ -82,8 +75,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_setup_service(hass) - entry.async_on_unload(entry.add_update_listener(update_listener)) - await update_listener(hass, entry) + agent = GoogleAssistantConversationAgent(hass, entry) + conversation.async_set_agent(hass, entry, agent) return True @@ -100,8 +93,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for service_name in hass.services.async_services()[DOMAIN]: hass.services.async_remove(DOMAIN, service_name) - if entry.options.get(CONF_ENABLE_CONVERSATION_AGENT, False): - conversation.async_unset_agent(hass, entry) + conversation.async_unset_agent(hass, entry) return True @@ -125,15 +117,6 @@ async def async_setup_service(hass: HomeAssistant) -> None: ) -async def update_listener(hass, entry): - """Handle options update.""" - if entry.options.get(CONF_ENABLE_CONVERSATION_AGENT, False): - agent = GoogleAssistantConversationAgent(hass, entry) - conversation.async_set_agent(hass, entry, agent) - else: - conversation.async_unset_agent(hass, entry) - - class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): """Google Assistant SDK conversation agent.""" @@ -143,6 +126,7 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): self.entry = entry self.assistant: TextAssistant | None = None self.session: OAuth2Session | None = None + self.language: str | None = None @property def attribution(self): @@ -155,10 +139,7 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): @property def supported_languages(self) -> list[str]: """Return a list of supported languages.""" - language_code = self.entry.options.get( - CONF_LANGUAGE_CODE, default_language_code(self.hass) - ) - return [language_code] + return SUPPORTED_LANGUAGE_CODES async def async_process( self, user_input: conversation.ConversationInput @@ -172,12 +153,10 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): if not session.valid_token: await session.async_ensure_token_valid() self.assistant = None - if not self.assistant: + if not self.assistant or user_input.language != self.language: credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) - language_code = self.entry.options.get( - CONF_LANGUAGE_CODE, default_language_code(self.hass) - ) - self.assistant = TextAssistant(credentials, language_code) + self.language = user_input.language + self.assistant = TextAssistant(credentials, self.language) resp = self.assistant.assist(user_input.text) text_response = resp[0] or "" diff --git a/homeassistant/components/google_assistant_sdk/config_flow.py b/homeassistant/components/google_assistant_sdk/config_flow.py index b93a3be93f2..b4f617ca029 100644 --- a/homeassistant/components/google_assistant_sdk/config_flow.py +++ b/homeassistant/components/google_assistant_sdk/config_flow.py @@ -13,13 +13,7 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow -from .const import ( - CONF_ENABLE_CONVERSATION_AGENT, - CONF_LANGUAGE_CODE, - DEFAULT_NAME, - DOMAIN, - SUPPORTED_LANGUAGE_CODES, -) +from .const import CONF_LANGUAGE_CODE, DEFAULT_NAME, DOMAIN, SUPPORTED_LANGUAGE_CODES from .helpers import default_language_code _LOGGER = logging.getLogger(__name__) @@ -114,12 +108,6 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_LANGUAGE_CODE, default=self.config_entry.options.get(CONF_LANGUAGE_CODE), ): vol.In(SUPPORTED_LANGUAGE_CODES), - vol.Required( - CONF_ENABLE_CONVERSATION_AGENT, - default=self.config_entry.options.get( - CONF_ENABLE_CONVERSATION_AGENT - ), - ): bool, } ), ) diff --git a/homeassistant/components/google_assistant_sdk/const.py b/homeassistant/components/google_assistant_sdk/const.py index c9f86160bb4..d63aec0ebd5 100644 --- a/homeassistant/components/google_assistant_sdk/const.py +++ b/homeassistant/components/google_assistant_sdk/const.py @@ -5,7 +5,6 @@ DOMAIN: Final = "google_assistant_sdk" DEFAULT_NAME: Final = "Google Assistant SDK" -CONF_ENABLE_CONVERSATION_AGENT: Final = "enable_conversation_agent" CONF_LANGUAGE_CODE: Final = "language_code" DATA_MEM_STORAGE: Final = "mem_storage" diff --git a/homeassistant/components/google_assistant_sdk/strings.json b/homeassistant/components/google_assistant_sdk/strings.json index d4c85be91e5..66a2b975b5e 100644 --- a/homeassistant/components/google_assistant_sdk/strings.json +++ b/homeassistant/components/google_assistant_sdk/strings.json @@ -31,10 +31,8 @@ "step": { "init": { "data": { - "enable_conversation_agent": "Enable the conversation agent", "language_code": "Language code" - }, - "description": "Set language for interactions with Google Assistant and whether you want to enable the conversation agent." + } } } }, diff --git a/tests/components/google_assistant_sdk/test_config_flow.py b/tests/components/google_assistant_sdk/test_config_flow.py index 1d350a8fe4f..c65477b18b1 100644 --- a/tests/components/google_assistant_sdk/test_config_flow.py +++ b/tests/components/google_assistant_sdk/test_config_flow.py @@ -223,65 +223,39 @@ async def test_options_flow( assert result["type"] == "form" assert result["step_id"] == "init" data_schema = result["data_schema"].schema - assert set(data_schema) == {"enable_conversation_agent", "language_code"} + assert set(data_schema) == {"language_code"} result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"enable_conversation_agent": False, "language_code": "es-ES"}, + user_input={"language_code": "es-ES"}, ) assert result["type"] == "create_entry" - assert config_entry.options == { - "enable_conversation_agent": False, - "language_code": "es-ES", - } + assert config_entry.options == {"language_code": "es-ES"} # Retrigger options flow, not change language result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == "form" assert result["step_id"] == "init" data_schema = result["data_schema"].schema - assert set(data_schema) == {"enable_conversation_agent", "language_code"} + assert set(data_schema) == {"language_code"} result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"enable_conversation_agent": False, "language_code": "es-ES"}, + user_input={"language_code": "es-ES"}, ) assert result["type"] == "create_entry" - assert config_entry.options == { - "enable_conversation_agent": False, - "language_code": "es-ES", - } + assert config_entry.options == {"language_code": "es-ES"} # Retrigger options flow, change language result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == "form" assert result["step_id"] == "init" data_schema = result["data_schema"].schema - assert set(data_schema) == {"enable_conversation_agent", "language_code"} + assert set(data_schema) == {"language_code"} result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"enable_conversation_agent": False, "language_code": "en-US"}, + user_input={"language_code": "en-US"}, ) assert result["type"] == "create_entry" - assert config_entry.options == { - "enable_conversation_agent": False, - "language_code": "en-US", - } - - # Retrigger options flow, enable conversation agent - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == "form" - assert result["step_id"] == "init" - data_schema = result["data_schema"].schema - assert set(data_schema) == {"enable_conversation_agent", "language_code"} - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={"enable_conversation_agent": True, "language_code": "en-US"}, - ) - assert result["type"] == "create_entry" - assert config_entry.options == { - "enable_conversation_agent": True, - "language_code": "en-US", - } + assert config_entry.options == {"language_code": "en-US"} diff --git a/tests/components/google_assistant_sdk/test_init.py b/tests/components/google_assistant_sdk/test_init.py index 25066f73b6d..99f264e4a3a 100644 --- a/tests/components/google_assistant_sdk/test_init.py +++ b/tests/components/google_assistant_sdk/test_init.py @@ -9,8 +9,9 @@ import pytest from homeassistant.components import conversation from homeassistant.components.google_assistant_sdk import DOMAIN +from homeassistant.components.google_assistant_sdk.const import SUPPORTED_LANGUAGE_CODES from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant +from homeassistant.core import Context, HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -29,13 +30,9 @@ async def fetch_api_url(hass_client, url): return response.status, contents -@pytest.mark.parametrize( - "enable_conversation_agent", [False, True], ids=["", "enable_conversation_agent"] -) async def test_setup_success( hass: HomeAssistant, setup_integration: ComponentSetup, - enable_conversation_agent: bool, ) -> None: """Test successful setup and unload.""" await setup_integration() @@ -44,12 +41,6 @@ async def test_setup_success( assert len(entries) == 1 assert entries[0].state is ConfigEntryState.LOADED - if enable_conversation_agent: - hass.config_entries.async_update_entry( - entries[0], options={"enable_conversation_agent": True} - ) - await hass.async_block_till_done() - await hass.config_entries.async_unload(entries[0].entry_id) await hass.async_block_till_done() @@ -333,30 +324,21 @@ async def test_conversation_agent( assert len(entries) == 1 entry = entries[0] assert entry.state is ConfigEntryState.LOADED - hass.config_entries.async_update_entry( - entry, options={"enable_conversation_agent": True} - ) - await hass.async_block_till_done() agent = await conversation._get_agent_manager(hass).async_get_agent(entry.entry_id) - assert agent.supported_languages == ["en-US"] + assert agent.attribution.keys() == {"name", "url"} + assert agent.supported_languages == SUPPORTED_LANGUAGE_CODES text1 = "tell me a joke" text2 = "tell me another one" with patch( "homeassistant.components.google_assistant_sdk.TextAssistant" ) as mock_text_assistant: - await hass.services.async_call( - "conversation", - "process", - {"text": text1, "agent_id": config_entry.entry_id}, - blocking=True, + await conversation.async_converse( + hass, text1, None, Context(), "en-US", config_entry.entry_id ) - await hass.services.async_call( - "conversation", - "process", - {"text": text2, "agent_id": config_entry.entry_id}, - blocking=True, + await conversation.async_converse( + hass, text2, None, Context(), "en-US", config_entry.entry_id ) # Assert constructor is called only once since it's reused across requests @@ -381,21 +363,14 @@ async def test_conversation_agent_refresh_token( assert len(entries) == 1 entry = entries[0] assert entry.state is ConfigEntryState.LOADED - hass.config_entries.async_update_entry( - entry, options={"enable_conversation_agent": True} - ) - await hass.async_block_till_done() text1 = "tell me a joke" text2 = "tell me another one" with patch( "homeassistant.components.google_assistant_sdk.TextAssistant" ) as mock_text_assistant: - await hass.services.async_call( - "conversation", - "process", - {"text": text1, "agent_id": config_entry.entry_id}, - blocking=True, + await conversation.async_converse( + hass, text1, None, Context(), "en-US", config_entry.entry_id ) # Expire the token between requests @@ -411,11 +386,8 @@ async def test_conversation_agent_refresh_token( }, ) - await hass.services.async_call( - "conversation", - "process", - {"text": text2, "agent_id": config_entry.entry_id}, - blocking=True, + await conversation.async_converse( + hass, text2, None, Context(), "en-US", config_entry.entry_id ) # Assert constructor is called twice since the token was expired @@ -426,3 +398,38 @@ async def test_conversation_agent_refresh_token( ) mock_text_assistant.assert_has_calls([call().assist(text1)]) mock_text_assistant.assert_has_calls([call().assist(text2)]) + + +async def test_conversation_agent_language_changed( + hass: HomeAssistant, + config_entry: MockConfigEntry, + setup_integration: ComponentSetup, +) -> None: + """Test GoogleAssistantConversationAgent when language is changed.""" + await setup_integration() + + assert await async_setup_component(hass, "conversation", {}) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.state is ConfigEntryState.LOADED + + text1 = "tell me a joke" + text2 = "cuéntame un chiste" + with patch( + "homeassistant.components.google_assistant_sdk.TextAssistant" + ) as mock_text_assistant: + await conversation.async_converse( + hass, text1, None, Context(), "en-US", config_entry.entry_id + ) + await conversation.async_converse( + hass, text2, None, Context(), "es-ES", config_entry.entry_id + ) + + # Assert constructor is called twice since the language was changed + assert mock_text_assistant.call_count == 2 + mock_text_assistant.assert_has_calls([call(ExpectedCredentials(), "en-US")]) + mock_text_assistant.assert_has_calls([call(ExpectedCredentials(), "es-ES")]) + mock_text_assistant.assert_has_calls([call().assist(text1)]) + mock_text_assistant.assert_has_calls([call().assist(text2)]) From a9b8e0077d4b91bae9f04be854dc557559fb4638 Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 29 Jun 2023 19:55:51 -0700 Subject: [PATCH 043/184] Bump google-generativeai to 0.1.0 (#95515) --- .../components/google_generative_ai_conversation/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/manifest.json b/homeassistant/components/google_generative_ai_conversation/manifest.json index 52de9215535..65d9e0b3894 100644 --- a/homeassistant/components/google_generative_ai_conversation/manifest.json +++ b/homeassistant/components/google_generative_ai_conversation/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/google_generative_ai_conversation", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["google-generativeai==0.1.0rc2"] + "requirements": ["google-generativeai==0.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 749a064f952..bc0d3208b35 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -880,7 +880,7 @@ google-cloud-pubsub==2.13.11 google-cloud-texttospeech==2.12.3 # homeassistant.components.google_generative_ai_conversation -google-generativeai==0.1.0rc2 +google-generativeai==0.1.0 # homeassistant.components.nest google-nest-sdm==2.2.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5761609b4b3..57e7adf7964 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -690,7 +690,7 @@ google-api-python-client==2.71.0 google-cloud-pubsub==2.13.11 # homeassistant.components.google_generative_ai_conversation -google-generativeai==0.1.0rc2 +google-generativeai==0.1.0 # homeassistant.components.nest google-nest-sdm==2.2.5 From a4449d39f352d0094b16d56eb065807a6f3c947c Mon Sep 17 00:00:00 2001 From: "Dr. Drinovac" <52541649+RobertD502@users.noreply.github.com> Date: Fri, 30 Jun 2023 08:48:20 -0400 Subject: [PATCH 044/184] Use explicit naming in Sensibo climate entity (#95591) * Use explicit naming in Sensibo climate entity * Fix black --------- Co-authored-by: G Johansson --- homeassistant/components/sensibo/climate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 9afdff43ef0..4ff63a25455 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -163,6 +163,8 @@ async def async_setup_entry( class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): """Representation of a Sensibo device.""" + _attr_name = None + def __init__( self, coordinator: SensiboDataUpdateCoordinator, device_id: str ) -> None: From 26d171fc925268c926463365402c339925860ed0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jun 2023 10:21:10 -0500 Subject: [PATCH 045/184] Handle DNSError during radio browser setup (#95597) ``` 2023-06-29 08:11:06.034 ERROR (MainThread) [homeassistant.config_entries] Error setting up entry Radio Browser for radio_browser Traceback (most recent call last): File "/usr/src/homeassistant/homeassistant/config_entries.py", line 390, in async_setup result = await component.async_setup_entry(hass, self) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/src/homeassistant/homeassistant/components/radio_browser/__init__.py", line 25, in async_setup_entry await radios.stats() File "/usr/local/lib/python3.11/site-packages/radios/radio_browser.py", line 124, in stats response = await self._request("stats") ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.11/site-packages/backoff/_async.py", line 151, in retry ret = await target(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.11/site-packages/radios/radio_browser.py", line 73, in _request result = await resolver.query("_api._tcp.radio-browser.info", "SRV") ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ aiodns.error.DNSError: (12, 'Timeout while contacting DNS servers') ``` --- homeassistant/components/radio_browser/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/radio_browser/__init__.py b/homeassistant/components/radio_browser/__init__.py index d93d7c48823..fdd7537e9e1 100644 --- a/homeassistant/components/radio_browser/__init__.py +++ b/homeassistant/components/radio_browser/__init__.py @@ -1,6 +1,7 @@ """The Radio Browser integration.""" from __future__ import annotations +from aiodns.error import DNSError from radios import RadioBrowser, RadioBrowserError from homeassistant.config_entries import ConfigEntry @@ -23,7 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await radios.stats() - except RadioBrowserError as err: + except (DNSError, RadioBrowserError) as err: raise ConfigEntryNotReady("Could not connect to Radio Browser API") from err hass.data[DOMAIN] = radios From 73cb17cbf5ed79eb46594b7f7a21e25167f4ad65 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 1 Jul 2023 03:52:52 +1000 Subject: [PATCH 046/184] Fix Diagnostics in Advantage Air (#95608) * Fix diag paths * Fix key sand add redactions * Name things better. * Add super basic test * Rename docstring * Add snapshot --------- Co-authored-by: Paulus Schoutsen --- .../components/advantage_air/diagnostics.py | 19 +- .../snapshots/test_diagnostics.ambr | 292 ++++++++++++++++++ .../advantage_air/test_diagnostics.py | 32 ++ 3 files changed, 340 insertions(+), 3 deletions(-) create mode 100644 tests/components/advantage_air/snapshots/test_diagnostics.ambr create mode 100644 tests/components/advantage_air/test_diagnostics.py diff --git a/homeassistant/components/advantage_air/diagnostics.py b/homeassistant/components/advantage_air/diagnostics.py index 27eaef09b43..4c440610838 100644 --- a/homeassistant/components/advantage_air/diagnostics.py +++ b/homeassistant/components/advantage_air/diagnostics.py @@ -9,17 +9,30 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN -TO_REDACT = ["dealerPhoneNumber", "latitude", "logoPIN", "longitude", "postCode"] +TO_REDACT = [ + "dealerPhoneNumber", + "latitude", + "logoPIN", + "longitude", + "postCode", + "rid", + "deviceNames", + "deviceIds", + "deviceIdsV2", + "backupId", +] async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]["coordinator"].data + data = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id].coordinator.data # Return only the relevant children return { - "aircons": data["aircons"], + "aircons": data.get("aircons"), + "myLights": data.get("myLights"), + "myThings": data.get("myThings"), "system": async_redact_data(data["system"], TO_REDACT), } diff --git a/tests/components/advantage_air/snapshots/test_diagnostics.ambr b/tests/components/advantage_air/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..a472d4fa1fc --- /dev/null +++ b/tests/components/advantage_air/snapshots/test_diagnostics.ambr @@ -0,0 +1,292 @@ +# serializer version: 1 +# name: test_select_async_setup_entry + dict({ + 'aircons': dict({ + 'ac1': dict({ + 'info': dict({ + 'aaAutoFanModeEnabled': False, + 'climateControlModeEnabled': False, + 'climateControlModeIsRunning': False, + 'countDownToOff': 10, + 'countDownToOn': 0, + 'fan': 'high', + 'filterCleanStatus': 0, + 'freshAirStatus': 'off', + 'mode': 'vent', + 'myAutoModeEnabled': False, + 'myAutoModeIsRunning': False, + 'myZone': 1, + 'name': 'myzone', + 'setTemp': 24, + 'state': 'on', + }), + 'zones': dict({ + 'z01': dict({ + 'error': 0, + 'maxDamper': 100, + 'measuredTemp': 25, + 'minDamper': 0, + 'motion': 20, + 'motionConfig': 2, + 'name': 'Zone open with Sensor', + 'number': 1, + 'rssi': 40, + 'setTemp': 24, + 'state': 'open', + 'type': 1, + 'value': 100, + }), + 'z02': dict({ + 'error': 0, + 'maxDamper': 100, + 'measuredTemp': 25, + 'minDamper': 0, + 'motion': 21, + 'motionConfig': 2, + 'name': 'Zone closed with Sensor', + 'number': 2, + 'rssi': 10, + 'setTemp': 24, + 'state': 'close', + 'type': 1, + 'value': 0, + }), + 'z03': dict({ + 'error': 0, + 'maxDamper': 100, + 'measuredTemp': 25, + 'minDamper': 0, + 'motion': 22, + 'motionConfig': 2, + 'name': 'Zone 3', + 'number': 3, + 'rssi': 25, + 'setTemp': 24, + 'state': 'close', + 'type': 1, + 'value': 0, + }), + 'z04': dict({ + 'error': 0, + 'maxDamper': 100, + 'measuredTemp': 25, + 'minDamper': 0, + 'motion': 1, + 'motionConfig': 1, + 'name': 'Zone 4', + 'number': 4, + 'rssi': 75, + 'setTemp': 24, + 'state': 'close', + 'type': 1, + 'value': 0, + }), + 'z05': dict({ + 'error': 0, + 'maxDamper': 100, + 'measuredTemp': 25, + 'minDamper': 0, + 'motion': 5, + 'motionConfig': 1, + 'name': 'Zone 5', + 'number': 5, + 'rssi': 100, + 'setTemp': 24, + 'state': 'close', + 'type': 1, + 'value': 0, + }), + }), + }), + 'ac2': dict({ + 'info': dict({ + 'aaAutoFanModeEnabled': True, + 'climateControlModeEnabled': True, + 'climateControlModeIsRunning': False, + 'countDownToOff': 0, + 'countDownToOn': 20, + 'fan': 'autoAA', + 'filterCleanStatus': 1, + 'freshAirStatus': 'none', + 'mode': 'cool', + 'myAutoModeCurrentSetMode': 'cool', + 'myAutoModeEnabled': False, + 'myAutoModeIsRunning': False, + 'myZone': 1, + 'name': 'mytemp', + 'setTemp': 24, + 'state': 'off', + }), + 'zones': dict({ + 'z01': dict({ + 'error': 0, + 'maxDamper': 100, + 'measuredTemp': 25, + 'minDamper': 0, + 'motion': 20, + 'motionConfig': 2, + 'name': 'Zone A', + 'number': 1, + 'rssi': 40, + 'setTemp': 24, + 'state': 'open', + 'type': 1, + 'value': 100, + }), + 'z02': dict({ + 'error': 0, + 'maxDamper': 100, + 'measuredTemp': 26, + 'minDamper': 0, + 'motion': 21, + 'motionConfig': 2, + 'name': 'Zone B', + 'number': 2, + 'rssi': 10, + 'setTemp': 23, + 'state': 'open', + 'type': 1, + 'value': 50, + }), + }), + }), + 'ac3': dict({ + 'info': dict({ + 'aaAutoFanModeEnabled': True, + 'climateControlModeEnabled': False, + 'climateControlModeIsRunning': False, + 'countDownToOff': 0, + 'countDownToOn': 0, + 'fan': 'autoAA', + 'filterCleanStatus': 1, + 'freshAirStatus': 'none', + 'mode': 'myauto', + 'myAutoCoolTargetTemp': 24, + 'myAutoHeatTargetTemp': 20, + 'myAutoModeCurrentSetMode': 'cool', + 'myAutoModeEnabled': True, + 'myAutoModeIsRunning': True, + 'myZone': 0, + 'name': 'myauto', + 'setTemp': 24, + 'state': 'on', + }), + 'zones': dict({ + 'z01': dict({ + 'error': 0, + 'maxDamper': 100, + 'measuredTemp': 0, + 'minDamper': 0, + 'motion': 0, + 'motionConfig': 0, + 'name': 'Zone Y', + 'number': 1, + 'rssi': 0, + 'setTemp': 24, + 'state': 'open', + 'type': 0, + 'value': 100, + }), + 'z02': dict({ + 'error': 0, + 'maxDamper': 100, + 'measuredTemp': 0, + 'minDamper': 0, + 'motion': 0, + 'motionConfig': 0, + 'name': 'Zone Z', + 'number': 2, + 'rssi': 0, + 'setTemp': 24, + 'state': 'close', + 'type': 0, + 'value': 0, + }), + }), + }), + }), + 'myLights': dict({ + 'lights': dict({ + '100': dict({ + 'id': '100', + 'moduleType': 'RM2', + 'name': 'Light A', + 'relay': True, + 'state': 'off', + }), + '101': dict({ + 'id': '101', + 'name': 'Light B', + 'state': 'on', + 'value': 50, + }), + }), + }), + 'myThings': dict({ + 'things': dict({ + '200': dict({ + 'buttonType': 'upDown', + 'channelDipState': 1, + 'id': '200', + 'name': 'Blind 1', + 'value': 100, + }), + '201': dict({ + 'buttonType': 'upDown', + 'channelDipState': 2, + 'id': '201', + 'name': 'Blind 2', + 'value': 0, + }), + '202': dict({ + 'buttonType': 'openClose', + 'channelDipState': 3, + 'id': '202', + 'name': 'Garage', + 'value': 100, + }), + '203': dict({ + 'buttonType': 'onOff', + 'channelDipState': 4, + 'id': '203', + 'name': 'Thing Light', + 'value': 100, + }), + '204': dict({ + 'buttonType': 'upDown', + 'channelDipState': 5, + 'id': '204', + 'name': 'Thing Light Dimmable', + 'value': 100, + }), + '205': dict({ + 'buttonType': 'onOff', + 'channelDipState': 8, + 'id': '205', + 'name': 'Relay', + 'value': 100, + }), + '206': dict({ + 'buttonType': 'onOff', + 'channelDipState': 9, + 'id': '206', + 'name': 'Fan', + 'value': 100, + }), + }), + }), + 'system': dict({ + 'hasAircons': True, + 'hasLights': True, + 'hasSensors': False, + 'hasThings': True, + 'hasThingsBOG': False, + 'hasThingsLight': False, + 'myAppRev': '1.234', + 'name': 'testname', + 'needsUpdate': False, + 'rid': '**REDACTED**', + 'sysType': 'e-zone', + }), + }) +# --- diff --git a/tests/components/advantage_air/test_diagnostics.py b/tests/components/advantage_air/test_diagnostics.py new file mode 100644 index 00000000000..ebd026c6cc7 --- /dev/null +++ b/tests/components/advantage_air/test_diagnostics.py @@ -0,0 +1,32 @@ +"""Test the Advantage Air Diagnostics.""" +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from . import ( + TEST_SYSTEM_DATA, + TEST_SYSTEM_URL, + add_mock_config, +) + +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.test_util.aiohttp import AiohttpClientMocker +from tests.typing import ClientSessionGenerator + + +async def test_select_async_setup_entry( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + snapshot: SnapshotAssertion, +) -> None: + """Test select platform.""" + + aioclient_mock.get( + TEST_SYSTEM_URL, + text=TEST_SYSTEM_DATA, + ) + + entry = await add_mock_config(hass) + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + assert diag == snapshot From a8807f600b4dbba35ab7412ac79aec4a739a5c8a Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 30 Jun 2023 19:37:57 +1000 Subject: [PATCH 047/184] Explicity use device name in Advantage Air (#95611) Explicity use device name --- homeassistant/components/advantage_air/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index 6170bd165e9..fa9f609ba10 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -90,6 +90,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): _attr_target_temperature_step = PRECISION_WHOLE _attr_max_temp = 32 _attr_min_temp = 16 + _attr_name = None _attr_hvac_modes = [ HVACMode.OFF, From 70d3dc2ef85d1a35020873d4af4cbc5cb1c90c79 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 30 Jun 2023 17:33:50 +0200 Subject: [PATCH 048/184] Fix explicit device naming for integrations a-j (#95619) Fix explicit device naming for a-j --- homeassistant/components/abode/alarm_control_panel.py | 1 + homeassistant/components/abode/camera.py | 1 + homeassistant/components/abode/cover.py | 1 + homeassistant/components/abode/light.py | 1 + homeassistant/components/abode/lock.py | 1 + homeassistant/components/abode/sensor.py | 1 - homeassistant/components/abode/switch.py | 1 + homeassistant/components/advantage_air/entity.py | 2 ++ homeassistant/components/advantage_air/light.py | 1 + homeassistant/components/anthemav/media_player.py | 1 + homeassistant/components/broadlink/switch.py | 1 + homeassistant/components/brottsplatskartan/sensor.py | 1 + homeassistant/components/bsblan/climate.py | 1 + .../components/devolo_home_control/devolo_multi_level_switch.py | 2 ++ homeassistant/components/devolo_home_control/switch.py | 2 ++ homeassistant/components/elgato/light.py | 1 + homeassistant/components/homewizard/switch.py | 1 + homeassistant/components/honeywell/climate.py | 1 + homeassistant/components/jellyfin/media_player.py | 1 + homeassistant/components/jellyfin/sensor.py | 1 + homeassistant/components/jvc_projector/remote.py | 2 ++ 21 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index 2546f762912..66a2e3b0db5 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -34,6 +34,7 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity): """An alarm_control_panel implementation for Abode.""" _attr_icon = ICON + _attr_name = None _attr_code_arm_required = False _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index 17d7b820d45..afe017bfcc7 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -39,6 +39,7 @@ class AbodeCamera(AbodeDevice, Camera): """Representation of an Abode camera.""" _device: AbodeCam + _attr_name = None def __init__(self, data: AbodeSystem, device: AbodeDev, event: Event) -> None: """Initialize the Abode device.""" diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index 507b1284362..d504040ee90 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -29,6 +29,7 @@ class AbodeCover(AbodeDevice, CoverEntity): """Representation of an Abode cover.""" _device: AbodeCV + _attr_name = None @property def is_closed(self) -> bool: diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index be69897431f..539b89a5546 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -42,6 +42,7 @@ class AbodeLight(AbodeDevice, LightEntity): """Representation of an Abode light.""" _device: AbodeLT + _attr_name = None def turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index 039b2423099..c110b3fd558 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -29,6 +29,7 @@ class AbodeLock(AbodeDevice, LockEntity): """Representation of an Abode lock.""" _device: AbodeLK + _attr_name = None def lock(self, **kwargs: Any) -> None: """Lock the device.""" diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index 11821773938..1e238783221 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -53,7 +53,6 @@ class AbodeSensor(AbodeDevice, SensorEntity): """A sensor implementation for Abode devices.""" _device: AbodeSense - _attr_has_entity_name = True def __init__( self, diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index ab83e3a20c1..14bdf4e0caf 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -44,6 +44,7 @@ class AbodeSwitch(AbodeDevice, SwitchEntity): """Representation of an Abode switch.""" _device: AbodeSW + _attr_name = None def turn_on(self, **kwargs: Any) -> None: """Turn on the device.""" diff --git a/homeassistant/components/advantage_air/entity.py b/homeassistant/components/advantage_air/entity.py index bbc8738c4ae..9e4f92e8c98 100644 --- a/homeassistant/components/advantage_air/entity.py +++ b/homeassistant/components/advantage_air/entity.py @@ -84,6 +84,8 @@ class AdvantageAirZoneEntity(AdvantageAirAcEntity): class AdvantageAirThingEntity(AdvantageAirEntity): """Parent class for Advantage Air Things Entities.""" + _attr_name = None + def __init__(self, instance: AdvantageAirData, thing: dict[str, Any]) -> None: """Initialize common aspects of an Advantage Air Things entity.""" super().__init__(instance) diff --git a/homeassistant/components/advantage_air/light.py b/homeassistant/components/advantage_air/light.py index 13a77d5cab3..7815354dd92 100644 --- a/homeassistant/components/advantage_air/light.py +++ b/homeassistant/components/advantage_air/light.py @@ -41,6 +41,7 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity): """Representation of Advantage Air Light.""" _attr_supported_color_modes = {ColorMode.ONOFF} + _attr_name = None def __init__(self, instance: AdvantageAirData, light: dict[str, Any]) -> None: """Initialize an Advantage Air Light.""" diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index 2ab23ff2d37..038e71750dd 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -80,6 +80,7 @@ class AnthemAVR(MediaPlayerEntity): self._attr_name = f"zone {zone_number}" self._attr_unique_id = f"{mac_address}_{zone_number}" else: + self._attr_name = None self._attr_unique_id = mac_address self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 009536a9adb..b8744865898 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -221,6 +221,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): _attr_assumed_state = False _attr_has_entity_name = True + _attr_name = None def __init__(self, device, *args, **kwargs): """Initialize the switch.""" diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index a70b9c134d0..5512bcd1176 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -83,6 +83,7 @@ class BrottsplatskartanSensor(SensorEntity): _attr_attribution = ATTRIBUTION _attr_has_entity_name = True + _attr_name = None def __init__(self, bpk: BrottsplatsKartan, name: str, entry_id: str) -> None: """Initialize the Brottsplatskartan sensor.""" diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 47afdf1539b..dc403611da2 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -71,6 +71,7 @@ class BSBLANClimate( """Defines a BSBLAN climate device.""" _attr_has_entity_name = True + _attr_name = None # Determine preset modes _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE diff --git a/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py b/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py index eafd1e63b1f..d2608ed43c7 100644 --- a/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py +++ b/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py @@ -8,6 +8,8 @@ from .devolo_device import DevoloDeviceEntity class DevoloMultiLevelSwitchDeviceEntity(DevoloDeviceEntity): """Representation of a multi level switch device within devolo Home Control. Something like a dimmer or a thermostat.""" + _attr_name = None + def __init__( self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str ) -> None: diff --git a/homeassistant/components/devolo_home_control/switch.py b/homeassistant/components/devolo_home_control/switch.py index 24b1d3545de..9b96e58da60 100644 --- a/homeassistant/components/devolo_home_control/switch.py +++ b/homeassistant/components/devolo_home_control/switch.py @@ -41,6 +41,8 @@ async def async_setup_entry( class DevoloSwitch(DevoloDeviceEntity, SwitchEntity): """Representation of a switch.""" + _attr_name = None + def __init__( self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str ) -> None: diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index 47da87306a3..f74ec04476f 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -47,6 +47,7 @@ async def async_setup_entry( class ElgatoLight(ElgatoEntity, LightEntity): """Defines an Elgato Light.""" + _attr_name = None _attr_min_mireds = 143 _attr_max_mireds = 344 diff --git a/homeassistant/components/homewizard/switch.py b/homeassistant/components/homewizard/switch.py index bec72b5d32d..cddcabc841e 100644 --- a/homeassistant/components/homewizard/switch.py +++ b/homeassistant/components/homewizard/switch.py @@ -45,6 +45,7 @@ class HomeWizardSwitchEntityDescription( SWITCHES = [ HomeWizardSwitchEntityDescription( key="power_on", + name=None, device_class=SwitchDeviceClass.OUTLET, create_fn=lambda coordinator: coordinator.supports_state(), available_fn=lambda data: data.state is not None and not data.state.switch_lock, diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index dd33da56297..db31baa53a6 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -101,6 +101,7 @@ class HoneywellUSThermostat(ClimateEntity): """Representation of a Honeywell US Thermostat.""" _attr_has_entity_name = True + _attr_name = None def __init__( self, diff --git a/homeassistant/components/jellyfin/media_player.py b/homeassistant/components/jellyfin/media_player.py index 2025e1a2a6c..bcd8e975823 100644 --- a/homeassistant/components/jellyfin/media_player.py +++ b/homeassistant/components/jellyfin/media_player.py @@ -90,6 +90,7 @@ class JellyfinMediaPlayer(JellyfinEntity, MediaPlayerEntity): sw_version=self.app_version, via_device=(DOMAIN, coordinator.server_id), ) + self._attr_name = None else: self._attr_device_info = None self._attr_has_entity_name = False diff --git a/homeassistant/components/jellyfin/sensor.py b/homeassistant/components/jellyfin/sensor.py index 1957adfc6eb..cd0e9ab21a2 100644 --- a/homeassistant/components/jellyfin/sensor.py +++ b/homeassistant/components/jellyfin/sensor.py @@ -42,6 +42,7 @@ def _count_now_playing(data: JellyfinDataT) -> int: SENSOR_TYPES: dict[str, JellyfinSensorEntityDescription] = { "sessions": JellyfinSensorEntityDescription( key="watching", + name=None, icon="mdi:television-play", native_unit_of_measurement="Watching", value_fn=_count_now_playing, diff --git a/homeassistant/components/jvc_projector/remote.py b/homeassistant/components/jvc_projector/remote.py index e33eef74c48..45f797a5aaa 100644 --- a/homeassistant/components/jvc_projector/remote.py +++ b/homeassistant/components/jvc_projector/remote.py @@ -52,6 +52,8 @@ async def async_setup_entry( class JvcProjectorRemote(JvcProjectorEntity, RemoteEntity): """Representation of a JVC Projector device.""" + _attr_name = None + @property def is_on(self) -> bool: """Return True if entity is on.""" From 8a6d54237f167d1ff2545e73aa7f9e4535485f05 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 30 Jun 2023 17:34:35 +0200 Subject: [PATCH 049/184] Fix explicit device naming for integrations m-r (#95620) Fix explicit device naming for m-r --- homeassistant/components/melnor/switch.py | 1 + homeassistant/components/open_meteo/weather.py | 1 + homeassistant/components/openhome/update.py | 1 + homeassistant/components/plugwise/climate.py | 1 + homeassistant/components/prusalink/sensor.py | 1 + homeassistant/components/rainbird/switch.py | 1 + homeassistant/components/recollect_waste/calendar.py | 1 + homeassistant/components/rfxtrx/__init__.py | 2 ++ homeassistant/components/ridwell/calendar.py | 1 + homeassistant/components/rituals_perfume_genie/switch.py | 1 + 10 files changed, 11 insertions(+) diff --git a/homeassistant/components/melnor/switch.py b/homeassistant/components/melnor/switch.py index a2854479abd..e5f70bc25a0 100644 --- a/homeassistant/components/melnor/switch.py +++ b/homeassistant/components/melnor/switch.py @@ -45,6 +45,7 @@ ZONE_ENTITY_DESCRIPTIONS = [ device_class=SwitchDeviceClass.SWITCH, icon="mdi:sprinkler", key="manual", + name=None, on_off_fn=lambda valve, bool: valve.set_is_watering(bool), state_fn=lambda valve: valve.is_watering, ), diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py index 2d06b20a30a..b23abb54f8b 100644 --- a/homeassistant/components/open_meteo/weather.py +++ b/homeassistant/components/open_meteo/weather.py @@ -34,6 +34,7 @@ class OpenMeteoWeatherEntity( """Defines an Open-Meteo weather entity.""" _attr_has_entity_name = True + _attr_name = None _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR diff --git a/homeassistant/components/openhome/update.py b/homeassistant/components/openhome/update.py index 22bffad44d8..54c2d16fb2b 100644 --- a/homeassistant/components/openhome/update.py +++ b/homeassistant/components/openhome/update.py @@ -48,6 +48,7 @@ class OpenhomeUpdateEntity(UpdateEntity): _attr_device_class = UpdateDeviceClass.FIRMWARE _attr_supported_features = UpdateEntityFeature.INSTALL _attr_has_entity_name = True + _attr_name = None def __init__(self, device): """Initialize a Linn DS update entity.""" diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index f7941d1f02d..36626c2324e 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -42,6 +42,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): """Representation of an Plugwise thermostat.""" _attr_has_entity_name = True + _attr_name = None _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key = DOMAIN diff --git a/homeassistant/components/prusalink/sensor.py b/homeassistant/components/prusalink/sensor.py index 4f93fd3407e..1ee4274e5bb 100644 --- a/homeassistant/components/prusalink/sensor.py +++ b/homeassistant/components/prusalink/sensor.py @@ -47,6 +47,7 @@ SENSORS: dict[str, tuple[PrusaLinkSensorEntityDescription, ...]] = { "printer": ( PrusaLinkSensorEntityDescription[PrinterInfo]( key="printer.state", + name=None, icon="mdi:printer-3d", value_fn=lambda data: ( "pausing" diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 38f3c03fb03..e915c52c9dc 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -67,6 +67,7 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity) self._attr_name = imported_name self._attr_has_entity_name = False else: + self._attr_name = None self._attr_has_entity_name = True self._state = None self._duration_minutes = duration_minutes diff --git a/homeassistant/components/recollect_waste/calendar.py b/homeassistant/components/recollect_waste/calendar.py index 120ab77c3b3..c439f647da5 100644 --- a/homeassistant/components/recollect_waste/calendar.py +++ b/homeassistant/components/recollect_waste/calendar.py @@ -48,6 +48,7 @@ class ReCollectWasteCalendar(ReCollectWasteEntity, CalendarEntity): """Define a ReCollect Waste calendar.""" _attr_icon = "mdi:delete-empty" + _attr_name = None def __init__( self, diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 0edd6f82195..3544abcfdd1 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -548,6 +548,8 @@ class RfxtrxCommandEntity(RfxtrxEntity): Contains the common logic for Rfxtrx lights and switches. """ + _attr_name = None + def __init__( self, device: rfxtrxmod.RFXtrxDevice, diff --git a/homeassistant/components/ridwell/calendar.py b/homeassistant/components/ridwell/calendar.py index 57919ed1feb..3ef3bbdc5ae 100644 --- a/homeassistant/components/ridwell/calendar.py +++ b/homeassistant/components/ridwell/calendar.py @@ -50,6 +50,7 @@ class RidwellCalendar(RidwellEntity, CalendarEntity): """Define a Ridwell calendar.""" _attr_icon = "mdi:delete-empty" + _attr_name = None def __init__( self, coordinator: RidwellDataUpdateCoordinator, account: RidwellAccount diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py index a6083e51430..77776704a60 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -36,6 +36,7 @@ class RitualsSwitchEntityDescription( ENTITY_DESCRIPTIONS = ( RitualsSwitchEntityDescription( key="is_on", + name=None, icon="mdi:fan", is_on_fn=lambda diffuser: diffuser.is_on, turn_on_fn=lambda diffuser: diffuser.turn_on(), From 19cbedcc053e10dafbd4803ec8ccbecc2880fc23 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Fri, 30 Jun 2023 15:29:44 -0300 Subject: [PATCH 050/184] Fix device source for Derivative (#95621) Fix Device Source --- homeassistant/components/derivative/sensor.py | 1 + tests/components/derivative/test_sensor.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index 793e8edc769..af04da27406 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -108,6 +108,7 @@ async def async_setup_entry( ): device_info = DeviceInfo( identifiers=device.identifiers, + connections=device.connections, ) else: device_info = None diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index 513e9597572..2d1d7a93afc 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -357,6 +357,7 @@ async def test_device_id(hass: HomeAssistant) -> None: source_device_entry = device_registry.async_get_or_create( config_entry_id=source_config_entry.entry_id, identifiers={("sensor", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, ) source_entity = entity_registry.async_get_or_create( "sensor", From 9a14d437dd25c7a2c32da74a93f6250729f9a5f7 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Fri, 30 Jun 2023 15:49:00 -0300 Subject: [PATCH 051/184] Fix device source for Riemann sum integral (#95622) Fix Device Source --- homeassistant/components/integration/sensor.py | 1 + tests/components/integration/test_sensor.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index ad0f96dd540..af4248e5e3b 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -162,6 +162,7 @@ async def async_setup_entry( ): device_info = DeviceInfo( identifiers=device.identifiers, + connections=device.connections, ) else: device_info = None diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 5b3734bd1be..a552d401681 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -688,6 +688,7 @@ async def test_device_id(hass: HomeAssistant) -> None: source_device_entry = device_registry.async_get_or_create( config_entry_id=source_config_entry.entry_id, identifiers={("sensor", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, ) source_entity = entity_registry.async_get_or_create( "sensor", From d1e8513d63ecd9716a8cd440c98ddbb9938a7b63 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Fri, 30 Jun 2023 15:48:36 -0300 Subject: [PATCH 052/184] Fix device source for Threshold (#95623) Fix Device Source --- homeassistant/components/threshold/binary_sensor.py | 1 + tests/components/threshold/test_binary_sensor.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index f7b8c9c097c..09f928303bf 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -96,6 +96,7 @@ async def async_setup_entry( ): device_info = DeviceInfo( identifiers=device.identifiers, + connections=device.connections, ) else: device_info = None diff --git a/tests/components/threshold/test_binary_sensor.py b/tests/components/threshold/test_binary_sensor.py index 2180d0aed7f..e26781029c5 100644 --- a/tests/components/threshold/test_binary_sensor.py +++ b/tests/components/threshold/test_binary_sensor.py @@ -600,6 +600,7 @@ async def test_device_id(hass: HomeAssistant) -> None: source_device_entry = device_registry.async_get_or_create( config_entry_id=source_config_entry.entry_id, identifiers={("sensor", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, ) source_entity = entity_registry.async_get_or_create( "sensor", From 4192fdbdfd8e84853208cd672f0dc99e6bca197d Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Fri, 30 Jun 2023 15:48:11 -0300 Subject: [PATCH 053/184] Fix device source for Utility Meter select (#95624) Fix Device Source --- homeassistant/components/utility_meter/select.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/utility_meter/select.py b/homeassistant/components/utility_meter/select.py index cf0e6e91ffb..cf2a6da9e08 100644 --- a/homeassistant/components/utility_meter/select.py +++ b/homeassistant/components/utility_meter/select.py @@ -56,6 +56,7 @@ async def async_setup_entry( ): device_info = DeviceInfo( identifiers=device.identifiers, + connections=device.connections, ) else: device_info = None From 11fd9ffa60003c57ab47504a6c864f3c10beef66 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jun 2023 10:37:04 -0500 Subject: [PATCH 054/184] Bump aioesphomeapi to 15.0.1 (#95629) fixes #87223 (the cases were the host gets too far behind, not the cases were the esp8266 runs out of ram but thats is not a core issue) --- 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 404339d4f30..085437fb02e 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,7 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ - "aioesphomeapi==15.0.0", + "aioesphomeapi==15.0.1", "bluetooth-data-tools==1.3.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index bc0d3208b35..a58b9f7e0d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -234,7 +234,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==15.0.0 +aioesphomeapi==15.0.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57e7adf7964..b2989389fc2 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==15.0.0 +aioesphomeapi==15.0.1 # homeassistant.components.flo aioflo==2021.11.0 From 313e15a915dbd1e3b1a5f3409635d1677abf7926 Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Fri, 30 Jun 2023 17:20:20 +0200 Subject: [PATCH 055/184] Update services.yaml (#95630) take out 'templates accepted' --- .../components/persistent_notification/services.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/persistent_notification/services.yaml b/homeassistant/components/persistent_notification/services.yaml index 5ebd7e34409..046ea237560 100644 --- a/homeassistant/components/persistent_notification/services.yaml +++ b/homeassistant/components/persistent_notification/services.yaml @@ -4,14 +4,14 @@ create: fields: message: name: Message - description: Message body of the notification. [Templates accepted] + description: Message body of the notification. required: true example: Please check your configuration.yaml. selector: text: title: name: Title - description: Optional title for your notification. [Templates accepted] + description: Optional title for your notification. example: Test notification selector: text: From 2de0b0f6acbc63d5bf6abc105c003fb419770feb Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 30 Jun 2023 19:55:03 +0200 Subject: [PATCH 056/184] Update frontend to 20230630.0 (#95635) --- 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 891a97d4d02..4a1edd4096e 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==20230629.0"] + "requirements": ["home-assistant-frontend==20230630.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e6e506273ad..b3d3f421bcd 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==20230629.0 +home-assistant-frontend==20230630.0 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 a58b9f7e0d1..ef07e91eb7d 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==20230629.0 +home-assistant-frontend==20230630.0 # homeassistant.components.conversation home-assistant-intents==2023.6.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2989389fc2..9b22fd45f88 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==20230629.0 +home-assistant-frontend==20230630.0 # homeassistant.components.conversation home-assistant-intents==2023.6.28 From 4993b6ee903ba1b0e58fa991bacb3d32b757a038 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 30 Jun 2023 15:07:20 -0400 Subject: [PATCH 057/184] Fix ZHA startup issue with older Silicon Labs firmwares (#95642) Bump ZHA dependencies --- 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 d7acc9788c4..293822987c3 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -20,7 +20,7 @@ "zigpy_znp" ], "requirements": [ - "bellows==0.35.7", + "bellows==0.35.8", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.101", diff --git a/requirements_all.txt b/requirements_all.txt index ef07e91eb7d..fe24b288f63 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -497,7 +497,7 @@ beautifulsoup4==4.11.1 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.35.7 +bellows==0.35.8 # homeassistant.components.bmw_connected_drive bimmer-connected==0.13.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b22fd45f88..306606bc46c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -418,7 +418,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.35.7 +bellows==0.35.8 # homeassistant.components.bmw_connected_drive bimmer-connected==0.13.7 From be49f90550369597fde8f2c4d1607f807fba4a48 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Jun 2023 15:08:35 -0400 Subject: [PATCH 058/184] Bumped version to 2023.7.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 ad465026058..7f5e9b599d0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 7 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 4725c939693..ef13abd141a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.7.0b2" +version = "2023.7.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 226f1d7c73fa4a0b13ddbe394329d1853a87d1f3 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sat, 1 Jul 2023 05:59:01 +0200 Subject: [PATCH 059/184] Fix missing EntityDescription names in Overkiz (#95583) * Fix labels * Update homeassistant/components/overkiz/entity.py * Check if description.name is string --------- Co-authored-by: Paulus Schoutsen --- homeassistant/components/overkiz/entity.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/overkiz/entity.py b/homeassistant/components/overkiz/entity.py index f1e3d96a219..16ea12a5d96 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -119,3 +119,5 @@ class OverkizDescriptiveEntity(OverkizEntity): # In case of sub device, use the provided label # and append the name of the type of entity self._attr_name = f"{self.device.label} {description.name}" + elif isinstance(description.name, str): + self._attr_name = description.name From 3fa009b98c0fb5d965aa130ed94dd44e4eb83305 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Sun, 2 Jul 2023 23:46:21 -0300 Subject: [PATCH 060/184] Fix source device when source entity is changed for Utility Meter (#95636) * Fix source device when source entity is changed * Update loop * Complement and add comments in the test_change_device_source test * Only clean up dev reg when options change --------- Co-authored-by: Paulus Schoutsen --- .../components/utility_meter/__init__.py | 25 ++- .../utility_meter/test_config_flow.py | 171 ++++++++++++++++++ 2 files changed, 194 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 11e58fca775..ffe6d7f5433 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -10,7 +10,11 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, CONF_UNIQUE_ID, Platform from homeassistant.core import HomeAssistant, split_entity_id -from homeassistant.helpers import discovery, entity_registry as er +from homeassistant.helpers import ( + device_registry as dr, + discovery, + entity_registry as er, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType @@ -182,7 +186,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Utility Meter from a config entry.""" entity_registry = er.async_get(hass) - hass.data[DATA_UTILITY][entry.entry_id] = {} + hass.data[DATA_UTILITY][entry.entry_id] = { + "source": entry.options[CONF_SOURCE_SENSOR], + } hass.data[DATA_UTILITY][entry.entry_id][DATA_TARIFF_SENSORS] = [] try: @@ -218,8 +224,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update listener, called when the config entry options are changed.""" + old_source = hass.data[DATA_UTILITY][entry.entry_id]["source"] await hass.config_entries.async_reload(entry.entry_id) + if old_source == entry.options[CONF_SOURCE_SENSOR]: + return + + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + old_source_entity = entity_registry.async_get(old_source) + if not old_source_entity or not old_source_entity.device_id: + return + + device_registry.async_update_device( + old_source_entity.device_id, remove_config_entry_id=entry.entry_id + ) + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" diff --git a/tests/components/utility_meter/test_config_flow.py b/tests/components/utility_meter/test_config_flow.py index 302d3879a04..88a77407c07 100644 --- a/tests/components/utility_meter/test_config_flow.py +++ b/tests/components/utility_meter/test_config_flow.py @@ -7,6 +7,7 @@ from homeassistant import config_entries from homeassistant.components.utility_meter.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -266,3 +267,173 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() state = hass.states.get("sensor.electricity_meter") assert state.attributes["source"] == input_sensor2_entity_id + + +async def test_change_device_source(hass: HomeAssistant) -> None: + """Test remove the device registry configuration entry when the source entity changes.""" + + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + # Configure source entity 1 (with a linked device) + source_config_entry_1 = MockConfigEntry() + source_device_entry_1 = device_registry.async_get_or_create( + config_entry_id=source_config_entry_1.entry_id, + identifiers={("sensor", "identifier_test1")}, + connections={("mac", "20:31:32:33:34:35")}, + ) + source_entity_1 = entity_registry.async_get_or_create( + "sensor", + "test", + "source1", + config_entry=source_config_entry_1, + device_id=source_device_entry_1.id, + ) + + # Configure source entity 2 (with a linked device) + source_config_entry_2 = MockConfigEntry() + source_device_entry_2 = device_registry.async_get_or_create( + config_entry_id=source_config_entry_2.entry_id, + identifiers={("sensor", "identifier_test2")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + source_entity_2 = entity_registry.async_get_or_create( + "sensor", + "test", + "source2", + config_entry=source_config_entry_2, + device_id=source_device_entry_2.id, + ) + + # Configure source entity 3 (without a device) + source_config_entry_3 = MockConfigEntry() + source_entity_3 = entity_registry.async_get_or_create( + "sensor", + "test", + "source3", + config_entry=source_config_entry_3, + ) + + await hass.async_block_till_done() + + input_sensor_entity_id_1 = "sensor.test_source1" + input_sensor_entity_id_2 = "sensor.test_source2" + input_sensor_entity_id_3 = "sensor.test_source3" + + # Test the existence of configured source entities + assert entity_registry.async_get(input_sensor_entity_id_1) is not None + assert entity_registry.async_get(input_sensor_entity_id_2) is not None + assert entity_registry.async_get(input_sensor_entity_id_3) is not None + + # Setup the config entry with source entity 1 (with a linked device) + current_entity_source = source_entity_1 + utility_meter_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "cycle": "monthly", + "delta_values": False, + "name": "Energy", + "net_consumption": False, + "offset": 0, + "periodically_resetting": True, + "source": current_entity_source.entity_id, + "tariffs": [], + }, + title="Energy", + ) + utility_meter_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(utility_meter_config_entry.entry_id) + await hass.async_block_till_done() + + # Confirm that the configuration entry has been added to the source entity 1 (current) device registry + current_device = device_registry.async_get( + device_id=current_entity_source.device_id + ) + assert utility_meter_config_entry.entry_id in current_device.config_entries + + # Change configuration options to use source entity 2 (with a linked device) and reload the integration + previous_entity_source = source_entity_1 + current_entity_source = source_entity_2 + + result = await hass.config_entries.options.async_init( + utility_meter_config_entry.entry_id + ) + assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "periodically_resetting": True, + "source": current_entity_source.entity_id, + }, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + await hass.async_block_till_done() + + # Confirm that the configuration entry has been removed from the source entity 1 (previous) device registry + previous_device = device_registry.async_get( + device_id=previous_entity_source.device_id + ) + assert utility_meter_config_entry.entry_id not in previous_device.config_entries + + # Confirm that the configuration entry has been added to the source entity 2 (current) device registry + current_device = device_registry.async_get( + device_id=current_entity_source.device_id + ) + assert utility_meter_config_entry.entry_id in current_device.config_entries + + # Change configuration options to use source entity 3 (without a device) and reload the integration + previous_entity_source = source_entity_2 + current_entity_source = source_entity_3 + + result = await hass.config_entries.options.async_init( + utility_meter_config_entry.entry_id + ) + assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "periodically_resetting": True, + "source": current_entity_source.entity_id, + }, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + await hass.async_block_till_done() + + # Confirm that the configuration entry has been removed from the source entity 2 (previous) device registry + previous_device = device_registry.async_get( + device_id=previous_entity_source.device_id + ) + assert utility_meter_config_entry.entry_id not in previous_device.config_entries + + # Confirm that there is no device with the helper configuration entry + assert ( + dr.async_entries_for_config_entry( + device_registry, utility_meter_config_entry.entry_id + ) + == [] + ) + + # Change configuration options to use source entity 2 (with a linked device) and reload the integration + previous_entity_source = source_entity_3 + current_entity_source = source_entity_2 + + result = await hass.config_entries.options.async_init( + utility_meter_config_entry.entry_id + ) + assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "periodically_resetting": True, + "source": current_entity_source.entity_id, + }, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + await hass.async_block_till_done() + + # Confirm that the configuration entry has been added to the source entity 2 (current) device registry + current_device = device_registry.async_get( + device_id=current_entity_source.device_id + ) + assert utility_meter_config_entry.entry_id in current_device.config_entries From e907585b7f29bbc7bfd12026c4421a521d0f2ad3 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Sat, 1 Jul 2023 10:41:03 +0200 Subject: [PATCH 061/184] Add bmw connected drive region-specific scan interval (#95649) Add region-specific scan interval Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/const.py | 6 ++++++ homeassistant/components/bmw_connected_drive/coordinator.py | 6 ++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/const.py b/homeassistant/components/bmw_connected_drive/const.py index 37225fc052f..96ef152307d 100644 --- a/homeassistant/components/bmw_connected_drive/const.py +++ b/homeassistant/components/bmw_connected_drive/const.py @@ -21,3 +21,9 @@ UNIT_MAP = { "LITERS": UnitOfVolume.LITERS, "GALLONS": UnitOfVolume.GALLONS, } + +SCAN_INTERVALS = { + "china": 300, + "north_america": 600, + "rest_of_world": 300, +} diff --git a/homeassistant/components/bmw_connected_drive/coordinator.py b/homeassistant/components/bmw_connected_drive/coordinator.py index f6354422312..4a586aab373 100644 --- a/homeassistant/components/bmw_connected_drive/coordinator.py +++ b/homeassistant/components/bmw_connected_drive/coordinator.py @@ -15,10 +15,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN +from .const import CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN, SCAN_INTERVALS -DEFAULT_SCAN_INTERVAL_SECONDS = 300 -SCAN_INTERVAL = timedelta(seconds=DEFAULT_SCAN_INTERVAL_SECONDS) _LOGGER = logging.getLogger(__name__) @@ -50,7 +48,7 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]): hass, _LOGGER, name=f"{DOMAIN}-{entry.data['username']}", - update_interval=SCAN_INTERVAL, + update_interval=timedelta(seconds=SCAN_INTERVALS[entry.data[CONF_REGION]]), ) async def _async_update_data(self) -> None: From 327a54e65a068dd51d6d36f30ace4f6e9127aa1f Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Sun, 2 Jul 2023 09:28:18 -0400 Subject: [PATCH 062/184] Upgrade pymazda to 0.3.9 (#95655) --- homeassistant/components/mazda/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index 2c2aafa960e..01f77cb2d38 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["pymazda"], "quality_scale": "platinum", - "requirements": ["pymazda==0.3.8"] + "requirements": ["pymazda==0.3.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index fe24b288f63..619a8721552 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1810,7 +1810,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.8 +pymazda==0.3.9 # homeassistant.components.mediaroom pymediaroom==0.6.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 306606bc46c..b1b5b9f2cbe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1338,7 +1338,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.8 +pymazda==0.3.9 # homeassistant.components.melcloud pymelcloud==2.5.8 From ca20663c31f1ddcbb3f0fb654360e93df2aa253a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Jul 2023 09:29:45 -0500 Subject: [PATCH 063/184] Handle missing or incorrect device name and unique id for ESPHome during manual add (#95678) * Handle incorrect or missing device name for ESPHome noise encryption If we did not have the device name during setup we could never get the key from the dashboard. The device will send us its name if we try encryption which allows us to find the right key from the dashboard. This should help get users unstuck when they change the key and cannot get the device back online after deleting and trying to set it up again manually * bump lib to get name * tweak * reduce number of connections * less connections when we know we will fail * coverage shows it works but it does not * add more coverage * fix test * bump again --- .../components/esphome/config_flow.py | 42 ++- .../components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/esphome/test_config_flow.py | 315 +++++++++++++++++- 5 files changed, 342 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 11deb5bb486..53c8577be44 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -40,6 +40,8 @@ ERROR_INVALID_ENCRYPTION_KEY = "invalid_psk" ESPHOME_URL = "https://esphome.io/" _LOGGER = logging.getLogger(__name__) +ZERO_NOISE_PSK = "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA=" + class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a esphome config flow.""" @@ -149,11 +151,22 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): async def _async_try_fetch_device_info(self) -> FlowResult: error = await self.fetch_device_info() - if ( - error == ERROR_REQUIRES_ENCRYPTION_KEY - and await self._retrieve_encryption_key_from_dashboard() - ): - error = await self.fetch_device_info() + if error == ERROR_REQUIRES_ENCRYPTION_KEY: + if not self._device_name and not self._noise_psk: + # If device name is not set we can send a zero noise psk + # to get the device name which will allow us to populate + # the device name and hopefully get the encryption key + # from the dashboard. + self._noise_psk = ZERO_NOISE_PSK + error = await self.fetch_device_info() + self._noise_psk = None + + if ( + self._device_name + and await self._retrieve_encryption_key_from_dashboard() + ): + error = await self.fetch_device_info() + # If the fetched key is invalid, unset it again. if error == ERROR_INVALID_ENCRYPTION_KEY: self._noise_psk = None @@ -323,7 +336,10 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._device_info = await cli.device_info() except RequiresEncryptionAPIError: return ERROR_REQUIRES_ENCRYPTION_KEY - except InvalidEncryptionKeyAPIError: + except InvalidEncryptionKeyAPIError as ex: + if ex.received_name: + self._device_name = ex.received_name + self._name = ex.received_name return ERROR_INVALID_ENCRYPTION_KEY except ResolveAPIError: return "resolve_error" @@ -334,9 +350,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._name = self._device_info.friendly_name or self._device_info.name self._device_name = self._device_info.name - await self.async_set_unique_id( - self._device_info.mac_address, raise_on_progress=False - ) + mac_address = format_mac(self._device_info.mac_address) + await self.async_set_unique_id(mac_address, raise_on_progress=False) if not self._reauth_entry: self._abort_if_unique_id_configured( updates={CONF_HOST: self._host, CONF_PORT: self._port} @@ -373,14 +388,13 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): Return boolean if a key was retrieved. """ - if self._device_name is None: - return False - - if (dashboard := async_get_dashboard(self.hass)) is None: + if ( + self._device_name is None + or (dashboard := async_get_dashboard(self.hass)) is None + ): return False await dashboard.async_request_refresh() - if not dashboard.last_update_success: return False diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 085437fb02e..8f5e6b95c39 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,7 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ - "aioesphomeapi==15.0.1", + "aioesphomeapi==15.1.1", "bluetooth-data-tools==1.3.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 619a8721552..7241667fb82 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -234,7 +234,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==15.0.1 +aioesphomeapi==15.1.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1b5b9f2cbe..ab6fd014adc 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==15.0.1 +aioesphomeapi==15.1.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index affe65949b2..4a99de77c1a 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -1,4 +1,5 @@ """Test config flow.""" +import asyncio from unittest.mock import AsyncMock, MagicMock, patch from aioesphomeapi import ( @@ -10,6 +11,7 @@ from aioesphomeapi import ( RequiresEncryptionAPIError, ResolveAPIError, ) +import aiohttp import pytest from homeassistant import config_entries, data_entry_flow @@ -35,6 +37,7 @@ from . import VALID_NOISE_PSK from tests.common import MockConfigEntry INVALID_NOISE_PSK = "lSYBYEjQI1bVL8s2Vask4YytGMj1f1epNtmoim2yuTM=" +WRONG_NOISE_PSK = "GP+ciK+nVfTQ/gcz6uOdS+oKEdJgesU+jeu8Ssj2how=" @pytest.fixture(autouse=False) @@ -115,6 +118,58 @@ async def test_user_connection_updates_host( assert entry.data[CONF_HOST] == "127.0.0.1" +async def test_user_sets_unique_id( + hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None +) -> None: + """Test that the user flow sets the unique id.""" + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.43.183", + addresses=["192.168.43.183"], + hostname="test8266.local.", + name="mock_name", + port=6053, + properties={ + "mac": "1122334455aa", + }, + type="mock_type", + ) + discovery_result = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + ) + + assert discovery_result["type"] == FlowResultType.FORM + assert discovery_result["step_id"] == "discovery_confirm" + + discovery_result = await hass.config_entries.flow.async_configure( + discovery_result["flow_id"], + {}, + ) + assert discovery_result["type"] == FlowResultType.CREATE_ENTRY + assert discovery_result["data"] == { + CONF_HOST: "192.168.43.183", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: "", + CONF_DEVICE_NAME: "test", + } + + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data=None, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + async def test_user_resolve_error( hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None ) -> None: @@ -140,6 +195,53 @@ async def test_user_resolve_error( assert len(mock_client.disconnect.mock_calls) == 1 +async def test_user_causes_zeroconf_to_abort( + hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None +) -> None: + """Test that the user flow sets the unique id and aborts the zeroconf flow.""" + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.43.183", + addresses=["192.168.43.183"], + hostname="test8266.local.", + name="mock_name", + port=6053, + properties={ + "mac": "1122334455aa", + }, + type="mock_type", + ) + discovery_result = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + ) + + assert discovery_result["type"] == FlowResultType.FORM + assert discovery_result["step_id"] == "discovery_confirm" + + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data=None, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: "", + CONF_DEVICE_NAME: "test", + } + + assert not hass.config_entries.flow.async_progress_by_handler(DOMAIN) + + async def test_user_connection_error( hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None ) -> None: @@ -217,6 +319,211 @@ async def test_user_invalid_password( assert result["errors"] == {"base": "invalid_auth"} +async def test_user_dashboard_has_wrong_key( + hass: HomeAssistant, + mock_client, + mock_dashboard, + mock_zeroconf: None, + mock_setup_entry: None, +) -> None: + """Test user step with key from dashboard that is incorrect.""" + mock_client.device_info.side_effect = [ + RequiresEncryptionAPIError, + InvalidEncryptionKeyAPIError, + DeviceInfo( + uses_password=False, + name="test", + mac_address="11:22:33:44:55:AA", + ), + ] + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=WRONG_NOISE_PSK, + ): + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "encryption_key" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: VALID_NOISE_PSK, + CONF_DEVICE_NAME: "test", + } + assert mock_client.noise_psk == VALID_NOISE_PSK + + +async def test_user_discovers_name_and_gets_key_from_dashboard( + hass: HomeAssistant, + mock_client, + mock_dashboard, + mock_zeroconf: None, + mock_setup_entry: None, +) -> None: + """Test user step can discover the name and get the key from the dashboard.""" + mock_client.device_info.side_effect = [ + RequiresEncryptionAPIError, + InvalidEncryptionKeyAPIError("Wrong key", "test"), + DeviceInfo( + uses_password=False, + name="test", + mac_address="11:22:33:44:55:AA", + ), + ] + + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + } + ) + await dashboard.async_get_dashboard(hass).async_refresh() + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ): + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: VALID_NOISE_PSK, + CONF_DEVICE_NAME: "test", + } + assert mock_client.noise_psk == VALID_NOISE_PSK + + +async def test_user_discovers_name_and_gets_key_from_dashboard_fails( + hass: HomeAssistant, + mock_client, + mock_dashboard, + mock_zeroconf: None, + mock_setup_entry: None, +) -> None: + """Test user step can discover the name and get the key from the dashboard.""" + mock_client.device_info.side_effect = [ + RequiresEncryptionAPIError, + InvalidEncryptionKeyAPIError("Wrong key", "test"), + DeviceInfo( + uses_password=False, + name="test", + mac_address="11:22:33:44:55:aa", + ), + ] + + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + } + ) + await dashboard.async_get_dashboard(hass).async_refresh() + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + side_effect=aiohttp.ClientError, + ): + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "encryption_key" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: VALID_NOISE_PSK, + CONF_DEVICE_NAME: "test", + } + assert mock_client.noise_psk == VALID_NOISE_PSK + + +async def test_user_discovers_name_and_dashboard_is_unavailable( + hass: HomeAssistant, + mock_client, + mock_dashboard, + mock_zeroconf: None, + mock_setup_entry: None, +) -> None: + """Test user step can discover the name but the dashboard is unavailable.""" + mock_client.device_info.side_effect = [ + RequiresEncryptionAPIError, + InvalidEncryptionKeyAPIError("Wrong key", "test"), + DeviceInfo( + uses_password=False, + name="test", + mac_address="11:22:33:44:55:AA", + ), + ] + + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + } + ) + + with patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.get_devices", + side_effect=asyncio.TimeoutError, + ): + await dashboard.async_get_dashboard(hass).async_refresh() + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "encryption_key" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: VALID_NOISE_PSK, + CONF_DEVICE_NAME: "test", + } + assert mock_client.noise_psk == VALID_NOISE_PSK + + async def test_login_connection_error( hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None ) -> None: @@ -398,9 +705,9 @@ async def test_user_requires_psk( assert result["step_id"] == "encryption_key" assert result["errors"] == {} - assert len(mock_client.connect.mock_calls) == 1 - assert len(mock_client.device_info.mock_calls) == 1 - assert len(mock_client.disconnect.mock_calls) == 1 + assert len(mock_client.connect.mock_calls) == 2 + assert len(mock_client.device_info.mock_calls) == 2 + assert len(mock_client.disconnect.mock_calls) == 2 async def test_encryption_key_valid_psk( @@ -894,7 +1201,7 @@ async def test_zeroconf_encryption_key_via_dashboard( DeviceInfo( uses_password=False, name="test8266", - mac_address="11:22:33:44:55:aa", + mac_address="11:22:33:44:55:AA", ), ] From 910aecb33b78564e657b24ee0268e4f5f79d1f40 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 1 Jul 2023 10:53:47 -0600 Subject: [PATCH 064/184] Fix implicit device name for SimpliSafe locks (#95681) --- homeassistant/components/simplisafe/lock.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index 1e7be48979b..9ce59eb3b56 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -49,6 +49,9 @@ async def async_setup_entry( class SimpliSafeLock(SimpliSafeEntity, LockEntity): """Define a SimpliSafe lock.""" + _attr_name = None + _device: Lock + def __init__(self, simplisafe: SimpliSafe, system: SystemV3, lock: Lock) -> None: """Initialize.""" super().__init__( @@ -58,8 +61,6 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): additional_websocket_events=WEBSOCKET_EVENTS_TO_LISTEN_FOR, ) - self._device: Lock - async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" try: From d38aab1607c8e28faa8e7ac28a0e9b3060daded1 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 2 Jul 2023 13:28:41 +0000 Subject: [PATCH 065/184] Fix Shelly button `unique_id` migration (#95707) Fix button unique_id migration --- homeassistant/components/shelly/button.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 1f684ce137c..ac01033f2c7 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -92,8 +92,8 @@ def async_migrate_unique_ids( device_name = slugify(coordinator.device.name) for key in ("reboot", "self_test", "mute", "unmute"): - if entity_entry.unique_id.startswith(device_name): - old_unique_id = entity_entry.unique_id + old_unique_id = f"{device_name}_{key}" + if entity_entry.unique_id == old_unique_id: new_unique_id = f"{coordinator.mac}_{key}" LOGGER.debug( "Migrating unique_id for %s entity from [%s] to [%s]", From 98242b5d54891b7af88f21d57eb111081e09ee5b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Jul 2023 12:23:41 -0500 Subject: [PATCH 066/184] Bump zeroconf to 0.70.0 (#95714) --- 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 9134a92c799..1c5d25dfb3d 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.69.0"] + "requirements": ["zeroconf==0.70.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b3d3f421bcd..a5be8daf6fc 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -52,7 +52,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.69.0 +zeroconf==0.70.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 7241667fb82..fcbe2dc3687 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2735,7 +2735,7 @@ zamg==0.2.2 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.69.0 +zeroconf==0.70.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab6fd014adc..6a25aae3c65 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2005,7 +2005,7 @@ youless-api==1.0.1 zamg==0.2.2 # homeassistant.components.zeroconf -zeroconf==0.69.0 +zeroconf==0.70.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From a77fb14bafa3fd8d77e5edb24f291e6d579579fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Jul 2023 12:09:20 -0500 Subject: [PATCH 067/184] Bump python-kasa to 0.5.2 (#95716) --- homeassistant/components/tplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 6683e7e4583..eaa1acc11bf 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -137,5 +137,5 @@ "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa==0.5.1"] + "requirements": ["python-kasa[speedups]==0.5.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index fcbe2dc3687..87f74d66523 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2099,7 +2099,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa==0.5.1 +python-kasa[speedups]==0.5.2 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6a25aae3c65..86e22c3644c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1540,7 +1540,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa==0.5.1 +python-kasa[speedups]==0.5.2 # homeassistant.components.matter python-matter-server==3.6.3 From fdd8489b934434c1b9676bb6bddd89e0efe32869 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Jul 2023 21:47:25 -0500 Subject: [PATCH 068/184] Handle invalid utf-8 from the ESPHome dashboard (#95743) If the yaml file has invalid utf-8, the config flow would raise an unhandled exception. Allow the encryption key to be entered manually in this case instead of a hard failure fixes #92772 --- homeassistant/components/esphome/config_flow.py | 6 ++++++ tests/components/esphome/test_config_flow.py | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 53c8577be44..731743e48c8 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import OrderedDict from collections.abc import Mapping +import json import logging from typing import Any @@ -408,6 +409,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): except aiohttp.ClientError as err: _LOGGER.error("Error talking to the dashboard: %s", err) return False + except json.JSONDecodeError as err: + _LOGGER.error( + "Error parsing response from dashboard: %s", err, exc_info=True + ) + return False self._noise_psk = noise_psk return True diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 4a99de77c1a..662816a53d8 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -1,5 +1,6 @@ """Test config flow.""" import asyncio +import json from unittest.mock import AsyncMock, MagicMock, patch from aioesphomeapi import ( @@ -414,8 +415,13 @@ async def test_user_discovers_name_and_gets_key_from_dashboard( assert mock_client.noise_psk == VALID_NOISE_PSK +@pytest.mark.parametrize( + "dashboard_exception", + [aiohttp.ClientError(), json.JSONDecodeError("test", "test", 0)], +) async def test_user_discovers_name_and_gets_key_from_dashboard_fails( hass: HomeAssistant, + dashboard_exception: Exception, mock_client, mock_dashboard, mock_zeroconf: None, @@ -442,7 +448,7 @@ async def test_user_discovers_name_and_gets_key_from_dashboard_fails( with patch( "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", - side_effect=aiohttp.ClientError, + side_effect=dashboard_exception, ): result = await hass.config_entries.flow.async_init( "esphome", From c260e787aaca158b53c077f997448e00e64bf1bf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 2 Jul 2023 22:50:36 -0400 Subject: [PATCH 069/184] Bumped version to 2023.7.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 7f5e9b599d0..cec8bf64fa7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 7 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index ef13abd141a..07af4f503c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.7.0b3" +version = "2023.7.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From ebb28973e00e085e2a26b535d0deb149aca28407 Mon Sep 17 00:00:00 2001 From: hidaris Date: Mon, 3 Jul 2023 18:33:50 +0800 Subject: [PATCH 070/184] Add Matter Climate support (#95434) * Add Matter Climate support * update set target temp and update callback * remove print * remove optional property * Adjust the code to improve readability. * add thermostat test * Remove irrelevant cases in setting the target temperature. * add temp range support * update hvac action * support adjust low high setpoint.. * support set hvac mode * address some review feedback * move some methods around * dont discover climate in switch platform * set some default values * fix some of the tests * fix some typos * Update thermostat.json * Update homeassistant/components/matter/climate.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/matter/climate.py Co-authored-by: Martin Hjelmare * support heat_cool in hvac_modes * address some review feedback * handle hvac mode param in set temp service * check hvac modes by featuremap * add comment to thermostat feature class * make ruff happy.. * use enum to enhance readability. * use builtin feature bitmap * fix target temp range and address some feedback * use instance attribute instead of class attr * make ruff happy... * address feedback about single case * add init docstring * more test * fix typo in tests * make ruff happy * fix hvac modes test * test case for update callback * remove optional check * more tests * more tests * update all attributes in the update callback * Update climate.py * fix missing test --------- Co-authored-by: Marcel van der Veldt Co-authored-by: Martin Hjelmare --- homeassistant/components/matter/climate.py | 313 ++++++++++++++ homeassistant/components/matter/discovery.py | 2 + homeassistant/components/matter/switch.py | 1 + .../matter/fixtures/nodes/thermostat.json | 370 ++++++++++++++++ tests/components/matter/test_climate.py | 399 ++++++++++++++++++ 5 files changed, 1085 insertions(+) create mode 100644 homeassistant/components/matter/climate.py create mode 100644 tests/components/matter/fixtures/nodes/thermostat.json create mode 100644 tests/components/matter/test_climate.py diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py new file mode 100644 index 00000000000..6da88533edc --- /dev/null +++ b/homeassistant/components/matter/climate.py @@ -0,0 +1,313 @@ +"""Matter climate platform.""" +from __future__ import annotations + +from enum import IntEnum +from typing import TYPE_CHECKING, Any + +from chip.clusters import Objects as clusters +from matter_server.client.models import device_types +from matter_server.common.helpers.util import create_attribute_path_from_attribute + +from homeassistant.components.climate import ( + ATTR_HVAC_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, + ClimateEntity, + ClimateEntityDescription, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, Platform, UnitOfTemperature +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import MatterEntity +from .helpers import get_matter +from .models import MatterDiscoverySchema + +if TYPE_CHECKING: + from matter_server.client import MatterClient + from matter_server.client.models.node import MatterEndpoint + + from .discovery import MatterEntityInfo + +TEMPERATURE_SCALING_FACTOR = 100 +HVAC_SYSTEM_MODE_MAP = { + HVACMode.OFF: 0, + HVACMode.HEAT_COOL: 1, + HVACMode.COOL: 3, + HVACMode.HEAT: 4, +} +SystemModeEnum = clusters.Thermostat.Enums.ThermostatSystemMode +ControlSequenceEnum = clusters.Thermostat.Enums.ThermostatControlSequence +ThermostatFeature = clusters.Thermostat.Bitmaps.ThermostatFeature + + +class ThermostatRunningState(IntEnum): + """Thermostat Running State, Matter spec Thermostat 7.33.""" + + Heat = 1 # 1 << 0 = 1 + Cool = 2 # 1 << 1 = 2 + Fan = 4 # 1 << 2 = 4 + HeatStage2 = 8 # 1 << 3 = 8 + CoolStage2 = 16 # 1 << 4 = 16 + FanStage2 = 32 # 1 << 5 = 32 + FanStage3 = 64 # 1 << 6 = 64 + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Matter climate platform from Config Entry.""" + matter = get_matter(hass) + matter.register_platform_handler(Platform.CLIMATE, async_add_entities) + + +class MatterClimate(MatterEntity, ClimateEntity): + """Representation of a Matter climate entity.""" + + _attr_temperature_unit: str = UnitOfTemperature.CELSIUS + _attr_supported_features: ClimateEntityFeature = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) + _attr_hvac_mode: HVACMode = HVACMode.OFF + + def __init__( + self, + matter_client: MatterClient, + endpoint: MatterEndpoint, + entity_info: MatterEntityInfo, + ) -> None: + """Initialize the Matter climate entity.""" + super().__init__(matter_client, endpoint, entity_info) + + # set hvac_modes based on feature map + self._attr_hvac_modes: list[HVACMode] = [HVACMode.OFF] + feature_map = int( + self.get_matter_attribute_value(clusters.Thermostat.Attributes.FeatureMap) + ) + if feature_map & ThermostatFeature.kHeating: + self._attr_hvac_modes.append(HVACMode.HEAT) + if feature_map & ThermostatFeature.kCooling: + self._attr_hvac_modes.append(HVACMode.COOL) + if feature_map & ThermostatFeature.kAutoMode: + self._attr_hvac_modes.append(HVACMode.HEAT_COOL) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + target_hvac_mode: HVACMode | None = kwargs.get(ATTR_HVAC_MODE) + if target_hvac_mode is not None: + await self.async_set_hvac_mode(target_hvac_mode) + + current_mode = target_hvac_mode or self.hvac_mode + command = None + if current_mode in (HVACMode.HEAT, HVACMode.COOL): + # when current mode is either heat or cool, the temperature arg must be provided. + temperature: float | None = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + raise ValueError("Temperature must be provided") + if self.target_temperature is None: + raise ValueError("Current target_temperature should not be None") + command = self._create_optional_setpoint_command( + clusters.Thermostat.Enums.SetpointAdjustMode.kCool + if current_mode == HVACMode.COOL + else clusters.Thermostat.Enums.SetpointAdjustMode.kHeat, + temperature, + self.target_temperature, + ) + elif current_mode == HVACMode.HEAT_COOL: + temperature_low: float | None = kwargs.get(ATTR_TARGET_TEMP_LOW) + temperature_high: float | None = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if temperature_low is None or temperature_high is None: + raise ValueError( + "temperature_low and temperature_high must be provided" + ) + if ( + self.target_temperature_low is None + or self.target_temperature_high is None + ): + raise ValueError( + "current target_temperature_low and target_temperature_high should not be None" + ) + # due to ha send both high and low temperature, we need to check which one is changed + command = self._create_optional_setpoint_command( + clusters.Thermostat.Enums.SetpointAdjustMode.kHeat, + temperature_low, + self.target_temperature_low, + ) + if command is None: + command = self._create_optional_setpoint_command( + clusters.Thermostat.Enums.SetpointAdjustMode.kCool, + temperature_high, + self.target_temperature_high, + ) + if command: + await self.matter_client.send_device_command( + node_id=self._endpoint.node.node_id, + endpoint_id=self._endpoint.endpoint_id, + command=command, + ) + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target hvac mode.""" + system_mode_path = create_attribute_path_from_attribute( + endpoint_id=self._endpoint.endpoint_id, + attribute=clusters.Thermostat.Attributes.SystemMode, + ) + system_mode_value = HVAC_SYSTEM_MODE_MAP.get(hvac_mode) + if system_mode_value is None: + raise ValueError(f"Unsupported hvac mode {hvac_mode} in Matter") + await self.matter_client.write_attribute( + node_id=self._endpoint.node.node_id, + attribute_path=system_mode_path, + value=system_mode_value, + ) + # we need to optimistically update the attribute's value here + # to prevent a race condition when adjusting the mode and temperature + # in the same call + self._endpoint.set_attribute_value(system_mode_path, system_mode_value) + self._update_from_device() + + @callback + def _update_from_device(self) -> None: + """Update from device.""" + self._attr_current_temperature = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.LocalTemperature + ) + # update hvac_mode from SystemMode + system_mode_value = int( + self.get_matter_attribute_value(clusters.Thermostat.Attributes.SystemMode) + ) + match system_mode_value: + case SystemModeEnum.kAuto: + self._attr_hvac_mode = HVACMode.HEAT_COOL + case SystemModeEnum.kDry: + self._attr_hvac_mode = HVACMode.DRY + case SystemModeEnum.kFanOnly: + self._attr_hvac_mode = HVACMode.FAN_ONLY + case SystemModeEnum.kCool | SystemModeEnum.kPrecooling: + self._attr_hvac_mode = HVACMode.COOL + case SystemModeEnum.kHeat | SystemModeEnum.kEmergencyHeat: + self._attr_hvac_mode = HVACMode.HEAT + case _: + self._attr_hvac_mode = HVACMode.OFF + # running state is an optional attribute + # which we map to hvac_action if it exists (its value is not None) + self._attr_hvac_action = None + if running_state_value := self.get_matter_attribute_value( + clusters.Thermostat.Attributes.ThermostatRunningState + ): + match running_state_value: + case ThermostatRunningState.Heat | ThermostatRunningState.HeatStage2: + self._attr_hvac_action = HVACAction.HEATING + case ThermostatRunningState.Cool | ThermostatRunningState.CoolStage2: + self._attr_hvac_action = HVACAction.COOLING + case ( + ThermostatRunningState.Fan + | ThermostatRunningState.FanStage2 + | ThermostatRunningState.FanStage3 + ): + self._attr_hvac_action = HVACAction.FAN + case _: + self._attr_hvac_action = HVACAction.OFF + # update target_temperature + if self._attr_hvac_mode == HVACMode.HEAT_COOL: + self._attr_target_temperature = None + elif self._attr_hvac_mode == HVACMode.COOL: + self._attr_target_temperature = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.OccupiedCoolingSetpoint + ) + else: + self._attr_target_temperature = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.OccupiedHeatingSetpoint + ) + # update target temperature high/low + if self._attr_hvac_mode == HVACMode.HEAT_COOL: + self._attr_target_temperature_high = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.OccupiedCoolingSetpoint + ) + self._attr_target_temperature_low = self._get_temperature_in_degrees( + clusters.Thermostat.Attributes.OccupiedHeatingSetpoint + ) + else: + self._attr_target_temperature_high = None + self._attr_target_temperature_low = None + # update min_temp + if self._attr_hvac_mode == HVACMode.COOL: + attribute = clusters.Thermostat.Attributes.AbsMinCoolSetpointLimit + else: + attribute = clusters.Thermostat.Attributes.AbsMinHeatSetpointLimit + if (value := self._get_temperature_in_degrees(attribute)) is not None: + self._attr_min_temp = value + else: + self._attr_min_temp = DEFAULT_MIN_TEMP + # update max_temp + if self._attr_hvac_mode in (HVACMode.COOL, HVACMode.HEAT_COOL): + attribute = clusters.Thermostat.Attributes.AbsMaxHeatSetpointLimit + else: + attribute = clusters.Thermostat.Attributes.AbsMaxCoolSetpointLimit + if (value := self._get_temperature_in_degrees(attribute)) is not None: + self._attr_max_temp = value + else: + self._attr_max_temp = DEFAULT_MAX_TEMP + + def _get_temperature_in_degrees( + self, attribute: type[clusters.ClusterAttributeDescriptor] + ) -> float | None: + """Return the scaled temperature value for the given attribute.""" + if value := self.get_matter_attribute_value(attribute): + return float(value) / TEMPERATURE_SCALING_FACTOR + return None + + @staticmethod + def _create_optional_setpoint_command( + mode: clusters.Thermostat.Enums.SetpointAdjustMode, + target_temp: float, + current_target_temp: float, + ) -> clusters.Thermostat.Commands.SetpointRaiseLower | None: + """Create a setpoint command if the target temperature is different from the current one.""" + + temp_diff = int((target_temp - current_target_temp) * 10) + + if temp_diff == 0: + return None + + return clusters.Thermostat.Commands.SetpointRaiseLower( + mode, + temp_diff, + ) + + +# Discovery schema(s) to map Matter Attributes to HA entities +DISCOVERY_SCHEMAS = [ + MatterDiscoverySchema( + platform=Platform.CLIMATE, + entity_description=ClimateEntityDescription( + key="MatterThermostat", + name=None, + ), + entity_class=MatterClimate, + required_attributes=(clusters.Thermostat.Attributes.LocalTemperature,), + optional_attributes=( + clusters.Thermostat.Attributes.FeatureMap, + clusters.Thermostat.Attributes.ControlSequenceOfOperation, + clusters.Thermostat.Attributes.Occupancy, + clusters.Thermostat.Attributes.OccupiedCoolingSetpoint, + clusters.Thermostat.Attributes.OccupiedHeatingSetpoint, + clusters.Thermostat.Attributes.SystemMode, + clusters.Thermostat.Attributes.ThermostatRunningMode, + clusters.Thermostat.Attributes.ThermostatRunningState, + clusters.Thermostat.Attributes.TemperatureSetpointHold, + clusters.Thermostat.Attributes.UnoccupiedCoolingSetpoint, + clusters.Thermostat.Attributes.UnoccupiedHeatingSetpoint, + ), + device_type=(device_types.Thermostat,), + ), +] diff --git a/homeassistant/components/matter/discovery.py b/homeassistant/components/matter/discovery.py index 28f5b6b7f90..0b4bacf00ca 100644 --- a/homeassistant/components/matter/discovery.py +++ b/homeassistant/components/matter/discovery.py @@ -10,6 +10,7 @@ from homeassistant.const import Platform from homeassistant.core import callback from .binary_sensor import DISCOVERY_SCHEMAS as BINARY_SENSOR_SCHEMAS +from .climate import DISCOVERY_SCHEMAS as CLIMATE_SENSOR_SCHEMAS from .cover import DISCOVERY_SCHEMAS as COVER_SCHEMAS from .light import DISCOVERY_SCHEMAS as LIGHT_SCHEMAS from .lock import DISCOVERY_SCHEMAS as LOCK_SCHEMAS @@ -19,6 +20,7 @@ from .switch import DISCOVERY_SCHEMAS as SWITCH_SCHEMAS DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = { Platform.BINARY_SENSOR: BINARY_SENSOR_SCHEMAS, + Platform.CLIMATE: CLIMATE_SENSOR_SCHEMAS, Platform.COVER: COVER_SCHEMAS, Platform.LIGHT: LIGHT_SCHEMAS, Platform.LOCK: LOCK_SCHEMAS, diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index 56c51d144d8..e1fb4464b83 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -77,6 +77,7 @@ DISCOVERY_SCHEMAS = [ device_types.ColorDimmerSwitch, device_types.DimmerSwitch, device_types.OnOffLightSwitch, + device_types.Thermostat, ), ), ] diff --git a/tests/components/matter/fixtures/nodes/thermostat.json b/tests/components/matter/fixtures/nodes/thermostat.json new file mode 100644 index 00000000000..85ac42e5429 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/thermostat.json @@ -0,0 +1,370 @@ +{ + "node_id": 4, + "date_commissioned": "2023-06-28T16:26:35.525058", + "last_interview": "2023-06-28T16:26:35.525060", + "interview_version": 4, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "deviceType": 22, + "revision": 1 + } + ], + "0/29/1": [29, 31, 40, 42, 48, 49, 50, 51, 54, 60, 62, 63, 64], + "0/29/2": [41], + "0/29/3": [1], + "0/29/65532": 0, + "0/29/65533": 1, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/31/0": [ + { + "privilege": 0, + "authMode": 0, + "subjects": null, + "targets": null, + "fabricIndex": 1 + }, + { + "privilege": 5, + "authMode": 2, + "subjects": [112233], + "targets": null, + "fabricIndex": 2 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 3, + "0/31/65532": 0, + "0/31/65533": 1, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/40/0": 1, + "0/40/1": "LONGAN-LINK", + "0/40/2": 4895, + "0/40/3": "Longan link HVAC", + "0/40/4": 8192, + "0/40/5": "", + "0/40/6": "XX", + "0/40/7": 1, + "0/40/8": "1.0", + "0/40/9": 2, + "0/40/10": "v2.0", + "0/40/11": "20200101", + "0/40/12": "", + "0/40/14": "", + "0/40/15": "5a1fd2d040f23cf66e3a9d2a88e11f78", + "0/40/16": false, + "0/40/17": true, + "0/40/18": "3D06D025F9E026A0", + "0/40/19": { + "caseSessionsPerFabric": 3, + "subscriptionsPerFabric": 3 + }, + "0/40/65532": 0, + "0/40/65533": 1, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 17, 18, 19, 65528, + 65529, 65531, 65532, 65533 + ], + "0/42/0": [], + "0/42/1": true, + "0/42/2": 1, + "0/42/3": null, + "0/42/65532": 0, + "0/42/65533": 1, + "0/42/65528": [], + "0/42/65529": [0], + "0/42/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/48/0": 0, + "0/48/1": { + "failSafeExpiryLengthSeconds": 60, + "maxCumulativeFailsafeSeconds": 900 + }, + "0/48/2": 0, + "0/48/3": 0, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 1, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/49/0": 1, + "0/49/1": [ + { + "networkID": "TE9OR0FOLUlPVA==", + "connected": true + } + ], + "0/49/2": 10, + "0/49/3": 30, + "0/49/4": true, + "0/49/5": 0, + "0/49/6": "TE9OR0FOLUlPVA==", + "0/49/7": null, + "0/49/65532": 1, + "0/49/65533": 1, + "0/49/65528": [1, 5, 7], + "0/49/65529": [0, 2, 4, 6, 8], + "0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533], + "0/50/65532": 0, + "0/50/65533": 1, + "0/50/65528": [1], + "0/50/65529": [0], + "0/50/65531": [65528, 65529, 65531, 65532, 65533], + "0/51/0": [ + { + "name": "WIFI_STA_DEF", + "isOperational": true, + "offPremiseServicesReachableIPv4": null, + "offPremiseServicesReachableIPv6": null, + "hardwareAddress": "3FR1X7qs", + "IPv4Addresses": ["wKgI7g=="], + "IPv6Addresses": [ + "/oAAAAAAAADeVHX//l+6rA==", + "JA4DsgZ9jUDeVHX//l+6rA==", + "/UgvJAe/AADeVHX//l+6rA==" + ], + "type": 1 + } + ], + "0/51/1": 4, + "0/51/2": 30, + "0/51/3": 0, + "0/51/4": 0, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/8": true, + "0/51/65532": 0, + "0/51/65533": 1, + "0/51/65528": [], + "0/51/65529": [0], + "0/51/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533 + ], + "0/54/0": "aHckDXAk", + "0/54/1": 0, + "0/54/2": 3, + "0/54/3": 1, + "0/54/4": -61, + "0/54/5": null, + "0/54/6": null, + "0/54/7": null, + "0/54/8": null, + "0/54/9": null, + "0/54/10": null, + "0/54/11": null, + "0/54/12": null, + "0/54/65532": 3, + "0/54/65533": 1, + "0/54/65528": [], + "0/54/65529": [0], + "0/54/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 65528, 65529, 65531, 65532, + 65533 + ], + "0/60/0": 0, + "0/60/1": null, + "0/60/2": null, + "0/60/65532": 0, + "0/60/65533": 1, + "0/60/65528": [], + "0/60/65529": [0, 1, 2], + "0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/62/0": [ + { + "noc": "", + "icac": null, + "fabricIndex": 1 + }, + { + "noc": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVASQRBBgkBwEkCAEwCUEETaqdhs6MRkbh8fdh4EEImZaziiE6anaVp6Mu3P/zIJUB0fHUMxydKRTAC8bIn7vUhBCM47OYlYTkX0zFhoKYrzcKNQEoARgkAgE2AwQCBAEYMAQUrouBLuksQTkLrFhNVAbTHkNvMSEwBRTPlgMACvPdpqPOzuvR0OfPgfUcxBgwC0AcUInETXp/2gIFGDQF2+u+9WtYtvIfo6C3MhoOIV1SrRBZWYxY3CVjPGK7edTibQrVA4GccZKnHhNSBjxktrPiGA==", + "icac": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEE+rI5XQyifTZbZRK1Z2DOuXdQkmdUkWklTv+G1x4ZfbSupbUDo4l7i/iFdyu//uJThAw1GPEkWe6i98IFKCOQpzcKNQEpARgkAmAwBBTPlgMACvPdpqPOzuvR0OfPgfUcxDAFFJQo6UEBWTLtZVYFZwRBgn+qstpTGDALQK3jYiaxwnYJMwTBQlcVNrGxPtuVTZrp5foZtQCp/JEX2ZWqVxKypilx0ES/CfMHZ0Lllv9QsLs8xV/HNLidllkY", + "fabricIndex": 2 + } + ], + "0/62/1": [ + { + "rootPublicKey": "BAP9BJt5aQ9N98ClPTdNxpMZ1/Vh8r9usw6C8Ygi79AImsJq4UjAaYad0UI9Lh0OmRA9sWE2aSPbHjf409i/970=", + "vendorID": 4996, + "fabricID": 1, + "nodeID": 1425709672, + "label": "", + "fabricIndex": 1 + }, + { + "rootPublicKey": "BJXfyipMp+Jx4pkoTnvYoAYODis4xJktKdQXu8MSpBLIwII58BD0KkIG9NmuHcp0xUQKzqlfyB/bkAanevO73ZI=", + "vendorID": 65521, + "fabricID": 1, + "nodeID": 4, + "label": "", + "fabricIndex": 2 + } + ], + "0/62/2": 5, + "0/62/3": 2, + "0/62/4": [ + "FTABAQAkAgE3AycULlZRw4lgwKgkFQEYJgT1e7grJgV1r5ktNwYnFC5WUcOJYMCoJBUBGCQHASQIATAJQQQD/QSbeWkPTffApT03TcaTGdf1YfK/brMOgvGIIu/QCJrCauFIwGmGndFCPS4dDpkQPbFhNmkj2x43+NPYv/e9Nwo1ASkBGCQCYDAEFCqZHzimE2c+jPoEuJoM1rQaAPFRMAUUKpkfOKYTZz6M+gS4mgzWtBoA8VEYMAtANu49PfywV8aJmtxNYZa7SJXGlK1EciiF6vhZsoqdDCwx1VQX8FdyVunw0H3ljzbvucU6o8aY6HwBsPJKCQVHzhg=", + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEld/KKkyn4nHimShOe9igBg4OKzjEmS0p1Be7wxKkEsjAgjnwEPQqQgb02a4dynTFRArOqV/IH9uQBqd687vdkjcKNQEpARgkAmAwBBSUKOlBAVky7WVWBWcEQYJ/qrLaUzAFFJQo6UEBWTLtZVYFZwRBgn+qstpTGDALQCNU8W3im+pmCBR5A4e15ByjPq2msE05NI9eeFI6BO0p/whhaBSGtjI7Tb1onNNu9AH6AQoji8XDDa7Nj/1w9KoY" + ], + "0/62/5": 2, + "0/62/65532": 0, + "0/62/65533": 1, + "0/62/65528": [1, 3, 5, 8], + "0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11], + "0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533], + "0/63/0": [], + "0/63/1": [], + "0/63/2": 3, + "0/63/3": 3, + "0/63/65532": 0, + "0/63/65533": 1, + "0/63/65528": [2, 5], + "0/63/65529": [0, 1, 3, 4], + "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/64/0": [ + { + "label": "room", + "value": "bedroom 2" + }, + { + "label": "orientation", + "value": "North" + }, + { + "label": "floor", + "value": "2" + }, + { + "label": "direction", + "value": "up" + } + ], + "0/64/65532": 0, + "0/64/65533": 1, + "0/64/65528": [], + "0/64/65529": [], + "0/64/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/3/0": 0, + "1/3/1": 4, + "1/3/65532": 0, + "1/3/65533": 4, + "1/3/65528": [], + "1/3/65529": [0], + "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/4/0": 128, + "1/4/65532": 1, + "1/4/65533": 4, + "1/4/65528": [0, 1, 2, 3], + "1/4/65529": [0, 1, 2, 3, 4, 5], + "1/4/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/6/0": true, + "1/6/65532": 0, + "1/6/65533": 4, + "1/6/65528": [], + "1/6/65529": [0, 1, 2], + "1/6/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/29/0": [ + { + "deviceType": 769, + "revision": 1 + } + ], + "1/29/1": [3, 4, 6, 29, 30, 64, 513, 514, 516], + "1/29/2": [], + "1/29/3": [], + "1/29/65532": 0, + "1/29/65533": 1, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/30/0": [], + "1/30/65532": 0, + "1/30/65533": 1, + "1/30/65528": [], + "1/30/65529": [], + "1/30/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/64/0": [ + { + "label": "room", + "value": "bedroom 2" + }, + { + "label": "orientation", + "value": "North" + }, + { + "label": "floor", + "value": "2" + }, + { + "label": "direction", + "value": "up" + } + ], + "1/64/65532": 0, + "1/64/65533": 1, + "1/64/65528": [], + "1/64/65529": [], + "1/64/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/513/0": 2830, + "1/513/3": null, + "1/513/4": null, + "1/513/5": null, + "1/513/6": null, + "1/513/9": 0, + "1/513/17": null, + "1/513/18": null, + "1/513/21": 1600, + "1/513/22": 3000, + "1/513/23": 1600, + "1/513/24": 3000, + "1/513/25": 5, + "1/513/27": 4, + "1/513/28": 3, + "1/513/30": 0, + "1/513/65532": 35, + "1/513/65533": 5, + "1/513/65528": [], + "1/513/65529": [0], + "1/513/65531": [ + 0, 3, 4, 5, 6, 9, 17, 18, 21, 22, 23, 24, 25, 27, 28, 30, 65528, 65529, + 65531, 65532, 65533 + ], + "1/514/0": 0, + "1/514/1": 2, + "1/514/2": 0, + "1/514/3": 0, + "1/514/65532": 0, + "1/514/65533": 1, + "1/514/65528": [], + "1/514/65529": [], + "1/514/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/516/0": 0, + "1/516/1": 0, + "1/516/65532": 0, + "1/516/65533": 1, + "1/516/65528": [], + "1/516/65529": [], + "1/516/65531": [0, 1, 65528, 65529, 65531, 65532, 65533] + }, + "attribute_subscriptions": [ + [1, 513, 17], + [1, 6, 0], + [1, 513, 0], + [1, 513, 28], + [1, 513, 65532], + [1, 513, 18], + [1, 513, 30], + [1, 513, 27] + ] +} diff --git a/tests/components/matter/test_climate.py b/tests/components/matter/test_climate.py new file mode 100644 index 00000000000..ec8453b5c56 --- /dev/null +++ b/tests/components/matter/test_climate.py @@ -0,0 +1,399 @@ +"""Test Matter locks.""" +from unittest.mock import MagicMock, call + +from chip.clusters import Objects as clusters +from matter_server.client.models.node import MatterNode +from matter_server.common.helpers.util import create_attribute_path_from_attribute +import pytest + +from homeassistant.components.climate import ( + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVACAction, + HVACMode, +) +from homeassistant.components.climate.const import ( + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_OFF, +) +from homeassistant.core import HomeAssistant + +from .common import ( + set_node_attribute, + setup_integration_with_node_fixture, + trigger_subscription_callback, +) + + +@pytest.fixture(name="thermostat") +async def thermostat_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a thermostat node.""" + return await setup_integration_with_node_fixture(hass, "thermostat", matter_client) + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_thermostat( + hass: HomeAssistant, + matter_client: MagicMock, + thermostat: MatterNode, +) -> None: + """Test thermostat.""" + # test default temp range + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["min_temp"] == 7 + assert state.attributes["max_temp"] == 35 + + # test set temperature when target temp is None + assert state.attributes["temperature"] is None + assert state.state == HVAC_MODE_COOL + with pytest.raises( + ValueError, match="Current target_temperature should not be None" + ): + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.longan_link_hvac", + "temperature": 22.5, + }, + blocking=True, + ) + with pytest.raises(ValueError, match="Temperature must be provided"): + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.longan_link_hvac", + "target_temp_low": 18, + "target_temp_high": 26, + }, + blocking=True, + ) + + # change system mode to heat_cool + set_node_attribute(thermostat, 1, 513, 28, 1) + await trigger_subscription_callback(hass, matter_client) + with pytest.raises( + ValueError, + match="current target_temperature_low and target_temperature_high should not be None", + ): + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.state == HVAC_MODE_HEAT_COOL + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.longan_link_hvac", + "target_temp_low": 18, + "target_temp_high": 26, + }, + blocking=True, + ) + + # initial state + set_node_attribute(thermostat, 1, 513, 3, 1600) + set_node_attribute(thermostat, 1, 513, 4, 3000) + set_node_attribute(thermostat, 1, 513, 5, 1600) + set_node_attribute(thermostat, 1, 513, 6, 3000) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["min_temp"] == 16 + assert state.attributes["max_temp"] == 30 + assert state.attributes["hvac_modes"] == [ + HVACMode.OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + ] + + # test system mode update from device + set_node_attribute(thermostat, 1, 513, 28, 0) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.state == HVAC_MODE_OFF + + set_node_attribute(thermostat, 1, 513, 28, 7) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.state == HVAC_MODE_FAN_ONLY + + set_node_attribute(thermostat, 1, 513, 28, 8) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.state == HVAC_MODE_DRY + + # test running state update from device + set_node_attribute(thermostat, 1, 513, 41, 1) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["hvac_action"] == HVACAction.HEATING + + set_node_attribute(thermostat, 1, 513, 41, 8) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["hvac_action"] == HVACAction.HEATING + + set_node_attribute(thermostat, 1, 513, 41, 2) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["hvac_action"] == HVACAction.COOLING + + set_node_attribute(thermostat, 1, 513, 41, 16) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["hvac_action"] == HVACAction.COOLING + + set_node_attribute(thermostat, 1, 513, 41, 4) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["hvac_action"] == HVACAction.FAN + + set_node_attribute(thermostat, 1, 513, 41, 32) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["hvac_action"] == HVACAction.FAN + + set_node_attribute(thermostat, 1, 513, 41, 64) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["hvac_action"] == HVACAction.FAN + + set_node_attribute(thermostat, 1, 513, 41, 66) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["hvac_action"] == HVACAction.OFF + + # change system mode to heat + set_node_attribute(thermostat, 1, 513, 28, 4) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.state == HVAC_MODE_HEAT + + # change occupied heating setpoint to 20 + set_node_attribute(thermostat, 1, 513, 18, 2000) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["temperature"] == 20 + + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.longan_link_hvac", + "temperature": 25, + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=thermostat.node_id, + endpoint_id=1, + command=clusters.Thermostat.Commands.SetpointRaiseLower( + clusters.Thermostat.Enums.SetpointAdjustMode.kHeat, + 50, + ), + ) + matter_client.send_device_command.reset_mock() + + # change system mode to cool + set_node_attribute(thermostat, 1, 513, 28, 3) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.state == HVAC_MODE_COOL + + # change occupied cooling setpoint to 18 + set_node_attribute(thermostat, 1, 513, 17, 1800) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["temperature"] == 18 + + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.longan_link_hvac", + "temperature": 16, + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=thermostat.node_id, + endpoint_id=1, + command=clusters.Thermostat.Commands.SetpointRaiseLower( + clusters.Thermostat.Enums.SetpointAdjustMode.kCool, -20 + ), + ) + matter_client.send_device_command.reset_mock() + + # change system mode to heat_cool + set_node_attribute(thermostat, 1, 513, 28, 1) + await trigger_subscription_callback(hass, matter_client) + with pytest.raises( + ValueError, match="temperature_low and temperature_high must be provided" + ): + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.longan_link_hvac", + "temperature": 18, + }, + blocking=True, + ) + + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.state == HVAC_MODE_HEAT_COOL + + # change occupied cooling setpoint to 18 + set_node_attribute(thermostat, 1, 513, 17, 2500) + await trigger_subscription_callback(hass, matter_client) + # change occupied heating setpoint to 18 + set_node_attribute(thermostat, 1, 513, 18, 1700) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("climate.longan_link_hvac") + assert state + assert state.attributes["target_temp_low"] == 17 + assert state.attributes["target_temp_high"] == 25 + + # change target_temp_low to 18 + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.longan_link_hvac", + "target_temp_low": 18, + "target_temp_high": 25, + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=thermostat.node_id, + endpoint_id=1, + command=clusters.Thermostat.Commands.SetpointRaiseLower( + clusters.Thermostat.Enums.SetpointAdjustMode.kHeat, 10 + ), + ) + matter_client.send_device_command.reset_mock() + set_node_attribute(thermostat, 1, 513, 18, 1800) + await trigger_subscription_callback(hass, matter_client) + + # change target_temp_high to 26 + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.longan_link_hvac", + "target_temp_low": 18, + "target_temp_high": 26, + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=thermostat.node_id, + endpoint_id=1, + command=clusters.Thermostat.Commands.SetpointRaiseLower( + clusters.Thermostat.Enums.SetpointAdjustMode.kCool, 10 + ), + ) + matter_client.send_device_command.reset_mock() + set_node_attribute(thermostat, 1, 513, 17, 2600) + await trigger_subscription_callback(hass, matter_client) + + await hass.services.async_call( + "climate", + "set_hvac_mode", + { + "entity_id": "climate.longan_link_hvac", + "hvac_mode": HVAC_MODE_HEAT, + }, + blocking=True, + ) + + assert matter_client.write_attribute.call_count == 1 + assert matter_client.write_attribute.call_args == call( + node_id=thermostat.node_id, + attribute_path=create_attribute_path_from_attribute( + endpoint_id=1, + attribute=clusters.Thermostat.Attributes.SystemMode, + ), + value=4, + ) + matter_client.send_device_command.reset_mock() + + with pytest.raises(ValueError, match="Unsupported hvac mode dry in Matter"): + await hass.services.async_call( + "climate", + "set_hvac_mode", + { + "entity_id": "climate.longan_link_hvac", + "hvac_mode": HVACMode.DRY, + }, + blocking=True, + ) + + # change target_temp and hvac_mode in the same call + matter_client.send_device_command.reset_mock() + matter_client.write_attribute.reset_mock() + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.longan_link_hvac", + "temperature": 22, + "hvac_mode": HVACMode.COOL, + }, + blocking=True, + ) + assert matter_client.write_attribute.call_count == 1 + assert matter_client.write_attribute.call_args == call( + node_id=thermostat.node_id, + attribute_path=create_attribute_path_from_attribute( + endpoint_id=1, + attribute=clusters.Thermostat.Attributes.SystemMode, + ), + value=3, + ) + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=thermostat.node_id, + endpoint_id=1, + command=clusters.Thermostat.Commands.SetpointRaiseLower( + clusters.Thermostat.Enums.SetpointAdjustMode.kCool, -40 + ), + ) From 1e66aaff0d06e3cc7b1e2667583389cf3db57193 Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Mon, 3 Jul 2023 05:23:32 -0400 Subject: [PATCH 071/184] Bump env_canada to v0.5.35 (#95497) Co-authored-by: J. Nick Koston --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 8fba07198f2..4a8a9dec587 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.5.34"] + "requirements": ["env-canada==0.5.35"] } diff --git a/requirements_all.txt b/requirements_all.txt index 87f74d66523..b9cb3b7db3e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -730,7 +730,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.5.34 +env-canada==0.5.35 # homeassistant.components.enphase_envoy envoy-reader==0.20.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 86e22c3644c..6d19fdb49f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -583,7 +583,7 @@ energyzero==0.4.1 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.5.34 +env-canada==0.5.35 # homeassistant.components.enphase_envoy envoy-reader==0.20.1 From e4e5ecf9b44e25283461f3d438ccf5d9d67a006a Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 30 Jun 2023 13:06:26 -0500 Subject: [PATCH 072/184] Ensure trigger sentences do not contain punctuation (#95633) * Ensure trigger sentences do not contain punctuation * Update homeassistant/components/conversation/trigger.py Co-authored-by: Paulus Schoutsen --------- Co-authored-by: Paulus Schoutsen --- .../components/conversation/trigger.py | 15 +++++++++++- tests/components/conversation/test_trigger.py | 23 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/conversation/trigger.py b/homeassistant/components/conversation/trigger.py index 5bd270ccfd5..b64b74c5fa6 100644 --- a/homeassistant/components/conversation/trigger.py +++ b/homeassistant/components/conversation/trigger.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any +from hassil.recognize import PUNCTUATION import voluptuous as vol from homeassistant.const import CONF_COMMAND, CONF_PLATFORM @@ -15,10 +16,22 @@ from . import HOME_ASSISTANT_AGENT, _get_agent_manager from .const import DOMAIN from .default_agent import DefaultAgent + +def has_no_punctuation(value: list[str]) -> list[str]: + """Validate result does not contain punctuation.""" + for sentence in value: + if PUNCTUATION.search(sentence): + raise vol.Invalid("sentence should not contain punctuation") + + return value + + TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): DOMAIN, - vol.Required(CONF_COMMAND): vol.All(cv.ensure_list, [cv.string]), + vol.Required(CONF_COMMAND): vol.All( + cv.ensure_list, [cv.string], has_no_punctuation + ), } ) diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py index 74a5e4df8e2..522162fa457 100644 --- a/tests/components/conversation/test_trigger.py +++ b/tests/components/conversation/test_trigger.py @@ -1,7 +1,9 @@ """Test conversation triggers.""" import pytest +import voluptuous as vol from homeassistant.core import HomeAssistant +from homeassistant.helpers import trigger from homeassistant.setup import async_setup_component from tests.common import async_mock_service @@ -165,3 +167,24 @@ async def test_same_sentence_multiple_triggers( ("trigger1", "conversation", "hello"), ("trigger2", "conversation", "hello"), } + + +@pytest.mark.parametrize( + "command", + ["hello?", "hello!", "4 a.m."], +) +async def test_fails_on_punctuation(hass: HomeAssistant, command: str) -> None: + """Test that validation fails when sentences contain punctuation.""" + with pytest.raises(vol.Invalid): + await trigger.async_validate_trigger_config( + hass, + [ + { + "id": "trigger1", + "platform": "conversation", + "command": [ + command, + ], + }, + ], + ) From 2e18641863e98f37dbc5aa75ced6cfb6be1cc3ca Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Mon, 3 Jul 2023 09:12:17 +0200 Subject: [PATCH 073/184] Bump bimmer_connected to 0.13.8 (#95660) Co-authored-by: rikroe Co-authored-by: J. Nick Koston --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index d30198bdc12..82426fbce08 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "iot_class": "cloud_polling", "loggers": ["bimmer_connected"], - "requirements": ["bimmer-connected==0.13.7"] + "requirements": ["bimmer-connected==0.13.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index b9cb3b7db3e..047987c7341 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -500,7 +500,7 @@ beautifulsoup4==4.11.1 bellows==0.35.8 # homeassistant.components.bmw_connected_drive -bimmer-connected==0.13.7 +bimmer-connected==0.13.8 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d19fdb49f5..9d0f3f6d2b1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -421,7 +421,7 @@ beautifulsoup4==4.11.1 bellows==0.35.8 # homeassistant.components.bmw_connected_drive -bimmer-connected==0.13.7 +bimmer-connected==0.13.8 # homeassistant.components.bluetooth bleak-retry-connector==3.0.2 From 806b0cb1b69dde13fd12e8bd6cca2ce38b269112 Mon Sep 17 00:00:00 2001 From: Mike Woudenberg Date: Mon, 3 Jul 2023 03:52:52 +0200 Subject: [PATCH 074/184] Quality improvement on LOQED integration (#95725) Remove generated translation Raise error correctly Remove obsolete consts Remove callback, hass assignment and info log Use name from LOQED API instead of default name Correct entity name for assertion --- homeassistant/components/loqed/config_flow.py | 9 ++-- homeassistant/components/loqed/const.py | 2 - homeassistant/components/loqed/coordinator.py | 9 ++-- homeassistant/components/loqed/entity.py | 2 +- homeassistant/components/loqed/lock.py | 8 ++-- homeassistant/components/loqed/strings.json | 3 +- .../components/loqed/translations/en.json | 22 --------- tests/components/loqed/test_init.py | 46 ++++--------------- tests/components/loqed/test_lock.py | 10 ++-- 9 files changed, 30 insertions(+), 81 deletions(-) delete mode 100644 homeassistant/components/loqed/translations/en.json diff --git a/homeassistant/components/loqed/config_flow.py b/homeassistant/components/loqed/config_flow.py index c757d2f0080..5eecc0b3f59 100644 --- a/homeassistant/components/loqed/config_flow.py +++ b/homeassistant/components/loqed/config_flow.py @@ -44,9 +44,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) cloud_client = cloud_loqed.LoqedCloudAPI(cloud_api_client) lock_data = await cloud_client.async_get_locks() - except aiohttp.ClientError: + except aiohttp.ClientError as err: _LOGGER.error("HTTP Connection error to loqed API") - raise CannotConnect from aiohttp.ClientError + raise CannotConnect from err try: selected_lock = next( @@ -137,7 +137,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_auth" else: await self.async_set_unique_id( - re.sub(r"LOQED-([a-f0-9]+)\.local", r"\1", info["bridge_mdns_hostname"]) + re.sub( + r"LOQED-([a-f0-9]+)\.local", r"\1", info["bridge_mdns_hostname"] + ), + raise_on_progress=False, ) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/loqed/const.py b/homeassistant/components/loqed/const.py index 0374e72d5f0..6b1c0311a2d 100644 --- a/homeassistant/components/loqed/const.py +++ b/homeassistant/components/loqed/const.py @@ -2,5 +2,3 @@ DOMAIN = "loqed" -OAUTH2_AUTHORIZE = "https://app.loqed.com/API/integration_oauth3/login.php" -OAUTH2_TOKEN = "https://app.loqed.com/API/integration_oauth3/token.php" diff --git a/homeassistant/components/loqed/coordinator.py b/homeassistant/components/loqed/coordinator.py index ec7d5467b49..507debc02ab 100644 --- a/homeassistant/components/loqed/coordinator.py +++ b/homeassistant/components/loqed/coordinator.py @@ -8,8 +8,8 @@ from loqedAPI import loqed from homeassistant.components import webhook from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_WEBHOOK_ID -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import CONF_NAME, CONF_WEBHOOK_ID +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN @@ -79,17 +79,16 @@ class LoqedDataCoordinator(DataUpdateCoordinator[StatusMessage]): ) -> None: """Initialize the Loqed Data Update coordinator.""" super().__init__(hass, _LOGGER, name="Loqed sensors") - self._hass = hass self._api = api self._entry = entry self.lock = lock + self.device_name = self._entry.data[CONF_NAME] async def _async_update_data(self) -> StatusMessage: """Fetch data from API endpoint.""" async with async_timeout.timeout(10): return await self._api.async_get_lock_details() - @callback async def _handle_webhook( self, hass: HomeAssistant, webhook_id: str, request: Request ) -> None: @@ -116,7 +115,7 @@ class LoqedDataCoordinator(DataUpdateCoordinator[StatusMessage]): self.hass, DOMAIN, "Loqed", webhook_id, self._handle_webhook ) webhook_url = webhook.async_generate_url(self.hass, webhook_id) - _LOGGER.info("Webhook URL: %s", webhook_url) + _LOGGER.debug("Webhook URL: %s", webhook_url) webhooks = await self.lock.getWebhooks() diff --git a/homeassistant/components/loqed/entity.py b/homeassistant/components/loqed/entity.py index 1b1731815b4..978fe844d61 100644 --- a/homeassistant/components/loqed/entity.py +++ b/homeassistant/components/loqed/entity.py @@ -23,7 +23,7 @@ class LoqedEntity(CoordinatorEntity[LoqedDataCoordinator]): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, lock_id)}, manufacturer="LOQED", - name="LOQED Lock", + name=coordinator.device_name, model="Touch Smart Lock", connections={(CONNECTION_NETWORK_MAC, lock_id)}, ) diff --git a/homeassistant/components/loqed/lock.py b/homeassistant/components/loqed/lock.py index 5a7540ba89e..d34df19e2d1 100644 --- a/homeassistant/components/loqed/lock.py +++ b/homeassistant/components/loqed/lock.py @@ -24,7 +24,7 @@ async def async_setup_entry( """Set up the Loqed lock platform.""" coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([LoqedLock(coordinator, entry.data["name"])]) + async_add_entities([LoqedLock(coordinator)]) class LoqedLock(LoqedEntity, LockEntity): @@ -32,17 +32,17 @@ class LoqedLock(LoqedEntity, LockEntity): _attr_supported_features = LockEntityFeature.OPEN - def __init__(self, coordinator: LoqedDataCoordinator, name: str) -> None: + def __init__(self, coordinator: LoqedDataCoordinator) -> None: """Initialize the lock.""" super().__init__(coordinator) self._lock = coordinator.lock self._attr_unique_id = self._lock.id - self._attr_name = name + self._attr_name = None @property def changed_by(self) -> str: """Return internal ID of last used key.""" - return "KeyID " + str(self._lock.last_key_id) + return f"KeyID {self._lock.last_key_id}" @property def is_locking(self) -> bool | None: diff --git a/homeassistant/components/loqed/strings.json b/homeassistant/components/loqed/strings.json index 5448f01b7f9..6f3316b283f 100644 --- a/homeassistant/components/loqed/strings.json +++ b/homeassistant/components/loqed/strings.json @@ -12,8 +12,7 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" diff --git a/homeassistant/components/loqed/translations/en.json b/homeassistant/components/loqed/translations/en.json deleted file mode 100644 index a961f10cb1b..00000000000 --- a/homeassistant/components/loqed/translations/en.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Device is already configured" - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "flow_title": "LOQED Touch Smartlock setup", - "step": { - "user": { - "data": { - "api_key": "API Key", - "name": "Name of your lock in the LOQED app." - }, - "description": "Login at {config_url} and: \n* Create an API-key by clicking 'Create' \n* Copy the created access token." - } - } - } -} \ No newline at end of file diff --git a/tests/components/loqed/test_init.py b/tests/components/loqed/test_init.py index 89b67ee3258..960ad9def6b 100644 --- a/tests/components/loqed/test_init.py +++ b/tests/components/loqed/test_init.py @@ -15,31 +15,6 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture -async def test_webhook_rejects_invalid_message( - hass: HomeAssistant, - hass_client_no_auth, - integration: MockConfigEntry, - lock: loqed.Lock, -): - """Test webhook called with invalid message.""" - await async_setup_component(hass, "http", {"http": {}}) - client = await hass_client_no_auth() - - coordinator = hass.data[DOMAIN][integration.entry_id] - lock.receiveWebhook = AsyncMock(return_value={"error": "invalid hash"}) - - with patch.object(coordinator, "async_set_updated_data") as mock: - message = load_fixture("loqed/battery_update.json") - timestamp = 1653304609 - await client.post( - f"/api/webhook/{integration.data[CONF_WEBHOOK_ID]}", - data=message, - headers={"timestamp": str(timestamp), "hash": "incorrect hash"}, - ) - - mock.assert_not_called() - - async def test_webhook_accepts_valid_message( hass: HomeAssistant, hass_client_no_auth, @@ -49,20 +24,17 @@ async def test_webhook_accepts_valid_message( """Test webhook called with valid message.""" await async_setup_component(hass, "http", {"http": {}}) client = await hass_client_no_auth() - processed_message = json.loads(load_fixture("loqed/battery_update.json")) - coordinator = hass.data[DOMAIN][integration.entry_id] + processed_message = json.loads(load_fixture("loqed/lock_going_to_nightlock.json")) lock.receiveWebhook = AsyncMock(return_value=processed_message) - with patch.object(coordinator, "async_update_listeners") as mock: - message = load_fixture("loqed/battery_update.json") - timestamp = 1653304609 - await client.post( - f"/api/webhook/{integration.data[CONF_WEBHOOK_ID]}", - data=message, - headers={"timestamp": str(timestamp), "hash": "incorrect hash"}, - ) - - mock.assert_called() + message = load_fixture("loqed/battery_update.json") + timestamp = 1653304609 + await client.post( + f"/api/webhook/{integration.data[CONF_WEBHOOK_ID]}", + data=message, + headers={"timestamp": str(timestamp), "hash": "incorrect hash"}, + ) + lock.receiveWebhook.assert_called() async def test_setup_webhook_in_bridge( diff --git a/tests/components/loqed/test_lock.py b/tests/components/loqed/test_lock.py index 422b7ab6830..59e70212f92 100644 --- a/tests/components/loqed/test_lock.py +++ b/tests/components/loqed/test_lock.py @@ -21,7 +21,7 @@ async def test_lock_entity( integration: MockConfigEntry, ) -> None: """Test the lock entity.""" - entity_id = "lock.loqed_lock_home" + entity_id = "lock.home" state = hass.states.get(entity_id) @@ -37,7 +37,7 @@ async def test_lock_responds_to_bolt_state_updates( lock.bolt_state = "night_lock" coordinator.async_update_listeners() - entity_id = "lock.loqed_lock_home" + entity_id = "lock.home" state = hass.states.get(entity_id) @@ -50,7 +50,7 @@ async def test_lock_transition_to_unlocked( ) -> None: """Tests the lock transitions to unlocked state.""" - entity_id = "lock.loqed_lock_home" + entity_id = "lock.home" await hass.services.async_call( "lock", SERVICE_UNLOCK, {ATTR_ENTITY_ID: entity_id}, blocking=True @@ -64,7 +64,7 @@ async def test_lock_transition_to_locked( ) -> None: """Tests the lock transitions to locked state.""" - entity_id = "lock.loqed_lock_home" + entity_id = "lock.home" await hass.services.async_call( "lock", SERVICE_LOCK, {ATTR_ENTITY_ID: entity_id}, blocking=True @@ -78,7 +78,7 @@ async def test_lock_transition_to_open( ) -> None: """Tests the lock transitions to open state.""" - entity_id = "lock.loqed_lock_home" + entity_id = "lock.home" await hass.services.async_call( "lock", SERVICE_OPEN, {ATTR_ENTITY_ID: entity_id}, blocking=True From d0e60f66fab4a70bae5bef4b1102fd4b12057e4a Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 3 Jul 2023 14:19:05 +0200 Subject: [PATCH 075/184] Bump aioslimproto to 2.3.2 (#95754) --- homeassistant/components/slimproto/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/slimproto/manifest.json b/homeassistant/components/slimproto/manifest.json index f3008d9f1c1..1ef87e84933 100644 --- a/homeassistant/components/slimproto/manifest.json +++ b/homeassistant/components/slimproto/manifest.json @@ -6,5 +6,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/slimproto", "iot_class": "local_push", - "requirements": ["aioslimproto==2.1.1"] + "requirements": ["aioslimproto==2.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 047987c7341..111bff60557 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -345,7 +345,7 @@ aioshelly==5.4.0 aioskybell==22.7.0 # homeassistant.components.slimproto -aioslimproto==2.1.1 +aioslimproto==2.3.2 # homeassistant.components.steamist aiosteamist==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d0f3f6d2b1..53c9fe23954 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -317,7 +317,7 @@ aioshelly==5.4.0 aioskybell==22.7.0 # homeassistant.components.slimproto -aioslimproto==2.1.1 +aioslimproto==2.3.2 # homeassistant.components.steamist aiosteamist==0.3.2 From 2720e2fadefbfd26bfceee16ca9e9d14840bc499 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 3 Jul 2023 16:41:51 +0200 Subject: [PATCH 076/184] Fix translation growatt inverter temperature (#95775) --- homeassistant/components/growatt_server/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/growatt_server/strings.json b/homeassistant/components/growatt_server/strings.json index b9d0e63ecba..d2c196dbfdd 100644 --- a/homeassistant/components/growatt_server/strings.json +++ b/homeassistant/components/growatt_server/strings.json @@ -83,7 +83,7 @@ "name": "Intelligent Power Management temperature" }, "inverter_temperature": { - "name": "Energytoday" + "name": "Inverter temperature" }, "mix_statement_of_charge": { "name": "Statement of charge" From 601ca8562ca57773993b75b31dcaf82942fc235b Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 3 Jul 2023 21:22:22 +0200 Subject: [PATCH 077/184] Fix datetime parameter validation for list events (#95778) --- homeassistant/components/calendar/__init__.py | 4 ++-- homeassistant/components/demo/calendar.py | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 86f61f0ed87..0086d20ba5e 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -264,8 +264,8 @@ SERVICE_LIST_EVENTS_SCHEMA: Final = vol.All( cv.has_at_most_one_key(EVENT_END_DATETIME, EVENT_DURATION), cv.make_entity_service_schema( { - vol.Optional(EVENT_START_DATETIME): datetime.datetime, - vol.Optional(EVENT_END_DATETIME): datetime.datetime, + vol.Optional(EVENT_START_DATETIME): cv.datetime, + vol.Optional(EVENT_END_DATETIME): cv.datetime, vol.Optional(EVENT_DURATION): vol.All( cv.time_period, cv.positive_timedelta ), diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index 73b45a55640..92dbf8d47b8 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -67,6 +67,14 @@ class DemoCalendar(CalendarEntity): end_date: datetime.datetime, ) -> list[CalendarEvent]: """Return calendar events within a datetime range.""" + if start_date.tzinfo is None: + start_date = start_date.replace( + tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) + if end_date.tzinfo is None: + end_date = end_date.replace( + tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) assert start_date < end_date if self._event.start_datetime_local >= end_date: return [] From 3c0d2343324fd2fc6df31e0b76d5ba0ca1e38c7e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 Jul 2023 17:38:03 +0200 Subject: [PATCH 078/184] Fix implicit use of device name in TwenteMilieu (#95780) --- homeassistant/components/twentemilieu/calendar.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/twentemilieu/calendar.py b/homeassistant/components/twentemilieu/calendar.py index e4ecbd9d866..f4d1e51b171 100644 --- a/homeassistant/components/twentemilieu/calendar.py +++ b/homeassistant/components/twentemilieu/calendar.py @@ -32,6 +32,7 @@ class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEntity): _attr_has_entity_name = True _attr_icon = "mdi:delete-empty" + _attr_name = None def __init__( self, From 0ce3b8913911c4233190c7d9300cbc5f87a27794 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 Jul 2023 17:38:54 +0200 Subject: [PATCH 079/184] Fix implicit use of device name in Verisure (#95781) --- homeassistant/components/verisure/alarm_control_panel.py | 1 + homeassistant/components/verisure/switch.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 9615404a9a6..284b8d6b00a 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -35,6 +35,7 @@ class VerisureAlarm( _attr_code_format = CodeFormat.NUMBER _attr_has_entity_name = True + _attr_name = None _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 62e9bdf6cf8..6c3dcd81295 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -32,6 +32,7 @@ class VerisureSmartplug(CoordinatorEntity[VerisureDataUpdateCoordinator], Switch """Representation of a Verisure smartplug.""" _attr_has_entity_name = True + _attr_name = None def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str From 3550a9e2d223395bedc1db5b3117016ce4fee53d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 3 Jul 2023 20:21:01 +0200 Subject: [PATCH 080/184] Fix execute device actions with WS execute_script (#95783) --- .../components/websocket_api/commands.py | 6 +- .../components/websocket_api/test_commands.py | 80 ++++++++++++++++++- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 619fc913e09..143d0617d51 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -671,10 +671,12 @@ async def handle_execute_script( """Handle execute script command.""" # Circular dep # pylint: disable-next=import-outside-toplevel - from homeassistant.helpers.script import Script + from homeassistant.helpers.script import Script, async_validate_actions_config + + script_config = await async_validate_actions_config(hass, msg["sequence"]) context = connection.context(msg) - script_obj = Script(hass, msg["sequence"], f"{const.DOMAIN} script", const.DOMAIN) + script_obj = Script(hass, script_config, f"{const.DOMAIN} script", const.DOMAIN) response = await script_obj.async_run(msg.get("variables"), context=context) connection.send_result( msg["id"], diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 7e46dc0d0bd..ca04fef4f77 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1,12 +1,14 @@ """Tests for WebSocket API commands.""" from copy import deepcopy import datetime -from unittest.mock import ANY, patch +from unittest.mock import ANY, AsyncMock, Mock, patch from async_timeout import timeout import pytest import voluptuous as vol +from homeassistant import config_entries, loader +from homeassistant.components.device_automation import toggle_entity from homeassistant.components.websocket_api import const from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, @@ -17,13 +19,20 @@ from homeassistant.components.websocket_api.const import FEATURE_COALESCE_MESSAG from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATIONS from homeassistant.core import Context, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity +from homeassistant.helpers import device_registry as dr, entity from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.loader import async_get_integration from homeassistant.setup import DATA_SETUP_TIME, async_setup_component from homeassistant.util.json import json_loads -from tests.common import MockEntity, MockEntityPlatform, MockUser, async_mock_service +from tests.common import ( + MockConfigEntry, + MockEntity, + MockEntityPlatform, + MockUser, + async_mock_service, + mock_platform, +) from tests.typing import ( ClientSessionGenerator, WebSocketGenerator, @@ -40,6 +49,25 @@ STATE_KEY_SHORT_NAMES = { STATE_KEY_LONG_NAMES = {v: k for k, v in STATE_KEY_SHORT_NAMES.items()} +@pytest.fixture +def fake_integration(hass: HomeAssistant): + """Set up a mock integration with device automation support.""" + DOMAIN = "fake_integration" + + hass.config.components.add(DOMAIN) + + mock_platform( + hass, + f"{DOMAIN}.device_action", + Mock( + ACTION_SCHEMA=toggle_entity.ACTION_SCHEMA.extend( + {vol.Required("domain"): DOMAIN} + ), + spec=["ACTION_SCHEMA"], + ), + ) + + def _apply_entities_changes(state_dict: dict, change_dict: dict) -> None: """Apply a diff set to a dict. @@ -1774,6 +1802,52 @@ async def test_execute_script_complex_response( } +async def test_execute_script_with_dynamically_validated_action( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + device_registry: dr.DeviceRegistry, + fake_integration, +) -> None: + """Test executing a script with an action which is dynamically validated.""" + + ws_client = await hass_ws_client(hass) + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_action"] + module.async_call_action_from_config = AsyncMock() + module.async_validate_action_config = AsyncMock( + side_effect=lambda hass, config: config + ) + + config_entry = MockConfigEntry(domain="fake_integration", data={}) + config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + await ws_client.send_json_auto_id( + { + "type": "execute_script", + "sequence": [ + { + "device_id": device_entry.id, + "domain": "fake_integration", + }, + ], + } + ) + + msg_no_var = await ws_client.receive_json() + assert msg_no_var["type"] == const.TYPE_RESULT + assert msg_no_var["success"] + assert msg_no_var["result"]["response"] is None + + module.async_validate_action_config.assert_awaited_once() + module.async_call_action_from_config.assert_awaited_once() + + async def test_subscribe_unsubscribe_bootstrap_integrations( hass: HomeAssistant, websocket_client, hass_admin_user: MockUser ) -> None: From d77a168121d7b264044686e828b73b695679b919 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jul 2023 18:36:37 +0200 Subject: [PATCH 081/184] Fix Growatt translation key (#95784) --- .../components/growatt_server/sensor_types/inverter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/growatt_server/sensor_types/inverter.py b/homeassistant/components/growatt_server/sensor_types/inverter.py index 2c06621d079..cfacadce528 100644 --- a/homeassistant/components/growatt_server/sensor_types/inverter.py +++ b/homeassistant/components/growatt_server/sensor_types/inverter.py @@ -161,7 +161,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = ( ), GrowattSensorEntityDescription( key="inverter_temperature", - translation_key="inverter_energy_today", + translation_key="inverter_temperature", api_key="temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, From 8db987e0b3e68aa27c5dd9bf204cf8afedf5e12e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 3 Jul 2023 20:17:24 +0200 Subject: [PATCH 082/184] Update frontend to 20230703.0 (#95795) --- 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 4a1edd4096e..f0875bc15d7 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==20230630.0"] + "requirements": ["home-assistant-frontend==20230703.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a5be8daf6fc..a669976ae74 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==20230630.0 +home-assistant-frontend==20230703.0 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 111bff60557..84cc7159c48 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==20230630.0 +home-assistant-frontend==20230703.0 # homeassistant.components.conversation home-assistant-intents==2023.6.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 53c9fe23954..ed546bc443f 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==20230630.0 +home-assistant-frontend==20230703.0 # homeassistant.components.conversation home-assistant-intents==2023.6.28 From 3df1e9bc99de03909f65109ac0fab4bf6a119821 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 3 Jul 2023 12:05:02 -0700 Subject: [PATCH 083/184] Ensure that calendar output values are json types (#95797) --- homeassistant/components/calendar/__init__.py | 2 +- tests/components/calendar/conftest.py | 8 ++++++++ tests/components/calendar/test_init.py | 17 ++++++++++++----- tests/components/calendar/test_trigger.py | 8 -------- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 0086d20ba5e..3286dd152e8 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -422,7 +422,7 @@ def _list_events_dict_factory( """Convert CalendarEvent dataclass items to dictionary of attributes.""" return { name: value - for name, value in obj + for name, value in _event_dict_factory(obj).items() if name in LIST_EVENT_FIELDS and value is not None } diff --git a/tests/components/calendar/conftest.py b/tests/components/calendar/conftest.py index 4d6b5adfde7..5d506d67c6f 100644 --- a/tests/components/calendar/conftest.py +++ b/tests/components/calendar/conftest.py @@ -9,3 +9,11 @@ from homeassistant.setup import async_setup_component async def setup_homeassistant(hass: HomeAssistant): """Set up the homeassistant integration.""" await async_setup_component(hass, "homeassistant", {}) + + +@pytest.fixture +def set_time_zone(hass: HomeAssistant) -> None: + """Set the time zone for the tests.""" + # Set our timezone to CST/Regina so we can check calculations + # This keeps UTC-6 all year round + hass.config.set_time_zone("America/Regina") diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 9fdc76abe03..463e075d169 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -4,8 +4,9 @@ from __future__ import annotations from datetime import timedelta from http import HTTPStatus from typing import Any -from unittest.mock import ANY, patch +from unittest.mock import patch +from freezegun import freeze_time import pytest import voluptuous as vol @@ -386,8 +387,14 @@ async def test_create_event_service_invalid_params( ) -async def test_list_events_service(hass: HomeAssistant) -> None: - """Test listing events from the service call using exlplicit start and end time.""" +@freeze_time("2023-06-22 10:30:00+00:00") +async def test_list_events_service(hass: HomeAssistant, set_time_zone: None) -> None: + """Test listing events from the service call using exlplicit start and end time. + + This test uses a fixed date/time so that it can deterministically test the + string output values. + """ + await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) await hass.async_block_till_done() @@ -408,8 +415,8 @@ async def test_list_events_service(hass: HomeAssistant) -> None: assert response == { "events": [ { - "start": ANY, - "end": ANY, + "start": "2023-06-22T05:00:00-06:00", + "end": "2023-06-22T06:00:00-06:00", "summary": "Future Event", "description": "Future Description", "location": "Future Location", diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py index 05c7d95d8ad..45dd9d6afe1 100644 --- a/tests/components/calendar/test_trigger.py +++ b/tests/components/calendar/test_trigger.py @@ -121,14 +121,6 @@ class FakeSchedule: await self.fire_time(dt_util.utcnow()) -@pytest.fixture -def set_time_zone(hass: HomeAssistant) -> None: - """Set the time zone for the tests.""" - # Set our timezone to CST/Regina so we can check calculations - # This keeps UTC-6 all year round - hass.config.set_time_zone("America/Regina") - - @pytest.fixture def fake_schedule( hass: HomeAssistant, freezer: FrozenDateTimeFactory From d8f8557ae16ef7d62747f57fef97919cdf83f293 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Jul 2023 15:23:57 -0400 Subject: [PATCH 084/184] Bumped version to 2023.7.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 cec8bf64fa7..b618675cdbf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 7 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 07af4f503c2..434a2ebe630 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.7.0b4" +version = "2023.7.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 239cbb7ad1a2bd372081720a1902a666ff22f943 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 4 Jul 2023 23:25:03 -0700 Subject: [PATCH 085/184] Fix timezones used in list events (#95804) * Fix timezones used in list events * Add additional tests that catch floating vs timezone datetime comparisons --- homeassistant/components/calendar/__init__.py | 4 +++- homeassistant/components/demo/calendar.py | 8 -------- tests/components/calendar/test_init.py | 19 +++++++++++++------ 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 3286dd152e8..d56b2b0ddfa 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -793,7 +793,9 @@ async def async_list_events_service( end = start + service_call.data[EVENT_DURATION] else: end = service_call.data[EVENT_END_DATETIME] - calendar_event_list = await calendar.async_get_events(calendar.hass, start, end) + calendar_event_list = await calendar.async_get_events( + calendar.hass, dt_util.as_local(start), dt_util.as_local(end) + ) return { "events": [ dataclasses.asdict(event, dict_factory=_list_events_dict_factory) diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index 92dbf8d47b8..73b45a55640 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -67,14 +67,6 @@ class DemoCalendar(CalendarEntity): end_date: datetime.datetime, ) -> list[CalendarEvent]: """Return calendar events within a datetime range.""" - if start_date.tzinfo is None: - start_date = start_date.replace( - tzinfo=dt_util.get_time_zone(hass.config.time_zone) - ) - if end_date.tzinfo is None: - end_date = end_date.replace( - tzinfo=dt_util.get_time_zone(hass.config.time_zone) - ) assert start_date < end_date if self._event.start_datetime_local >= end_date: return [] diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 463e075d169..e0fbbf0cdeb 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -388,7 +388,17 @@ async def test_create_event_service_invalid_params( @freeze_time("2023-06-22 10:30:00+00:00") -async def test_list_events_service(hass: HomeAssistant, set_time_zone: None) -> None: +@pytest.mark.parametrize( + ("start_time", "end_time"), + [ + ("2023-06-22T04:30:00-06:00", "2023-06-22T06:30:00-06:00"), + ("2023-06-22T04:30:00", "2023-06-22T06:30:00"), + ("2023-06-22T10:30:00Z", "2023-06-22T12:30:00Z"), + ], +) +async def test_list_events_service( + hass: HomeAssistant, set_time_zone: None, start_time: str, end_time: str +) -> None: """Test listing events from the service call using exlplicit start and end time. This test uses a fixed date/time so that it can deterministically test the @@ -398,16 +408,13 @@ async def test_list_events_service(hass: HomeAssistant, set_time_zone: None) -> await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) await hass.async_block_till_done() - start = dt_util.now() - end = start + timedelta(days=1) - response = await hass.services.async_call( DOMAIN, SERVICE_LIST_EVENTS, { "entity_id": "calendar.calendar_1", - "start_date_time": start, - "end_date_time": end, + "start_date_time": start_time, + "end_date_time": end_time, }, blocking=True, return_response=True, From 89c580f27d5881dd0debfc72f49ed3aee8d3ef67 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 3 Jul 2023 23:55:23 +0200 Subject: [PATCH 086/184] Bump aiounifi to v49 (#95813) --- 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 f48191e471a..9bfb01e5a88 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==48"], + "requirements": ["aiounifi==49"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 84cc7159c48..ff171d9840b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -360,7 +360,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==48 +aiounifi==49 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ed546bc443f..6a19c41d60d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -332,7 +332,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==48 +aiounifi==49 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From e22b79dbd9db706b1b096ee634e486157efe8701 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 4 Jul 2023 13:40:22 +0200 Subject: [PATCH 087/184] Reolink fix missing title_placeholders (#95827) --- homeassistant/components/reolink/config_flow.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index df5bf968ae1..75ad26665c3 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -79,6 +79,10 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._username = entry_data[CONF_USERNAME] self._password = entry_data[CONF_PASSWORD] self._reauth = True + self.context["title_placeholders"]["ip_address"] = entry_data[CONF_HOST] + self.context["title_placeholders"]["hostname"] = self.context[ + "title_placeholders" + ]["name"] return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( From 10a9b063fa57b6446b9448fad2660822db289786 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 4 Jul 2023 12:52:04 +0200 Subject: [PATCH 088/184] Disable proximity no platform log (#95838) --- homeassistant/components/proximity/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index 0567c551d98..a4520435161 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -110,6 +110,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class Proximity(Entity): """Representation of a Proximity.""" + # This entity is legacy and does not have a platform. + # We can't fix this easily without breaking changes. + _no_platform_reported = True + def __init__( self, hass: HomeAssistant, From 577ffef25c026fafdd4a2384c548104d8e333e5c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 4 Jul 2023 13:26:48 +0200 Subject: [PATCH 089/184] Disable legacy device tracker no platform log (#95839) --- homeassistant/components/device_tracker/legacy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index e27ff57f03f..b428018cd9e 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -726,6 +726,10 @@ class DeviceTracker: class Device(RestoreEntity): """Base class for a tracked device.""" + # This entity is legacy and does not have a platform. + # We can't fix this easily without breaking changes. + _no_platform_reported = True + host_name: str | None = None location_name: str | None = None gps: GPSType | None = None From c123c22846f9ad700955fd559f827bbf29566bda Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 4 Jul 2023 14:41:19 +0200 Subject: [PATCH 090/184] Revert "Remove airplay filter now that apple tv supports airplay 2" (#95843) --- .../components/apple_tv/manifest.json | 19 ++++++++++++++++++- homeassistant/generated/zeroconf.py | 15 +++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index 891264d4290..4ead41e86e9 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -16,7 +16,24 @@ "_touch-able._tcp.local.", "_appletv-v2._tcp.local.", "_hscp._tcp.local.", - "_airplay._tcp.local.", + { + "type": "_airplay._tcp.local.", + "properties": { + "model": "appletv*" + } + }, + { + "type": "_airplay._tcp.local.", + "properties": { + "model": "audioaccessory*" + } + }, + { + "type": "_airplay._tcp.local.", + "properties": { + "am": "airport*" + } + }, { "type": "_raop._tcp.local.", "properties": { diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index ccf35e384bb..6b5676c4a25 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -251,6 +251,21 @@ ZEROCONF = { "_airplay._tcp.local.": [ { "domain": "apple_tv", + "properties": { + "model": "appletv*", + }, + }, + { + "domain": "apple_tv", + "properties": { + "model": "audioaccessory*", + }, + }, + { + "domain": "apple_tv", + "properties": { + "am": "airport*", + }, }, { "domain": "samsungtv", From 0bb0e3fe72717dfb233340537bfbbc5d42e297eb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 4 Jul 2023 17:58:15 +0200 Subject: [PATCH 091/184] Remove unsupported services and fields from fan/services.yaml (#95858) --- homeassistant/components/fan/services.yaml | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index cfc44029e23..52d5aca070a 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -1,19 +1,4 @@ # Describes the format for available fan services -set_speed: - name: Set speed - description: Set fan speed. - target: - entity: - domain: fan - fields: - speed: - name: Speed - description: Speed setting. - required: true - example: "low" - selector: - text: - set_preset_mode: name: Set preset mode description: Set preset mode for a fan device. @@ -53,12 +38,6 @@ turn_on: entity: domain: fan fields: - speed: - name: Speed - description: Speed setting. - example: "high" - selector: - text: percentage: name: Percentage description: Percentage speed setting. From 8e2c0984b7a59b2c67bcf129be15a79dc43b6ff8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Jul 2023 12:13:52 -0500 Subject: [PATCH 092/184] Fix reload in cert_expiry (#95867) --- homeassistant/components/cert_expiry/__init__.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 5f6152b7bc7..4fc89bc918b 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -8,10 +8,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PORT, - EVENT_HOMEASSISTANT_STARTED, Platform, ) -from homeassistant.core import CoreState, HomeAssistant +from homeassistant.core import HomeAssistant +from homeassistant.helpers.start import async_at_started from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DEFAULT_PORT, DOMAIN @@ -38,19 +38,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=f"{host}:{port}") - async def async_finish_startup(_): + async def _async_finish_startup(_): await coordinator.async_refresh() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - if hass.state == CoreState.running: - await async_finish_startup(None) - else: - entry.async_on_unload( - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, async_finish_startup - ) - ) - + async_at_started(hass, _async_finish_startup) return True From 284926159b88b3f354b4727515c623f046de4f6b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Jul 2023 01:35:40 -0500 Subject: [PATCH 093/184] Fix ESPHome alarm_control_panel when state is missing (#95871) --- .../components/esphome/alarm_control_panel.py | 2 ++ .../esphome/test_alarm_control_panel.py | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/homeassistant/components/esphome/alarm_control_panel.py b/homeassistant/components/esphome/alarm_control_panel.py index 669241b05aa..639f47272d9 100644 --- a/homeassistant/components/esphome/alarm_control_panel.py +++ b/homeassistant/components/esphome/alarm_control_panel.py @@ -33,6 +33,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import ( EsphomeEntity, + esphome_state_property, platform_async_setup_entry, ) from .enum_mapper import EsphomeEnumMapper @@ -111,6 +112,7 @@ class EsphomeAlarmControlPanel( self._attr_code_arm_required = bool(static_info.requires_code_to_arm) @property + @esphome_state_property def state(self) -> str | None: """Return the state of the device.""" return _ESPHOME_ACP_STATE_TO_HASS_STATE.from_esphome(self._state.state) diff --git a/tests/components/esphome/test_alarm_control_panel.py b/tests/components/esphome/test_alarm_control_panel.py index ddca7bf60ac..90d7bde5215 100644 --- a/tests/components/esphome/test_alarm_control_panel.py +++ b/tests/components/esphome/test_alarm_control_panel.py @@ -24,6 +24,7 @@ from homeassistant.components.esphome.alarm_control_panel import EspHomeACPFeatu from homeassistant.const import ( ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant @@ -209,3 +210,38 @@ async def test_generic_alarm_control_panel_no_code( [call(1, AlarmControlPanelCommand.DISARM, None)] ) mock_client.alarm_control_panel_command.reset_mock() + + +async def test_generic_alarm_control_panel_missing_state( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry, +) -> None: + """Test a generic alarm_control_panel entity that is missing state.""" + entity_info = [ + AlarmControlPanelInfo( + object_id="myalarm_control_panel", + key=1, + name="my alarm_control_panel", + unique_id="my_alarm_control_panel", + supported_features=EspHomeACPFeatures.ARM_AWAY + | EspHomeACPFeatures.ARM_CUSTOM_BYPASS + | EspHomeACPFeatures.ARM_HOME + | EspHomeACPFeatures.ARM_NIGHT + | EspHomeACPFeatures.ARM_VACATION + | EspHomeACPFeatures.TRIGGER, + requires_code=False, + requires_code_to_arm=False, + ) + ] + states = [] + user_service = [] + await mock_generic_device_entry( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("alarm_control_panel.test_my_alarm_control_panel") + assert state is not None + assert state.state == STATE_UNKNOWN From a709b6af4c087f7c8dfb846841975ac8439e77f0 Mon Sep 17 00:00:00 2001 From: Daniel Gangl <31815106+killer0071234@users.noreply.github.com> Date: Wed, 5 Jul 2023 08:50:30 +0200 Subject: [PATCH 094/184] Bump zamg to 0.2.4 (#95874) Co-authored-by: J. Nick Koston --- 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 72a7f8de946..3ff7612d47e 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.2"] + "requirements": ["zamg==0.2.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index ff171d9840b..711776342b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2729,7 +2729,7 @@ youless-api==1.0.1 yt-dlp==2023.3.4 # homeassistant.components.zamg -zamg==0.2.2 +zamg==0.2.4 # homeassistant.components.zengge zengge==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6a19c41d60d..fde7ba48323 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2002,7 +2002,7 @@ yolink-api==0.2.9 youless-api==1.0.1 # homeassistant.components.zamg -zamg==0.2.2 +zamg==0.2.4 # homeassistant.components.zeroconf zeroconf==0.70.0 From 36f6c02c522d6db78055a6086cd14f06e1ba4bc5 Mon Sep 17 00:00:00 2001 From: Emilv2 Date: Wed, 5 Jul 2023 08:35:02 +0200 Subject: [PATCH 095/184] Bump pydelijn to 1.1.0 (#95878) --- homeassistant/components/delijn/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/delijn/manifest.json b/homeassistant/components/delijn/manifest.json index 81307c47bba..d25dab4234e 100644 --- a/homeassistant/components/delijn/manifest.json +++ b/homeassistant/components/delijn/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/delijn", "iot_class": "cloud_polling", "loggers": ["pydelijn"], - "requirements": ["pydelijn==1.0.0"] + "requirements": ["pydelijn==1.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 711776342b6..fb1c68776ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1624,7 +1624,7 @@ pydanfossair==0.1.0 pydeconz==113 # homeassistant.components.delijn -pydelijn==1.0.0 +pydelijn==1.1.0 # homeassistant.components.dexcom pydexcom==0.2.3 From 8ed7390a61314abe5ea15fbbd2df79303d8cdcc9 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 Jul 2023 09:54:23 +0200 Subject: [PATCH 096/184] Update frontend to 20230705.0 (#95890) --- 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 f0875bc15d7..9f53aef8165 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==20230703.0"] + "requirements": ["home-assistant-frontend==20230705.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a669976ae74..1686f91bac3 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==20230703.0 +home-assistant-frontend==20230705.0 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 fb1c68776ed..9ef98d4e402 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==20230703.0 +home-assistant-frontend==20230705.0 # homeassistant.components.conversation home-assistant-intents==2023.6.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fde7ba48323..7c075eb1426 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==20230703.0 +home-assistant-frontend==20230705.0 # homeassistant.components.conversation home-assistant-intents==2023.6.28 From 025969a76f4cd9d5ee8a1699261e3f31522d56d6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 Jul 2023 09:57:56 +0200 Subject: [PATCH 097/184] Bumped version to 2023.7.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 b618675cdbf..15929c40135 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 7 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 434a2ebe630..39c3d81feaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.7.0b5" +version = "2023.7.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From b90df4bdca91359717789b61f9ac13568a64153b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 Jul 2023 16:06:09 +0200 Subject: [PATCH 098/184] Bumped version to 2023.7.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 15929c40135..7b85163fba7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 7 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 39c3d81feaf..cd0e6eb47f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.7.0b6" +version = "2023.7.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2196bd3a13a277fd8ea84fa42728df740139ba28 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 6 Jul 2023 17:14:09 +0200 Subject: [PATCH 099/184] 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 3d22e2538a3..e87eb15b954 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -772,9 +772,10 @@ class Entity(ABC): ): 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 85a7932aef8..7de6f70e793 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -20,7 +20,7 @@ from homeassistant.const import ( 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 _test_friendly_name( @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" Date: Thu, 6 Jul 2023 05:19:06 -1000 Subject: [PATCH 100/184] 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 fa0e57d501c..09c861421b0 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 f6299312b53..b062a323305 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 Date: Wed, 5 Jul 2023 18:20:10 +0200 Subject: [PATCH 101/184] 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 9f53aef8165..07c5585833d 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 1686f91bac3..71ca8fc4c3e 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 9ef98d4e402..c6294603728 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 7c075eb1426..c28835fee55 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" Date: Wed, 5 Jul 2023 18:56:09 -0500 Subject: [PATCH 102/184] 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 2d31a883e4b..fb9f906527c 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 103/184] 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 5607cabffea..8a81648b580 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -3,6 +3,7 @@ import asyncio 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 23a76de4c25..24ee63fb3d5 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 Date: Thu, 6 Jul 2023 16:17:59 +0200 Subject: [PATCH 104/184] 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 55560d3bf8c..a1a75ef8260 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 Date: Thu, 6 Jul 2023 16:19:42 +0200 Subject: [PATCH 105/184] 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 a2cd2d5fc41..afa40f27afd 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -511,6 +511,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "rqbj": ( TuyaSensorEntityDescription( key=DPCode.GAS_SENSOR_VALUE, + name=None, icon="mdi:gas-cylinder", state_class=SensorStateClass.MEASUREMENT, ), @@ -633,6 +634,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "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 Date: Thu, 6 Jul 2023 16:21:15 +0200 Subject: [PATCH 106/184] 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 2210f44bf7a..a0844fe6cdb 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 Date: Thu, 6 Jul 2023 08:50:51 -0400 Subject: [PATCH 107/184] 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 facdb6752d3..02919baa8f1 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -128,7 +128,7 @@ class MatterLight(MatterEntity, LightEntity): renormalize( brightness, (0, 255), - (level_control.minLevel, level_control.maxLevel), + (level_control.minLevel or 1, level_control.maxLevel or 254), ) ) @@ -220,7 +220,7 @@ class MatterLight(MatterEntity, LightEntity): 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 Date: Thu, 6 Jul 2023 17:20:20 +0200 Subject: [PATCH 108/184] 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 cf6feb44fbd..42549fb20d9 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -130,14 +130,11 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): @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 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): 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 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): 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 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): 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 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): 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 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): """ _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 Date: Thu, 6 Jul 2023 01:26:10 -0700 Subject: [PATCH 109/184] 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 2216d060f29..a44cfb3ce13 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 c6294603728..2edbba41b01 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 c28835fee55..057a0e1d77d 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 Date: Thu, 6 Jul 2023 09:02:32 +0200 Subject: [PATCH 110/184] 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 26ca5dedd34..36946b81c0c 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 Date: Thu, 6 Jul 2023 16:24:34 +0200 Subject: [PATCH 111/184] 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 09c861421b0..1164c2d8015 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 b062a323305..6adec334bb0 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 Date: Thu, 6 Jul 2023 13:25:34 +0200 Subject: [PATCH 112/184] 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 247b91d1b06..a3bb28e7a8b 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -292,6 +292,8 @@ async def async_setup_entry( 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 82ede87848e..0438b606efd 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 Date: Thu, 6 Jul 2023 16:48:03 +0200 Subject: [PATCH 113/184] 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 f8d41db0e11..8530aa3b04c 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -608,7 +608,7 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity): 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 199c3e08942..cc41b6c404c 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -26,7 +26,7 @@ from homeassistant.core import ( 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 Date: Thu, 6 Jul 2023 16:28:20 +0200 Subject: [PATCH 114/184] 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 641d3b8ae4d..c7c6585e002 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -27,8 +27,10 @@ from homeassistant.util.dt import utcnow 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 Date: Thu, 6 Jul 2023 11:52:01 -0400 Subject: [PATCH 115/184] 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 7b85163fba7..cc04180a618 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 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 cd0e6eb47f0..2de9c9de5d1 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 Date: Thu, 6 Jul 2023 13:53:56 -0400 Subject: [PATCH 116/184] 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 7de6f70e793..60d47ca9a44 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -1002,7 +1002,8 @@ async def _test_friendly_name( (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( From 80b24b23d3d10b6ec6cb6fdcd8233f6923037df4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jul 2023 04:57:17 -1000 Subject: [PATCH 117/184] Fix ESPHome deep sleep devices staying unavailable after unexpected disconnect (#96354) backport of #96353 --- homeassistant/components/esphome/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index afaefe117ba..ed55180bc0e 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -388,6 +388,7 @@ async def async_setup_entry( # noqa: C901 assert cli.api_version is not None entry_data.api_version = cli.api_version entry_data.available = True + entry_data.expected_disconnect = True if entry_data.device_info.name: reconnect_logic.name = entry_data.device_info.name From 3667eb94004f5d50702680e6b936dba03249ce31 Mon Sep 17 00:00:00 2001 From: Aaron Collins Date: Thu, 6 Jul 2023 00:12:18 +1200 Subject: [PATCH 118/184] Bump pydaikin 2.10.5 (#95656) --- .coveragerc | 1 - homeassistant/components/daikin/__init__.py | 86 +++++++++++- homeassistant/components/daikin/manifest.json | 2 +- homeassistant/components/daikin/switch.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/daikin/test_init.py | 128 ++++++++++++++++++ 7 files changed, 216 insertions(+), 7 deletions(-) create mode 100644 tests/components/daikin/test_init.py diff --git a/.coveragerc b/.coveragerc index 6a2a0db3ea4..442432dd71c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -182,7 +182,6 @@ omit = homeassistant/components/crownstone/listeners.py homeassistant/components/cups/sensor.py homeassistant/components/currencylayer/sensor.py - homeassistant/components/daikin/__init__.py homeassistant/components/daikin/climate.py homeassistant/components/daikin/sensor.py homeassistant/components/daikin/switch.py diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 481a072bdb3..b0097f607d5 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -15,8 +15,9 @@ from homeassistant.const import ( CONF_UUID, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -52,6 +53,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not daikin_api: return False + await async_migrate_unique_id(hass, entry, daikin_api) + hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api}) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -67,7 +70,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def daikin_api_setup(hass, host, key, uuid, password): +async def daikin_api_setup(hass: HomeAssistant, host, key, uuid, password): """Create a Daikin instance only once.""" session = async_get_clientsession(hass) @@ -127,3 +130,82 @@ class DaikinApi: name=info.get("name"), sw_version=info.get("ver", "").replace("_", "."), ) + + +async def async_migrate_unique_id( + hass: HomeAssistant, config_entry: ConfigEntry, api: DaikinApi +) -> None: + """Migrate old entry.""" + dev_reg = dr.async_get(hass) + old_unique_id = config_entry.unique_id + new_unique_id = api.device.mac + new_name = api.device.values["name"] + + @callback + def _update_unique_id(entity_entry: er.RegistryEntry) -> dict[str, str] | None: + """Update unique ID of entity entry.""" + return update_unique_id(entity_entry, new_unique_id) + + if new_unique_id == old_unique_id: + return + + # Migrate devices + for device_entry in dr.async_entries_for_config_entry( + dev_reg, config_entry.entry_id + ): + for connection in device_entry.connections: + if connection[1] == old_unique_id: + new_connections = { + (CONNECTION_NETWORK_MAC, dr.format_mac(new_unique_id)) + } + + _LOGGER.debug( + "Migrating device %s connections to %s", + device_entry.name, + new_connections, + ) + dev_reg.async_update_device( + device_entry.id, + merge_connections=new_connections, + ) + + if device_entry.name is None: + _LOGGER.debug( + "Migrating device name to %s", + new_name, + ) + dev_reg.async_update_device( + device_entry.id, + name=new_name, + ) + + # Migrate entities + await er.async_migrate_entries(hass, config_entry.entry_id, _update_unique_id) + + new_data = {**config_entry.data, KEY_MAC: dr.format_mac(new_unique_id)} + + hass.config_entries.async_update_entry( + config_entry, unique_id=new_unique_id, data=new_data + ) + + +@callback +def update_unique_id( + entity_entry: er.RegistryEntry, unique_id: str +) -> dict[str, str] | None: + """Update unique ID of entity entry.""" + if entity_entry.unique_id.startswith(unique_id): + # Already correct, nothing to do + return None + + unique_id_parts = entity_entry.unique_id.split("-") + unique_id_parts[0] = unique_id + entity_new_unique_id = "-".join(unique_id_parts) + + _LOGGER.debug( + "Migrating entity %s from %s to new id %s", + entity_entry.entity_id, + entity_entry.unique_id, + entity_new_unique_id, + ) + return {"new_unique_id": entity_new_unique_id} diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 6f90b0cf5ef..c6334dfaeca 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "loggers": ["pydaikin"], "quality_scale": "platinum", - "requirements": ["pydaikin==2.9.0"], + "requirements": ["pydaikin==2.10.5"], "zeroconf": ["_dkapi._tcp.local."] } diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index 37b3ec45c4c..847f030fae5 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -42,7 +42,7 @@ async def async_setup_entry( [ DaikinZoneSwitch(daikin_api, zone_id) for zone_id, zone in enumerate(zones) - if zone != ("-", "0") + if zone[0] != "-" ] ) if daikin_api.device.support_advanced_modes: diff --git a/requirements_all.txt b/requirements_all.txt index 2edbba41b01..492fc719fd4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1615,7 +1615,7 @@ pycsspeechtts==1.0.8 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.9.0 +pydaikin==2.10.5 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 057a0e1d77d..0637fcf5ddd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1197,7 +1197,7 @@ pycoolmasternet-async==0.1.5 pycsspeechtts==1.0.8 # homeassistant.components.daikin -pydaikin==2.9.0 +pydaikin==2.10.5 # homeassistant.components.deconz pydeconz==113 diff --git a/tests/components/daikin/test_init.py b/tests/components/daikin/test_init.py new file mode 100644 index 00000000000..8145a7a1e99 --- /dev/null +++ b/tests/components/daikin/test_init.py @@ -0,0 +1,128 @@ +"""Define tests for the Daikin init.""" +import asyncio +from unittest.mock import AsyncMock, PropertyMock, patch + +from aiohttp import ClientConnectionError +import pytest + +from homeassistant.components.daikin import update_unique_id +from homeassistant.components.daikin.const import DOMAIN, KEY_MAC +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .test_config_flow import HOST, MAC + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_daikin(): + """Mock pydaikin.""" + + async def mock_daikin_factory(*args, **kwargs): + """Mock the init function in pydaikin.""" + return Appliance + + with patch("homeassistant.components.daikin.Appliance") as Appliance: + Appliance.factory.side_effect = mock_daikin_factory + type(Appliance).update_status = AsyncMock() + type(Appliance).inside_temperature = PropertyMock(return_value=22) + type(Appliance).target_temperature = PropertyMock(return_value=22) + type(Appliance).zones = PropertyMock(return_value=[("Zone 1", "0", 0)]) + type(Appliance).fan_rate = PropertyMock(return_value=[]) + type(Appliance).swing_modes = PropertyMock(return_value=[]) + yield Appliance + + +DATA = { + "ver": "1_1_8", + "name": "DaikinAP00000", + "mac": MAC, + "model": "NOTSUPPORT", +} + + +INVALID_DATA = {**DATA, "name": None, "mac": HOST} + + +async def test_unique_id_migrate(hass: HomeAssistant, mock_daikin) -> None: + """Test unique id migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=HOST, + title=None, + data={CONF_HOST: HOST, KEY_MAC: HOST}, + ) + config_entry.add_to_hass(hass) + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + type(mock_daikin).mac = PropertyMock(return_value=HOST) + type(mock_daikin).values = PropertyMock(return_value=INVALID_DATA) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.unique_id == HOST + + assert device_registry.async_get_device({}, {(KEY_MAC, HOST)}).name is None + + entity = entity_registry.async_get("climate.daikin_127_0_0_1") + assert entity.unique_id == HOST + assert update_unique_id(entity, MAC) is not None + + assert entity_registry.async_get("switch.none_zone_1").unique_id.startswith(HOST) + + type(mock_daikin).mac = PropertyMock(return_value=MAC) + type(mock_daikin).values = PropertyMock(return_value=DATA) + + assert config_entry.unique_id != MAC + + assert await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.unique_id == MAC + + assert ( + device_registry.async_get_device({}, {(KEY_MAC, MAC)}).name == "DaikinAP00000" + ) + + entity = entity_registry.async_get("climate.daikin_127_0_0_1") + assert entity.unique_id == MAC + assert update_unique_id(entity, MAC) is None + + assert entity_registry.async_get("switch.none_zone_1").unique_id.startswith(MAC) + + +async def test_client_connection_error(hass: HomeAssistant, mock_daikin) -> None: + """Test unique id migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MAC, + data={CONF_HOST: HOST, KEY_MAC: MAC}, + ) + config_entry.add_to_hass(hass) + + mock_daikin.factory.side_effect = ClientConnectionError + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_timeout_error(hass: HomeAssistant, mock_daikin) -> None: + """Test unique id migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MAC, + data={CONF_HOST: HOST, KEY_MAC: MAC}, + ) + config_entry.add_to_hass(hass) + + mock_daikin.factory.side_effect = asyncio.TimeoutError + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.SETUP_RETRY From 52f60f1e48f6872e967344460f857d762c33c24e Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Sat, 8 Jul 2023 00:26:19 -0700 Subject: [PATCH 119/184] Bump pywemo from 0.9.1 to 1.1.0 (#95951) --- homeassistant/components/wemo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 3a562296a50..bb19d2e1655 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -9,7 +9,7 @@ }, "iot_class": "local_push", "loggers": ["pywemo"], - "requirements": ["pywemo==0.9.1"], + "requirements": ["pywemo==1.1.0"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index 492fc719fd4..40b1b5c0fba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2213,7 +2213,7 @@ pyvolumio==0.1.5 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.9.1 +pywemo==1.1.0 # homeassistant.components.wilight pywilight==0.0.74 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0637fcf5ddd..f50ce4b89d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1621,7 +1621,7 @@ pyvolumio==0.1.5 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.9.1 +pywemo==1.1.0 # homeassistant.components.wilight pywilight==0.0.74 From 7bd9933092bd25b16363f775f9362377a9ce00a5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jul 2023 17:24:41 +0200 Subject: [PATCH 120/184] Get MyStrom device state before checking support (#96004) * Get device state before checking support * Add full default device response to test * Add test mocks * Add test coverage --- homeassistant/components/mystrom/__init__.py | 39 +++-- tests/components/mystrom/__init__.py | 171 +++++++++++++++++++ tests/components/mystrom/test_init.py | 64 ++++--- 3 files changed, 241 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/mystrom/__init__.py b/homeassistant/components/mystrom/__init__.py index 160cd0e8634..64f7dafc1b7 100644 --- a/homeassistant/components/mystrom/__init__.py +++ b/homeassistant/components/mystrom/__init__.py @@ -22,6 +22,24 @@ PLATFORMS_BULB = [Platform.LIGHT] _LOGGER = logging.getLogger(__name__) +async def _async_get_device_state( + device: MyStromSwitch | MyStromBulb, ip_address: str +) -> None: + try: + await device.get_state() + except MyStromConnectionError as err: + _LOGGER.error("No route to myStrom plug: %s", ip_address) + raise ConfigEntryNotReady() from err + + +def _get_mystrom_bulb(host: str, mac: str) -> MyStromBulb: + return MyStromBulb(host, mac) + + +def _get_mystrom_switch(host: str) -> MyStromSwitch: + return MyStromSwitch(host) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up myStrom from a config entry.""" host = entry.data[CONF_HOST] @@ -34,12 +52,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_type = info["type"] if device_type in [101, 106, 107]: - device = MyStromSwitch(host) + device = _get_mystrom_switch(host) platforms = PLATFORMS_SWITCH - elif device_type == 102: + await _async_get_device_state(device, info["ip"]) + elif device_type in [102, 105]: mac = info["mac"] - device = MyStromBulb(host, mac) + device = _get_mystrom_bulb(host, mac) platforms = PLATFORMS_BULB + await _async_get_device_state(device, info["ip"]) if device.bulb_type not in ["rgblamp", "strip"]: _LOGGER.error( "Device %s (%s) is not a myStrom bulb nor myStrom LED Strip", @@ -51,12 +71,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Unsupported myStrom device type: %s", device_type) return False - try: - await device.get_state() - except MyStromConnectionError as err: - _LOGGER.error("No route to myStrom plug: %s", info["ip"]) - raise ConfigEntryNotReady() from err - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = MyStromData( device=device, info=info, @@ -69,10 +83,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" device_type = hass.data[DOMAIN][entry.entry_id].info["type"] + platforms = [] if device_type in [101, 106, 107]: - platforms = PLATFORMS_SWITCH - elif device_type == 102: - platforms = PLATFORMS_BULB + platforms.extend(PLATFORMS_SWITCH) + elif device_type in [102, 105]: + platforms.extend(PLATFORMS_BULB) if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms): hass.data[DOMAIN].pop(entry.entry_id) diff --git a/tests/components/mystrom/__init__.py b/tests/components/mystrom/__init__.py index f0cc6224191..8b3e2f8535f 100644 --- a/tests/components/mystrom/__init__.py +++ b/tests/components/mystrom/__init__.py @@ -1 +1,172 @@ """Tests for the myStrom integration.""" +from typing import Any, Optional + + +def get_default_device_response(device_type: int) -> dict[str, Any]: + """Return default device response.""" + return { + "version": "2.59.32", + "mac": "6001940376EB", + "type": device_type, + "ssid": "personal", + "ip": "192.168.0.23", + "mask": "255.255.255.0", + "gw": "192.168.0.1", + "dns": "192.168.0.1", + "static": False, + "connected": True, + "signal": 94, + } + + +def get_default_bulb_state() -> dict[str, Any]: + """Get default bulb state.""" + return { + "type": "rgblamp", + "battery": False, + "reachable": True, + "meshroot": True, + "on": False, + "color": "46;18;100", + "mode": "hsv", + "ramp": 10, + "power": 0.45, + "fw_version": "2.58.0", + } + + +def get_default_switch_state() -> dict[str, Any]: + """Get default switch state.""" + return { + "power": 1.69, + "Ws": 0.81, + "relay": True, + "temperature": 24.87, + "version": "2.59.32", + "mac": "6001940376EB", + "ssid": "personal", + "ip": "192.168.0.23", + "mask": "255.255.255.0", + "gw": "192.168.0.1", + "dns": "192.168.0.1", + "static": False, + "connected": True, + "signal": 94, + } + + +class MyStromDeviceMock: + """Base device mock.""" + + def __init__(self, state: dict[str, Any]) -> None: + """Initialize device mock.""" + self._requested_state = False + self._state = state + + async def get_state(self) -> None: + """Set if state is requested.""" + self._requested_state = True + + +class MyStromBulbMock(MyStromDeviceMock): + """MyStrom Bulb mock.""" + + def __init__(self, mac: str, state: dict[str, Any]) -> None: + """Initialize bulb mock.""" + super().__init__(state) + self.mac = mac + + @property + def firmware(self) -> Optional[str]: + """Return current firmware.""" + if not self._requested_state: + return None + return self._state["fw_version"] + + @property + def consumption(self) -> Optional[float]: + """Return current firmware.""" + if not self._requested_state: + return None + return self._state["power"] + + @property + def color(self) -> Optional[str]: + """Return current color settings.""" + if not self._requested_state: + return None + return self._state["color"] + + @property + def mode(self) -> Optional[str]: + """Return current mode.""" + if not self._requested_state: + return None + return self._state["mode"] + + @property + def transition_time(self) -> Optional[int]: + """Return current transition time (ramp).""" + if not self._requested_state: + return None + return self._state["ramp"] + + @property + def bulb_type(self) -> Optional[str]: + """Return the type of the bulb.""" + if not self._requested_state: + return None + return self._state["type"] + + @property + def state(self) -> Optional[bool]: + """Return the current state of the bulb.""" + if not self._requested_state: + return None + return self._state["on"] + + +class MyStromSwitchMock(MyStromDeviceMock): + """MyStrom Switch mock.""" + + @property + def relay(self) -> Optional[bool]: + """Return the relay state.""" + if not self._requested_state: + return None + return self._state["on"] + + @property + def consumption(self) -> Optional[float]: + """Return the current power consumption in mWh.""" + if not self._requested_state: + return None + return self._state["power"] + + @property + def consumedWs(self) -> Optional[float]: + """The average of energy consumed per second since last report call.""" + if not self._requested_state: + return None + return self._state["Ws"] + + @property + def firmware(self) -> Optional[str]: + """Return the current firmware.""" + if not self._requested_state: + return None + return self._state["version"] + + @property + def mac(self) -> Optional[str]: + """Return the MAC address.""" + if not self._requested_state: + return None + return self._state["mac"] + + @property + def temperature(self) -> Optional[float]: + """Return the current temperature in celsius.""" + if not self._requested_state: + return None + return self._state["temperature"] diff --git a/tests/components/mystrom/test_init.py b/tests/components/mystrom/test_init.py index 01b52d2cb94..281d7af9947 100644 --- a/tests/components/mystrom/test_init.py +++ b/tests/components/mystrom/test_init.py @@ -2,11 +2,19 @@ from unittest.mock import AsyncMock, PropertyMock, patch from pymystrom.exceptions import MyStromConnectionError +import pytest from homeassistant.components.mystrom.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from . import ( + MyStromBulbMock, + MyStromSwitchMock, + get_default_bulb_state, + get_default_device_response, + get_default_switch_state, +) from .conftest import DEVICE_MAC from tests.common import MockConfigEntry @@ -16,30 +24,21 @@ async def init_integration( hass: HomeAssistant, config_entry: MockConfigEntry, device_type: int, - bulb_type: str = "strip", ) -> None: """Inititialize integration for testing.""" with patch( "pymystrom.get_device_info", - side_effect=AsyncMock(return_value={"type": device_type, "mac": DEVICE_MAC}), - ), patch("pymystrom.switch.MyStromSwitch.get_state", return_value={}), patch( - "pymystrom.bulb.MyStromBulb.get_state", return_value={} + side_effect=AsyncMock(return_value=get_default_device_response(device_type)), ), patch( - "pymystrom.bulb.MyStromBulb.bulb_type", bulb_type + "homeassistant.components.mystrom._get_mystrom_bulb", + return_value=MyStromBulbMock("6001940376EB", get_default_bulb_state()), ), patch( - "pymystrom.switch.MyStromSwitch.mac", - new_callable=PropertyMock, - return_value=DEVICE_MAC, - ), patch( - "pymystrom.bulb.MyStromBulb.mac", - new_callable=PropertyMock, - return_value=DEVICE_MAC, + "homeassistant.components.mystrom._get_mystrom_switch", + return_value=MyStromSwitchMock(get_default_switch_state()), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED - async def test_init_switch_and_unload( hass: HomeAssistant, config_entry: MockConfigEntry @@ -56,12 +55,35 @@ async def test_init_switch_and_unload( assert not hass.data.get(DOMAIN) -async def test_init_bulb(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: +@pytest.mark.parametrize( + ("device_type", "platform", "entry_state", "entity_state_none"), + [ + (101, "switch", ConfigEntryState.LOADED, False), + (102, "light", ConfigEntryState.LOADED, False), + (103, "button", ConfigEntryState.SETUP_ERROR, True), + (104, "button", ConfigEntryState.SETUP_ERROR, True), + (105, "light", ConfigEntryState.LOADED, False), + (106, "switch", ConfigEntryState.LOADED, False), + (107, "switch", ConfigEntryState.LOADED, False), + (110, "sensor", ConfigEntryState.SETUP_ERROR, True), + (113, "switch", ConfigEntryState.SETUP_ERROR, True), + (118, "button", ConfigEntryState.SETUP_ERROR, True), + (120, "switch", ConfigEntryState.SETUP_ERROR, True), + ], +) +async def test_init_bulb( + hass: HomeAssistant, + config_entry: MockConfigEntry, + device_type: int, + platform: str, + entry_state: ConfigEntryState, + entity_state_none: bool, +) -> None: """Test the initialization of a myStrom bulb.""" - await init_integration(hass, config_entry, 102) - state = hass.states.get("light.mystrom_device") - assert state is not None - assert config_entry.state is ConfigEntryState.LOADED + await init_integration(hass, config_entry, device_type) + state = hass.states.get(f"{platform}.mystrom_device") + assert (state is None) == entity_state_none + assert config_entry.state is entry_state async def test_init_of_unknown_bulb( @@ -120,7 +142,7 @@ async def test_init_cannot_connect_because_of_get_state( """Test error handling for failing get_state.""" with patch( "pymystrom.get_device_info", - side_effect=AsyncMock(return_value={"type": 101, "mac": DEVICE_MAC}), + side_effect=AsyncMock(return_value=get_default_device_response(101)), ), patch( "pymystrom.switch.MyStromSwitch.get_state", side_effect=MyStromConnectionError() ), patch( @@ -129,4 +151,4 @@ async def test_init_cannot_connect_because_of_get_state( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + assert config_entry.state == ConfigEntryState.SETUP_RETRY From 6f5373fa6e54ee0f233c6566d3abad05bffc094d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 7 Jul 2023 20:52:38 +0200 Subject: [PATCH 121/184] Refactor async_get_hass to rely on threading.local instead of a ContextVar (#96005) * Test for async_get_hass * Add Fix --- homeassistant/core.py | 22 ++- homeassistant/helpers/config_validation.py | 8 +- tests/conftest.py | 12 +- tests/helpers/test_config_validation.py | 7 +- tests/test_core.py | 181 +++++++++++++++++++++ 5 files changed, 205 insertions(+), 25 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 47b52d9ff76..528ab3f6d01 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -16,7 +16,6 @@ from collections.abc import ( ) import concurrent.futures from contextlib import suppress -from contextvars import ContextVar import datetime import enum import functools @@ -156,8 +155,6 @@ MAX_EXPECTED_ENTITY_IDS = 16384 _LOGGER = logging.getLogger(__name__) -_cv_hass: ContextVar[HomeAssistant] = ContextVar("hass") - @functools.lru_cache(MAX_EXPECTED_ENTITY_IDS) def split_entity_id(entity_id: str) -> tuple[str, str]: @@ -200,16 +197,27 @@ def is_callback(func: Callable[..., Any]) -> bool: return getattr(func, "_hass_callback", False) is True +class _Hass(threading.local): + """Container which makes a HomeAssistant instance available to the event loop.""" + + hass: HomeAssistant | None = None + + +_hass = _Hass() + + @callback def async_get_hass() -> HomeAssistant: """Return the HomeAssistant instance. - Raises LookupError if no HomeAssistant instance is available. + Raises HomeAssistantError when called from the wrong thread. This should be used where it's very cumbersome or downright impossible to pass hass to the code which needs it. """ - return _cv_hass.get() + if not _hass.hass: + raise HomeAssistantError("async_get_hass called from the wrong thread") + return _hass.hass @enum.unique @@ -293,9 +301,9 @@ class HomeAssistant: config_entries: ConfigEntries = None # type: ignore[assignment] def __new__(cls) -> HomeAssistant: - """Set the _cv_hass context variable.""" + """Set the _hass thread local data.""" hass = super().__new__(cls) - _cv_hass.set(hass) + _hass.hass = hass return hass def __init__(self) -> None: diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index cea8a866f5c..e8f1e58615c 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -93,7 +93,7 @@ from homeassistant.core import ( split_entity_id, valid_entity_id, ) -from homeassistant.exceptions import TemplateError +from homeassistant.exceptions import HomeAssistantError, TemplateError from homeassistant.generated import currencies from homeassistant.generated.countries import COUNTRIES from homeassistant.generated.languages import LANGUAGES @@ -609,7 +609,7 @@ def template(value: Any | None) -> template_helper.Template: raise vol.Invalid("template value should be a string") hass: HomeAssistant | None = None - with contextlib.suppress(LookupError): + with contextlib.suppress(HomeAssistantError): hass = async_get_hass() template_value = template_helper.Template(str(value), hass) @@ -631,7 +631,7 @@ def dynamic_template(value: Any | None) -> template_helper.Template: raise vol.Invalid("template value does not contain a dynamic template") hass: HomeAssistant | None = None - with contextlib.suppress(LookupError): + with contextlib.suppress(HomeAssistantError): hass = async_get_hass() template_value = template_helper.Template(str(value), hass) @@ -1098,7 +1098,7 @@ def _no_yaml_config_schema( # pylint: disable-next=import-outside-toplevel from .issue_registry import IssueSeverity, async_create_issue - with contextlib.suppress(LookupError): + with contextlib.suppress(HomeAssistantError): hass = async_get_hass() async_create_issue( hass, diff --git a/tests/conftest.py b/tests/conftest.py index 56014d7a556..922e42c7a7e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -490,17 +490,7 @@ def hass_fixture_setup() -> list[bool]: @pytest.fixture -def hass(_hass: HomeAssistant) -> HomeAssistant: - """Fixture to provide a test instance of Home Assistant.""" - # This wraps the async _hass fixture inside a sync fixture, to ensure - # the `hass` context variable is set in the execution context in which - # the test itself is executed - ha._cv_hass.set(_hass) - return _hass - - -@pytest.fixture -async def _hass( +async def hass( hass_fixture_setup: list[bool], event_loop: asyncio.AbstractEventLoop, load_registries: bool, diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 458774b748c..5ea6df42349 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -12,6 +12,7 @@ import voluptuous as vol import homeassistant from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( config_validation as cv, issue_registry as ir, @@ -383,7 +384,7 @@ def test_service() -> None: schema("homeassistant.turn_on") -def test_service_schema() -> None: +def test_service_schema(hass: HomeAssistant) -> None: """Test service_schema validation.""" options = ( {}, @@ -1550,10 +1551,10 @@ def test_config_entry_only_schema_cant_find_module() -> None: def test_config_entry_only_schema_no_hass( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: - """Test if the the hass context var is not set in our context.""" + """Test if the the hass context is not set in our context.""" with patch( "homeassistant.helpers.config_validation.async_get_hass", - side_effect=LookupError, + side_effect=HomeAssistantError, ): cv.config_entry_only_config_schema("test_domain")( {"test_domain": {"foo": "bar"}} diff --git a/tests/test_core.py b/tests/test_core.py index 8b63eab7b42..7e0766c8ac5 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -9,10 +9,12 @@ import gc import logging import os from tempfile import TemporaryDirectory +import threading import time from typing import Any from unittest.mock import MagicMock, Mock, PropertyMock, patch +import async_timeout import pytest import voluptuous as vol @@ -40,6 +42,7 @@ from homeassistant.core import ( ServiceResponse, State, SupportsResponse, + callback, ) from homeassistant.exceptions import ( HomeAssistantError, @@ -202,6 +205,184 @@ def test_async_run_hass_job_delegates_non_async() -> None: assert len(hass.async_add_hass_job.mock_calls) == 1 +async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: + """Test calling async_get_hass via different paths. + + The test asserts async_get_hass can be called from: + - Coroutines and callbacks + - Callbacks scheduled from callbacks, coroutines and threads + - Coroutines scheduled from callbacks, coroutines and threads + + The test also asserts async_get_hass can not be called from threads + other than the event loop. + """ + task_finished = asyncio.Event() + + def can_call_async_get_hass() -> bool: + """Test if it's possible to call async_get_hass.""" + try: + if ha.async_get_hass() is hass: + return True + raise Exception + except HomeAssistantError: + return False + + raise Exception + + # Test scheduling a coroutine which calls async_get_hass via hass.async_create_task + async def _async_create_task() -> None: + task_finished.set() + assert can_call_async_get_hass() + + hass.async_create_task(_async_create_task(), "create_task") + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + + # Test scheduling a callback which calls async_get_hass via hass.async_add_job + @callback + def _add_job() -> None: + assert can_call_async_get_hass() + task_finished.set() + + hass.async_add_job(_add_job) + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + + # Test scheduling a callback which calls async_get_hass from a callback + @callback + def _schedule_callback_from_callback() -> None: + @callback + def _callback(): + assert can_call_async_get_hass() + task_finished.set() + + # Test the scheduled callback itself can call async_get_hass + assert can_call_async_get_hass() + hass.async_add_job(_callback) + + _schedule_callback_from_callback() + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + + # Test scheduling a coroutine which calls async_get_hass from a callback + @callback + def _schedule_coroutine_from_callback() -> None: + async def _coroutine(): + assert can_call_async_get_hass() + task_finished.set() + + # Test the scheduled callback itself can call async_get_hass + assert can_call_async_get_hass() + hass.async_add_job(_coroutine()) + + _schedule_coroutine_from_callback() + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + + # Test scheduling a callback which calls async_get_hass from a coroutine + async def _schedule_callback_from_coroutine() -> None: + @callback + def _callback(): + assert can_call_async_get_hass() + task_finished.set() + + # Test the coroutine itself can call async_get_hass + assert can_call_async_get_hass() + hass.async_add_job(_callback) + + await _schedule_callback_from_coroutine() + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + + # Test scheduling a coroutine which calls async_get_hass from a coroutine + async def _schedule_callback_from_coroutine() -> None: + async def _coroutine(): + assert can_call_async_get_hass() + task_finished.set() + + # Test the coroutine itself can call async_get_hass + assert can_call_async_get_hass() + await hass.async_create_task(_coroutine()) + + await _schedule_callback_from_coroutine() + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + + # Test scheduling a callback which calls async_get_hass from an executor + def _async_add_executor_job_add_job() -> None: + @callback + def _async_add_job(): + assert can_call_async_get_hass() + task_finished.set() + + # Test the executor itself can not call async_get_hass + assert not can_call_async_get_hass() + hass.add_job(_async_add_job) + + await hass.async_add_executor_job(_async_add_executor_job_add_job) + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + + # Test scheduling a coroutine which calls async_get_hass from an executor + def _async_add_executor_job_create_task() -> None: + async def _async_create_task() -> None: + assert can_call_async_get_hass() + task_finished.set() + + # Test the executor itself can not call async_get_hass + assert not can_call_async_get_hass() + hass.create_task(_async_create_task()) + + await hass.async_add_executor_job(_async_add_executor_job_create_task) + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + + # Test scheduling a callback which calls async_get_hass from a worker thread + class MyJobAddJob(threading.Thread): + @callback + def _my_threaded_job_add_job(self) -> None: + assert can_call_async_get_hass() + task_finished.set() + + def run(self) -> None: + # Test the worker thread itself can not call async_get_hass + assert not can_call_async_get_hass() + hass.add_job(self._my_threaded_job_add_job) + + my_job_add_job = MyJobAddJob() + my_job_add_job.start() + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + my_job_add_job.join() + + # Test scheduling a coroutine which calls async_get_hass from a worker thread + class MyJobCreateTask(threading.Thread): + async def _my_threaded_job_create_task(self) -> None: + assert can_call_async_get_hass() + task_finished.set() + + def run(self) -> None: + # Test the worker thread itself can not call async_get_hass + assert not can_call_async_get_hass() + hass.create_task(self._my_threaded_job_create_task()) + + my_job_create_task = MyJobCreateTask() + my_job_create_task.start() + async with async_timeout.timeout(1): + await task_finished.wait() + task_finished.clear() + my_job_create_task.join() + + async def test_stage_shutdown(hass: HomeAssistant) -> None: """Simulate a shutdown, test calling stuff.""" test_stop = async_capture_events(hass, EVENT_HOMEASSISTANT_STOP) From c6b7a3d5647304d4ab8f048b8b44d2a349f684b7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 6 Jul 2023 23:01:06 +0200 Subject: [PATCH 122/184] Use explicit device naming for Switchbot (#96011) Use explicit entity naming for Switchbot --- homeassistant/components/switchbot/cover.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index 1da879cb02b..35083c4b089 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -122,6 +122,7 @@ class SwitchBotBlindTiltEntity(SwitchbotEntity, CoverEntity, RestoreEntity): | CoverEntityFeature.STOP_TILT | CoverEntityFeature.SET_TILT_POSITION ) + _attr_name = None _attr_translation_key = "cover" CLOSED_UP_THRESHOLD = 80 CLOSED_DOWN_THRESHOLD = 20 From 32927e050f3ce2b24dda5a45e3b148942f1ea346 Mon Sep 17 00:00:00 2001 From: Barry Williams Date: Fri, 7 Jul 2023 13:24:42 +0100 Subject: [PATCH 123/184] Remove openhome from discovery component (#96021) --- homeassistant/components/discovery/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 53b2478490d..79653e1c9bc 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -60,7 +60,6 @@ class ServiceDetails(NamedTuple): SERVICE_HANDLERS = { SERVICE_ENIGMA2: ServiceDetails("media_player", "enigma2"), "yamaha": ServiceDetails("media_player", "yamaha"), - "openhome": ServiceDetails("media_player", "openhome"), "bluesound": ServiceDetails("media_player", "bluesound"), } @@ -87,6 +86,7 @@ MIGRATED_SERVICE_HANDLERS = [ SERVICE_MOBILE_APP, SERVICE_NETGEAR, SERVICE_OCTOPRINT, + "openhome", "philips_hue", SERVICE_SAMSUNG_PRINTER, "sonos", From eec05d4fc81f5f0ee684f4370d904826282576e0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jul 2023 17:25:58 +0200 Subject: [PATCH 124/184] UPB explicit device name (#96042) --- homeassistant/components/upb/light.py | 3 ++- homeassistant/components/upb/scene.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upb/light.py b/homeassistant/components/upb/light.py index 47680714d19..4a71789423f 100644 --- a/homeassistant/components/upb/light.py +++ b/homeassistant/components/upb/light.py @@ -49,9 +49,10 @@ async def async_setup_entry( class UpbLight(UpbAttachedEntity, LightEntity): - """Representation of an UPB Light.""" + """Representation of a UPB Light.""" _attr_has_entity_name = True + _attr_name = None def __init__(self, element, unique_id, upb): """Initialize an UpbLight.""" diff --git a/homeassistant/components/upb/scene.py b/homeassistant/components/upb/scene.py index fe6f07199c4..d1272b7a1f6 100644 --- a/homeassistant/components/upb/scene.py +++ b/homeassistant/components/upb/scene.py @@ -47,7 +47,7 @@ async def async_setup_entry( class UpbLink(UpbEntity, Scene): - """Representation of an UPB Link.""" + """Representation of a UPB Link.""" def __init__(self, element, unique_id, upb): """Initialize the base of all UPB devices.""" From 3c7ced21ad0a6b40d5b7ccb8a5a163e5ea1fa41e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 8 Jul 2023 17:10:51 +0200 Subject: [PATCH 125/184] Use default MyStrom devicetype if not present (#96070) Co-authored-by: Paulus Schoutsen --- homeassistant/components/mystrom/__init__.py | 2 ++ tests/components/mystrom/__init__.py | 8 +++++--- tests/components/mystrom/test_init.py | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mystrom/__init__.py b/homeassistant/components/mystrom/__init__.py index 64f7dafc1b7..972db00e476 100644 --- a/homeassistant/components/mystrom/__init__.py +++ b/homeassistant/components/mystrom/__init__.py @@ -50,6 +50,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("No route to myStrom plug: %s", host) raise ConfigEntryNotReady() from err + info.setdefault("type", 101) + device_type = info["type"] if device_type in [101, 106, 107]: device = _get_mystrom_switch(host) diff --git a/tests/components/mystrom/__init__.py b/tests/components/mystrom/__init__.py index 8b3e2f8535f..21f6bd7a549 100644 --- a/tests/components/mystrom/__init__.py +++ b/tests/components/mystrom/__init__.py @@ -2,12 +2,11 @@ from typing import Any, Optional -def get_default_device_response(device_type: int) -> dict[str, Any]: +def get_default_device_response(device_type: int | None) -> dict[str, Any]: """Return default device response.""" - return { + response = { "version": "2.59.32", "mac": "6001940376EB", - "type": device_type, "ssid": "personal", "ip": "192.168.0.23", "mask": "255.255.255.0", @@ -17,6 +16,9 @@ def get_default_device_response(device_type: int) -> dict[str, Any]: "connected": True, "signal": 94, } + if device_type is not None: + response["type"] = device_type + return response def get_default_bulb_state() -> dict[str, Any]: diff --git a/tests/components/mystrom/test_init.py b/tests/components/mystrom/test_init.py index 281d7af9947..80011b47915 100644 --- a/tests/components/mystrom/test_init.py +++ b/tests/components/mystrom/test_init.py @@ -44,7 +44,7 @@ async def test_init_switch_and_unload( hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test the initialization of a myStrom switch.""" - await init_integration(hass, config_entry, 101) + await init_integration(hass, config_entry, 106) state = hass.states.get("switch.mystrom_device") assert state is not None assert config_entry.state is ConfigEntryState.LOADED @@ -58,7 +58,7 @@ async def test_init_switch_and_unload( @pytest.mark.parametrize( ("device_type", "platform", "entry_state", "entity_state_none"), [ - (101, "switch", ConfigEntryState.LOADED, False), + (None, "switch", ConfigEntryState.LOADED, False), (102, "light", ConfigEntryState.LOADED, False), (103, "button", ConfigEntryState.SETUP_ERROR, True), (104, "button", ConfigEntryState.SETUP_ERROR, True), From b26e624b2b47b98e5c3f4b9a98a7925e3d925359 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 7 Jul 2023 16:19:29 +0200 Subject: [PATCH 126/184] Fix implicit use of device name in Slimproto (#96081) --- homeassistant/components/slimproto/media_player.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/slimproto/media_player.py b/homeassistant/components/slimproto/media_player.py index c7c6585e002..9bd9f7668c8 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -90,6 +90,7 @@ class SlimProtoPlayer(MediaPlayerEntity): | MediaPlayerEntityFeature.BROWSE_MEDIA ) _attr_device_class = MediaPlayerDeviceClass.SPEAKER + _attr_name = None def __init__(self, slimserver: SlimServer, player: SlimClient) -> None: """Initialize MediaPlayer entity.""" From 98b8e27b08b11f7ff7e80a7a6e59b3292cb66d94 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 7 Jul 2023 09:25:23 -0600 Subject: [PATCH 127/184] Fix implicit device name for RainMachine `update` entity (#96094) Fix implicit device name for RainMachine update entity --- homeassistant/components/rainmachine/update.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/rainmachine/update.py b/homeassistant/components/rainmachine/update.py index a811894a0c2..f603cf0ccd7 100644 --- a/homeassistant/components/rainmachine/update.py +++ b/homeassistant/components/rainmachine/update.py @@ -62,6 +62,7 @@ class RainMachineUpdateEntity(RainMachineEntity, UpdateEntity): """Define a RainMachine update entity.""" _attr_device_class = UpdateDeviceClass.FIRMWARE + _attr_name = None _attr_supported_features = ( UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS From 855962d729ff76b3590a19cab0766300291282d5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jul 2023 19:15:41 +0200 Subject: [PATCH 128/184] Use explicit device name for Yalexs BLE (#96105) --- homeassistant/components/yalexs_ble/lock.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/yalexs_ble/lock.py b/homeassistant/components/yalexs_ble/lock.py index 9e97c2f080f..0ecf0e7b697 100644 --- a/homeassistant/components/yalexs_ble/lock.py +++ b/homeassistant/components/yalexs_ble/lock.py @@ -29,6 +29,7 @@ class YaleXSBLELock(YALEXSBLEEntity, LockEntity): """A yale xs ble lock.""" _attr_has_entity_name = True + _attr_name = None @callback def _async_update_state( From 9969f67508447e2b2ef45ec471f912df8da75a8b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 8 Jul 2023 01:36:14 +0200 Subject: [PATCH 129/184] Fix reference to translation reference in buienradar translations (#96119) Do not reference a reference --- .../components/buienradar/strings.json | 114 +++++++++--------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/buienradar/strings.json b/homeassistant/components/buienradar/strings.json index d7af3b66688..bac4e63e288 100644 --- a/homeassistant/components/buienradar/strings.json +++ b/homeassistant/components/buienradar/strings.json @@ -301,55 +301,55 @@ "name": "Condition 1d", "state": { "clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]", - "cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]", - "snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]", - "lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]" + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", + "snowy": "[%key:component::weather::entity_component::_::state::snowy%]", + "lightning": "[%key:component::weather::entity_component::_::state::lightning%]" } }, "condition_2d": { "name": "Condition 2d", "state": { "clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]", - "cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]", - "snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]", - "lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]" + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", + "snowy": "[%key:component::weather::entity_component::_::state::snowy%]", + "lightning": "[%key:component::weather::entity_component::_::state::lightning%]" } }, "condition_3d": { "name": "Condition 3d", "state": { "clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]", - "cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]", - "snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]", - "lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]" + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", + "snowy": "[%key:component::weather::entity_component::_::state::snowy%]", + "lightning": "[%key:component::weather::entity_component::_::state::lightning%]" } }, "condition_4d": { "name": "Condition 4d", "state": { "clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]", - "cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]", - "snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]", - "lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]" + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", + "snowy": "[%key:component::weather::entity_component::_::state::snowy%]", + "lightning": "[%key:component::weather::entity_component::_::state::lightning%]" } }, "condition_5d": { "name": "Condition 5d", "state": { "clear": "[%key:component::buienradar::entity::sensor::condition::state::clear%]", - "cloudy": "[%key:component::buienradar::entity::sensor::condition::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::condition::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::condition::state::rainy%]", - "snowy": "[%key:component::buienradar::entity::sensor::condition::state::snowy%]", - "lightning": "[%key:component::buienradar::entity::sensor::condition::state::lightning%]" + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", + "snowy": "[%key:component::weather::entity_component::_::state::snowy%]", + "lightning": "[%key:component::weather::entity_component::_::state::lightning%]" } }, "conditioncode_1d": { @@ -371,76 +371,76 @@ "name": "Detailed condition 1d", "state": { "clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]", - "partlycloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy%]", + "partlycloudy": "[%key:component::weather::entity_component::_::state::partlycloudy%]", "partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]", "partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]", "partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]", - "cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]", + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", "light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]", "light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]", "partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]", "partlycloudy-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]", "partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]", - "snowy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy%]", - "snowy-rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy-rainy%]", - "lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::lightning%]" + "snowy": "[%key:component::weather::entity_component::_::state::snowy%]", + "snowy-rainy": "[%key:component::weather::entity_component::_::state::snowy-rainy%]", + "lightning": "[%key:component::weather::entity_component::_::state::lightning%]" } }, "conditiondetailed_2d": { "name": "Detailed condition 2d", "state": { "clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]", - "partlycloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy%]", + "partlycloudy": "[%key:component::weather::entity_component::_::state::partlycloudy%]", "partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]", "partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]", "partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]", - "cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]", + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", "light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]", "light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]", "partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]", "partlycloudy-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]", "partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]", - "snowy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy%]", - "snowy-rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy-rainy%]", - "lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::lightning%]" + "snowy": "[%key:component::weather::entity_component::_::state::snowy%]", + "snowy-rainy": "[%key:component::weather::entity_component::_::state::snowy-rainy%]", + "lightning": "[%key:component::weather::entity_component::_::state::lightning%]" } }, "conditiondetailed_3d": { "name": "Detailed condition 3d", "state": { "clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]", - "partlycloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy%]", + "partlycloudy": "[%key:component::weather::entity_component::_::state::partlycloudy%]", "partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]", "partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]", "partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]", - "cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]", + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", "light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]", "light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]", "partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]", "partlycloudy-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]", "partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]", - "snowy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy%]", - "snowy-rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy-rainy%]", - "lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::lightning%]" + "snowy": "[%key:component::weather::entity_component::_::state::snowy%]", + "snowy-rainy": "[%key:component::weather::entity_component::_::state::snowy-rainy%]", + "lightning": "[%key:component::weather::entity_component::_::state::lightning%]" } }, "conditiondetailed_4d": { "name": "Detailed condition 4d", "state": { "clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]", - "partlycloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy%]", + "partlycloudy": "[%key:component::weather::entity_component::_::state::partlycloudy%]", "partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]", "partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]", "partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]", - "cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]", + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", "light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]", "light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]", "partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]", @@ -455,21 +455,21 @@ "name": "Detailed condition 5d", "state": { "clear": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::clear%]", - "partlycloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy%]", + "partlycloudy": "[%key:component::weather::entity_component::_::state::partlycloudy%]", "partlycloudy-fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-fog%]", "partlycloudy-light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-rain%]", "partlycloudy-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-rain%]", - "cloudy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::cloudy%]", - "fog": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::fog%]", - "rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::rainy%]", + "cloudy": "[%key:component::weather::entity_component::_::state::cloudy%]", + "fog": "[%key:component::weather::entity_component::_::state::fog%]", + "rainy": "[%key:component::weather::entity_component::_::state::rainy%]", "light-rain": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-rain%]", "light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::light-snow%]", "partlycloudy-light-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-light-snow%]", "partlycloudy-snow": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-snow%]", "partlycloudy-lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::partlycloudy-lightning%]", - "snowy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy%]", - "snowy-rainy": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::snowy-rainy%]", - "lightning": "[%key:component::buienradar::entity::sensor::conditiondetailed::state::lightning%]" + "snowy": "[%key:component::weather::entity_component::_::state::snowy%]", + "snowy-rainy": "[%key:component::weather::entity_component::_::state::snowy-rainy%]", + "lightning": "[%key:component::weather::entity_component::_::state::lightning%]" } }, "conditionexact_1d": { From 4874e13af8bbd426c8fa51d29ec66fee7e9c1a25 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 7 Jul 2023 21:40:18 -0400 Subject: [PATCH 130/184] Bump goalzero to 0.2.2 (#96121) --- homeassistant/components/goalzero/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/goalzero/manifest.json b/homeassistant/components/goalzero/manifest.json index 88bcdd4987b..f1bfc7de876 100644 --- a/homeassistant/components/goalzero/manifest.json +++ b/homeassistant/components/goalzero/manifest.json @@ -16,5 +16,5 @@ "iot_class": "local_polling", "loggers": ["goalzero"], "quality_scale": "silver", - "requirements": ["goalzero==0.2.1"] + "requirements": ["goalzero==0.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 40b1b5c0fba..e585228dd41 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -864,7 +864,7 @@ gitterpy==0.1.7 glances-api==0.4.3 # homeassistant.components.goalzero -goalzero==0.2.1 +goalzero==0.2.2 # homeassistant.components.goodwe goodwe==0.2.31 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f50ce4b89d3..c02d19b9aa1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -677,7 +677,7 @@ gios==3.1.0 glances-api==0.4.3 # homeassistant.components.goalzero -goalzero==0.2.1 +goalzero==0.2.2 # homeassistant.components.goodwe goodwe==0.2.31 From ebc123c3553e12c16bbafe8c1183ab468208624d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 8 Jul 2023 20:03:02 +0200 Subject: [PATCH 131/184] Fix implicitly using device name in Yale Smart Living (#96161) Yale Smart Living device name --- .../components/yale_smart_alarm/alarm_control_panel.py | 1 + homeassistant/components/yale_smart_alarm/lock.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index 799949a462a..7ced3487269 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -43,6 +43,7 @@ class YaleAlarmDevice(YaleAlarmEntity, AlarmControlPanelEntity): AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY ) + _attr_name = None def __init__(self, coordinator: YaleDataUpdateCoordinator) -> None: """Initialize the Yale Alarm Device.""" diff --git a/homeassistant/components/yale_smart_alarm/lock.py b/homeassistant/components/yale_smart_alarm/lock.py index fde08d08fbd..397a9cc8db1 100644 --- a/homeassistant/components/yale_smart_alarm/lock.py +++ b/homeassistant/components/yale_smart_alarm/lock.py @@ -40,6 +40,8 @@ async def async_setup_entry( class YaleDoorlock(YaleEntity, LockEntity): """Representation of a Yale doorlock.""" + _attr_name = None + def __init__( self, coordinator: YaleDataUpdateCoordinator, data: dict, code_format: int ) -> None: From c2a41fc21e66306029da57beef02917381c260a7 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Sat, 8 Jul 2023 21:23:25 +0200 Subject: [PATCH 132/184] Bump bthome to 2.12.1 (#96166) --- homeassistant/components/bthome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 91f4940a4e5..b38c1d3829b 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -20,5 +20,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/bthome", "iot_class": "local_push", - "requirements": ["bthome-ble==2.12.0"] + "requirements": ["bthome-ble==2.12.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index e585228dd41..efb87edfa0f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -565,7 +565,7 @@ brunt==1.2.0 bt-proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==2.12.0 +bthome-ble==2.12.1 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c02d19b9aa1..94d4e4857da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -469,7 +469,7 @@ brottsplatskartan==0.0.1 brunt==1.2.0 # homeassistant.components.bthome -bthome-ble==2.12.0 +bthome-ble==2.12.1 # homeassistant.components.buienradar buienradar==1.0.5 From e493cd642cbd13ec30cdbf971391a7eb4bec97ee Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 9 Jul 2023 20:19:05 +0200 Subject: [PATCH 133/184] Use explicit device name for Stookwijzer (#96184) --- homeassistant/components/stookwijzer/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/stookwijzer/sensor.py b/homeassistant/components/stookwijzer/sensor.py index cd84bec11b2..5b0bc4d4c63 100644 --- a/homeassistant/components/stookwijzer/sensor.py +++ b/homeassistant/components/stookwijzer/sensor.py @@ -33,6 +33,7 @@ class StookwijzerSensor(SensorEntity): _attr_attribution = "Data provided by stookwijzer.nu" _attr_device_class = SensorDeviceClass.ENUM _attr_has_entity_name = True + _attr_name = None _attr_translation_key = "stookwijzer" def __init__(self, client: Stookwijzer, entry: ConfigEntry) -> None: From de1a367cff725340ed5fd489a08391de58151c6d Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 9 Jul 2023 16:49:44 -0700 Subject: [PATCH 134/184] Enable retries on rainbird devices by loading model and version (#96190) Update rainbird to load device model and version --- homeassistant/components/rainbird/__init__.py | 7 +++++++ homeassistant/components/rainbird/coordinator.py | 5 +++++ tests/components/rainbird/conftest.py | 10 +++++++++- tests/components/rainbird/test_number.py | 2 ++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index 14a81f2c665..2af0cb30f1e 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -2,10 +2,12 @@ from __future__ import annotations from pyrainbird.async_client import AsyncRainbirdClient, AsyncRainbirdController +from pyrainbird.exceptions import RainbirdApiException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_SERIAL_NUMBER @@ -29,11 +31,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_PASSWORD], ) ) + try: + model_info = await controller.get_model_and_version() + except RainbirdApiException as err: + raise ConfigEntryNotReady from err coordinator = RainbirdUpdateCoordinator( hass, name=entry.title, controller=controller, serial_number=entry.data[CONF_SERIAL_NUMBER], + model_info=model_info, ) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/rainbird/coordinator.py b/homeassistant/components/rainbird/coordinator.py index 14598921a61..e1b52c6ff7d 100644 --- a/homeassistant/components/rainbird/coordinator.py +++ b/homeassistant/components/rainbird/coordinator.py @@ -9,6 +9,7 @@ from typing import TypeVar import async_timeout from pyrainbird.async_client import AsyncRainbirdController, RainbirdApiException +from pyrainbird.data import ModelAndVersion from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo @@ -42,6 +43,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]): name: str, controller: AsyncRainbirdController, serial_number: str, + model_info: ModelAndVersion, ) -> None: """Initialize ZoneStateUpdateCoordinator.""" super().__init__( @@ -54,6 +56,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]): self._controller = controller self._serial_number = serial_number self._zones: set[int] | None = None + self._model_info = model_info @property def controller(self) -> AsyncRainbirdController: @@ -72,6 +75,8 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]): default_name=f"{MANUFACTURER} Controller", identifiers={(DOMAIN, self._serial_number)}, manufacturer=MANUFACTURER, + model=self._model_info.model_name, + sw_version=f"{self._model_info.major}.{self._model_info.minor}", ) async def _async_update_data(self) -> RainbirdDeviceState: diff --git a/tests/components/rainbird/conftest.py b/tests/components/rainbird/conftest.py index 22f238ce553..21ad5230581 100644 --- a/tests/components/rainbird/conftest.py +++ b/tests/components/rainbird/conftest.py @@ -35,6 +35,8 @@ SERIAL_NUMBER = 0x12635436566 # Get serial number Command 0x85. Serial is 0x12635436566 SERIAL_RESPONSE = "850000012635436566" +# Model and version command 0x82 +MODEL_AND_VERSION_RESPONSE = "820006090C" # Get available stations command 0x83 AVAILABLE_STATIONS_RESPONSE = "83017F000000" # Mask for 7 zones EMPTY_STATIONS_RESPONSE = "830000000000" @@ -183,7 +185,13 @@ def mock_api_responses( These are returned in the order they are requested by the update coordinator. """ - return [stations_response, zone_state_response, rain_response, rain_delay_response] + return [ + MODEL_AND_VERSION_RESPONSE, + stations_response, + zone_state_response, + rain_response, + rain_delay_response, + ] @pytest.fixture(name="responses") diff --git a/tests/components/rainbird/test_number.py b/tests/components/rainbird/test_number.py index 4bf214c50f7..2ecdfcc537f 100644 --- a/tests/components/rainbird/test_number.py +++ b/tests/components/rainbird/test_number.py @@ -70,6 +70,8 @@ async def test_set_value( device = device_registry.async_get_device({(DOMAIN, SERIAL_NUMBER)}) assert device assert device.name == "Rain Bird Controller" + assert device.model == "ST8x-WiFi" + assert device.sw_version == "9.12" aioclient_mock.mock_calls.clear() responses.append(mock_response(ACK_ECHO)) From 76ac7fa6a029c8ddaf8a69a908e6de76222dab52 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Jul 2023 08:11:51 -1000 Subject: [PATCH 135/184] Fix race fetching ESPHome dashboard when there are no devices set up (#96196) * Fix fetching ESPHome dashboard when there are no devices setup fixes #96194 * coverage * fix --- .../components/esphome/config_flow.py | 6 ++- homeassistant/components/esphome/dashboard.py | 9 +++- tests/components/esphome/test_config_flow.py | 42 +++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 731743e48c8..7f554901812 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -34,7 +34,7 @@ from .const import ( DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS, DOMAIN, ) -from .dashboard import async_get_dashboard, async_set_dashboard_info +from .dashboard import async_get_or_create_dashboard_manager, async_set_dashboard_info ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key" ERROR_INVALID_ENCRYPTION_KEY = "invalid_psk" @@ -391,7 +391,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): """ if ( self._device_name is None - or (dashboard := async_get_dashboard(self.hass)) is None + or (manager := await async_get_or_create_dashboard_manager(self.hass)) + is None + or (dashboard := manager.async_get()) is None ): return False diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index 35e9cf74555..c9d74f0b30c 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -143,7 +143,14 @@ class ESPHomeDashboardManager: @callback def async_get_dashboard(hass: HomeAssistant) -> ESPHomeDashboard | None: - """Get an instance of the dashboard if set.""" + """Get an instance of the dashboard if set. + + This is only safe to call after `async_setup` has been completed. + + It should not be called from the config flow because there is a race + where manager can be an asyncio.Event instead of the actual manager + because the singleton decorator is not yet done. + """ manager: ESPHomeDashboardManager | None = hass.data.get(KEY_DASHBOARD_MANAGER) return manager.async_get() if manager else None diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 662816a53d8..86472a8aa57 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -1308,3 +1308,45 @@ async def test_option_flow( assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_ALLOW_SERVICE_CALLS: option_value} assert len(mock_reload.mock_calls) == int(option_value) + + +async def test_user_discovers_name_no_dashboard( + hass: HomeAssistant, + mock_client, + mock_zeroconf: None, + mock_setup_entry: None, +) -> None: + """Test user step can discover the name and the there is not dashboard.""" + mock_client.device_info.side_effect = [ + RequiresEncryptionAPIError, + InvalidEncryptionKeyAPIError("Wrong key", "test"), + DeviceInfo( + uses_password=False, + name="test", + mac_address="11:22:33:44:55:AA", + ), + ] + + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "encryption_key" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: VALID_NOISE_PSK, + CONF_DEVICE_NAME: "test", + } + assert mock_client.noise_psk == VALID_NOISE_PSK From 6a54c1881847e763e04dba36b8a6860d933eb87c Mon Sep 17 00:00:00 2001 From: Luke Date: Tue, 11 Jul 2023 11:32:06 -0400 Subject: [PATCH 136/184] Bump Roborock to v0.30.0 (#96268) bump to v0.30.0 --- 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 baab687e64a..0cf6db4ae81 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.29.2"] + "requirements": ["python-roborock==0.30.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index efb87edfa0f..0419e0a123e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2139,7 +2139,7 @@ python-qbittorrent==0.4.3 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.29.2 +python-roborock==0.30.0 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 94d4e4857da..86995da0c3b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1565,7 +1565,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.3 # homeassistant.components.roborock -python-roborock==0.29.2 +python-roborock==0.30.0 # homeassistant.components.smarttub python-smarttub==0.0.33 From 382bfa24a873e4269fa42fafddb415d0eede10f6 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 11 Jul 2023 20:10:14 +0200 Subject: [PATCH 137/184] Use explicit device naming for Escea (#96270) --- homeassistant/components/escea/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/escea/climate.py b/homeassistant/components/escea/climate.py index df191afb859..0c85705a2a6 100644 --- a/homeassistant/components/escea/climate.py +++ b/homeassistant/components/escea/climate.py @@ -76,6 +76,7 @@ class ControllerEntity(ClimateEntity): _attr_fan_modes = list(_HA_FAN_TO_ESCEA) _attr_has_entity_name = True + _attr_name = None _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] _attr_icon = ICON _attr_precision = PRECISION_WHOLE From bdcc9ec984f924e31245d5526dcc8c547775a36d Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 11 Jul 2023 14:09:52 -0400 Subject: [PATCH 138/184] Fix ZHA serialization issue with warning devices (#96275) * Bump ZHA dependencies * Update unit tests to reduce mocks --- homeassistant/components/zha/manifest.json | 4 +- requirements_all.txt | 4 +- requirements_test_all.txt | 4 +- tests/components/zha/test_siren.py | 88 +++++++++++++++------- 4 files changed, 68 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 293822987c3..7694a85b8ed 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -25,10 +25,10 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.101", "zigpy-deconz==0.21.0", - "zigpy==0.56.1", + "zigpy==0.56.2", "zigpy-xbee==0.18.1", "zigpy-zigate==0.11.0", - "zigpy-znp==0.11.2" + "zigpy-znp==0.11.3" ], "usb": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 0419e0a123e..a3938050427 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2759,10 +2759,10 @@ zigpy-xbee==0.18.1 zigpy-zigate==0.11.0 # homeassistant.components.zha -zigpy-znp==0.11.2 +zigpy-znp==0.11.3 # homeassistant.components.zha -zigpy==0.56.1 +zigpy==0.56.2 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 86995da0c3b..12432ad8310 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2023,10 +2023,10 @@ zigpy-xbee==0.18.1 zigpy-zigate==0.11.0 # homeassistant.components.zha -zigpy-znp==0.11.2 +zigpy-znp==0.11.3 # homeassistant.components.zha -zigpy==0.56.1 +zigpy==0.56.2 # homeassistant.components.zwave_js zwave-js-server-python==0.49.0 diff --git a/tests/components/zha/test_siren.py b/tests/components/zha/test_siren.py index 7346f1e5bcb..2df6c2be5db 100644 --- a/tests/components/zha/test_siren.py +++ b/tests/components/zha/test_siren.py @@ -1,10 +1,11 @@ """Test zha siren.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import ANY, call, patch import pytest from zigpy.const import SIG_EP_PROFILE import zigpy.profiles.zha as zha +import zigpy.zcl import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.security as security import zigpy.zcl.foundation as zcl_f @@ -85,48 +86,76 @@ async def test_siren(hass: HomeAssistant, siren) -> None: # turn on from HA with patch( - "zigpy.zcl.Cluster.request", + "zigpy.device.Device.request", return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), + ), patch( + "zigpy.zcl.Cluster.request", + side_effect=zigpy.zcl.Cluster.request, + autospec=True, ): # turn on via UI await hass.services.async_call( SIREN_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True ) - assert len(cluster.request.mock_calls) == 1 - assert cluster.request.call_args[0][0] is False - assert cluster.request.call_args[0][1] == 0 - assert cluster.request.call_args[0][3] == 50 # bitmask for default args - assert cluster.request.call_args[0][4] == 5 # duration in seconds - assert cluster.request.call_args[0][5] == 0 - assert cluster.request.call_args[0][6] == 2 + assert cluster.request.mock_calls == [ + call( + cluster, + False, + 0, + ANY, + 50, # bitmask for default args + 5, # duration in seconds + 0, + 2, + manufacturer=None, + expect_reply=True, + tsn=None, + ) + ] # test that the state has changed to on assert hass.states.get(entity_id).state == STATE_ON # turn off from HA with patch( - "zigpy.zcl.Cluster.request", + "zigpy.device.Device.request", return_value=mock_coro([0x01, zcl_f.Status.SUCCESS]), + ), patch( + "zigpy.zcl.Cluster.request", + side_effect=zigpy.zcl.Cluster.request, + autospec=True, ): # turn off via UI await hass.services.async_call( SIREN_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True ) - assert len(cluster.request.mock_calls) == 1 - assert cluster.request.call_args[0][0] is False - assert cluster.request.call_args[0][1] == 0 - assert cluster.request.call_args[0][3] == 2 # bitmask for default args - assert cluster.request.call_args[0][4] == 5 # duration in seconds - assert cluster.request.call_args[0][5] == 0 - assert cluster.request.call_args[0][6] == 2 + assert cluster.request.mock_calls == [ + call( + cluster, + False, + 0, + ANY, + 2, # bitmask for default args + 5, # duration in seconds + 0, + 2, + manufacturer=None, + expect_reply=True, + tsn=None, + ) + ] # test that the state has changed to off assert hass.states.get(entity_id).state == STATE_OFF # turn on from HA with patch( - "zigpy.zcl.Cluster.request", + "zigpy.device.Device.request", return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), + ), patch( + "zigpy.zcl.Cluster.request", + side_effect=zigpy.zcl.Cluster.request, + autospec=True, ): # turn on via UI await hass.services.async_call( @@ -140,14 +169,21 @@ async def test_siren(hass: HomeAssistant, siren) -> None: }, blocking=True, ) - assert len(cluster.request.mock_calls) == 1 - assert cluster.request.call_args[0][0] is False - assert cluster.request.call_args[0][1] == 0 - assert cluster.request.call_args[0][3] == 97 # bitmask for passed args - assert cluster.request.call_args[0][4] == 10 # duration in seconds - assert cluster.request.call_args[0][5] == 0 - assert cluster.request.call_args[0][6] == 2 - + assert cluster.request.mock_calls == [ + call( + cluster, + False, + 0, + ANY, + 97, # bitmask for passed args + 10, # duration in seconds + 0, + 2, + manufacturer=None, + expect_reply=True, + tsn=None, + ) + ] # test that the state has changed to on assert hass.states.get(entity_id).state == STATE_ON From 0b3ff859e615b11bcc751cf074676feb4e55dcc1 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 11 Jul 2023 19:42:59 +0200 Subject: [PATCH 139/184] Bump reolink_aio to 0.7.3 (#96284) --- homeassistant/components/reolink/manifest.json | 2 +- homeassistant/components/reolink/select.py | 2 +- homeassistant/components/reolink/strings.json | 4 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 69b3d5db6f7..00f0e0f518b 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.1"] + "requirements": ["reolink-aio==0.7.3"] } diff --git a/homeassistant/components/reolink/select.py b/homeassistant/components/reolink/select.py index 6303bc58131..2ae3442278e 100644 --- a/homeassistant/components/reolink/select.py +++ b/homeassistant/components/reolink/select.py @@ -49,7 +49,7 @@ SELECT_ENTITIES = ( icon="mdi:spotlight-beam", entity_category=EntityCategory.CONFIG, translation_key="floodlight_mode", - get_options=[mode.name for mode in SpotlightModeEnum], + get_options=lambda api, ch: api.whiteled_mode_list(ch), supported=lambda api, ch: api.supported(ch, "floodLight"), value=lambda api, ch: SpotlightModeEnum(api.whiteled_mode(ch)).name, method=lambda api, ch, name: api.set_whiteled(ch, mode=name), diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index f208e3e4035..8abbbf23aad 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -62,7 +62,9 @@ "state": { "off": "Off", "auto": "Auto", - "schedule": "Schedule" + "schedule": "Schedule", + "adaptive": "Adaptive", + "autoadaptive": "Auto adaptive" } }, "day_night_mode": { diff --git a/requirements_all.txt b/requirements_all.txt index a3938050427..50926badfd8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2267,7 +2267,7 @@ renault-api==0.1.13 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.1 +reolink-aio==0.7.3 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12432ad8310..2b98fd2e3ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1660,7 +1660,7 @@ renault-api==0.1.13 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.1 +reolink-aio==0.7.3 # homeassistant.components.rflink rflink==0.0.65 From 15ab483f6195bd8474637a3fe30c729dc292aef1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Jul 2023 01:29:05 -1000 Subject: [PATCH 140/184] Bump aiohomekit to 2.6.7 (#96291) --- 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 d0a88bf8249..2a9e2225e9f 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==2.6.5"], + "requirements": ["aiohomekit==2.6.7"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 50926badfd8..ee2ac9755c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -252,7 +252,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==2.6.5 +aiohomekit==2.6.7 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b98fd2e3ce..e2048599d6a 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==2.6.5 +aiohomekit==2.6.7 # homeassistant.components.emulated_hue # homeassistant.components.http From 89e737facbfd077f2de16c81196e4acf478c6a15 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 11 Jul 2023 20:12:16 +0200 Subject: [PATCH 141/184] Add has entity name to Blink (#96322) --- homeassistant/components/blink/alarm_control_panel.py | 1 + homeassistant/components/blink/binary_sensor.py | 3 ++- homeassistant/components/blink/camera.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py index 5d0ea67f31d..75a2644791e 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -42,6 +42,7 @@ class BlinkSyncModule(AlarmControlPanelEntity): _attr_icon = ICON _attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY _attr_name = None + _attr_has_entity_name = True def __init__(self, data, name, sync): """Initialize the alarm control panel.""" diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index c7daf0ec1e1..1487c6a7b42 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -58,13 +58,14 @@ async def async_setup_entry( class BlinkBinarySensor(BinarySensorEntity): """Representation of a Blink binary sensor.""" + _attr_has_entity_name = True + def __init__( self, data, camera, description: BinarySensorEntityDescription ) -> None: """Initialize the sensor.""" self.data = data self.entity_description = description - self._attr_name = f"{DOMAIN} {camera} {description.name}" self._camera = data.cameras[camera] self._attr_unique_id = f"{self._camera.serial}-{description.key}" self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index e74555f8db9..9740e427e9c 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -38,6 +38,7 @@ async def async_setup_entry( class BlinkCamera(Camera): """An implementation of a Blink Camera.""" + _attr_has_entity_name = True _attr_name = None def __init__(self, data, name, camera): From 3dc1ceed0b8383b483bfb350f9b1f167dc816a6b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 11 Jul 2023 10:21:05 -0600 Subject: [PATCH 142/184] Fix extra verbiage in Ridwell rotating category sensor (#96345) --- homeassistant/components/ridwell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ridwell/manifest.json b/homeassistant/components/ridwell/manifest.json index 5b9b443b65e..72a29182169 100644 --- a/homeassistant/components/ridwell/manifest.json +++ b/homeassistant/components/ridwell/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["aioridwell"], - "requirements": ["aioridwell==2023.01.0"] + "requirements": ["aioridwell==2023.07.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index ee2ac9755c9..80b832e905d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -330,7 +330,7 @@ aioqsw==0.3.2 aiorecollect==1.0.8 # homeassistant.components.ridwell -aioridwell==2023.01.0 +aioridwell==2023.07.0 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2048599d6a..ba38d8b3b2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -302,7 +302,7 @@ aioqsw==0.3.2 aiorecollect==1.0.8 # homeassistant.components.ridwell -aioridwell==2023.01.0 +aioridwell==2023.07.0 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 From c78628aa291c108f5dd751f957aeaa25a9f744f8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 01:08:31 +0200 Subject: [PATCH 143/184] Update RestrictedPython to 6.1 (#96358) --- 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 63aa2f2f916..ea153be11cf 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.0"] + "requirements": ["RestrictedPython==6.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 80b832e905d..5c0f16dae97 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -124,7 +124,7 @@ PyXiaomiGateway==0.14.3 RachioPy==1.0.3 # homeassistant.components.python_script -RestrictedPython==6.0 +RestrictedPython==6.1 # homeassistant.components.remember_the_milk RtmAPI==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba38d8b3b2e..2c50f874cf4 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.0 +RestrictedPython==6.1 # homeassistant.components.remember_the_milk RtmAPI==0.7.2 From e6c8e0460fde87860c8fd816926380fd24e2d501 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 12 Jul 2023 16:40:03 +0200 Subject: [PATCH 144/184] Add explicit device naming to Led BLE (#96421) --- homeassistant/components/led_ble/light.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/led_ble/light.py b/homeassistant/components/led_ble/light.py index 22a52e61b63..94f445f1ec1 100644 --- a/homeassistant/components/led_ble/light.py +++ b/homeassistant/components/led_ble/light.py @@ -43,6 +43,7 @@ class LEDBLEEntity(CoordinatorEntity, LightEntity): _attr_supported_color_modes = {ColorMode.RGB, ColorMode.WHITE} _attr_has_entity_name = True + _attr_name = None _attr_supported_features = LightEntityFeature.EFFECT def __init__( From 658e87b6a537b308a87461f6e661012635cc8417 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 12 Jul 2023 20:54:48 +0200 Subject: [PATCH 145/184] Always add guest wifi qr code entity in AVM Fritz!Tools (#96435) --- homeassistant/components/fritz/image.py | 3 -- .../fritz/snapshots/test_image.ambr | 3 ++ tests/components/fritz/test_image.py | 44 +++++++++---------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/fritz/image.py b/homeassistant/components/fritz/image.py index 597dd8ddb53..d14c562bd76 100644 --- a/homeassistant/components/fritz/image.py +++ b/homeassistant/components/fritz/image.py @@ -30,9 +30,6 @@ async def async_setup_entry( avm_wrapper.fritz_guest_wifi.get_info ) - if not guest_wifi_info.get("NewEnable"): - return - async_add_entities( [ FritzGuestWifiQRImage( diff --git a/tests/components/fritz/snapshots/test_image.ambr b/tests/components/fritz/snapshots/test_image.ambr index b64d8601a8a..452aab2a887 100644 --- a/tests/components/fritz/snapshots/test_image.ambr +++ b/tests/components/fritz/snapshots/test_image.ambr @@ -8,6 +8,9 @@ # name: test_image_entity[fc_data0] b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x94\x00\x00\x00\x94\x01\x00\x00\x00\x00]G=y\x00\x00\x00\xf5IDATx\xda\xedVQ\x0eC!\x0c"\xbb@\xef\x7fKn\xe0\x00\xfd\xdb\xcf6\xf9|\xc6\xc4\xc6\x0f\xd2\x02\xadb},\xe2\xb9\xfb\xe5\x0e\xc0(\x18\xf2\x84/|\xaeo\xef\x847\xda\x14\x1af\x1c\xde\xe3\x19(X\tKxN\xb2\x87\x17j9\x1d None: - """Test image entities.""" - - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) - entry.add_to_hass(hass) - - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - assert entry.state == ConfigEntryState.LOADED - - images = hass.states.async_all(IMAGE_DOMAIN) - assert len(images) == 0 From 4f95039dfd3a48fba83fd1fa94556268131a6f13 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jul 2023 14:39:51 -1000 Subject: [PATCH 146/184] Fix mixed case service schema registration (#96448) --- homeassistant/core.py | 2 +- homeassistant/helpers/service.py | 3 +++ tests/helpers/test_service.py | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 528ab3f6d01..661f087fe6a 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1768,7 +1768,7 @@ class ServiceRegistry: the context. Will return NONE if the service does not exist as there is other error handling when calling the service if it does not exist. """ - if not (handler := self._services[domain][service]): + if not (handler := self._services[domain.lower()][service.lower()]): return SupportsResponse.NONE return handler.supports_response diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 1164c2d8015..dcd7115f363 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -670,6 +670,9 @@ def async_set_service_schema( hass: HomeAssistant, domain: str, service: str, schema: dict[str, Any] ) -> None: """Register a description for a service.""" + domain = domain.lower() + service = service.lower() + descriptions_cache: dict[ tuple[str, str], dict[str, Any] | None ] = hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {}) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 6adec334bb0..36f87b7553b 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -724,6 +724,27 @@ async def test_async_get_all_descriptions_dynamically_created_services( } +async def test_register_with_mixed_case(hass: HomeAssistant) -> None: + """Test registering a service with mixed case. + + For backwards compatibility, we have historically allowed mixed case, + and automatically converted it to lowercase. + """ + logger = hass.components.logger + logger_config = {logger.DOMAIN: {}} + await async_setup_component(hass, logger.DOMAIN, logger_config) + logger_domain_mixed = "LoGgEr" + hass.services.async_register( + logger_domain_mixed, "NeW_SeRVICE", lambda x: None, None + ) + service.async_set_service_schema( + hass, logger_domain_mixed, "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" + + 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 2822d9854450cb7bf7aa56e5ccb8b1855cce1560 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jul 2023 14:37:28 -1000 Subject: [PATCH 147/184] Ensure ESPHome dashboard connection recovers if its down when core starts (#96449) --- homeassistant/components/esphome/dashboard.py | 7 ------- tests/components/esphome/test_dashboard.py | 4 +++- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index c9d74f0b30c..4cbb9cbe847 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -93,13 +93,6 @@ class ESPHomeDashboardManager: hass, addon_slug, url, async_get_clientsession(hass) ) await dashboard.async_request_refresh() - if not cur_dashboard and not dashboard.last_update_success: - # If there was no previous dashboard and the new one is not available, - # we skip setup and wait for discovery. - _LOGGER.error( - "Dashboard unavailable; skipping setup: %s", dashboard.last_exception - ) - return self._current_dashboard = dashboard diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index d16bf7c4d00..d8732ea0453 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -58,7 +58,9 @@ async def test_setup_dashboard_fails( assert mock_config_entry.state == ConfigEntryState.LOADED assert mock_get_devices.call_count == 1 - assert dashboard.STORAGE_KEY not in hass_storage + # The dashboard addon might recover later so we still + # allow it to be set up. + assert dashboard.STORAGE_KEY in hass_storage async def test_setup_dashboard_fails_when_already_setup( From a98295814383616538dc3dba2bfdb2b37a212eb3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 13 Jul 2023 11:36:22 -0400 Subject: [PATCH 148/184] Bumped version to 2023.7.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 cc04180a618..30a7fc37c9e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 7 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 2de9c9de5d1..a902941213d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.7.1" +version = "2023.7.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 6ed119006deeebc8c326b98c11e651837dcd1a75 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 8 Jul 2023 01:42:19 +0200 Subject: [PATCH 149/184] Use device class naming for SimpliSafe (#96093) --- homeassistant/components/simplisafe/alarm_control_panel.py | 1 + homeassistant/components/simplisafe/binary_sensor.py | 1 - homeassistant/components/simplisafe/button.py | 2 +- homeassistant/components/simplisafe/strings.json | 7 +++++++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 4913d76c0c9..b895be83f2e 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -127,6 +127,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY ) + _attr_name = None def __init__(self, simplisafe: SimpliSafe, system: SystemType) -> None: """Initialize the SimpliSafe alarm.""" diff --git a/homeassistant/components/simplisafe/binary_sensor.py b/homeassistant/components/simplisafe/binary_sensor.py index d31dc5da282..34c0ea5ea95 100644 --- a/homeassistant/components/simplisafe/binary_sensor.py +++ b/homeassistant/components/simplisafe/binary_sensor.py @@ -111,7 +111,6 @@ class BatteryBinarySensor(SimpliSafeEntity, BinarySensorEntity): """Initialize.""" super().__init__(simplisafe, system, device=device) - self._attr_name = "Battery" self._attr_unique_id = f"{super().unique_id}-battery" self._device: DeviceV3 diff --git a/homeassistant/components/simplisafe/button.py b/homeassistant/components/simplisafe/button.py index d8da8bc7592..bd60c040f56 100644 --- a/homeassistant/components/simplisafe/button.py +++ b/homeassistant/components/simplisafe/button.py @@ -44,7 +44,7 @@ async def _async_clear_notifications(system: System) -> None: BUTTON_DESCRIPTIONS = ( SimpliSafeButtonDescription( key=BUTTON_KIND_CLEAR_NOTIFICATIONS, - name="Clear notifications", + translation_key=BUTTON_KIND_CLEAR_NOTIFICATIONS, push_action=_async_clear_notifications, ), ) diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 618c21566f7..4f230442f85 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -29,5 +29,12 @@ } } } + }, + "entity": { + "button": { + "clear_notifications": { + "name": "Clear notifications" + } + } } } From 1f11a75ab1354ecec88cf4f026ee3275bf954081 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jul 2023 15:23:00 -1000 Subject: [PATCH 150/184] Always try PullPoint with ONVIF (#96377) --- homeassistant/components/onvif/device.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index a524d8ea519..358cbbf5c83 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -387,8 +387,12 @@ class ONVIFDevice: "WSPullPointSupport" ) LOGGER.debug("%s: WSPullPointSupport: %s", self.name, pull_point_support) + # Even if the camera claims it does not support PullPoint, try anyway + # since at least some AXIS and Bosch models do. The reverse is also + # true where some cameras claim they support PullPoint but don't so + # the only way to know is to try. return await self.events.async_start( - pull_point_support is not False, + True, self.config_entry.options.get( CONF_ENABLE_WEBHOOKS, DEFAULT_ENABLE_WEBHOOKS ), From bc795f4953e10437734e32738be62f4159813ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20L=C3=B6vdahl?= Date: Fri, 14 Jul 2023 00:36:26 +0300 Subject: [PATCH 151/184] Bump vallox-websocket-api to 3.3.0 (#96493) --- homeassistant/components/vallox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vallox/manifest.json b/homeassistant/components/vallox/manifest.json index 4f3fcbf9c87..479c84d238c 100644 --- a/homeassistant/components/vallox/manifest.json +++ b/homeassistant/components/vallox/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/vallox", "iot_class": "local_polling", "loggers": ["vallox_websocket_api"], - "requirements": ["vallox-websocket-api==3.2.1"] + "requirements": ["vallox-websocket-api==3.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5c0f16dae97..e41656ef005 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2599,7 +2599,7 @@ url-normalize==1.4.3 uvcclient==0.11.0 # homeassistant.components.vallox -vallox-websocket-api==3.2.1 +vallox-websocket-api==3.3.0 # homeassistant.components.rdw vehicle==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2c50f874cf4..1ab2d6b0ac6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1899,7 +1899,7 @@ url-normalize==1.4.3 uvcclient==0.11.0 # homeassistant.components.vallox -vallox-websocket-api==3.2.1 +vallox-websocket-api==3.3.0 # homeassistant.components.rdw vehicle==1.0.1 From a50a96b687024a0c3ca71413ec68e19b67b30388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20L=C3=B6vdahl?= Date: Thu, 13 Jul 2023 21:42:30 +0300 Subject: [PATCH 152/184] Fix Vallox fan entity naming (#96494) --- homeassistant/components/vallox/fan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index 34eee944114..b43dabbba80 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -84,6 +84,7 @@ class ValloxFanEntity(ValloxEntity, FanEntity): """Representation of the fan.""" _attr_has_entity_name = True + _attr_name = None _attr_supported_features = FanEntityFeature.PRESET_MODE | FanEntityFeature.SET_SPEED def __init__( From a9902a21bf4b86b40a02c54324128e23cf3a8761 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Fri, 14 Jul 2023 15:04:23 +0200 Subject: [PATCH 153/184] Bump devolo_plc_api to 1.3.2 (#96499) --- homeassistant/components/devolo_home_network/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/devolo_home_network/manifest.json b/homeassistant/components/devolo_home_network/manifest.json index e635b1f7021..54b65c17e60 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.3.1"], + "requirements": ["devolo-plc-api==1.3.2"], "zeroconf": [ { "type": "_dvl-deviceapi._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index e41656ef005..ea0e96dea6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ denonavr==0.11.2 devolo-home-control-api==0.18.2 # homeassistant.components.devolo_home_network -devolo-plc-api==1.3.1 +devolo-plc-api==1.3.2 # homeassistant.components.directv directv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ab2d6b0ac6..6febdf51903 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -532,7 +532,7 @@ denonavr==0.11.2 devolo-home-control-api==0.18.2 # homeassistant.components.devolo_home_network -devolo-plc-api==1.3.1 +devolo-plc-api==1.3.2 # homeassistant.components.directv directv==0.4.0 From 2967a00bc663ec60935a700780bb62ec2a0564f2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 14 Jul 2023 15:04:48 +0200 Subject: [PATCH 154/184] Support MyStrom switch 120 (#96535) --- homeassistant/components/mystrom/__init__.py | 4 ++-- tests/components/mystrom/test_init.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mystrom/__init__.py b/homeassistant/components/mystrom/__init__.py index 972db00e476..3166c05db19 100644 --- a/homeassistant/components/mystrom/__init__.py +++ b/homeassistant/components/mystrom/__init__.py @@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: info.setdefault("type", 101) device_type = info["type"] - if device_type in [101, 106, 107]: + if device_type in [101, 106, 107, 120]: device = _get_mystrom_switch(host) platforms = PLATFORMS_SWITCH await _async_get_device_state(device, info["ip"]) @@ -86,7 +86,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" device_type = hass.data[DOMAIN][entry.entry_id].info["type"] platforms = [] - if device_type in [101, 106, 107]: + if device_type in [101, 106, 107, 120]: platforms.extend(PLATFORMS_SWITCH) elif device_type in [102, 105]: platforms.extend(PLATFORMS_BULB) diff --git a/tests/components/mystrom/test_init.py b/tests/components/mystrom/test_init.py index 80011b47915..4100a270e0a 100644 --- a/tests/components/mystrom/test_init.py +++ b/tests/components/mystrom/test_init.py @@ -68,7 +68,7 @@ async def test_init_switch_and_unload( (110, "sensor", ConfigEntryState.SETUP_ERROR, True), (113, "switch", ConfigEntryState.SETUP_ERROR, True), (118, "button", ConfigEntryState.SETUP_ERROR, True), - (120, "switch", ConfigEntryState.SETUP_ERROR, True), + (120, "switch", ConfigEntryState.LOADED, False), ], ) async def test_init_bulb( From 8382feeff41a9e76193e907e9402a36c397eb083 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jul 2023 14:28:29 -1000 Subject: [PATCH 155/184] Defer SSDP UPNP server start until the started event (#96555) --- homeassistant/components/ssdp/__init__.py | 12 ++++++++---- tests/components/ssdp/test_init.py | 2 ++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index e448fe066c4..4bc9bb24835 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -42,11 +42,12 @@ from async_upnp_client.utils import CaseInsensitiveDict from homeassistant import config_entries from homeassistant.components import network from homeassistant.const import ( + EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL, __version__ as current_version, ) -from homeassistant.core import HomeAssistant, callback as core_callback +from homeassistant.core import Event, HomeAssistant, callback as core_callback from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import config_validation as cv, discovery_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -728,15 +729,18 @@ class Server: async def async_start(self) -> None: """Start the server.""" - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) - await self._async_start_upnp_servers() + bus = self.hass.bus + bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) + bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, self._async_start_upnp_servers + ) async def _async_get_instance_udn(self) -> str: """Get Unique Device Name for this instance.""" instance_id = await async_get_instance_id(self.hass) return f"uuid:{instance_id[0:8]}-{instance_id[8:12]}-{instance_id[12:16]}-{instance_id[16:20]}-{instance_id[20:32]}".upper() - async def _async_start_upnp_servers(self) -> None: + async def _async_start_upnp_servers(self, event: Event) -> None: """Start the UPnP/SSDP servers.""" # Update UDN with our instance UDN. udn = await self._async_get_instance_udn() diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index a80b9f48798..ed5241a42ad 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -742,6 +742,8 @@ async def test_bind_failure_skips_adapter( SsdpListener.async_start = _async_start UpnpServer.async_start = _async_start await init_ssdp_component(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() assert "Failed to setup listener for" in caplog.text From 7425d97ee18c472cb0c2e6b434e9e1a591f9f1e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jul 2023 12:14:32 -1000 Subject: [PATCH 156/184] Handle empty strings for ESPHome UOMs (#96556) --- homeassistant/components/esphome/number.py | 5 +++- homeassistant/components/esphome/sensor.py | 5 +++- tests/components/esphome/test_number.py | 35 +++++++++++++++++++++- tests/components/esphome/test_sensor.py | 29 +++++++++++++++++- 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index 4e3d052e6ef..6be1822f90f 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -63,7 +63,10 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): self._attr_native_min_value = static_info.min_value self._attr_native_max_value = static_info.max_value self._attr_native_step = static_info.step - self._attr_native_unit_of_measurement = static_info.unit_of_measurement + # protobuf doesn't support nullable strings so we need to check + # if the string is empty + if unit_of_measurement := static_info.unit_of_measurement: + self._attr_native_unit_of_measurement = unit_of_measurement if mode := static_info.mode: self._attr_mode = NUMBER_MODES.from_esphome(mode) else: diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 3185a5eb536..2e658389e03 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -76,7 +76,10 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): super()._on_static_info_update(static_info) static_info = self._static_info self._attr_force_update = static_info.force_update - self._attr_native_unit_of_measurement = static_info.unit_of_measurement + # protobuf doesn't support nullable strings so we need to check + # if the string is empty + if unit_of_measurement := static_info.unit_of_measurement: + self._attr_native_unit_of_measurement = unit_of_measurement self._attr_device_class = try_parse_enum( SensorDeviceClass, static_info.device_class ) diff --git a/tests/components/esphome/test_number.py b/tests/components/esphome/test_number.py index 8157c5f5c3d..f928146996c 100644 --- a/tests/components/esphome/test_number.py +++ b/tests/components/esphome/test_number.py @@ -15,7 +15,7 @@ from homeassistant.components.number import ( DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN from homeassistant.core import HomeAssistant @@ -89,3 +89,36 @@ async def test_generic_number_nan( state = hass.states.get("number.test_my_number") assert state is not None assert state.state == STATE_UNKNOWN + + +async def test_generic_number_with_unit_of_measurement_as_empty_string( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry, +) -> None: + """Test a generic number entity with nan state.""" + entity_info = [ + NumberInfo( + object_id="mynumber", + key=1, + name="my number", + unique_id="my_number", + max_value=100, + min_value=0, + step=1, + unit_of_measurement="", + mode=ESPHomeNumberMode.SLIDER, + ) + ] + states = [NumberState(key=1, state=42)] + user_service = [] + await mock_generic_device_entry( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("number.test_mynumber") + assert state is not None + assert state.state == "42" + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes diff --git a/tests/components/esphome/test_sensor.py b/tests/components/esphome/test_sensor.py index 8f4eb0f9513..43431ca36a0 100644 --- a/tests/components/esphome/test_sensor.py +++ b/tests/components/esphome/test_sensor.py @@ -13,7 +13,7 @@ from aioesphomeapi import ( ) from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass -from homeassistant.const import ATTR_ICON, STATE_UNKNOWN +from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory @@ -275,3 +275,30 @@ async def test_generic_text_sensor( state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == "i am a teapot" + + +async def test_generic_numeric_sensor_empty_string_uom( + hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry +) -> None: + """Test a generic numeric sensor that has an empty string as the uom.""" + entity_info = [ + SensorInfo( + object_id="mysensor", + key=1, + name="my sensor", + unique_id="my_sensor", + unit_of_measurement="", + ) + ] + states = [SensorState(key=1, state=123, missing_state=False)] + user_service = [] + await mock_generic_device_entry( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("sensor.test_mysensor") + assert state is not None + assert state.state == "123" + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes From 173a61bdff2064360a47a3644b361aca9460d5aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jul 2023 14:49:20 -1000 Subject: [PATCH 157/184] Bump onvif-zeep-async to 3.1.12 (#96560) --- homeassistant/components/onvif/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index e92e80a9a68..d03073dcfd3 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/onvif", "iot_class": "local_push", "loggers": ["onvif", "wsdiscovery", "zeep"], - "requirements": ["onvif-zeep-async==3.1.9", "WSDiscovery==2.0.0"] + "requirements": ["onvif-zeep-async==3.1.12", "WSDiscovery==2.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index ea0e96dea6f..1f1ca9bf97b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1327,7 +1327,7 @@ ondilo==0.2.0 onkyo-eiscp==1.2.7 # homeassistant.components.onvif -onvif-zeep-async==3.1.9 +onvif-zeep-async==3.1.12 # homeassistant.components.opengarage open-garage==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6febdf51903..b560c4a720b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1011,7 +1011,7 @@ omnilogic==0.4.5 ondilo==0.2.0 # homeassistant.components.onvif -onvif-zeep-async==3.1.9 +onvif-zeep-async==3.1.12 # homeassistant.components.opengarage open-garage==0.2.0 From b193b5667bfa45fa3112bfaa7a8ab31fc4ebc2cd Mon Sep 17 00:00:00 2001 From: Aaron Collins Date: Sun, 16 Jul 2023 00:17:02 +1200 Subject: [PATCH 158/184] Fix daikin missing key after migration (#96575) Fix daikin migration --- homeassistant/components/daikin/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index b0097f607d5..3ef9c0aba62 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -139,7 +139,7 @@ async def async_migrate_unique_id( dev_reg = dr.async_get(hass) old_unique_id = config_entry.unique_id new_unique_id = api.device.mac - new_name = api.device.values["name"] + new_name = api.device.values.get("name") @callback def _update_unique_id(entity_entry: er.RegistryEntry) -> dict[str, str] | None: From cfcdf866ddb0010bf757593bef0ca376bc15b3b7 Mon Sep 17 00:00:00 2001 From: quthla Date: Thu, 20 Jul 2023 10:07:03 +0200 Subject: [PATCH 159/184] Ensure androidtv_remote does not block startup of HA (#96582) * Ensure androidtv_remote does not block startup of HA * Fix lint * Use asyncio.wait_for * Update homeassistant/components/androidtv_remote/__init__.py Co-authored-by: Erik Montnemery * Update homeassistant/components/androidtv_remote/__init__.py Co-authored-by: Erik Montnemery * Fix lint * Lint * Update __init__.py --------- Co-authored-by: Erik Montnemery --- homeassistant/components/androidtv_remote/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/androidtv_remote/__init__.py b/homeassistant/components/androidtv_remote/__init__.py index bdcf08bb2f6..9299b1ed0b0 100644 --- a/homeassistant/components/androidtv_remote/__init__.py +++ b/homeassistant/components/androidtv_remote/__init__.py @@ -1,6 +1,7 @@ """The Android TV Remote integration.""" from __future__ import annotations +import asyncio import logging from androidtvremote2 import ( @@ -9,6 +10,7 @@ from androidtvremote2 import ( ConnectionClosed, InvalidAuth, ) +import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform @@ -43,11 +45,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api.add_is_available_updated_callback(is_available_updated) try: - await api.async_connect() + async with async_timeout.timeout(5.0): + await api.async_connect() except InvalidAuth as exc: # The Android TV is hard reset or the certificate and key files were deleted. raise ConfigEntryAuthFailed from exc - except (CannotConnect, ConnectionClosed) as exc: + except (CannotConnect, ConnectionClosed, asyncio.TimeoutError) as exc: # The Android TV is network unreachable. Raise exception and let Home Assistant retry # later. If device gets a new IP address the zeroconf flow will update the config. raise ConfigEntryNotReady from exc From f5ba9af4948b98eacef9c9a38fa86e1b3ef451a7 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 15 Jul 2023 09:02:59 -0700 Subject: [PATCH 160/184] Bump pyrainbird to 2.1.1 (#96601) --- 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 a44cfb3ce13..f1f1aed044b 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.1.0"] + "requirements": ["pyrainbird==2.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1f1ca9bf97b..6ab293b3333 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.1.0 +pyrainbird==2.1.1 # homeassistant.components.recswitch pyrecswitch==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b560c4a720b..9d78be42bda 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.1.0 +pyrainbird==2.1.1 # homeassistant.components.risco pyrisco==0.5.7 From 653b1e5ad8d6d98250f6813c83cb67f8aa0912d3 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 15 Jul 2023 14:11:14 -0700 Subject: [PATCH 161/184] Bump pyrainbird to 3.0.0 (#96610) --- 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 f1f1aed044b..986e89783d7 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.1.1"] + "requirements": ["pyrainbird==3.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6ab293b3333..db28b2f0e7a 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.1.1 +pyrainbird==3.0.0 # homeassistant.components.recswitch pyrecswitch==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d78be42bda..709d305e151 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.1.1 +pyrainbird==3.0.0 # homeassistant.components.risco pyrisco==0.5.7 From d866cdb9cf4003ab55ea7c734e7192b389c69ca1 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Tue, 18 Jul 2023 07:13:31 -0500 Subject: [PATCH 162/184] Fix SmartThings Cover Set Position (for window shades) (#96612) * Update smartthings dependencies * Update cover to support window_shade_level --- homeassistant/components/smartthings/cover.py | 28 +++++++++++--- .../components/smartthings/manifest.json | 2 +- requirements_all.txt | 4 +- requirements_test_all.txt | 4 +- tests/components/smartthings/test_cover.py | 37 ++++++++++++++++++- 5 files changed, 62 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/smartthings/cover.py b/homeassistant/components/smartthings/cover.py index d2d0dba6773..5d7e29c1312 100644 --- a/homeassistant/components/smartthings/cover.py +++ b/homeassistant/components/smartthings/cover.py @@ -62,7 +62,11 @@ def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: # Must have one of the min_required if any(capability in capabilities for capability in min_required): # Return all capabilities supported/consumed - return min_required + [Capability.battery, Capability.switch_level] + return min_required + [ + Capability.battery, + Capability.switch_level, + Capability.window_shade_level, + ] return None @@ -74,12 +78,16 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): """Initialize the cover class.""" super().__init__(device) self._device_class = None + self._current_cover_position = None self._state = None self._state_attrs = None self._attr_supported_features = ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE ) - if Capability.switch_level in device.capabilities: + if ( + Capability.switch_level in device.capabilities + or Capability.window_shade_level in device.capabilities + ): self._attr_supported_features |= CoverEntityFeature.SET_POSITION async def async_close_cover(self, **kwargs: Any) -> None: @@ -103,7 +111,12 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): if not self.supported_features & CoverEntityFeature.SET_POSITION: return # Do not set_status=True as device will report progress. - await self._device.set_level(kwargs[ATTR_POSITION], 0) + if Capability.window_shade_level in self._device.capabilities: + await self._device.set_window_shade_level( + kwargs[ATTR_POSITION], set_status=False + ) + else: + await self._device.set_level(kwargs[ATTR_POSITION], set_status=False) async def async_update(self) -> None: """Update the attrs of the cover.""" @@ -117,6 +130,11 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): self._device_class = CoverDeviceClass.GARAGE self._state = VALUE_TO_STATE.get(self._device.status.door) + if Capability.window_shade_level in self._device.capabilities: + self._current_cover_position = self._device.status.shade_level + elif Capability.switch_level in self._device.capabilities: + self._current_cover_position = self._device.status.level + self._state_attrs = {} battery = self._device.status.attributes[Attribute.battery].value if battery is not None: @@ -142,9 +160,7 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): @property def current_cover_position(self) -> int | None: """Return current position of cover.""" - if not self.supported_features & CoverEntityFeature.SET_POSITION: - return None - return self._device.status.level + return self._current_cover_position @property def device_class(self) -> CoverDeviceClass | None: diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 29eb681dc4d..89e5071051c 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -30,5 +30,5 @@ "documentation": "https://www.home-assistant.io/integrations/smartthings", "iot_class": "cloud_push", "loggers": ["httpsig", "pysmartapp", "pysmartthings"], - "requirements": ["pysmartapp==0.3.3", "pysmartthings==0.7.6"] + "requirements": ["pysmartapp==0.3.5", "pysmartthings==0.7.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index db28b2f0e7a..875db1a01c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2000,10 +2000,10 @@ pysma==0.7.3 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartapp==0.3.3 +pysmartapp==0.3.5 # homeassistant.components.smartthings -pysmartthings==0.7.6 +pysmartthings==0.7.8 # homeassistant.components.edl21 pysml==0.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 709d305e151..3bc951d6768 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1486,10 +1486,10 @@ pysma==0.7.3 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartapp==0.3.3 +pysmartapp==0.3.5 # homeassistant.components.smartthings -pysmartthings==0.7.6 +pysmartthings==0.7.8 # homeassistant.components.edl21 pysml==0.0.12 diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 715f26beaa7..4e637450fec 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -103,8 +103,10 @@ async def test_close(hass: HomeAssistant, device_factory) -> None: assert state.state == STATE_CLOSING -async def test_set_cover_position(hass: HomeAssistant, device_factory) -> None: - """Test the cover sets to the specific position.""" +async def test_set_cover_position_switch_level( + hass: HomeAssistant, device_factory +) -> None: + """Test the cover sets to the specific position for legacy devices that use Capability.switch_level.""" # Arrange device = device_factory( "Shade", @@ -130,6 +132,37 @@ async def test_set_cover_position(hass: HomeAssistant, device_factory) -> None: assert device._api.post_device_command.call_count == 1 # type: ignore +async def test_set_cover_position(hass: HomeAssistant, device_factory) -> None: + """Test the cover sets to the specific position.""" + # Arrange + device = device_factory( + "Shade", + [Capability.window_shade, Capability.battery, Capability.window_shade_level], + { + Attribute.window_shade: "opening", + Attribute.battery: 95, + Attribute.shade_level: 10, + }, + ) + await setup_platform(hass, COVER_DOMAIN, devices=[device]) + # Act + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_POSITION: 50, "entity_id": "all"}, + blocking=True, + ) + + state = hass.states.get("cover.shade") + # Result of call does not update state + assert state.state == STATE_OPENING + assert state.attributes[ATTR_BATTERY_LEVEL] == 95 + assert state.attributes[ATTR_CURRENT_POSITION] == 10 + # Ensure API called + + assert device._api.post_device_command.call_count == 1 # type: ignore + + async def test_set_cover_position_unsupported( hass: HomeAssistant, device_factory ) -> None: From fff04a5e9b69b26e2a8faf210814b51768e5f9a4 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 17 Jul 2023 09:44:47 +0200 Subject: [PATCH 163/184] Improve imap error handling for config entry (#96724) * Improve error handling config entry * Removed CancelledError * Add cleanup * Do not call protected async_set_state() --- homeassistant/components/imap/coordinator.py | 44 ++++++++++++++------ tests/components/imap/test_init.py | 42 +++++++++++++++++++ 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index bf7f173e647..c3cd21e6b2d 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -13,7 +13,7 @@ from typing import Any from aioimaplib import AUTH, IMAP4_SSL, NONAUTH, SELECTED, AioImapException import async_timeout -from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, @@ -54,6 +54,7 @@ _LOGGER = logging.getLogger(__name__) BACKOFF_TIME = 10 EVENT_IMAP = "imap_content" +MAX_ERRORS = 3 MAX_EVENT_DATA_BYTES = 32168 @@ -174,6 +175,7 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): ) -> None: """Initiate imap client.""" self.imap_client = imap_client + self.auth_errors: int = 0 self._last_message_id: str | None = None self.custom_event_template = None _custom_event_template = entry.data.get(CONF_CUSTOM_EVENT_DATA_TEMPLATE) @@ -315,7 +317,9 @@ class ImapPollingDataUpdateCoordinator(ImapDataUpdateCoordinator): async def _async_update_data(self) -> int | None: """Update the number of unread emails.""" try: - return await self._async_fetch_number_of_messages() + messages = await self._async_fetch_number_of_messages() + self.auth_errors = 0 + return messages except ( AioImapException, UpdateFailed, @@ -330,8 +334,15 @@ class ImapPollingDataUpdateCoordinator(ImapDataUpdateCoordinator): self.async_set_update_error(ex) raise ConfigEntryError("Selected mailbox folder is invalid.") from ex except InvalidAuth as ex: - _LOGGER.warning("Username or password incorrect, starting reauthentication") await self._cleanup() + self.auth_errors += 1 + if self.auth_errors <= MAX_ERRORS: + _LOGGER.warning("Authentication failed, retrying") + else: + _LOGGER.warning( + "Username or password incorrect, starting reauthentication" + ) + self.config_entry.async_start_reauth(self.hass) self.async_set_update_error(ex) raise ConfigEntryAuthFailed() from ex @@ -359,27 +370,28 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): async def _async_wait_push_loop(self) -> None: """Wait for data push from server.""" + cleanup = False while True: try: number_of_messages = await self._async_fetch_number_of_messages() except InvalidAuth as ex: + self.auth_errors += 1 await self._cleanup() - _LOGGER.warning( - "Username or password incorrect, starting reauthentication" - ) - self.config_entry.async_start_reauth(self.hass) + if self.auth_errors <= MAX_ERRORS: + _LOGGER.warning("Authentication failed, retrying") + else: + _LOGGER.warning( + "Username or password incorrect, starting reauthentication" + ) + self.config_entry.async_start_reauth(self.hass) self.async_set_update_error(ex) await asyncio.sleep(BACKOFF_TIME) except InvalidFolder as ex: _LOGGER.warning("Selected mailbox folder is invalid") await self._cleanup() - self.config_entry.async_set_state( - self.hass, - ConfigEntryState.SETUP_ERROR, - "Selected mailbox folder is invalid.", - ) self.async_set_update_error(ex) await asyncio.sleep(BACKOFF_TIME) + continue except ( UpdateFailed, AioImapException, @@ -390,6 +402,7 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): await asyncio.sleep(BACKOFF_TIME) continue else: + self.auth_errors = 0 self.async_set_updated_data(number_of_messages) try: idle: asyncio.Future = await self.imap_client.idle_start() @@ -398,6 +411,10 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): async with async_timeout.timeout(10): await idle + # From python 3.11 asyncio.TimeoutError is an alias of TimeoutError + except asyncio.CancelledError as ex: + cleanup = True + raise asyncio.CancelledError from ex except (AioImapException, asyncio.TimeoutError): _LOGGER.debug( "Lost %s (will attempt to reconnect after %s s)", @@ -406,6 +423,9 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): ) await self._cleanup() await asyncio.sleep(BACKOFF_TIME) + finally: + if cleanup: + await self._cleanup() async def shutdown(self, *_: Any) -> None: """Close resources.""" diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index ff949423614..055f8fd82bc 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -228,6 +228,48 @@ async def test_initial_invalid_folder_error( assert (state is not None) == success +@patch("homeassistant.components.imap.coordinator.MAX_ERRORS", 1) +@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) +async def test_late_authentication_retry( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mock_imap_protocol: MagicMock, +) -> None: + """Test retrying authentication after a search was failed.""" + + # Mock an error in waiting for a pushed update + mock_imap_protocol.wait_server_push.side_effect = AioImapException( + "Something went wrong" + ) + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) + await hass.async_block_till_done() + + # Mock that the search fails, this will trigger + # that the connection will be restarted + # Then fail selecting the folder + mock_imap_protocol.search.return_value = Response(*BAD_RESPONSE) + mock_imap_protocol.login.side_effect = Response(*BAD_RESPONSE) + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) + await hass.async_block_till_done() + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) + await hass.async_block_till_done() + assert "Authentication failed, retrying" in caplog.text + + # we still should have an entity with an unavailable state + state = hass.states.get("sensor.imap_email_email_com") + assert state is not None + assert state.state == STATE_UNAVAILABLE + + +@patch("homeassistant.components.imap.coordinator.MAX_ERRORS", 0) @pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) async def test_late_authentication_error( hass: HomeAssistant, From 38d58b4d91eeb6ebd27d046b7061e53c07994904 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 12:58:51 +0200 Subject: [PATCH 164/184] Use explicit device name for Stookalert (#96755) --- homeassistant/components/stookalert/binary_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py index d3920d3f0e4..1d074bba9c2 100644 --- a/homeassistant/components/stookalert/binary_sensor.py +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -36,6 +36,7 @@ class StookalertBinarySensor(BinarySensorEntity): _attr_attribution = "Data provided by rivm.nl" _attr_device_class = BinarySensorDeviceClass.SAFETY _attr_has_entity_name = True + _attr_name = None def __init__(self, client: stookalert.stookalert, entry: ConfigEntry) -> None: """Initialize a Stookalert device.""" From 77480000263209714ad7a597c54066f7dac3e187 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 17 Jul 2023 21:22:50 +0200 Subject: [PATCH 165/184] Prevent otbr creating multiple config entries (#96783) --- homeassistant/components/otbr/config_flow.py | 5 ++ tests/components/otbr/test_config_flow.py | 80 ++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py index 67c8412102d..a3fe046409b 100644 --- a/homeassistant/components/otbr/config_flow.py +++ b/homeassistant/components/otbr/config_flow.py @@ -130,6 +130,11 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): url = f"http://{config['host']}:{config['port']}" config_entry_data = {"url": url} + if self._async_in_progress(include_uninitialized=True): + # We currently don't handle multiple config entries, abort if hassio + # discovers multiple addons with otbr support + return self.async_abort(reason="single_instance_allowed") + if current_entries := self._async_current_entries(): for current_entry in current_entries: if current_entry.source != SOURCE_HASSIO: diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index b6cb0df78cd..da25edde045 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -23,6 +23,12 @@ HASSIO_DATA = hassio.HassioServiceInfo( slug="otbr", uuid="12345", ) +HASSIO_DATA_2 = hassio.HassioServiceInfo( + config={"host": "core-silabs-multiprotocol_2", "port": 8082}, + name="Silicon Labs Multiprotocol", + slug="other_addon", + uuid="23456", +) @pytest.fixture(name="addon_info") @@ -313,6 +319,80 @@ async def test_hassio_discovery_flow_sky_connect( assert config_entry.unique_id == HASSIO_DATA.uuid +async def test_hassio_discovery_flow_2x_addons( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, addon_info +) -> None: + """Test the hassio discovery flow when the user has 2 addons with otbr support.""" + url1 = "http://core-silabs-multiprotocol:8081" + url2 = "http://core-silabs-multiprotocol_2:8081" + aioclient_mock.get(f"{url1}/node/dataset/active", text="aa") + aioclient_mock.get(f"{url2}/node/dataset/active", text="bb") + + async def _addon_info(hass, slug): + await asyncio.sleep(0) + if slug == "otbr": + return { + "available": True, + "hostname": None, + "options": { + "device": ( + "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" + "9e2adbd75b8beb119fe564a0f320645d-if00-port0" + ) + }, + "state": None, + "update_available": False, + "version": None, + } + return { + "available": True, + "hostname": None, + "options": { + "device": ( + "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" + "9e2adbd75b8beb119fe564a0f320645d-if00-port1" + ) + }, + "state": None, + "update_available": False, + "version": None, + } + + addon_info.side_effect = _addon_info + + with patch( + "homeassistant.components.otbr.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + results = await asyncio.gather( + hass.config_entries.flow.async_init( + otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA + ), + hass.config_entries.flow.async_init( + otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA_2 + ), + ) + + expected_data = { + "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", + } + + assert results[0]["type"] == FlowResultType.CREATE_ENTRY + assert results[0]["title"] == "Home Assistant SkyConnect" + assert results[0]["data"] == expected_data + assert results[0]["options"] == {} + assert results[1]["type"] == FlowResultType.ABORT + assert results[1]["reason"] == "single_instance_allowed" + assert len(hass.config_entries.async_entries(otbr.DOMAIN)) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] + assert config_entry.data == expected_data + assert config_entry.options == {} + assert config_entry.title == "Home Assistant SkyConnect" + assert config_entry.unique_id == HASSIO_DATA.uuid + + async def test_hassio_discovery_flow_router_not_setup( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, addon_info ) -> None: From 4501bdb5bffba7b99fc09c43986406e3c26b887d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 17 Jul 2023 22:21:52 +0200 Subject: [PATCH 166/184] Fix check for HA Yellow radio in otbr config flow (#96789) --- homeassistant/components/otbr/config_flow.py | 4 ++-- tests/components/otbr/test_config_flow.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py index a3fe046409b..9fa38cedbe8 100644 --- a/homeassistant/components/otbr/config_flow.py +++ b/homeassistant/components/otbr/config_flow.py @@ -50,8 +50,8 @@ async def _title(hass: HomeAssistant, discovery_info: HassioServiceInfo) -> str: addon_info = await async_get_addon_info(hass, discovery_info.slug) device = addon_info.get("options", {}).get("device") - if _is_yellow(hass) and device == "/dev/TTYAMA1": - return "Home Assistant Yellow" + if _is_yellow(hass) and device == "/dev/ttyAMA1": + return f"Home Assistant Yellow ({discovery_info.name})" if device and "SkyConnect" in device: return "Home Assistant SkyConnect" diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index da25edde045..f828151a3b4 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -240,7 +240,7 @@ async def test_hassio_discovery_flow_yellow( addon_info.return_value = { "available": True, "hostname": None, - "options": {"device": "/dev/TTYAMA1"}, + "options": {"device": "/dev/ttyAMA1"}, "state": None, "update_available": False, "version": None, From 72073b28cc5d01ddbcd2957190e683098e8c15c7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 18 Jul 2023 08:50:15 +0200 Subject: [PATCH 167/184] Update PyYAML to 6.0.1 (#96800) --- 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 71ca8fc4c3e..942f98df332 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -42,7 +42,7 @@ pyserial==3.5 python-slugify==4.0.1 PyTurboJPEG==1.6.7 pyudev==0.23.2 -PyYAML==6.0 +PyYAML==6.0.1 requests==2.31.0 scapy==2.5.0 SQLAlchemy==2.0.15 diff --git a/pyproject.toml b/pyproject.toml index a902941213d..1e078b8c2b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ dependencies = [ "orjson==3.9.1", "pip>=21.3.1,<23.2", "python-slugify==4.0.1", - "PyYAML==6.0", + "PyYAML==6.0.1", "requests==2.31.0", "typing_extensions>=4.6.3,<5.0", "ulid-transform==0.7.2", diff --git a/requirements.txt b/requirements.txt index f4f2608b597..965ccbc9e9c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,7 @@ pyOpenSSL==23.2.0 orjson==3.9.1 pip>=21.3.1,<23.2 python-slugify==4.0.1 -PyYAML==6.0 +PyYAML==6.0.1 requests==2.31.0 typing_extensions>=4.6.3,<5.0 ulid-transform==0.7.2 From 3890d8d1320f1692ea2e7a8b43330f568e891a8c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 18 Jul 2023 13:07:16 +0200 Subject: [PATCH 168/184] Update pycocotools to 2.0.6 (#96831) --- homeassistant/components/tensorflow/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 672bd899962..ecdefd36b2a 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -8,7 +8,7 @@ "requirements": [ "tensorflow==2.5.0", "tf-models-official==2.5.0", - "pycocotools==2.0.1", + "pycocotools==2.0.6", "numpy==1.23.2", "Pillow==9.5.0" ] diff --git a/requirements_all.txt b/requirements_all.txt index 875db1a01c1..1ce15900b93 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1600,7 +1600,7 @@ pycketcasts==1.0.1 pycmus==0.1.1 # homeassistant.components.tensorflow -pycocotools==2.0.1 +pycocotools==2.0.6 # homeassistant.components.comfoconnect pycomfoconnect==0.5.1 From 7f5a141f6978df3518440778ffc915c79fb8dbc9 Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 19 Jul 2023 09:25:10 -0400 Subject: [PATCH 169/184] Add username to Reauth flow in Honeywell (#96850) * pre-populate username/password on reauth * Update homeassistant/components/honeywell/config_flow.py Co-authored-by: Joost Lekkerkerker * Use add_suggested_value_to_schema * Optimize code --------- Co-authored-by: Joost Lekkerkerker --- .../components/honeywell/config_flow.py | 25 ++++++++++--------- .../components/honeywell/test_config_flow.py | 8 +++--- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py index 8b24fc912f1..dab8353c773 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -22,7 +22,12 @@ from .const import ( DOMAIN, ) -REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) +REAUTH_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -42,18 +47,12 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Confirm re-authentication with Honeywell.""" errors: dict[str, str] = {} - + assert self.entry is not None if user_input: - assert self.entry is not None - password = user_input[CONF_PASSWORD] - data = { - CONF_USERNAME: self.entry.data[CONF_USERNAME], - CONF_PASSWORD: password, - } - try: await self.is_valid( - username=data[CONF_USERNAME], password=data[CONF_PASSWORD] + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], ) except aiosomecomfort.AuthError: @@ -71,7 +70,7 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.entry, data={ **self.entry.data, - CONF_PASSWORD: password, + **user_input, }, ) await self.hass.config_entries.async_reload(self.entry.entry_id) @@ -79,7 +78,9 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="reauth_confirm", - data_schema=REAUTH_SCHEMA, + data_schema=self.add_suggested_values_to_schema( + REAUTH_SCHEMA, self.entry.data + ), errors=errors, ) diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index a416f030a05..25ffa0a6093 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -156,14 +156,14 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_PASSWORD: "new-password"}, + {CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password"}, ) await hass.async_block_till_done() assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_entry.data == { - CONF_USERNAME: "test-username", + CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password", } @@ -200,7 +200,7 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, client: MagicMock) -> ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_PASSWORD: "new-password"}, + {CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password"}, ) await hass.async_block_till_done() @@ -246,7 +246,7 @@ async def test_reauth_flow_connnection_error( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_PASSWORD: "new-password"}, + {CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password"}, ) await hass.async_block_till_done() From c118574bf48fdf0e4b34dac59f3da8cf4f7b5e96 Mon Sep 17 00:00:00 2001 From: Luke Date: Tue, 18 Jul 2023 18:25:24 -0600 Subject: [PATCH 170/184] bump python-Roborock to 0.30.1 (#96877) bump to 0.30.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 0cf6db4ae81..5f6aa63ce2f 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.30.0"] + "requirements": ["python-roborock==0.30.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1ce15900b93..88eeefdbfd5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2139,7 +2139,7 @@ python-qbittorrent==0.4.3 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.30.0 +python-roborock==0.30.1 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3bc951d6768..75fcf90c838 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1565,7 +1565,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.3 # homeassistant.components.roborock -python-roborock==0.30.0 +python-roborock==0.30.1 # homeassistant.components.smarttub python-smarttub==0.0.33 From 58c3c8a7a8052b621d88dbb9743ea30ca311ab34 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 19 Jul 2023 15:06:04 +0800 Subject: [PATCH 171/184] Avoid infinite loop on corrupt stream recording (#96881) * Avoid infinite loop on corrupt stream recording * Update tests --- homeassistant/components/stream/fmp4utils.py | 2 +- tests/components/stream/test_worker.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index 5ec27a1768c..7276e7a0d9b 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -151,7 +151,7 @@ def find_moov(mp4_io: BufferedIOBase) -> int: while 1: mp4_io.seek(index) box_header = mp4_io.read(8) - if len(box_header) != 8: + if len(box_header) != 8 or box_header[0:4] == b"\x00\x00\x00\x00": raise HomeAssistantError("moov atom not found") if box_header[4:8] == b"moov": return index diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 0dc67c37403..e0152190d90 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -245,7 +245,7 @@ class FakePyAvBuffer: # Forward to appropriate FakeStream packet.stream.mux(packet) # Make new init/part data available to the worker - self.memory_file.write(b"\x00\x00\x00\x00moov") + self.memory_file.write(b"\x00\x00\x00\x08moov") def close(self): """Close the buffer.""" From 0c10005270b2be395b14818343f6dec7b555de1b Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 19 Jul 2023 14:30:39 -0400 Subject: [PATCH 172/184] Bump AIOSomecomfort to 0.0.15 in Honeywell (#96904) --- homeassistant/components/honeywell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index 16b07e91446..aa07a5248cf 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/honeywell", "iot_class": "cloud_polling", "loggers": ["somecomfort"], - "requirements": ["AIOSomecomfort==0.0.14"] + "requirements": ["AIOSomecomfort==0.0.15"] } diff --git a/requirements_all.txt b/requirements_all.txt index 88eeefdbfd5..c02079f9e21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -8,7 +8,7 @@ AEMET-OpenData==0.2.2 AIOAladdinConnect==0.1.56 # homeassistant.components.honeywell -AIOSomecomfort==0.0.14 +AIOSomecomfort==0.0.15 # homeassistant.components.adax Adax-local==0.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75fcf90c838..71bb2956716 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -10,7 +10,7 @@ AEMET-OpenData==0.2.2 AIOAladdinConnect==0.1.56 # homeassistant.components.honeywell -AIOSomecomfort==0.0.14 +AIOSomecomfort==0.0.15 # homeassistant.components.adax Adax-local==0.1.5 From db32aa7c5ee073f69061aab23f6d1a74fa403c51 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 20 Jul 2023 11:27:30 +0200 Subject: [PATCH 173/184] Fix timer switch in Sensibo (#96911) --- homeassistant/components/sensibo/switch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensibo/switch.py b/homeassistant/components/sensibo/switch.py index ee9c946268f..cce72dfaae6 100644 --- a/homeassistant/components/sensibo/switch.py +++ b/homeassistant/components/sensibo/switch.py @@ -151,9 +151,10 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity): @async_handle_api_call async def async_turn_on_timer(self, key: str, value: bool) -> bool: """Make service call to api for setting timer.""" + new_state = not self.device_data.device_on data = { "minutesFromNow": 60, - "acState": {**self.device_data.ac_states, "on": value}, + "acState": {**self.device_data.ac_states, "on": new_state}, } result = await self._client.async_set_timer(self._device_id, data) return bool(result.get("status") == "success") From 7105b4b28bd044771a92b79f9fb32cfdd90243cc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 20 Jul 2023 11:45:02 +0200 Subject: [PATCH 174/184] Bumped version to 2023.7.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 30a7fc37c9e..e9b7abd5200 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 7 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 1e078b8c2b3..525ebda7715 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.7.2" +version = "2023.7.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 86d9d9cea5465de11f835992e2856a9db7a95e07 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 20 Jul 2023 13:16:02 +0200 Subject: [PATCH 175/184] Disable wheels building for pycocotools (#96937) --- requirements_all.txt | 2 +- script/gen_requirements_all.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements_all.txt b/requirements_all.txt index c02079f9e21..cee32260345 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1600,7 +1600,7 @@ pycketcasts==1.0.1 pycmus==0.1.1 # homeassistant.components.tensorflow -pycocotools==2.0.6 +# pycocotools==2.0.6 # homeassistant.components.comfoconnect pycomfoconnect==0.5.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 0bbbd97c926..c211a0fca81 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -33,6 +33,7 @@ COMMENT_REQUIREMENTS = ( "face-recognition", "opencv-python-headless", "pybluez", + "pycocotools", "pycups", "python-eq3bt", "python-gammu", From 71ca976d589ff485bbb47c697d983dfee25607e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Jul 2023 14:05:08 -0500 Subject: [PATCH 176/184] Fix esphome entity ids in test (#96965) --- tests/components/esphome/test_number.py | 2 +- tests/components/esphome/test_sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/esphome/test_number.py b/tests/components/esphome/test_number.py index f928146996c..3af94cba39d 100644 --- a/tests/components/esphome/test_number.py +++ b/tests/components/esphome/test_number.py @@ -118,7 +118,7 @@ async def test_generic_number_with_unit_of_measurement_as_empty_string( user_service=user_service, states=states, ) - state = hass.states.get("number.test_mynumber") + state = hass.states.get("number.test_my_number") assert state is not None assert state.state == "42" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes diff --git a/tests/components/esphome/test_sensor.py b/tests/components/esphome/test_sensor.py index 43431ca36a0..27644617a7a 100644 --- a/tests/components/esphome/test_sensor.py +++ b/tests/components/esphome/test_sensor.py @@ -298,7 +298,7 @@ async def test_generic_numeric_sensor_empty_string_uom( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_mysensor") + state = hass.states.get("sensor.test_my_sensor") assert state is not None assert state.state == "123" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes From 35ab024298560346d67d8c97720ec4c1e1ca3a9e Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Thu, 20 Jul 2023 14:43:38 +0200 Subject: [PATCH 177/184] Add custom bypass night arming to SIA alarm codes (#95736) * Add SIA codes for night arming with custom bypass * Set night custom bypass to ARMED_CUSTOM_BYPASS --- homeassistant/components/sia/alarm_control_panel.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py index ef2ecc7aa23..c59150266d9 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -61,6 +61,8 @@ ENTITY_DESCRIPTION_ALARM = SIAAlarmControlPanelEntityDescription( "OS": STATE_ALARM_DISARMED, "NC": STATE_ALARM_ARMED_NIGHT, "NL": STATE_ALARM_ARMED_NIGHT, + "NE": STATE_ALARM_ARMED_CUSTOM_BYPASS, + "NF": STATE_ALARM_ARMED_CUSTOM_BYPASS, "BR": PREVIOUS_STATE, "NP": PREVIOUS_STATE, "NO": PREVIOUS_STATE, From 55e5947330332d632b95b4e346ba7774eed87c3f Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler <2292715+bdr99@users.noreply.github.com> Date: Thu, 20 Jul 2023 14:16:08 -0400 Subject: [PATCH 178/184] Upgrade pymazda to 0.3.10 (#96954) --- homeassistant/components/mazda/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index 01f77cb2d38..dd29d02d655 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["pymazda"], "quality_scale": "platinum", - "requirements": ["pymazda==0.3.9"] + "requirements": ["pymazda==0.3.10"] } diff --git a/requirements_all.txt b/requirements_all.txt index cee32260345..ea5b418de71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1810,7 +1810,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.9 +pymazda==0.3.10 # homeassistant.components.mediaroom pymediaroom==0.6.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 71bb2956716..a8871783d3b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1338,7 +1338,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.9 +pymazda==0.3.10 # homeassistant.components.melcloud pymelcloud==2.5.8 From 7389fea22348d2d784e12f3a444192d2ff1bd9c3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 20 Jul 2023 21:09:59 +0200 Subject: [PATCH 179/184] Fix OTBR tests on RC (#96966) --- tests/components/otbr/test_config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index f828151a3b4..2659f8d151d 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -261,7 +261,7 @@ async def test_hassio_discovery_flow_yellow( } assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Home Assistant Yellow" + assert result["title"] == "Home Assistant Yellow (Silicon Labs Multiprotocol)" assert result["data"] == expected_data assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -269,7 +269,7 @@ async def test_hassio_discovery_flow_yellow( config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] assert config_entry.data == expected_data assert config_entry.options == {} - assert config_entry.title == "Home Assistant Yellow" + assert config_entry.title == "Home Assistant Yellow (Silicon Labs Multiprotocol)" assert config_entry.unique_id == HASSIO_DATA.uuid From 26584e366074a53fc1a4abe85f6f33844b457310 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 20 Jul 2023 16:11:14 +0200 Subject: [PATCH 180/184] Update aiohttp to 3.8.5 (#96945) --- 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 942f98df332..b08aa7246db 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ aiodiscover==1.4.16 aiohttp-cors==0.7.0 -aiohttp==3.8.4 +aiohttp==3.8.5 astral==2.2 async-timeout==4.0.2 async-upnp-client==0.33.2 diff --git a/pyproject.toml b/pyproject.toml index 525ebda7715..42b857c4401 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ ] requires-python = ">=3.10.0" dependencies = [ - "aiohttp==3.8.4", + "aiohttp==3.8.5", "astral==2.2", "async-timeout==4.0.2", "attrs==22.2.0", diff --git a/requirements.txt b/requirements.txt index 965ccbc9e9c..5063ab16a59 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -c homeassistant/package_constraints.txt # Home Assistant Core -aiohttp==3.8.4 +aiohttp==3.8.5 astral==2.2 async-timeout==4.0.2 attrs==22.2.0 From fd3bdeaef141b113a8dbbd5121390cdb2478b7e6 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 21 Jul 2023 09:00:04 +0200 Subject: [PATCH 181/184] Remove stateclass from Systemmonitor process sensor (#96973) Remove stateclass --- homeassistant/components/systemmonitor/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 8dc04e7da86..7f0866ce62e 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -205,7 +205,6 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { key="process", name="Process", icon=CPU_ICON, - state_class=SensorStateClass.MEASUREMENT, mandatory_arg=True, ), "processor_use": SysMonitorSensorEntityDescription( From 4644355eff8095abd2839ccedd047e3e3c1dd57c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Jul 2023 20:00:07 -0500 Subject: [PATCH 182/184] Fix task leak on config entry unload/retry (#96981) Since the task was added to self._tasks without a `task.add_done_callback(self._tasks.remove)` each unload/retry would leak a new set of tasks --- homeassistant/config_entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a52b869b830..7b8401ec8b5 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -694,7 +694,7 @@ class ConfigEntry: if self._on_unload is not None: while self._on_unload: if job := self._on_unload.pop()(): - self._tasks.add(hass.async_create_task(job)) + self.async_create_task(hass, job) if not self._tasks and not self._background_tasks: return From fe140bdca30356411c23861ab422e8b9459f0c54 Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Fri, 21 Jul 2023 02:54:57 -0400 Subject: [PATCH 183/184] Bump env_canada to v0.5.36 (#96987) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 4a8a9dec587..0575ac132d4 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.5.35"] + "requirements": ["env-canada==0.5.36"] } diff --git a/requirements_all.txt b/requirements_all.txt index ea5b418de71..a4b0e8110f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -730,7 +730,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.5.35 +env-canada==0.5.36 # homeassistant.components.enphase_envoy envoy-reader==0.20.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a8871783d3b..4d0d15b5e22 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -583,7 +583,7 @@ energyzero==0.4.1 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.5.35 +env-canada==0.5.36 # homeassistant.components.enphase_envoy envoy-reader==0.20.1 From be7818fafb3ef9d5b0df1d87f256c58d64be698f Mon Sep 17 00:00:00 2001 From: rappenze Date: Fri, 21 Jul 2023 15:22:45 +0200 Subject: [PATCH 184/184] Update pyfibaro dependency (#97004) --- homeassistant/components/fibaro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index 4b3721eed15..d90a9d28662 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["pyfibaro"], - "requirements": ["pyfibaro==0.7.1"] + "requirements": ["pyfibaro==0.7.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index a4b0e8110f4..6d6428f614c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1666,7 +1666,7 @@ pyevilgenius==2.0.0 pyezviz==0.2.1.2 # homeassistant.components.fibaro -pyfibaro==0.7.1 +pyfibaro==0.7.2 # homeassistant.components.fido pyfido==2.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d0d15b5e22..4ad65cdccfd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1227,7 +1227,7 @@ pyevilgenius==2.0.0 pyezviz==0.2.1.2 # homeassistant.components.fibaro -pyfibaro==0.7.1 +pyfibaro==0.7.2 # homeassistant.components.fido pyfido==2.1.2