From aaf2846a53fd85f4696df456a46e5a2860f3c303 Mon Sep 17 00:00:00 2001 From: Barry Williams Date: Wed, 28 Jun 2023 21:06:24 +0100 Subject: [PATCH 0001/1009] 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 d201646e81cd81..c7ee5a7d00c728 100644 --- a/homeassistant/components/openhome/__init__.py +++ b/homeassistant/components/openhome/__init__.py @@ -17,7 +17,7 @@ _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 61d425895bfd9e..de6c56a01ddaac 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 c0941906e40dfa..77ab0ac0aafb5c 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -126,9 +126,9 @@ def device_info(self): 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 00000000000000..22bffad44d83ea --- /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 875c138aa12ad2..b3cb0d2a6a3b03 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 7af3d3178306fa..4d309c5f18bd4f 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 00000000000000..d975cc29af43d7 --- /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 ec7beee4c1e06cd43ceb056d901815c27434d0ac Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 28 Jun 2023 22:07:54 +0200 Subject: [PATCH 0002/1009] Bump version to 2023.8.0dev0 (#95476) --- .github/workflows/ci.yaml | 2 +- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bcc19bfb55d216..331a1bc151a5da 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -32,7 +32,7 @@ env: CACHE_VERSION: 5 PIP_CACHE_VERSION: 4 MYPY_CACHE_VERSION: 4 - HA_SHORT_VERSION: 2023.7 + HA_SHORT_VERSION: 2023.8 DEFAULT_PYTHON: "3.10" ALL_PYTHON_VERSIONS: "['3.10', '3.11']" # 10.3 is the oldest supported version diff --git a/homeassistant/const.py b/homeassistant/const.py index 4bc5e189cf26e1..f3d3d48fdd25ab 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 -MINOR_VERSION: Final = 7 +MINOR_VERSION: Final = 8 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" diff --git a/pyproject.toml b/pyproject.toml index 9c67835c5448fe..e857abd31e55d5 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.8.0.dev0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 0b81550092ba40c54b26d2980ec095d1ad272501 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 28 Jun 2023 23:40:12 +0200 Subject: [PATCH 0003/1009] 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 bd65b3a092577e..7c94c07c8cd87a 100644 --- a/homeassistant/components/matter/binary_sensor.py +++ b/homeassistant/components/matter/binary_sensor.py @@ -65,7 +65,6 @@ def _update_from_device(self) -> None: 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 @@ def _update_from_device(self) -> None: 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 @@ def _update_from_device(self) -> None: 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 @@ def _update_from_device(self) -> None: 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 61c5d4cd2ff48e..590f325cf22960 100644 --- a/homeassistant/components/matter/cover.py +++ b/homeassistant/components/matter/cover.py @@ -199,7 +199,7 @@ def _update_from_device(self) -> None: 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 @@ def _update_from_device(self) -> None: ), 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 @@ def _update_from_device(self) -> None: ), 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 @@ def _update_from_device(self) -> None: 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 4c220ab85ea1a4..facdb6752d3b0a 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -358,7 +358,7 @@ def _update_from_device(self) -> None: 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 @@ def _update_from_device(self) -> None: # 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 @@ def _update_from_device(self) -> None: # 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 @@ def _update_from_device(self) -> None: MatterDiscoverySchema( platform=Platform.LIGHT, entity_description=LightEntityDescription( - key="MatterColorTemperatureLightFallback" + key="MatterColorTemperatureLightFallback", name=None ), entity_class=MatterLight, required_attributes=( @@ -430,7 +434,9 @@ def _update_from_device(self) -> None: # 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 f78529b72684e1..7df6d84c79471c 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 84e68695d639e0..027dcda65a7985 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -68,7 +68,6 @@ def _update_from_device(self) -> None: 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 @@ def _update_from_device(self) -> None: 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 @@ def _update_from_device(self) -> None: 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 @@ def _update_from_device(self) -> None: 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 @@ def _update_from_device(self) -> None: 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 @@ def _update_from_device(self) -> None: 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 594998c236f2df..dc5eb30df51a33 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 2eb3c22c1f7a72..56c51d144d8a91 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -63,7 +63,7 @@ def _update_from_device(self) -> None: 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 743619ddde9aae..d7982e1d5aedd4 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 487dd3f95685175a0ffd8d170e7a384421622881 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 28 Jun 2023 17:34:43 -0500 Subject: [PATCH 0004/1009] 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 f704a8baa33967..5b82b5dae72e0f 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -8,6 +8,7 @@ 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 23afe9ce3f79ec..8ef0cef52f9117 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 ec2128e3bd709b..6ad9beb3362160 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 392e2af2b7354222508e7b1a8a3c84f112e6a507 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 0005/1009] 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 ae5718e108b2dc..fa1c382926e778 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 9fbfa03b92812c..29214083d27693 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -250,11 +250,12 @@ async def async_restore_backup_step_1(self) -> bool: 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 b3cb0d2a6a3b03..79f8926932cf69 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 4d309c5f18bd4f..5e1af46951c3f9 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 48c45dd241d0bb..17665994806887 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 1a6c32f8e9ce048f344d8866b730b7d9c1403211 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 03:36:46 +0200 Subject: [PATCH 0006/1009] Update featured integrations screenshot (#95473) --- docs/screenshot-integrations.png | Bin 168458 -> 178429 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/screenshot-integrations.png b/docs/screenshot-integrations.png index bc304f11b160f23376cf981a08a89e4ad85e696e..f169e4486a65472f5c0dcef7285a03970c7c2c3a 100644 GIT binary patch literal 178429 zcmd?P^;euhvo1Qw;O;KL-CcqN2oQq1ySuxG-~UI-Q9I?8Qh&S-`;1hbjOuAAW~t_jdm{1&0!{VxGKe@rH16-R6b}TY@FW9 zl|u88`jL1d9$Fc$ZtHA|-^*3hHUQiwqep_opN$(4XArGQK@7~8^7H2h$|(lh`6cst zN#@j(8$`VuJX3lhe-9SOhGsXxX*Qqz%bz{*3)$x}U~Vf>m>FAqQp>iR}Ou z1N;_i&BrMCr28Cm#_gP)g7}!B0etHn$=Vi##W||o4AWncVn~H}o5}lrDmTeK+MUdo zPFltm{OdZ$3Hpy}X#baLpgLarRoE8H_)0I%B>M1>{fP>Ppr-v~-ARTc$Wgfyp-l&4 zWP|zE=@^>jiT(qM|8K=CKojkaJDQ4we;5bj$qnO`1ngJ2Q-!^2-A?95{zqKCOg!4R zuh2PGgio&p z;5f$C)vh`Xai|qFP&$?fx0|>sN%;~k8)`FSib&QdaDyQSVx}8&h{r`B$dgkvb?jjP zK$a7Tk)a)vXcZE1HMDyEM-juoH}@#ST@Wfw$&XIz)nRYKM2nn1|SDO>0{|T2u2QxA=_5X3N^;G;r6`|69FlCxZ z+Rk(4?Ivowdb8V+E<_LVh3J~Bc z_y)9lP6R;*EKiUoo&HA^Q-Q~d&Q1gDCAF{m`ubl@Og~l+@7aDwe^wn9Qb@nvY&-Z8 z>~g8?n)fe$fdK$H=ab-n;VoeKk2iGGe0ylqC(PO}3%{eMc2cEpMuc^Ab(t0x78v9R z5~34nkzVD#aQ<6lP+|v;4p;&`=M9~Z#(QuchkA57u*yG4RR6*T@8=^%eWIHmol>Ncedf)jv=f`W@3oCH&*K}{gbL-E?+eM(u^KtQ*`;tDLrlEgBMdPZC z%MhvOFyEFRRRnoJ^S4Yv9>2~Ni}Vp?8B&^mozGe;f9wETcTlxxxV7l`jYHZFIMZ0~be-yOzf2f%fVwy7C(_I;Hi;}U=6g!{u;H0~+V%17a?$Ysmvf)}@hW2giPFdGEp^O|%O)<6 z>oA5$mqDSa=PU&rdahe9gI9n_}l*uev6!qwDM9*iZXJ6mf?X;?DQ^|?P~$PRce zt+!cVT)LFM_7~OYchl(m;r64^CAj9tJoo}91$tnI%jwPj-~4FkMdbW^Q$p*i^@*-t z`jK7X=nwVEg4SAT4{g=GKwWk4#QDprfX*zV0ffXtLY|G6DZv|61 z%xl$XCal=YkGGYQ3E3t=l0X-h)uu#moV~NUl3}8m7AFO7=Rd8EY#o;-&97eR z_8+PzMRGcUHE3;mnP0~2J_|=%2$a=)6xWXz(ZmnFX$AJS=zX&HbUQs>oh;gzlG5#T zMDQAujvl9Qg6F<+5sB3{-+M zYCA4UH{0otyoS^sV^a9~)X{96&31JYyghmMGcx2rr>3|2wXv%qayA$^lmrMeRmuD= z#h)(w(D#=N1OMd5iwZ)jf)qp1e$hSEfT#TwC3OGEp&i`Hq{F6V6XfIRT*(vDUCKp~ zzmhm|FcV^@HHyx(A-Tz~3{~Jo#6{q7D=J>@yc85_j@c z2#3ag*9}=3tE1nRoDJ=Fq%4hL-x(FsHs0k9>Xq9{#`^y1$fXW*a_?)o8uwc3PZi|QI8k*elS>8?G6G}(m&`jHG2Na_(3lRu5i zP`xX7ri7_1v;7<0E;dE- zUz)Xt$oy}WSL~ZMkW%xsvkIrcl}!dnr*$6Z?KhL4Spk>5;rhFGu;B;N)8=BhRQ zF4JH!%YC-4^@(6SUY0fsPW~f4#wl*P)tRnBM@2h7mXdT*SSH0(nl8v-&R{i@n`yUD zBz>>1!s0a=q^|wCdP5iSS%6L%)V~{-QmR@s%+iFOmeeYuYZ&dkTu3FRC;0}cJ4c&L zyOPKQ3FxrZ!0}46jS9rtR=Gg)WAo%h^Av3zNO_N0#+Y2L`Wlp;cf;dx`fv@2{bLtI zbzJTz^%QQ6_G{93%Jy$6@+`TZ<%<$|xN*J~68Dh@z7wvJJ?tu8LoOLmUfVs|CRMQ# zF`A{ZWY);~tWSbL1y3#%k|w_o$u^1uR3$KidA9^n)v$TXU zH?7xlZuxPskcQvWVXOY_?tQpT74>^L^Spk!7$2IihC4{LB-1vD957!oPeB9s$#Fbm z{0sFs?n7^7{m8m((s8gTX(;-`+zf)Qy;W&tP0^=)&HOu;ddu|tIC{iV%O~LVY6vi% zw`hxf`>^Kt`3Vw8%y(n?&}yv58NeAe*mZDPT$(Z_yCr?h@;p2|J&Z$J?R}o@eK+>M z4H^6tp~SXTBJU89Ra=+&z;lrN#oBgSEAKT_p?^!4jvIGBA%X7Iw`!R@P&X+k&)N(M z(Jl&hR<7okMoyOAdC#NpgQSFHcrq`F3O;NP*MYrlgXd+__z^*H1=VzxBM7kFIgXqLwJ}i*F{0P>Hk+ z4{s4G;Zjy5{ArP~h^EGfLPRM^3wyG;usmlM?qQOfUsC17(amXU;K@pF7d2s;W9L)v zyS1fhOe!w16)20(6NZENB#QspXD$P(5&?a-dESX?VdAG zD%MTAUT!h{pM;K?alNt+n(*};%9CF!bc1YCA6#sLs)(ZQN?V`XiUsCfga`hp%8g`F z=@#$gZk#A>P?NZ11b1pDPXQGqxXduL&h?m9q*GmTTy+gxc2$G9fjkAA*vi=&T7`c579jni3zBRoX zz*nbqR#FfS`#bqyG-d8x2aAy8hVJ_wo-)61dI!*fHjuTzhS$27mvj7b5twk86)3yu`vCK_zqF-x^mAOMcE_g>v3FRGsft3^ zz9AL*7iwt2{9n;8TmCl_P5U(Z*i;bPf z*n-+haFcn^p}-2YznB$Kl-GTI(XGDd(!howCE$cfqEzUsNeovnYv(P?N8z3c4l%Q+~G1=QG)s z!N6NW}=N4Ys{7eaTJy;tAm>HH*RRtp9j+xsTN#`|bu(r~GO=?^LYR`%Evu)2Vhy zC?1uX{%c-|nB_6<;V~;4QZO6bubVdgKIl>cSV~aiHTs_Wi*5SGHzzWlh3afly4p^F%k67=MlP=@rxezzDJ@Q#_vdRkqJY zxpW7=3`a3BXXBIZ*!CWUcz7-QqmR^0bz+=w0S{s40vjo1sMRb@}bk0Z1T!V22wiLTK7ovVX_^Y&K7I80(^l?YPtUOaM{}F z{RJX-^QzbV*oxz`=rEo^d_m(ahgs!Fp<(U$-nG65>_V5?=(AK7Hzf3|p(GTpv1Q?ZR3B_3 znrQZUm-$lJ&SIVXtqtjd6iKrIU=j8^I_q)}U0XC3S!yrJhorrRUF7BqUqa`K4cs{MwQ9Z*b~YbjY*(@1Fo_06?P#7JSCFXqk$tM|V>X(ckKJ8Zj)R^6%l#$*oXL zOSi`nqdwrO%m(ts^Dsp0{R3SG^dL+zY4>^ZXN+#!Ps-7xCvbWIOhf|>spvs%wQ?!D z@O`1OKRYELja1u-{vP7C8A!Q6BXn`QCAQ>y-eLFyJy61wALom$5L}UE8dT~?m@GTg zDHg*lHW)#(2}$5G$e;V(@+HGxwu!d$<6WlZV3l}58g=m+G3hqCY(Ht$y)9xB1vPX0L3!T_@hlD}N5EFMe%U`pBj$XE zhK;stf<%+Ov?O}cs2`TZfE5!)8&cj5M>N}jteizG~fVei;S?Jn2)sH z@4j$ z>7$Gw`P8)Gp<6O!1*If<(S9Q#(iefALh7+>j0eh&yO&P-cYcClc8-02`1{CA@btbaOoVrs*FWsam5jNMU!j6(OvlC^`_@!a;rH$+wXb=ID#D>vv8jYX-{~^G3^s?8B&-n z{NG-0ruqy<-+$oN{^fKia>n3`xl8J(%%t5KY5I-RzD={@*st~^r85#!sw%Pi@l*yb z+>%?EjdS||whHR(Bn5AMOYtnW&OCX5u+aGui>YIpazh?OswzgA7-bsh@U+kAGh4$` zpFc>#*22};L9AJEqrb00nBXI@bYPKCUTQeOnuY;|U5cux?sO|oyRjCsTjMv`ez!F- zM3vwEok6VcQrl_sdClT)MfRPNg-fUA*npYB`D~GbuC_tLxowF?7%F8e0mn+dGWv_= z`i#(VNH~ujrN|dd1BUx|?pT56$KCinTldoRGS_-g2(s+q0=Hxe+@wr47tu5t2NA+| zjZO_uZsFBAI8EZ zJw}vI`a=16Z#O=)JD6C9E*4o^H5|r7=2U>dGsDIPZ~kd7)vM;P1ZGv&E^5ZIzAAGj zUm9K<3BdjVjZBX!SiC?{OeJTEE>u3po2KhX{E4mevFzSl5cyLxiHWn0%5Dkove53O?{eXi$+Z*;ocR;uD9HF4juh`X&d2qxYFWo`B`(7r&kE^ka*U( zMtdeL>>3x46wTTxF<06*bdpE97}x=gvruibK3`T|!OT?A?Dx~&w2F?2?W15SpiqNi z)q-W39J_NI$#b=ouAGLZZGj1G)5|lv!*i+6m##*@*fALH7G9}+l<^~Re;(KA{pk?d zpN7>jrYcYmk?z}HobH$zks!tmu*ZfsyaH91r2%~O4Dez69fmcj%e_-FkG?YcorA8~ ztg2!0q_&_n1=Dw!KR{hm?m1}e*sH;3*^gja@OB!;%r`UI56xQ_yYJjri6s}wAn6tr zq(0QAXV={0Q&woeP$3^g1b7v4KlA%|L%%x=t!A}ne(i+hny+1*9idXZOFbE@#crPm z3M0Mwf>TASi!t-1Y7_KmAVI2iThwYdz4XzFMr(7r_2F!dukcsA?xvlAg0+N5v&nXu zCFqA`p0lO%81W%)h;%0?R$$0P`rs?ld2oup$BLcph*gwW&rQ9&ACX4q%xEz}#dORv z7qj=u0RT(}%>tZ8(BcC0EdLbfOOz(o>fQlO97%rm%C5bc6pFAVc#I=o92Ypuzz(2q zHckXlr(BxfAm-5Iq5w6OE5cJZCoDVm`|xKPk}I<{QlJ_K>8V}sn3f(|9z!pPeY5O^ z#3W6Xv?yoxqCp`6r7ElS6)8PK^~u15;7I+~Q>s$mZR^sPUC&Y-8|`0bo{7oN7uZ|v z%!h&Au3r;04OQuy90EEk8bMK%1$6bqOw#yRr`4M)R4tyE=CGrYUOlUm@6|!0(3d)* zwzTA{csHr^UVTY`%fUP9;f?_<_}f3cN?{aSz2Ir!{%~G2YuYbbxJ3KRwbuAHcNuFU zb&E@q>(>;+0Ky~1MP1+Iz_|{~LrT}^TBL9S+5B%kHc@D*8OvL|gGXEUsSDQj*WPN% zmUz0H7@_FR@V}XiDgs;1*;pC8UT!Z0TTJS02Gw$;QVa@1R=1<)M+cEh76np+YZC^3 zRwd_Y_Q_sg&fQ8lHmoj`^Xh{)D(quP>fPQ)Ehb~u_KaewvN#1qMJSZ*r+s- ztFzA(SukbYpFuK@dHc8iogYCcPCYKbOoA6En;J?F#&q!3Pt5GGv(O7Qad1RVH^L$r z=@3zQW!bsCfC~T0rC!;)TdR zXCuWD_Y!Grriu3EFVMrMz8;SIU>Ytm!Me~UMuC%I$=*~ycgwXbTKh`5 zU&(|o>^Ad!oL|Wg!Q+>ah%pyd)?_J1Q&2F$#!2>pR2am*yS`ZN-JAj&ED+;`$n2yppY5iLycUe{k_qDuXDFwdEM@KZ1 zQ>RhSNo2)uq(3zajbJSUn|Nd+b{rz`Gmu2@}%Z4?R& z)Z(FCkZ5N3!aeCC<+U#DWpY)A-_W3IGuuvLqzQ_rEHk*j!(=8Nrr!_BK{D7}^y1eH z{s6keet5G!*WuIs=gz#yDXA;PQ%BZrJpZNwkp1v7k+*wDD_0_o3YR>qmNwIyrfMaf zOkgL`pXIMCb$g`qB2snor3tG4G*OxLIP@nkD%=mX|0(Q=cy6)q5HWZ4NE81U2;WDj zoVu`)A}Tu0%=URjTv0@il0*~Yw$e4y0E~4LJNyQ^_vvdj47Rzi=r8J6o+We=O22n$ z8YaCTW|IT(7U`MY^zWBN>}pBpA5t3)RathV@NXe+G`gCsvmnptNW#L^ZJFDI)vpST z5Qa)(df1eOisl`@ESM|@JH|WUGSd^52_xpI5H7$qp_f_t_tb^3>%<>TAnm6bc+&#m zl5{>(Eg8aNkRHA@v^iKt z`%GkiA&-l}s%(Em1X<5hhcMDgUoM{`hsf~P=UP34q!t%p5CMI!#x!l$*G#a{>)pDJ zzs!^Md8aXoL>j;h+mp>)iKT@KMo-FoO_FG88sLZ>+iPQA}~ z3%{izP2pj{Pm60mPDsCz7j_z$#xCrI?S9}NaIy|xPkSF47OXaYQNe?W87p6-x&Yn( zCFp9ApA@B8yyx4B`2}`tmJ&aW*HM%R>XvIMnRV$7a)mZ55bDkYP1qRPR`R&n$9|ph zhrBZ-OQMO24J8bZT5`ud44%Myp|*P`%x>@ETyn@m*ochM2onq2oQV zYqw>J{yWplMU$r$FVr=*$k!&?#|-a|`_*(6_e&CuT6xw^3N7wAM0Ah8#&=-<0|5rW z=pn*earUCRxLEHhZlPqWD0}geo@;*l+vJNlLqau?1{3|3cxAfpUVZhnwCNMGc=;DS zYEZ0QbOfn~*0Ltbay4%x7`i?;H9oZI2#Or&Fhu5S*ZdOO`#y;axJ`KjvKtQ^>3zFQ zp0uA`lGCzZ*V5(z0mG@jouepP!TCU^ zK_Ti+@fvwyGQej8z|=IXt+*iN>>{!!64vfl6ytVwO{G{Nv!}#X3Jx*fZ|v2dxZH!o z&EMC~7r;O)U#BXQ`tFWcYN6K&9*T+wH$eYMG^6#->u>q#_&~#>oB9+mDVPov`xfJF zo7|))7q90|tgpn`J*PFwVP!O95ojt>EW~B?V0!nv7Y2C%+COCSw9uR& z8?-3wDhw#>tpc=ubJhN!bp^HSEUE3 zyl44&P(b14sj15C({Szt`c~;3=>_Wbp=d)!Yo(x%^RZk=qmG)8o~b+XsnY2FM`)i{I{aBvAjMavj9d~#$Tsge{SHrGy)GXowCx4Fm@5qDoXC8BC2c}a=EAYFFYGyoD~T7l zE&&$FbJ+dj6%;Mbg-`@qi9O-_L7U}?T4 zH|$>D`5}wg1;_62l#?Gqyncfg!*DGFcI zIg+0&k}PW~R8nxCod)*tCtWhd8~dti9FjcvT`|xJIjJ+k8y)z)BKtm3!b^~yt0`Yv zyqPG7AqO0NHj4Kh%@d1&g-2jR5JdQ-tm*BW?QI*gANx)LdD}YFwbOTYBu!;J#wSBX zU|(Mw^mk|`mp0|&oNyK#BbGo*=G)bLd?Hq_|FwbbBWYXlMJv0SK*}1kp`DWY6NR=& z>Y(Lo9B!0d(tI{ryHqf7RXTzNPDQ!ZQ7Rj=PLTZX2%HKFEsM(daUPBDi|}2#>kRdh zC1J@inch-CX782kyxk&)oMNae$mRSYB?H*C8jnI*EYr{5Iq`!CfkX}vs!IH*@H_J# zs}F`CAwB(@mHgS6hML0_Z1TlX^(pWjX1G~;KUI#qkbpxHL+u(|rFm~odcm2$Zo5lu)tb_pd)gcvdbe3B z!w7{;stv)65?Yxhdu|p7cp()MnH~_XcISPgyhqD1dmXlHmm$f@LW6J{_7_?yJvcT2hi?U=ur)ZF;s8i+_!sW7W4~j z`RlmNF;|TXF7qy!`;@+Uo&mAZ*rhEBz%-@j24OuBoQfR>7hLyvCEwPkhfVjC&3unG zof0gD)*2+}&GQyVt)G~?Aa;4NTM7|z7DLF~Ld1Gu{b(@(c9Ab$>qTu}9Hqt?v^!VB zyEKcVGDUhM0yoJgUUI#TJvta(%<`Z8qH=#BMzaj7z4M%Q$h>N&@=}M8WE!Biqi4$+-$*a z{lio0yS~au%|`pnj$0yX6saO{{Z=mNgR3GRy-Yov5`HSYfSZD}lo2-!VXXJpw68CQZiQ{?XcWP^=_8mgNi2-Nld z_983O5z1oVD0rFdbUcSMzWR~$U65C+1@I|#U2mtJ1N1dxJ=O9)>4JL&;Wf6fqPA8C zdz7SH^bK4L6rXN?DeX|3eicD1;`!lLI`D_}5N>HG4B0H83aOFx8NHcX3(LN%C&cSc zJteGA#yLv zjeag1x{Ip@6!e=H-dQR!}hF62c z{P&p~<`=R*!`v%!c(cBNjv}>%_15|f*leotL5JB1SJY67Su#DlUzXNhPqNfq?L4hJ z#>k-Bue6IyZu`uypTYmoU7?N!uI+@aE@}?|TFz5YFX?v^FPEfv_!TOC`0js9(S8a2 zOf~PU^bgt;Ik^g#LAul8*oT!>9^Y8rW7jj)F1PrTtN)Df74Y@4Ngq2zCHPLU7fUaI z5dL+lmHlaQ74{V59Q5#|_0HIT5cTgKYmhz`in0M@V%(^v7`4_Z?N!c@%;zW^x0WTCq@o%Aql z;yW!456qEzWuizam$qZ(UBox=q6FCC&|f0ngb$#lIC#z1qHt;624Af7L1kSs*TOoa z-r`SBcparc>A-}H8#gk2CvcVRS;M?Ax!0hC3G@bP7BlP8Wg^3Brv8_^(}Btwl6+7(0Qs)%r;}- z|5Y5JF}lc2ZvUe`w(^mOJQuBz3>x-=9CP zv$7P>LRZI!JncbsbW9yl4p}wkZ9HUb@Gvc{wj2)<@4fq8FbQ~tdUW_aD6~G&Fq=~^ z@fRCjguw0{1$XO+E73*5@Alh-0xuGyr?Lmkrw_8DliS?En2WfU*5S~}f9lcS>d{*c zc-NkXX@v_8^!`bDon95gSbZZYIeB+o9cksr>&f2Lag^;=F(z>=R8ruijVe%Y2YS?0Q$OkoEu9#*)j_KNy$)C`eO>K$VMO?e%gYvV|s z3ejjV`Pv?LqDzHRZ9bAf`8fizt<0M_td;Bf1l-xrbwRDkGRNtqlY>Zq=50->ze7<% zi&LKPMjC}-1nFXgc&*#d9zehM0?~3Akk%_M8m0NiF$spR2^z@O{J00NldCr9&oJaT z3-21A2Gr5Z2}0yrLz2lcBnF$jB6T-wgIUAqVAb@HF?aG_BuLr`ml@3Luk2_qE$-IB zH^)&qVw-1H(m;mKVL9W3A$VlL#Nta>bYEUzym}iFTeV<<_?UQkO}sF1O5-~z9~1p3 zJK2ns0ZK}bDJUfx5tZ1mT4HQBkvT{rl8IdQ*;i1D&O@-v%GI#0_(qgScwkMA$J4qC z%VBFq!Fl>b)duO|Ul>5Cdm-6q@W~w!nmVgNbIPz_+CT>9)A?HALPGF0T{jzW^A>qu zs3GaA`$u`ffS`{)+$KPKXD%k7;v!cv;)f`}Nr`OP^PVl_Gq?2OPTi4TEvwUZ-%gl2 z&ZUDKR)yfD008E)_jw=@d5Zl)tDtJ88jD#gh^-sdg%0|&(ZKKI`B+~{f7oUt1&cUu zJY}JfRDzSJn3=NddX- z-BL++Kdz~ezN{z`Vk2Q%VSLU9{GKKd4YH`XE16{&Z<~!0`OD(~LbWBh0HvW&MCIFVDwTYe}U7wchY^-38zZnNQ$Si15S*}v=dxe z2Q_IkJKCrdRwS1DnDN?{db6O~H_BFZnH{fAh1f?Ak>&>(0MJmo z?@Og~*VjvrU3+HkLtuA{3&7@1I#YCN_U;`tctn+UjK!yUH3T79+uh!Z9WO4D>ql(}a?EqkZX)2T2o89#0CMGAIsvTpc zdns*nHedK*2JL9E(o*sP@Rl}3Xw44LaYCoCElJJKJ+_))^vbuHo-WWY2O-sY_+bZV z*P&(xPPQ0tIMm!}oJcIwfhm8KlIFam&?Z|IUVXN+W3uY#mxjzliO|418v0%RX-(3H z@MEFnCVcMd^UNXPNXN7>6w4j?S5lgK-u7~jmT!7q+bxtrJKrijp-Duphj+K%*gXbSK?yW)euhUgy=yyG;qJixLHu?yi}1{8woG*>HHENw-!Zt#dPv zY<}H-1Z-DqZsrCx{YkXh9Z=QX8PLsnD~uy0NRJk=+i}rAB=+F&KMezdJtn5Wa-6pi z_+LUwzjNH6PG@NGs1G2#)7cIO;AfubJ^P?>GG9gWZG6X|Et!9U<7+*oF9d&Kfq^p7<5JO8~Sgh!4%S+5erPJk4WwjWzS2^r#I z)*IyxYSMKeqL*A~|J0;5p{=8KZFa62S!&Vdus0CgGf(R-8wz*Xf zqj!Zj1#X$^;ks?+(A`y*q!oOSy4G84W{C9tp4t66A^H>w{oJ`qc2-o!sPyqCMW6Lw znLN_gZr;%I&sLjS?as^5F&bRPiv{`WmYB3<3Rs=`vZWFxVzF9|@XltjyYB@_+% zgMCow{antYy(RYi)VXiKFx=N%a(R}9Fg>3UV63$$xUF&0XF4 z|H12rL7*znOqzzEnP7jdW@a_7y=Zv@gMKfT3GiR%$!v@^&COYg{d<2$Dh~@Kh1w{G z40%1$Yv~~v7qOu%FgK*|`~0 z*EGmLYUG7t+#VI87EWod%9V1e`KA}0AX=v@dMkExv`FeXy`>Vuyo5)RMr1w`F8B4eN0O~iNaI-gd>lpuAvxgbH0Ql{YzB=e=J8!^PQyq zij`t)9rlJ0Um_QA;CP*O1sTc~!CDKmTFeYgRU_LhM@aBRi|Z1u1K`*!%V}XHaX)^f zZPP`S1AU%DsR32d;=Bn(LQ$|II6_d(zMK~J5Q+Ked;HchHgw0#?4VJUKCHnaw}imC zDJsJNh${)R>#6X&gKvYu=3>P@?g;-5P~^|9@bs)Rpy@nK^@CjfS-#NZ~AVs)(aP(N_8|gRFkI6iSuWOs>SsPgwb+pgkW>6ZwUt-qTpXL8l=Vv zM;$5z@V26KId05H$+^&~=|Gb*oT!kF(Ia8a36pv;&b{A{aG6hR;0Xo*Xy@|ui+3j% z>lV6&_eOY!)w>k#M%pI=p=x8N7zV*E>SdV+8u{n08!4FX8NGcnH12`clU}O4`eYA+ zi&TNC?&oT>)J{ahk@2%cLhoinqBzN!3L?Ydi%Ab;n1)Ii;<`l+sJ_Vk*8)xKwYv(# zd_QbMpZ`tNG`{A+3jRG0u(`aXj0a%^CurON2s$xkSA-hki*Oe+S=yV`(=w>A!it9c zbO}#a>qh#yUX-*vWJHNz;q*GJWa>&AD!Ta*bJN@D3|^o!qgLmOZR3d4%{Q%HGqKCv zcma(7wQL`(=xXDM{E&Z`1QgDF4NE|U!8i2Jzg>iw1-<&Niz2vz@)KuiE#S{RZAahi#BJZ@gixy{T#ovxT7#4{FkLNE*a2TNb`*-ZhsspT<@cWnlMf_7D%} z=Hzps&n!nY8Xot~8pJ4w_O5fjJ)cn(RfjX}H2S&4{r0kzCXS6Ol4Bt|(K?Ka`uma6 z${mHw%@?qxb(XWlQnq6AFDJ_+1aAsjN%pHtZEO3h@6803j*Y%1!nmHg*!ZrHW0&f6 z7|me`)5f8VzhvZ3cE{v|9}uW}PA)VzO?5pcW*9z7j9L&$dY;zEOx8MlJ$IfRDJ~1* z<*A_A+4Wd;ZcCW#%yo!vd3FG&we{sKVHp)R6P0VFX=UoUTRf8)AM_`EM~(oprdoM8 zV+-qutNspzyd?!K_<$=}HWrXpF;G2P-xK_?bQBj}2y?moSER8MNCF;v7=mLOWn$l@ z(XU;5iHa!*G6FYO(t3D1jPwxfML%B0K%LfjA}5{S^eJ0hg6_ZNkn=kwv|lsUeAZ$H zFMMikZ~bx0P*55(wKMvh488J&{L2H%df3#sjweT$+#1((7>v)UAhzh?CERaHJ4)Cb zZG{y3S0#@v|aFAg%GXVOhA!q@3VAZ{uFGd zvOM72;$ZaW8a9ZoRTJ^p@_%LlzUOHIDZB>RxEnjoX1@&e5KlpoA$J^3u(HO7@L`uY zTjy_iIbxLe@j$KMVDujwGXQV5qoiLGW;l^Hb4c-6D==m{`U7MhVcJvW@g*z;FQs+q zQ6N@30r`tjxP0lX38F_jb?B5JY$Wi*TU6^0JxMnN@6VB+n6HzYQIsY~-IeIum&$1e z{0d`n;0+bS`n&Lc+c95ST}ew?I?bV8_DyY}(`g^mH6mdmOOzk zqFcl`-x|J$+f+irEMGP2UIW!MT(81T5UGWfL!Uo&Y8Ye`tQZa3E#);Zs|o0{4&Rgk z*5x%4Tf^eWetRCZ3Yj;?ZP_^~pS_RZJ{Y18 zz5hgI!g&x2uf3;-6X4TyUi%>q{{xmLHE5-i=whJA`Atlvb*c)gX{u8dPVnTCN^j|6=beqv~k7E-wxtxI+kT!QFzpTOep~clY4#9^BpCgS)$52<{MElWCs! znKkQMvu1wInjhc4yZUxlo!Y1BRCQOKz0++-52))?2N5;3Sbpp%@F=;c)&sdG!-H*_ z$AJ_>gg(+OeYE{-7Mo3Ti7)MEMLpeZLSi}|4umW-7bgjLe}cjGF-5I==-|MJ)zQD2 z%vgrTX${pk{`dT*nJGNuhj1g@9J8YM$uu>wP1o*=`xg$*NL~s$P`3|@k2IMBF~5IR zNz}!8G~QLW<~(4^~fkp6QG+H@>0yLtG|&2 z_l1lLuc;$ecl}9DbdH32cR4t)^^0`%!@2O$zAt-^XFq3Fp}lRR6b=PHSvl1bsVGXR z<0N4Y99?(7{U?!%t)39GyKlz`8nEve#fKzr8TyLZZOB8k?jz<2>>vvbqoQ37sy}S969s#= zy5Q}W0gC{%ZX!F}yT){RAfl)D>IEMw!n}e_p*|ya ziDMKoGiZmo2YVxI4cz-u9e2y&*B*jQMCzfaz%)*!2G!Rs|q(##kffMSp(x-R-{H2`T#j@;82 z&pRPG9+|$x0>g04IoF3o?~>pVE+Le2>`Uau%83n8bskr!I}~D1M9)al_C% zmi-DkN}(Hagq5T`qpb<~=b6hyzq{7qr8X8crm8pyfU{%S_F8Ar(GA**Y)U9?}zD9DYZbp!i z19@26o=#Uq$8VEQzz|LSHwP_@&@L$AUCBPj$n&E*_zK2WU91LG(W=rw3S^~-D%*-! zl~@SOtQWd1St=2kjFi;{3lJ2QHn%E1iQ%=c1?SDh?2NseX>nd?rRq*TamF{`h0$K& zcV;*}ghB*cJ#D_Oa-p1CCM>YtFfCB)AkxnY8MH5>n!ee!TigD4uIbPfwP4h9DT{z! zD8axC{Uwbbq`rJfqIfM*btn)ZI|1@`>L5`rU#`WE-n;Gu99>q*iy%?6hgw;nZ;&JlePJrsCW8){8{G17D;MDsp5F`h! zXMtD;CNn`L9`b^OuU*cyGsvzN^#L(x6V`If!tEG8O=#Ci)T02gD;FL*P?>R2d`-lG z1}uYr4J&xUYmoLr>Vfz=PotchA~^L`mFnQ7JgA{uEqenJBLbJn^KwYjU6bCU@fSZ$ z)U%ley|-FPakc2;qMi2U(}++#u94>ad)ANtG*TA z$M!{kCN=uf5Y_!;2Dgazj-2WkR&+gWh5{M9y^3R*9~(&VSLFoUFNGmuB7I!XV-;YM zu;SRv0P!PBAm=ZgO~RbB2+WO!!17HE-BkqylOfxnDTr^wE4Bby(X+TJb}F5Y9^nl6v|ztO{|;eA*5#qCSU8I;(@F zC;ZWAN2gwLfY-+63;Z`F`s%Z=CJ7E8X~SPk7dZG-PITh0BOav~dY8G3?pT^{8%dJM zWE?Ig$N)(j6lu}FmpFPQ{))K{Y_F#G*Fng!=*-G>;GWlkXa;2+VoGY{+ncDi2ZP6` zjkmh}dPvdXY`qjb3n6U>2`W74P)`-KVNr;i5rdK5uRO83lm7YxW)hQOeyq4WBh|Mr z&n`;=`MeNC`F+f%{&b`^Vz;k^S8E_ss&43p-o1AKq4(lz3_B1>lT;hngQ0{Yya?+3 zd>+$3=pYUvwmaD2T_=WcI>kA~nn}C_F>61Apnc)cU2(a9_CDb0)ST+~n1a<~`iDs* z&~XM|^k3l;m-nWIC48|!nr(p;{e{XXU$rlhx8TNVu`2Ayw@5ys;D|+BA9!lng4F1y z8Y~$C?1>?9S%Q!~DXsPOMIKQ*SHnD3sA@fGv-8}1ZKxZ!VK|)*qtjWuAv(|yrQhR^ zQdan4^Wu1?{MPgCGfw0~{JzWmrwQ_+rg@rkT?elj3#{Lzuh5;zmasQrhT7a=??aX> zC}R_Uk;X{lmXA^W$}e*MAmUv)M8(ipZn5-D&02>{hrN&%qJaF%#E`h&R8*=q6;AXZ zOA($-=#sL}TvBbTb-w17&zkcmj z6J+T7?5wn2(H$d`zT?XLqYzGUn9n@mwhUBZF=GIyBWYP3$@ZmQO|v(?k3CAj_lO4u z=AmD+=hOYxE6Ok;zGOIT3EWp0GgMwSikTtDGM#ptttb-K9a))?iiS|wr90jeD|eBg zm^rpZ+qGqiZL`QKE=g43US!_T5KFoq=Ot}g&XZ14a=Nep&gb;!+3$;y3a->h ztXTPdXVL+l=N(!e7U;?J>@}mM?EvTF z<8CiDZT{+VNYrFrE6;Z7@TyF|+&+ey5*mk)4IUk1Y-Q_5x5@RFo3Dg6*;x$=VrTeW zq-BGw>YblXnc`~ub0PV-ca>Ltm3sP*$Ssbk4RaxsnKTmkPGIXY@ouugvfl)&l3L~0 z@gT{3nJOZ%lUNSr#_EyKlzDT;%Ek;cH4_rS@R?CPaQh_R%v)M*zWl2$-u2mX!Cs{% z=hVV&ns1dDR20WxIxM(obv*UARs>z0T}#8~QI+36I9>8pfSfmwF*BX5+E8JN4q=_- z_NR9Ynrihv4#k<30{b*Zf9GY!TJNU<2m5n>Od+Rk(*62A;+4M#WN>ZtdVe3B8~1eW z^t4@95C|DPb*%6gqpcV#SmQiTaNTg%S}cno`66D4hh5!ciWx`|NP}z$`3KET($gB! zejSEhH>IJ)S5B)z$`jtAzYtVmXFfV;dq=400rjcHCQgH4&J&)L94#k@erPr{0(%kS zaO6oM0C_mSpB~j$8(Pp{@Q~G+)?oieF6)RMQzS6imlbg< zmko(_Db?!Zk5QEEj{ z@Gx#uFvR{%fCof0HNIPxH#`O6b(Gi-$4_WoI(T(*SKME-r*YPCA)fB|9f&Rr#df$W zE|cR3_u!xXMx+}eWGJ7*XzI3q+cde(aC}zaM-cpk%`f-?gais{=W+%)tW=vNSXB=P zl79yBp~4Q1$Sk7|wlLxwx;j~F{9{)~YUGgECJ=+bJ)@vpJrZ2Lu_pdk*_$Ku3R0&?(1nd_5Hw}ayS zIaywa>9j8goba@Y7n?^CdMTP)xT_Z1)S{<-=k6^05YxZAv3N6YaR@QGwr`ziJ~oPH z&tulCpD7jEaG6%CQ^+(S(_md;%USj4=HrJCg$PhyJ4qTf1N?H*cDhC^$X^`K8M|KS z6!`y$O7iHh?(?e7*$>6$9 z5ctJ1CpersPp(3|CwqIeVYegtWc0+n^@6w?wWOQ!xNzaIr>ce=5#qxCvekzwBTUeK z+H^wq^XdLXhiP5cHDGYeeo9N5y0I&U@7U*R2jJ`5+Xp@%`00COxU_jaKH}5`QeyU? zWVA0Y59^ifD0~*GiH|qiJwB~)Dj`(lmKOc6y5n!(3q8&G76 z6?XYDE@RunJH7}01rGG8@G<)4$oGfYAX&A%t*B)#5Ze;8Re9;}2nev%`0LYc3S$?3 z;Y9pvoifUP(v4=HKUi$z0$(N{m3N3g-!hu*b&QVn zBr~P+mgUTLumtvVVt6bag}pb-%4~Q&!x7Zqo#+vms}*U($H+E0kcgmAKveJ=vdzzf zEwKj@q|Xd&rk0WN_bimQ)RPT z+=A9Yc8Mi>^qC;@TbAqwOW))vq>fb2@(H2_aa_@yT|z;gW4t?(0K#Ik$$G=3#XGwSSr zjy}ObAXINgq953*hi$Yzk57=0dR@!Jfb&o87mykg@GUK@>I^q7u-KV$()u)>w7!ov z?Pp1B2xBDb$2i3_rEV(e88a{eu5T1=QZ`t#%EEY+HR6WLG2{zxn_d|>ANEha&2V)i zdW7=r<<}d<*iVhf|3&p=nx*2)@qjgkI|cc}Yz{pur=m7ch+0r_dHK0v$islns0+qy z+0Dw!hy!N4n@XKvPGk2>BR&s=9(NBatKeJY$_e#2c^#rj8lBv2pnCrc3i8L=mnWTOfn!wQrAKi-yLHm5JTPqBEm7$5ONM zW8=t!ue8`OqT0N4iWsd?C-SHw9^~>wqk}`G$@9!ZEAt$Cm0Zw{v@=p!R&(Vk^OVzy zjgp+Uat>kqM$5 z(;4>A7M?1)dV=4nC&T+gspZqW!sJ`wiX8KkQvMZzJ#o49!bhj5vwIE#Jm|-~xUs^}`mpT#ZH;GdFL%a0@=Dn`NZBZ8M2gTD0_i`+XpAUJ*S1!Kcy$B1=LA( zN0bES+H$@0W9$9`PlqZn^C=W4LKQ`-4m(I+VUcW0BhK*D;RPPH49`S*Z=IkDHNF;V zv`Pcp1q1~Cu(h)LHnxV_p8F6JG{1GAd(x=_&*bjt9F4o_pHdsqHavIOSnYz=$o@H>t(|#=EY4F-AK}?u??a!`74UGW71(XND;IM7%Q_F z2tkxEQEC@cn_WK&9r6Oi0w%TS;*53s8i7{OrqImLGJ*u>x|krWo7yjDBvo2FIkgU3 z?}9;czcP&f4!!eV!-wINIh=5-;iyT;(H6HWp2|Rgzky$iOn1c7R6)6AMp6H0Wuu+| zzn=Uj+Z$99mFL&HtRVujd1voX&}bVvTMB$4(T#X~%~ZTz`uivH%lFoZd0A$xy9$Oi z)lVo3s!hqGQR3kZnk?0{P6h@N$iu&tr|{1sSRU~JoVuR zvWGc~)Vkz>5K#U1kWu}$M|j}WYX+W|bF&KQ-n@H2xsbn&K2`()^KgTHvhJ)r8wDP`oteY4 zex5@)m^`=V`#B+Tcj%>_KS|zlI@aTzJ5_pe6Eps*@RQyYhtZCWByTI&Tv7o4O1|lP zL3)anqw4Ug`Ki9^nKP8?^&d6Vcp4d+k^>fNX&wk>oX$CK+uMQ&JfofCvM`?Se*+@w zFs!TUM!R+^`1u?(3Grufz_rU&2U(sii?{ z?hZ?HH;Pi~*dPV%-!;Ja&zTM` zrX>#~i4$t_p8wf53qvja)lLg0W8kE=E5FwK_kAvw-b7ZTGm>B*51iTP7dsgk#Wd6C zsMJ3{EKOf@wj)6=nCsPwe~M_mqMf08I6pS&PeD9{p4bfv0jcn@1%^=`US7JkCHWzkao`rL$i}e0_2{$ykG`ioj7- zf{muG?7R$qS9Qv!shPT1z*VjcQ%(nj2m@Lg&%C8~VDW7q%;u*`1wQtVcBkqK zKPWgg>2!&|pE*PadQBdK>yV^S_1)%&GI!7Rc>KBtF0^ zwop1e(qj`F=!D*G>9RH7ARwaPn^eg42cT*BUB0L8$wbzJ{6RuYWX+e1%hH`IcJPsE z9sQ9{rS{er7R&%&osNCWSk=3XLms&tRhr8a{022sX1KOU9U+ zHni@+m$>pP=^Z=no`nyUU;4rtF`XKre~Mq0+3+gp?I4p0ol?7-ob^jL(RrKITgyZ zvLchc%+aQ^y&nUX#%GTpT!iwq1P|t&d;v>ovxSdjm`&%?@~dUZKG_4GjPf5BK2MU` zj5F+9@W7;jVDLScYc*U?7F9OBV1#fH(Ao=sYIgabX93J54hJUIZ%vqEknqZ^sZ%*D zF>v{x)OWxP8F;%>*V;zjJzSrdn4shtM zv)HBXk~;7<@VIEp?;M-N7>E0FTH^5hz^cpju8kr*LG*qU(V?{7f%=fJ!dj-tH0}Nu z3Z*>fK|P$IU=B`Ef-#uh3Hj6=s&)n09C}ry?G~OP3n9X)P!n@;o}6L*I%zn3Wjg-+ z5;%Va#NBT`Uw(vrH?39nW!5%D&Es6R>)Xl`U1T$euXzADP5WQt^>W!*w@mz!9=Lx& z;v!Rmt#X53{~Y6q%uEWu(sBkOchDeooGBvN9C}0}2G#63c5AXh=^8G67kN?}b=@}X z2?}pS){8R99fSQWv(hE&JoMZLHnB0i*^p-FVg0%pF=5)en!e{Lqt0^ouJoxsWc)#7 zPc%xY=`$z^i#_jSf^zfhoN{gVqS5v{1o{2s-u2qQs4?aYk*65c%9o&SKJ*mxNl6)b z7S7-M>nt&kkvl|Cwoa7qn!Y{f%YS>x|MpX9OtX;kW2ub_4HdmMZ6@!Rks*xZy*PkI zc}RLfnHz>Sv2DGfBn99jd^PQPcQj4fMGJ97;w!L&NzDlpZoj-C{plG;S570uZUZ0W zt8}6z!g=uEnHm^JELrXX>-zH|;obvp`e^RpeIrlhdd?3VPl{m54Zjn|UzfWD8#d@?b zwWl`$LOkuJb%T1?nI7#!I}xvkxFKbY$x!-r%c%{c`wdg1_s#B4szK6%ovnBJaT|7h zw*}>z6o~dGQCAXPNJg4kcaJsCAHU#aQZ?{RAXmU>ZpkC&SX28(=)PoC$7;XlA2Y44 zsWqe*8H2&HPdQ1VEhB1NpY_{PGAD z&s>JDur}!&O->~Hlqk{I{uoK|BumK{-a=s3(ODv6-M~MmS9~H--8jFI(a(Ad9mm3!moWvSeRhmr;2EedpB-rpa{MRzB^~iNeDp zXwb9O%R^T{BPwDd)*@u`m|LuOm2+xN$Yhk}DcJks)ozLKgoS8Jcmwzzq3s>{9VHW_ z-yd$j!l1^Eo*A1O_k%+4gS{h&<1hdw+6V+<_x-Sid@mF)fhr;q{|-FRE5;#1$KNky zKlMcgtvAm%Uol9Eq`_Kxieph(b+R@w@8U08c446|i@WfvZneywZeO8I8)cpOhE)B` zDJ|9++t|LgiG9Ru_Z{SD^#qmOjJ`9++C zbFI2-Ci~vuU&7q2QaMf9CG_=s0ulOdJHhD!>?-&CLbi3PnNG9Jb$6PUMNbJFRcte*n1uXK zhZt|!(8l2=!;q4~M83{VmhiIF=}h{0{!ievOe=#!)*#my-aG2fA(I`A!}!TthI5YR zYsRkQpfOH!mo{4wz9GxxF@Qppa~^{zVq$`Iioe=k%(d8;%IS38T%$AQ0&`+~fn>^= z+ffH34j0slCg=LILsK|L6{pkPQS&Rom}%jbcl8_%Q}dfHhE2K>k%CIWn|c5jEx6c3 z%+KJ13#C!E<2k62DH0jd9}t>bQWG#gVUeV9m?m}20Wd44`EEhD>&|sRK?m$f zUR12P?cDDD$d8dc2{!Nc^+f+CjbZ75djT%cLvWM-+3X4wH85sOlgi&I{>|+dMXLg1 zDkWt=8^pVtUtMdq7&dkc#KzXqp6z~@8M-9ii@E@(;kYKByl<($ z0lv*K5fmzIidyPfl<$k*ERp2q9URnBmP^Edecfv03#dQGwfM(AH`c)E=1G@I;WlSK zF&x@cCg+U;T@nUvbD<8=gl1ITALo20ea0zNRY+Tz{idq;p3Q>lDJNC!YE|Y4oApH> z-7W#3|Cszp?{BlpExr1h%qlNHVM+)LvW%$yk2Tz!-y;7Ks($#vzI2#&LMS7s6z$e5 ze6%&iN%Sr;U@8s8*}FjA5Bg16LHZwMg#oTpWgCk$=wIuOj2SSW_2tNRHa$Fqu1qYf z0ls#uULA7E8xp!dw@Ypt@5M*b@dfVg)-EV@h%i!U$jlreSQqPqur-&N?yq?q$jgnsvdvsrMA4tbN79dFm>Dk0wB>3Qn&5s`R6FS?yE)$YI>bfcHHdOQ zZQU^w>~ZuP|D9K|WLtn5`GpTY712@9mhI2qO)r%rW}zDSOg8&@hC{{$Q>lDxGTLL8 z?i}}QPznGEYUKOQYp}*)Z%d6AT10cSrW(ZKOuKuLY-*W}w?NR?i0*S2C)?t2F=(Hj zm)M*8u-p6QXUW-YTO7WYkcin_*Wbdc#J`Y| zrZ(ayycJS)sSXRWq+^seNqt$iM*PRYuEe*e$zzfIMDbic@Iz_A4|~@Lsh4r9d@hm|5vo$do-z13B&u zJN_ZjItB`#VAJc=BDdk)_7;*|ZSyxZuyJ#y@$N9AlYAATwNixuf`@8>V1fodgm~D) z05sPQ_fyyCC)%KhsO3m??!yo7;evdyC7|G6|z$& zU=-*vcPZoXa35)VpSZ3<1K5#nOekOVHK09G8rBnry32 zWOYcXXmb9_EL}P2gxn;x=2qVX@w*&1XiNSCj^%rqDbewdaqIa2>PnF`TV9KiRGT4x zVwAdj)&#^YY=>!LvmggPpUQ@cO?MuE;r!PKx9CY8CH z3pIB-TvWDdhd#CkM<0+8Q*|qmG9W^{o_n6B6lZyGs7rz1P1sqR<)O^gnjz%=hzn9B zA9X|Bsg|z=yEoceI}R;xk+_cBZ~pz^v@qw1mw??4O}(Af-|vMh$QGMYW2XvhX9p|= z{-G8AIw(K5cALnGdGnIlQI{)y;k7CP9Kn9R|}PZ^Y1epxgPz7LD5Wp$c2f z0QGtT7w_!i^%$A=MTtf6n4O7zF5v`SkoQ{nH#9x^4kwd!PSjLO;s;J`K_g1eMXqM7 zgefmZ$9I5?m`@gWR3@an-?&$>aOSgc+(w?%^!7Jutg^65iObY7zv}{0isvMpVn8a$ z9+SFB5f3X->dtR$-SZfo%lY^wQPD=Yc3_L(ta{`cUqIpm zv07)p8^&*6Y*r%%H!O<{63X|@vwyAXRqgh6+PyADp*jdX*J*z76Rz4Ulr=(b>iF*= zeH~9uvyZ>ZFsb!=Iy#vD^yrNq3ZZQBjWXR%6-H3?aB%%oOFIh=Auq#PPU{bZEp&!# z&?64p_p`@?Z}GN6(MW<|UuCvTr-u!OjIV8|tf4RtfRuvVbYXYf}w_PS-A@P z35D9dExJY~@K^c!%#RL)G@A_u70QJ_fJzM|ZFNU{MdLm`Nsm;UhXcJFoyf(BGr}BZ zzv`#M92CL@>Ba&`J*bi*v~ndo3AzFge@l{WZYGV87>6Np5-Ug_^L4+SY%BoteJr^Ovkb z#_V^d|H|g=AC^T5C(=LB&XOI*2dZCZZl205BiDM7io{AE=$6_pFESw~!hmo(Vcz6j zv6ly|OdOQJYtHY;{}IB1`M(Qc)hTzQ@ahu(7X>pwO$_!QHL(ET{c~B%4$$ekW(S1C zSYfnF=&qpw!3_Vm>I~AnV~yWrlvoseIWOL%x&9T$`d9Mpf0yR6asTgf{+A@)f8+Dt z{pUZDb^njX#V+)^=S64T-#^b|f9qYb>>N~=o=?Zy1~c8@(bx*q5zw_Yb6P~~Y3cte zuhYPUH^V@Rg<~@|kVxW(%Rr-}3kwS;wp5&qzf8Gq^48m$N`OaC$Jh8`Hfz!|@2fIA zr`iwNH(L)dv9XmkG&I6PC_2kr$ZkZ+ss}xD*xQ!f^TfxijwivXII$`Ri0*ZUJrGh#S^;FGtg-w_i8HMlSjs8eHg%hADlHSOn@l|4eX0eIZ zt)2qV1fZIoFI6ec%k%$p*W5J&gQGpIw&Id@urXZ3F1>6HJwF)j=3Q%Pc+Weq^A0@g zgR45V?$-=x#bgD)$3wr}*Y67sS`x=?6$wFRx1{8tb6K~SxE=?zI>Zoh7}$tydi-I2 zIY383Sc!d`;(xAy@HxnI{RpJOlkHo=*dA4@%xE4O^F1GzS09$-N9SE#Iq(2V9p+uH zclqZV?Q;Mj=+RMeHisVa({EtbD)O?6H+F2Ty2mFx zc3B#~H;-HfSFOfhkkuQ>e8XjpWbnk^B3Nc`{uEws0nNEVHTWG^-GBKg;A998-4X=U zMwk%MUDQdFa7J0oXUUHjD+ylr7=4%#<^KM8Jpy}P6hfhVo<5!L4($utz@yVv58Uky zjMb5PL7K|)I4;#sL16|NB9%ER4RivtZ0pLe{-DgM&DV2RIqfEc#w88YJcN1ll^nX_ zHS$&Xl~CCl3{QZ5He`+ZRctL2(~T{kIPMhvmMpXD{>mVw%3**Mx|x1uCS(oq<~qjb z#SkMi@fo0Y)$b$T`U%)OjT@cU-g1y;S(ST`?E_vX^El~{*uMf``RLLtr13`1Sbr}l zvzAN6Bw8*efe+L}G*R7EkUxUCQn|`!WFhUh$v92@*uhwz2>5O4iS|4%T^Vacw zTVSnq-gf^sLfJ==qAd)_?>Vbi*zA9>aq%S5#?hY&i$X|0>q8M>yA6xNLh5LA{?^ub z-(Pt-ryyWmN6Dj8xUr705+XV^%qszX;k4soNY+*xwzDu3=aE>#0Aou>vSQt%!8yJE z19hgv{{8XGOdR{Tn8B%eKXIESdfW z5Q)Jz4(S2DC+Egs%Lb+w+=k<196)Ykh#|Fau+{G60A_99Ui@Oe#L2b zN-jP8bYsyQh%k7z2fWg;c^o_K#Xll-@6YQn^W-K7{C%8?ik8usw)N#R)tHRJS&>X- zx5VDCA0nJMc3R_TzAwDyDo|PbGPR1+fzZ<1&f}m$t|=GRon8SGfx}pYMkd}6?ru4< zy1d%Bezuw{@7P7Qwynw+(Ts+wvUBH=z-cAVj_f9l>kW@C{-+B8KIjnzR0W z=T&Q)^RQEqJX5{wO+X$&{|>`Bz{uY64QE3DXP(`nDh3oVbcL8a53VI-vS@7HdR@!b zMP6*29!di0c}krOi`|ysDiDs-AElqYep0@}<2F++PC%ezU?{TR8DvL9_UEyy{MNbp z61n*8g-$0v%V_JYHf@lH9mBiY~175(B1aqZ07h{!M(KzPIx9o070LAqNiu)$WHg{ z7sqO!Ib)pxli65{HgnkLMHNL)=ZiW!xnEL-ZXiV0XhjE+J2)6@F>`0~-sgRh=b&*= z9wZ2K`##3v7nmx-9S2)dZ)M($>ov2w7T?tPg(FTHUeFUzyx~0hgQ4(Q-$gFtD)~^{ zP9h-Pca0TipLz?UF+Oz6UDKJ=Wuy>V(we;8)&CpZO6+wh~fkj+voOPZ+|~^%9qBRK)>)wBpbro z{i6=?C7DszW)U62)>?;Pqhe2f>I0{x*4G(&Q-RJh(@F?in}ZL#0j@r5F;vAH*cU$~ zg}crFHVkz*Szd(6utoQ1yQ~nX zXoB0GG=Q=nlG8>=!O}S4@bu&A2Xp99r4u8;OmEAuQ$Qr6vT_MP#_~D+^nlcRk?aJ+ z+c6)m)h*pT zB^x0In~z1aVZ_Wzhm=y$NxoZ4YiUrO=hRD~!NmHy4cyT1`fZ_JeT#mv2#Vu%eUiIn zTH<=kZq=hEY$zYlf@b!#*XQSiKBPCoy7NUZwooF8aE#Az{(-F+B74?bY%rQtO-OE6$gg7AeOaZOe@^2MLOj z6&sDiWy&yM(v@d3QoFSf^3duz?9zYc!dtJ(x#W#N9J?pNkv$M z*JWAUQ=Z)INS?63@*^IkJOZ+HaG^^>ZiBF`%uJzgyeCFH=VM5EoCz_CgE|XgEhvk%tP+Sc4>>CJ*#<%9T67p0fq7X8>pUcLFhP%b$WZnOne9vfGCw0<}HJ<%A6Ui?g z%(!c7fpY>6 zR;e4EljEz_8Q+Ij?z?5fBj8gQq6CLT>ZG3bgKM5ADFWwS|HG1M+l>7qX;6|O_DnplgyHafR5r2r--4Lm7h27C9AJCQ@@ukfT#piOSa z>R+2-F1)KE9KwA8;wk(xWuTQtr?$mmVO%_K^iuTQ7yZvGg~INu>lH?PJw zwH}GE>@MMreF0*ASr}yC@Em#|G%UqrnH0REXj(r^byBHHaoT34xSb4AUNVbqNj1G? zzSiZwiSJLF+Q##mTYcRnOQlKy`J(Db|^l1uG`uCDd7=fd2Rn&zy1b5DJ;A+pVq@IB4kK zJj|0vVdYwmGSXH(=HIM79;-49*_Kmbmu^AtCX%X12dWz`Sgr27OOtwsj8hFPhSobi z80bvuJgDz+i?Fq4mc-pQ7F&@fIPEBwlQ(19eHbYlk&ze7B3Ka<=ao+GCC}GpWONyc zmGDf&qer`YdoA#JJ<&~v)(%bf9%P_4Qx`)bNk^O zCY81KHTD7PJ?Q#sP9H@>mJNV)M}yMF=YU%kD`S1qjnz;a>uodVf^>OTjmu;ksNA!o zHuS^y5jsnRd}pm+$#XpRlj3GwJTxH7Oh@oKhrd9Kl#0pRhC^d^xEmKVWM-;Cwk9+xC7d0A;pJwLA^R*HTOFsUd|TXKP|C zzu5W$6McH1O67gN3+V(!jz{u7RKdgvCb}Y@E1Q95%0ukk>OJ^T>&Q@igXeCxD zbe@bL4sRyjC%;|bU%E$@Mf@+o{XB4AAV&bxns>+4Efo>^lYALmc!d7BcC2w_T)|!f zRPKaa0v*o?)Hsq-yO@Kncv<8qND*s*P@&DX@RQ!K_$ea^xI^ZBy$j0#fL-z%WU7F3-;5^L1)BUXs(k+zjM?$8fvKEtfB|iY z8hyiJlL8n9ixj~cyr~9nmAygs32^?u-dU*sj|)q({w??&_+JCZ)87CC+8}QHhQ$Rs zfaM8#Uqz;DEieuW0mwZ5ar=L|!vERr`*Lr90d2@9kuZzHC2i~JBEdY zMRxB>MKfMWQxo69%q(YhRTEe@08_o;prqx|E;=g3_{fMkBsj;kO=@-oPPctY|E(}4 zB}*|K^|uk=cG-ABNC|PMAtVIRV9vvMOzP-JL?dU1_!0+W2!v2L;8No>nfljAw zp2;>Z@UYk_-q&4)50K-nem$0ab4x)(V^tL}=IX0>u2wIi6G1oc#|cgcD=O&vng)!( zAaI8G-d0Zll4@~^v>?Sa4+3<6kY=qxQ0~usW?Q?=FMO7nR<|@szChQNJ@Ef8iXNKyHhvNQOJFDl{_n97tq5!Z^yxc32g3FIgy{GMwd1%m7`6Xin;MfPJ}LW? zNs_O0juNE2G{MhNfToxwod~@G`uo_VFU2$~f@Ypa5vvr%mOOyp`EG%0DfB8S(D)`v zg=|%XmIT4id;w?FF8QpIQuV$RAX`1?K<|WlTRmhwvrlf0oVXremlAzd5OVGG=KD=G z)H&@%g0m2wi#!(D4DT-c;S20nFfM&rU+!7og!QD6?YGT zN14YW_n?+%96s=5q4>F`kp>IfWQt6P z8`W&3yQKmU)e|zY4lx*BbZ*Dfcgic}QQis};xePOaW{6f-F>xY{=_L!6ey{WlnU@bfjL>3W9g#=JAjZ9p0QWiT{psckVzy$V8R zI=RES-0lA$?<=3$?4oZIBtUWZLUD=(DZ#B2Dbgav-6;;mU5XTUf>S8aq6LaekOIZs zCAe#Fy?O7<@57zBbN_++^_k2wXL9y)_C9N`we}D-@*T?m_jlyx3NxzoqB$kKmii9g z+T6_8?E3w{sj+J55d5CrpBVrSH;7W`<+@#FKux1xDB9P(iz}$-eEctUJ((AAUHo_0 zw@QXDgkGde73C_zk`zw3tUnAvAr#486l;Tt7D80-(w~-R$$p_B|F^LpDp`V1*=T^{ z2-ETn-YxMbWcDtvBVG9~b$Rxw>L1f0|8H;W^12_$N4u00%2CU^J!XlGoQ37(Lz~{H z&Zzoh9f5%-S4R4kL<+=6|E5}&>=Ts~yudhOGK+;wXupq(X@~-)>3>yq-ulWuC9to z?7prDPEvg*0=Zv^2ZGNSV)V#7jgONgIF29+%H7)9qHH02sh`-XoJ}hLSJ8n4H6Zjc1s zGK_kz0r`p7)d`>#*3{Rh{rdIG1_`F+NP`wIQl;KASAW@4FAT#}UKc}v#JgFd_I2M! z$a9=9{TEA2s6Uq#?EZ_>%Tt>Tktno$>)YGgUdNOSEZ$`n^w!Q)Y`8;DBw>y$&H`?# z)IL+S#Xlm{6}ZSAn$sA65Cj>Qv9U8t-v52S-o5I+N$tqDDy{w=Pji2XJiq}fCXjl} z)=yem6a9&dqVY_1tjNr(2#=r(z~nGx^7`Ma`390JJJS4bHJaT&a}+iHTg@KKrdA{s zxf*nI^cO24)1{xS#h8&vS?rC$f=)cL$QUX_Eg8N_K}OL3JnGDn$572$MlL1VaEDUq zNbwH&Gfw|LIsKXcV|k}(?dFAC2Mqs}m(e(N9{fN4Tk_AFKj+;4oo(EFV++G6?~w)0 z`@dDGWoVJe%0w4gPW?YB6DfoL${=Fd|92UL{{MIRfm73hJ)%OAny+$sgu^;l$Eq)xWMb@)VmmJL-KdUBfh5!@a1^Vg-Pf<;j0hvo! za1u1HF4q3EmlEeK56dak7y;0#Y^i}K@uggf-dC`6KfgJPV9P&)Ux)CGUOzmhhx28#pZPV*2b3i?KWa>=wSi@$Dh z>|#7Mm4BwKC|j1vqNw|X_L(iAvl-sU^3~qanH}m*GB+3hOHv}I!)>2-mSpoj6gXfC z)*gC3(8zwf_+_+Etqoz@(vB_=F1dKco)2W6ANFB18L4~YuUSH>{vl5uS?`YO*eErZ zA`5xM9MWJ8=Y~rNVRRnoO>SxNl?{Bu_TB%4#n|z;?q(V>nV`aam7S_*!&@}MQ%no>7)#7#XdVT%vk z{-oQnGov1$ic1~#CMDhQLz6CUO7f=<^Ijw5`O1Mo4$76YmBc1sSRSadEcl?@4v7cc zaXwk7e=;OXo!$Ou>F_djcJrgH3U}(K9q0#JckWM9>&b5~{pim4O~Fx5z$q8Q9(SUK z#lB~}P|=G?kO$dVbjFlvXx#?aq=fjn&5>wHaa05Om36%U`ENmf(t{C7I@Z-PmuN`rITc|(xlQ%lMDf7R& z<7IKYdayRa&7aii<8QR5V-(iiL2=j2pe!P6dtsB_$y|4zKy){sU`91r6-3!8*9PW% zb@eIVwLNZ@JpH@s{UUM)9tmtwMO*&cficX*d`?Y$)dI>6=I3t>rR}|X{WWt^xFqq+ zBBzAsmkX*ugG}7A^ot?L*>AigBm4Yqi((uFRB(4hXA)4n@d3A0S8b46Gj*z=CqTUs8p|h^8LkrqOtVf+dnw)U${bpaE74~iEmiS6oM3v7#b5|2MJw8VbHR2XYuO$I`y|CvtCnHp8)r=}Te^ z26v!HTzODA?;}>-f7ewzZ5Ng*Fm)q3WQf)0MCLr5XOq^?gK+KZ6yGTyd-Ny{S`E~D z@%rU??A;H0{e9ua&Dd`TNo_^e|InROVW0-~!R7Hr@$){E&oRHK<^cRsu^pE1B#n=j zu=z*{YMYh-J6nN3xl5xDix*4E{?UUJ`JV_o%?El#)83Jv@sbt2Lkn9b&as{t!<%JX zxRB#jq*ko%4S?bX+ycNNlIl29jd3Z?PIsyLVnmh|?t9kuY7lxh zGAEMonz9vrzG5QeOIlgsWn6WJND#LH8L>65t81`IvJae_^8Cz~iI^vxfYd@@Y&6`X zu)Fu_dw1`pnYPjl^Sw-aKxK&eEJC<12*1-P=1{*IMDnh)hNC3T}PJSRY&vc=FrOxY|q6nkyH82fWFfNhVH zJpRwHlF!UBeAw2ov#w^DpxdM(I2Pt3Sbdrd{gK`Eo@(9~)j`tcO^Xy)3GK~JX}cY6 zR$6kOn-(B**gkj}o>tlxWQXfS{ z{lX}xAw;jWl4scPO{7&7g$zOqw}?*gqIlsON_6Uwg|r2)Z>MB;O=_*bYHwi<{u2_1 z*ed_r(TtlAqWygi#ne|Xam?c-U5u+=2;EzTyzoJLbXc%N*l>FtI(*}Wyn9| zyn#a_QBKqP8>`qTBv}V15B>aO&O;mUBwE=B@_U@Jyg)qMv&8slE#6M`6 z$@@3LS|h)G#w*_PlFJMNmYlBQv0mkr$V>hUh|?|~$_9O=aK^l4k(8zMUTy*yz4{H< zPbI?gQXE}CZ~Y6j2Lf<~um74yYyBP~st1Epm2@nm^@-wLcl+Rdk3Co z81scl=<{UA<*ob(Ur?@HBPp#a{AfhBb-`n$O)NOU`Ai6MCMAKt8$Rjd zW;x+L=l08YW*P_j;Z?4V%zU@<;JIDydzFPM`4EA-{`rYGPerPF=jFyfNqT>VKKJ6Y ztEAjNFI%IYiQ{R4B0^2EgFbDT&;m#$Bns;pL;v_U+~;0V*Cu$-*2LUhbDEnEpfICM zQK6(!r){Z6t!Cz>M`bhqM6q_rDT|V#9cY+C8{8s(9=MSIPbz9BNIE&R_13o6b8$T8 zYawn1wiX|Mgb5o>-QUr217i9JDNZsqhG#n$ZIaeMIu^EPL8c2gNn=m7@5v`OI97`T zD{fO>Km2Kx1DBRHP;de58_6|(1kWbL*w5_QM?ajN6|ch|+UpF*1?&9Jz*HJS8FHjJ z-+@iyKdn8~GKTF=;)s}6bPK8Gio6JX(|=3s133j5*|C424-O2=}3Gln*97tXA~hq2lLGgc$n zir8eF`MA`AAKl&G)K>R@G6jl)9lX80r&>Ins349s$YJ!3Qx()FHha^uC`xIZ1xSzj zj{yM^pENbcxLXcoSaZIp>%CW;(>lwMW88AuM&Atisxlb&r$Eh!@AcxCij(;380r#W z{vLTk$GSXm0e4R#ehBU%!vt4)%?*A1o50vud#&#{D`(}a>XuS89=!Z>v3wk%{JR!p zA8jU`=pr1k(m8=>1& zJD-5t}{n*DX>4e6n)B-VkrYvy+fHx z@QrmI;Y#i(eKBSO`Yd_yoAzkhoaV?tEGc~Fx`Za1H`?`|aC5P-?jda@%VApfThcJg z7Ad`h9LqdvYVi@03X5LH@w?ZVO*PpPcc%RQ?e-ik6tyxI2&D*JVGq-_Vf@{=KjBm# zBVyI)IPmilsnt`YcaGjfjx3r3Fl1})is&m{E<{LL|K+0=Az_l(_Pc!!R6!WYnz=_A zZCMDVf>rAN!NG7zV`B!3N~SCZjk8p`z&Yck+Gdm2nc3Ze@iP_32|3N1BPZZ)&y-my z;`);kg@!QB;-^wq9?H}`aC*{W_$iqX`nKeUcKqjf!%v@Y{HRAzr=e~DUH|uNWxf)= z(t0h$I^IL$PNIHpARd|EUn0NsH9mW&@ZT(No2Qd`MWXc@E4Pa~@fD<@VjC9>;qw$M z=z+Wq90Xh91TN-s{a97;AQsm4-TnA8MPEp=Uh2qY1$_x%RVtbLVZ;7{e4dq%&{Xf ze6!PKOGw-WM>4o~)a-tki{^%*A~CC%x_uW55XnyyEvpVi4IPWJ#@ws#aV0pvwMKQu z&@9+2F%h03N8v>%buZwW5qRzuIqZgrV>s&(Mn|Dy&rc@@dQx~1Z_sj%|3`#7n5}}C zF!J+HRy#PSqDc{VFZ(!IQd}znM8SMFFOP!@<>Lj1hzwp|)|~%(IC2WKLs?N3e4@XoSYjHBDu?!HVrk1O9mDhmVoE;?KUUt^$@E*>1&Ex%wtvQ|RW6DAMt%fZV(`g^02?J|MJPlD6D*au}OEmlo(RQ@2Fw>~c# zanGCni2KHt6y6;xMqEG@-XiVm=?dGQ2{qKug@uRlM z791)nDpr@zlhmLJFLNJ_g7nel5t#vmw=;nX?lgvYzgt6a?a#<8G*~cL&d%<80g&<( zfOnJlO-ruWQ>(sSx3d10kDJHv3uQB{&%ahRr3;2XJ(Q=82}Gkpr^sViR^gBA<|~XO zHq|5J6x$U$aYL>C3H@4#@(c|cMrz~#;R4J%BX`^Wlx3%o6ViqQ7R@Nk4x@VY$O<0x z=-kBL6|!1*-&`9_1f!Co22f)x!P=@aE+mE!9n9ekJ{MmS^` zjMTpyC^X#1t94cQjOWQOI0Ia#7)o{gSD4-)<34FqHs0V#jBzH>%IEFqm!9qWZz$Gr&oI*zZjMe9##uJ8ibFZVJlzGVUM;Y z47uc2%*XM2m@`CC>ac9${%JROl4r-Na(rNY=99RSINd+Xzoiux>F+r#4Jmqpqc#*$ z?{PTtC24t+Jg5iQy{mCN)yTN_;3_y?*>=7UXMMQQpnBXG>?KzDI2y(KQZ@|0d zy`jE)UPIAU46k4wZTZGbq4hW*2S&d8iTP0c0%D9~`L^2kUASo7R!fBX&SZQ|ZFc&5(z4x~ z4B1l02h*e%v+-KvZv27Ysp5Q~YTCBEE>;~RihVAAkaSangn*V+g66gSSDO8mDTaw( zm5|d^PD6xSPTKEd77GClAP;tthtqCbB-a>!6krX1@i6|+3I+?jKWf-R9tnkr5z%-M z{>!C$=pUd)3`|{Q2C5+3;-2kf-XO2j_Z4*x{Tqa_%X5@?JG zsLUikKrgf z9(kBBD>hVrwu$&tl6cd_-`ZqK_l#BR)O$2Gyj6^s5-Mm$%$(~NllqU2^R45cd;R!v z^uLH58glB4@IIU>k^o7u`}yN9gVY`QbXxY^LjgZbb>2#E&dWRsB2H|oLm&|1@8&0o zQjDdSQK^%=;qN|ZuyfcTrHflu-Ep0m9oN(eIHHot;ruo&Ub{TIDZ$C?tA{Z92g!`f z3%3Y+Cq*iV;>BcKP>qQL^YVS?d!IYR^r6JS6}`Mg?<9Ovlw4GUXSdK|mt>@pc_)xD%9BTk(krdy&2@bl%fN<0d)F9N;`;O=Pj#J@A zJQAm&Up+?%BwezEHP@>vNTu=2$mK6-#phfkkfP-drlli-`~z9_OSFIUi++-(N$)v4 z`Z}k=*F)G*b4QAxB#8r4;wg728wi$w*erT+>0{GPvZic`*kk_M`-gm@W-Ih5bonVj zE=A=egDynG{Hgj+WVTo@!R9p`&qwG`E4nf)3IC99Ld|)_Avyh`oUm0mn0kk5!x;he zNW4H8G@VD%ibT zA{#G1#I;OleSiCx%zaD_bc&Y+ErXMebaTqN(a8hY(+>J@{U6X6?v`(v*=l_?8OJbG z^&nYSn@0I}txfQwi^O$&UWB1`pBlfli>tk?4O%kuw33A~cC}t?{kK2Iwi5%+>&`PK z+$Gc)?(_AlK9h3#D&77suJab#t?aNW=H{YntXn!=sAbCc+*fUa>K#N9 zsMxx)3M#!g!WYUl=0*&I18%!<5d4ysCpS!AKs9jCRp9nknUxlSvK8p=xWHDNe!;0L zgSs0cx>?-J*KD>%;^VNvs)eRF6O|7QB>RlNeg9Kl?vY2^XWl?7El<-X(f;bHNPZ@FdL6ATH-R4pz1!|s!GCInKpk7_A0 zUHhy=Sz2M9z>3ve@%N{xhA?`AR>p%Pcnbhp6QtbzO+1c8_tkLu;#B`2Lik*fXAg?L zVv!3Z06UvY5tp(R{eGK1nc|lFy{(ta^ePsY!Xtj@LSH$H*d8d4CiFpH%QW)X8zI$v z_Lum^bA-zVASKWg5^|-+&6@gj-n?jIlQ+!r9fI?ULbks{H1J*E!a-aauCC=D9zE%_ zGOM@l65_sRtGnqffhvD2p(%@P!B0=eMqfsZd6&gH8eU8ue@OI7W z(4`|i<~_uX-r?l;C|w4eVX&BZ82;e!$F2h5(7)P?lhq;r%JK@1aPXj~0yNX-sTGC< z(3k`v`2W=l2hv?2-2M)AoxS?Z$>N+|^bL2j%+d;Xlwu?10HsCyi5TzCX@^4|?@Vm? z`TG}+CXn}VVLU4oMq$QiM%|}F(7@atk#Fd7G3SI<>56T}f2|moZ_GNvt01_Rk!;`b zCA)aQO0uRpY>(qL(lXqe_+J84{ub3FeHgZ}0=X{X$Q8;eEnQ;Ti^E#Un18+QoAz`j zvj3?EF=^Eb+rPb09;vw$w_UVzdLxW4+zzqwO2IN|3L}(n_j}Hi9N(lRb9MElo96Sa zyQZru5|+f_DF zo@vF!>jL+pp;K6)CUh=Jw+t0~20YUPYV8a8*yvntP6dq18h zm-iOqGS?&%X}x}@0D$TK#EY!gnx2fEOdEeo=Xzma6v0I{;xkD}#?7BM`Qm1;X|flo zH)(eF0iC%x5yqI9j+{h+@3_Y01BvpeS$Ztw%iK*krkP+?iZ zI(3TOX~F+Me|t11p)n_5gA#t;d;HwX5G?-HkE21ot5ff4C@D-_-pmax`aiGa1zc}J z#X1IWoyI$}{(b6Sb>}G0KXGaczDofQhBk`eZ9D8O7zrGQXO)7eUz${!1wk4G!YNwY z?ju@oz1~VCXgqreibMP*t(G&~(>~s)7{fs^pn-3D^%}W_cJW!g!2#s~ROCt~%#L?G z`jrK`k})yZE7^bLB6P^S>;PVi-gqi4=Rnk7XsuWjg4{YoLQkg}PE?+cTr~AG<8&KM zxpL4#EB^RTS(aB@xD6APi`>>=YoNNpZz+Sh)TBNzfAqTufNxBm!_myU+r_O?o}bm0^HC=JV_Ql1HUZRTdONt0jp?COhi8r zBnf`w-QE}pVmfh-Ym7G5l~rnR9ZL^?-3^m|0>RFL7ii+*!CN;DdjhKLCW3kPEaJKj zN}rk((Vgi0PJu`^`ZHfSHI6qRbM^V@8jILo)bMWU1&dLs2bypvB`1Kanx!ievs9KK zHj0=v^YlW<^7iqrrBq>wf+i$BuYwvH@bj$D0Hzk9x} z?P_7LVmE6EqdG1CU@ue5n&;OIqpXYb3NMS?wWlVa_mpA^j zocd}Q*?A~qd6y|h^2_?V?D;X=Z}8dHpvc54oT3rfV@Xn7W)X;YzY4gP)6r`!U-#3O zrlSti4Nr5)8#?LSS-CO#eY#nqV~VpN^9E-EE=O4}qDEWmJdLye%NcKELYNMN+jeef z$3(D}Bk~Nm_BA_L&x#wh$B~##;FkB?oo@Ow?R(J5h8R{8-0H3IOOe0 zLBy!HTgOYHv(@o1#&;c{Gd7CC%D27IVBI<4Ph^t zD4fKrk*f9wsV=iX@ih1M&ZI3WJ~-CB z7+=BvV6p=};fKrWHu*&FH<9L>>Ax4}Vk%s(ul81d zYt@5)!jGWVxx7tnu>F+u^sMR=01cO^hTc36f+A=dDWkNGaSNjNi7T9R_;0K&@--U$M!FkW(wrgQ)!F4Q(X%QRss?2_6Z} z-Mg?@GtQR*1WOnI^mN5U_9b)R_axpG1@Tj)tU9y3(7%-Uvj3~ZtLoukjF&brr#eB{ z5n&B^G4be&{B&G20GKI^JdJo1F7|R;U_VtXl!8@OBlYZUdzW2R+YB1N;umjZ*K{hq z?&0Nel71j~XVEXqn$HesMM-#H?#8;!y5oYl+#AW-&v=V0OvcM@IQ7G2c>x)y=2}B0 zs!OcHVQU@MF7%in1Ij-*csT$(!jA(piTIAm;a@{L`%PnSX|nT1J1J2K4j0KhYAsI5 z_!ghoP4KRt0y1=Bk94YqMX9N!sT7dslace`QpjP1-pyC?>N;A5XYfh`%gekv?8}Ex1y5oi= z$|k(|pA8!f)q`2_9?g3`de!!r2LTmZ$F|L5b5B<1P9de^))%9Fro#s|YYwyHLAi&I z=6S4jGjqF{Q)n(v!`B><0T+WRzk+(uFzs199Xlpo zl=zcYSpL2<7bowi)Imta{a+A$EeZ>Dn^0CzH{Yl3r|iSjT=B z`s$ro)nEWGzzIN|%%bvsR|dlh^)Rj4)8`>CN!m$9F9{EkpXz0 zcl^`|Mmb zGCI~pIY&rPTavWLYrsJ{ipM#Q^PmNs+-ixSrzk<%2okMWW<{XYG;LJOi7nb=y;nLU zfzkKeOGGAnnlwSGY!EbK0&e)~IR^FT*HtvtbgiV@utq%@7V|jzwQ)XrGa}+vqFu%e zXBJ1O_@x-y&d*uH5yKS_IduLgoLpp^ypb?e+6sJi&Wt}9>qMi7NXOmEj*JwsE#$SX zCPr28*3N?&S`%pr4EF5P%AvY@%njZPEJBmNvhme~nk~G=?IC{JS>AJY=axBTJxz^ew-(=k)r#h|? z-H93c9wNo+{zG+!#v3a83LreS%|L_MWDg4h!%iF53~kMf5$L%WMy0~$W3IH1Ol&HRw?B^+}$8u3!qD$HXQ(n zAAT{W_4XK95PSBkx71(X$Yy=?P+DRc2TlF48qRcvQ#2AYU zZawV^vnKiRA>kG&rnkOB?cL9I6XMerHKtIqe*w3r-^7Ps)#g181(`Sct@{E#fs341 zdl~_U>ta;BSmbw}NV5$pK=eKEZf*UWcW}UlGSi%14C5V&r+(v9F4Fv?c;$oS<5WLR zV4oanFB-rL>26`=I0%&ul7mQVG_RY@j!Ys)v=vPJAX%vC_xYLCQ>xpl|0sX%@Q}^8 zf5vSSfOqR#E6RlW+6}2Y!uth-;h4mQMp=AW?F~cVLyx>&sL&MZaPA(AD48D|Duw`iV55p77i(oWitbXlKizt* z%8bufN{{>Z@bm~Sek-VMjbwPhf&i$)z(HOJ|$1V7LupOC2O3qF3KjbxEyhsTjYAUEGvm_?`{j{lP}zSH|+#6bU=7Uv!1#vIrsz zmFj6?III;TD}AnPnh~kyLim=H(}6Cg3v5mQg@2gV@VMjL_;UGkS0>ima6cynV2}wj zrXAfuk_oH3^1sTTi(P%9zi(%~AnF*-84Rzlb~H{7dpDO8x6mo7%XJ%8=3WhZFm?S( zmuCL^PE&nT;z;YACFz>^Oox@dUSu=EC3R_OTaiLLecgtz$W*a6DY}A~llZ3Nf?cX^ zEA6-WoW)n=vI7(visCc>SAVlk_ED$G2=VObBjy}SdUjN9p|0LyPEZ1;$~XZwJ@Yw&5J6eatM z?}kXd4Gc!H761?e#M64ry}}WoL)wq%)!8HhC3gcm0v?rXoU^OGwVF&ZilayYV*FAt zoPi+KAUqU9dN}$WW`hDC5_!{HO`-D+F7Q42Gpnr21$08O7?-S8PIAM=nf#G%L}f*u zV?qTu{!b;Jk8woR>loh%Sz#W18xF{7^jP)x-0+xR5mhv4KsxnU!ALj#2G~o+kZRfa za&HsT&IHpT=UA#Y0{~Y8q)QL~s;QAou}}mh*Yy%odok1sn}=LrSPJo2!-#fKf*kEC zD;wwj$f+JZD#MQ^+1#efG>yAeIlas8+gfxxrK^WXa5r;2Ku$Cq;wjzSHO#18qPxdeQWK2pqn!hGKYBDyM(`noK^z!+&7%PU+H|yE`~+iC$#!< zz^GZBEFqQQVk!G9IBNg}5p zQk2NGrg%SlD1hB$sjIZuWe%2eY3u%)q_HY`fp2zBP)5RMScrT#{#?^u{ zwQxj^Ft$xspByjQkE(2g$GmiL**=xbe>-q94DP7xMYH}aO`V5*-Om_mKi__g(cKrE zPx6%ZUG=@kADu+yczy@%H!^*W5P1-b+IN!?u)MJRqWRo5cYrc(niWcNd%u0>DiUzz zaG|nCR7*}zk0rdZ@5OH+<@mwJvI|$B8FtR+mPCJka{leT&;H+8p?;O(sQjfAw$3Kr zt7Evz{?Z|*$k{FXnRMeX5!`)X^k8A9w!IiVIFWFVfj|O8;;(NQa;tz*?k_v~8#!wYrgLt~wucUX&;(sPjVY0@XFbQyA!=9uU?>3qfpqQe0#p2E zxcd|4O$-qR@|#SdwZ6*0)S*X?sD!WrTvWZFUI2TOdjb=57zjqfIncDD7sd0HIW#&O ziB2G0n^;P}*79^U=R06pg)G?t!WuC~-`CiJKLgU$$pS*4(ONIt-`?j2+}Wb)0IuLK zFdGybkmeUKV*j@zZ_p6V^|Vr$^vFrm`NaNG%R>0!AD*{bX{GeVpS={n$hyilk_@yfQ-_wwj4tX88wpQkWa2evPx@ zXCGshi0i)L=OpZ{rTM;5W+6Iv+?uo0WWhPkkw4F~`?3GJ7d|YTZ*j%lZ)ru}M{{g? zIs+@$x8ay`tmk*TOMFfd)>;l9u8sXGBwD{hn;hqWJXD7o9w+N2``2TPL*il$c9&lCK);mkpSg2vmE&;9NPmn}a?_G7( z%;8RbrUv~Cq3XpEX|0u}JJq|%x=`ER?3{mn)}Y}wmAlVKE^(a8$x+7-{Hj~GsSC8I zx~^6$dh7s)qb~?|f$IoDl59`8?|mYNzR0}ii?t;e2lih51!2=1Iq|qZtoiMqkMRzA z(No{?!yH5PTPBedMMK3U>6f9yEL$t)ywOTHF8;w>HtA2kMeDhG23Yya=x!&3dK>cPw_1$pU!#f?T@#D;xHsG(9d>c!)x}^I*>humgizBZ=7( zybCM6t20!X{d0`c0w$$Dk_WNP-H%zW!w1u6x{0vk9_c8E2iQ`u7HPwZgNM*B$cHHI zLN`~P8A(0W0D-b|VfNYSb%sVkW<^SmfMA7adc|hK{2y<$wG9sS7)v~SbYIzdwpvV>(aLQwaLC|OE4-(9#cnM&y?4x3H4v#9i)|N0bcQg z%(n&WCxUbnd6kIuPm5d2cC30hChQ(jfXAjOM_2?wWV4iMs>62Tz)l%*=PkT*oP9qd z$sKOQ=Jqh!KYTFm8K>A9%6bf2I-zR)s>^G&oGF`SZ?VF{kGOjJbp18ijXG&*#yWX1 z_Szn=4ZCUC!}2sd+q3?S1HY4mDkXu1mD8vT_HMH8NwMu@--M~B(Le4vErLXy!z^I( z(3pR~V}*drmxnrFqMpqOK6E!PQp|g(jeLSS?^wDMy`(s{%LhrdR^3vYW~lmUGq<|}Ansc!xE=2pwf#+sZAfu!;*f9T`>E}b=xi8fF_ zMh7om;VhZ`_M9LOY6xt-@KHXs(wtcN5_UaawCwqVcQG!;wk)a*@%PYaH|us4g5hL9 zb|?pQS|4FMN({`qiub-)g6Okn-7lD230J6)?$X2+O(UQ%4%W*8$wxCyWdJM8;qE$X zH$5y zQ;D8F)iV+2u<2Jf2AnadQmVH$*(kn*%$ zBw=CUSEJ`g?uI%SMx!fR)N~*jIxLy579y``e0B@AL)|Q$vC;cst7{`os{!|ammdLO zASNoT>jZNvFVg!hT_@E_AQGXx?XDHt0Ku_lF!QF%w5iqdM@>@3oU$WO$HJpI)M=TT zi#jNXW!Gg5|8oNTH<#?*$xL!wlwoedx2DS8A`NiPuoLo(KsX5pp=MWC z$gcu4QebC$zio-Yp)?C1*BfL=eNuXC;`6`I;P6Inz^ zez*_l)MIIdZc>sbFG&mT0L^(#ua*k|uv9C9>;e7s0~Qnw!mupp#>banhgS2Eak|x( zeN_B#@u` zXQUV<$k7J7h54zmm=$$e|5hxPR;oWL`!9MDYA#LNJ^7c@kyW*Kx`N9$Gl`m5+A3BL zjVZ+%6M|Y5t^V19a4A)4K)znCkV6m&vU=d<_j-8TndG4REt(tBAiJM8b>d|7fU{it z@FxsD42{tb(r1=uewsei-<|w+qWldAfEWbiO5#Fa?I-HXJTGjqEc~>PT?%4V-R2_n zA~uC-4VuE;yT&6lco&t&?e%_pwp*NDxzn6_>~8wo>%;*7Z-?@FxLJ1 zvVU4DKyX(;0^mG!JdK!#$;VO3A^E8_+dt$%IuGNz&6eVyd$W2DTPbO z0Te^SVIW^{Y&jzHOb5VG^GR@s?+((u-{zCbTJ?-X%GQKEz|}FK0Iz(pMj%QZp!x{u z)m53C7CrurFHzKSAceiyULODEt7Lqksk~j_Xy-1anH&C72!d5I^zAt&iNL$SV!F7w?l&CWxhQ;i*3D9oqf8l z+lpIjhqFRpz?7xo_2tC6$rD!9o)I{@6~y3-&kHgwFNrh9CrsbxgqIQ3NkV+loz5^pHIt4*t>aBe&f-zrEM9?K zuJB(pG^~sz$NE%Tsf^`-Y8&^Mi_eMIae<>dAzg$@hj@@zR1S*OelOhg(gdaSxB(-5 zzh7N7A6;MUJ-CoOTwSCo#}=&|ASqWo(n}YQ{9jJqU&B>`s8MogAO~7^)n@H~8(%4q zLSF2wQwb5-UN15D{n@}x$}X^QbK+fv}MxsSoK3r2+W1cli#VTX-sX4iL|wV*g1zJA0?DB2>m#+ z!gRHZ7#25$$w)0Hn!yD?2Y>*CvQ22z5m-$0D3>hF1S&)(7(oDsclme#s!d{4B}Nkt zH8?3NG4mlk0K-6(aE&l(uu~livCZ-oWg#>Gi1s}m*?tAvnXCXNm}1)gfGjgy$5^vZ z-uB2@1E>I${i8=sTq*3_nQY`H?1|Y#vGR2%yemRVkq?d(o>N$9_DhW}ESao#SOEM6 zy~Yp}d+dYAAm&8tYuJ*hedkEQFokFo;9K*lQ}!ehO?l8boykm8DzvsgCG)$NJ+ED~ z05~Q$#d@zK7^KdrB;mJ#Ve7&P-JI0?PNzR0@CxcsNVc~UV1>Xi${onZa#BY8kap(_-e=nF}Wx1~)X-KfaS(3gEir+@=WHvhaYa%c}8mQg>SAZk7iOW&PI zz7=~dKObfk$&0i%oUcqg%=B6VKL6kiokO4qpW7c=4s%(-DL7}GMS$xn$*W-O?Fh$3 zVxIn%o0e1W8gHavfgL3~`2Em^)a0v`qdpMc$fb?|%6GM{ALFiBDHEBfpCqq|$7P*F zx6qrf8h>gDsL9U75IcVud8E`m<*+o@1;(~+d%WI&b9Bxh(aksg5!ppXuLB)pUdx%U z1frhEF?n6UHr8LDL2;v||9Sd+!Fl~~(bi`kV%p1vv*g81;+9`N>(LbKaUj9+%9&qd z0>tUK#FBF)?|XiJZo6}~INp=vY15ni^@tYa}hzHnXT z#@V1+&u?kow=a4fI@@n15&Cpy*)pbshcNED^{WxqUSU;g7+Z_?SF%ifqZYjQQ`EgA zF#9tQQcJmc^={rMIY0D&vG-O{aYfyhaN+JANYLPJ1%%*McyM=jcXxO9U_k>34i(%A zcX!tWcSu*h+xeeK;qPcRNO*`}0MY778Srfsf=?+AyE9Z-nmTTMo?f%W>A6OmuRP_}1|s}6w5K^38he1@%62bDO~r6(T$ zaONWTv`CM3D&!pN+{3PhD6)*%sKJR4{d;WGaI^?>>q?c3iqHfa)imEqV&`^^PEFDa z;3A7DD>T_5wfFZ3CJwPe>)1}s#{*30Do@G(hg}FTHsOujqS|WMJg?Bm#Om?>rNCAv zKd7aWE-CQ#l-3A@Sgdd-Rs~@0y;cOsJw7 z^86b#lv@>^n!lT0XpYi^1p$$+HQEKq>S>fm_&1j9aiwgMN)#;e?rJ$5L0J^gMDd!! z?HXN51^z(PeBXu5d;$$1ULX4&^hOvQ%=yT5ZGkz#zDMU#)~JG1B&=K(-J&PXMU+To z=n!wM*~sN~;J9s->f{?o#o|EyV|DlF5{=LKPJ!0CnO|Ipv?5ftxrL-d0`gw`%uQlH_v}kohx#T zUCx%G(qm4S{5ew~67*qx94=1veNCk-0vI&b4^f(VC!y zw$zb;$MFuvV@8;p^;5YVzhnU!l!+UOEZ!u(33g@u^zsk4KY(}Q`@7H`Fk0esI31a( zP}C{8d~-Zg>(;+}?Uik&2~y0&mf$7|kk$LBwDOhov*%MQ?VYx7ynTBH^rT6Pcn?*; zR+!)kB7b4Q^^-shb3$dB8I~G;!4iI%YV9*t*>)Wu(9DvF$9+dGA~%ut_50=S5UHj# z9XpB=Z2_N=j+5|2G3Y3uRgQ{-i`Dsanyc{x${qG=^qP56Ahl#Kl;*WcgZq93#RHi+ zanGyo6(B&5CBlKr4k`?*h1;^VZUnKb6I-Ed!4fItt)?>hY0!b4%(C#Yk}6l_Rxk$9 zQ} zxEhW@)b81AH@!jPErkg%|LTs0g^S_8!zE*h$1mYAu=W;9_ZCZ9AB_~(5>L`T}ej(uLxzD6Uc6jh`Vf{sTJ zTw(wVPSbM_=xDBXUI&J(!<$cQ!2IkZ>%fEljx~wjhY%x^wkAX`0_=L!4<-ac`U%V^kAhZ8T zxP6Z^D#*zAlZ4LUFO;B`jk$LE>FGJwcZNSv4;v@?OBRDQTpCKpHTXMMZdg>@UoISy z+8`FhwWTfl1uSY3@a3Qk_XVK=e9cjULWu(J9TAMQ82ScnoPm6yzUw#VLVP2)ZBsff zT`#MWQ8StcQ+{{LDYxIHiEVyN4E$spKYzk;Uq^nu`3?NU7esy%M=$aOVZb$b85eg0 zmtwG6BU05D+o#2Lm_->1GzH7AytU_A9qVk!m;{kb(X4~KOk3Hmq%gHyHc?v@1}!(X zo}y0Y_&sL=Wi2_KOc57^b|YB2DX23Ao7oIkjs;e}(McP~Z!s=Q`057%zxi9gIs{Yb z)r)te36%SkrKzgG2M~N4t?gxl(mHgSE^2Oa_E-%CGInNzubMi={?@< zd$odYKmz7eeJxdbNZTV|dbZ@|goCJIoSYUvom7)Tg0K$g?9xc4;ehqb?NZ}(lK-Bb?24ijLM3vq5xv;#`j zyKW*8S`BvGQh9(D{85OXf+7}C<_*T-3r#ng8O#DIWZ8M3c+UL$*0(qoGx!$uxNtqb zu1E?RBN-fhe9-upy4fL}lb|(r%pSY_O#ULI>>I?MW7IF7 zIj7qtawXQIOc;zd!z*&upnH^`W&U!b&--JmpZpHuiS&YiBjbC5j=$^<;{E5KJ8&sp zN8W5MsJPbIs6(n#+k?fTE}h-4l85LUEXLfdK(HD4nwFaX{DYZ=GRrdo7=|MSgTL z(}y4~W{lA9%$Ro0f4lBmdUW~W(vIZQj58iP9MEM6H+kb_;76YamdWs+LSW@&q(b;uGb++Cq_S15ThF0+f-#0I=i=Fa%9!51N zmQPu=I=9%-vy`J@1?IlF9LLJLUla}?9%v5CNdB_o?%;}8;+f%$Etw98QX6f?X-EZB zlI!`iwUj+BOF5@E+90;>l~AmQxGnDv&5Ap|7ZpO>b(%(>m=knljr`2evfdaq39j`D z?;#zm^lpUE4LVf?!KBKU4s&&N=58!KA#}*@jAiWq>dBVENJhiH*->1ag8AiXjpNj? z@Zo+ex0L%QAXR%HZasbUcWQ|=`d9z^H5R*SXqwN28Vs-3Cnz8--S3?1hH3y&uNKeFIOGh60Z% zm#D(pBhhjtE<;5z`bZqo9?*%|i$Ezhtxv%`Fte~3a7Gvre6%D0LUd;U75}8Ug3+iy z*5Mq0qT6Eo3^(o-T2~@;%VZgv=WKdL9Z57H_>U@VQQRwov?TJ0wd=(NZmd+0rY-`l zpz-~EusuvG39nt&7lB41ScF+*pme>G*9Xh)Bqr$Y3?C*4CkXlwO9bo1=&^!1r|52Q z&c!zbyfOV5==X>0IhY=lbeqW~j*ik11|5!CsLUgnUEyUoZIi9d+0evz;=~2gxMHVLABC{iD z8yh4U9nq0T|BTbz_%IRSV=ydtE`kSU*RGc?@OP z#dpt@A54SFu{_c${)iWVpB*ltWA#d#5IEvO>*nwhVf8xIpHC}u9>H*Ih(P4Miw$*K zBt@DbFTZG`pD2^!fTBJ_FZv!|_-nzZjD6tjECInLINs}KGX=+YkG>X_NpS)nt`Q|o zjBf7YTc#*Kv9Y(EbQ-Q_4(jDuw^OE2HA^pY}?dHmJ2fEhy9lE(0JU`bu> zx?EhBxh8VPX7I>u)WcgRw#!If+55RpJLK3Bb+GP&Hoy2pUrjHQ*wsRm@4LA#fo>8n zxxOd53f}YSYJU}2aqeeig0%Q|V#ph57zF9F)|E~YSKzjEd6Uebb}(n`|8)Ro)7t-q z<5A<0DsW3+$tXv3Knq&M`|}&r6|g6Gwa)=(60@UFRtMD_-Ob-_9@84rrBxlk0IR*2 zp8d_HSqC`O*}ulU?DOtgtX3PX8A*fdch%jT3Zq#N9VR?Q1zSK%vb2$G+~}XU658y! zqe<#&n8RM71o>uryyjXX-|?K$u~i`2QbF~$`HxF%*0U0*0QdItWPF7^vJ?Es^36b1 zIS&1J4X-8Kd(z@0g1@@NEhU5a)1#L5lX4a5%;0Kv2dXYi7)L5C;$5SptqA0MVR>W# zTF?i;$5zUnP*7YK92jr|;{^kc3>VV1i@wT#n1{!8ffDBqPa#_2H-|td+f9XTFK^U{ zv@f@8h4hvn@-Y>&^!W_;s>hWi<1vAdTi!LSF2Y4z`*sX+ z_D<-vLlTwst~}fQBewr;y+T{GZ%X z)81bLu6nci=eFT9l8{mo;EJ}rzpa+Wk$1D}nNBM^;q^hDm|>^E4=>3>yxBS0Th-1cDca+8hl)Q!OZw^?9E$! zACO@R*AexB(-?C-;N_H=j-8^}44v8p=LI<|WuW`KjRfzwF_LWypW5&D5IpEdx2@Ab zn{|m3^cZ=EyMjShzxPHDwJi^4)}L=gg9adS$84p*b@ucZb29Vk1V(EwGxjdbi;OUcY;OLdI zIIQu4C`J%=!<6>mQv<1;qVu-Jv+0KE}WCSppL%Tp^ zk3qXOnaJap`fhZL{R5X2c4rV(`|GO}+KM~0!qPjV12X9s%sc!m>AzEi{Jtn2L=N>1 z|L7uZ$#OHlQNE$_6dIzg$1;_fZ#y;0G@pQCs8PG0Ol!6S9quok*mgX^ zCdZxeus2ICvlPyZ2|Fc$3Yf0PybSILB$bq~`$VJ7GeUunD@(0YaKrR2TysO`((i()2X9L= zC)R?gl|A&g5Fg!%y<+p?jAFZkn6-cQ6Q97&(NIyLo62JY(D*wLc+n$XhL8Uo{~s;@ z=BKkQ9|u#;pmnh{v3lXG?%sNtZajIbRxGMk#HB0=nGcp_-Q7JrDjpyEBeg`xx&gQ@ zC2JCkL*#*Wu;l<6zSYQpU0CAK(QkRav2M9xq?8jDDA$Ody$urr)g)6Q%TBtIB1A!W zCG;MsFmu$mFjHH86iXEg22|p4ENe0_3zwhL^iEQUf|^$Q7#O_W2?1E_5to3Z1Tw68 z<0w9cE%I0FsYK2T&p9{?kHI*S8)yl!te?p(SYxD}LtON5o6)}2uDgJ4~TiOFgK;>!?+F^p-5PY^kPu{E6S(caMHXj`&%ZBf)=t38dxu`SVU5(aP;CV zZ!S3L4ASRCOZT=tZ2K38R%pxy!fhGBTqy#HKU!E%T7v1*u|mb(1$ixa~vHs4NxI&|HJ(wJ)lH5#VaHhUmqHazT zx8m%;1J9?wR+crs_978nw$T%0?RFNec}}FQ0f2dVp*k&{WVtT?0|_6Or?<48vC08% z*(s80j-I+SZ&#*Mvo+5?F^dfi9t8r5?u{3-qGV$)6qt|k_2ib7aNa`grv2J(ka|Pf zcyBz?4#PqNw;N-m>AynHDV=|fNgUlYNmd7s&;Th4jeA$qL7(YD4_&uGR zJ}UgqwEWr7z-I(0ZLNegf#^?k{o=G_$$#vJ79wb#D~9^+ zy$eDTSyZn}gCYeuaYfkIhm*kXX}P@ioM+(x3gRsQE+1UP7HtYMKTc~5ZP0RT2U@&d9EK%R`4s1Xl6Q5C1S3B?`d0geKy+wq}i zAL|@$>1e<~4-n|Y6&Cvex1a=*T^*VlQ;5nb2(g1EeYc7Ul?6ggO~0!nj9(veE5hT$9`hs4?Vz1Z5}TiBLXK$}|2(lB4g& zGVi&?x$1F+Is};i+mRj7gyc>rM}O9cCq0rW%Dr09winR^D(qN^LROa*(i0+3Hg8ex zzfus0l>9dL##Wi~7=wbLLh9pN@g-Iy7aILA(13&!8>TF&DX1crFCrhgdr2)`gO+VF zgClAn`SpcMR@0@8EILJBEriUTQ!U0Xj0~AFbFzceOSJ`ZR|tYC`k;Zc2~bi8-(XC` z^6vHrwy$MCcAtgiX`i{ z)k!k&wtZ*62L5<ux9 z2@;Bytr7krhXQ9;CArn96wX}*kuoh{o`4D1qxBn{ z`z>vN@ePj1oIRBfbkcD!PhPl3xxQnS&ecOX7#F%;D9Md)=>}geH~h8yBLFC&Z@-XZ zOA@vBq)gDO@>VcF8JoD z3)tbBy?VBO{&D#%y#)u(v*Q!j=2&jFqXBgx(E6zYC}THvGtghiJ3_B*LQ+gYpLM9f ztlU~eSRfJ`gqQ780K92B?~3Med<(L@+%QUMFu9`DkgE;%F|oQzWfU=Ff4aBN4-Cff zKM=V_jN}kO&e`1G$xx#4^#FduCoQ^IKhz?lByi2mWvCJ=Oe#;5S1&Pj{w+E?6X>}Z z#?eq0Vmf>yqPOqAZ zgyP)fb-&{aW|M1L*7J}Dn`m{Z)Xs1Gb5~J=>bD2fM}s z5nJE|KUY>I51cuy<=#`kZPAzJ`Z}~4*qfks z^g8HNFl3uIc*z^;>5mH}7aB8xiTN9BZ3|Re%|%E2DUhdkP9U!V-nyC)7Dn zcu~a7dRgT-d#>v0DwG={G{gH*P>&}0lWSp%r}n_WqJL-SHxJ#~2D-QqYt`fG!q6Sx zbnyA>J8`!Xavf&clkWvoW9-8FziCMJh(Bwk7{v6v!bswn)>;2StHb@LCC=!(o}M83qVm>Ss| z7LUE}n_O@U;a%80p?hd5T?!>ETmkX`6GAP{E3M^+dquOW15eMiQ#k5oKX}Kpe0{3P z+^I1}So*j3k+uq$l5Nv|vIA8jgJcz7Sh_i!Y)n2sRKD_6C|24q>x5W8W83>&;M=F% zWpGq@+bVzK)r;P%1=PyZ7p`Umw)CZ>S5st;5!}AELaSnGr7Igx{XXNOz-)3F)vOI> zUJF#opZ8Z{PU)5F@Vpv5gg)*cBIbN7qMZL%oVZgEruJY!sW;;3Y$E`}o9gP_<- z^hrZKqJqv+>m^*iA&739ta1`KPUhXd7&R9N^@A9#PbeaW3xX_13JjGu{)u5hT#|*= zQoXT*H9<(}@d~1r;XvFs!Op0BZPI-+>MkZUoJ{xBTY3fmxIgtXY(0&YjWY#7UD7En zg)-N?aN_>9LXOEY8;vkAQcTIjO%$x4(E+JU+d3MIvX?a&rU{vFZ|$Sjg3A2OZe=&_fsC> zrSX07!tcqVBo+}r2`>yTe~MXuL3clc+$2*OsHiE;{G4tw_k8aa2-1A;@1w%(v|#`B>zvEri;SQJFdc)uDYG-Xy)$3fuD ziWeRkw)b~|=NLU}-J$Wj{m*j$YtV-BlIgSn2%hSAUYy!yiCv&xuhH(~ONT9%S;>BG zJ6nlI>LX1unD!{*r~je@@E-RE_l)l=x)xRR>etjZr|D0pTXmt!*rELj2RQ7 z+u-X7S2S&<_Yyb^kg=q(i(xaBa+Hfhweq~&o;oIJc&MLgCyaaiR%gBPz>~_&g9*O5EwnTHAS7@K*+C33s+j+K!XixiRzzsxfHeZWF7 zoU0d~FV0vhi%H9WEx%DAx95=f#+Yy>c@h*NC>S;@rU@ti-G85-bSq(kX{&f4ciJDf zX^5CKviFu~>$sDS>YIJPSk)*Kn8U$L0Z`?hNn`f~-z%b6+k`NCEskERwT(w!e*@*< z`zJJ)nJ));Q+DX86lpZ}SLO!_CRZBU`sW&3QGv?W%|rUvwB>$Z`Khz}yoES$4QvFk z5rJ*EPqidyn`N&sU$W_2Vo=H`MQULh?c{>D>tp4Dq;75c_K77` zK3ebf-R+fk9^9Xae11LC)jYGIMe7pc z0x?uZn6k+Uuxf4ky8|oIVvpYOqW+Y(7ETta+gt}& ze7A-(taE@1{4-e5Ok4!dZm6fG7CcNzcQpkulK!4>X+T8lxbQUcy+iN`eB7xMewobP zynKGq&9I1U*vx!$dUCs(T2JXpCt+@jlMvlu^@V^Vg!v6$Ehda3-XS6~_m%)YQ6|4- zl+Igx5RViODgNDD_y&=?Yce}%Fo8@VyzcOo;;xwUs#V*2##1Ta5X(E&U4C>VQf~8E zmF6%(lnEcSCIS1q0^UlW>w|{i9Ad!JA@UlZKa<4nXDa_7&i&*%QrbA&WV%5AR+hQi z;{8&l+JQ8E)4uQboZby++Gu~uj7a}<{q2-VPzV;Vn!>3}-97;E$eYF6r86X<`75>c z(Z8cH{2D64ZqFzV>rP^i3(@T_sN*gKt}uT!^?(}-COv~ROwLv3zki+-&93r}q6l?l!{DRf09goK)u(!GDV zT=-*;jK*64&L_LNl)_+VZXaR!$`YuwDaXCpMY%iIA}q$l0r_ilf{GRoveB&jS7Rs04Js7UOHe2lsKG;U0;#5PEOY8v$7jNN#li|Krs2>utRW5l%}S z?z@%@nKeR8A*2({d0g3qoWE>wkB=1nMpf@Qt4rl#qNZjf>AJ%)@Qg1*?O{wikxzr2 zD4gUsUI=HeyhRGIv7JlU|Kc|n3Q_+B@?gEk;m2j38N5ATCCcHof2Z*cZ9mk*sOuxt zb%Ntd2UZ~JX2!z#;DIgP=oyKvYpc~iE1$Ie^1E6#{k*QI+c#KTK8{S23bc&O$X=$Ir+;_t zb5PQ@clZq}bfamKXoY&F2qKfhl<-lsUyirQ`xM3^2#~pS)zf#rQj8T9Byx5fypMN|SAD>0a7%T(M$As?HMY8r=CW=oSH; z`%G4yT>o0*q0b$=$&r^e&&nZq`&mC%XZJC+Uz!#&a`Jla=UmB^Crow$?;+|uw#npRTf}FlI8y!| z;whHvIyU7F7jzONLD7NJdUWK{`q9K+(+9Sz9AejDN!}+m>#Hy>u>^6^jWfLR#NW`O z$Zw1|7SNC97Cc&_g4-+=qpecE@M)pqQW?osOK-LN=&ucH!l2^&3)@dBIsIMmq!2(e zSG{lYz~Lh^LZB~?$F=DxMxzzUZ^hS~8^GO7*K2eZ^oijG(J*21IDX@u_`)EG1R$evy!_qq4W|0ud^;`txIgVUeuI))IpIVbso+o#DEPh!|n~6YH=+#R||Eu#<8EV9PxLVjFKI99YfV8?4nP!RtH8O_c!pM z`)zYNM@GLVMQOiM>phxeJhKpFDc0ZE5KT;o-aMoARMl@7J}kW87HcrtgaqeeP3r40kM5ShO zASK|^W|buF$4EY=^(fl%N;*8_=k1(&9LP$Je!TJdcd3i}KzH^faf0}oua8{)2VZ#; zFvEtGq$%w<=K>jc>-KNjAc|_9Beo$4CfMlb~&+56x)2tmwWgQf(5#a z4qdDSJ5n;{!)7HZZ?{gr?hpU1#YQoD9%0$;xGp4G5o~yI6ykAw=*)WHvs$SSX7I+_ zDSD1O7j{CnVAG=4^FrltPw?cwC99X{WW!osXj0|v_WYaYW<6;|^;M1bk(lSF1E1`OR4=LS~4h}HDrb^N)_ykC< zA=Lv9bBP!y5+LlZn39Y-5qR85-@q^^Xn1QJ1CRp5R^C*nd ze1aNXBqU_mr^f0m(($~g0^oJ#AF*^6SFu=<*YZ_%+2^Wr5a$_>OB13Jmh895LBEe$ zKD4S+dzsv1ax3ssdOK)jiHW4kQR+rGa{HJ)82Dy#_ss;L;YmM<-i$DI!x2a@T+$px2gguPmsa7K=g33*`+aZ{gvzAILWS%l>|IO437t2M9;euT-KPZcNzVmS<#at3 z^6I3MorXo^I6P5nG{Ta}wMT`jFgJKvTE{mC{pL7w+9z1(M96#(m8K*KMiqmaeoW}9 zJfs$ve^Ka*s@SQSq8%$lEd}{~GXKm(^x5XH(@w)zi+yw&r=e--j|A@)`A`+A$it-= zDwT;q`n9EaAxj^JEO#PMN%CxLtSTaGiKGfg-)Fycv|Li6WLEKK1_Gb9rhVP*jU5ze z@`e?|Oi56jF3bqfK?Ud?s?~iHCG*CM$tMr@HZp5fy~e1Uh4I0773);Fyq!VA?{IlI zL-Ez;{1;`Dc{#u^WTV7c(frSj;#)P4GMraP#San7Chh%sJ{|%=Hzqu2R)@B2` z*^1}VW%hBumDl8pWcI5lJ_N(&vB1rF1jrT{!gi`2_YlwP}( zVO^ZZG|;MqI9l1?)w4f=A%8L76Try#%Mc=^{T}*gu~+*r#R=>1Vg={-E1KIOD?)@P zJuWIDP1gCM@7EFGTx*Py0?A|dmoUh2^?jdx;a@b4@T@7)UC|00`bucj3HekrK6LYg zHB(4ISQQRhDzMN50%V81qr#TYmGw<_+B)}o((>y)7J0Nurjplyq$)?+5^xSc_XP88 zW&()6rg-HEJhM~zLBeWcbn-CKb08DxJ*lTYoJ!FUyzl@6^`tFzg)wZJ?>hT#6~SGq zjAi1tvLp}Qgfzw%X;qinNm2K-CN~3Kb4CNwhoWRc9LJo;o(PE zVde+M>=}74#KYyn6&z!|M>!<5*y{?SuA32)e^DD1j#MsG8?QXD?h@#i*@KK{YU%h4+|%ABBv*k>66Px zY4E#f95dZck-u>}97**e%CExDzR8Dsp|{EQ#~09X=0vvlkD&axc>QcCI%L5h6rEd> zOXD;d(1KB5=zV(mM;O z3~c@WHCTilvV75^*%wRxN&@K4#TI}Z+}M{%o#4}!JbR%CbAm4R96#S{JNI3@%1nf5 z#LAG<{vhnSRKgEX6ylf>R$`n9Gf`wK0R$3JM7ySkf`$h+C6}uN5*Vw~N-{JiS=DT+@pdaj&m#n|4-PVO3AIbd!nIH_Q$j>e|r?kx0Cm~B=?t1THg)0=!y zjf{)SBs!UJC_YOYAI8hf)q?Es_l7O22?I-nGtoLSWhR+)NitO?%lmNI3wsZom$9go`UiS&*oeA%CWaBl zK^_{IP>Oo+kSZEOhR@B5&ju52y!vpF3F=J0<6~0xrEo|6!E^xBF%Sn`%-N^yNw5~v zprqbttNJa<+kWJ6`7W{hllhg#z*{DOYP`m*9(R$&0-hyECg;a%Q#D<8-|Iw}G2BG+ zj4^?SQawc58|K>er0_!eMA+#kz_m0v@M9+*E=caf2kbSqYP7ucIO~G`!-D8A8mDWl zH%wd3makWu)G`B@*(;l<%3t*8su8mE z&&;EX+v@-KJ$#IqcCT5AR|+e&yUjw7L|l^M;)y<;(Fu7Zpq3~(ukE>PzogCV4w<)FynW)Ua|>HG~+^;-)>{y`=Al$OeW7(v<6 zNxc^@*h&Aax1ptNUXilfWpmtN<){=~CGU{!BV( ze=H!b4PGi}KW+#>Z6pjUzi~E{C1(T=rs|G;kA*GO9RRO2@n1h_O?aV0XEW;Vz48D0 z4}>%kMylHw-b?k^3CZcwI2bUht>wlAoH2&a0`srI=m}Co8~Jz5f71w$qaY$NOeD^h zLRsJY?tw@ISY}J(>G3t0NF1Z9gMaPD0oXkLUGt1%#NR+}${Gy)+~w#-UP=U{0-re! zaEW0Sz?Z~E@MKa!z;sMc01-JMa(!O7B+!-my;Kk+_g}kQ|Aq{`=D!)T*7cxi5RMZu z%!Zl(wDW#i6i~|%u8+mL5koEauiZ)H3zH$}HTC2$8C?H$=M1h7aB)Ge$n4(#yKCeB zV-%=y{%|HB1fNNqtqAV;1=}h zcPpF&_)-eZ@yB06orOLDo{ zo^%bGo@@MLD`Lh(+SH^s45fI}=k`S1gcv&bL=)=Uemte_R_EOj+pLULUzrG*K{PZp zIk4;P$^2ATM=W)8)108L43A_E-c(0egRc~iH$G7hktH|tmKaRhSCD7K_C#j z*)X=%Y4?LN=$b^Def`?F6wc_)MeV>n7{_Vp zzBZ!3o2+#UMIbV@^nS6FpS)_AZ=Cq|&;Z{Sc+&R946hEXUq&uvZMC3fsud_d@29C6 zK?`Ag>@11jdU?F2{QOy;j5llD)GvxR)9r$ijeoep>HaHn?dTDc=kEweDm5NO#sQa1Bkrg1szIPT_LjEwlMCKA7He zc`9e{`Ig###%8X9lGo4ma3Y*hy{h|QJoojthat^?=4u^HZNdmD~3D%&y{(2=W`?co>i}IG=F@lFzNG^!YaJ)SWvB@* z*h}w;KX=dUbz{_2{eRVv3Yn^77eOYH4Tkw#T$^pcVPHhjEM)=8)lt3vLm&rwBH$?HU zTB8;fk517I0vrG5pNeyn$aE`(Ldf*ZR17mQlqQIIQEIMXK2hj=8V?4=&3Q{V`$Jtz zECXaA^Je`)`4I5bA)u-JE80ouVJp<&&o%cqnH1$5ewwL0F6fKL=zYx-;18+(CY1K) z_2qif_;P2U7q^6)lox7V*;guRYJH0g8~@sc>BZpKni3mh(=vmftQx<0ntO;?gK2eb z{GpbGC9ySKkK2v7`<5@@j{D5f;9mnS#1s~KKI39f;qd9AdrXSNoDo#{Adr%ww~mH{ z7to`oxd8S)W^p8CW-3iYE7pE9H;HAeMEp2nTDEban2{t9pWB@vq!VoRvWjk{l*M#cY)x>fbdjZS0BLtXS2Seb%cWZ$whoY&aO#we%90zMc=}o-TxzwfdL` zjt{F~z6Vj>N^MktrQ+YES0LFxc>LKoPtQs(KUXt=jwPJB#;5X63fiwr{3zOBXiaTR z{aWUV&L>K(POP#4#k*X#Gj$VlbxfM=FXNel*?-lj)fdw9eC+$6au;O z=#@jzqo}$vZv|ZTqH+@}fB11Rj~}2`-)j$yYX&0q-hZXB>>SRWzdViA%DTzh|7 zppiWZ4ow%jmsA1IE7_bL7xf)oA1J-At>GE$BMfw6QXuu)HbKz+_8WzV-xYOeCav0L zW{$$OpmAr=nHG{|BV;{aleNGSxDTZ{?^$xgYh53h)1*aoXJrdQs7iITrM4(VIaJMc ze@(WE^4P%aSs!%=!>#M^n~&gsoxid;P|0cb4N;HcvA46nF|Vw7#4O3{2kF_%y+T z&9Yl#D~k6O`@VfDAB{JDs~+O3m#F-I?ndNY@~1Y=kSUTyN|@N%`0ID_vfJ4kNfp2O z#D!Mqg3=IX9wF@B_;bOF%CTUub@^a%7P{hHgv%8{GIR=+QyWAESG0iq4o9w3s!&l) z+_x2rgCq-AT9qs~4ywgxG~KV5{F9zO zI6Jgr{yC0Rx*K**ma$0_nVu_{6=1#kj@p7_rDuuHP_Eq+dD12l1kEz)vK5%gvF! zakRIpaj*|5h8NG;<<0TzKzCCp7!p3R7%|qGd{dfSFR)hg0G>XkMNlrfQwpOKX1 z!N!^#V1u8F#Vvb=D&f^ZZQ|H)F2NeC{$NGGe>sG}cW}I+;g!vqX4{YJh0Qm3)=Nnh z!Fi?IL#`Cbz*8&pTFPG8fTo|!|JxsMhpz~2TnLA?NKX%}*M|F$cJ+QA2h97a{1%P3 zO_vv@qE3(9m82oumz;dB*$JE; z>iDpeM#UAZ7@g7_F`p}Il~W_;$&rwgYK)#Me|i>=PF|kDf!-=`TI9jF>IYKyJg=Fk z${w$|;mTl%jgeKmpEa&BMG!-JNVYH(>%8Uux1jvM+AmKqi-z5*$mEW`qjQR8OWCOn zC|KEZeD7b&W8t!etI#`DZeC>^aO(VNU_$Se6n8TtcZ-HbXQU3RG3=Q+(Oj z{5pmBWfNm{%&|r#jWOCv!6@rouL=S0A0KY+7`hC~&TE6B(36jE)mG}!G<5HDGg z2_;-eV^yaD^Xc!7XQjiwe{*_G6OJat5U?)7ei_6E(9oov#9m8lBPp))mxO#g^y5Zb1bZ3C=U8f%6CfFAtYfjn=A~ zCvfx3lH(*U(~?BaRY4Tl(SG)bmN5#xSx}OJogDLjZ_+{2wDRy#1WPi_Kf3Zkj+cmfy4NxjK*;d<}Z8Y6S@v+ z?lo!Q;A!8vOAu{5)Hpual?(CQ(NGpD(N8P8#5!+9|HL|Iwzbg}A$kMIXkj{25b`LR z!diW=YWvZf>%fzXY8xWK#g2ovp#u&2EqOLBCz~klqMJia>= z;sF7ZEBRdyqN*NpOLId)wG@hFO5UQcKQ=)fyD6TtOMl$W0ySu8MRo z)KYQSE5bU&$7bI=|7nTnbeL;~|CwYBnY5v0w3BT(T-IBcrkB9 zWB%$On10Ey&;L*sVR+&oRFNQvE<%vr0@bi^xIDVLLZ;y7y{Q^=$;HP*?;oz6SiQA~ z+kD@kaLOwE;hg{!;>pIVaLwM10Hn519657_FE?7Jvb)oCab0fu zuN9Rt5sC&6dvXBT0oWA$a`N){&x1!tR#9k67WAluTemyhYj49J*tX9f~YW|a3%NjCoPR6-}b7^ zgZ{F_j)k1x8ldgs%?%$dafHjv#umKZ?uIenoW38U!t`}QDU4lw09E0?1!nudp>anG z@jnLe^fY@aqStqd&>>O*}DsSy@^5uMe4~7KkJPW{BF!0?RCmWg5~HzC$Jq=FL^eY_x(9NoCkH z0o?ElxUe{SD8a@6Y7!Dm40U;flRU5=&lxQn+>)N;_!Sf*{264R3iLFM;F-G^K9q)O z^Gl0r-us$!83`gO{EstbikaWbT_6@i087oMKaTHnc0%+GgONfIm zI{%Z|IfJG1<{A48Aek&7Cx>GQhQ1MY1i#{@({D4rCDlYe6Vl^8qAJV_yfEg|%WFN- z&qG-}=soC`%>*%C9D3Q=jA7%B5Ucr;)0>3CjgrP;{0Ap=43nOc&M(o+RcDgJmb@6? z@OR@B3=b1kFfzR6roxo=mew$NW6~ZQ-(#l!7AEWhnb@2t4kd;?Y|U@1=~?1(L3mrk zlK6QTz~%yqz%nv{6@$9gB3{bjVq_-x3NnL4evdk-OBrIH3CH(YFQbjvn6Qb!7gD#p z&yRs^ZO|QOGY8jyQmYHiDR`4txMW1jn!_AI5mZ++apqa1`-!KnCZ~N_G6H!v7)b)S zRGc8WvbMTL``5#kRWM3Cm5S;=ltNzIi8WicCo}5-0N%YSd2A{t12Wo8xU!{E8K9E< zZX<)RW(>c7N<#ij?PQz%A7Jw!wn-K&2K$iwjlngk13cJ=-0)W9%jW`*c1j-GejOEE zWsmTqVk1UZfagQM5=q^}%RF|%cD_8d6Z`*j1lUJd7b(W=s+OR~Yn*RyZ_7BEs=202 z0Ml)HP!T!cW&SXKreoMid079?JX`^ce%-vKU1U3l0WlcQV%ucM;{RRH8)0jKJ@P%w zmOLgA)1Jz<5$7I-;{Z{Wtn$f z3%cMmP8t9wx!L{p>a^ztk&<7rP2Y&wW0Meyti+r9&oXeibJz<)gza;hBjy?lb$SWM z_vGZPx3M$MGmu-=c;HM2o&&v8Ei9CR8 z-xuN@DcH$L{HM^S)_*5s;Qco@0)l}qjXx^qAOYB`(;e|VV1eYX;XOU$CWu+m|Ecr9 zj)bEkCD>TuFy$z?2sCg!{O{|8e1_T44F22k&EOV`{2;jh6x0gvQ5MR>!;|N6wVxk^ z*c^;!j~W0&9u^sb*bGBAQ^ z=PTFJ@}&HU+w=Z%)clC2gT1VXa&R7-1@^MQiuRT|$;kg~%hQiFuCSRz86I9UGz0Aa z+iGiFTNxqP|H*AObuF<+7G}9_-eN7Xsr+XPst9Fd9x=oIjP08Y2}6TDn9fYEsmT9_ zA~HW4e58l_4~vBV->}FY*8f04IMe?PBuxAN3~5C4ypUB##JhK*JD^c96o*4ccgia( zGp{}>M1A>obM`meB6oB$x_i&5D<}VHY=mf1)?_@2j#lggyq-=MSsRcv(1o*SvJf8K z)#wDiLb!#8HWH>h!(G{cta-3|2&3%Z=14Fz42hF!_%>SkHD!pLr=H{qghV`$aGjpGREti`ryK)ZqRF8|Dp-OP&YigOoFXKf>kf6+=3cr#e2sDcEK!2bB_tmM3Wa;^F

%k>B#vY2Ukocp2BUVSN{5$h6I3vut)F4toyTd2x_5vq4^ zZ0AH18MAg>tCrt+$a=)%Cc_~i{o9QSM;BE)t8%esU$w_BAy50l;jn?Z9@R?m!t#G;`>j3<+O?SID^Fm6@~o@?(MkG0lO9^qt>-2bj~$ z)U})3LyHeAN8+6bMCj#BQjb6XGN8dTjpnyzz%ukdwpc&wOzrei1xi$`Fg={@JjMM6Q@_d07P=ml@>8(ab zhlAE_Tz|)Ixb{G?yl7oMvzQlINch@y_0R~3ktJF)N>=YT=1qIVXJBKw+H{mH54+weeWh&`gdRi6l>GasJ@lKps+y42z1MSZb(w)Y^@4|;ALHam-U@gC( zM&8A~t?IB4WC+L14<#wqRfW?eTlVM=!CEW1@TXu~oYTkVpcW;EZNcR8k)|_)P68wF zTbp}n^% zwC@a!3#OcqzJwv_xhSQnzPAzs{~0aZSM8n#EimW0J!OTbd*WBd3(h`aC37wG7JC$e zAU_01LDEsjzfrZB;a%-m>Syb>B6QUxR$lp3Gi9ITo+DTy1JlAu!XsrGBT&bJUyT~d zwK)`{>oDqRIq7gRnr*OsIiSnPt;181{)Fk(50M(Gs~#zNHx&-D>f$b^=Tqg(lGPdF|0@;4mHH_mQQcYIS<`SOD-Tc$=zY{ z_lT0Y49$S~TNYx(d)QRn@p$&gu%j`tNyS&3GSz|SarNzmD?_yHq2Y+R3~^#iWrpC3 zTpieym)iYYafApK8*8EGqZ|*98WN|+Eud#h=z?D-M z)kFC7a(44o@UH#wd7i4aFSb~iCAj0pyN<~~^!D%OOb6*WdV8%@~@#D-%7Ml>- z2N$i)6d*X@)@Rkm*Q(7mW+dLi#pa+YBw|rD_z=E`Isqh>VvNYIH#aquNo!U#w|08( zgQC)6A1DvA(4`&DlPPcto1;TV_*tVYlI5Nl(RMBsM@|29^3re$)qV*oqCiv{&!hoY zDkTCH*WPzE;aZ3aDDy@{OR|$%PpM=V?vdr$V1~qHg#n5K`(K$TE0i*K&O1px`(mFROj9h!*vEHoR4f<$N-r^2GS(U!N?RgP zs8{s4z_Y2@lFb?8)%k96@hyvmW+;sMb1oh;@ITqsJlpM-mZ;h1^wpewX-Ci<|AOS_ z{ebe^d?hYdKMhA9#*H00aTO<`-L`6mf@^91x zG}m}x;M3vL&~d_7G?pP30%JHRJ!X%=b6v>`cdDRu$4k@e^umyZYA*cYlui9MgnH<2 z=uqa5tH28klUK+UKRe+|JZXNv{-R0S7~@4dGv1zb`=Re8YoTjI!A+Kj+39p{@fm7& zzjeg15`0Jue1GYL9^SmheR8v4g+6N>Jo{9f8A9EdL5| z>1oucRt#Kcn=?{8sq-eKQ>LBUTqE5sm7DtbbH94cg3?!`g`Nf{X5=^c%Ru6=eoGu1 z^(k<+BjP0@K$bKpDI5;aU_urcK186XIEHw{gUmLqn6>??Tjc{e?)3iC$aQ>{0EqWx zRcsS9LBnyDqv^o=nc0!BTyU)Uscf#pg3w>eXqj~R{3&|FbW0z@bg+#wgOP(pSNR>y zzTn!4)&yr3+bsI*W>wTg;FEYeb)q#O9lvESmn{qj*%wadyj1VXn(MMQp_m?A7Z`bK zJGW6t)9PBcp@CYwk-}L`PIMH}p*Yj>I68l>*sYdFLk3GY0mrZ1VCu2=WvFNkB*Uo} z+Y#Y~2mhNC%0VLh&fA2_{U^7N851NGmEM)2_|_5*0tk^@7b^v^a9ua&t2Y|yEYS-&44leuW;}_jt?}$c#Q(tmuITj z!>jx3>8H&ehvb(V_YXCLWu4k~cDgm7+{myN#afhP#qY{m>dkGlA7_1_LfA*Bondzt za0dh{y8~qZN>(vllmQ| z2TF-z!A%6s$unVmlIqmG+5F`ZCt0m!-AeFLlxGWA*g(TLuE!r_DIh|hBKVCK$on~pt{k-1FG-7^8xCJR zV`=2U*wEfe$=8`zP6IAVFJfh)M>!PLe{UCHEdEG@ZTFE`#c;2!;Dbmb98&^!#-#C; za>nnV4RPiMlZR!691(+=teY5RsQ)^vo3te1-o8szg0v;Nnvu-x>m%DeY`Mog_D5R`VDE=lKuH%@r9U~W!Fh*pDQ zYkFL4Y^d?)n&3OA2a2!$1FK5LC9U((0@f!Qt9DdBYC&fbj_$uo?ik&rlR}N%5)xkH zjXoy3BAVI-Iks!U_&$BnVgVT_1QwJLZaV7`U-f^V0So0yy1F(k zDKOG4*BW&S&4H)jG6ZbUV;tT^#;`-bq%0&&pi!KzY2(uh`<}PN6VF@&7@1N)bnDXY zc=?I6C-b{Aq*kO{PY=s<#uT#U(NU33R$HKgq5~cbo_y=V3NGhf^)fd-QQ`oC5|qD@H0@XGN@b4$_3t*3ppT}8o(6rZ1%T~ybN^2Wf=8rD7d z>j$c_wGp3HgWtxVa@|oLS|VB%ne=2_!%1{(G_f8#7jg9NtWTZX@glz7k4U9GjTwv^ zB){i3gp_8e;|CnSIrt@9O^n<)E~c6uTxLrND|CfzOJPQIBGKW8_KGG>@anP3~ zau%^h3kbeTk@x-hl6`F#DWKoy)vbNrMm3{HOTXwTFvza_A%w5?kTcIG^L?*=``nq# zk(^(e=KHG0;t`*)WuZc3LLT2*x$9(`BG0&sO?98t=`oFk!SN-PMRzTN^@3%Vw-l$B zRO4|ty>DkNJ`(8&;^px?9;|zC7^#D>*to`pJ(gpkRdE{Ez}iv9pgEuMMFL-{wnY^F zw66|CNyuS-gNZ7EF+<89^vW=wUE+a3T(7u;c(cCdalX0P3xAmj?nKJ*yO8}A(6ODU zyp`+s8W7;Nj`Ta1@?8}13nl^zChoes*4yiOUrQ<*5>uW(X7h$8D}|8#6j4nU<0w2^ zq0QaDbIh*Q4XoF$PA}62gvG&Ai>(n+gCjRAjyzWLLCT8S) zcIq#Yaxt?+{F6Er-83~r`jLZy0jVqNq46^_>>TVD9rO%2$w~dm#|&4E3{jeAkyI)q zEc22x1TA!t^4TsDTJu$GRpg>VYHfDp&}6;~ikfYtG_D3K*RvWc2x+ zisiQ0a}*SWuhs2!+*P@szZ+5_(;pKaggs=@`r z`T5gvbOuoI_4Vr|PRH_&Ds!aVC2OM+U*jxtvtH=}zL@nqneObre7pjX@OK9@ zEvDO`JZq=n^GZ5A0j`aB<~;}d8s4sy<=*s>0l&f zhGI#?{%M!TT`y_pBr`-UU1+G7ljcFqckYZnE~9ytLl`U68`$r-@xaSOde#W@G`@dj z1eXmgpe79{BR1v#Ql|n;n5DmS%;dWI(VI!TT96>-vk?)K zc(n+ux4^h`9UXsRKhv)=>DwJgS2FdEo7aAaoMYknrzmH$%%=Idz0FO1!P&M~`%$7x z{$=ex{npzZ76%!&7`WVn;de;HI~F-LUdT{148LbQvV!H^xG2BSmX?n#jmCaoTFsC| z1ie45i5?n}yqqYc-A>D^H|p_!X))*4uGS;ceR&mDN#`Rq@_2EP#2;g1-;G#5dm#!eP?S`MLYl3|>VR6>|F^X$RV2IaV%8;EU?w5Db7u1Y2E9itm&t z@;*Sm&sXSR_!>9Ar}EvDOcVBMqRHN?{+{If0k#E$kBq&sDJQ@gnqFX~3SyX3hxykGb`6Br&4z(w`wMqo8Y6M8P^Oj-mt%k6xb!|!ny z6$4C>izfp0x9i6m8tSfnq(>vU#Ank!94_#Z$WYwERoRCqQmY%Y%WrubbH4Ul{urXM zTCnO!1_2pBg8JdIKxPs?aJ=pA>5eb8&8t6hz2eNh272&MdN5C28BZyZtv*V~n?6#= zQ?o?x#asNnB^s}eLK$WP|6ghILSEVeE3fdfV(P#Iftm(H4lnAS*X3>PwzAt~+=Qj2 zk)I9TYOYZ3rR*m7SvxqsPybLOxL2|?Q!3$B$%~tt;VxFQTnQJb1X16LihH4uBYO%2 zoIMR}fU*9QfpQ4Vp+toABIv&EZpx{s67r7x= zt=jjcIq02U#``6Ej}Hd}KW!H6MBNLN~3L5*e~0GyD%K;_h%&!0GvC9ipPOU}FANqN#+qgss9oot zMecEyPSv3pEKN6$DD+N`Kl54!8l9_*8l$iNxJKZib#`-`b+Mjd6;tDZaRFF&y!z>U zVb*e2(>?&;zFJ+jUs$&Mz2WzU_4%%gBK+L@c0SdHG=E)PhWG%NV&^11hK}EE>7+$G@-odbNK(5KXFH?b(Q*>9vyQcD6=(d$xK;C(B1W zBeV7t$vOqNvh7y$a{IBt@wYHT440dBoBw&)ax1p|UxNtR2$Jc!=v^NU#{2M?y6a`S zF*1Q4sLG#T;eS*4!CUy=5Y^1C+`;bN)cS~(PsbZ?eSh&S~Xh8j~ z;jv)C8c~r4gV1C2-171FtZ8p1h6k6DW;_yH6x;=%ZDff@xk3=xFF( ziHL@tZUi-$=dw6jWcendX3EXLIG2aL(XQ|lHPWCP73ZzXEYl-uv^fK2T@u>**um*} z({HZ*Xzuvb%mX^xnRw-e@&n$q%pYU(zIZ@E^@?H`C1cGb~N}aj3-0hiJhuJ zCNY$#u?>&=9^4^{;@a-~JlJ!Uu+`Q~~SNd2dk?d1j*L z%QProA8iQ6p6tX#t~YV1@pZo27iwTfVQN(3e`$f!@{UN;b0A?-&q|xIWs6dtY$sY0@CG*BIfP3aTCk#Uk)4N~fyk)$o9y)z%mqkjg z_lo2u%H|2~a6K>x>J?Y+XDrA_k;8|`@hEsN1@VCZ)RXsFn23k|rT?2!(fI2KM@0jd$f<4o@GxxbKC0JZhr+P-BZ0xP9hpGyeg z8IGl*G~UIA5dZ47J6W6znN6UV-)wpX{@%z0VqgL!A!(xYmbIecX~L*(o7r+JTEFRxyKC*&^|0IJI?+B>Y!v;7Hv2C(q$zYFx- z>`Wz|OhAF7*#=s}+`{u@WTz}2T*!OO)hy$9ob-E0T0E*DvP(c^Zd;oSqM~FE$UEQl zD3g1mXH0`v6>aSzaL0Z)PuZkLm_$V>QEcXL8wXErC+%7eDiiJ_qN7%min+{GPYa|( z_&$WH@n2wMUqh<$V7N-bt}mslna~0|=jhS}f`X`xobF>mG49{N#`iXlk_G@{t?>IZ z`T1!!yd41jLYHwSL!Z;U*>lMa5wY-e4A^9-+We-7Yb_E(Q*$%T1Z#XVZ^^xdHfQly zfaQCz0$lXKxL~rs2=($pjmpYlI2A}oeUJj5q-uaN1>nPt|s6pFBfh=q+3{z{J>Li@uGoi zOo{+)@Wv|mYZ!D^04nlLvuBB6yz;u7UrlYJ(ETT?>L*k}kT3m_;#Kr-Ot6-g5E_Bc z)}-Y{A|R^jlf}t1f!J!-JM0~b#OxRsxjx-+02Yy*J6w>4X2Lr9WlCIdQn&%fc1Z5` zeY#kUi0yWx8=F2>!wLUcn}PvGuZ_z7qQ?!8$U=%_kn3Wn)DIO>y`rfzG=z3eX!QT2*d;7b0 z^;d3KSn*5G&EJG(lys5)&kKppVag4|!ykWjT=!v^(XTy5&u%Gaxd)?>u?Kw!nr+`S#d);Nq`y~p*J3H-)S|V5Zy=2GOxt$~ zdCO$Q4mQ$t_I23A*HLEK7~QQVF&!LUWpbLn{srqm`#_AI+f7avdm4m!GR2}W#ACIi zS&uEFU5h5`Z~+pCaI*Si%B-{+7M79_AHrA~l9mKhoCjqw(l(Yu;AxZsPqiudjC(p+ zA#PToa{`%~LFqpR6iRs0xcX=tv(K{O*6xLkckU!VdGu~;=rK&^yMEd85YVJz8?wks z-+8J@j#{s4kcYNXvnNU`)?izd^s@TBAReogNFh_n5ar9UljC$b;TAqiVdew_YJAOj z=E7C`C6s=}$NsQ0_I&?CJmsBLk&m?g2r9tKydg16kQKt!y2;v4@b3Y%F` zUry$z*kLDONE9KNg(%Z;BDWh(U~B8UNQStpO!sM%?Mk?x-~ARLNu1d0r8=adm8d$t z>)8=GTIO_FX|99as17ky{3U5gUc(2*o4g~SU2U);@D{eLITvM`RjNQqx16bODB<JA<9^ znZFTz*FxbjrtWE~c56R1RJ2ty`3b_G17ClriE?YFiMpOnQ!>(%KB1}}Ex_81?HGG} zRS_XHt+N=<_=ySJ-jQge(fas2mw%G_ukswajp1BI*Hc?pjyfFx- zT^e5fkVdv;iRu4~*g;8_7xT1V6WhqvepvQ9L57wHzjIk;lPW0IKET8Kkbv$K{$*XE zr(pmIh@(>x{@U82?{-hDb`Wg=k*Uip_z>3YkLyu(BobS~mgzA4VE$MBr|FBQ&&3S= z`H^`noeW2xD;`=Q4K;P1G3f)X(ze`pg%}frhEu<-Vh56lhRQ`jbD~GOXbNsA(3#A- z%bTR90_gdCgR}dVj0(+VU{`K7(~c_9-L zH|5x>D64e^dg9{O1}*I> z3oaO)-3UFm;{u-v?ToD-&VA7b)OM?Pf+)9OU^y3i)UZDH&)5W@%9H;Hb=mVkoWH

73t-*k6Q`;+C8N+*j@OCkQh$QrtB77H%-(8`|jKD$c2X_s{P-cnFs*f|?bJKw! z0E{Q(5fw;vlly*mykb9w@5(v8VW(fx-FLm&9wsC$2SO7Z9a03a|_n#~KE)7u7I0pIJE5)0(-$skYzX8t5uXbq-P8HNeZg|G9U+bliWylPgNF(2Zl{6o5YGHF zxk0j>k2eF1Zw_-=?7a$!aoaKq{iCb0kWca|#1eq`Atb}%mzgQ;6 zBa$_9Tx6^dIsJh-9kl6s!SD2{cG*^c`XW4H-@3x0`O1p^&|DET&wP*ath}-R!^4O@wF~JRV z2;R|?+;`fUy_cY4iL7p$xM=<@y>uByun#%%t$j_qdt zGLDsxR2V2}cHBe`H2=eK4eD^d{F2)XK6~A_r~}P)k>Ze*{m6F_rfjrpCo~4mn3xJH z(hEMDW)xbquZ6;v(g}Ssn|c8&lu)w0GaL8j(d3rjsLIFfe8ZWW(b%vNeB-h~ky#hP$@5e`V@%>0UzGxY-CBn9 zDXgHM7%p96}H1?V{q!ZGfV^cCaad4AQ-`P|v;7MGsW?m^xM z0DiLpz$#|DvRAsa(6z$T5IClw#l-ndd-&+>Bs9#|2xoBSic5$cnOKTljOtg<%sa}h zKDl`x{Er`gZ*bnf>w5EjcRf%xbMTF;;pBuZE!Zo*sK~qpJS8f%U`z&-L?ooGm!;E? ziDiygz%rPGrMn|?cHHAfQ}Jt3q|L~_Iy6vem=I(T4AQqA+ixLFm21hn+iVNRhwM1w zc4a>AH-R_a5p4LPNIz{LTjslhm+SWWP{4CTmWI2wJZ)M1blmM&^zP!1O9WDoMPeNZfZLPDwP`@fYEa3I2O@)Wo)oFzNfX-)+n==SwU=ANxoK25e<-L!s z%m6YeFBP0FaeN|Rdrr#1D?+l9=u#WTGN zu_N(KD;5Y-RuVzY9Yd;w07n{&UEPn!PfBF^D^(i$F=F5x?_vO6*Xix5>GjoQt0^hw z?l`2IXx`^|r26M`;okWmam$CX5yFTTMcsLsH5rrH%bC2bbgZq2vs{1co5r7aQiU?l z$WPg@mc&1JH31zZBKFp}upBsUclU+gQ@4Mc{!M>O#=a@`_zjmHzeYo9v~&SaF$lSy zu9lOgqAJ9|5exbckpI^OuF*PfCjMQ8Vv#)6mXQ7}3d+72;DOWgD0pl-!w+dFi$)&T z@4Sldc)8#^*=V5tlf^Gb{*6#wG7WTyf$<_G1+<=JE?X~Xl7ACs9j&VZ?k#3M-5VsZ z#w3D9mG=`WH=4Revu5v&7CMONx5M|@@gk(mIWnXfCAVMVBp040r5Q#%7VkL1+pv5V zK$!Wp;-+C zjvR`7IOTF-+Blf+-%*;h@7qECw<)jvT6->b@&YG}c+5Xiqq~S-go~K^3!Bz5)hZ0` zm>uaK;`j#|J+4Z|K~m|ow;V5aZ5p`zo+y-jeviusQW)|mAz!~PD_pNgsaC<(w3Jarn<~_k7 zG3VRBf=zv}f<4GV-0_)Yui|96o#^zsiGI)@m>^B^Lj9Ql*>57KFhv9NVo{~;n0(b% zfRPf4B^i4zFcKaoK>Av=m=>(t_#K}oHH5)d-8)_NFV65PEJ(TvFQ0Z4CQb0g3u~*M za<(r|L6#U|z$a4NeKooTMjvxyh7PaSOQaUrO2;=L$>1JK!z<`955Li;frt$8-|0FP55O_$uDnHsbo zY3=VAjd_>ne$-90x@@NNPxinlh$hcSJF;*j2_N;Xq1VIW0^GU)Y&ENR&64@gW_8{( zj`x3|wGH-970ZAd>bCuPu|ffn_N}k8b!i<$zqe66r2QHd<~n4xRsF*=bZ9@kz?r)p zVL3<}&nl2w3xB*Ye`2b^DrsP8i2N*+50V9?WV%V>7j!b?-A!J7Ao!@KUJb(r~t8X)j<4CXE!{o zu#)U0g2YEZLmYx=coPCSV>tc7h%f8N!2K9p83uKHDc+bd3qz%m{aK~S@b$iRy8DQd z$MX`FL3eTGo{LhXYoBrlD_4zD&DGJ2=)uZ^$Tkh6T=7}?Bm0~fRup-r|En*%Kw?|9 z%`a=Mr3E$p)Akx$QSx2*-SK350`P?@wU&ax3JU^)ID%%(o3Z&hSh+QK@2(CCNsYAJ zsT(bM>T+voRUmUP&JCCI>&_G@f{pX(0+wEmYsTPUT8GuE;+`Z_*R1Xbk6WWBo<=EZ zVuSy1NU#DnUSSBRFgM&c>Y&*wN~70tJ?mF-1RY+7184V%)@@&HkH9{?O&%DE{AlbZ(53z5|awm9DDtR4et{Xrw;YTf)u*7Zst)xqO1FBJIKCqO_;3h&InDS-A-gBKCn63nJ z-sZIAOF1di1?7ree(-+#a~DsMZtz1aqW{c&+MxwJ<1_UddyAW{mT@A*_&m@b=s%m%hp?I(Grb}B149l_AP%_}$c4@y(NVD*IAer*q;qj}z?joIsu~>S~ zj)yc-1=6dWZJ9#kZ?+b%r>e?jP5Eq-k5RRr;*jRLJYwWYB~!IgK~$|bK<4_7XZqpP zf1YP`33E$no9f7Q4EvvmXXzwo{^2F5Jsa|HKDJv9E}&krYK+VpeS59q(xON4;>-LI z;sN!{;^|Kmb(eLs`ARITB&OQ5(m+}hP%2kM$~qbKoW-1mB8UYM^+?`jnW8!!ID$2U z@ZGgNgs4end%75w*jrOaLL>}GoVffE_mY;Q@oL_)8!$I~c67i;i<<6zRUNB#`|!B0 zk$6L1;{v=Y@-h^>l8X%%KILVJZ62Chwdf-1uSqxSw=S1)ZPHHY0X>L0OGN32fr8HKy><^I_^lFAtJOxGWo`$K?=BVw;l z`d_@=>CH>O`Qrn?NM$Zw9XH>nT8)&u`R(kkvbzM82Of?Z+s-JFxMruh-Tc51sgMmXfjo-V9%lH78$ZU%_Yw@@H7l>8oq*MSt{!-N41 z)Vtz$7uQzXc#PmZd1w`zR7J_xQ>~e%Y(Gga_xox2Edv#R(uRin+v+s2CUPzA3T+3U zU$d0E&vvFgf5zbB*$4uATggR%E1Tpvd5kc<2V`2%E8l7lMN5n@?41u6 zbzTNj{NE3kOIt;b$U@P6qFyO9bl1!Ay=eznlzrog&cf*jF@etBH;5T)doVYyp0Lc% z(+A{eM^FaMYr~)-m5{lQJHi7TjmA+wllW%c7g~|QjqC`3MraNly@*qni^dL`o*{eZ z(9<)?WCay!nxIq~Il%`(E|b4-An`oW{_QwV7%v7COA=MOH%H8bo#n4lEf>*4Yq8Rd zM=meX7&>qA3&j|Se(Uh(#G5 zmMhgo^r{qx!9!WPez*Wcq3_VbTV{mBqG}vIk*U`P(KIGG5I=Z{qG5F6L|%vpy>Iaz zQntdLe(}S7BAOk=@L>HRNJVdGX7rP1&qE77C28h>osx1bl*5a3#$J?@4VIihg>$oG z(|eo^UaUfRp4QCpr=eGd3R+@$UoT3A%k!id65;@xE6{SUrTq*r5}00&c+u~SseP`$ zdy*SZW(;$l+P;52r+1cL#64nS+Mr)lRXeV4KJyVhuB?G};V50EBov2my6Pv{PYrAz zDX?*1z8ZXOIeZ*B-97c06TJAv-O?H!Y~buTkzW)N8eis&BMAI6__b2svm|Fw@xvGF zz!A`h6mc>njFC@HhbOy>iIFh(b;c=uNv<;U4!=c{kMCJ9-3zyC$&o2%WOtL@Gtv0D z2G<8*#?|0xAR<({FVp*r{=$COox|fHZs|?=GqQa6sVJM(~*I?*3s%h(2jW)D4 z<&gcE1uMolg^kN1xli@(G;U#sU(CY4Fm_4ff28vqaK_4&O=RYjgcdEEU+PR1eg#=W zGv>hReQx2OszLaVyY*`M{Et;%PyLcRV9oAl+NP;zyoXfz)3r3ac-ml8WCDkO z%uzEn%$mX(rvG%~HXNh`1xdKBmTP((CAdZREnD`Ia3D`^10_E3^9hy@Bhi zqTCOHf#XcJuO=)8evXLiVwbMg;%%Z>wcJ?~r{b2uH$ba(Tr8)<{d{Ma&dN?YFe({O zMJ!$xgH{C`1svTeTq*kDr5v&KGE>l!XZ`K5*G*c^x~stH*DfZfPl7|K>^c&&?^KUo z7yqSdzq6CLMez`x+>N>4Y_a!U0Ia(*?NK9~l3)j9>tD3|2t8d+3Q<$tF9WG^JEPqQB6Ez47abY&!*Jutd)4OJ2jNF z@__rduFIys9m-BvMTHN0ezN~=ls*e%2|pnucIf=DOw-)d>=IE)$$Ru^vmqe6T(UK? zm0Q8~&TRT8EJ=a25MEU{ez&HgK0*_h8_;=9E-j?~lyU=_%-3>WoTZQ)ZEE>`81@m( zwhQ&q!H&A(%k3qF9JA8@0mDE%zYnabAO1wMKHtEmFdXs9=Af@JF6La4NO z>NA7BL8F(F6h`k%uT2-u8c8;Bf&?;~78*gl?!Ae|J=NJO6ERQmxzhWtbg`TN=9}u& zsZ^<}Y^IYXsZ80-A%3rF=6UKTPCxNDM>I93JJ4p6 zMR+W(S^t~LvExYd20S%+!}x*5_**7q1N!z;(ZEojOj<&|AIajMeSQ!r63)Jy=6=*z z_>;UrD>b_QXu5dxqVkUcc|o$=OZ<;#E2`A65_K8dg&N({NXdX7oHqW*FJPD_4=q+ zCf`Munm1FD?ztW2(ynE@E=$r4A{@%=$F=ugOHX|AgjZ|6qq#64CP5|Xjyn~n)+}>y z6tojWvHLsSsd>-lLf+ggCRe(id9zj}Po75M@6M+S?DIyfbyFsjPrpnNU3+BBWKtKd z{oa<3ps;^Fr9>PUb2Q6PHYE;!kb{;KspOa(no|XA;Zy;zIN|^%`8dy8N-a(mkZsW> zSJ7{W#!zemABICK9UV%o%MW*%DnOe|zW?_^T6<;(U5ZQRq~w?|qG(2o-c-I|agPsq zUG}k0xru*%%NCcV3nflgp>ks|3C&(wRT=W{f+#Hz3JdXcHs!tNlWl*YE-~ zq__C(NMkiwtjEW&wn1} z_pydbzfaXKnqD2yng%@#QzOSVzbULqtKWSp?LHF2uVznDrG~QAmPyF^cb}vt^((4B z^d7g6-uZSLGZv2V3g!al!wY@M!ue?bS1*&+(49OROI7b3Pw|(y4l03U7EYpgt&S?3 zMlTKjiwYLH#KOc$OL>tFk4UF=E6dXIZ?2{br`S9voa+GX7*3v1N$uB$Hb5iB2`fW+ zf^Bl+S!&zuAQdTgi6aGt8w3ufm;69f{uJ7?yC}_>P?z5WldKF9NU`B`)9okep+0+9 zvuAsb??7o`O!SfxBWTj-1{8CeHJn@*gV^KrI+C90egn;T5zzo$+goRcPbAl+Nr+F4Fn|~ryK9XYR7R_5D4kPS|=?B_`2#&zXta;Mm z|7XsbO&vORR3Vd*BpU%u^nJis2P{D zB4=XJML(LOOWx?6irl4%UMH9`@%jDpR|RaX@FQyDRcmzHxbaX~RouI#@An z1%314H<@zHsWlOYf^1b0RXmEu?jB3gMWfwntsTvKdOZKJ@DKW+`v*p_5?&+hL9}eq zP|?Pb+ecFA8jjNv=*^zwc{6-*axBl=l<5@l!MlpQrL(VMH199MroByJH#E=iX~>%h}xVu^x|5l`2)#=Lo-ty-n-nw)+o5X<=k<**jF3G?JP%zy#D$| z_Rm1xCUIJwX9hm44yX0HgcJfuM*wno=FDko%N8Sb>!R;T9b|v}zfaU5CFT))M>+${ z_oq&urVl^-SUm^xf5ag`Lhr1ac01~jfFy6>O)@gCgNx`5`p`$3(YPnucy$WbOcTftLN2c<5!h-SLsY~l?)Kg!s+DcuAeo7G>CK~iZ$aUOqp3FXZyE^^(8~b!bbCf4GS%5!UCdH+W`){*=41vibZ!AEbUxBy2)mvY|3dSZv(qErWwz7^lGE(NML;Nw<}%jFmb@-3_03<IG9{ItreNh6;9++KKY5J#(ArWSz|et(S`HRyWIqtdK-BPm-Y001BWNkl`Nu!#U>+legzz8bbXC3{d^(C?JEifAl+h zKH7tR;e2>1Vjn+iL_JKS8k|>c#j2GqyVcj4H>DZlLHyNs%Qz{hv0%BoK#XR*I*neR zGe@-rbyOk$`N7tA+@V7C<9hi12k5u8Yt=lY%}_Ows{@kt;}2fVml}&ZTHVHnEW0z2 zLglB%?LeNk(c{KC%=2a?Z`M=*gPwVo7Jp&n5j58qZNZ{?6`x=t_m7#h<`&JHCT~jf zrYAymE@k-e;kuA^f$H5-kDmSVSt?MBPoQnN)07V!6S7@vchQ8F6DTSqN-aTJqZ*eM zM-AIFq(RFD*-4-FeOoka5iOp$Seaa^a(SavvSvvdwPVy3Yu@xY#Czk;a*jDpnw@x# zgH~|_NYq&ev{Duj@He(CAzJI!s7E7u?7hb_nRKF^uE-nmsp-HV$9Em4aSe=|f93-S zwI0|sVr?e2p*BO>*duYelO6NtTI)Hv`D8k>{)oyuXD4rbBia$h$%e}{E=Mn|HwdMp zOUPRWDp$ImdDDN%qhF!O1)oq7vjcYBqEHr_)cHJr)T)1lpHZi``;zD?o~Ip=Fs*s}kF5jt_ajQMg=a0wDO^f(q{@m9XN%9;<+-sXg<2J&>*T);trQV*;3L{Xwc7NIiDn(fHIj%%@;-26s|@uUC*S_2Ujg7 zZ+lNCs-)e?AqD6?jyU^heZ7@u(i+l@e`?5&6+e| zKz}t4Oky(kD3i4c9Mu@l3>!X@x^Z67MvbmhEtoN579C?^)vs@Fs>r7M*e9&v=-a>W z-slCk-avj-XyB&u`q`7$DkS?xlrqaNt2B=s8xIX=C0Uv(HQXjVT!38ba`yYHn zgPwavxum@R!H4RQ;=XQO)#rF8-i@{E6V5@0HTS^>?pL41^JmfQ%95Q|K$5r6#xK}3 zQbrCLm;X5(8y=ZykT(`OZBppqJjH~$@u``-eLmrSMcy!hCtgaS zy4|MGp%XED-U9b-V?i@~6~bu4`%lsJb&Nr$S-AeW&v@3RE#*x&pU|e@^p7utm`2ds zFW*a#-`hm_y5-|=tNoa~z4i5GbuR=#kvHCGgS_qfVu)Sy2J%*`%On$dGfrQOY8m-+ z%$LKq^Sot{x9zNXW3w4{K%YOW7VZA4Fq;psFBAxw))3--wxA)Z81;AC-}J%`5|_xQ zwUO!6chD}XR{K09Fo6R(%g<($d-oQh*_p^&#-;!p6S(v}H+%<`DjUx#bNJ7}WDMjj zT#>i4O7jNt#tW|vzh}BNVe*zYHE-BNKi=apI(+b;GGoJ7yQ*|)>i1-SdhGGXY44sr zD)OH`A=0GBY=I4&U^iJ`g+M%hUSiYvJjsxt^Z@ATk?iU7D=mT zbL2yB%Su1^`TQa5bz<2nRA)%>~GR6vZEyvLrf8^YCFbhH8nISK{ zX#Y&(qY2;q_ui#DSi9;;l5TC{xR6gH2eUTUYPzW3xp?W*u&O*QGIl`pA?j#$G~`)#I> z@ArM5ew_cKJq>^+Ey|S87&{VJX7RJdq3b1(dLtfzrloP*w$g0v{jlcc|zW_+|9gmChc3f z&mdNCNwBS7I%Po=Cjo{}UCGKNy&`Zrc*-wt{z8lTFS< zy|ytanzx+Qyrus13x)A|bSV<<6MQJg-y^#Bq>$p7P2a5Rg4^G1hv#YgIS=Ns-`E5i zdl=qRAm`vUJ&6jGcKiz4bLkiNQ0TFRJYMh|yNB>zC#^&q-YD6ePGzKtC-idJ%BZJ0U2isXJyZFBzod0O!KLgkyL zuaTq1Q0GqfvZl_kew#eyRjS9@v3oe8v7WcaPnbm4Gr7B#wRs8gOr)4}Enc#O%CdV& z$9p>{v&bn^r_$3*-jL@FL~PXPvGmBp4^i>r#d*KM@iKWmef^C$sbj|u)R6Pt{=RV| zeaG6o+u7f|e*O9?$D3}$mMwo0l8xWN`>6W$>v?2Rk$~A&VdZ1Oo28IQ0(tAogL66L~Y6O-4{&CU2N%+OW3e(7{8>BmjXc9_{&U9{y%t7!3SQC*Jy zf!U^~%`E{-;PaMA^QOt0uCyi`N{eiy0cd_-b9(Z# zCk;Yii^m4^UJ% zCn@H+H@nH3S`~3P8Dp4-#-EC}Yv1hhZCf)m;c5ItV|wJRM=~E&qOBfk-t-vXv3v); z(dG?G;{!RBcwDcLScCapq3n-TzEOD^_`^V@%~bc;NTk)@Q;k`WOymINd;VGZ56x>o zPX$uJdZ;v^w!VU952l&XC8Jd`aF}K~yM(-DAd{1tx2&u!!SIYem|yVEJo+bb2hZ0q zpo*X*)*{EpGm%1Ww{+g2B27SeK1z>_rsQzr*qdmK``eWhDvk z1y%HLeDOS^zT1CiYYD{A0%67e4?o8xs?Y^t{A^lSL7LmnIeF7_4CL~a@e`;A`&6&u z!(>E&+{o!f|@({-`#A! ziFe`J!B!yaIVI0AjtNk=Ze6a|y>@Gfe>kYuZ|m37(WC!So!YgibLY-#t8AThjQj ziedA~2sYb<&)ci7Xw6&jkT<~o<)SY*uh)8d`k7}4Ns@8RVV|UFJR!lqW&xX`l_^t( zzF4@39^liYB1MWQUnn@|BCrK~jYfAe&dsFL+CKq9-cWH1fjgUWmioTk zmu`KW{ZwqEMr+?b9{4eRKj(Xuz+02MB=*ZLUa>fh-!-0tHW}*&K7&8jg24-D>8ner zNCcZsCZ+p?ylFDEeCl#qHe}i5erRZ-O^b;7!AGoQmCPCtZ!~Y_@i60#8MJ@peoEnR z@add4O08SA4HbM0p>-_NpbR}U{~@ZSqc+15IMO0fV8DDX~W;}a}w{da8HsZZ_v`IFN3(*$(xO|Wso`C z!<93Uw^W2~U;@bT_b4^87`@%DIz4`4CFSY^i#hA1=l>;a?Q@njZkTg=J>HYHZQD*r z34nFzjyrCrYE`S+*KBCWpn1ENwPCnkoi;;}F5IUrbf!+7Mm1{IP+B%eS3&~F+pF{( zA5LR$gy)95^fH@O_E4HLcNf;FvE#-o6UuA2Z1aUZ%{Y#TiL$k6r@Qxg@_FErH`O}5 zLC+?$N%$sU6T}1et|rx(Fm+U#E=Nv=nn6Q@c6F)3b~8miiMQdXlPfm<%jTFVEEr1P ztfI-KvKs2EsSm5@lptx)-eCjM?wPmg`wh4sw#XS|1$NgP-Wx!TY^Ii2UGPi>d4t)c zu`fvD_gKv)p$%NaC$dIb0fQ_?@X7Cn?FVVZn?Fz(FIvhsk1tg}e$)8=cTwZI75N!< zwZ+LM3v*)n8QPORhNh0MNhkl~=soNlY$sw`2H6y{NwYI_^Xwe?>V2>!XH-vkCb;fk15pr}n zB5(RW5TpooYUSCb2~|sWM22Z3OdU_L7Dk%`LWVSHz-bu5<2SkKCaO}kikfht(LkNg z@u?Y1N#Xwo!fCy97MVsiRIY09?%E2RNdiLC~SW#oTwAgV*e4ERpd0B%gZ1PtZ)sCIcq&x9kkJ=5^Mu3O57U@NV>rPro0IEJ4$E?4NFohcHH7NiQYTYH#I4_NrRn!f zr|1yY4%vL(veRr*V~)B*%ZL#EvBzUGv~L!zThk|-(gSlIxJyF3ki^X~_@~Fg%1BKiF7(Rk*GK6)Kexg$_Y)&K)^NdspwJwQsMb18aC)8_Uy+zp~je_kpWm147<1q%tSTTRIM-V1c1530QhSz9ZQ=ExQOD zA8@BL1IeTi8<4R{4Naekvu7~ogWzAZs?fp7v z$=ahU=%=I0DVBZqK+Y1GSS7IdB(bc3xTyr){5_RTDDksF-VDu~H{>l@Y2ui?A;z4E zyoE673sI55!>LKxqV(mXH_}ywjC5CyE-go}6qz)6D&5$m2|e`C1ImAT*ocu-l-K#; z!(LX8>OOgK@JrPFzOHn`4L7L2k9%dJ3UJb{T^l=j+t0zhX1z9t?z;016>ubp13e*y z%}u<2h>ng@W|AXDj$$*(hZXt4yMJA?h88dWiiu%I_KT~ah$9ZXn{f~n%mvW8!K4#z z3h_+-KL30XJ@(k6RHbrd`kRU78QzDKD|fZ3`+@}vS(ErMN4Kt;9o?_q5xC^dWa^il zW|M}tE1bIBT89=)dc

^~U9km0PLX%bzOprpOq~Ww3GL6OL}JYSU*EAGVu79{BGp z)xB>jZ#-cJX2_!=QY=7CIZ(-(cb>AJGH6X*zX^-j=gk(d#D&?Uvj+Uk!BK8|@@;M@ z5;YoA402beu;8wq*Oyvcm-+NEi4M`TZ@onGW>=(8P7eU3opF+EGc#f0*7^R!bbE)x zEEv$BU^3+ej|p59Yts(zD@^ld)}nJ43}VIcv<(47P9XMoYuMx*^?Z7l68`=B3$xFg zIoc%3!ObASwicmmR?QbKl*(q8+o?$LM2e5iN7KgFqqC>@RGMwTA%OO1lX;xHX{{_G z5WX~cF#YknVRDEKJTz)Bg>26zksxD8p!~|1G0GJ33*I0%WLt0qpnz{2^1>kv0D?w5 zH}Dyoi9rax#lBIkSrex>o_bt4BX6!WSz@>eq^n3Mp zt<12R(&@kBS@UN)YGrA*b^mJFH+J&}SEdZPk-X{lA7Vd8q*VY>Qsdp`8;BGD(3)y( z92T1wINbi1ta(+I0Jb0}b{uW~ayngsSH(;=8|71-LYuG!1%9(ty*2)C<&Qco*tCwzJ(7QjfRs zO~=!o^?N9qBNfBMC!9l0Uv8jV0HhzR9m!U^6e%E_1rHfFfP{yCM zt^o({#xKTa_)>En!us<8Tun|9;LY{b*GrXeS*1!wg!P|({+WstE~2zo=9m#*vs=Us z969o8PSlNS#ScHKAS87-TDEx~0`vVJ_^=ZZxH0#ZXA7lvZQIxbpMCo20u^byU%x&o z@CdH0Tes0VKFq$9bV*sI)Maa*TiJ{g*8yI8H~#*IGULPr@>stf?_2pg#AR(*|GWAO zbwFgxYZ^3gY2o7yHn=yBk+&;1n>2!-L{O)ewP@kwM|tyOXvfrLBM%s#_?!N_qm3Vu zf7=6>*tAraEm}K61nxjYVW`|Oo{+JKGc#-528Thnq+fDhO%88*nCWW^b0P}7nOiZ*W901$qD`88 z-nh347syZLuPS1g6q*(X_`$(e_?i1|r2gHSISh2536eQ*38F_T-w=Km@{YYexE=ND z(oB)HCOu}-Uwconh=UXv4(K3xrHXN&684WP%F(p+b-CuvW;V(1b>#DA9eDcrZ`b@o z_kg^yo>3DqBlSTjeK+%Qx~pleOlF<2mrhcHu2bmX5pDw}FyLYw!EmDR5PEUsb}CUO zUWMz2DI~ORO8a4pJc+1F(Mu)F%NSMEEV5iv!y4|NXAv(I?{aw4!MmB{T&RV&iy?QBA$p`L34jMj8 z;LUwk{LI&T@4Q1Dn3R;TB(Bus3Yxc;9I^5EvEyEm4o4HB*6uXrbOfJp1V|kLaog6d zG>ILU*RmOd-mGg4peAb8_i5s$i5~6)A%*$q3quAgTs$FKc(1OwVGS!ZtyXO%QcBF2HjPs! z{G5q!YmJ$epgY6z-cFr3dG@m$Jf?!fI-~zuHf^Syxy^Mz-4NmOElwux4liJV_rU)R zesK8RnU?gx(IYO`Szm4{y&`X>xeNq2Ha(V}|K@pW*tTINQ^;Mv?4n7nCsF=%j)87d zCG+tf6)jhkChVS|$R6ZXVaR0fwGOY*re&L`Apa{-!-zI%H@UPSlhtUGW?*$+p4feY zzI^^mI=K0u>X)bjQFPsX*HQbi?G-U~)K6I@Zzx}nm8GMX(u%PwaMZ~uYS`DC*H9hA z#DLZyarQ$TyUHN57T(?_o3g-IpF^n-AF4k5!NXMluKG@+OFEiw@VrEu%-Lv@$&+5C zi1**2^Q?K|tnCyQ9YO^^T1Fw2t7nVmEp_!w3jb>^CGr6@^1MaBqNgj9x2xE%Nm(6P zHG4*Cfb^4`o@eu8td)kV4DulIzOUL?3R9^95PKkNnm6ObA(ZwUUrc|UoJ}!tydOms z3WQNB&hu8Qc&p1aZ|a~zZI=w0n7Cms?TbIb2cn@WC`qA+qBQr`{=ARleUpzao$|+< z*Xy|+b+#TJTIVAr_qM)pM%2uE0b^Z<`Fch_=yeXCnGc3_S^B`%dT6XaH}6Hv@6vU1 zW!>WqF8W>}Z#q?gIohNe?A)p9^g=TS{UZwxCj=kWvl)%*bEn<((b3|Dz$Mo_I9)}u zG6)%>PJ$qXQ`34^D|5*8TMtt6-mkM#&qzp(PInZjL=TuNHJh}Px3xRzo(Z4# z@rft2YlV_&<<}Kx$tM*lq%fx#V7qIjfQR;uHE0j^+E2H%JwjWzlw^{p1D2?vpV1Ad zst$ajQxc%N_+7mR?4z%il%wN^c(aqwoaZg?Yu?NgjCqoPEqB+BooFZ9U>9b4ahOcP zKkTm7x6__oyQwdy6F{^@+;`_~t?3A>`}60|Mljf_001BWNkl5)TA=P9a zfp>oV`6riM=~MC+XNxv@^dhCJoRubRj)PihasfOLMY8R)ZRx@34`wojJa_yY4X-hr zlH=GU*48r$4-SVu?w@f~stU^||I_5{yfBK)sd^pj$=+JXfPioYuky^Jg<~_MY^Ok{S_jYFT_VKr* z$Xj-3-hQ1$VOwTrCU4!owvjgnButx4p81VJ*LP#tWOHvOMG*y9TV4KHim1U6oUcIh zmd4M*uWif0*J$5`#T-SP{nv2#&E)O2axW|Lrth(0rTySO-SsuCI<=V+W7&TdktErN zZ_-VVQS~BaE=%z3$=J{Cd$1;`*bE-Zo}==yFM(OVyZuXc*F7iSC*;jS-qgkbrWdf? zhfvr~=&2+`}GgHi^v~g zoWuk}p0~>;Y1@VvWAG?D<@RQi$z2?__GY%#{(#LW;dl!_sWWWf-GQ|yI)wm&oP5o> zPujL?N6&H|6-1}R23#e7zU+iCqf|KYf-GdmL>56{%|VYRcKz{4QtOI(HKMWVd(f}u3kw+1MOgdAe4>f_aUby9yJs&47db>{IKSQ{cCyA* zpg=*DBd{X-9aOJgoxfM|3QI?A1zrd^gSvK@AT!S zQfuCH5D)X2pQ|=PLlw`_;1GZWfk;$au}1KJ7;nSt4yTiQPf{eC(Cg0-v}EY&p;V(; z4I@!DKLgF%Hxs_01)~;FvCv{vq;e6*W|Owl_kVx?m!`FtW<*)UWg%}!@QoaFt}Ziy zw#%2zEYjI?T9&tcyN%xN_%@s4b3h0i(bZx9k)(Ubx*<-B)&%tQfz$NO$Zu%lCmU66 zI~9DyHt*{;S;?G|1>3Tt$eyXAl+PZ!4V11~ng)D7fJ#&>;r4LBTFGgxrqRK*2MvEZ z+q$Z{liv+r#J*qmrFwVQ^ZLE+@Z7X;6Mgv5hnaKL@k1)fI0s&-c{e9H{(6Ad2Sc97 zr}Z9R%9*vS+(+JQ+PIiqL_goI_9H2w5tNUkbtjeVN)gw*dWD)d1A*mRpJ3C+-4w^^ zm6CBl;{|!!dTJ-l+WtREOh9ZS4ptIffI3}$BRyENh1#QH&*YCw&kbwWu2XIR4?Xmt z{Q&oK_SJ6AfkLpBo2@vk@{N5m=FL~9O{WL$zhC*j={>r69Yk43z+JFl0ejSCJiBw} zE)GPpgSvF-Y=^*Fo}>MPqxIZXfTQfdHxH0EouoVR%93;|&6`2o?CNkGE+0tSCVZu~ zW(V@oJ#VIweeQ6mdBg2`AR2*7KC_u88sy9%6yY?GO)r1?V=ryrf0{K_tcl`6lxB{d zW1H45%bK~TFPm%~n@t+(a4f@R+2q5V=gm4uGR!75c{7_$LYSd>%P^bNnzvKuVySkQ z$#jlSSwY@l@)g1he>#Vtty(vh2EOn&U19>IPVV7%q;NDAO0`5H(RaaA`gQr$^!fXh zDTMR8spQ@!&0F`!4p8g%hv+Z6=FJurMQPjki=$QE|G;66mRO2T9))%dzH^55P8F=l zp!$>HK(BomgTZXl)dK(hx8EwwTZ@}-R)pv7JMN^|n2U5vt6P;{944satldC_(TZ$p zmm4o_I*}EEqy61{J)!yoH*Ym{x{g`J(;rx7wTHiLu z3M)lLY8IhU+d1`z<T zs+9=afJl4%5An-&lu30KX!y$^?Lg@UrD@PFgH&!ePuiyMfgj+xzs|8a+9g$v!p8QG zj~D3C#gEdp_g?F=)z2R;#pvjpZ}G)xeUGV%L~~?8eP@PmvUw; zH`kFjUBTq@l>TE2&bfG=qc(CXBM!2Y7*(61T7IvjEo&DJYWR>YaX+ot){7E2t9BCa zQ9UJZy38bwayw-8G&;qJyHh!uZ4yU7FU+Qs^KR`;5j?->pjBC`W7eP2d*<(#uTa4_ zp5mNq`fvaHPY)i zjc)crC;Ej zcQ5XwNRBW$V{AQ+EXD6IR!3t(w3$&Fq%pYiy!m2skar3Kc?khuZ7TH!W8-HV>8lsN z%FxbwV6um`C50mr7c5bbMr;~kB!cGW)SAHl)pNVdrR}S>Qxu2M!zMxd$n|-r4_)7x zO>AwNH)vSaeX@=|81Mm=Ve_ZmY1`ZPqn0S96>s}HhlU$2md%o>)u|c zd;rbnqadT{$>~(4ei>@@6#Stw?`PbCoP`^FC1;MRl55Euwk50C_cP&7j=s%-L3lqB z%4UEO&DkHXU`<<7yd!Z)4-mp8lNju(Csb(v|NcA13BIc`kz-2*9{+gf z<}H8H#~=TXlY4hk)+xy4hQIG(R}#33lq^|7^-*kW9QWTPDq6HCZQs6wl9H3CcCA{O zEQ4_0L3ZQV#ag?9T$jRy3M(rRGjYs<7`c5UaLJn<{yhFcc#x-J=%ZH{umt8O$h^)g zN{1FT>BBMib3`6fv`ICA8;*`q9E~c!Em5j7l=WcD9Js_rdTdUlhQEt3!yD#?p;U?& z@LgZLL`4d*cFA%%j5cXA$Mh~>iDzb$(7fphzx|+h=-YMsl@?Ja5>JQO4JYDm&uIeM zb~(s7*4Xa>CXI#)4pSJP?u4*2dkUTUuK-PB(uFX!=1l|I5A){1!*@{GO0l$aM==gq zV)A)Y3%lVL_gLS()S~TCTJTL6Y^8bA#6Ea@-rP#O+#m=9DFQjM zsl3F#Z%J_+mK#1;+V9r|-gZL3Q&)lF1u_u?tOg(-seEvzHEhbqkN@Q_n4iM)vrh*! zX0|ymB{78(ViMRyi*-Ew6)cxBS;85hDfobHdr|@i^a6f-MdXjL`{c!*iKXc+r_;%Q zPAUSY6Mrk5xi2(JM>rN44Q&NNm&CvU6S=j~4~eBMwo?bG)6I0`vF zpDuAUZsg#N;z;=kr5>dG^hXLLpOQ!~u9;0|nT#N<6VhX)N3ca%!}fIjRh9fg;LPN0-@bj+zh7U~&09E7)jRLJ zPftJfq#|dykhFU#M^kRh;rZbQy=KiiHg)Vot#50ka+@MyHiD9sySl6*QfoONTl;(3 z(Ji;MRQ~yF!X;1pf+z%=DQ zWHTsK3$T$r!#F7mWuJvGC5+)Tpz{r!AiR^q1>&2>jqB{j#CAg>y$ZQ$@@&i z40BOjIKZ?N`9X&a98M~WtnaaAQn5$MZ*@XLeKFhG*$wOoW=hj8%{~ z8d0f~%n7yUO{-0Ncd;)M|IW!zRGCJS?og^$JC?O-e{&tuX`eMoH_RdVBolsdk3YGW zuD$sL=Q9eUdDH9A?j40Wa-~g+mr3(x5N*59Th}HWZ89TCw>f=+_UrR%;AG>?Il0(n zW3}Z~fY) zzLPYq`82wC(iUV!ryVd6xQOHkYSNbz1;4?`=iT`8p$QIyrkL8Mg2H&9!SfbtcX%p4 z9i8&X+n0NJzpv%YTpgd2H*W)$7=yq#Z9cb|(tqo~3IN_^@$YGH#M4~ z`PO_z)ZmAWYs!>Y>F&GlQVF)P&wl=e!E{T@o2fHv)^Kg$G(TT`{WXo}v^J0h_|1=> zFpSf-Ot9hD5B{v%CMZ0zLC7Hv}dNOiuY7A>06!yYc*3b4Ddr22kM;Mxv+o0hYa zxYAnL*pjh$Wejj*f(@q)?+>6xwaYtX?auSOT|vMS528(?J*UpcQZ4p-i@BJ@zrmk? zFD%0NiM4C_IG9Msu7{~n)6;y)grvdzXLhJP^lu?r{AndRu$PaXm}nWkX~v~O{a=ir z2cOtaH?=yhv~-sx=>|z;a+uEKt=E%#=$huI)O%yjaZ>Ja^(odKz{F-cY4f$qMBchF zd3)XwI1G@I*je$5rGS2tg@r-IVESPYpx`3 zwtW#cKxu0_Q&_?#PNog*8y_aatTp;-ifs4}g=zCi7OZq$7@QM=Vp_-e>J&!Yx^=Qo_#o^>bXT#(`Mmh`xI3+wEu>)jM-=IVp1vNICaXFI4?@@5PbepkVKRJm+%<6RC+lZ{A*%;1@9 zJagYo)Vs6e{7lxsCCWz)$wlp&L)ZiIz=7!}pRVW>0?x=pW}mmstfw$4>Ar6yt zn+L_^0ZTkf6`VOhH6`gbCJIg79`C=G8n!$^ zaTnNsp-2+_x&A78XLb$NzHz`8kT#(8F2P@qK#cS zH*f@{@LnGVDM|yEz(L?r*~YU$;WG4K z)dtl3s&W+Jh4$?T2etcl-!WSJ_kXnYL@ZrbvJic8UlS_KaU@(_X7YwH1fMq@*>MhOw~E6ba&283jzj`1@hLL$yGJ>b;E_|m2b>@i^h%_rOYVt zjG4TtU{L%lB3=H(`A^N{?W?br(&$kmjDS&m&*z^nqzfF`x%U&j)VtSnzT72?zoN2i zqKW9r)mTG!U$?IIx@T$pdGJu+k~h^j?GdhhosgT&sD77W2ADf8P){G585w{f93n09{dkgt9zbP0EBbM z^;8a7UW8^();@1=GUWGTVU1zU+W3Hvczh-%xO7 z^7br8_MASbgR0V$?7d}Boy`^|ibEg}EV#QvaCdii4el-*mjFp{myNrV;I0X7!F5A| z2X}{g`OY~tH8XWj)vdaJZq@wl-Fv;OSKI1m*}K4x>z&z}#fEYWMpaP6e4w11wpzZm z*}OzVilnFJt*wt~PmO6j$S@%x!9#kcmm13R>R2sv{$#D+PQ)#xLXooMFU;!i)al_& zLx)T3goO9Sl#ZSMXqcpCIdrp*jL7i;$FMlBoHsNC%m0x$T+<4xnQ`P|pjXW2rF3W| zR}ljIhEy7a^lrB^T4)lS@95^cgk-pUY>VyXj}}=Hc^v~A-Sa#5UCqUZ-||S6q8QZ9 zC8vE0s=AsW7TAz4KDj?>5^-6bN@uKt@_#h+dxUVzemZ-?xT}~NM}-s??gka2Mh?=Q z4}9f~nj|#91&KA~Df&E}klNl23t^LvM&9k4CI2w_f^nybc_M!oH5Ef$Qxfr0UTr%x zyc}*fWPET@R5%et8W0=yg(yZTPejEiFZW$`K_lX%>f6#%QyN+FLy^>+ZeHen%{YFS zJTjefq_JN#`SgAT3!7emf!@LG_`vyF#z$f_5_OH@>|f8;dnobE7**wdp~EQ|9pirP zT@=nOFOPR9oS+d+0v@l15yaAQ8Zk)V^Bpcw0;cS*whcrSG7wE-+igu=$_xdPcybxf z5lX9RuOZheSI?lxz)P*W>$?+g8asIlgj^&MkC!Gh*_yQ(UZnQj7reSum12>2Re@m3 zRbTKeevqk*RE|buJTH9%bhOk>-{yieU&xN8RK=TxQTOg*InwHFeJbl|{qpAX2hF@h zRzB4?0)+zYMD~zxQ@0QYb$i$wVj+p<%WRpK?C4_IoT0!w4{+I;uV%|@5M7ZLrL zozd2FlcKUs2C?6P1Nsh zQ%L!cC_jyUKf?Q2DhAxXE!Pw`^@&7U<>H}t>y+1d8!=G3R52gVuYai{m-mG**8ui} z323Q}zQpSTMIiky|Jjm2cW!;b2M~(oxwH<~(5a()cyvrp@v~4wpGrUv3(0=+&>22@ zGNFEpD%vp)M)(jcdDcn77d%pzei4>zCDa++QLLnN>9pHt|8P?`y|9|{58(wDX~nRm zsWSsFi{aEPMz#=fJxcF`ISF1lo)ZbjFC9YrZ1v#_&5=#N1log2Igo7n#L`-<6#t0R zK{M=semtD+^8}n3kmkq2&#gZ*<@CTY@2E5E){PE|DYqCo(8!&A$;t|K>`Ua;B`%#~ z7;#=+tbMP=sMfQcA4%ifU8AS4?q450PUfcygrChB`<=$;U!L~Ao905r^X>|KbSL^{6gX`UXOq)daPrgs6F!6PD|+(q$youlW0hV%=_~ z$+4_}o;I5{;P;Ubv>c?5pnF40U|bSq>!|)q+#)SxfWdV54W$cEA=ja)^B8%^$C|2h zy7WD2z-mC>u@R#3bmO}TRa)4VhK*o2inE(wmI>IDz56|ANDxIS@~s_86cXaQ5Ua1w zJ?HAF@NfwXC@#7dGHHR&0>z+u>is4}CbM<1z(H>gzv*lxdp^S~k8JNl5#{Vie_1~? zlMN^u!TJw+;v3ixmOC8-$Tm#jBbtdSjy~#p_Zwnu#p6NPgKAV-MM}>GU61tK^>SX% zpXSk2=+6<>dC_#IgWhq$fz1DujCs>= zmN#NKkB{EZ{=Fx-{8{0sS>vI${5WYwaQ5TqazdwjrgE~$d*MjUMi_={iN6vTsn-j;*iBY=uKH}(`zrYs*Q`-{-y^t; ziaUL}^JQg)>Vl`AE!oopGynLEVvpv-s`JUpB6IpreJo!@MJU(km2E-8fCo;F_QK?j z3Ro?`@jOWeFjOrJr;j(>_gr`grw$WhzmcLSQ@WGW{O&>t8T2g${IeP=HZ{eW0=!Ha znT0wc<%oV!*QjD*HL0St!H{Hzu7e&j$JUeX{P-1_mOSZVx+(3bTL@j1<^SXbsrgH+ z*y+rSj@wUp>TrZ>uyC_*rf^FpgdHK$SZndp$p#n%Cw9L)8&K~_%R~7KTOS`>_FaS> z27L3_=eb;%cjV0CvWnjhdJz)c9x+n7(^L42fvm=d!Gqx) z5ajQZ37eAa4l3Bi2N42w2VKi-){al+kLxm&0NVlqudr)&-J4Ek*r8Q=VnlFhRNs1Mm!sorm3LOucN z1KE}K#DW$)dOvv~BH>t8tr0RVxAzi4Ew0KTnY@a|K{P=sDHFolB zY?<97|M&u2O=SwzPIZ3}sb8`{ zGBVn;veO8FGazq`Gv2RiD}Qt%);qr}_R;q|gnIILq;nMVi%|Yy@<$WqE?LeJIf*Nt z_o_I^irIiYsbgC0mxt1q0uR5R)3f>abDCi@xp-IC(JifE>bth!t7V7xf4Y^AmKkoA zM^Z%8I(7<#JLg}9OgSr*PV=&=R?%4*b3;AK%97C?2TJ6m_>{zlnW`CFUynJUBHr;dGJf51P1)U91l$Vnre-kTH6=wWSa zaF3-J;4%SE-nx>E-*waQ&7|Uf{SJncRta zbGR8X3&lTzIX^)+-*E&nN-l}pEEA_WFDX>N4zR-IYDOpMl*N?gKK$ey zhfhuMvwfTjF=EiJxZOgqmy}o|4P& zmW7c2{Q3YC89RLdkz!yvZe5Qo!~aRa@Dhokl%*Mb874+Ye`2)kgz{V>^5=*$9mg!M7-(X%EI>gP7d+3OpycoEX7_3Qp zjGr}0$&+)H6ry%URjBp)wenS=D>Ugc3l*&3Xw>{P8kR?4F;dzwCYQKD+UJpmj+K1u zTjd%Z-n-s9)%xL?5RatJJO0cP{C5We4Os@Gy(d<8Ue{SQgbmJYSAyerJEw-I$|k{& zu3cR)>K6#AXv88igUa=`Va(8_0(op+>Wf7+9%rGNrScIYeisvz9tl{Le=TN@A?DA9 z-Szr+n3^F5Ut0udxa@B8_VlB7L%n+EE>Gq3bO1)nm|J1i?`~8QUZ6^wUoKx(DKV z9w1mAk8gJ;etWa5G|2Q`rLD0#GM5mSdV5vW~z>Q8{fp=W{V(%;iZPK zat79$uvCZ8o2W!bp8yo%VBr6g7n%9$JKg%xy>Jw0JL1 ztf$t^3tmiK@Gn_*|9Byoc(rKgt+=xvji_ zocAHJ=koRh9g~PmAQT8oQtB=j++*9~HWyp5P-rj~@U*wEL4xU$5#1WQ!Bcn>8XV7q z4;vbsixK~GVv?BbCn7v>k&FErpOzJ&1Q^*xi~kb_15NJy+BCEp9n9p_+Ui~81cC=t z-Hz#`=~$p`NYCk*xZJ>4OmxV=cwf#|riX$o%BaUHTT?I?9i0y)Zz8b*f$3F2Sn*Up zgAxb2L3ySE`I=w5K@N3nwT{I_x0t-IB4=rUHn&us2QB@Sc69Xf)b@J`JFHJgLmzQJ zWwR(vxB~pU5xLQ`Az=V?`;^mYc6njMA+JrU=tHmEu~Wc7ufKBc7hMb|Cc33K5KOk= z2luJtj>Mc^4Bh3+Yah}7+6Vod##|Krlmn)&{L&p!_Dt;c;i>9T&pADbgao>HZ3=y* zg7Dv|jDw@0#d;=@hDxUY55aDYzc!)3)GZFc0TS*4oQlz3C;9C^lRO0e z7ujDOuitr$4t8L8T7_2kS0y8J!@*u-in-(f#PFZ}QoE$FIbArw- z?AF>mW9Oi^0c1Uy+hK8|Zk)C2wx9xBXbc%CEJ1PkKZ>SDE&PVq;;s zFKv1r%on5JvP=Q0pF7-zC=(SPPWyXC?Wr~Ib)}ktjOKvI@7~9&d<6U+WzBK-m(TDh zI8%NnoiYdW)rE_Tx}gr7_VdvQs07KBZgHRzu`mS9B3TUmdMi{Q==PC4V?3tiu0i8= zyO+$mYhY^bp)qyafeQV|y{GHE62;!_v?7X>Pv&ArG--AnQV(!$GYFuWt8{3PI z+x8YoZ_+*7vJi!J+D&>rP<{Nm#dWjHb#vga7_-drK>n4mclYiOUU32Ejx*`|qvmxY zoYEVh5L*_O=SZa`u+7XS^@qWw;u&jNeIIX6x?lMHkEV+iYZpLh2V5>$=URH#pN~C+nti{32lb05WLvNaAk049tg8UU z+gk}PviUs=KK}gr3y7xLZ(K0eaJ#-gsMY8We5xy?D?RDyfNk}XTs>8F`Y~P_yl71F z)-4r_m!Gj;5i*j>d~n$_F_)RYjfhw!~W>4sRNaqthvrMhsoW5G4rHcI=P zE?NOZaD4NjIj(Net(^Yn8YK(qAQH#Ybn zJ39ty8X9$fe?hHU<24hbMB0%#_Sh@9O(F{gdn^yws}PbQV%gNYp2?KQ4Up2IA@QP* zd)>)+C#WFItqi00TMh`NxRTHN&yO#h4vTCLzkRgjbhL9XnwV>janDn;ZID44NIvG= zZbEhXhPafk6Dmn1^`W zu1>A7lKDV%zIASZHgha*we3n%^)i#bQsF9UeheWG!v#<~6OG8_p~IMW{I637Y^j`X zuuKd=@fhX!mU8pm<^IgyC*2maz`KLm?uu|i+~u#iDgQTV}$0HVe>W{M!eP8 z0^T(yJOjrr-j^UYCa;`C9&42NyOW+NJDBhfWiPS0?OsO}7CO$CY}dwtcb50SK0IW9 z0#J!ckl1J}3+&biQyiX-RXvVOKw0BVjiNMLgc(1O^Z=>dK_cR`o%{Rj=Au3c&TX9V zz3gyjyb(3^PX3SZk3q zflBPQ+aoFR^%hN=kppjj3_GCNaItbG@%STL@ye1bVguz0H(qUzwzzg zMvKWt9X@KdZK53>ovX|~^I`=7A4T(svSaxPG<|<@>lXb!C{c;yRO=V`#+&&j z+t{a6F{8kR3!Jp?yLY48h?Ob$_r~5KL=D){DJNK;nQ^=v16_*y5;P_e`+-N>)=>nZ zQ=Z84tpo;d^wS_|P}W#bzZnV=h9b^~iM;L5c@%TlXV-Y4A+!OOEoD;PJ^nv;VCOEq zPF!=3(~Vxh*MmGQ5UelU@r&JDx#hU+K$F-dION`Gxj}9iljTUF;vI{qB>Bz60RNWb zU@Y-9_usYyZt-KF>f8-TJuWK|(If@yt=kUcp&+PMZ@>&KLV~}tmcydS=2wAcxq7jp z@SZQkdv~|+#GS?(g3j)Id{y-176+&b8QxptzAk3M7BdInLQlHNEhWLj$oX{CvIMp}1t=JD<#Wq$eBZ{!hIi#CU!<1@ZnUVLsk-KOyz-gvj??)A|;cC)hZ z)E`LE{&K;E)N(sRfRN#Ru3`V%-LjhF9SrAr2|y&&PN1VT3hSICp0Q0K-%C}T_xBrp zOZ*l_gFx(b)javU=Ty?wz*71ZXK;Me#<}Lj&iH-m!q%{!-=_|}cxbsE`X6H%92^0m zSjg2}{rjE5*=#bVa2dVDD(6Dv&UlBhoYt9dZ9_kDiOr_er~Y=@`1{Z~H~+iSs;&eV zh$UvnVsbvLtp>|6#%fnxQI&9|?{N1QVH4oUB(ScTZPrePnSFO}-k)Cdf;UAQ6eiH=AWPHeacuA~iT|X^?wk!DJoY!AIykf8M}~E^AwB+{to5+2?n;qXUl1 zT#uO)^|`V>R`glD3h&<9k{?Ay;A>ZpuGs=rMoTmSK6i`3Vo@=zOAM?znDpKkpG!}~ z1Ug%9E{FWkmFD>ZlDs7r>Z_kupD?}^IcFv9eoU~z{Ctmy)LjT8Ir?Eob)^vpE*)^}M>(oH-$W1Q~< zceIYEnRl%zS`ppZZ3FN1CwtW(YiRkgrunvPE3w(FcO`5yB}(mMrM-%KF6W_p$yQ3i z-0(l!&x~({R(%hu89sl>z&2>~)qWAT)*o7Bb&9pe1NMk9tuwIoY)v$V5YQOLFzCdF z@}N7em+ezldD&-v^bQtCMQIX15w}R@7U(}I7!8M}bbt)rCDe;b^)z2KjkJI|gV*V& z2lmeFsGWVEJc{14Ei7kefeMu_c8u34(wZS8Zx1q>g?7ptVIc?f`NPWR2==s90y~cm zHS>f)@jUjTg3b?QwQH8@|2stFH6RJ)%h}E7Z!#mIA{;{~!o1>ys783&Y_R2)_JZ@22mo z%8;xh2~UNpgP7W3Fh{+e%IhtZRyD6P1cv9UCa!bZ$Z{-0UnZeK_(fg~EhIn zGwntVI3OrN;OJbr^S*6_6n)J1lFVr^ArkhMY^mxiU9qJ0^eseL7qp*cw>Dd9w1O4W z9HW)hdmo$S%Yug^;5-bj?-8_Wy-e(4aDaE~K7&^H$3Bl`Yl$5zNqp2S~E8MupUzg=u~##$JHmAP%EM5QujBLia5 z%!#%^VXHmPXkwk2T=pgN#P)2A?rR<77Le5gxrc2MSm0f}ssyPzL#ryU71VRC19!9< zoOJ4~hf1M^#der&^chb2_Fm&fMJ{!yMm8*v-x1qxjXmp3eEu?@YRjC2_F|{}K3H`N z$vJjvXF?smCwatR987Y_HY)xV+B(gtyFKKjqSHs4wZ}!DEAq?yl_(ga00(!u+?f+e8@R#P4Y@zm?ae78Si}l!8J94!#BFNh1xjdr<%Yx<5&5 zJIcA=BV?tH_$T7$yR>?$H9d0Y9Jm|NjGD2|u_G?$K4RPE%a2ENnbdzjDw9RWKl4h# zH?>q{!gt0Ca9lOe>`N2IAB;PMrfT&L4etWa2X~7^`=5(U?}omA4qWKv6a9qlyQ)~_ zami1-3VYIDo=+L+kWFSxinI4w7cU8@MxY}OQAM0ppcVUh(uRt6aQgHst)Bn)w*u)H zg0yWA=R=!b(#mFbY+|~&ftiw_^H0*{xN8B5tCr+$JzlIG3t%!do8td~Ba3&futm3d zqCgM2Ha&uUYXEitkObwgfch`+Kwa60d7i4ZwZ$h!27dhpI5>F$35MnM@%)?o6;(*! zSU1l~!35Hp!8fixM8`L;uJEYWlZlDqw3F@I*p`?85YGkUqq(A5D(-Ix;T^N!Lh&Z7foA@rZWY%$C7>t|;80l*f^&2`Ky)5$9t z?|XIqL_dJ?XlDQ=*Qfvh=!^;k&@;bLg|qmMG8LO-hlu?ORLvQwFF{EGLlYHHt{_$g zx}lUECywk$0o_KUIvE^C9A9zVCX$5vEh%8&=O1h3!ES`FSd>AWY(OX)wBBK8&1i9N zARwQycX0blL{12e0RGcdy`t=<$AfC_yaB)JQ zwCE7{wFwsx;=iG#*G0fYjRc7N-*DR2_`hID8_4T-fWL631Rg`t3)8&*krSI_p zJnFT{7+6)x|3dd9C;la?&sqP5)8cvlMYiDo3g(|h^#8G7fO<`5XJ?w#`dsS^ssP@2 z&I|K!eN3MiGEb~M}MrzpD(1m~cRMvOpZIl!bz*1UG&D3qe0+Kd zJPr(%+SQrn?^8nILOQHzRaUW+!3j10*FHKd_|liy=6UOhlyx}(=a zeT&I{aYQ3DSo#%z4t>R+|Fc4E_&7-K+o_#Tf6B?rt9S952K@Z`z8>uRrYICRzxZI{ zi-DYm^vv8t1{M}YPi$~~Z}?yV-Y$qVS~bwW0oXW=IdJ;^Po#Iu4dFZ@fbt`C3zai2 ztFhu>*i=_Y$8QwyjwBWcU!rAK(xGYcCEOsFUVj8zJ^LyAHwQ#qOlP|aeI84Q~$ zI^i*e382n$p~2RG{6G;&tJFw_ZvdI>QcDsC(QW%z&M)c`AI;6*gJ$5+dKFIL*%AMgbk}K1f#lJL9*}J2 zxVt3wHf+AKR$p@`@eE;EC~PsE#G%-lUBB81I(fzbh0bAA!|{iyH+ zoW8!5&r(f1)}MgMfCB~u3(f3;&RKt&F-)k>0GJr=4fKDL0`u}1)}BmY__}m-f)wnZ z!h$LJX8dH0C?mH}t&&=LtIYa$)W@iwW)jrxOesvY!|dxyRJ6%dT28$a>U$1N$~})? zZ$R(MuvrRc!0wRR{Gg+sCV=ijZQT|zQScIJb6)Z$YyRyXBDJdA@t#Gw)UkArKl?`- zXDiTj&;on0T(;h|n>S;zS7>n1>y5|ytnTab3eOWgF;!o(!qjYyF@wS;!ApQUsVml> zo%Pt1X;>6!dtDuSHz}DqYT@FLxj}ViGn=Tnqx3hnP-o zzBf$0AhjfNxEIL*DOO97r_@f*w?|78Q3UGiFN2bb5kf_erJyX{wqhnk8|-@5BQS|I z$N}o_N~C)IDqz#lJ3gT-H`@R>!?_KhV;*`!(%n`>Wcse}oMBMl5BZkyqaVp6ma0Ei z)1$TfPF+Hb%$p2KkKcVKTD}44kCpOt%w)WX+N*F=W+_}=_v30;QYIoAz-Gvdj`~cG zl;|k_wBmk1VMdf5NKaN#F?!?z;SYkN*ixM(Z1a`6yBJv-nakng2fr>JW{)YSgGVO{ z>(-*eA_C|bfIU&~Tuouo*NY`SHqj1&6DGcXXh^ovd$uHLqQUuVhuAAI_u8-T84)}- zi_b$vokTaL@+C$d@j`95!Jo1K0QGCuc7$>wKb#=I6ZCrcbTHKGYJlRGtH3)&5}hb0 zD46ZSz%iINrP)CM@{Bb)kvXR@>Y({R6+wpvcVqCnY;Z+O@chRYAUM0D*B=vijGF6p z&wIa~9cYVyYLMPMD51e3d}WF-f^0i5Qd{ z4l!}{bqv2%s7Ae<62x=_{#?6Szflk-j8g*rl=Ss<*VsZWNwPXe_}6)4vMxIeLVV?I zCT%)#HR^S;j|j{4AONk`6RA*Vii$2&eg6}ZX8O-~@5UgQmlJVc3rg{j=U zXi+d6_*>3jWm`p-S5+&`D_WCLg&c~HQ8h0(zFBataymWuRmThd%Tn$T3~YFuJYw28 zG&*2`!&I~v)j@v+xN%LqWF1{ErSO`>Kh)>V^9{uUj9v z8=Hjcb?eK?C~!Oh)Ofo4Si@0ZBmuh|-G8;q^-#as?w~OKtL<*+|Nj=i2NAyoXgsYs z4_YCrOGuQ*k62{xOn7vR_mUN#$zX6KMVW$)4m-p0Ld&q#pryoYzha{x%hygPdA_`#huxkTtK63j0e=&&*IUF)#?GGa7Gt>FzFKGrF z-Taf!X%C#2hqNZIYz;5a<`X>K=)VqOTKEWZVYSp?TQB)3QURe_(|qFVr_GbgXvNlt z8nNl^feCT4!AbG$S;vr<<INr3D8(eSD~(j9U0u7%o~ktI%1G2$JI&|E97L5(*A3APklZ5QuY$ zs4<_KfURjOvc(rr8%53Z$v7vGXMFSBC7985Q9kt;m>yle1ZuWl6I&e2J0fJ2yUI(X zP*YH-M?eI0eb-k}4h{LAV{e0MP4*&s?L%IsN0obZ=MbpFuarZ%CJcnwsE&20Vm6_dz=FratMXnhxt=&>m<*G~!a2ugK*e^3W z_pIafFCH@CB*)24Aya|^#qF$`cN#_T;u}X)M`dWlwP}D+2VXk|N*N_YAE9dTcYlOz zZH|!m*oxe8&|Ud_(6(i%Q*=;2*ZKDoui+i@--)R{*rupGF--Ua)k}eFd5Uaa(8CnH zRSDtlPo(wf&1`Zi3Mp*C!tJMm&q<4z>RjAXke2+WA5sY!{a0fl69_U+KfN}`R7kU< z2Q#g`cs$%HNqGVl{kGMRzIvL|HTNSGsCeSey^IJ7;17N*Q_jbk!a*9ZpVCEY?D^sh ztVDq`N5eb;I-kO`ZB&R&ExixQ2WNOYNYdY;m%h=7Ui1t%s1FX_f#0>RsbS-bsqjRl zTuhN)$N&cub4`q4Nk!}OMK*fO_cRF@9#7+LEmRwe;px0SMg(c{%zqi)bkJnD78kwf zeV@Los-x8kiePYeYw8Iy%gN4SCalmZgRgs?UhzWmSs2=`S1JBzb`3d3hXGh<(l~!* zA@(i$QpPV6=M-wc;EqHz1$~;pKO!r{)8BYLgX}eFb45! zIwgk6kX(-sX!b|1QP^Uuyq_a>oHQOzK3EpCrZqlhTMt95?^`DFU*SxJ~fw$^tgy z?ekIAv^q=T+Gwob`9;2P6>{H#UIO(RY!a9ZTT}xdj(9Yh`y=0hQKcfd9T}dVGo`4U7j}>|~s^ zf-bB$5L!Zvs&qeSG}liG8|XLKm<>Q_ZKCiCOKenstB2bqwCcSdRB#htc{%q3W4(bW zO9os#_~&od>ys_Aw!fS-DMgB@&>V)n_p_M(i#-f=FfiFb2gZ`C6yY~!DJNoiXQCw$ z3(F*l?=4qKCoO<9f3y?FBinbTIDFdMcBb3k@t&Ui$WCe`ekaAUr{SvZH=D3v4wbDa zo*B-2OGEZ3?4(MSsKj>GYJYiHbjQj<`hB-eMEfl~*5Aw3q0N&PSqi|)Fp*N#jB&aHSQzeuB6_fQZCm`ZB zlQMYZX_($DYr7Uw0WU@4peIkz%hQb|;L#4PRq3`p;`Vg4FxnFWTMW2b>|~ij;}fbf z8F%Oo#F4VP?~Ek?_Gko69ui3jAgRK&+bDDMQ0s1v{R9fmP*IHc0KWCmIL|s00KcOw zduHEwln7ogn%6n63&VcWJj-x2sI!p6=dw$dl#+s9MQbItxq1H(wA^5OK1`cCq$2V} z6^Uk+G_PW|_IIw*dbvd=js#%}zCPyYO!C_!#P4Jn@L1ShFbTRMl$dY_{6iRdAU=hc z2Ma2I2IurQmGtTm-RFV;DoYUb-e4(V&@K2jiNQK@PxR8e3L>z$!7So=>c?arUq-TL zeU8W7LrIYP(CaG_l=SN}>m%X}1>A+@ZY`?HkDHMJL1Y@6vx&FEvcj40FfC-QFMEBo zP}t*@(nDd@l?0t~;Ge?-ZBHrD>3oQ)7Np_>?`94ytRy8z1&Aww)az+Zq>ev`!d7zS zcs3}|Z_LJpHY)BuK2*4QZQp7vU#W;K@Eo9x6r!Nz#l%|v#BMtp8{A%Hgb8((uSHP* z+I>ssaFUa>fW*!Rbx9*Zh9FX1MF|&-X<+Ef$wC2}sWk!(rSe0UKVd--*rZ<^z*CLD z{aJ;BG%u(@b^$o4j1YUTk{>M8@@Q#-6?pQGd=Da89W~fUStdz^d>hR8kzqF+Ht*9+ z0zWKdaoMMqA~y-`8HrRAzOy3Mr23lcy+18Q5_oF{%YAO%vpZ)E_sIeZDDkWY+RKkb zzFNHwSY(A8fOCd~AJU4jsu#)TM^22BuADdEA|;df zpNiSemZ>#0vD!#A#sUxf4}@@xIyIbWbw`?hZ@1=OEKnlRh#79yymKYRdzh!r@PRvG zkMCr-(fh>sZ54AJ;pGwFJT%_3L7~>FPCd27RV*HkoYM~1R*$K8x)Y!>?>(AYT?gAP zx%w;5)3D8hzC84eUZJ$7#EJczhAw!Ufhl??6u1M+8)vSvwvCwdRw?a6se&U;DCoSJ zszrLl9(*=*d`7OQsOTd@+Le;X3HpJWE0}}SEVO)*q#|KIGzr9XOL+_bH?8HyMVF5H zYBWsij95Cfa#rkkB6TuEzgRv}%}>G&QTkm5F5II_^GO9fuTIQ0k=$=v>9rB6AfH;* zX9-5XwJt$PBz4xHMAV&YX`J)JEMC84{+to!SFvKnfNHM)<&ft5Fy_S2;BOl1M@^@G z6|G!3!HF+9)&pAUjuzNzu%y-l%;|(0%1M#(W$?=TI7kD68nD-ZD^UvIy0W&YY2j`Y z=fk+UOg7}i%T+Klot19O&PwWk8ghqU7UEzs+4MjH+QI<+Eie5PjS}*yB6L@qr#{QK z`i;Iw08QJR(DUu4+itdZ<_ZqZ4hnujXDEMZVeV^_p9#saG*0WjkXM#ILcG{rEp zl|7~Fyd2zi-2REO1bj=>0oRq_0c)fVJxZnzAJia(B4T?w*+y4AYN8t(W=>r2JnQ}f zIzV=Qn(peywOb>BBn^}9SVl`h1cgFr)MCdKZa2^NCiMoJU%@n%Y&~G1yLUu_bS42; zjbqsYD%!Qr8knb5dRLyjoDzQKxEq%oP0XE>$f&`VFFG2;N~GtQ=ds(DMN9M#rnYhOAS4Im_eR1QgpSOPhI?mMG*ztD~JADL6qS_jZsV*{1GxiYltt4 z?UUztk(m-KxM>B~#Gjlq`2sffQn&5k7rgWJ;S$MIYFcyZIGiu6z`R=RsI?|#&O+&f zkq75`+&3U$9EQaWVFpi~SckH_{8woFj+%h=&`XZov_mbYjhI~AWRQlcBsz!96q$Jx zqfN6riQj^OR@sFwNz?bxmde_$WhhW=({~agrBl;I;~`FVg&=s(#{p zg^T>uitAShEMgHTYJMPlG>7wE0JS*$VmD(V&}GZ!?)M-yLi*m#-|13VxpG&IyVJO_ z`5x$};>||lN9DY}P#$8_s+9*)#pE+ANZQlOV^U$)bzbwq=;UY`BVy1YxY)0`#wxg^ zlsFBEN>+L8F5`zohgfVW?|=t4IozX?0X4t!7GMFScOAaz@isM&MzOpuz-si)?E*BU z?#wSK#86<%0lZrbHm`^^zqrf8Uu!3M!T@q`IC{JuXv~fXCs!-ccp7yzDD=UZuZQp< zp$y8@Zlm?g?J2|39V&`)cw(aB1UEH_Huk%LRDRCT=KP^Tsu)L;Jk3A}SZcxyqFBgl z+rxJaY0XcaW%jnKm-)*s=bD<9O6;o3qe6mUjh*gT@>!NX4Et^FwL>uH+ocQzVn)S+ zz?mOhsu^&L5`Qi|@*FK!8e*1IU?-RvgeKfY>xemSoa0(US6?K@I1G9${lx2-R^3%o zNqLzFW2r}ssfty8`|Rpc6Ynl96y_Urn;w{JIuY+fX{{1$gXEdjCY=x)-oL(lH5ClNNeJO8(RYiajfl>m*#ow=oRPTa8$+ zwGHBW1k1=Nk_a${Lp$@+Hv{gIg11w2u+g`AaA68-V%aNsQ`r>lU8}vI$6t{cntG^{ zP*8{%VV~wGsq6+h+&GJo91;)JkEMMm-U^Jm4kxN_N`0wxZ|OuN*5e?780j`C19x_cBF*g}hDej!}=zw0zMVnkrs}-p3c>)n>bC~ZzZe5!dO;ohx%Y4nEEra>l@sTf$U1q9`C^R7Z zhfJT~*M7eBEquQbdLN3<<``CWk65ix@(ZN{!!-OSPSQ2HH9n-y4$Tz@o>0V5fI*^b zncA!+g7z1Wgx-=g)$E5#dYTK@8(&jdm7`hY<=NgG>k(dOzC^msh2!K_fonO29$abi zc>@Bk#m`7GQJ5{RtDCtPVsr6|*YA^NWuU*gO27oy1ccYQ*>8VU&sjM8c!A7jAzqp# z$&6kW;)AKxDysEnKoC$L!TyhDg9B6A(( z!eYdqx4Rjzp8~*^jCx)EtTTv?E6?y$t4$>FLSFP!a-GIR~l=Bf-@ zXRuqI6Xm|1a_@Q#9_;ydpqzbWF+9ZIJ}=oA%z44+!F%XLY?SELqii?o7_*~<)Iw{h zo|c5!QycP~5D3Dd42a;JnvQq)|AYl4SR7GU%4e__Zy)iPaI|Y7^vJal8N1|27S|;F zzz)`UbZn2F_KrD|!a)B@=QhlWsfO*g&0ukStnIkkBFDexlVpuJx>79~LiIjc7|aci zlFyO;r1VYn{*MJe4IXQI&;@9g(QELx=Kj;LFDzZOVdufq--ujLI`O#~P{UxrOIXP+ zJ7x9xAsdIYh*r!zuuC~oK*4d`yQ8% zlh_hE3Z+vBVPFlRqMmT8dk-p(wJy|@Gwf>8|D}YC_hQCPuCTZ_na)t(A)TL*3x9y^ z6ju#tGI!$n@FlsrQ0moWz3t3+0~bY7d^vZw{A#5Lbu&W zIx~L?e0lQDZzoQ{Di_M*jt#{orbXTW+(}u;^XQR=)K8`3^H>6qH&|LWU|`>lrm^Op z*dr2l?ANQ^ZfDxp4F9ItMXhtZ+}bJ^1XE1l!FED@`NMqPy>b??Ei66;^-f-7FL z++BYwD#y~#N>B2x$QZ}EsCCC2{n?2{0Z&80&7_;+c$A0SD~e4SHtRSQ_7I|6V1?U? z_H)1iSzAX?I+mhp$48c`9Zr61&D~Gbh|??7bA3R8fi%RzNeTH~rAkD$`tbFP zw@-9wvV*$DDgN@ct%$vBoNYo<+C1$D?H_JM6sSCxiW9#s^0X!MpYhZtu%Ly)~|H+mgyVVXC9*z?Q9dJv(MgHave$%x%_Pf?E=$3M@o6y-xicu)DX?sR<*o)=-%P zGo@8xY|0Af%UPL*zp+DSwgVR(YLnv=Ty zax2zxy>f)^wvwbZ`>gaFHl($>XmflhRJgzL&C^ZwUcvwiaU|YC>=A!|`Pk060|*U~ z=KQ&$mAEMug29t66H7$jyyj&f`ZT&~0cSZW^3nmVJwj7))SrvofK-R1d2avW!{N*y z_(mT|5U=@qJIy3>)jg!?%?eHWhSI^0YciC9De4tewKX}AiLl6$N(qH&EN74Xnb>Z zlW%OXuLF|ysB#)Noio7!$rsYBAEyo8-NEi*zX~`}sDYvBW z)nS6;^cjKV1rKC)?Kb+rv<@-0lXpw?y#B|#4imSw_<2zv7m-NDTXG!LUQ-p!7k|}{ z-a^!fSn&&#NykY|^+Zn#6vY;gu5#tiRX^rBab4ZZ**wV3o{x1w+jBgI-qTDjw(Zj2 zAdWR{>N$Sb`~J)<&oo!V*8yfGUbOuebS<|oKaKBP-lvcb@A=5G#bxHJv2AsK#w$k$ zlVi2b$;ie#$0w|~)BE`CJP@CcYg55Ro*lNlbA#=5Rf^&)Aa-@58Cp(zraZ(TJ=+$pYjjo_0h)TnyPHtu?ys?|OugeCP_YmS10D5E)C1WO=D%rp zbtHunvU6C>E!IFb^?AbYnCy5j_&GoCGYnzwElvflbo%kYtt#`dWH4Pzw}e6uq$*|c zs{LK4%{{`ioNI7w3>=w1bKm)V0O@RZSnm=-4AuZg@%0SR{NkEgCosJ!M1>rtGeJzC ziE78W?N(b{d$_gPdcoI-E2igL!#Tbn?$fnd)hZ72-+Ssq1>D|Fjt+6qG^18s?cLA! zi~zM_)h4bKzNJWRHS^}uP#$)>BpRjjVEcKoL2JNiAGLBsT=kgI^OKY0Dc;v}^xLGT z(aGDb@4&r%rA)S=^lm|}NQ5t;wQztq#(WS{u__f!(d2_G{~j;7b>IhBpbmsX*Z{7N zJ7gctp2vSt2+CPV2ut~d>;AWl$g8;f)_SHC4_-0#k;(8Hr~(tjdN|6mIJtW% z6$qU|Y4iO__3J9XCFOu|glo2@+t5#>oKbe{&o!X9W)|Aog(}g6((ntoChLLWzSNq zf+v$6>RVOpbye0U@rPRP2tgq?s%$$M*FX38DlH&hcpigTMgypmQJyUS0}(;)zJqOP z;@j{Qew2!fmO0m4B-1wUl!=>n$e>2Gq-nX5a>asWl7eH&$eDiPW0<@aj=Qko<%$)T z8%q?G2kX|#7yCwcu>{1}kSQpQxCvc112tR&Z_$(Y$T?qsZ4B;CojYeU*#Cj++HqI4 zFfSI4ZN~PBkBgV$C5jv23{4D7m+x|TU;8V%rZ7L%s)zbUfP*`3SIQt4Z1QXDz}03ZNKL_t)D%WikOjp?%5 zwryMa`RAW=fMjil>#n=b0I%G}iq7ZOty|0Y-+!M?r$PIT?vPhpam5KApM1^`c(Z!j zA9%|Kf?WCMmMxk~hilr)>5Wg95}3gByQP@P!bOYZvl(B=KX8@ci#gpR7oBxOW$kY6 z)l-HI9g=P0qNVpUUyGLD7EnwqR*`xUz56fdE>A;#^GNvdH1PlDh^={T^$UWH(hfX}d1R^jJ{8O%qc8qUu(zyobZw!P~#=Qk&q z0^3&>LE3*ErTvv@wAKq=VW?}t{)FRll1>v;DsxTG)A}ZMgGrvULAoi5%yq0mc}&4&b8-#tlCS#tiEgF8^R7 zuV15+4guCVaP|{iSl>$~7`)?>|zUpSgq0{4kjF31ODKjrP zN6I3lfWPwD^yxAUaXA~-Z!iK2ROqc(v66}L>DIlQoQC{C|K-B|p)kaoufHKOcp@YV9;l- zX8TQ9qvPdouqw?jb=RW8|@Ruol!B7o8$yk{)%p# z2V(4dUyH08lV)=+vFNqbZey$!1xyHj0s%d~>Z*Wm_3G6oev6({$|~`Ay%!Y`nLjN* zF1NnGn;GHmJ5M*{SqC!0Q>IKY43NKP2^M(M@lVi1*HCXE8f9MTw%WIE-+=qvyPfrU zXwzPJ;f3-yg#6SsQ|F|%2{#Rx!_L>-^atM9j$dQbi1Chrj!$Jo3o<@K;LYl3f8b4b za%hm2FJD$hk9`a$6K^fS-NEaX??i`koj(*aWoGSPEp&yx6}2v%{6Kex&L zxAm1f?-=Nz@!9!R(B|E_O1QZ?PSbTPau){`OcHjI=~aGy-B zHb&y=4L1pxkAv~MzSE7^aoT2%W&3Cym`>sG#~$&!koa4?PlfLb`?AZ=GdiwzOeCBW zo-js@A|M1v{M|20;<+*U4%aOj6CtrMS{DFVJ-#qIu<|6%wnRq4422+W=~S>D}IQw`F!pe)IC2goC?^y zn=OfKVX1eT0aviZfL^8<5Semwqg>Mr>^Yk1$-!qxHm{CiyYLyy#5uCvTG$?Y zjyXZzzKUTx&=|!rj*8)5uuEa*nWn_}Xz3Ptd-+b}6+}Kmu8*#}Bm{Pi`y|~a!{9P` z6l>hY)yv8otag+I&hM~+Gnq!Hjb1bx&3HjWVnz&RTW znegTqU9FN_UlFmP_|ki`L3fvj48BLc!mh8rV{8MBztiX&hMoH^U2pQR1NO1HDd+u; z@^-`2O+oD&eZTlKK774zcnv#XpMTpfMfN#1pt8rleokYFb-4e&5%AWSZr-Q=6~-=%4=W4 zuD@AnT?YE?x84G^_sqp z^*1jIJI{Q%O)8isAelFBo<}KBpV42P^7FkK`;3*k_uhN6v2J=l9z#(KGG_k3j=y^= zfO`vLWZ=MoIh3~*#87>c!YP5?FE{X}^GwI5f+m9D4?q0S?>&M6-gI7&m1gXnE&icy zh-??;`J;d$kG#G8^3s=%Qs|>bKjLv^^bO#`fU8~mYfWILPELFGfO56dr+#YW{>cvJM(=YH zF(1mhb?fn>@9t52vaiM8X$*Wd>kG5%L`iJEEDtYiCeg_&pgx(G`y?2VissGYaxE*iY7D3;VJQVF2$%A~GPyU&k2T1TQ^5S|bNi@GT5j z=PYxXY#y~H^;LAdjA~iNB$E!*b@D)B8eAsVdDtV(?`8{b@VX1{b`*6KhPTL`W0exy9)syoh`Bqrxb$!pK3QFYOM?2*-|sW`c9>6hT)59;JO;4I z^YS&-<-Dpnl`;bv=Xo7d*m8|7iR(Y#JIP z0iQqBqOr87R!-*ry-(WLs33E8?vo+QR%5fnVdBWr;cAkCe=%4GgH4qr3V9!fBD6ioy#U@&Wc^^~)xTspSsvJb{Kc89GR<7&c%>7oX;5+Y< zms^?b{-4`=oEM9fUzfGq`Y;ftYQSy%jd9Oo?}W?SU4xOP#ws^K32@Vfjh>B7win$@ zS@>)v;G%E4Yqdv=)yUDKvVk|Y?Hsft4K!aRka`wmEa!Gzf9-pszxRHY!p^g&o_b0i zeDJ|+Qu<~Maa||Lj1X8+yh2N17!2?FYdcf*@5dj1%ocm620H;JT{r^;QvUi|J&y>u zs7R=QjpwP$<_$O8AfJBvX~3RHzGB{TW0Y*|hPU5-Tl)3um+P|y1H5Uwz3{>dCXUS( z|IjstHYVfLXb-X!Hf%=k>79l`H)_}iwCTxka;@|(dDYn-pJewaM#G;cVtnc~L*UKo zp}fGG#;9=zZ{i0Nuv^g3?{0ui+27FlCZWO%iw)qo7;x|~j%?&Z4|pbwKj!V$KE36h z!FL&O#X5bAz{kg)7?XqH(@EK{Zy&iEyO8-Q>egse%!ajrOBxNCXU04sSA&7m%>rJe^7Bk53W2!hwgctxISf;R zlbDj_rB{P6=>}t6^RKZUk~Y~yNxwXw_pkzaQvN_Z3i8o(u4 zU}xh1f!sw)OYPVMd8<=x2bkt?tIT38*_NOFen3_LSfxieh8(ZQb}`DUUf|7K3#1(r3s|{U%ZyN^BmVt zE&BI10LvbeHyFm-!cR6o7yVJGNG4dnh9-w!uuCxJS@Q98 z>H7L2`6mesBI13?vK_?^alJ|jU46AY@zj&X4T{DFcbbRXJ=lP`S6+Wj>eQ)Ynw191>#x3M`o46j((=e-kIJTv z8|9lh-x%=(eqQ^u+HyJKH>%gD?pcfJk~wegT;Yy6$1`2Wnjv209K=uORp^2?A9eZj z&p#zKCDo*!X@c0MmN3AX@b;-Eo;0TuJUs-DmRM{yQuh6wx864GP!;zX4i&ehh>uyg zV4(qA<;s_arr5(q0Azk% zzCs0Q(V~T%3CVBo@&Z%^x4@HybEH$38;x2X&lv-t!sq8i7<|uQsg3@b^Yt8AflW>J zZ5)6*CGOb|G$_4|ZyT8XwB#rGX~_~Jmri52PTe}DeOO<*k*@r0rKuKUf7&6}tO5r1 zBR7A)N65swb?eLqKum0m87nQoRFY}T4c0kSR<;*IX@1 z8fZH+29K!}yq{g`%<)MP8)dp}d)|&&BumQi*}Qr4Y<5I2#wY6@0&mtV$P2tF2wq#XtNAAN+x)6TLf|gQ778!^e}I_%D_`!5fah^%e)brQpRB0~NDj z!$-*1c=7W3n$(ilQR9<}jSU;rH!*4c78}QXF&rXWfcp+33pX^Gu_zokBDmIl_uG5B5g$)u`}OMLwYjG>#9oPxmeo8)guyLP>)i`+jV ze}TDyEMzD1F@=5E<@Ic8xF1YBF0cN$R~B#M%P2z9Qt=Z;r3w4x4!;Yq^jhIWZj9l< ze};cSZf68UOha%L03q2U0!Nk1|Kzv* z@-Bc|I-Wlh?2p0&jr?L;ghL6HZi8g7A~6T-qRiN#q%?Vb$zJ(-{ZTXK(~}b!^5nFm zA+sEG>Oxiz=ruFIeMs}{IF1BoL~K#8E$Q;iRaNB7%JEr%#=W_b_W$57K^i?Yuw%H` z#as(6llNazQm$^8Q=CPC*M#MJWbE(oyG%mNoZF`cU;>1MvrWRI1caA-)T5!#Gy??Y z-Qc>pBZ2jR>m=Pm&2u{c*v?^4_&m2}_ar}(Ht#5-Ga#w>|vIpy61sKiG zZ`>?nx9*S<0Bz|Q&&Ba?cGGjE1UCA#s3EZf@f(-7y4)DL+?<#-=PRR_#=q57kh|B+ zg~e1a`rcv;SHC`v8(=8_DaMq%2KG+ld-Ogu6dweb>T=UfNNe+=y!-Y$9-}&|G9uS^ zzCoUV#5>;&_J#+q{~~tRKYafKk7A$QRTp4)x^26*^2q3i%{ZWx{*t9XNinz=ehndr zy9V89<`18#Es_@VS{!WrEE2$~6AYyL?jIrNoOxD`cPsB>T$<2+FTDH`a=2cTW0Wjg zwoJNpxyb=<0I>@eEik~>yDVI=$auRLP>8s3#?w9b%(EtUlU?|ISHu@S4B2mWIbYH1|M`CxS-P7p|>n&nL!Y5!eO-rt@-}_ePdL!JDFZ#*VGWt|*HQ4%L8C@Tn8g-=d<= z<{d+!lb+W(;g`0VZScOsNZlMb5~G-doftOv(3|E598@q^#P zql(}kSUE6B>Br%U>rr%c#(gQoGc~SMOs;8I!nnA37Xl8dK^|p@6VA;R^X4*d4ww&@ zn21c1m&`jx%D$!oHzoqVnMtZuYy)2?6KH?}PXZu(u5D$*9ue3a$A-m=i+0P`o8XuR zg+)yu?vZC_J@C(fJcy`VQaM~6ZNpf(EFl-Y|2DXoJ~(HG{G9{zw_P zGc{q-aNBK?FJr5|I`;|uGSJ>tiew-sT|E;Q$D*KX-ugpuH{35_sfe+1+sEdNpf8}U3Ehyk7k?|0AQ~(m}a12Ht?kF@H3dkJHFg5VQFYb zuX-rECIF)MUsPH;obG@~vtFQWzF7T_JoUpN!yM|ldI@K(L5qR)8{fH>G%O#N1HcAj zjt66&t=@J>?s$(za+(}Fo+f9VRz}|K(@X|@v`pr2+#@}ko+iUuHIysfoi7QPJ1OYz z>Sf}k$JsTdYoluN<)&To_8*(k57;~au&Z7?PQGs2RPKNq<&2%XrBlty^5WSIrOlER zvI!tBmYWf1pD!*vS85cA&CxD<5rp3Y%$p`O9x!O2-0R9|v(4^8@6hA~=b;PL%nj96T|$IZQD6mn_V@1MbU z-ECs=^xnC>GszV3UB;xrU(Na2fHyLH18*N-x}NVvHE50DYU(QTC1S-ZR;VaVT%*YQ z{l@zd)RJ{1_|x~L8tL3+@7b-p3*Jh~Jg|dg|I|2kHZmhT%p0$#Nte~&RHILiwG0Pd+hHYIL4BD5o zW}5oxb(bw&ijCYpk_1DT8&|&G@BEDCs2IkeJMSl>+ajt^Y7Yc~MWM;{5ukS^>zV||(LixT|Xwr$HMA8)S%zTz|)KSe>4o6z3;oyBGt z{jDR~uCHPvWANa?9%*^jN&BHkj~*sJmp^w@fA`Amd$O`jA)w<*0Tt^;w@JEB26At2 z@TP2A87R=!s8Pd!7v6){)$y)BmHi@!)?9h|U17nSUWcA&Oi`fgg|1Efn>P0P_3QH` z&=N>{YyXl}q|2tZvE9F9{P@hiTw-;8QT&AM%Z(RJyWqXHDe$+~L!Q&AQzsKwc-m>F z<$HYE&lm!4R%_%2-gJ>@*sy_6Wih`+C)!YD$8Z6q{`Cb(U^wf`8OFtd#UFU<+ov}e zKxY9?f{bz7cJ1LVvCp%Ek!47l_iCYv?KZ#*=_0^11G8s3DnU+h;6VD(HInqhd06~Y z@SJA(biX&K)rca*MMHhENE0c3*%IUGWQg*%SKBRfU??UV*siU(?&Nhx6l|b4e+L-5;h1lqkzU0`tzR8 zH_NI6V8&9C02c9?7xTtr%|-@TpEs|oDGe&*ZnP_q`RJE@^4`isM{&_&Z#c$Gl4`I= z2qlyEyVf@#XfMPyp7?I3!_*nz;9H0#E4@IxJakzZXUpQN%kbs z-7yiM&>its0}%gZM#jjHi^|GbV9~UW{Cx9)WO-yRgf5I*rPKP#ToF(T+Ww7etI3&_ z;vHtn^hp@p4L8dVyWkQEWlb`Pb}@_eCS zbFT=Q(4{tF@UpvaDtLQx-gcS2`M5{z(!1ZO@JSFktye>nA5PnieSF7f8=*joRt8JS zHqocxPviCpz+I83y3H~j?tdkt$-YI%C+D0Yjf!DhxN&Ye&)CLf zj>x*G5pfA(b*{hRdb4X?1~JKctp4QsP4ISePbw2Tn+{yV2S6;LP1`mS39zsPu|Cg? zdD=+i+raA@FdV(WKou)m44`AZJdIcsE%a1?&V88?KVcvtNM@6_E&iVEm1K#F-`>ouL7&dj~V!bh)ex3VHO-G{Qvy8|}br{!&4jz&Z01{@;n&klw1m?F79AM5peE6`mz49u< z{(0oi0bE+OZUv*aC{*>f%T$CT{IT+P2XGSv;=H>7O76rx*lt(1X$y7it)`9Hx4bvs z!F0Wx@ZNaD$xk&dt4z7T`vxMMQrLN>?+^AVW!EY5?wugpp4bMUyXv57DD!wSarfMF zkC_L~TIk>FNc)?t(=*RJW7?e?BfN*UG1;gQU@n6d@21b9*JGmZapT5i6T}FF74lvT zy}_HZOxz$Lb4ZXv^-f>O$Mt!+`9sl=4?g(7qkPH!Y1pt~Hq};bf4X)ObSVkpYk}5& zpzCQ%xaHdMb_yn$I$#kL+k;)SjS0SQx#bqqzY1Q}b(1VEQzzs$UUYnN%?vIaM?sO7Uwl5GMZj)k9WYL8 z7nHDIDJgi<<9LrRKc6l&Yt}R`l54NKK{jsOl*1K7Uy`@t#X+}87L&u`>JD-YKJvzZ zwa}U}2Em*Jy2HR20g9H`fXn25gRzmc@l}c3bhjKy;;yU}W8*>xIK4>eVo2Uyd6X1s z@G$Dkoxk8K%mwqgPt>&v`?AZcUMnHYF?1GUSfEfy%Yg=x0)}kN8^4V%BK1l~$gNGv z%DFz+H1GR3u5aAg&i;)8t(O+=G{%008>i(B-W(Sl{0GB*%vCU8s}-XcrU8+@3UAkc z>IQ-C1>7FN9+`u(n70Vtph$7)ST{!QyttCA+<8o%{C>CWOTtZjuvRvfG;M-$TN1Hy zlY7*cc!-y1(c>QdcAL!q8-IblOk)h7!B-B*QbbJls8md*Oz)l3^^k6h2q zo-_k)rO7M+5`L>njxLu|5Fa8;5@e;cKg{N@`<)J-Ero`#O*wi zif32{Q5ei|*FD!h*LbJK6qo+z#7pn%suy)UgJ1De-^_|I|KmyjYgOU27PFml7bS!rV6qA;%BZg;LUEL&XK z&G=E?Y<;$zRTgnxE7r)TTX#y&y4B>NGwaCZ-!GG0X{kow2G*5)(71t|RRSu(Swqd! zPmEt9d&Kuw)}~#UiQ!^AS_gobDvCf{5JnD5J4Tqdt#H``n4eA&`vP;tD~ zxKQ!@3O>M_){*XieQ<1EWgrYv#_h3qvyk?A1qE+vMEB`?t2_W^%`S|kxvBGw<~jK9 zsi((CXY8=E+=QPimXWa)dDsGPS6|iEfH%(nnzd@lXP?h7u8MZyHPt1O?p~8W{lt9D zn8lY~coBOh%%ck7npatDmoNpg_(J z9qEaDxD-Jlu|d4e~*H7~pD5_to;$MWh_U!A6d^^r#(m#NbT-m=G~@ti=xo0(*I zG4um?W9%Kv0lc)^U44xlhV*n!@RnV!+?QKLeis>siKIV#QRC{#Sc)v#-2q_#W;B7p>%13`9whpd}sf>@ZEq0#I z%h8$v;CZ2a6*;>y6$W$WoYM`6_4|{hKiMOsr*IoIAKKqp1Q(o=J|9V!Dkp>u7iiv-&W-e#=&N1k7LSTYf(=IyFz zSQ7wFidBBvv!SQoyr1(pD%%wRaAzVaYKgNrPO^+q#OH9C9CThWq#1xDJ#J)D51{Dp zbK$x<>N_e$CS-GaG;X$TUlM#i4G^iyp@z}Hbte6?S6=`5m|;HEl~PBOd2Rqq!E@92GyG_-d8>uz8={ zdqpGZbar)VGj_i0PDqwwV6gwe@r^5%kWX)DCbxXOLKf}%M|vO^+>mqXNFsz};^0pH z#2@Qr%J$t-95Hj5WLAsB$hgM!qzU4e*w*&mhOWCb#$I{pW#ei{p$irp$T7C;nP;Cd zED8%*uUJv)&lo2>S9jX^UZhbrSEP10p2c>!K_f&&d_Mf%&5Q7>^%5;y$?z^(WJ#EUFPB`)IO^1W@E=S$)KJ z&Hnl;^C;JKxK>uJT4myu`}XfABVg2P$^>Ny*-vzryc{5Hj~iP@`TGHP3^MIPw@or7 zyidhS6~QE4FBbr;ksU3IP!27D=B?uK!I)>fj`k6A_dWdZ!^V|OMNBmA-gZgzUN3A= zK#wMZ6f!+|8OZt_W1c9M@&5_l)K0t)!aUDD`>baS@VSBs-sm>TI9Mu$vyh!+oR>cr zN(!`m4uH>fg5yI0i?$hm2Ex4QbIt;zOqNRlxww^ZIbF;EP1(2GF3p2Ze5MA z2Ma|@=r*|J zbjbOZCP}4lk)r30GoKts{K!hUcQk3@P{{vFC0H*zUu|~p7jRwN4>vtZ%v&Upf;Y!T zr%Ol_To-FdtmA$`Pb(27&7j^m@pq_EB33WVBvN*gxzc6OMg?pGO)uaufJNl# zD=W%{HNXJ6Akqkm;Jf#h?UjiDqM3LWZAb6+G@?DY#xqw{kft?YXuAl7pEF);@T|XK zWoE^TsmmIb71JT~QNMU5(k(!^#cgx~m^ehw1$LQKuE;dZ2z-3Zh zY<1nc<&$-?<`8-*H5mpYMP;%!4=9H(c=j`4N4(vMVhkq+xUs6im}krW+9xC5|HFW{ zbO3WE;Me&h5&C< zZoEi(&sr`&!Id%$z_om_SgBDWPBtYZ%5J#%6^#LKBkM+B4H1*~5eMhZyy;M|jf`DT zE?l&55#;jc3R$hAM~@hHSQ-SB!JanbGh=)$UbIkp_v&pvuY`4+?vCEBP6ThQTeLJ_ zknfeL$e(uhIZl2u+m*gAVy-B(!MR0oi=o4ZnMBzyJolUd9Bm-{@cNrMRpZnhlj@dN zUG9KFU(6fpNMrG9xKpZFh4;Fob?V>mHUnT;2;K;2e7Uz3lzHQQ*j|i{v*pC~{>%l( zoN!#@?in&f2HkassY_nq%`g~fJI3X)t#~b)(I5~W4EHM5neLyA31p00EA$QfhU z8nfCb!I)=yE!&VK3r7id8uh9gs%#wV?hWL;*&_lV%KKA@BsT+D+v|Y`9x&{eFSkju zB>etFxlP^*cA2V^8h2*9Z004V;XNmEW8S!lLcq>-z(XuRVCND1Q_!IsBx*_A`{n3@Gm(#KpQNU7 zTip?Da1X){2$e*GxrnMN#hR~#VGU6Uv@25ujCuN51C9v_gFR^r{H*^ECkFFn=Uv@F z(>BWbLjcfl!E(ehvf``^cyXDpD##B%0b&`!U_4Zn@sV$~$>JRVPfVok1H3u+c9KF* zM66RZ1RQcJ1cWzFUMKJZ^j1j2OjWRPTUP?u#8c~&bo4^`Dm_b<| z{mFV{AP%zvw1Orgy+Djr4}F&VAalQLH`G|<`SK2@O)K%aq zU>e`(SY6ICadO#NOa1#P+}n;-%bJTW=mlfOGy^YP>nKEekAZ#23E26AHIjl(G$|0Q z*=!U#iB16`udf#^_g+@TJf9Idz{g}PdwsGIYZ5b;xea1A>uQ#PDRo#^Xi7{95ED?;wJsF-k*^Q~m@82R$fmNIkIPI+L~ zZ^msh5^j@kT-QX-u2fPkpSTd8p{;m6&P~BUijIna!N}`!BLJQV_$&jtJwIuBhBU;x z!PiS&L6BiOJ@Lnan6O=DH~77=<8s7I5o}TD!T}>lIt?aC!HBNC49X|-Mz({W(WQ?v z`1Vda-!T+Ep+aI9j_D7Wx+waZ(!GO=$ID){=F zIoX&u9$)LUT43bn=F0Ya7?S}5g}dskFTvaWxft0V@CJjcN7uk(-gsSZuzmw@Y|GQ@ z^%OjESl?~AqWW{2Gipu`wG3U@GGi*?tMn_DI8te}7fGS8`pcJ%cudY{)`drj^Ji|k9#f^KqTNdhsj zbA6%Z@dab_mLTPWw_pjG(rMGCnK;Wp?wuESXUZGm%5x9L`eDOu{_M5(yeojQaPljurh?ku{*Zs4$$}fA+4{2Zu+;}5(UbVp+ zL)^wA-YFR1jn_>=j8sp6twU~%6h$h$lXR1@2`f@eF0T;*H^b~np|$=Bo|shIB&p7! zbm)LLZgo4E=o~K1#9$#&cQt_0F>R~LgynnX+pUh+hOKt!DEvG1V65C-msIizAo@?; zC~FS@wV@qp(-=P4Vlo{TteCVvElP%Z<*cLfW|t5Dk|W1)jB&dpsDrC#ECgBrCNHdB zR0cLJC$-%?Z<=o5!=LxcyDJln%VfHnG~M$eaGAzx=r@XdXgUP%@tlC{;{;v@AsDLn zwvTbr48+R=mz2(tW>qlWwK=F0f~>rI*I3J+5v()CpR3fg}AR&bWa;OBKi`aV<_mlC?Y9irBIsgYF$pfgnQMG;d=_-;GP* zy4c-yUCfPnI}O0(TL7k9UA1BG(dZm9_#VV}eQxT~um5c(HyrP46fvz*Q1Hg{C_+NF zNgKRrTiDFsU>L#_0AcaRyz#kSwG=Ux(csUS*o`N6<2{slBk&?Cx&!HTc_vu#%njFHhg>%!JugOI;LV=QWWDBmHPZvgX3m-o|NaM!@oi5o-9@<# zNlNd!bC4(hj}7kZLBeyrUv#+*BF_Tix=SW+pCl1$ddkscxcpEcLs5dcfXU1wrHaB` zvP^#|diI+LWIt|_ME4&sNSFtYd9|2_j=AGHb-F&^lgHm+KUu#&Q7=0_@B1tE%13`d zGypMgu8_Xl8;G=gaCv!)?hA|8yPe<-u7AuAr|yyp-i*7Xxdu}dR5HRVtd%jM@g?KM9$&B@ z00wH2P{U+?yvVd9xvX{xxwcU$IR}7;MW5f9n_+UxCy@3BX3B)SAyr%1W}bC}zk}`S z)i_T2T?mm6oMK!@-J-zjyA6kAddpcFX?tR?H9>jK%*ZWI5KZ3uTWaek`K=~lupb^zEHSB{Y9uBqj- z{|hn(b2}#(^K8ey1nKkGd`At`1#X&*o4_pr5~NYF5s+JtK`3Xq6ayfQ1eCRR<|E6!kM*N~3dc=uF>kf8+dn4{@TNvL+hID`2^Qv3 zV~)B!-6jjbyeW9ISpmKO04N21gPdxdICmlTkIIfL!2)k};Wjz@tJ$VqDbB+9KDHZ; z+^L@-T!G?h^swM&nPI=fjB=<^7NsI1+y;gs+=9u2wJ_m20lG@->*q8!Z@i(Cahv7y z`4TkoH6DX8&+?l0Hd|yLN3co8|9DMM35eV+>br3>IxR<{+cTlmZJK zvm8%MEY0NLfiPf!tYJDv-+JpU83>nFWeRyN>#myy>=WH&@ps)oz?%&MwGMQPB)do^ z+!qM8Lvz$7&5zq8>#g@C%Sd-a?H2`6w5=KEI0}ME`2|tAwTs%ke=UKo*#F zJAC*E+%m5=i;WG;bU<*&?4wB^NFD58YQVW&4|~9IvEI4UN}OmJg>@h4^(;_)!NeDd zgKzp2V^jpP!{l!fJK4WKkuSTy;d*x)fCtqPGnmhXF9UXu`;WgH@ByXCbIM1`n0BP= zK`^NGw5c8@_ZrsCakcYR zl91n^2Xe;!gK#%TH8W@7)&MnOGfeXEIO)_VUTz1dAwc-*uLJVzkJvZ?b7YHt*x;j5 zT&BDa2FnO_Sgyq?5HWJjR#~zOXOMj&gXA7&9!FVa7{`;l*O96voVY<=CJGHQ|) zT&mH!i~)>L_`C)A;rd>P{BRCxHSI{H)BZCcLyl))GIg_dIH@2_F-;AZ(MMX8kt^$! zG;E|-Ui6?&-C)eKWH6b%Mt>&gkb*h6KPgp4&0me!)FS|JOby_41Q|J8NNPgp zWJ;^^JkIB48o6VX%8E~d;UWXW_X6{GQI4N6OI+tC!M*FLC!FNjd{2=TyY2S=CPq!& zCb?Tq<}DgR9C~fO8v+GyeQt)^Bm_@bZXbl0HvlNU$GRd`iY`FBH~)=BezR*kcJwe7 zWEq}^FvASI0}QapMQxJLN>?qqD*9vInBHJ43?yEQyZY}>{LsYFd6$ARZ>&ooZj-zp zg`z8q3~5Lz$L4A*RFlIbXm)Z4Sm=(TKKzg&N#s<-S4^`CtJHeU3BFJk#`xZTG2Y z3Uk*@nlve3z?fg~#x~XGq1qxvL&%QV?3CRv+DAA)^FpyQeDK$xRZr z1>L!Gi**fjoY>FyAjE%oPg`MA!6(;Ej$eu_1*7;HpG<);6e&OGzN`1;y|}jN_~PaY z1#|K$A@F9kjz8wj9;}pEy>Hl1>DaMDt}m1yz$QKM)EMMZ+2(O)^xnnc?JGcGYc?eJ z?Eq+kw+$OMW>dq_s+!$3{L%KRE9Kc|#&{UOr=i&O;lz(T_tN8afK;hc*|;R?;%>jF z_1HCQ)*@!%KG}rbIB(iMYmr0_ufcc9t$l8ROGPSnk)!44PuEDy(OGiD+g;MN*15wU z8<{4NO%N+kt`%;V34>#&+|a3uB)~;L!K1bhnav5~#|3S148}w}QP()pFT3}a?vW`Q zU^OQka8w^n>t`s8AkZjj$*%fm>NP4!u_f z$pQXMw*o9jLF{Bw*E&#&bc}y3&o0;@pRI?#rsm{hySdqGU^JMvUrnxgn!*z)RwExt|~p> zT!gd&h`|JdmVyST1O{$GuVylS`DU59W+&2G)RjwWmXmgq7aNz$a4>a@g)=D}5TkQN z`FMHa{07+ox`H<*Se^<22h$b!NAS0XSuvfu_xx<#74r zuD`FCH@%NuSM$8FE_pF;w?i<6pn-icaL^rc&(I;}Qu$%t^q%_DcAMnpD;Y@7SR8V% ze2$z{G=90e&ptc@0H7%HNbN#Q(pnh5Hao()s0tb{O6UiAzk+}_-cPU3jTgG`ag)V65w%Sa@SOZI zZ#uT=>Pm%0Z(CEJdGzSf!heCJM#yX2=>4e5dluq$RgqW86a|k=p3Qi*U@@w8ootT- zu8C}0t`)Z0rUF;ikuj+L<{4A3kcnme?YZf_$rT2%jZ8Ko=|(<0ceegZB`@&C=hOja z|7-~QmF!pJ(;G?YGNp|Q(wa5vWC31|%a$>imadi#?d@xTas$-^gj~OU-it9{Yu7sY zUu?f)yS>S#QSGr@IY;t!FMQV9U(*pgWi%?XyN5Y-5ajkKWcRAW}-kAD5(ZcS_MS zUp9s$&%3qnZSoV^$QW{_eH_vw%hmH%xO(1E=!tu>kI@tPveWpZQFFG*FaJbJ1`>6% zUkxDB26r1sbA}a#ct-n%rKEM8;!+dplYx}K_9V%4$l8AgQIJgL^TIfAxEAL=CJDC7 z(uRYnsb3;f9=oy%5)(VgoPF)6X9=Hdmqidh(U>c5;Avd-0GvuAzuJQ>D#~!vX>HU05y7#9Wy?cwP)34NAvQ2OSc_G%oNcM*-IDqx**up5A&xCfKTP( z(WWo$DjQ=M9~{9$Z$5|wvzEw6j+@~-CP$Bi;l`Lm!zh}`@6>S{nA^jQTf}kR>w$AE z$o#{-uU`4MY|Id=x(;F`rrqg>>lSwiIO_|5Uwm|!)IqFY7PF|0nbpgz2gK>6LxW;+ z{TaCfY=7glu=6Yp&q3GjNpm*H#Ba7EUeQUL!1^6PPPc2C*OGo68p@R;W*c|OM2xrZ z9&am8f4N%bto&O_m57zSiOJIEvbu8pxz(lBYxCenj{d^{UUYkF8G>AJKmNTRDFyHu z-HPd!2@o?1iN4#{$eHZDRajk3(>92^yC%51y9Srw?h@QRxVw9R5D4zB8wkPO-Q6L$ zlUY2^`yR|cGygT$oX&T;cdymmRo&IK?y9c)uj0WNVS~&^BKODRA8V*K}@}_BxHbS5X2MX!RWPQ~0w-Xf0 zXtX4}?`_0Z*m!g`h(S}F^fR)mFH8{k{@~7r{IsCMn}urb`;7)# zbGbKi@fp+MjCG7A7_qJ2VtKHb$d0W&9Ia<)wvcK}@|9F-Myd^E=(+evDi1aGn*-|S zFTV^0xVjxZ*wy^*`n?}KV1mNoHn5iL>r2Vx%vM+Y;AA8ie zz$YU#T82czrzNa69aPex(ZlFT+CXL}MV({rVNczASpu(gc`# zAM-5&HOn&NIfn>IaOcfco=3j~xDe*HabaY$I{d7Q;%nv^Z=t#fadtGI>;-98?x2Fa zxC_pyRL&w1bGbUNo7?eB9G}>}x=w9Y9mE`|CW5f~<4;ybPf+^fmRG$tyaQtIIbBum z57!kWSGh+)4G(NiAZG={}oN*d_Lc!kK*j%)z#)t(`l1vwH>UHiRS-UyA59WcWv>!Rh`94BjZ$>kfKA zJ*K~NPVq$_7n07}z^iW$`g@C4+*|VQ2XPqN0IL1%k^h{23u>#P_(55TTJz54Tm(nTFjn3JU z)`(O!ps~o=@VWUxc59yavlRl1IQo?$2ft(zhtFb-B99dH`^ohYm7LS5$ zUz{8$klX}9JI$vy5i48!OPTqk{GKyU=0Da3uld1PpLNpQnCs$vI!{9nFjxrTsx5yD ze)0|iVxSzk@Qc|!T0b@LD{=&`vB%AwdB+Xenc@(V!m9p}P?7zX zZBQKpVm)O$x*mnlZga2Ja|XC>4yxq#_7`x5mUF^M?$9X1yMO5~*W?Rq6h^udv@2kN{u!fv@CkkJWsdr)cwiA93p+h@ht!Q5`Y7(#vx4F*GF z5v~C@p7CNt?Q5Jd1u=(xSR|;H%O}9aT=%twBwsmdGkwLckpCKVKQNIq9F$2dnZTRGB_;4 zB(*G?vA46{QUZ5Vmzv2mcD4W!y~KY|$z({G`X&x9)zzcw1$=w=Mf#X3sH6(nO(_B+NIa|L{1Zlio5Fg{($>y*gpU;2?yWUWm3kTVD z+vhWpkaxxC{#Ogt5R7RBVoPWKE@-!WiO1>@<6;GOs5|dRY8+1*9A3PXUyGgzMrZO* znICovP!SehY)tA{g%NR|*ya)U<Y5l_O0MaLM5 z^MM~yHXbXVbrNp$GtX0^@29OyqzG%4@yM#UqKnl;qYQX3^0c->vVw(H^UZwC(U%-t zN7eKphxrd2m)5SDuG@2~=#r8IU6NuM1wa)e{ACcvDl;p@nwB8;p9kk#a4##~pz&nm z6dL_~F0{WKRuFJSN*;2ky;7Kb1DU3cl@H0+OMQfQaw|@dZYq)dNG9{Lo5&A7x47Um z+|zS8qxSTu5oF*8w@HrQXl81>U3Vz7kfgfyprY*=N9(f|q`3CSDs*@#h>vTaJ|7K5 zf;o+Um0X{)^uE%w6E2^)^%A%Y9*7s!w55nO*Ctj`U2@MjPr@9I^S>d1CpB!DMg{~A zYJ|IP92YpTi*ym{moeaVb_Hn4ExgjTU~NdLFeS4)p66k=grls=($OMYNSzZy=Bf$bx`(b%pgIp6yjOg?6O~0ZLw_%k& zzpJqdN&KuxBJG#?#gH!2e<$mA85Qo-L)qeStx zs*QcFuj-oob)NFXj_Y2EaXRKccHCSY-^bQ3wTc=K*V>z%l$)R#JVEoRAMD@DwO@0f z8ClW=s%ZTwIvYLM#48w~+G#x1VkN}7UV@1An&Gp~Rc;9>t9SRwu}eMH_-L`iP1&2Cqbv{^Dj!Ly)yzZ0dvg%GXuegpsOZQuXM8d1`txe+I zgTlhc%8~M`f9iT|JETx3Th5cd!;#!0IHog5cnl%j%lhnC2%aZBO?#nN3GJ+VZ%{vf zLG|1x&-}Bl)pE2yYKm#uo;x}KePVuafj@`LicRwJj4Wj~-bQ{kP-Th%GX?j<8gg9v z5>IsmnqyxRmu0Gg%XBKh88(Kdyr?D0F%*Xm*}m;AG?10ITvoZYO%%TMv|;;0O-dGz z%zXZMwD?z_VVI?F8YKT(T z40?(Ewb2*}<@Q#`9h7T|Mzgl6flH<1xJ3%`X5V*&2;e+j+DXcNmnz=qEPoYbC4v9g z&Og2kBi8o_sS904&46BySZ2EvX<8`&cfi9;jWQeLJ?y&pk&zjc?L~xp!zTS%Y!o*z zeZ_?C^fw>DB?08=D8qOgx3EX5B(ft_$pPNb{IO!k=+_X1Kx;~}y}RoXg3Y2|Lk`q? z<j;wYc&#Zw#|v?7}3lAGz3pib(QcMT64mh=l3sVPN|Hwt_Tr3un-O0-l)K1Cqn? zDe+indA6p*unP?Owxu_Lf(55MG)}dSD}7E|JXGX-DzclPp@ke~%0&M0hJxHRy$`0< zN6Scor!cq;3Xr@LU+_Wk!sxY>fi>)bv(IWgVL#4Ucgsx#422i4RZc4J5b%kqM5#T$ z;6JbPSb?*m*m?8g3~2r>NhGrLhBE!W#D#txKQ&2-XCaT_U)=V^V9%`ugZ*cbjmL#W-7UrgC102to^quXq}s{P&Z~rC0IkqNdBP+xn?)(KGzB^CESiX0{a#X zdl~ZqN61(Fn!t@tE$$9xQQveXeR&8Qdz6ys;<$JKX6WGBIXM(6eJ!*}L*87Rw>90* zC#Sc?ZFJ~Zke0pK@c;YTFQL(069@gKMpf&5fBxGKr6KmrNjy|_J{qjcVWX2O&>LX! zxF&+Q==$5~q`&`~5~-@2wQvqus7I%|ul{}ml-PeWHe4k?m(`zvZ}LS!Z7s(!h-C{Q zv?UUJP}BuBrPYV=17CoF@0pv|bBht*{Zed^nmXpTSr^!sqRvbf@RZ~pHOxRB)2rEl!UFIveYz>Th;%R0mc z1zQQx@AdHZ@p5^4Z!1-wl9e7E*+gKLxJ*TQru&84UZ);B-oo;t{5XyyR+SGfs1$?7 z-HeJh=`^vXuWyI@NQ>1$9F+zPx~tjuuh(TBYd3OOxMY#K zkNGSyuV7h~V2;A}aw)F^*=9XlmZ}B3*S{i zt(P4=S9@%%3MgBtktWd&r;sM)j=Bs9gvu8p!Cw{|<{e56W`rcMvg3i}GF62cP(xwy0Z%!0BLBoxbiYs|nf20`5$@ zziq*-u53xdNao!u!SZNbU4N%#2I75uXfzv}OH55nec&CYA|ZtZoPt41XScR2J~8L$N0S1%VxG~* zJN3rPrVB*Lr-0&g@6+HeM?zoP317pKub^4OxfX9yXM>YJ&W1D$VzH6wY->9K{-&TG z6nx*Hr~Xt8d?9#zCgCwLBoT-p4B=yKn8asPM1n1Yok4rsmGu0VAw3P)fOno2z(P1kp!sU z6oHFWT^(Vh=|Cs(L zACYIPY(t{&JJM%?M=ZgcKc99Gz^`C1sH3M|^Anl5Us@YGZnc#5todFWRA1-$2j;9^ zD}rgN{~#{6K=0XMOF1=#CcNx3lL)blA6?R#b1!Y!zHFIT_#fqE3-ipMpSm!!oD<|X z?3|N3iu^hU(w*chU+rns$B!wVS#+4&rP~a&C(&XrsGequwgJ+B}H!7&}vkBhsCvzK~zF=_* zFU0%JeojA)ofw1V*g~X47GM0Muu-ZK2ayj7&^Eod_LQoHs%i62zPkVD;pdFcXz}V3 zyO1-0+7H_&enWvSK;rZ(LOMTD9LnBqPx_y3 zC!T2)Ojql+aS=&s#cXMEsW^_R`fllu5I@Ot4O#QyL>-YrU*e5T#mq#Bq;%|s;8$w2&L zaLxA75f!v0Vqgp|DI}6YInS;4PFy0hB)eYW z5Lh;tg)})Ob)V<0_5H74ljR!sn?DtDKsFnHd&+P+8V}NT?Z92398+h)at(9q0`uox zJ3_x}MvmmJk~L~mI69%72GvC}E}yI`TlTBz!mfIqSu@4+M|TmjIei=*JDj3O3FCAQ z1EhhE9t^p4*>-TrR@Ilu*HinU+1!>(V?+lqd4a=T%|uQ?@@46ovXqtfKib;Y&>hoV zP08_I`f1V8ikm}LjzA$z+i>(72Hxl0OfWx7p>wUhw2e5uE_-*DzIC2jPk;F-Ib^Dl z`b2uSA>A1(Y^*%E%WL8cS0DOI5!0v{@?JJXjR$wAY}p}6EZh;dM_EKmQ|zy8-F5pM zRJEBGPKc~yH9}9>Q}7;>kM1l#wcjPKU`K$yEq4tK3~DTIoH6OQxY&Bjm&>yf_-JRM z>&5sRX79}}cphTWpv%e({$BewAbmzE7}c)t8oprN=h=28g$DD@vYMULkzT{0=wMx6 zzP?j`=rs@y6=nUw8=d#bs0dmVGBWt2fYj*}$0~wIcX9FeAU}2|Y^mDtVFVN7(W$Sl zYBWrD@;vp(%Zz(J4!9R;=gqcUcHQ7mIqs;55tjSb`Y3Eihc*`E(6p0bORTYij^T_O z3?e}U?Cjm9qzMLC#J-Y(2>37Ncj85+^EjJJBGr@F2>W>&oSg zl#$qmS+sttLu?-G@Uw^O*ju(};q$``BhAF4?(*vM>L)C{T4w5C?t~j`drqHW%D%gq zQ0w&z*z+rtfLj1g?}~xXYf}`&{;$QHt_K!23ii!*P;#TIB1m>Pf-}`Wokoxjt_a4y z518aEX>@nJt;xIWSLZw6Ruxgwb4_+Zhn_z55~A+znN8!6ILfaMvk%j9Pe_ds9H@q; zA(!fgQ#JSx6wQJHlUoFJ=KC$Ti_Ixwlq53Zqq9N~$vIH64qcneTp%F|s;X9L+f^4{d@N9D$6 zq5U5Y;Z%^ebU3*K*vH+o@9k2s1&@1956e~B^=Vx8+ai9Q)hM|FB%(BLDRPQvzwN?; z8SNi)SUavG#o197Z`xYS#oD`C7A|El6iFP-ui@h}JPMf@lyq5#M$*mQ&Yk>4Tc2s7 znK4$&X5u^rF=Ny9?HW6%n3I>Sf6im-L-pD@FME_9zO^!_>Ksy>a@p6|O0*lrx49a} z;}lKX80yNqylv0;#X-7Azs@7xYt|gfOc48U?6;sao@3yZyY1uQHtXGbXc&2EJG^XnidoE&aMH?QVAhz!$fDN~LC7jNm z20eb%hB**H-ekj%N=7gRQCxH)zs?$gE?$AJ1~&Gg&mq#uA;^xIyd8CWn8urE2~Ucw z*&*Gc>$yLc&>~5yhh6pyzS%84K^A?AOhVo(-|t~Tb^T@V8v!k z?=dEoJPF#^@hEgAY_zua-Dy3q0z!GqcHJ6v@K~VH4s)8us&Mv9)T+|}@pr(uXkLBE zBZ>lT|Y~@7V zz1uq`xnXRrO8OaZDE)K(s_sm>e*TU4O5!&->C@H8h4jf&?Cj*k>%W<5F!JoqOKTUc z_3lM!w=VKxSAG+3ViF9!#=5)gT5E+tYp`4W@S@3))XjxJ`rFow1a2+4lubQ6m}|C`+*?%CGbsY zJi)v5_TZq?H?qjkLYi4xRX3c7XDD) zbk0W-F6W;xOe-%uV+RhRN6hopM?zOPCMZf(F(ptj<8qxq!XW*iuWI=X7yhY^6-BdC zAJxLuHI?UL24auOK(4uRpYTNA>TH1`Ag?W zYpQ7t5)GE=$I{&~G2RLz_zN*b;ajXW^WHxtOl-`MmG=cq5K}-ngh_0<-jf`x`x`sY zx?LYtQ!_p(N(;H!42m1wVc&Hd9-I5POV?yRM9B)4eXPe$7$LXFot_BBt;MrjJ~07_ zU*p{lnOjbuH;&coQGiIpLS{!~_&6BW$3z6M*6trA@ToGk1)_LH=HeB&SNTBIYx;y| zu5Sg>{gcFT5V*1tc_NMwGL6Q!VsOAB5g{`&KAOyfJZoGqa2+HKM_VNb6`9kBkgeFy z-9$7&Cc{9!3TtX(4ZFp*0{b$ajgRMV#1MjX{>?G`lZhL}TiEIwz}!iE=+3Yos86+g z_4a&`^|6Je2Z5HZ;kYsF~Ou?6Wo zV>P&by@Md1Z}r)>i5uj8+jx@GzExH-GPu6Q5SPjFy4fd@p)A-dD!;NnJHFSqnxztm z)I5gF5+nq0ha*e`G}idFVj?N14xTQwzQe2z*r5!PWWxm7Ub9s~PWD{_PdA?xdB=9g z6i0s1J*p`eqZ2Rj1>0ZDFjdh-wT+u_iZ|#<7|}ztjk|KuK-5YBeMN(a{eKTNgk5t> zPnK6sMwl()>I~JG3Jd6n^O5{uZg<7o+dcc4(5xN3FcZtJsR~0cBW8zDl_X6mO7z?x zh^^Lro9`wWsG8d?4<4Cv43F3@u#5(ztpri z2wC$jKKYoC0kiKV!nIR3hZ5V3O*rNH5$3_fO{^XqXfDk6uf)aCkX!{?K)t|Gc`_3HXFzC-Bg za9W3aav{o-Tm^i9B2|1t8vP>!0+O0P;X&Jp>yQJZZusDL=EF$(0la%Pr| zu5^WS06uw){XQomrT^`m8bR+ZwqpY9>2?IbbbNJ4ew;8chH3NTTf7U$3#{R*govU0 z`@|Q?fxqtXY>=Y@s@QWNqeM>MJMxZcgpm+o{2$T$|3EY(M?+Z?2Z&KB9baz`e?g)a z@SnE?->m>;Z713^PA}DYAZXyunYUR*zSEqEfga7+L&!Lt^W@p;3NYdmA;z;t=}| zosU!i17G5I?{i+hofvj&4=!peyQjMHoWN+aaXPQ^9M;k?ooTO}z?MdbjddUHolWNn z!F!I$i44|Y90n~T=~aMuTP>>xy^cw{cyDiS|3gJzCc6d9dAdjYb4S-s9RD=H2A&G+ zMywgHoH?AEM9_$Jxq{B;GqMDXUAx1{Jkif6Qn3UQ?`$$!9r+!kh}>!_9}QwWMSqJ? zYKVB@FwcD%`-U!!rYLSIvHrV}Gijo5%z4^Mm_926s5=)Pd?p9j?YpkS^N zJ4dX4D(Fa^kta;=B0K)BRFRgfLB_{{(3AY`wU00U-Kg|rqf2c`irD+2gWK(BnzFz<>^13~<66GeenIAiu!_O4+_g)uqXGF0(*v)TBmXslP?Y}&uUWF>7Ge6LfkxxAXjZI1K^x+!H#|u4K zDmAN?9sn2EIMe2i=h3Y(m2^jx4c%*{_@;U3?@h&@hv}=K7TVY4 zzPYsVlM1eW_SF~E#=mVxMOlYS7P*|ybeYjw#dY?DdLWz4CWaIKS$<|cuU`-_DMQKB zggc=NujY0}83=AbIp3c214aHbtVJwna+ZhR--Z2!);xD&XDfBNbwHl1Kni?}PP09+ zBYC3pDQ=_yHB;u6G>V=en$4zrPRIy_LBiQ`BfH;g)83feN{y4i!)EXiz&%$0d`&6= zY~rJc?ypa`VYaef=bea;pM)JcZZ!aAFIHcG-2O}L799-@9B-H1$ugkQBU-;nIm3e3 z2NUqpPg#>6UtYsM`Ypn=1NBMpJrtboOJ#`dqb=>2?0uK|KAcG(R1jB3jU7Sb>Y+W@ zJL;yAXL{t#lXt?_47pVEzxXZHnZxf)<2)xu=_4P3#(Teoii4W{ zYd>qsqLo{(4pLXYyr|6rh>M*0s_U0Uc&9xwJET&4nfA$$5blH)*8Oi!m)LjtCv3d( z_l*2e2R)?KpZAx4v0xudYhS^XPXml%HhBIe$jLtN?fTefsvb_{wfb^@*n;?rUda%o z*6)@qK9b9BH%V^wVqO|=06|a6tNlj1ZGz+TV9>AMw`;!n%&$lXowR4-U6)C{Cc>vR zJ)@-z6-IY^jm$yq&wEr8D~rZHm-#8_P6{Ux2PX6B`e#2r4rQmd9yW!wF6b6qGtGWY z!ObD2r#ole>a?(i?bxlR%vs2^ZP|s=+wK#o3X1d?dhYQP9@O)whEKpE%8%@W54ZOG z*it#zgd3BS7q(dXb-mMkZ_ zHJ7eR5J(+5PyuRno*EqiT@$^X$mdIY;wC$&!0{@goX$grcHlLsJH7LaFS0KflRvwu z9XDd3X~-&XCI4Vu?Q87T<=2l-a3{nDb&j}+3gCY6xQh>$JCM{x2G3+}?R|=7m^Sf+ z+2_O2T}5uQw)k_WR=!JfNL|w3@Axdl$NG-_!dr3?o5ugVI*zgqKyh_kC8?zkNlWtF zbhM2Rww(1KFS@5nmHaYK*KdlIBbN?iG{LY|@Qh_eluTQ7GrGw~zs9LpHM;#Nv|`7$ z>e7wkZr;B{p|P$V;n4-Ke#YF#pJ@=};OAr%9`cpvI9)z5AvQ{VqCX3UtG*DZ-W&RW z{M>}d400qy{Cf5yrLD05$_ufG3x1Eb8GEq9IBBL&spsUbIems>^+JddNh2%^l;gFZ znW{(KXO2Og0@(CMJDxq;m8LT;YA*w>XiN6<;PzdhEIbl?Fe2@$@Nez))z`j}W1zCm z&vZjSFT&K%)xM=sJZ+zyCNHY=+cl!55>lYM`}50M20WP^fW2w~RrakuZ{NhNOB_@o zw!UwY(>~Mb!eE9Sw6y&I~Xi$=wE$lpW58TPWv<{)$`g%?2UW0 zi<1pS?j!1XB&WVR){k;lt53Dpr#-_*c4pCVzL2ipkWk^rd(L}uE1|L0N?hTy;)X#+ z6|taz1^w%oJd5Y^F(0B3jwkLWiEU39a$s=qw*>B$Lc|d0@aXUlUrW%LYS!q*wNEiJ zLF(8VXcfZ0a4x++`ras$#43^%nn&~<&${6ts&%9v`K5bN>Kbf#8H^<3>?BwMW%pB| zVmMM$?k|ztES8g-jfC#If7Q)VUh*=iZ3Qs}Kki4L>;fE~*p8W>yz&?vwitZo+_DX;DJvg~L>r9w3Q_~xo_vCXQIsub43Ba^M|Hk36R>Vc4QW~)hSRya0)SLzKveTvM4si^k z%wvE#KL}FzqZD}JTn+lM>)K~y_&br?2=<;(&-=7isxaWeat=Knx{|ql@#lx5?As@@ z@^cWPy3jldSBKJO4>rRG?mNrEFYl*_BMsOHf!hA+act97P33B|wh^L@r&AUZE_d>& zeey4gMHuxvq$Y2n>9v+ne6fydRjaD41bRt44hKxRQ#3h^gXYu>DG*-UXzL;XCtdoZg-GS6qlm1vZ>M{cD6^;`|S zM3>9|9HjEV9f7vWYm2`CxVaN9qgevXvdPkJ0dmgIP^uJ&;VwK^RJrZ7$5dFY%@45 z^;Wf|am$oLE{N!oef*BmVw+d3WS`u$vXChCLAw$d%t+-SUTg+;$_b@Vve7{hLmJoFp(U#^tb5zJ|5Rq#1fdN&&;ke|nE~)~` zcS#~lQ{f!2)#RiObD19MV{W5P6cQTgL*HU944K8EpJN z)C|^|I_nX_kl;bc0!&;q5CDK+=lO?zVnek1*{TlwYTXeLS|7C=Ju zV@c(oAU+j<*A`cUu73yF`ajLPVa0P-Mz5b_S#xaRx|*eLxM{s2tT{|bLhy8Q!x%s>EO zi52VU^3bPuxkL)^EjOu&iY#cC#T49;C!zyD-Qdrkc5>K2mxaU9*1}0RK#!DXlU6;+ zZ~%@Z!=bf2TOa{EFpZMKB@p>8txv&)3JMDE()zYj)*;IPjE8dG)6zx-6L_GMw`#e( z_Z@gLi4AX}LVgzk0ZN;D>pakx#z}%rs8s|oc@84iGv`WjKveUj3kcYO!vH9zXz_oV z-K5(;;Eo`m*MH$3wa9?ZQ`ew>;HU-df8i*7i~pc9yNv&$GD+O;s7yBcq=NZ7iu4%~ z7+ql>iw*To41k%vLsr%AUHyxE{a29xm##|o6y94{K)I^ozoSTi7y(S}U5o(owafSp zSn-G>R38>m5dihskm$r10_a zi_EWzIoM5sk3{Uk!a_AQwNF`GcGC*x5J*3`NFUJ?Br>wHbR$8=IY*>2SMk+%B&_eM z$J_Z)DQ)+@ufZVV9xJ3&h0J|(QLua9kH zEZ9Td9a~pxvRj$YIKEMV!U{YlRWdM81mGa{q6?!m?iR&kzVZ)EPbp+1?*MJtI2*$+5RDKHx=;QP5h3i&E6IT@1&se8MZ z4-y#MKCxi?a^t69JwB)p5Alr2dD*)7)OL7@JAg1HL&Xe*>yZD41zNk>PARCiVU zy8?~|LKnS8Vfl}`{d`3x?+aQj#slL@5T%v{nNI$RyhRe(=6eig-Eq1dFxdd!TS0Yr zR38<<4OOONvB*E%IfX;JBv}Ksi;M$a+bMHO$N;-&N#}LVh4)qSE140q7z2;iY_ElO zh5dF|j2qQFx4y*@uVKT&oH($z$5RpG$v0C7YmL8#xA>_iWmg&dJO>_EwG z%fT!(o{02KSTmGf1;4U|PrkC~(!Nl=4Y9(thiH~jA6dOuc9BouZR8fTk0%h%j9t-7 z#`$`{3pywA`B8LqtjW=AbC%Kqj4s##iz`Uo;4vljqs%Rg^NUSp^ag@s8?rYM<3U%5v;hGxNev5TE2IBie3hSpD zC(E)Gth4hJxDe-KKNZ%2CA^?zZG(v$m_Wf8F*laL-3wZ(wYwXN?7YG7_|a${86IGF z?EZI62oBy&C(~1K8iy7lGF+s_?aN=36Z+X`mdmd{8C9T?5W$;Q%$yqybj#r=U?H~F zjlr*|s**6Me^ez8vHn^fejwj;->dV@Sj&il|L`*NRar!88MP- zk|jCG`EfTJMOM{6#KzBO6Tr1BWJW+OK{0cFE$dK%!UC5NL05vJBR2w6o9u}%rh+dM zMsDnnPy*~DHAyg=BOo!GGZII1^^{`m0jX{nh}+E5KyZqS%;Qfx5AxM-=s$fZ$M1Y< z0NT?)M2VP?B`7N?88JAJ*w^4R*eXuakB7h47z-5@LjwL3&qz%AF!ay$K;xtD8 z#WylE#A>6UM?LqHR?}06torYeA|N2bK;2g`+b|y0fQgXI(_$1umv(?kW7Pe!8ed!6 zutrroj(~^=EV}!f$#p4sz*GT-vwqxY3xx^`tFWvA@1KbQ^CKZeW7$=xuqSaiTWS6r z5oSe>6=)`DbTnNEWPRSt=sBiiT@{@$WJ+nSe5Zjk%7OO^zEv2~&5wy`5(h-{{ss*= zvkJqmPKd1G{Cu)ykQcifU;#DUtXiW)mPu+nFHP!8L%SrFF>#$ zP@ymjw3YB_L3{W!_?Z#o`_a&BuTSaF7|^JZ_x@}ESppv5Bgn-#O==Xp`?e~!B8~j; zk$@8uLWEha;!-xz7p6SJdwUdYf zTvNtA7`67~g!fku8c#{mzV`%}Y&5`4WQEd_M_#nV`Wu?xESWrwhk zi(sSG`k+8oV&YaUR|n$~pw6^Y@{+U9Fn}H?duXf(vH#ur`R|%%u?%W>^wE{UjaZV} zc$0MSuTA*!w4ksAJgm0D)Q`CeoBX_?1mwww4`P; zJW4y$x_1Y5NlIy@`d_^QNiMSMVkG-icDbXv@}E0d)7LWub2YHBc+Dz7DMLE=<~qA5 z^PIx5?7L^-Lp>$8cd=x|xE;bn=p~$2{W$tAS_zp)B>n!(fA=-%avm8|#~w3f{q+_1 z=>Ha8xe?L^UsP^B9@%K6v*~c;RVY@{)O%YD^O-nYxvgZf)zxRS4G%|rX7G%nt}blJ zcKVZtI*)zoAtN+4-T5fHKJAA<1NP4fEg%MlsV9zdFL@f?x6HLupL_U*E0l-Y`IPEE zM7UdrL1nYJ@I}ozwAaPlw(uvCEuPm*bgZctKBMpD7wZ*Jt?MOz%YSnL+7ybGrpvIe zV3PKz?e^sU=1-k$r&Y&Qed6&P&fA+BkXuQKQD%98t_&ZURj-jIe@ zYt1*8%xZtbJ9$4H6Y_%ITW0A-mvyPQWtV~VoWjZa>2bHE-)jJTmF;q5uKkIOzQd*$KI=|$jM@7T4L5t zi{x(z+p0>954sE%<+lUQ(Y=Ps_D=Ae1-5jydf{wR*%GY_+1psq3-alNCNV$C-D|gT z;B9m#z6&bwE-j7gFff`wlbf;||Ar#qaU}7u&}|;J)+E{&B~AG(c#Acb%{$Woe`_i~ z{P0`=5W;YL8O0kO8;e!PRB6(qbfD#LSZ4jtgwX)A*@LrC4fVL5j(8MRTRrJOKc7?$ ziWvG9uTM&>qB*=9$L2yipsDD4cOhSVzZ@NR*o2wf_#~Rww~tgYfx|zgogoC*8v_cI zvRf9T#Et)XdSEg2h3+RtQ^Q`(97DAKCdLuZIbUx@SqY~S|3SFEpQvL?HpUBmdgTzM z`D}?ma#Y+}eBH;7VwntW$%jyiaN1pdua?UVR$&y_xVVHwb#-;4d>a6~Zn+Nf0u{Zo z1~sx!icTtRcpcRP8KqLZSh?y-D8`8CoExwcd&!Hy(VWqHc`}n5( zd7>ceqi+_s=U&QKyI}$+8gnvP0HYo9Ro&r>srRJBfO|0$u?|#%1b)kaZAz@fC$i{l z-4sx0Zb+%8)a+2!#%*m%s$%qKVi~vesyxn2%1^vYyX9F@_-Le7)4v|;4TQ`$Js2`n zyETl+KPB}ge}j)fCpR{c%-+v8kwaIvr7J*a;?k_3{c+Vbuhb`eQUVkx7JuS0Q&$R} zMxt9P#5V78CNw{r=GuZM+Mfm@)}IZ1Z&S77$4Q6h@#fB&H@^El?%|dh3P0q@G3@6j z3ze5q{t~6pr0cQYt@&^4sE4V$?$!zy)eUCYAsbl&;g@gFQ zx(i%F&Cw_nM%usi&w~q~NcJU6hSY$BW}tF;5(>{Im`e%7bQmJ5L(2#6Q`WZ4AVh1< zeZUccP|L4}h2bt#Y8(G}` znPCl)>SGGa`>$<5$?nYg6aIR#*Xb1n*yZ!rAKDUz>CoKN6y*$v@=76+{Bu672A3pq z3(L^u+!Ib+{8d`I@K^1-nyX+N63!;+d~tAh+wHyVb*b`kAvMQJzY+~ehWTaT=O_aO z@R~%r(CR# z-P<%f2xIbz>ckt!A8`e82d1v}Mif*DIV?qhV7}mcy!!hbRZauxc)aM8)&l)O+%+3( znfQ9poSn0@WFkwnT^XjG&_>OcEE6-}D|DhwHs!FB+}chtd& zp7qi$tfe-~%(O{Tfb#{3?|hT#OEx8EK#B6U2fYUFWQs>;k=4i1pAPq>=XM(z5kA zo41C&TD&uX!+`5ilTcc6CSGK!WU2=MuW$98C|EZ@Y!d+i;qauoi>oRbNW0-btLZ^84xflL4wY866va$nP2j2iG>qsa zPV~WK{dX#3NI4(H^rMiA^H?aRE&M{9qsZ58baUxbL$~LjyNs*ShX?=hmP^A#bP~Bq zjSgNnT!?n=1a((sn{-xrCjJlh-ukPpZVMZ2aZ2&x8r&%wDDDo07Ax-VR-iy}D8;R~ zJH;V5MT)y?fB?mz1ov;h=e%c(JMR4x?)PJ0Kz8JI6Zvy*Z@mT`HX z>-i3+KB{E28Y+_t6Mr5>y3VF;{s zj1bA~-P_f9D3jE&p~f<1Nu014p`27aP9-48(%Y@wo%q&V{zE+S`km%Oc#R-i(9YK1h`0wAL z75`S*S7?Qp-uM{HKddaLF=IO?2)_^aIqc4l?Pa+TeP|<8LFgN6YKqQy7bUBbtx$on zc=29KdP(_+E{l+jgem9Pp}nL(Qsf!^y1WkW6Za#-hhi0k4<;~cs&d)f{_Qa*j}`YP z>*1UW(9a1=yx&zFEBbmLT0$M;=z~6W6G9m1Ng9fb)P{Zk6#pzkl5E*ZYY$6n*zCjo zxZB8CMm-^i`AQJ?W(f6t$38?;sa=}?8c?_uGJhyWLK3FQ`T+9@hi89*E4=&r3sD8v z?ayeQ+qfWOoE&Bse(1o8V_Wpy^=X&NXhwt0TB}EKC6VFhK>vp`IVpi{Nk50fd*ZjmY1xx=t5sX*+KJWB_x#^FL#l-4fXlBBL);eLH#B)=_?q z)>A<-!f^SZzv!lS&fd3%wL`POuO$*0)rtWc*}`n()iTkl66jzW;$-v=fP>2n+C1mV zQB(R95*enxlkn^T<$^v==Dz$xNlbU0T8^lt@YSlD+PO>N?KQ9kw%T6!|LdNbi`@I{ReA(r>;kS3XfU!VkWwtleIRlNmD?D?l0ADAC-U; zwm(xcQ)ANmx-5RgGxiMznW)`NF{TK>*>OLr9hkOJN!u~YkpY^ zB7c)H0z-jm4HDKr5IHU_AHMDTx!vnjc5$Ou_Fa=T%I}MH;9VLL|90Is6iSs_{HjQS zE0w`%O>F}iK;ZJWPkfe|mIMnI=cNR2uJn2x*>gE1M@51`fEL`G)WZ_??&-SgnFL1b z5GN)<9)?Zjm>rG*^5;CWm}aQG$YX0ZhOGnahH{YfO}u{uG8SPaaN|LhJD7yRaVf|c zgh;boYo;{T!52h+k~!G#_NaY!lC?+cyV;zlApq@SNe5LMh;4dXYakN5$$l-L9@>i~ zG)gDpG+(Kkd^rlx;>Ja;^=Gj46S6@Y^M~c5lwzy(0H28rzFee4q(&(7revEvfuP`; z<-gO9LIeb6HK;kF+hud=egNl$Nt8q+$mD%2(o0{D*%mg{x_kbrgxJnIWCI^?PLP|j z9{&I-vb2gZ!q8~3qfld^-Go4hs<62i{t82?TY0-j_;F0QQm8_KA6l%q$`BczHPMkb z*AClV)GLtlAlDCEgYJlxND-#kNIjH3f%1gA>o&!d(R8_@ftXwkRiXDivF1s)yL{CY zy^Ihic}yZzm^)%TeJWO^LeqgTkTIp100yyK-Qm&Dn!mbU+{V;;i_>DfeGH_`K|;J@87J}GaVryEQF%^)*lwhxR)Qr@hVOugD#3sIfCUNwQmzH zd>|_V6t@B(9g{jPN|nvom1ZLRbJ%z~-$?UE9~X2@ zIN=l|2!J*JJKsV;1Ne7|(Z9hM&xMbik2&hWyj{UN-_M)ZcsFqFx|Cv;!kR@M;P$Ow zjw+7c583)sn*|oGrOmdx7_=S-73}42`5=WTW{Ez2RlS{Af`zqP8AhyFiM<~R2>YH$K(?^T!E2h?v30CiVAr9$}@??XrVLia`?@f*3@^LLX`5=>g*-APZMq!-J zEWHKBkeeRi@s+%Rr%1%5?@7yU6RuUD5|HY8Ecg6$Ukyq&t*vLb%R^Z@NfeH{fdG5- zYTt{ndr?jJADi2~|IPN8L&Oym!GbL!@N)EvZitfLbuxbD1ZjN3gsRXa5q|YmP9^O= zziZ3w-tBc~-V>xA<0Hyyv9s8&C(OXvMwXvq#*EQtuk}NziksZo7$L7ho?xCqBmx2- zy;CSyOd_qIc)DMiL9wP)Mt=46LLxDw^IqdA3Zfy4HDKuja;y(1OfU<}=v)77>cSI_ zw~D4-DZNxAy6n29kR*18$A&^{58FxbK<^0+S%w+z`V-pW!EXVcZv!qi?nnv_JxJ|6 z`vcI{d=CPNMuoIz0UcS04j2bjr^p_x6qg0io8tj`ob*3bAax&UWugNFU4*m5rYK~E zB+fIoRfdYX_Pnm3(7vwY`Uz3@F}{UKpbvC%osnojI&%h)CLW8xBl)8&2ZE4aL!I|0 zWH`za?MnbZ!D985Ljp5#zT*Y@BdRXKIIC7Absv_bHPT}PdS=`dnwo zpJ4Z}Il>qorG&&E0IIdg8}@}tT{9#@V}g+Uk_+@}B@rM2s~E^@2+LAcg!V`lg8#F+ zbJ2jQEkJw>`QCB8!#Qg;Xx9Jj&yn8Jx41M5*s71ajW+Yk(*=yD+-z-SXVsWT7njAO zB<^D}pFbepJ9wvwLI&-S!w^Idzh5v?YefIs&%E5VGG_0UMJr;9F0ojCx6CxFRD}_k z*7$ZgE^Imm8V)xiQP@sy_B%_ITAg=jh{))Q;t!P07HDcGpll?X^F8e1QzUsm_vNb) z9sg5}c;spa4qc@S4vWr@R?lPGK_dQV!$GoxK4MU(BAbTfuMl~O3#61{291h_&+HuQg8i=#7h?Y*I&Z-uc0t5J zi=r7xuUrrVgW3*kO0o#w+~xS+4V8NMpO1k9TMN~kXOPm~95O%o1A?xPG__3v=<>wU zYC${6Qt1dfn79x|KRj*iLe>{1;d-{3sRTC94Ar>IcpVoQ*LUs zk#6Ub(L59~HgfG}QIX~2h;+IpDM57kNlb(^%@~qN zeS|O16rqzS-dI%`HQA9h--MnpFS{-9Ypn*+8Ie8=V#%{@-{J;@W2CeJd+G416@;q! zAC4}Amg--~&uDlS(g+!w>!6#}02YD~o#?t*tcbEmM2VCwGQE^nZK0$~Z#25*=9ig(8zVT=pL4h=IIRpgjXS8bfX zetUw#2FsoA7z)duXrr}u{pCE8T{YYFOwX;HRdTB>gq{)?SB0*yAWz_FI5-4vF$IT8 ziYKu26rnlmt}4I#N_*uTK{jz+9-)WqlmIf;fRAy0be+Zg7$`_a?f6uM?6zcHl`^zi z)vn-Y`}dFz&gY1e9GKK7n%oLWD|58}?fJ?#u_joCZ=1-N<*q>;t6%7tI1Nq;z6w$N#;zrEL88S! zq{sMULoklJuNid9&-RVl7iP-x*m^myhb{(azrq??*-Zg#t^?FJh=`idWYQZZz7Sdy zbb&=FyjY0v^*8?Ik}13OA#+Klsq0Q2W@#^j9g{PFF%@Gqo8(h6nJhcV1iVN2$N@|? zENpB*|8}X&K|tMyjGTJMQ~HmA%$pS#zj@xGuk8ndNjNBy4+==4LY$O_~T9rG=CqYZ9D>E%I++9rZQy!O9G0?XZXaE1nDx3E2pPL^hL z{Olz#aF8Cz9LE4c6}Z;A8{X*cl7ZAM&PJ$@^xHyhPzoSNL=)KEAGfHPa5VFrG|Z?r zxb6%`%rVF;;c^7Yn_~thI1`y}yr=4u=%ZoK;xu;sO`uJ@pol{S`aI3q{2{+9Vk;3t z;8zbC9`bZp7i!{_97)acrhng1w+ud^xX!MV(pBaRx)DO!fF( zyjO$r;^HtWoap=az#~oPATzqlakH(J-vdox!DMu7PLiVb+Cn%GTw2-5!FXCot%i4PJ7|IoZG5@W&IYpPS8)WLP2rI;;U~0?* zV3GxXU3~HetyH@oGXU~|x@IZ{mPp zYbH5o7IT#$OH4bt^h!v4ZKh~o-Ra`{#(l6;UqL^h85SXhuRSzPMfkxbhm3ivny-Q0 z*rom>eFdp_)#5;6N4~!(-%aQmLk9RPDV0?x-gLjapMuMAP|tE8FOmKyjl?Rjs-`L$d-l0TWQxx_xQh? z!hWRqZ~|oHBwH2V=sBHPk1K>$x0Eg_yK~>2)Y%0XDD(YsBiOck*o9N1nwSBe@RQ5i&YSdh zkyPssI7pMmnMSpzOPUg_v-%DDNV&xcDkDlaTGaz1q;?czFM>fAKJ*VaDg$|P$8#`- zT(=X5J^yraqoX2bT08E5ht@S7O&F%wEQJ&<1Yb=!9tfuUEuLB4CJLPW*_~ycHv1|Bc|=~xlTSbg&J)(sh!|xfcw;*fTPbHPWAdnm4w%T*a`w6*FquE5h(ENq~ZJ*q;~FRwYXn!+NGmRqjf$5+V7NLLY)Bs7m<{Bv-52PTr9U zc^>iK&eM4j^IubAIREB(OH432oJs^|NCEfh0fds;Xk#v?2nk@8cHDCX4Eu#n+v|to z8O(=Sy%T?cXM^u@?MY4$_ND#a2fOdK1wV+fCBFBTMsU}gm9F&6FJ>l22)GwV?n5W! z)am>xSZVF5;4k<M7wyozQ(tOs_c=uK_0X9-lSFJHk!N7G?-#ycAr7wlr-DAL(6o}KWvT+0?0CEVwl5qimJ zmM;xvBmEbb_`zi+9m4gX<(}h#U3t9D%aBav@0s>L_|JATZbesv`C&X4m_(H!b^=p% ztA~9x=I>|1?6b~{Zq{sLFp_*TF)fhEH0nx4qtrK8*f=(+REO;vgB?Ua#%;*LnqGy&VVis{Bl4Db3m1`|!mBki}13h^!2MNdp!;ghIrvYubEwc#k0 zg^m!{VlufhPGT;6ya)ka91fkGxSn5zTbjD!#`j+mg~rRkBs!;w&S#_WU;pC* z++$)kKMLXx{Moi0F&TR9wduQ!+3spqzG}I`^?5%0{e%Rym)Pp+a66TmZM)}?SR<0K z#cIFsPj53u-VI`PV{>Fc42%V5>t1x=|<*9Bj<&WKDUWEOkx?} z=^}!QBlgF=y+@&p320jV*SlzQFz@y&vpP+Uleon)NX{5pk;cp)MnOlX!4L?O++DEB z8%USRx$MP~l(vQ+gN7K}Cj~zgvEgIhmwsX7gtcPpJB*%I-2}$NgSY2qm*#WhoGojm z_{2#c&8NKOPE}%G3q{9lEcQ^PKBx$eYDxI8w)nbsY_vxoEac_nP^4VgRRBHG-Vme= z)>GSnoL@vO+svjpbVu!Y@1EQ4B}WF+qW8v&TB!~o)l^tNeV(-3V%g^ei@y^2b&@kp zBFrp7sl=c!(SjH|pz!iXL^wjGp82?VBTmb&8XL$@9?7riU?)ddTg}SVLoV_!Jyc~ZWXk`I<45#ZgFE9-q zjQgAaHw(HAAiqHfZoA!IlO6}@w+(&WBrcewbl#_aNT(&RomqVgp0Z->t zi9+K~iq9HIVKpj8td9>aOY6S%A(VnV}=pe(RxrinMuomZZ{Odk% zp+|k(<&m2@xXk!LE+HOLY7ixRaqb4L=`r^#+Dwifo=sclCh^H;_n;4J-sFbSk_ftw zw1m$%5M7-KvxkZPC0!G8XsW#vq??TF?kzrUQ;;fkYx~ph+N}5dJUtyCa4DXaXr5UI zw_YCfcR$$l1+3xxpdGx=*K-W$-ny2QYyMB0?&sbeDnd{97x(TnTU37;vd%*{_bb1U zy4g6jRy}XNB*$ulNu*!)a`QhD+~RX@$<8J!RY$)|ogozlBn5E~O>9ksx!`m(hCvF1KB^YCZfaGs*6v1vS697EG*g40G?e8_V`HA!2e zro|;ZK&$C!bQ~g4G%Mo|u>mVpu99>( zX2??*I$rJi8YjR3-#j8ROaBv_K8!yS9tg zRylUw_EhYQ8BoC?-E}Ci?N0Gk9DKBVy5rv>3BH)G(y8+$BV=PK9bnLq*D%9wL}_Be z0bo>uRPK_<&__x060HADB73Fj=g~O(dgS|fY30o0eC93G_{8h=fb~NBxDf+vwsHVS zs8VSBdiGU@0}r8Odu(KQ()V8|Km+}sO2r8MMMGCl^w!(#P)8SerzX_qDe16Gs-pNM zZo-lnTT$?m;&yOHG&rw_yqkYxP{d|B!XG3fg+(cl-Ff5P(xuT>u9x@#P56? zXZX(xeuY+tHkRQKT(2?CfKQh2VRgZQ4Ds5Dr8J&Mq_xrf?h*>=YyF}5*PT@x+nceY zH^Vrx`qbZQXo7OtjT*B(soti)`WzhFIu-r_Ybm<8ZiTVx{OMHC=TFC}ioIc)I@Kue zKK9jKL-6ebkt$v1Zp(FzOt9J0gTm+()(oer)nv7{cKVkQ-9ON9qimJbSl_wYNeSk} z0BuIj@S&6FSz95-&0Di-KZ(@UbM6&vB}_&$RE2|4W}-hTq_$}1ox8C`r-Y9dfAKX{ zWvAkcuM;7{P>AD`vHNs&ZF&OuMa|h_hK>lFj76N^p5cJN{;YBU0bJzE?cy6AxpEaP z^1C_t@d>=^-xkAdr?}b|H+oz;a?E7vF>+UbQbFhvtck&;CXc{wt}y?d?hB>pmu8Sx z2LIVh483g0$Uhs`HSk|SA&p1B;V3~b5D+lluziGGSq+4B2YLK@i8_f1UNx?itC7s> zsFClU!@q2vAu(Pp6syqzLDJ%I=}^NpEK7BGXHmTdKvH2p?x5>>H1|695SrG zU1INS^2BlDVoE5JS9(*+?2#=~qXLt+7ASBC;-?j$sr9SSuI6Wc5tmD@n81zYtcQ#q z?4kBoLd-&#PE+zN27^8=SIi*hLaT?}3`zs0mMqTPKZxb9JwTLK?+H09Hc- z00s{Kg<;ex(HIUP62`9g*>0vS=J;nb30>9`ZYP^#xEXuKnlSDf+@ay|Jjl`|(_unx)3LDlnuoFaCOI zDgKUPD!F)-|EuV&BNxRx>mX3B<@r#9HNytV(+$*tK=7SRwKo=oYwP8a!JeH%m_^JZktBmFZJ7~r6g^k^@h{#b%`%4 zjdoUVrLY7Z21PvAO&nwrT#|M}0Vq?c{Z!N;{@*^u9#>ugKL4jLzQNSvC95FNV9Tv} zIw}~|{YFo1R*Bi@TlP{(+tDdnED!8kr4n%_p{UayP)q@XTw``32^f~f)Gy?v7tN}( zKJh3Z1-d4D57Lj-T6%?3dozyl7(NkQ z9$T7DgfJbuJ&9cO!UBjHRpWmV4qvfnOp4jPvUURv5K15?BI{?jo_&0`D;9IpO+uSp zyiYX&<_f}!AI)PGIB|GrWSsisDYbT7`8>UD$DeMcqH!_G)+CQr%t1jOdG^TB9^3i! zyH-C4^_Pag^;AWv-tK&D){YC42h))&2IORx+9l z9215Gn6Tl&-t9v#R?u^UFEu03B3@SN58|U}D{ZS*3_EDBsW+o)fpGBq%mQbZw$)9K zj<%;w&m?{Axp$hDyG{85o*v>-z+l0~t=pr!Rfn2LvLw)EYs7j>qQ%zw;H@Vx#z$pK z)u_7Yu0nl`Z&1HeVFmYBFua&LpbSOQ+cH|BYF#GmsRUl=U@_I2$nQ+4XP!P6n@gScDA(e z;)vHp2|m`6$VPw2*xYHrPd>+Q89Ekt8K++v=sjnih`B<*$+xbrZQkAQ`HgK0?DsS7 zHBJz^>W!5CkYQ*tQ(82tURPakSzYy;>2iksp-zCQ$%@|Ru_`NuWC;{wSEZ@y8}f^I zjikBv$NOe*TLpB5_!x2pqMI?dDfH}cC(0W5@Tz$4vJw*9hNDyU$#Uy2Dgr2yn7c6^@tl1jl@Rm;l5`@_=WXEBr^dN>u#8QX{6{#YXNg%g;tSP=5;r zk1joWHou56yY*=oD|U<5KDEf_7qnO34)Y^qcL3y0a{OD)^H=WsdOLe8y-zx4GeZ?u zyy_eif^q~U@STY>ttL}G0xp*kQ=+x#X3ai@)$0|D!S%aRp5D{z$5316{W-hnf{2qZE0c|3Q`?+(|W}oOQrUsg9u#BE6O` zGm0=^3JQmV{ss~Y=rjiDmWAk`2TF;pKH^spiPobx#0kv?`)-(e&!~WrLp~kV;&G$? zRxV`vimbY-pA}4lDz=iWe`m1}d=E_QB@t??^&nM@a|-lMbGm$Y-~X*g-2Z?WV+)CM zuT;nhoXG3W_e(|erdPI5Q0N{^D1o!otQG@MH(;Fh6l7;6~N$Ztv5PN&ISRkJN?`87dd?v?Zb4VkrHl|j#r^| zIAkabgDVrKGQE#Iw0KToI^1$Y({fS8>&n3blqb=g{yY4FUErY}zm|)|N&K=)&*;6q zJhgQHWLE=j^kgljyFo|aLAgGuWy zF<-@czyECh`#t}F<8Q@?mFce1Qf!RVht`DoTOE=qPL};_JMpzqy+gG)9#1s>u3ltD zr5Izn6qUSJqP&9+(4=ACBT0`O;)A#WW2CxoQ}~df&xv3?5W%4@0M)t!qVjaQO;==m zmhM$TI|6-liEQIWGU3n8zb>bpB$y@0vtWI9JK7h)Fal>KcxpAgHT z?nJ2jl#EGcNe_!fViQM=~d(bJ}A-$VCr0vm$z*23)SuBYYF&yqOw2|k`n7DQWj z=tT4K8~`OPx9I;#15tpY#0LCs=I?=h9OZIa^MnSDAFhbtOa!USZ1MbY)?ZZ|4rQfT zUyY+bb9%RnHau3#-#d?X-Fat{etSui=%1UZfLTvuHDi9_5--xG)ZE%abz??Ft6 z-LpE|=ej^H*qC4d7I*@=vO&wZj#`98M_OZCs&?iQAtY!$0DnKW`%U8EiV zCooE3kK#qv%Py@4YE)#_3dij)W|F^SPBhW z>x-H8SYj9BkPICY{tOJeeAS1<<-}t(UI^`=_^o-3EF;Urvs@Jb@JQR|0IzzdN@quH zF=h-cZn|DdUhB9r>=fZr)o;e(eUYG-eDGh97ovlpKJis;z`ofA@JR}b*gOYZBx3!a zZg(9{5Sf9QFvty$`Hm3Ux8(N|(mp73i?aD}VFFmXut)C+jmxR9O94?5Jv#M`#JEl2SD;J!+a zs0DsTp>UA(exE5V2*wsjk~MU9r{*+>jq0zx8B`q4LAjQs!W2Gx;d9n|x|idTQKsOh zitBt9uy^3XnWpa%ezFv^#&9@;K{Zw)ti{3x)3<}TvIOf1Fp^a*h~CDpQRVPUV>qBn zv|TkI?q^F3OdaNom25c~d%Hi4Hp{{d_|NWBwn|#vmcPF89FX<}CAe;!99x4}NuNes zoJKn1Y`jjR>4aLg!*4^<+Sa&N_Q>7>HGNzcK-;6q{#Q^A0hQ z^10Oy46e^458ri=de@BRc7a41@FOrUPlip~YEUA{!ACfgzu~hInQloN zOm%k0ob{Z7dd~iF!%Lu7`u>h)6*I31jYvVKKwNk`D2f4g6w)4vhJgkJl61(azfoJ` z2-TXMy+=7Yoi0+MhQ2n(_UI*r0K%B_dw|qKjEmiDn(na`*wAusHPDx51bC%Uup74j zn6KK=Py9|MPdCk-8;gS#&x$9x(@A8TT8i4T8aGN7LcCog6^>O*S!y^;pz!U5x9pIf zidMAf|5&l#(F*Cjr5=*}1iK%Sw}Np-njC~91)?1MKAA$tTefu5fnd+;tRG;;G|VtxqgCFlQF~d>$7xls{2Hp$t&QIrHzaez-BmZ^lW!{Lv#ikO*VEMcxze{? z*@IK5wm5h4tLj)~;BNh0TCqRcW<1`^{3}Ac{ZL{1$_F&fAtHgCtO8{8h4-o^amfy@ zfUkLHOZQQ((!fk6^2pFYtrLoz*exEBhR&qrchT58%4R+K9aW(}nAEujX4S$mSEtkC ztEg}F>gP~7(ue$IqT64h1t1);dx@#tE{Mt4)swg|m|Y4oxG04mGaoKK4h`tsd)nJf zqnh)UIaOL`8<0l**l<|v5z8nu9Yc9y&UGbxV1Bk@#~rW%vIR?PvLzZZelnvlg=F9=6s$0Zu4M zV8}P@w;;2M+_KZ9L*0m)I=+FX_B@!Y>V}Bf#qF$bSC)!cJTx~XQn3S#;B|o@bc%(w ze0z++*-KPPkd|QvrRi5neHu@7b0(Yuoftn=3#BOS&VwmP%?|{Of*WX?56VTi8eOxY3UEby-ZMfCki|Ka|Kn(|mWs2rFZc z!DSy^vyJK=ZVcu5+kykqL6D&FTr@#7qj1$5C}dOjcym30O7$fUpivJyBz!Cg7_Zq6>q^ zw65iTFJrxQK{Hl>%W3h&y&nSw{Q$Yo=fCLg=xy9SA_PRZcslUDSJwJ%pFhlnY{mGS zoW%hWJS2YqnqPsL>?TU`3_P;5zb8C`b=Cqbj9TFh+3w4K6)vVr#4rg9zXBq07AFZiaN~3Pr<1hjQnYMDkC-O}m4Jy6`s!;Oc&F1%_ zS18HYK5^WiRqcl??703E+-@Qwk79JO6xN2MrwE=TxZYA|jWwIR5#P+BYOaNWs5IX$ zjc}MB;^F^QCcGb__^dx28`*|frxm~QdN|q{5^{qEt8oUrfe{vOk(G=Mf|aQ8f_n)x z;28>d$AEZx3R~>KSX%kw+dm>r)9{}u|D$;R0B-NO0Egb9f+6KxthGjIe~!# zUNSd)Shu;DU0J_dj=lRMXTLPx^@3>@?n2VeK@oz8f>L?=tnM49ygF+8~u(Qcd`CYxeva^k`l=psujG zVk!xJ3$>>RDuS3_#eVcz`aJw>Lh>{Co)v%NH7YPO8vU*(5b@9>7}!9B!@Wt|A}fzt zV`n#<=}d1y)oRtp{IW-r>~zAyvFErJAm;OWo#@Zd_0@q=N#5We&Q%cArOBhkd@+wu z!tV*okT}@ziA354%wOfCITFwM=|~Q>;VIt>iXF86OHV092cCm)F5~uo)4+P(3Zvhi z7f~O>lAH?JT#2_DW*(u6Zs^38$K6<>z|y5!VA4yPMUz454;kRo&aW}hu5eLN?fmm-&j=O~cAU{aKHs0EVikXT9KF`z(RB+=YLv^hncBk|SM z$m(-A{QCyFv@8t)VPqwv570XSMmztN*6)V_Hsjc|9Aq^C^Y(=tY_IBKwtgG?v2NZ} zeZbl`4UyrmFE@Hyk?|&$dxKIS;B1{YB+)MFuz>E~Q(8@@rB;b7{r5c9QQ2B#ptei< za?A5I8$n$g`7n#=;V0~-km#$DH#$Rv9p@)sIE+1{Bu`a;rccxMs`8w~%vk!%VRt?t z?^w98Wg5*Qo6=$%cyW8!b{KqR%l}Cfldh<=B!l0wB@V|CTIiIa^+`Hk$YZ-8qT#5S zwx|uo=17+zSM=C-St~O-?Fep-HGFr6e77?ZpmLK;>s~jqtWTfNRCuBGZD{0|FiE%y z|Dtc4T~ec7>u{zUaAzn!m;Cqium-#$?2f<)JoLg)Iu5wN-!XsV_WAu^0Z92bk4n|? zPzMjfwpN03!u#w%KWpc%-g4nu>|!k|y*=BzmIfhx!R85Q`|_iQ-s=h$9%9wJ>{#Pc zn7`f6cyMn6c`cshYrXq1uJoNK?HbJ!P8Orube2VyC}9WtI(n*-w7mzwdo-r8OD-PS5j%X0f zTmb1%>jMsvJO^>E-L4GR&Dh-b+>EaGjG4{|B3FxM3{#DG5NCW2pBz^2D9V&i3I9%g zT>rV49+!k@64TD{E<2WvUp=%RH3m^IwZ8F2b|DJwJ18EfypMVDf@n}dTH+&rhV8?y z+L3|jES$%tuSIThFzU|wyK;zG{2*i?w7um;+22I&0gJl2t-{(zxb*1XsAM|slv?`? zotaHpL2tOZ4EE#EClDRfOh8m82B^tA4lvw#JHn z;w7?%+KB*VFX2nkpX;L>HqYzOmQWp!O791|naHfqN=jnn>bx@)sOmwq#kdkY*bFc< z6gZ6j2jW>b7XvLQHz6WIo`!`M508Kf{-&L48x$yuG!Yz6s7^)46f`5!KeZ(#OzIS= z(5@W7x4Tl}bGhtW-FWre`n#`mjdBgrD7 zt%I`%#QM7lVOE0xQrhQRs;KQT!5QKU`oB`?^@S4cMQLj3_863G(<>{iA|fIuZxlQ5 zHUPC$0xC9Tv!DI(@iDi}G~NEmNeX!V%Ll`NM~~|knOz;0CH)B+X!=M7MB)Vk&evZv zj9))jbfYrBo_4DP5;;5flb`QK1aH>^F)G)%ZV=_5*n*oAeoyknk25*j6VLPP&!W!4 z&wKF>Y{`|~s5WEYJ}XsN)v&4|;W&B289V}CAW^>vWCC8PGik~f$ekQc-QGPMU-{jR zEIp@e?cVK#cOJqQa{N=)pFh)0U^^u%Y`rtT8`(04Ry?yk-*IMfKePl2&7x3VHP7vR zO|q10FPi20k<5w^0nu+s92WoQqQ#MbS7>qa2!YMO8@WWrQzD$9Q$t}qw5Y(lzeF?7 zcmZQydFs7O!V~|$fBt`O-T!ZvP2O`1r3ecKAzNws{n^%l#msjqF~k1vB(y{v`osyc zU2Xo4tzQ6_5jZfg!M=N%%#*4E1jKSCa)R#WqS}eO)Q{u zG+Sh)NCc>^bHe^Xg}0_1bf&P&!-Ww5J?J}lGin(?HD6`Gy3*tjrNc4=1HN#->1BP; z{`sB`zI)lHCy9M^c{$YScQ5G%__D0{fnB%dTikY4m%wZWtsk+dMA^WOt8ukv11Zy4 zqJE!%kboWkg`94Qs#pB%&%o?wdTuVc2F2Wt4!AHd0nbjhi0{=PU~RuWTlQgkZ7uq< zop~>-&Qb&+o%!rdbSP%BI@JG63t9G$7PPY4)MJ zhNED$(JnR0ey;$x3m`wWS|pQBu`=TE9Rr_XtxXxNT!n?)*T+Y{0f>5afp?=)z1D5j zwX$zv#X4gs@+A6B86Nr8p%-voj@M(UbZ`W6AY31AToyRk;P$uw_`TLzLz9?4m;)nK zDFy}iCD?eDkW9cq!G1&RUY*KwWID&9cp^u}?6u#`X1JrP995&-;qgk*lLj8#SW&I~~9*c^V2e z#B)`c4s9HwgN;>}EEmP9wBC5yh)m>&o!;fmt5IbQw)k9@-`fL14{j%DyT&K$`TKBMYu(n>9s@vG{kX?c9JCNX(hr*}7vgFi4y9da^d?>Yf-AxMeI zA=eHF&W$d<_5G`CzYG;raswWz#MO$>k{MBl$s67FeY#())$KH1r%(?Bsq2B8xL%t9=Ue7tn4iIHJ|ngBx;LI z9iN{b>Ej9B=%`;m{@8f+O)?^K6=5G|J7yQoYrh#&As{=iyh*R2krk z({7l&NXys5R0>0jFkj)1l_-4)E~&pu#8@WM{987tTWk6|x9M_D+o%OG$llu}$z5X8 z26*VB0MO@ObKpBAAK3V<0%lh_6UKdOhI{o5=!Lc(oZ2O9p3;4KgdJgH z|13l5_ZUE7VZV6F)<#WJ$6`YFO~&FEdiPGTN8W0DcRWik-2hS)&EGs{A<-@3`l%lX zGRbtFl~@PLjw0g6=NkNg{ler!j5^za4!NZPZPd&bru}%F{AHbQH7yLksM4dR&=r`>6>aesyfS)`}zu?o!-ZKi!KXNp0uHpKrMc%{pwo3@C6` z^(^m~FOWn(y`&UnKcDA*%NKniKTZxq_o_7?c_2NqL( z-My=?Qa@F{nmk0k80Yc3K=WI3I^d*l8p3Kr9PeVG{wxNovl!8D(2d>ZOsz5m*5M_K z{ACr`Z`gn!cl{Q(zZAx9ySCN%=b~7)y=VcfgYuuJ91YZBrNPGKO-e2MSwsYT8;h0; zPbNiYg(6ud_m-jnl3M6gF`_!f50vY!MG5+ylYES}LOs;&9KS2w?L%xlnla5Lnn*~E zMYqX#gI}IH$NqfOCJ+Uqr>$J1)T)W*2LN(XRkUbrrFU6gP!iXrFSG95cXW~MHY6-n z%FYL;I~@C}EKpvff1Wbp|NAN1=E)hNC4t`4oz0iZ;e1oaiVS|Qcm4J=oY!s3HfozobSeFs@SK8+o75T0&4FY;6(p(@u}nr zljiH^C@ig1bs#GS%5IY>=vKVfsbcV|3^f-Cn^ad`K!FSTKW$xkJk)C!H)9Kx zq0r4XmUmYIkHEZQ|MKW2&YagLQZ z$zEtte$T#c9zA&UG8}VSn3|lO2CTflCDt)qBbw5iWy_G}>(ogYmD?nJR{J#?ViAOK zEgp@5i4NYHiF}lJxd(W`jz5V`fpy3$l82^JU|x_)k%a)^U{tYRtu+ykgiuGM?*%d@ z6DA&vNmb%oVnZiT*>DBd7-L?9)2}<6ZVL05;pv-#0LcVKm*X~6kP@&B9jmM(H_AMj zlzuPqy!Gb6rMY*_r?0+iatkqQ7T3nV-4iP+^#M}Q4B^_5P0JJ1lsa2-+*e0} z&kPkrr{XIFau={dSGu99$_e9?JNGF>(d2TvXY$Lk|CS1IroNHa@#qiIj3x za;FFPjH|35Cc6duQ;h8BxhsQbJ_s8wb1T0D4k4BUNtt41*WO3#xz}peB-UZrvZvGU zS^?Sqdj+g8hP~v6XKx;g2c8_82EVp9^Wen~P)zL4{{Hcd-5goVZ`ukuOSqwxrmuZb zf!Y(`eN91ApH#5Vz2GYHysFxtzg&k}Z7p||kG}P(sl|FVX+mj|)-5kOlxFUQS|w8p zmHxsAC=Z%`VK4}fZvu$5;U3pYryQg|2)DEh#o?^`wPY?-dlBJcEm9E7HCg#Bs zK7fDG4~&SgS2lP$&}6U}qpHQAwGrlID67F4DQ znYlSPtOmW9?vFMG^%gs3d9rx^<`VW?>s%)t%gsJ`F+ta9puVxO7wojp?Ar<~H|J=W zwWxb$#2U@OcyhSh^7^N-rVhhP>C+!Dypz9;1{9t-XnBu`I$#A# zzx(%27Tjse-eGu9VXRJ`z#KWR+U}F4lNV~{yMRZTAHD?rL$PJWMALa?ov)Bvd&cmL z#;yHVmPLz3$UbM?kM}cD&E=xDo6G0J%Lm!jVe&;6)!DZv$Q*%s*$6G#ctqocLFfsV zgLWr}4ZHgbY^9Tx2G!ZVZ*uidS9(gA?;!kb0$tH~?|JN*qU&Si$do4cI8uP@>aVHQ zp@W?s6>ByP6^K_~84Af1DhK;ahz|h%hH&iCVwRgOz?OEc))6xGmVC4`uwIht7u(pMI{JifR5SW@%#!Ce2;eYlcYr-Y02d<{P1T3c5#bMUgk#Fe=Eh6ek9 zfbvr3t7LNU)jE3D#vLntJ|N`r-gyufjaLJ85S^pWV>cT;%xpKr9~OEs$$MSa88cot zn*2xw;R7X3Vd6ENUWbw?M7BvX^Iv|pBtf*q!-qrMgh6cxEEMO^<}?>+1kKRrw%*x# z|D^TS?akTaTQ7T(8xK%>Ca_ixvf*_t)OP0itszcJpy38dJ;<8vy8RKgEO6|80dj7{ z*}I^=2SseZq83Olq<2`;lszxRYW~*g8b87~QKEw-KE5fG#5O1?an9Id?vmk{Fn?!Y z)4IlBgMD_Mz*J*z^h)vdp$pVn_m?ULc@N;J7}R@7?{qq!Nc-pD#!#hK$T_k~1bE`u zoSd9bxhBmgUfr`GA2F|uf@fBhk_%Nr2g2O&_g`csyU1nz)b*^wqj7BO`5IGnzC6?o zFH4mPN@~PxlPBHy=Y1g^ej>M4m=a+DU$)0jA3cSmDC7Ha*ZF zJoFMXm?42IcP(bTY0Ljhk`JdHJh)L1SP9`gS4|P5$l>ZZ41&LR0zkR$qdyoyCt%j2 zSlYwA*#JcEcSUooAly8F2C*S>NDmYNlHXE(XCjbErYs)DgVo}pxW7tC>;F*U8VDoS zRH#fER?dqC5s8K#al3j9mYDo*S&T=8@c#il?aXldc3_4iTd33mR*pf+0wC}TEY5H^ zI%u)7oiUrT|IE&&!FA*xSuEBEnqO{kJnp9zJC$P;bldi(F86WkZ`u`kBK7o^g$}7o zdc%_^Jy46Ou&Ai3znU&euxb!kwGxpZCLqx`GHq-9vr*|wMgikgh{SN-^^Jw`Tb)^J zMi~1?vr_Dgb4b=3=zXWgdV>o3vT9^)mIe@KZurQuy`S%B=l*hSZS7CjDyDE=B&t*kF;9f%A=il-ZcOv_v5=x}lycx@+=)dST)Ebb9-aM& zHadec5Lrwm$^;PtB>qwWV(^PQ>c?8BL&>Hot?)4F$ObcbGGSDB{X_R2+pb7?X&I9Q zd^mDjR{tG!0L&2v8k@ z@(3-qvATD^>F8E*s<5&@cu`Z%kFQ`6QO*ouyOh!q6{R~(BZime{p2GG=q5u>O#M-j zvQ?uT^mt)+;MTrbfzbBHWI(d8*`hC!B$}n-oqvH8pT)g%N{NAXK8Ad9k{HE7yGoN5 zG!U(nf$dAFS2x`kS&}9sQl2Yrz+(FIAcm8XZDb}`>zzn8#VE#CofOshjnw$%jCjln zsv1$aTZ!9VxJy(2wj+*s?@hHPRitB+);8oA5J&0FJy)}jFSdM{4V?3*xSZ!E0|i3x Nu_utMD=oca{{u)synO%w literal 168458 zcmd42RaD$T(>^#ra1HJf+=IJYaCdiicZVdny9al75AKq|hY9Ww+}U~G@BDYqw-B9bLoW)yY)_IUtQHp@{%19GmiHeqBE-tRW-Y>@k zwLvMn&Rk?jl45$kzP)p}Q?frZun|kZA<3aEw^C=A7xq7E`s%dFYr9gH*JAgl z3eCq|*q+?<&9huI;y(xz|1X4#1JZeXC?!b2wfgN&N@C?9>V=`$_yS%I0Oy72+H}?N zr<%~;cPn|&vPirIPiihmsrisWO3NGD!Pwugb&bGrOet2MU9Jw0$LZ%?knpk0e8Umx zJ7@(8xN_T!!qo@pzySyLgY+tXkp5};qo6Q#@=0q8#`cbOk5oi9XEad20x#@Oc_@*y zpYqD}GZ3!+BP7fJ5|X&*iO>a!lO6W8!F~_%>L3ReLT&a4=L?fOd19P2+uYX1acr#*&6)XIJV0ba2yLfBLG3}h6`de|Bp@lhn9i=(&F=mN$tN}-GD5EyPb?{WWY zv~d4jMtJ-GNZ7Ie|2u(d32H(iW5}j$YC4{cdvE|je85@{M}}~6nf!nF^>*s9o{50)J41wZ4z9JGE4k5K$|t+r z>JfuFZ1y!L4a8Q=fv}Q&|LSv@4Fy3whlgfuP=6_3=CHX;NCedF8x8^VW>oN*E}I*c zxQFZf=^uy-=D(_?&=lTYCX_I1t>_=r<9!1M(eHye-&<8qM7cmrFC{eFq;7h}b$^n&u4i$Ej7|w3zmGqrQaKU2!>?VhI+$`srCogd@9J}3YcnTKHK-Y6u@n}NX7SjRI){?PBXZd*R#xnfAGZ5-HI(=^H8Hb&w--r>5t*D#Eha=cU?J;$?VfyH+{PPzX_ z+c*sdjaL*7jhJpY8a+cS98MPEIQQ$iFOq*?(N2oNlTZb}{);vU_&3tD8!sT&CpvpS z{XDJF7KrOIAyN~&kbFw=oL8+<{wDvxxvmE6%p+1!wLZV1{9WQ|lUTn)J2SLV82 zjc&ajZ0EW@{jIB3{lUy099h%nQF_*(SL<i~5rSipd1{v$G zxlJE7-M#5b0R37ArS}!E<96=Pv&e|Gm2UUnD5AY28PX&u7`X9^?n{=3E$@P}_U$0I z%iagL!ux`cR~e(=H_u~GZ#Prhs+k*P=b8F9M#idb;3@^Md^u}Qh{b3764I^j+{*=M z-_I~M3VQt~W4qngThn=vz1Z$-{B*mdT&h{E)fr`+e0kIOrL36{SkBRsh8+8++$gxb z1Dg)FAoSGPzf_MjK>1fyTja9FoXYmjiKO+ECU=bub=tmW)6L13kF8?0VAc!X9`15c z^|ApGIMB4-{n9NV!9R)G08K;sW-H)$jjej=7hCCYT}AZhrfDBnTr!44`-Q%=>2h-C zOQkQT%sNH5{Oto=`y-kseZ99sF@LXjYunzHD{~lZ-$wY)+&nRvo8NYIIj25-gILzD0|N*2x%D_(M4oQ5tO>i(KbE_gXbN--mTEQK zeS1!-sxAJrVfP0IKRxt+Fqh~1*#Rs8Z8=U|vNX#1Opsl@&%k3rs_AhjW=c!n5r@f+ zEbvwp;JxA0T}$k}>B{bNW#W4?!?QmKPnL=<@~RT_cBjpEQc(i!qwlq5xnEu1H)&rL z{Is8;f*v?Kwu4ifddRckz;QfZBynQCO92#0AA(S*-}mwEZ~7X;)bzck|NR-Nu+6D< za?N7exMlVMeR+kWnIke!;V#cG=c<1Zu~PmZV5Lqi^YYUCkc4 ze)P5r#ZnbjI(16_V4SjNS04Xi?R8($j9(Hqa|Axel6ydSlE&3^3_P5bKkBDepCCLz z{g-YJ)t4 zp5nV`j&nZ=>-JHhYk-vYi9&gvjU#3N$6U7y!cDX4|)ZYzy8k(WPNY< z0#9$=2j+e{Fc70IU~2rmvsgN%W*VqNFHN@NInNUKXtc7wo${_VHN(^jY)Ov9J)x8L z1bVJn9$$HzDOoSotCX?Q6qp`A{f#Dr{u-lKuQ=sVt~yQzYtcM)X`6eux(%8yB7*iif}p; zv1P#K+mRuBziQqD)U!wQ*f9(_P181_XV$L*ll}-SDabHz`;qH5Ne@Z&AZ}^@J7s)D z3USmwwf4?xejmozCBjMT-QfBp^C&IZ*3r>rs@?VGyO1r;n!{_P&SRA{O1#0QSZmpX z_ML#_c;}{8g1~&C8)YH=N7>{eSX>?Qvx2s<8xFD&TT_jE|NMBrp4EKK6Oew+4Ld$F zy$?qqVqNlKFqeE8J^50e$m#~eMr#J_RMjt;awJ{rPAHZ2$jDzK=Yz4s- zto^UqT@itVXkC(D!l{;SU@~llT#v_gV!YKTTs_wUnia7Jm`FgZk~Rs@qXZmQ75h6R zrGq)w{U2}2R@0f4@V#w$4ogABk+mvNh59R&7U8Hs^_Vd@dQ<-GaG{L-8BB781);Mi zJR>#j<`ozl#1I16VQvLM3_HNrTFd=3?Nny&ZAoTQdsu!+TfHN_n6NTLtiFuH{9sv- z{{zg^{>rw-(f4VYrX9Uf#G!GdDOOs2hsGb!Nq6Ci%Zb90+krPT?fcY*SmvVvMdVEk zaX1a!HQJ)Yr>3VL4xd8|0#_{A9;ZL~ib9d^K*E7K7?jx2m&^X(1~n*RIb9w1JRES} z^(13h8Y58$oaBz#kJJ6eF;b!~DHNl*d?A=^soI7bU!T-Vkl$(T71IxGfHO8s7=R`O zO^f0EbIKV1n4ixSbNeT4Wgy0#shENPd8>Nh9)T!507LJZ)HOJkvQaq=aWY?7_(`%M z5lW@99FnsRwqh5Kb8H9P#4(Ttp`W7?uv6Wle{EloCW1ZP!<1)Aaz%0xBM}J-s6cnI zRW^{PaMKo!>BBW1UA`CQu*Hk}nNt>3$i_dCGKes5q;Mk9F)Ej5Q zL0~4PT@oOB<(C#N5LDtm>ZtNKZ_ohA2G{gK_6Y7PxSzzpMnl-4uHR}s7hR{5cGa~& zi}J-;uKURHf*}4YfyuJcbcS49MX$sgmoYgc(EQbxRFvm{r~v&XJu5{i{R-j9^9|Vt zEls!IYB=Xl8BF(n))0bJ!s4GIxH%tU~Id+;F1&baa*rxeHf$5%Sh?7)heYgrn(Z&#@Aa64YWiP8V8&tC4v zm@b5a0uy;d=s}qLvRB4HHu7GK`SSDo_r=t0MH`No;j$bDCjZOcZjZAL6C>b2#ntno zeElS4328>bpBgXXfDK-DAzvh%fJ49Hq*?OR$X6mC4*L;ETuSM^ z$){G!77Q0k!2R>OMVGG-#vC!Mes<+2BNOWjSwlNh|7Mf#0!1H9EQ?LF4CoZe}7 zad)jeh>id1iCf>99+Eg(H;Ch`N1I(`waO~2t1AmpD+~UU%nQQrfa!spXa0k?9oejHdn`o%K1 z0Sy-7rlU7G4TEwkUO+WjmBa|0--vSw7&gY$NXn*|W!kbFJ&_ML9cp#178hR;o!?#? zb8kms)fT$c^yX|RhT904H`(hOvIvMcrsTNCSUSFSA*~2G@7&{G=da`WYfV zH*a9>bo9`sjq!l^F<5f-a#CGybOp+;WmPXzK$N3=^#2=)hlZ3Ea9{ZxJ9jd|2t5Zs z`!7vPuTof(>&Y8pFZgdCZ^H7@ud4K#SU+5sBsy!M+ku*HYT7u-9X+XYZi)0%P)d5h z{P#{h(nkr1GiH_`P(9VJ96>p5&9$?;O%Zp-7^enU=v%-wu$M}-c{g_&E?a*-JaO-< zX3-YveAZI%PBsYq5mbQg!y%euJ3RfzC^zcZCMia^v#*_-&~H`h#csDt8TJ;Qj4Y-7Z!)=nW?1~eMS0LkSw0d!!sR;?yXU+%5oF)vQn0i#WGt1v{>ZlEdv0Eb#C zG~WF{jncBS5a)2Nucp58AyfleaY%Q}o-j`J$4lr*o}`%3nYa2~IcxBCrsuXWdM z(G~xTUSsJvKXG$jY)N|oxH9V;sH}+y*{@J%nDmQSrwH2Z?0hC4yP*q_{T)3E^Em**Q zdWh1&(p-!Dw@7NAXpOQ-G{EDMaZ-(hFJGp1~>&%^)bj87@oXyMs`ET&&ZBoldHW0J9+_|t?zIuED_!yEpw_##DFSeK>u zc9HL3aa49FDZ|K*z9%{jDkQuWDHH*jU#s7y2}9jw6(3_0SI?%X0bT_XuOuXTE~QEd z&iz3cjzUG=hQW}<#LUwM!Pc0k3A|nn55K!y4Z=@3wr}B{u;~F(=L9Z$ROTvFODrI% zZ#~TG(gMilI{4!uxasL2CtBuvQTOZhlzX2&OFp;Nk*$9acWHgA?Hk+E;NA}#ktKRU zUy(i*q=_NhbIFGcgEu9aZiND_)Va2PpX}-7w{~(}7Qvd)kaRBg*Hs{UFwvH_hN*V? zg%W*Ux!5IY(pa#+qMjmshOJ|D>B=?yBxuU9o`QE$n_y?#0o1V*&V_Ca9 zP!CPmus1|jqramWgZdpQD?wpyJr^qjvO%syHJrpZ`VQWQ%sbnRrk$~Q4`O)=$0)wSj%o~ znkn5awo6*+nzb)>RXC;+-&v#;?roNb-*LQemAXt!QT27uRU@>2_KHZD#|ytdQa;HU zo|K&uGq=nSEe7Ju%$J7VEF8wdwMfLmg{7p`FO$TDc!phWR89>{ZfPfJY8g^*b7lHD zH34JnH`=!Y7=z#MtN{;se=Ktgmlj3m{jnvQ zsclEifFzZgFYGC>pAFC&d6_F+HBM{Wy^a&#W6u_tkv{=x7QULXkp$o=(Yy*ysY8=wPkkg)b z6%c-PovJx!zD)SCI+*&Hy)qzZ)f9e`d&42ki!xtGrhh^T&$#G#`LDU>0@`W67SvBW z9WE!k{WPuA{d8mQ-)(Fw>S-Cli`F0ITIB0i%=2VeebY$ZC-k(H)YRR77|}pJ-j}^z zOT+#$jeMrgp>VhGDm{yGAIX->I4*;*cnE>iESE%vA03!V*__lN1SS3e&90U6rltFA z>7axjG~5#8i>z=sW18U8fM`ioJ-jz_C6@^U^EgdQSx;8V`$X5*s=Cx$sEnl_c6SX= zUkg3QDPE@9`0<{IuiI#(K@Wv`;y67t$3z3w@zI?+!M&au^1b3Z82~Q8qxZ@B)B6kA zNk95fS4-p7%Mh}6YN+)CI_{bQeDgmUwnru;P(#;WVVm8Y+D98|9ngGNLE!xweO$oi zwbhNC9M;Jo4zjio438jIoIs2|h_@bt^BUH9nh%$QT}I6Xz2~fH>OeOd3-(jx%kO-1fCKc}lb--Exw2dsC#God&y=j^Ejjr?4mDDZMI8eEI3ZO56o!t4H zcKYIRsQua;b-e}*=i-Em#KBbTLtr_|m@HMC(j)Nlus{BeqHwljO(r{otSmo6)-Nx0 z$ns_5f{yv_5Np>c{ieT%k7~TSwkk=VN61)9*X9Xh5o>Q;f%6@q38zEmo?R$8xeI zI-G6-12=00Q}3?@~n3u%1c@9OXP1wLQux8M8OSVf>7ifiZ>iDVbetjA2` zrBUn74r56X^VyJDsy-^^y~O$`5@gp}t?AeW?3UeN;B%>bP03i@A@-dUay$GkAP(Ry z$WlRfZ$fBWzMU!04bq!e+K>c^TniKUZPdE8Tfyu;$lPH-iMDEqhL{Zf0`mkMr)fvP z3ha#%?LS!;AiuvjVsEZ; z)t{pY_`8eN5jOhcGB$!k!Y}7Ji@f5y9^je$57|rcuGht>y+9l|IvdVaUYlx(=%>pl?Xh-|{^&GzEO&&b+_}w15y)+1D z&OZy&eGO}s8Eaf;V667nyv!x?)A&Zlg%l&{=`;4}y>-P%!tbmr#cDZ6oxE?`8TS6-LUu#tID8EP*82ssl(ugz5+f}Yguh&1VuAUJC{#K zueP&J@Ug#qf63O|@+K0Pq?nw5JL+^{QP3{Amgu&g`$vFYn$1jiDda9tIb%Qc}EHlC7EkQSD$4O>>cyt zNw3y1;fSI&G}|G=(5tgBX-rRA?iF1FMo5Pi|K_zp;1WTL9REfh_4R-}Qn#3{VB7Z~ zo3S2ALDCF;^qa2=kMn3lI{TtnG%?du1#V@jo;0m(8xQ{3dUc&rkN@!9UzM*9RnZzc ze+8i3e&nnhq36fF_qrk=SyN^{u1FG;!T)pjt01XtMCrcF+7MT%4ZHSSF`(RPU}A{F zUAWh?-{8RMiPKiR;l=Jnwy`VPLsis$3*D8D=NY^te*#{@(@ z4a<4$?`tFAc2cy1zQ}tYW7;4ru%G~(jM;`BIks7<)+S(cR(f9UZDHICH>(=z%_m<{3-}e^h(-+JZ zr0|lKlm+-KTz*k~I*cI7B>@2Wfav1mr5T1KH7qn19Q6kb8MZJjbKCP&*eeF^DQzMf zs2m;ZBz`8}cufOi>?VtUH-?Aa1)Mrk-lCNys2SI{tuwg2x_+R#6X03+9`)R?syXU^ zp^t$Ywfu9QAwp4otxyZ&346(?cT-Py)wz!0TZ5 z=$_lxG-lV;F3@*5H%&yjU5UM%;$!MQ#g6_1>AV5zDR|H|`2%vHg{^+@J>+uSs!The zw(xbD%u^FG-ulYT!#m32r+WZ7P;S2(4g@)pQEDasKAjBRQr?;7Ez+?CtjZz(koT4*EHK$ME5q`*`)zg z$RX?tvdPfTh0aVcbKb_U+Q`SQmUm}}OCF3%?(a;9kInFE!_Rxna#_khKd|<Huv7a2_C)bcVn%y(1yrZ>CCotVp*%za;GeugHF?KKFEC+39$Xm~`aB(;O*F-@ zfblHiElUbA|ox2&Pn8q#&|M zc8>D(aHHzl+HXwCVsPW)&nYaXze}B2%Rp}&mB8x^7kSNt2jKTf41?{?YpVQwLa_1C zO-sfp$tf*3?k(2SKE2&QF2TT)$WVoSU`c0^&BkQGD%f18T!6#o!Tc_D2nKlsIxuYZ ztkkDkSAjn;3cc~@2JI(S9R?KDmuUBLYB_3AFHT&1QgNUJ>4A{N4GRNIF5?o89jb>^b{{{BdI;<-- zt9j0D^QYHckoz$}jVfrlpS5?1S}UxKBPwDP6MYn3)LJZPef57EH0_yzdDvpbc%AjX zlqK0jKRV!(WM-kI|GKJ*XW8X8t7|=Vdh*j%u3#yAzMw95p}nA|GKJ^l$Joui=g6XW z4CytVk>8J+qO5*5$EJihKaD!AC_epNsD>z2m(PM4m*4b*OHlU>guX7~;68kgCaz*h ztsXDcHpqu&O0Z*s0-uM3qiM{0f0I_!Ovv*_RB)ZSTiNgZVWB7Akx1I|0oAlSD?pEb zn)bPefcg0dis!j`fn6bRNuVl(fOXj$9Q4B2u&Ev|^v(-=$x#}~K9l$SO45#=vV`N{ zSTuM6SCJsRGY&T5@px?D!$h~x|DYxH?ulIhyc`<&u`w2KYE)PRF0L#o5s{0WmZ{}@ zveAi|6|xR-l8o7>-Xwf0Q_7=xgGWg%2!4*F1jHk!mowIP(4F(QN+5-TmzQP{oD%K{ zh~(;1lvV84^ypD7S!N796w%3C+x3Qx&>F1+U+$Fy@1*cf{@?ss_gw{iOl+)r9HT&nN{WVlU8%YPNC%#E;~4O z3C3lLnu_Wu@%!4IE>rcgWu&ly>r

9mn`@2h@EIeadNi_3z@_{`-QdjqMrh0s|t|IKeGi=v+;@c}+oam6^^Qpy&cFc)W(aW&DRe{hi zrKQ)oDT~2>Iz+p=C67$%&l;ZWJ{$wODHo-zX1qU0#cl@#r--rFDg6a6WTNdL z)BI>=D;|L1gD?+;Fz{@fuB&eGFU4Btp@w5tFGMH=Rvy=1vFiyM4B*vCxk6?EP79r1 z3*rU=v#4>{udD*c;?{s)E}Jj7SIIwPrrN(=k#dH6QLzlcAmYib4|`_!=@(SS+{JR` zT{;Gw)qB++d@GEHKeT>|7r6WR)}qGgEYu}q?3 zXgy1Oih`Hr(+$_H>Mbq?MA|N!SN;iTdEbkkSl5Tu13Z3@Dtk}DADH;+do`rl^iP~H(AIn|j0L(0oEDN5miO-IZJ+y*JM;xX|EScD8E zjy0K9!5(F%2?0OWxvaOj#K4DbWi-tdY#4eTqu_9WX!N$H^}HSax5klad;mu~C?ICj zf5p%*Vg1~$Nbc9sC-EAFcZ{5_^nTCz}wRynOnvpLxGt>=7sK2-_5;1 zgib5+^33uxQELxDAVyU@|0@FDPQOf5&VEoRo5G9i7I(dXV|!p#U&ZMRJ?%p>YwBn_ zy~^;;_o>sshj+w#dh&McTJr;m`hbJ7=C=Wz}Uwa^jwdwcXs}^H6KXg?c|unr`xt| z`us>Fuj}huN|VYb9r(PWh#Xevd#V0hS@`6SsbG&{dpMHDatOCF7J+OLREN~Y0*Bhi zse|d*KN#k7rO9c5QD3Beg|@KCrKz(y<4#S)){qo?&$GyT?I3jH;RSpFj#!>5wmTF+yTM=rK(emFKCS2U~)#_2B1$- zP3rI&V>o3wBN4i8$~DgFia3xE_?Y+eh^E*pO7^!uK*FBq5CNPpia-f+YKpk66&XW- zVw1^ElC$blEadNBs$q1xep zSs0FnbUt*s{Nu`wldVt8hD%Wxntz2s5{mnfCS4K8wM$`ypm&HlzqHU^6u_;XkYFCI zlyON@g;VU8oo~JNP$5}1q1Ds?dhtK~%v`E?>XsFd<@ZVOBSL`+(dSwaiIlbrgsbsT zQ)TWaf12lw4vw2+$0;FWNr4n)1ZESLGj8vqW zV9a#z^%O5S7#KO>4Wu!FPk`y6U93ev^J`?~4Y`QDNo5&Hg3@4xECKL|s+tACkVW|8o zrlwC$S+{ueGd@QdP&p#};NJ95ftPQAA&8=8vGI8=!l(s-quk zC7jYXF7YYkkD#(H2mm0*t{qVZx>r&`GmBizQ z_bCkfLGQ|fx6 zX0oJO;k7pT2>Y~m6gr?Mrb7Dze)qRsIP7I|%v}90yUop<)KstE9pdTF<`xC0DWvu2 z(Di5o_Z-JS3oKGK_qr7d!TZN0C{7wQ4bKn0JE|@G1@Ii?Zl}OAvlt4OS4ai4y!;NS zARRuFkUnneqY0!ywr07KHYAq&y=EA+2G(HD4S-S)5P4L?`hqHG?c&x84PX?%&S;)H z(T+xsV{z`it26MtyDKW7vB|AVALsg$);!PGc=$tv$mO7u-n5|iBC`Y?ug>Fp7+VgH zf$muBlUj80_Z&lrE-vROw0_85sm|w88R9ytWN2Nmpkyn*t*9DKmE6u~SW868owTxH zmWN=37GikjNPZPZ7+^*a6TINIb%zT63mrX=4eJE-)~6&sKtQqZjnzwV6ecwDm0xj6 z^+KS|TJ+m^-KmP!L=mGl6q`avpxD*#`OWOSCW<$N18u z^idgqzeLu-24G_cB}4r=Y48jKARk!@f7U3n{^nM8&LKR&e~}3gKuR~cfM${$G<#!! zD+uH^P0y_=mPq@;r)eKSjHs$&(Vx=NQKnV@S7m=E(*I{osqQqr`emP?9xBZs5Fy7=m03EM1jwbP>f|QznnIak0}RW z?b+R@n{~b)Qp1*kuZ)6XiO#A5g zFU$6;7=eo?{3M<}!xo>F$09}|aDFdP1<2W;kcn~t{WOlT4-9QbNT&&TuA;bNyChGGJve9KJ)1NIF8cD1uz$Y5S^F5!F!&mL8?+J zb#3yOR@;7`r8R?cNEL1OuG^&7_S?_wU|9pH?oFO)~p+tcM|_i&-ukdabGg zfg^|&Q#8NdDx*j9yU$5??vxZp4e@=tJu~%ozgS&94yq&g#5e)IzO+|K?Ja_yIEl!J zlrvbS#`~%q4OCkxvIcT=y;ul2b@CVR!$9SEgqtwqgmwouHvC%6NT3QrPEOkuhK`{c zlI;j%po7F=fxDAVdyZpgWETe7xZihEJNTpb(jU}%wy)?!8*X~6jY$unK;?Hd#Unkiq%qDw3y;N&bv@kFugyU$Jy)BFB(7~h0jPLV-@Sh> z!I@BWOaj(UN zRe|gC!OV}xRn;u%uy-MG>5w-OEmhIPJ;CG2x@2N>EER@63;Ps|HalI>5gQ~<;RX$) zc&iOQy&vx~McV-~O=&T#{QXAj9?zzCDX$i1tIo;k-AJ$#)l>An$_fh``7ck0#$o$& zH8=Cu7`;{@-Rmb3$olXdifFtku16g$#&pQvDw@-c_VQ}6Aj~kV=b_~R)>Vl82=%#82;pNDC^hmemn}1yJLx<`v_oHZOOz1?aj~#G1=`%{{0@o3zOs~A>F)ai(RCjx{C_d< zHo|?i!m@Hy_>BaGfZ+v&5yCoZ?Rbp_%{atm@;T8tqe{R3GYTIvR5%?DM)DHQr$DAg zg!x;%YT3a1`Karx3FrDiRc5?XoNx|1PTkGeiNAu}LJSl7&bR7RG!qJ&r+QZrON(*OVk zQbCRyeZEcjyFtaoRt3?NJ7)|JuEIJGV_GenD2aW7D~wOE=Q_s~NjhVVPV5xZ{6F_( zAp~s`Cg-ySouOkMH|})%0oRtyFZqG#_!>-RzpvMcH~MT$5`u~~LdyZeVsIMvTtMXk z27i)Zb61P)p9~-$^as_=Qpb8R#ApG}e@(#1PDN>+wkeiANszj+8GeC4>0|uJSQtkwHaSXESfQ0K zrR_K|7!^5#9Tj|@o)a~W(CRb;1xlXF+>i6&$4z6N@Ahmat83K0g%bOW_sxLx7%!Sk zR_qz|sD6U0t&Z~3ns~Pg)bvdi>^FD_o-PDbX!Z5b10Le33syhF^757qUG-7J4s_*^ zdbWf)sb0Q2U)=M6<5t;2Y}?En#XhR*I@G?Q4M|HJC_P6ZTMKx0<`uhY5HU!g3tn#4 zr1NY_y5d=0nACu~W2?hviEe-O?Q5poZkHr1PUpB=WwQGb7p>9uYl{ZAwaIiJ*B~ti z?^|X}Ri7jeJ#-v33K!R9JEXJrT)W9ePWs8^QJSi}P6*L=ckUPhukYiA+FjCJ4&kbouN&|VyE=QT_v zWe`)NAbvIXWBb&(ibihijzKO4y5cI-kzDxci;YtV5^fD{twdYv}9_l97-wLFsrJ*v(KKiOc4 zVlwp=8hGxHRGQdh6=K%)3=g~iI3?8kgg5oO72%kpdtU-QBHpOi75~lpw9SL1 z=d#U0c-Bv>0DBB@NfG?@TaRt{4aPE+0 zx3UqxpFYyH>!&P)z9^*7f~x6s1wj#$|Jvc7z;F1vnjP`*&@Z#^|V)Shn)T{eADd+uwaHZ5=t7%;#T0{Q4lwxvpO~fpJfc*S+lN2c%rQCZ z9F@R9=a;kQVvWE0I5pI%^kAeAqN7ec2<8Z2rc>40nbZ6kx73v3^qJt#3PoLygV3tatxVf&pXmm)0PZbpiDU&}5mn z!)*F@LK4l#bm$L(?>o`;%$c%wk(-|7d6wmdUT7jSD#Mgs;h`fZ;d`nkes82m(M zlCwRN7zyeANz9+%bKl+r`Wn~8>h(36{}_o0jeKIxhb>G_+@tdJeR?R%p=w#~s>8_a ztP05-^9BW#?;*<1s%m#ZUTZ~Rel`U$QQYzQ>|W+pyE+#H|D)#Xzluy%@-n zcKUYVx+7bDM3_ek#Qf(faM7Un&Dz+weZ(NJ-E(Hkbip36LCqjelS#2gsg9o`FyX8F z(2w${VdxA6&h8FG($YfuNfFg6s{uR6LzGmI9c*m^oPc5QxG-XE`RQBid{&=BDdIOY zfwRI?QRxli1d?+2CqC0SbqthQkxQl-lN_V3ED_w0vN21SZO#}{Pvf$>L<2rif0}u8 z0(K1}-?UXSru0_3&xTrt5^&gy8l%>(mVxr24#nuj{%}r1))uwh_~NDK1U5rZLSA`@ z1@1mDlNfCn(DOBAQ{1*${;rJTF$EKj$^4x+sG*6;N*&JMH>gfO6+-jI)+;w(WX?;y zWX&R;61~L&;8 zaC2b)e`)~+h#ZT)eh=fR4u60O9sm3^3SCUs(EWsDxoUQ$tE2uIy~?#8I9a~EGMNyZ z82U?r|5&wK?$AxwYaxQ!2`yX{GgI?}Q_N0dRVyVv^*D19sf_|UF>~P>;optSDhz#m$;8~0tl_kph@!$;~!^GQri=Wp>BtUtTz$0jbxgKGWf;_idQhlH$1Att#%JGX zwP8lim`g>z?qL|083tymBe=zn!2x_@<-gFhzvYn^Sy`qirBt z*YRA{@N!2<*d8YFmdcXwELut0EkXW{Pd?oJ5q5Ztqy_j$g5?0;-I-Ur{= z8a;aTteSInbyauG`zk=NEJw{qW(w|olL3L2REP`;@EiKvG}TW$^kLYl;E(@&0_tuf zjzYA~EK_DVsJf~=flq&^Y9O8D^clj%-;J-Qyj91}96CRYL5B*bP^Xd`dbrB?0*EH7P~n&I0E z%l$mOj-KMiHwW(Q7O0n`$}}H=VoV(lTqf~*dgOJGYH!|2Tf?{MGU;z&aO+uP2j1f< zW8`>;6hcLcOF9p9XTQZvfD*DG;C-9SFke3uoZFVcxgC}@x@)$RV`s{S;3o0JY-7V= zUO0B<&$(DjtelY94}0kIFnAkXo;$!yBP)Q*ll3}~2_^_XcPrN;@%K#1AbdEZSLpRw z@KMF6raoKX=M~dQ73w&s4W`}5ek^g`)==Y(NqJLu4k9Rv$vo3=cQM8Yybl?e1?mY6 zvWZTn)HL8;h!mpi!9)LT0vm>3Fu|LL2vCGEyG2mgH{9scm{aXin)IDA^+YFPH!(j% zH?hm8_h$HkrpvnarZ9?>MU+kk2XA3pm-u+j;Ulr6j$qcmS3r|=7lNUfzT1zaP9;s? zJk7ERXk~im5O|q9Bb5{~E%hv&VY}mQmiCgA;4uB|~xiXdJvUN4U(JLW!8lF7t=nd_LSsbf{Gb3`i5F!>GkZf6X%{tnevHp2jf z89W}DqK@XX<=%pNQx&%pg68jdy*ans>B4i_U4Tr{vcMiL083U?FR`mk-xlmjAvKvmBp zgM@koo5Bd7bV%S7u(4=|x(9nBZS>uO=?^>Q2<`CdTEDQK_P@RSExj6@+t-RlAj5JF z@3y=8euQ{fsDC++$(G!*B(t>rOBtgY+`d$k?BnF85+mv{UX4td=2$PLnK{qmU#2t0 zN|DH=HJmHi^N(ls96MS0vwMUmy69cKN!!h(6z|Fk>LosNkY4F zTT4LdNs;(l(z>Sn%=l|3cz3(*ni;K%o+$iaSa4mTq9`OyIE|e!slW|iieoAf`?;*B1)Q*0+FHwhxokJ-MY2E5iEJYw16wk!l zjX?eT0oNAu?wI&$8m1~C;pc7T-|Qz{__}W%tdNZSlTsrN-X{j^cggTu6(JnO!|HJI zPtzvq;x!LEP*`sNA<6{9#h5O}*;|vktem8TUe@d| zm+d(uq(mhpVC9QxY4%(UvC95BC}vzE_gBX>tuSp_w++unR%3r-Yj}7xc@6o17;miz z*H5Nh619WY61NZYoi&;CpNODfu?!y(T^M~t32nZ}QibMH=+TQ{7rbmfEe(lVDp)qR z<$cn%4&Xgv?wahz*fMk$6~Ppprh|4QP;Z7k=@0Qlls*VM8S0&*82y6akE?0QKar)O z3~y^9P16rIY3RzMp{?PyD#4>o^5x|tH8%eSGCr>2n03tUWmW4A%6es(9%a^|W?u|w zF^eL}0biRz2H20g-eD(Lh{vxh`%StO=CkISMd4ox<1K{GW3~#G-vT?hsPA)>=FNqF z=3lSRU37~go8fnnek9)LY6iQ?edpOQVTbpIYlK+^Q3)udF+L4zf-|c&HvI`*Q(+-h z>zJp~76IKbB7tf83mQpOV8M8C!ba(83@DJDFip$bAaPzW?>w3JwmamP`d>F8Qa7;# z@%xV*b})HSQ~V?x>Bcu7I%8#1&cpK$q9*8{42z#aYr^LVdH4v%KsA%mG2`$3oQW$D z)O#$Dr24I=nQW8SsZB5`2E3pVs}{5E3^HrPeL*bRq*dJ02wTPvW7^e{H5l^!>%pRVv84rcOXS^d6cjpX@T>G+Cv7h{mmFEXs4|=BttKESqFuIqCNsgm8@89zvkH3iCAc{aqPRcmjiA zB-)qA?>1OuM(lSzg%0V#m6D%F``*ZvD?`%SR|hno5=rKzbiQp!nJ#2B zZr0R!5q^xWW}sXU`n{mk_ zE(X86X6fUZt16E3$mEIV(&)VCOl3>AVu7-aStvCEP3re)XD9!D6-Am8w(PPKAW(rRmG>)6r}6N(<1nU5(~Ax=GM=M*qImbL#oKHBCcPoPfnAm6&{&Uv zw;v(Co>b2%~InOB`wR&K@?`7Ao;Wd(b) z%A;(#-Cd>L&uNJCNAu9O*A5y2w2E1LwW4Y1o82@iLqU~oltmfyCG`L@U; zj>&)3)TQMciXN(62TNEg?w!54;}Z*f%@qi_iQYO1SoNqfYaVBzn4iS|lrWqBETis_ zRPYDky-r~kC{vwLeTS^|FO>U780jg{_3si8nn6eb&4ZhKo#g^r6PuAqnl(eQOt_id zNhild43Ekkud3m5{SO_gAN~99>KcE34}hhlIj#(Fi|fbMb{(s8s9E=v=#JratLbQB zJuw}-a|)S$ugu1`TF%+HH{Oz8L8rf@4O3cHIn`RUWxQ%e$Nh5KPgKbB-dV&UarG>v zwzD&T>`1W0kJ83^<)k5&dM<3~+jHL54&z!Nz8bk3x&gW#u#of_%BM`Qk;tKSQv3E1U2lH@GS=dF5JVHHt1*9;n8I($q= zcz*p*&|H7#e5tKkfwZr|Ica9tYCdRPGL}Z#QMaEH9w;-CD}Wp2Zpg!ze~SdAh}Uyr z;;iklG)EauQ=YSt!;&m_{iAj z1aqv9=K`r&TNMiD=QA4}#d45BJ;NkZO9sylb@islhViiAY6*m6gRj zF#SCoZyrhn>WCbTk0J`YK?LN01qvn1otO=pH09j#-gLtJ&R}*j<$+&qyO?G-knG39 zgfNwdos1V;2ZJezSqNIMtWxE>WlV!5O!%Dja5Ky%!eZL{#;Oxzh z^skh%l68uz{B=*#Ah~IQDKdjpHVD&b){m30m{g3yNO6{n+Iis*w()M*J4Bh^tYrqY zh&-bdjftz4xX4f5;i@WpzNTqoEU$m}MaNF=%h6wG4&$#OP)h43Oe}sg6PNsq*78~8 zL%gFYeeGc+QmxpIeh_G0|BO*C5bC7<5h6&6oqQhk2=1j*@aKJi0*Iaf2V~HykPfoh z#`HnJ%oc$npfX$}f=`;kIE3TPG23vzp^Rf1<-yY*FBX4taUbL!+SB$H)wMH(-Cube z^lpv9johp3){=0EqdrEUW1M1PNj|187z`U|Sz7@L#(hFRbGpe>%v@0Ri$ z#+3Z#hVua`{pZ9|su=>Nl<#lr8{M04`=eexrc;5mQ%McHZq#%{6o|Y7nm?F^$zaD6 zePUs^LtX)R$0iEdx#xrBJ+el%vs7_(gP8%!3(=gSfY2dZa)lUG%h+u3jQ=B5 ztUWlz5zwA7b-(ySFFK7h!~Lr`K45md2XAi`5)ATbFU3H@Ww zzY)lT)v=cs-4}EY?+$-exOUn3uAvdJ4hY)XXV1O}?GFAO#t?qNWZ{|knuUTovo|}l zJu=Y|P0N{$=(Dlf*DnFDOzPVG$3H$!oLWuS)q2?Z4;z=Qu6Lj6bb)oMT_V}99-KlT4N{ia+Dec+o!Vcp2u~CwJI_kiP^&Ut2G+tlaoECl|4~%I?Y2+ zbt(NK&I$$l0~RWAG1~H%#r@``W!xC5<3giCSW1hMJoW~aUv<6ba=^ffg2!i*tZK@z z9gz*giU%a2%^j|46@jng6-4HhX+eyn)@Picp$U)i8AQkUaFb0@i=>#a$sNdZTZ_+{>ji0+V{5b=3iWK5|l1K zyd@S$%pVfJHJ_#@K3}t+*#9bm7l^BlW}>zChFh8rt6@2S`MdRF2<~F(v)Fw^vm8iF zi^Htg#V|TH6@6^}LFdE+V z8=QXp+TbS!iwMG>pwgp&eEI%cv}W85OSF>i9Aa#}MF=ay?1{Q zT_kGE8;B)HXUq4<@=dG$hpIr@d9}meu7Sd*x&h^2_?F(4`LOg%_~>NvKo?~1+G`7& z6t7@AR6V1%-p3zD_(-Jc4cE+l8#Nwv)~7Mb9SO?~E%fNH%%SAcBf{X~{PMQy{CtC+ z2`8<0qw%Y|Sd)I16x{+8)W}}$!S4NhV#}UPHa8h%wAJz0D<+p|`=(SQA3&r>UuJ{G zSzILXY9V{AJ&%Lrpf@*A${Ft`v`Nr}r-U!lpliD-)cf7=I>l2H&S8xsX;DR?{JZlH3H+UT41Z9M%~rE3f+;DV(>6#r%idhaw*qJRR4rGhw|343NMS= zLe}v8%JAvsSe!MV`DP5X&ihCWinq6oZAD#09Yw+yPnWKUEUsEb6cakOx<_q#xstwg z{G|tT$lZSt$Xm%-+<&a=VwK^HpoTk28V5HL=Q6uhGx=Qomh8G15UrZ%P@8=YWXYKN zSV}c*y<|hlzn#*2phWhiY@+$7tO9HtO2Mngl~RyZ^Vtw(2I*6%`rEb|tIILy!BFHF z+Y-F3rNYc9BUD`mvJ3`N73!|O^_8R6tH~a zQ_z-;8sIb$Es2{anlKXd*FZ1g%hvT54<80TmMbk!QQDOGj z`kArj7K^F+Rg01653v*L^J7tam&T&}uzKIj{C(R<7n2WpuWLC}0`gy;`S3tJ^IEo8 zB4Wid7Hu!*xfan6`SZwZik@e4VqA*+cyP6O#37`}>b>8*udg>)5Z{^=T3)1BvM8gi zrdigsUx5)kig80dN3g2`cRVO6Xz6buo?l#7FZ#7@s`?5XdVeDn3ick(kPD!DWO$`m zxcr^HmQ{*#SaQon<1DB9onUO!1*o7ELm!UBgiw5)L2mjJ%oq^dFxz*7$`nZMbzXHt z`P-v8V;WL84XOGJyh|h|_J^;M-LH$Xr0<1@%b^llsYFTl} zLlOcL1~(5x8rDYU?}QL&QDRC&{}@6E;tAsYFulAWd7y$S6kgHy^det$Z|UnRG2F4L zQBshM+)_T`5;QEvwNsYw6~IPSmN0rW3^sfGat~uksE4l@ggH*qqYN>;`*}ZDd!v2{ z#;yKT{Za|9FK4i(2uXh{0v)K=1QST1Ry_`d2dOgSXCPFrIHdTEl_2s41V(uXxd87q z<)u!18LjLeO));>5SoXTfS$)r5S&K^F{CSnxu}QKVySNDr3&9bfl#iOpMyZecm5vx znN576#FkOFSv=nUyOU5LcxDJlsTc-i#Zm^+Sq~_@%Xs#4$t{nYhd-;n{eynSVFU4a zilj|nR9g?BzyBdLMZTNb-kl1T2N`R>gy0__RMYK+jB$DOczTV$#E$oId>#N6U_|VS z*ME979M)XTkW2EJRul8`bAsY5CGjoUWa_o`60E8aKQ2ih)%#ep+4zn_;*a_l2JhZ| z8bwANEK*zgC>OzOI{e~J$1oIieV#Y9}(>&01*aZuHC_}$KKsi&lT5z0bdn;zEpiO7#xUPnipyzGmv*~X|d9w5r zv-ES3uZ93><7UNG0XMC_!cniki});dF_7~C?I6VZbn<$|wj)-^`#^^3C&u-ujIPUS z`H!90h-mt20ac9nKx3%$UR(@f+ghJC*lJm)8fX1)Ncs~BCLh`qSgn1vy)aCZ& z_WEN968p8b2Z>Ct-)u@|+K}ZW6ghQvt^3LUsh^GvnvfOKyOQM@IM|(C4r`H`T~b${ z9%>pgMVpit!)Q^vW+^-3+GT>Ig4Ew_1N&-GZf)T3WXf~BxC2Eqt0GUAk8EOi*1hN< z?m}WB2(_5RU&Dk5#(Y(vcH@*EChBeWiGJy|IdhWUSE0c(z+;|~-itKKu-Gp-Q8`j0 ziAue}UTfS+bfRx(RQp|30fFe%Pl{3U;nRr-op(~FM9|oOtoIvwmE%rOK5Ct_^*${9 z1Kt`a?PqG5GPvi{h;8D`Bfp1Wl=@!nPXu{F%N1jq*_&j7j){>Wmj{wh$-x&Il-BIJ zdA*Y0ZmyprwpAdkBT)!~@YL$2OHdF38pk}{X4m&J5jXg_4S9t3P@|a9QT1dpsDvAn zIgU{{Hla@$#fu&yFt+%xGs2GoD=(OE`7}hK77gZ69t2T+aUdFZ1Q}4b@K=UvX@i5% z3I!0UA=gp#$?w^rMf|p(O@B<7rB+UV2ZAX?fJHL!Ya(Ri@KC?R-xRyrZ? zk9EBDEZ@3=A%pHw(6;M!6ihwG!tTIJJqIhk?WcvWDmSCzeYW8gtqN+1-XZp*6_^?c~AY(_*=vnn=_g*q_&&HTtlyS{% zy&?9t_~Ymod+sdNLJ#|+w`h3FnX=G}OGGd?!jVugqvxodKViwzxqPbIMf$jPSJ~~7 zx2YWXYnxqkB(@HBBkn{@0{?3-fN_{Vk0m}|5^i<7cU2_m5sLS{m=2xw77R)35=V*R z`$_k|Fc_s-cj{p*c{2!#lB~hZPUy$(FjY(F=CI4Lp4Uh$8A#z)O`31MF3_;7Ud9iG zY5a;gJs(k31Ef~J`Kly^`j}QJf!VZ8F>`?Lw9gwk;`5vaF_m{9r%C@y!fsw0>&CI) zpZji~(0Hl8z?Zurt_nnXqO(yUE;XLKFW3G$pj}%$acl1btf!dnNhLlx|p+*Z#azHYBeQdN;(#oR7}k8eD3W-yC|`BFXgYQsVmF z{M7I*N-JU;PC&?Ng?yehRI>Q$ra8=KBTZKxyobfVgg7<~(T*5R*@2F1LV!Kb+!Z=NHewgbIAn)q%!TzVk!Fsl+P=*R) z)RPLeaGYhru(^8n>`uX_GOHr+i7-mMbO_$P4pKA*)DvN$h@o;8LC9KJUbXL^xj1LU zg|aRXL0$WttK*N3L}>yf|5^)EyS>!t)`b!9I2fw1bk4lbqt8p@5mBO6AF7uD#c5jb zjJeJCjcDM1S=`9L{-!|pF1++DDQoiM2bauBYVh!`G2h=^1@-VHDeG^lqD zX+ezhuvbFQn&U{dK9A%}wm`|+m?_hgDZBfZ-Zhb}lTSJpx!1|S9v@-$w%F69;Mtz# zA@NwJ46;8}edY$&#yP6>H=E@H$#F2wxLiY+ah!n+PVG5tH9_T(`w9zwo%_p!@aHX>*T+m8$a-0?awPx)l8hHt@EYmO^Q|k4QOcrl7YWFl1 zn-A#FB0_%3-H4=&_KRVJMjiDo5IHeaweS_pmE}&iP4MEX`?{V6`T7BELmOgT#>fOK=>liH? z;tV@|dFZHeoy4D)ZYa zv@e90;WXLeov8X7yEi5883OC&4wiy7ONbnq^7kKw$4t!+5@jaXw0E@Q+o0h|*Zi;jC-}*Tf!2(V%y)rFdhzYBJgPs&K~(=CUpPU0uBqMql4G zF>wEt4xOJRB8I__GkexEoL!}%{$Vkxsut3GI84b%m7JOLa&hp~gM;wFCJ||8*f2^s z|KshgRDQEMq;3AtVb&Kz?R=K540q(gZbx;&++!0%$s5GPG;jXpVC@JtGEw8*Y-%~V zN%5v<4dnT5OTl67n#YoE*1qecT&tnnp&Jk?HwgZu>|N<1EiEhpMdC~RR{v?m_Ldyi z|8``NMK{m#amiFSdL{UWlsAH2mXHWS7VlbDB6{mmo5EzOl`2qXSPN`;g zKdMc7p`BQ>0H^0hdqv2@rtNjoD$f=6wn|1(-MuIw6_sq8IMgm4AyE26`gRBzQzh)L!s z-3Bz7V;-+!%njI!h_gh<>sMS#qjlI0%S}6kpJkzG#BsuDrpZ}ZAI};ZG+9(GQ*xhj z{8n#&{grC;(W@seFnK%Ufj&bZ`j9!K`aT1wl^siWS?1O*8dF8bqpzyBQOU*DJ&6j`AnyV7*32+r@oz8MH3#^)0{#HS;upRL-sbmK`oxH*B3dpj%-cXmLnaX@Xssr)}rq}P@w%zdmQ@a zyCo;*_^j2ax2z3ucg>BQS3dfs57pXRCa^n=Zvf#GY->VduZ!sF?^yS+3>l3ek33+7ebNCuDx zIU*m=qfDv?vMS3XN4O{RemwwhNW+|fB*suc}m z*0LVch+qmfZ3UVX9&{?UO8Bjo25l;|@dS&gq zU=!kmU+y@J2}LJurbJ`minC;i%r{e{ru|lQ%MJ9PQ04*AY;vVJEpn5V6V-RTCCFg7 zaFR@%OBnU6C=e`8Z!w0x4+0z_0$HQ&{4QGO?^E}^bm~|e|GXns;Ir=tDco;gA35wP zr7n!jEhRJQ_xqNdlfsSkWYry3&za1Q{l|S>#2W^zZIIT1a{i$tHr@R+ZT#gNbzkhO zcQSr-HJs29*nh&2?D`$AeDmYKBI4^bEHn-Bm~`!9x8bnr&oRuf(aytMR)N1sEbIH- z2XI;TM{A~ERN9(cWHPIM^ez#OU~oV;aO-tWcp(wtlS{0aC zPzZ?qt(llVv_9weyPuSF8Uip2B+>~<5qWT~xHca2!`kgCtAgOQRb>nabJW7bCLMkn zVgr-%6bY7Rnt^&e%l+DLeQEJ)dG|*ZAifoYgySRCe0b0nAswy*{l;Fwz(5FS?Zm-lYt+4z1ZVT{p`e(S{j;sDv>MgH58Vz#T$2@A0K+WL-|ab7N^EyfZp2Q*jR zN*FoDC)t6WRrZPCah(5*;_07QJ--fL_%@l(l(n0$-*NZa?7=;4{cozW%5d} zXC;zE)InDcr(*klZxRu4bL-DuV~H)(R>Fo%+Y>(k;HiA!iM0M#P{Yo;9v`KBaM*GDd6 z;qJpGsOpfmKlM98Z4#wp7o+kKI-7oD{EKNRvp*yGuN_Y{vW=4@Lk09lYU>7zIZeN_ z@ve(tMiZGaUTJsv8g-%PD@Tj?v}jlZaNJVj5S_Lvl%lc6Psh@Y(_w9=ljZzxj%>u8 zwAw;Y7p!Ja%a;>vG{1h3lY?8K^3C`T&RcYNE1W1zFj2M(Qf<<*Wc*y~J}tnqfnVh@ z?LxMhvc;kD%0^(Bv5&B2PL?gpC|Az1YSjzuG~c97c|Dk>*{0lQO z$`>g{Z+W{xnQj3n&w4%P`Mt^R$4g|QG+UbfE48o&d zCcC^(ya?~#mYa4ua!F<{z5&ER*);6?aE&78)KHM*r-6;RgocD3x)m}>*%am49}&Fw zOTL!S;(~}pxMU3D{rL@sgzh$SH`c>e-r3}uP3})90i=N@b%xPCgK+0}!`i)ju`fOk zOG%R2e{zDCWMH2yu4xovGh9xpUw(S}K z6AhvvTZ2&zpszN{e$scQx&kbvy??1z|H8%qZrUpy3*e@m|HIgWYWOdhn0ArX{{j;O z+U*YA>R$K?=lX|g^tNBzJM_qS@Ks))L)-euxDtAQ z^v4izNn8@kRboeA%k;wg7lAo(VHsLRx)YY}CtQE=+n%+iUzoU>ny!z#o79YqdN!na zPPFW`{A$|M_BhCQ0(8O)Tg?Gg#Zb=yR^9frae+%&58&WI%UL_e#18^Ce{{hs#b!Xy zWo!7es!xvT_KOi8EuZV3KKzE_QeydElTHlB9>E`;C&e(P!dcs;UV%|R`H#D&awTUL zs|*i-a<8HAs==)~NkkmSY6MVA%pF%Do^g*}$SH3Dt~aCsg(hy*Z;7rFp%BM-{= z6=orLw-K;w31@p>p&Y77U?TXq5rDC8D*Q+YFy^N12S_CWXc)%z$x7m6A+UJs27lNe z0Kyh~*){Y}halq(g%}s#%*@RXASSg8g^Y*$W?Q(eRj^fvXaw&&nMl;#JZ?`(8}^-4 z2K9Y}imaA%1N9SROzQ&ulrXIEN-N|*B2G7*N7nZq~?tO z80c&Q2xxPIY?i8xXDf7tUmL1~qy;pT_=5utvu5pgV6iTqD-mfiBMeG41>`4K1(>S! z9pUP39X@ciK&v}{A|;K__UB2UaCz8x_yf?*W&oDyn13KCUdM%~#?7ju!r@FI#u~t_ zFv%tqaC^T7OxbJyzC5O;6A{=qR`Le~p@c_kCt+4IhEALxBhjaAGW^___B+KK+{u6DudW~*MVo<`!KYbBMr4`HED}3t)hI4U= zk9%&JKfA+ zEN}GB@Hk{}jM^{rjvCzo=vG6H4IecSNSzc@Z$%2MWfnrbNCUhq6Av}^{COa(>hw4# z;BYLnj+Gfgai4;+woxs~`Ti_R_vSk0U%NhP_4E)Ax_bF{6O2nnyg-N#E=E{ydx?z3 zfA419b~$6+q%9^@E&)^*X64L9_0Uc&$vtC5PlZ+FUx0fWxu#s=(H){XFb*<)y8b|1 zciBQWpoXssRi2xQ_lPTJfwN_zT(a)c;F;V_!VE34M?D09re=fDlum%WJ<-xP*EMI8 zLio(C3E$BqHj@Q-Qv@qrFDRNj)LV#pl|Py1FH{3y?V!g;yW z>V6sVkpVe42uoY4Yw?*-8BXSwHU#^G$C%g@Vi zY{O_u7)ae>>ox%9TaFy}Ql8GJ7M-P`=fXvnrIygxbGem_`Zd#>iTJyFxOB&pCJd1L zJc|{{vR)sC7oXub__|v=a+bxe%9GyRx|4c}+q=i;Q^LhP05TUc;FDt$&GgZ_mx9~A zny*IOv-%mU9ZFGC-C3sxZowZd9T<)y_9G;2aWC$m)UunQ^T zomKIQZ*67|R|oyh@c1uyySZ)EC^?{eI|X(87O!ZX9PaP(4{IGp_OX%SM*L;!h@H`f z=$ji4z$$sEU#|bJ6+7ONukQbsomN109)6J?aJtV-h(!DI0@g>Gcw^;HgS;Gz+A0l3 z+)wtU7t{ga)()-HoD41}4QY}YS&vAP?uT&)sZRJWfAS%OPj@73QkYLv*3D zt-IY85{3eUbpToiJGXAMXRicq3)?*(N;p1;{!V#}%zHq(+zNn_G zCLV6o_||mUWVR8|dtz+(5WrP8#hR~7UuSb{tG+cZ%7BBhEk6BxWdpzO4lq-sw2RJL z8_8E}R1R`9HlL#$wj2FdGnv4{PxqH&np7KQxt4~}b*nd{NY(YYVKz(8sHGWo2(3UW zMPsZ#hYDm^wJ^_l`-pbd`~H(ny|hFFdavY~n);=IfXNUbqpYX0CXApVPuvEIsl2g# z4*6Sf80&gqziTWyL>Q=!72q)pk*kFm3H67H2GTdHUKsCk1cg@1JoqOt-cqSi58k`~8b0N0!Sk_MbZCam8)d!Om5dw!Ssb zX}?U@bTjw0Fa@RRwGMUNl;D`E@YiIQ>GVr*h8U;#saG7(Jnu_PyE?T>%wpYyd-BWU#=H zJ|RjuIX{#IEJnSVMY*{iT{Te&zV59dnduDUC~4QW&73u8;c{7vYj1$s1;R3sjv zNW1y9#4NfPy?vQK{Ud#eFDkznH?K%o{arx&2CZKW)zQ@P`P3ZNS~&mHr?=AU ze?lD$^wF@+us%>{Sf2-TI~!S}La!hD6l$$BH*nLQ>8M|i{$NUDnn)cxA1n}`{IKxz z(dgteekINKvAHjv7?Qp}1TLgJ?tX`~4eq|3LGKUpEo>Q3crWtTUtr5O%E&+$p)Qoe zTWv>l0VTm5rFw+wyDnJ8vnv%X_;|7ek>`gDmE`bth(8sa-`{Qxn5V^C$_3(bqhjdu z^mI?Ozw!+$Q-sx6L}Z-JR~kxigXQ@rn$#%S!Vfe`w1f9OD%B%F7fxT(ff0QV%D11`UJNFH+K3{u`_T=2bjFP5a6@%==Z z$#gx1_h9f-vte?HX|W`i4j9v2(?7a1t5<^yN&kmbY~w~9yZxs4WrH>BYv_ktv)g|_ zB0qL%Mc0+_A1X`=1!+tiBbF?Fe}($Uz4f{}2&K<>?>A4=P?2YZ%(uthrl2$qD~CJz zPqH_m0&`qh1Pz|&VmGS`>Lo!REzW(8>2HuJVCJ6r#|XuO8XnH}g%6w73Zn#LskuV0 zON-rP5U&=@@|JP6U>Rjyk*?mn+UdXc0=z*1k7lYop85W5kw!+xuL!^pT5KhW(Yayq zs=heJ90Ss)qkTLsQdpV?z3#Jv4Ag+nSGjKv&=>xLY9eP?9jm5~)znJ8R zF9P_k0)QyS%CaGv1H1YH3u)W~c%oNN4;<3dXq~2#t75;{%~OS^+;W7Rf?JWZQRixA zbJJLe@&v3UP5X#KMNk0{U47`A2$`6fp2q5cR3|t$nY#G`E#d%wo>@OnE72|mmg_p92IHBsp1Mdxt+S^4cI z;Oq-8aIHIVti+U*SfcS}EQzg$gmnp6ij29p2tWPCs5P6);fC7a`x+}mYVvCPa^HNV zCFs>QUQbc_pyR9Udyx5MbI^ZmN4#*%4~<8=K=8czjtHCO zfqo&vOQ#Ze0Dt_iL;ekzs4?7Co-!i%EvJI*%Pmi@r};z#)aL-lt|6XL6*nxxG+7(W z!u=n$zI0+w_LYMXbX(Squ@wc6*uPkQf@1>4JZLhn6OTk`kuI&oP)$Z9B$@;;iwhrz z3iMJUaJd^}2v-r%Mkn+l*lJcNfNz^vHJ^no#Q>#q?BxHSV{vLFsO-9dY9IVx`}q;I#)TcY^m)P- z)a0Qm;HrPEmze(3L`A`w4_VlQ?LY{Pb^A2~K8qytq{F=PBasn_9DcQ}iwW3d{H{MQ zzpDF3+a0!I#2)J~Y)(-@cQZ=M~gTmL7yaBKLYyDdG-+rDacoSNpy@N7de|4$@6BO4wN5HoC zM=2m^6u}z&$%a76jQjzaUB7$v@;{#cKYMvJ_0?ZLXtN}YUv0A@0yc4b-&@iac+8)J|EIK%t3#s7ql-S*XAK!l0nUv0Nj&}4ah#q5Ekk5&Pp*Z~6Q5&xQD z|2Xykm|=>$|4$?jJG||bS=@ZNj!daqMFB9^<9%eF1#)st8#*Tt7Z>+UsJN`HmZql4 zMDu;4N$db1-PmcTrqiVP3T(sx*bTBKyWib$8*n!|xxw|PX$Lij=h+D8z^Envh*sX-ddBQU$TC}lYhVUzv%_vKkot-|XRQZGjv zZHo;?xwzOFi6nL~Hl~e_k9Qx&5AOp;PV3HE!R$ET27VhNM@-5N7zncJtZmAU6QL4} zy|j_;($djMhq9HElcP(52CatSGMWMJRd!KHSP0TRieXitLuo07gE(wp;8C;My&s7- zg7s+g^YiUZ*MUI_uNZcj9!T4pA-PN?BqJ)|r)*ySZ3%GHj{=x~H)i)!fqB3j6vCCZ zAiFz16xx@v#LptMn6l7%OLpUES!lv{4T`_2k#aonWD`6RKT{;Xj(s-&zi}>ax+ZQT za)}FOy|cZm5|(3fb=B&nKkC~%;4XZR1S!EbK^eMiDk$4VC=j}^fzhUI)2y=+ee&n5GT7sEth=}2nbl8`^xI@pl)#-T{myR` z2w_5U>y=Hsh$rxKo~-zQb?&`wXeY6daxQz#$H^8HEokU@ zb|~8t(;z|9+S}yUq0lk;0Oy^bERfR)#l2TP@glp}h$W^-U8!KsRJd~vXyk2n@;BZ& zk20Mm3&&|BFCEs=KmtGS1>ICS0)Lusx|p2?LI*Uw;8OH;STxEp9n#!NcgG2s7P>SE zXg+q81Rf!Q%FsFA@2oT3aX_Y-?!XJTJp#*8M^>P{t5JUSS&y%ewa2KcoA12I59Tp#Uy8W`zdu?SazzGsEn$H88sHU{UR{14{d zGODdF`W8(H1oxuBiWYY(?yjZ9Dee&5B|w2vT#9SamSTmX!5xDCN^vRf?gei8fA4;H z7q2|-z6^Jis_-Ag8R_d8g0iYDfz6GUx`)4zr);2MJ44ll1KUwxNF`u^_$(D zfa*ee?Z3}*$WU>FAAvN{9v;<&fm*e$S^k)Uq_|-Ae=%kq2QO~q{yt#Atrxd++D(KaamUwclusK}L#H24RP8 zV>vmH`?&CeeQ|ti40!{DEo@0;?EpD?4om%3ahdU>xR&80^u&mb5@qM=oc71OOvUYA1}3l;q%GHOka}r4axAM-u-s@ zXDKex@5IuT{(Y+A?#VJQV`Qu&#elyYDd9op=+-0nWP*1Qsyt>lTn5vJ%naZgC z%eT>{V$1#i{%slNnp|OKo_hZ_ij5Yib{*3Zb<8XAyp<4SPlZjO72lgoJ))YYsDQ(f^k z-Q3zAv_}66se>Yp+~>x~rJV-`HF%@ZAs-IQJn3hw=s)l^9xV;g1|T1f9VRo&wQceb zrAVSKz+^L!4;Lv3YIrvNm!zwce$49Z{=4mxqVVwq3z>@#uvnd_*}`7@gIDGMpTP6~ zhgYos(19kw|3I$)BRY8Te;WpyHQkZ^9$E2fdZEoA?%DJ|(?H06NaR?>-!VDbX}RYc zQO-LH4Vuxsoj{z6yik@_j<-Car2Wd0Y=1h(lrZzvbd7XHOV}7}7j_zxo)g%`92EY> zIlO<`KW}|xclevku$TS*C`tcBOSBZ*b7H5B>MT~W^aY1 z^AFd%{l}R!d%hg4*We>MRr^T2w2kid^3crJ*(3AvaO6R<(3s|#F)<2A$0_4!;vDfF zV1+U%&hV*Q7rTo%#K_q5eBE)MXQ~;!18Gq$#hkKp}pf@KN3kN95 z=*yyAc;*xAuN?|5(El@i8a>)At5@6_Gr>&F--@4XHIzdM6ZTzIx>k;hitUIA7GZ5VhvefC>b#qW%?GB2Gcfm+iy zqn`cEOaMoe>A6-u@p>m4;BEsTNdbuP>rlEI1PI*ZTY}Y?#`JV&mD@)k6?Xkb`(sk= zYKyYdhF5Vfz3MY->U^>mLGJdA!ehC9ycWf#14VymbIN^SkucS9((fuv#K7bYCg zSEn{F6FxPeA5UtL&524+2#-o|{Jtu5%?tV5aQln+T=A%<#Ll0g!F84Z0++bQA3I{iaDv(Q48M#}##}0M} zx0BBTwffXomEVf@zk$JSPBW%V=*ONWq~m6y&Bf z(LbOpDcmYa=OxD;q-T*&dgEp~-K{nFOu)uNc+>-C=YxNrQ(>Z7u9%Sg{53m6iXD&1 zTQC%Bc>S?3T*xV7_cEMzP4vjCx>@MsqV>;k;k=1|unG&gpCU`yTSsY1FXjJeMyu8T zv)&KAGY;2KXg@AYU$&GG>}G?29|fZ;2#F!i4tOf%ev`YuOt#j{reKyBh`Kq;EjZYqS=oKwBir$5TXI*2?w3!Mfo6h3-5KaPr*5in~bxu_1RTQ#m_tRIG3X`~B?l!;A$+j31myC_!~4 zX`jAm=o!%zZ69%!d@<#+yJ08)-qFRpg7v}f2z#J`1W?MqP-8=m`9WpkH8s3n0ox6b z!{Y@0F368N+c$*WnPtd!(vmGi-JI4)bB=q&<0a|R+bL}9TW2vd!tOtgCGDBTS!)b% z&;bXam?3;30dfZ?CYnlCr^E9?2T)*5KfqdBsy4g_=a1xoJ+9NItkMIy_nd|qWt^0< zw48HEW!y#uw%pXpxZV;}?|yth(^(jL5Wy4M;q`nf;00or@7b;VP>-#@5myVyC0+N`X^tQ0PliO7EzFn1Wm63qCm~YiOdNf{VlOK@ahey7o!Cj^gKeM{mcX;Je~Ts2TOAjD8R;+qKz#g3>(~y? zM~ktLQ6djzMHkTc3Ko_7U|BnxLm>!gpmpKP@L9ITb7||QU#NY?*E3lUr+^9Yy!p!j zz?E}CXH^;GTm*tWcsb!~;1DwP_f~%Z`J-503cz!~hCk?K7+w1d^w|?Q%jMY7lYyQ> zMZP=Eh%VP)qn|$#LEE>7G-sk6mg(+7VY%T=CWu#6{8^|A%^Cx_{Lm+MRwUYHhuabD zRRkscoPXVA8zJid9hV*Yc?TLX2;a+U>g&ax1cQ{Skz&nZ>72%jKY#xGx5u-wvH7o3 zpL>6`Q|e#Sylvt+_Zxl==B$94#kbYMNh-fc1n2N5tX3E z^rRXQS^Ee0Ta2&S3oi_8hY`>Cb>h-V&Q2iwN>QrfhXq_ zlqMe!9W&!b;JJG9_Gvt>xnLM`eDR;w8jTOFxj!E6;?dHH3w^~ERE7z+J_ zjz&6AX%ozd26~@j4o=6lW^{>HjXOtr;^zkVT?#uL>P&A2h^pL|xMFUMDbpf;LzUQfoy ziy4>Qy!o8T?6>VyH>!A-x(KTe_IW7as+V3Xm_TkcC=ji*$vX!1HC1h&qYd9I!eS_I zj2>wHwCZEmezIh@G`P`#SlR${e6`-jwOe@sr(hTa+5jAC;OAJur`_maXBcZw;(S%( zJGA=VvG2NX-lX1rt*$PUxLHcQMvgm&3!Fb1v7G__x-wBsU*0euQ0x}2H1NS#nNZ_;k*F`ZLVW%YnJ zWGZwOi`I~w**DqqRhqbW*&j+maiandytTeX<;sd8j4bkz{AtFK>`ZT_h4 z!zaoEqlkDOSgasF`9pP-@2Y6->&_jw&+kq(9}3$);`yanBKAo-LpTJhEKUd5l~hV% zC?8za`a3fZQKhCT-!Ks{|0}Qs>8Rt{TUB;OUSz){`U8$2o8fH>k{&#{r*RQ>F6u=U zJbhu;1%|Da0<~*r9XvT$Gd*W8$=~78CB$gMb`p17q_x_c*dMPRbsA3FWSBi{K3uG< zw_8f*)?>WxoPmj;LGy&-jI>#l=?E{PnE1JtI?Ao2iP6a_EYp1#wM@M0`JbZ8itv3CXwVCySY}`cIWLe{1r{{v%cI2S_fL` zh0v!9S|s@(5izH7N|9qo>^qOF4@?dvGvKzhA}b=>aQwY((uJe%RaLHqd--Rfw8&&T z(-FBH0(A9Wt;^ZWdU5x}>A1P;aX8s`Z0&q@{i>J}>WPvnvIx_V`cKE%UqUMO^iM8+0w_!1id-|f=s|bt5}kG7Ft?Z&Vf^fp zDg1>#t9$_lxLaQxVTAIg-{wc&r%opPh0qyrU>cfPGoT?#pjZ(}(R3+UL`yO;V1v zw6;Yjy4z7;!eaV44O2d-%HUn`Q(Nacyl~+;50Q)QR*hq^L+6lZx&Y@83*0~n5GL3; z4PjhWQC1cuoSHK~R^DC9jARwLFNo5C#m9e2oDVTXDaa|9o2*H;?3LwAuLPdB%n8&w zKVf?ZKi%yJ_@7&6ONDj-oy-ph5~v{+PYt*@XSjDbD+btt|oy=+q(Z{ob?CIL2A8Fq0<8NbEZ zHM*5}`(IP;$o&mpjM7*-%_`J{u@Zci^-?k-_EieFqK-^v#*rQjOMYdHU?%3xF%QD4 z&WgyC&zQ}QnB+x5dBW==z7^CYl6)KO!L_Q=)y5YO{W;8;d1iU+VaTi_dupqVT87gbuIH$g*3qC(+8NjyuAmD1 zO*IjyQUyx;!}H;6jWcxzq_XMht2vS@QHJH!{@YrS8ltQ#}7^Wtu zRQ53gtt^PK-J1w8uGy_*i<-w^en{IU zx>0;+61Qx7y?_V0&QBC)nP{+B%l$QL5>gJTp~8l{Oci%cz{5n_4yxPY7K;JGFr-4b zWajm5O0LR8$CKAdW%~NyJM6df$1dJlAHO zK0Tb|P~)Tinn#|+yzr}!hvM_mzIlh_X1I`*w&pW~!b(frMAO+{)}OOmaJLb$eYMP9pnHLVx<#L-vhwf7pGgqQVOL{4Qlnq zLmV$Y-a(}K~R z%7f3l5`H(?01BS#F++A=tStX1jScm(G{)J+0vUeBZtqD$^Nfg{DLgwZhR4WOf6v~E zuV`y=ixx@j%G9sWUHX_++3u7V>u}IGf4dehlBMEL(2zB~U&&nm+;bAmvby}aI&{5U zAg~*ZIIP*`-N7v;B{d@=)kL=XF^g`zKBI<%IqDWSfZUL zkhKEy>l+NW>3X|~h6%4MDRnqi#Y`4&EJ(gGf^v2UlT7 zxtLO}gd6Q==uTzZ43tir036_$BwNa9dV{l;Do$=FxmoNM+nkOl>F{~S^{wJV*@ZHs zBeMI&HYq1@bBu7M^o#3riEfg#9!`epcys%xSoV;q<9Q@ASt8#3r>`4qg%*lll7dqz z;@#c_SOYorR zq^kM~|BXW>#sv!wFIQ(6MSnaxN9jJ>F|8DS&bQ=Z+mNB*I>I7C^>jV%WM1m7eSrnm z*%p@S&t`wzX3e?E@zK5KgC=jxXxMV*&hIC_-7k-o=z&&wYOBk5$VvpBF5G^*h zkPp$`AB3oCCQMiF`=Z;zU_hKuO(`JZmD#ZEkHe89@2wP^O=pnRsq z1vND2%V1XT8C!G>lC@q3%vI*EbNo{P*1@NvP_g+yE*gXS%@baz?SbAhvj!yqCd@n6 zT}BmhyFT#tM|(`uOjx`1y*Yni3UL*K-DjfDcFgKx6H@aQmq#CCeUyXua*h_U2H0l! zp?RFx_3Ev3R6sq(d!})&KZgs$ECtRXqrp$SeVt4ifG!$o!f~m1he8UKgS`YNDRvg$7o;2HaL%FeB@I2q2ViBG+Zh5-c#)nlIon7nFTk zIq1;Dxx@1?P(s+Il>S0YJqNcD#{DI?ysE)2?%2;gwDmg6>J~cAX%CR5Z}AAm;bRl| zdih-4njBSCpL;>^x)75qA~z%7mV;cqZP}yc07!yv`W=N@e{5Uu?d7K7&5T7`h%Bxk zlipNQ*3<2R>t*G`z?+iR0YJ7quFR(h(u6e8T;j-1&tK55TK?@8gOgHtdoso*-V$S3 z6Zyiq{9{0!nAbB{heQ)}&UKzeR7dVOZP^{}^UF(SWFm%jvXi~`Xb<`J6tQPw+|eJqc}|EM?3AVH;0o~`F8 z!K-H5Rqa=$2k0HqVuwfxF^4ZPC~;x_a^@6_Cip0`TQmrg+V5^k>fTH#^?;c|(GBf# zTDNjasY^l_?pH47Q=eHwyR5Q>oxAWM4s}JFq^Hdz34k%PIS8FBOE|`!ZwKmRaF`2~ zdlIsp`}A;Ck`fZsF@`G-lK$Oe_g}>z9Uf2DU6%D?ZYPv=d*@qLBzqVwSq9=bMXxE1 z9vAU_ox&>D-PHWXv9rI3^r`89f1q{Bq%GSh{Rf2UP#w<}g`uS)jlu9Z2$#ggx3R7J zSeMkF{l8NI4)p+13c+r|@3~NrTji00((yH(gvY(re{JaHH3mm8z{b{w)exfiFUVYf zplf5C?4~r9Mk2C8ze;5~?**K<{#WctR^Y{3fkLtgt&B62N;1}%Dfr35%(niDxd#o= z?>s6I_amyi^LX|UHLJ#~{^28r|GEyRXoTO((uTGZMo$d3*h!s+)AO8eOvECR~%&G(B!!SQ6H2ScS+))@k>{zwrd ze5Isr_{7ikYiZ2V$L{a&$#{`|+40bnt*Gjc7;qK=u<7eK%+*%yW?c{Wx#NPBA$%e2zIK%Rvyx&!Z^>>eNc3o55f*&4FLZNYnfTC@NbMscUdmMQ~xnxJJL25}7Wk&<69<|&YwxC=vHdwa|kyNiUJFD(W@{|bLf zlJFbt29R`N{^-*M6yFAKVH+Nw_fL9LiR_a@YHqAtv319?Y& z#Jrmgj5!+m1?Q42Kv$>68`j!PY}scSxxV<0d#57x?}leaT$P!6vZ7G_8P2;H%W?Fk z^odzc!|nmL@7B?Ky*jPC1A}j-@_l(T@O6vLE;N zjVEg2K)SpS?hKY)w>7}TCqNW9C0$)bI2n%wI2dZDxlavBArV_6EtdbF9AM6~iA za1@YX`nXSwU3>N-Jc#=RO`uFP!fmh^{hxkt*|$&?podV2FI1V{_f<aQ!yKnG7T) z966G+4WOykaVAR#J&)0}N$VT(lE9F6n#ZYprBpscwx!niAM!O*j>F@Gki=l&kSVcGf<|Y5=J`VnSHV7U< z5wHpYzGr5e*^D3@LxrOueI&+=4M4vLB>?a)&-m-veoL>P`gL2Co8|~hC@JrUWU4WU#?_jw&p##3>XjI-hJ}=C(7M~>yCV0T<_*#_9Y!-z6Xgxu3`CxZbbdhz(e*Ct#kfFPUbB1NM<=5MW-$FBcs~M}^F$3%H zO#p3+vD%9|8XRZg6*?lUiP1>I!>s znRu;fOs19b%~G&1IbfWthUG*!hKBYTRq*%Ed@c1ds`Gt|rH9wV(=w6{7Z1rt$-yulI^ovCH;Y`(Oc?CLCci!!R}f_C%~E zFP&|}J1jKYO3Sq?MK)gO4rw~`o7X6nZ;XwddOARE);(oj@({rHr}Kz?Dn~KUnYYyDPv|@Nx^shJ>Vn`IA8(6iw z6Q<`wQMc#;T&@cPP`T0~J#u>0`a}9sDef2VAeGe!K8my(QtH=)S)t4=5$)s1fFHvZQiEUI>OSGRNYmXrjaSh; z=kaEB6}Bqx0$XIT=2mCPCr&pIA^>zH5ttchibq`dWzJOSpW|5O5@@^~z`w&n)=*W6 z5sb?u$U=_k`}6a_X0_^Em+>ZrbS;}%6a*hVQWHiK0Uh5UH7w_be%rW;|LcJO^s%!laDoXFenjLY=osou~`;=!M?Gm+|q4ZNv2HNzs7N^yi`yO1COLEp@-n z48GCmlI=iO!926hOV`1?`smL;vdaTSgShA#ki*E4G`DzU6`Q{b zo0`Ot@EIV_QQf$73`Ki{9Z12e ziFhk#=d~z|PdrN&X&@?;!!`d)LInN@YK7dQj*z5W&mmGmht+jm`VmP27ka#^2^Hju zB2uM2ND+0mCtGN!+3Wcol{G+V;OyqFhNK zVQZPyXYL!NXa5(U|Lah12!D?NYw=(F8gfmI8?!7V6H!*#qtk4RUvdQmwnsOA1-OAA z9&S0PZtNCoJG`zKmeZ*+X0*{N#=UgTSE{~*?i`2Y1*OH5H&j$UmvK5nS5-z`eQ5T$ zU{7{G^t^fDgxBlM8tEW-_uNJf`~1(!@n_iE?3+ z90CL*|E+?wC2{=QWk-;z}Vsl1!VG74RoFaIYkI zb*GTVWmz_U7K{RQ6;ZN%dO8W0n#^>Ovb#~{ns~Xm%&v$#R;t$qCKR!1uz03j5P{L3 zTe-iDD^p)hyN2IkVK*B#!0M>#-avmyXLl~X*bG|#QQ?Le{Ha>}+{W>bWO{RT%~$51 z1z~b!DK#0FMGIQA_0p!f9Z11iLZRg{)aq0u!5!qVQ zN%xC&1l0lxPb9y!eQ|A+J@LqRp9_(TwCQT_O&*yD`1pfzcznl~JOI1BSkGl~5Jmmj z#$dTb@vGo)1X3f>dxnll4EI+lGK?LMAA+1!aYVHbPJWnC8;!w0hikTa{)4E#X} zkyutJ0CA5^W&Wm@_%K!20G-SmBz0_4C8e-=?pwbh}f%K9xEo*G4_r=EPlI z;@YEzt^7~eA>J@-WJ$N32K}wMIhbd#TmI9%YnQVuCZu4TtJQsecUavNEjj`uhK?OO z-()}>o_b0llwh>SGyAvqBs=}>ss~-FQSW5`EuD_@FWEO_0OJ>~sq9WMl|}ZfO%^AC zx8ET`B)QVB|BIMd!d()&yAHbU|IO)$q@;X4K8J}uAV-~ki-t;@h;}pbnUJqAfsQA$Y*nT}OcW}ZGRiEb+ z9F-GFK9}2ZVolkxLbL*K*Tu$gnw`^DY!9q1^Tf+_7E-ZX5*PjA7$oJ7?n?1QhJxAcT zy-=k9w!IbRlk`5E{8o+Wqo!3?lRbdAximwQF+G_ql*dFHee!Am0ws?ik8-0wWiIFz zQJe?x|C`Ly&;i(VJ}jRgR&Qy*l0>f<6lA_&ui@g5lQS&g1JKv%ki(6B(iceZT8NZO znTF8<@?;{wS=Q?!@3%V5|6`Cqkp{#CrJ=h4!J05U6jQo2w0n#uWxz-NagpA%lg_Tm zM`ZjHwy{AX`=KwP=2?t8l;mZeh88s}}nNgLUc!L4_A7esvh zq6qnx9m&6^a{SzG*SL=|6xTNB95R300n67Wt#dz>9H~Q@@6TiOe(d@?8jT*$eE$9b zdJyTB=^D8*D)3#PfV+RcG!ShYx;C3Ir6SMeFD!So-BT3_2OQtvhyp}}j!(zG8IE_W z%()P)*7Z{ux2O5yB5g2wG`3rJQvi){DSC669k?=^>2@;q8D4ieI-h^=ZZPmG(??V! z?;@F+MVkVYnI&wZ0jX+2FuF#yF;;s;bzBPjuHnXeak6b%u>+NX8@4&xuIy;=Usr{L z69F(ynqCJn4#g4MJoQx&FSMHCDdK7>tOne0*3xyl6}R@-^x?k-@84%3PFlgUV-{qY#rerwe~t?Dy}8r+{M# zumAGj+)I9MmmpBJ@i?B{TWGERcw{XsxA6B6&^jnQDi zp|k&*KP;Lxb$t}ww{s;E%#d2v*Ua6%GZQ`TNNM8T{JhbjKlTvyz5$Kag{Y;Y)*K6V zx2F~ANFHZR_{e9m(x;)>g+nd~gkmtGkYbplb&Y}Pgff#nH2}`$4+O~HU<8gq+Ki^y zm^#E_c#WQ=@GKmgi${j@umj^Dyzylwrbi8s;17*B`*2xz1<*v*@D|sWOUK^>eDH4y z4u)oqUk!81!J(|}1LyBTJgQ%2MlbsSUc$ry5SZpA(0fCiT=o$?hTv1}-7C;UQ80Pp zI2ih65O4^v6+5hzn-)%o`FVDPQ3wvvI_Dj&U^Y#uaku}M_VPjos}C;yd=?iRHr(eM za}J+(cA2aXM{bfX?EHrsnEdL+xRSPo6W@FNao78M6q}oyQzZa~GJ)ohR3t&mCKWye zgvjS23M)eR0}oWDD@8K*qzXPAqw0VsEH)ZBW#_pPoh6i8Y&^cR>?U>I&g;id_$sU2 zX`L??IVgU^`hdm0O=D+L z>Gul8m$@(;%v0%aMPp#UoKc`!qq^@luIjcTJ&$t+K-kLp?zM%G^BoP?0F+hwa5&&3 zq98-f=i8teTx_hV1qu^Zf@naJaF!S^uB&HX0IeFI)0oXJqd({{=XAHahrr99`9CFe z?oerFk98X}Hsv&po;2n~e1`|zRb4LX;}s6mLbV7b`R$Ld3Ih29#l}SkBpRRMR$Bk~ zV$KkkywShN4&%7}`4j2yQw{Su+M}1LGZM2ltI;Y*)$&(p?~-9nZ~uqls=4hwqjIng z<96pASk-=u9zQRwI{8_orWyNtvF^6nZ;AOp-sUaF z!jpjp;n*uk?QUonoixZmp!Va}kqCk#Dqla%@ebhHnlm|!UUC(wG2(DISBE*8`kIb7 z*N|Aai_SGQ39XCW1ht#=3&$mTm;fU=7QJZ?vYXr85QE?yAQz6~HNlF;+bXG0;c{TG z7O)w4;YntV`i=v#HirMSo*jDu-DQxb9KhTosdb?91U{niM?KNq2`?%4o4)9xB@}uI zc}Qu*9wO~ex(A{plggxcF-WTga+w}3F#N<5XK(-Nv zRHy!uoAV}Ii!YYs^JD*J%RCN01FneNm{XJ!@Ys2C^iJoxdy=8{i5hkfx7_*XI>ov_ zHGTONR6Wpa#z(Bfzv`YN-^GB+Vfc232TF4A=1ir4$nK2~*7$#xCoqH7hhk*UZD@#P z4`Y1qdMOg0(nk>mYRyOe1Gqxq!WY~{l&_M{oqE;O)W1U7C8{G>xmWVITF5fKX&>jY zjmu`If#^c=eTZ-mNXcA?UiV8WkB=c1AsNeCOglo1EtM0@O^}oMznemvK8rEk5RVmL zQW0;$9@%;tkP~qjzv99Ccr(7Z_gRHuuh5v=g3OuaqcY$17)-@>cctwfOerYty#VOc zY<-X$AW+;KW;mwO;)Y)^IA;0Fu2evgm>K$ERs*8@`%*$;WUcCJWV^PXi#H(6^BNMG zBcAe|jj1%(ibRy%w8na)5pmo-7E}W4tn+o4H5wY`h(&2%`Vsf;Pi$kjfOo)-uFX#A zm1yzQEEp*jj+N_Q`8w{J!Y4tLERg~K&dY`b4m1IlbSXzcAaPcxbxFC7)P#MV&yUam zqy$3TB7Nqo=H2dKyh0G2`W%A={O_U4msX3(-r8s}>V?=YCX9DyhxHTLNv8l~z^9Lr z7{aaAUGi}czs&;2 z>We7Ja6ve7Ko|ES+Rl9dw~P2YVF;4o0%5?qr0Al09Pk$bxHLD@|B`7{yN~6SVbuIj z(0Pt0HBQ2q%tUrw^OGo;nfN%(9-Rvyf)LDY>%9}sz3P2*^Hg|t?~Z@L$~dVHFo{DO zhZikFT$_vTj7?Y{H=HAc5m1D z9e10!%q%Ka4TRPq&lcX}pEGrvGe77JQyXD)i%Gp){LZ?%fo$YCmB`oj4lU>#b}m}*d%K1>%iP*!K-C=%9j9eb?eYpzIYLTaVT zaRJ&@8U;eqBQYQ-0*5B}hT?K*v5nOa`?SoUJqmrncRkPaoe@H6uUK|5v3mV)u8yCk z^^Y7&5?BEHi0b^iKlVEsI*#@qgtf%ayCj$}`rgreck#Z$by;9=($$ijiO0SYaDPqJ zwmi;k{o$J*sNOrjzasx(Ze`5(dr=U1m?I=(lD}SaOZ=Pi*DZO}!BEcciKR@OdS-;B zWl@$!b>G9&IXTOJd5gT$?z^^6yT5*~1_JZNXfG>kP{F^t<0^RxK{#k4ZNo%4GT_G^ zPip=JYi5W2>1n{19(g8FpnJfQ_{x}J8wKNtO;--%UiA~}QJV^o4{HATxOoGQ8vuUd zq+|b8a@tmNM}pV$)07W5$H*bE4^yLued5t@i%o^me)Xc-`Hw*J{rJz9Lue`7&qzpe zu1f=xdBMzWXgDogn?R&NREYob0Qet49Z{ED7oA7Z(T`CsRuZ%|P~i(4;(JJ%zg!`S zye(=mb27(Espc2Rt^my|{4{oHY``Kiu#<^6v}D~5T$K)OCgku~gtL-e0V^KnzXML@ z8kzv7NO6D-=80_i%oqBK;y7-RNXZ)_mH!IpXv`-x8NvdYwm@Ah3%F}|H(K(1Kaq4o zhv-+GP=eFz5#ASuxA^GrvmO!(7(dj2{MP1uJFwmfXNAC|B<_vhC3PQn(9U0%@7d4Q zYcP7-Gev!4GSkE=1<_GaWlZBJ-d;GbTjhpwT9p3=KU5H#d`=W$e8C25`lT6e35NEw z@cDt<^iF5LUcAy|sN|Ht}BEfx6_CnzIyJgrPx3p~^% z{{7HZDpe|Xvy{?dRGWq;vSVT9m^)@CiVjZF(J@nnF8RL3rCt&)3Vzi%bQGG6XnC<} zmlwlV;nnQ8xB4~ORqJ=&dP#Q6VSeaTYK`X2aW6;0`O(3(4Sqf5#@A@<^Yl95A+z#T zEx}5Ny6CP|nLZ(?;Zy#Y*SX*sia2 z=Wx>;R2m7sgq9F8IN9$^sX6R!Q)Nbj@WbNeq#2NhEza}bX-~;s>|B58TpD%9BoG;T z>kI4aFd3M;=v;d>&H<_Q+ZIFbuvq=sQkVJUw}WByGgsJ6^xo;i4!Fx1yFg3os@8Nb zsy^`)<)tlKg!4+l+ihIj=z|gf2{{5;Emm^Ie_)PE4hG{Yie+*F&;Vcnp?nJ}1YMe* z4&|z>jX;CQ932L5Q7FU%P;L@~)ELa!P1i_rVu*YQV4&)}kEb8|w#>hN@W)P(1s=(C z0#K#~3lWe^K^`c;foNbw-8sMT6?$;bous_8{3LD*z*_fik~yWc+5U?OtklaxRhTBG zW6*DdVuD<9w0rWT8OJl#VD-6 zejX6jh|57Bwa|_zTEqhe(4d^$v}*l_LKKmU)(TGf!YjpArvnIM%<>l&r?a}qH^mQC zCU+){mBd+b+vddeW4AoD9Y^q*Gay}tDw1d|ezatvW|<$V`3=ylL3+_MWzxNN#E6t5 z$mG0p)MMUvvW`}&@J$TJ4xah^UuU)_;5?0xfY_^E3bMw@a{&g$88&FSc6_4Ag_B8$ zBr}{`Z!g)p>N>m`DR`JwRl ztfP(rlV$o#@`8sb3+FmJN_P*em{6_bVbqc^&Nu%T z@iFS3$P`Dykt-&ddb8z7mT;}<*YW5gcz-Nej;9fTUgiy2A?-AGvk?|K=oSWH9WPx+ zua5A@lEyBKzhl{*%e;vj?Fbv|G306HqNis;sUr_YY@%hDld9c`aDSBFu$Q`zs&^cg zJqLAJRh#48FEd9jf7X677eMYv>(evE0PRVUoABOS0MFtDfor*2=V3*TeFv9MR)@N* zXD6&F2HANKezrqNfAMl*6HtQ1qe+eD$G(MIKx#Wq4ktlUo1kELw+qsxmMP2kY?@Tazj9K!{MhZANb?`m2l{xAl_{Nl`+P3WopV(qd=*)Jj|hZ@G!j(1CJkZUkSB_hd0stXrNvQZe>I4 zD~ndA2FM=M-?^FN%R4uu02+K`mRQzZBc&Ps@?W(c*jM6q+UTpYcmwW1Zz;VWNd&Gc z$6o2KqsrKOPWHc|uTXt*`J&Nt#2ePo9FHtu^)_V>$JB>f9h!Nj$9b}M@OxS<LtzZywFf2$dn)ioIaYvMdu{Q5GKI8oVZs zXAjmjl1`0puzhlg%Y7PwT_rpnmDjzkO3)y;)Mj zAN?2aN>*o--g7bg1o&nNvo7QXnrWOp58U=h@Trzw{?17#DoC6kSh~9j)hcG-4$W4L zZ5Aiid~t}cyl%M=Qt!PN8v42>9+xR+==shK?}$GH<#T2s)pb3SKtScyUX{}L9|m5X z#y7W1&tnraz1+cwubS)Ed-n8Q6KhWr4zr#aeuza~Fq9zy{`uy`Jp1Ub)a+~Hn8OgJ zdx#jt;a%U_(R5|Gj>6mG8K{p_(0;PUUaGHXurdk%LC`F20Flz6%2H?rwPWNc!*m?a z>3)+dC{G|n$mPc!TVn*pM++--9NM<`iur^>2EzUl-ap5%Z{ilxMQd!km_ z;}m5?oSF&#mI--CgoJ$P-){cRRpn&2ojl(Ftmu%!JoD(TbL9l#CH%sObHIy7n92f+ zGd19S*%WiPUU$DXN^_S2@y<2;cYGqz;HJXE%bCKA21p!PretjfK!2z+gRRH@%XujW zGDhfYNk+6W0(hl=*IHEV>{~k@Nv_j4Qx$S2G3fH5#$?CvP2$%Mkh2f`rb=m*(jCkV~Mr0E7ESP9uYnjTvoYfR6@ z7DAgQ_7b@h%HB(ejFZLa>;fGkMmr1%DW0w9`CbE!ahlz`7o+QiZB7Ru3^83fO&B6m z)EmVbjn`yAB}<$YAF3yk_Q3wCfnZ(O)NOR4RpTj36<6w$>yO-fKKs`$6Raj3R^X{_vcWf zPENBkX1*aq(nflA=;F6Kg%`|$Rl=j+!fV7rET`Ue%Z5oHp?mQInH1lR;{5w*81jfZ zRKJHmcWOvwl{J^!iBKoxY_(mtz^JI;cA#fNFmbJ<=-Ls#?aJ&AV|z@AzYpt|Tjjst z6#KbyE>}E0!p0^V?&`WX{Q9+XT@LF%TXR?C>`ZRaT=*=Rsw@ELkJaW)!}IllGZOA= zW64DhL_>tnl#i4m0l4 zb|nKunhzci{}*L%6%}XDw2R{I?h-V(g$eF%fh0(f;F93(4DRj>?(WXu1b2eFyStys z|9$_t+Q%eN-4I(+!2E>5+VXx6e8RH8QH+A3 zhno=XLgPH^yT$Z^?h06@99HQ2(*1fK?emfc&9X&6v@;5V5|NBgI7mX0h1o!*if180 zR-=bvfpHY;DCQod^XlajLB4>|0yc=-JL`B?litJFiseVJ8pAbxj|kNH+!;(ot1pYK zizhdxT9tvOeFR+Lz&K#t-67>>T)$&frQeuh)lIgf+SZ% z3_t3Ngp{9nK++OTmzu!fpX1>6VY4-YmsKF3bVC>H3#+pi>Gk(vR9ltUhBuYl#ulWL zO!&Q7;*x1HS&L8jdXK{f2dVq8WRwzD&@4561bu($$I<8W?#V@6(}(rFO}qEQ zoIU*q^JG%?Jj}10HqZ>wp#(#f83NXCbnQO76(LH-eh9M;Fy~`ct(i$I=Od%QF+37< zK&ARvS*gWKH~UQczF=deVq~>DrD~Iu5`G zr?sAR(U%>~O};q&CyiOrnb!cg3E23}i)u-FUnUZ&)yZcE8_RXgDE8l|#?s1x;p3yA}7X zcNVi8IfbB_4C*&WpMfTYW9_OWJi2XtEdoF^ z3k-tM-1>uw2vi@sV1U>_^*H-H!D?|vOkb`^h8yfti8X#V3@^+h-j7y@`mm%31jx5J zY(R4$B~bbXc^7sPINL|eafk^;akYATJkP$`HuX%g3oA&&h++(U%lm0-S1+c{)sHV` z)a7pA$>AJ#K{{cG7PF3i`iD=5Bcx|MZNBjM3Yi%CAP4;bb%_>oIvR0gRZ~L|Q@vBM zg{p^MQTYglQ*pu+ziyzDA`f;3yTLx{K(b(cO^nxx{{Q&(3+XCyB`MTQ7l?aN>CWuJ zl=pZ+Un1^|%NgVdn}nr9H)hRaF13b- zpbw@3){sOq0r5;w{fsG1i4?BCOGxCU2FjL=c?%W)1!?^9<3#X9*544}Kcgmj-gY5} z8bAmpR}&xx`mfHcjP7^1-9I-H-W4e@T(mHb7f8!>fjEU9+iLY1>Mt5c(5_7lkE&Vm zol=|l*3t8o*iDmAhKeCe@Cz-1H2Y5vA@b%|#rU$baGSXTR<_Q-iE3a&JnN+-P2~9` z!P=M2>gOy@)aF0#3ZJW?U_Y-ptsj;Cw&+hLlMI76IOqE$`)fOO*d}~H$Gk1GA?y}U zxAP8v+mJlun@VSyg9s$Hojn;H7=o^dby3ZV7dC(p91PZCfXQ}mvxB>ju@k)@( zLjsMSH|M#>(%ole-EQ7!LElBfUA|9_Up{%gH7Pt=CgWnOY4#N9J%%VZ-?=%b!Vx)` zN+1uLA-T?B3rqcuoB>+m#Tp7mBCIP zqFK^WFC=-pWKOj?2)=zAO{Y+TDnV!`h7EM}v&D&yOD%Q9nXnn(zVptUR6R`!H9=sQ z?#2;EC?;kTraOYYWkf{0*=DPsjX$#`q)9d5f1f; zy9$fuh{7`2anur@<*Gd7p1Y-~UqqmCINY1zLo)*ndKW+5puPu9XM0Mfz}d#y?*LYK zenOt`*^zmc5c#qNp)i2o>#>0fFd<0Aut-wbxH?dN`X#nmJ2?`{e9ti2@D~xSn_)<) zTZsFsRFq(jN#{XyeGD&PIy55$Q5ugXPmv91U5{*%t^0s}5$0e7wHlM-1@Zor69Z|w z_@6>AgI@mStnr^z_bP%myu&H$i5=h$acoT|JM{CZ_sRMx>?O4hAhRW!l#8cN%|A&& zj3FE!&A}km6sM5o#{dTHTQ$iMivSIp`*SnQMMSAm1d-AFC;W9FPrUv&4Cm+AO+y+{ zgl&7ypVvZ_DB)O&%go9L>6?|5=1Z+&T@S(H}z!?c-BW@FFu4TAgJVVDAtLaihj@zs8lYpvT%9qlYV zN_YS6&&|No@=@MjgYoK|d;PT(@)i4D_?vk+cbj9%ooj0{cIi8#SegKbV!LuMyUff%rBCeFeM~ z0pzbfEDY6z6~3gS%RwVDR0(Pb);_6%ValHRWTd&Hdalx}q#IvVMo8gDHH5e~#&is` zUE-GW4U86*IMo#qPr%5qg;~UfBaIBkwq&OG6l9z~Z1fxU3mJL;sC+)!`Az9Lc9t@R zwv?TseACM~^X0wy6%IE8hYJ0KbeQh}EOU@}ZznQ=c>I96a=q?z!m`xu)DTd)ovMC; z&?JW_(O}ExeSyGhJIrvsQV)Zn$zrm6^E$CNGmF`7J#Cdz=jNGuXD0`U6ah*{k_}JT zF2N$id~R#yy@U0_Z;P_-v~YNO)28Q^&oO4h!VT1)NY0RX7xg%4NrXc-D`U+& zq`c)8Q#wlJq;cRK2iKYq%D%(3A&Wt~Y zH7;6wG=foH?LALeu3S%3f7x&W?EvZaMzwugl^Q!Z$OEDEfzR8JU`R6{oTx8tL{K+N z8-^=J7VPNYJx9!?(70u{xr!N?zZSG693vF4>lI3jiN^Ki<4J}3WG(#?9zs;cllG9?!sNFi+E7LPWeNI>P zCuL3%zCjg<;f7*@B0GWu!i)r7ifcoAa?ZuT1Myb?yD(jSe7vKuf~b7{k8m7)FMefA z{iLUaSZGiY?$B#c(sl-Vo{$iD=}P@W$omc{%vL{&R?e$A>XH*A=#50*G@9lO!2{(n z&&NDGy&bL%6|g|10dZxCEl~`6fWJY@IAj70)?%1|iza{x(%T(KtnuJ=f2-zb=X0$< zo}GLw`M^!vE0XnAxpl<`t6{E^eQMd~??C9F6SbjO`af1&>VH0>M9rJWFQ#ZfUKNAg zdX<}CH_$wK(Z*I8UIWJkjN_2K)<@Z-|Aa&#c=WIG$?n)QfcD<%jWQ!nbrG9b9Qu`X zQddaZ?fuTSH=+AZFUH4en5a5}n~4fZj2F!Z+I0~8LZQmj@;ffJR0_O1-s*KnbtixO zY~nabugHc5&R`|g5NQO}`SeQbGy6`OqDum%ls#M+w0v5?rWiX=vaD?8dl97Tl)Id4 zVm!ik({HaL1Cx(F{F%CZ*xhrhvRts^K(*AS`l<-aRwaH*4P^3$dxD$!^?sZ5A}gXo z+;Np_I3Mq69{!`SB6L@7{fJ<~mbEma0>{6?GzY2L$`?9R`(AD7(iC0-%krKS1T#P% zv=~FIM;eT8T)WX`9(Q3JPTSVrQ4`=6Xa%)qCHmjjATlzOb#BVdOny=ZQ@3rmwNVh6 zjgTwCX~(OyT{Q1B48Kp0$<2n41RpD+2&|f_p60_?*J+3m;k37i?%VRu{bOxSmf>a9?lVF25%=bBYphQ{QALk9;byTiWvbt*t$DgPjAF~gqi z?-2j@Rpw#qo1=r>`$_QP`^B&+b<@40{iNU6t7k*^V4 zdTNXY8lg!TNv+F}4q$9y78lrx)F`3Q#}WM^A^7iHeG5I2c>3f%u-RL2>A$42nsv<) z`=y`+yNLQ>MVCY<^n0=F)Vf5TsIw_eHXQ!KnmS+Y4BZ?SWu?`|HersEb_{Sc!mfvU zl(pMI=`)@%8T*d;NnXR8P!LIT1VFi~Av_@hMsg&A3-Jvg05A0!$i3*iuFrdX3=^ab z;!<5Wn%(qE#wrM^qu^B9RjHGs5ZYBw+m+=<1*e6 z2ej|^ItpV>%4>`a=ci5Zau27au~6t!#A`itTQ)|2l+KItyDbaF*n@d(U!h9rJoW6p>%!|^ z>>C}{$UUoJa~EOQQHz@4+01tfT=c{nj&(UWS)UOY8?IgSv0PDz5XehPGd~&HNn7PH~s-V z#*?c^ZmSs)GEvM`zI6XE-v;iP)s}_VTA64@-5zMwCr+dL-SUo)DS&&r0QGLKI2w(p zg~p)qms_;nt5t7_4|_@z;D)J>%Sh5+LWsFdi8O{G_)Oi^gfKvTAgt^L8j&AJwsyyH zgDDor->bXjQ#F^Zj3lwXf^7#nf{^z%$`9TMM<}XlHvLE_gswhcJ28Roi+t3k=vKHF z=oqCe;S3kdy|g=MOEi4|8&IT~xJL>Tc|sL2r4*7-JOF3V4c~}t?e~BYf%^p8IN`JC zPZ$sX}(nhlp+sgm)kB)Ok~f)|(@U=<6fKu0G3VCJIOTQID1w(5)UB z;&%%-=d@1vT$Ox=60e$C$)58dd^Wo5kj@13ZIzoI+lVBjfL(L9>%sv7Rz^fPjaWia zzoYaj!|4;Gsx#w8(V0$dwqP42r{#V}ZPAI6)Ec!WC9~pcM0rHw!xD$jaxu9mw2sdZ ziHE2>hATI7oKZ#d^$^_RhA0Ki!tD7OZdM_zvwl)i5%&0R>cQX|Laz;Jph_Xmt;jw> zh6U%|Ta+L(zOyv(=`YW)V4Q{$nkxj`2z%O*X?ySU3*micN-GoKayk5{&IPd`Y`Rw$ z`WOWK!`rxS2pn3deqWqTeSG7rg96`nN%41%TN>KN<0rfAG}FI-27l zuu28YTg8niIqr?IE4g`ZB}^P*0Rel&@F9F9m=Cfet3{L0A|w?1b6WEkLRMZoRy;n~ z_rCjevEg=zyN6>Fx}IZZe|^5RBhO=M$l`+GsyO&0!m=6Ap3a_EWuFX5C^bqAU99q8 zvo{_A+%9JJJ3|#RtOdj~2$Sti(Y9aVo2!)F9$l-{5c^WQ7(SUeUT~rw3HET=#FB$~uEyKB>Xu>1Q?>GbJL?b|WD^#9Zs@OZI)5stcuhsBc@lY(RiE7CRfN zM^8;iJ(~5mhZ2M+zX5FN4jDO=PU%5p=rGz+VfKB2k4ZnQVLjmqC6HgV7~-<@K&&?Q z?>Dxp7D0S~V-=kwB3xh>Z$BnL*!(J%xB&umCxpkf=y_u)QGL zYj*A6@pFKiM`WH78{Bn}dA#Rd#ZO8?8(+8WItzc!^n3WqC@!CQ zX>)(2<3qm@k!@~!oPs!?fkz+iX9VX84O}I6J6QM4j~IL3A*X??(+9Tx(QI;;Bb}w= zY$=}8ZD_PBTK&*6%3P)gRx?T6S57Uy=JJ{GG>Lat1Fs)o;CczzV)}@;WK>(eqIR*! z7$A>Pr6>mK6!hU!Ym#JCta`h2dB~cm7v$jmSC)eF2J`a6RiGzjD(LJ@+uidVM=zqFL;Keav&aSb1eoA z67vz2p6>EO6?&I~<5|FprQlI3(#g4BS zX`KCuyv@6vGl(L+9xyv+E6jr7Y8axC?#828!D`Fh2*D93B#*K51DF|O>A-;)dJ+Vb zV4#>kZgSgsTAu=nPNX!_`ROgD&1wMPE;_WhTuVfEw^<@0wwHN)$BCaslNBl*BSR;o z;CdKk6YQt;?R{5Y`6WL<=dy9(D5-x_xXwS<=2I64?weM%E4)?rDCvq=q-OP6H)20Qxw4?pADF1pG429f_P^|0+Bw>1iwwX<-vl>GI$XIUn{<0yF7a99*SEn{xXrr4S+AI~Xm0PnAAACg^5ioGmGEQK zD^QLRfIK_m5S$vn;4Uefm1aA=Y&;Pns6mNhAC9;OjM2%0WId8(b!tVNgtsA^Jo$2t zL!fT1Fv>>4Ilo$z8?^SQwa3ynJI!G8KNQXBEO4hx*YdIc+FO54M+UbIHiFIG*17fv zvW9GMas%$c7>C)WtUDFG+<680pCtOI;@MF*x9XUdjYvqSDy*x+1lW$N9874mKmy5- z`i;6Kxm0<1R%7eQmY!FDofUYzY{U0zAMJ8*)BY|&6U63R_;KMg?~9W74*+z%m%id+ z^U3&Dvx80L81lRf2Uf@tauWJ{IlTWt4&XkQVU0qGIjQcMw}^<5%^(Orn-zK4v9`yR*YNk>7}nirrjzo zd4>y=6mrH$WpaGcc;Unz+xBf}Dr2*W-)k5rchjuC>E5`7!wW;ra{l(8AR}}CN3x6z zrY`m|^(HxJo5p2YdwqYm-%cRBJ*&eoev->;Z40T>AFyj!5Y$dRBeFH0?N}$rvnn$+ zSXjn7c4*ScLF}&K*pGM0W$x04ryBElW|WlDQt9hs=`sx|O-9~)(fwqtK;hdI0Dvj1 zPUF{A8~Jm1;LUjx;`IGGP`7uH5AX{o42Q#9SbUr>l@yY&t5=3{fB}sNsVYND?Z&$T zPxrkCTZT;RUp|3Oh*o-ugrq#gl0nC0i;ttHLSp_Vnu~6w(F;MRR3CKi6}oU{k%3&7 z8*HOei5L#qjs`|SZO)&~Rz>Xww~pERPVs?Ci+sxwQFddfkT6+%yV_Z#EUjN98!z?) z6I;a^!{7#uzeAa*@tJgpHU}AP9+rEB&1zgBzqgb^K6(BH#{Z^lUF9KZ8^Bw#*-;c0 z9_mLK=of4Dh9i(g*wJ||sr#e%Ww!xYZ41^Csm3~2%U-`t8e;(x6cu~3u-rDIKxrd^ z?o@uAm|hw3m8u@z%G5D#SPdd)izKJL$k|#y0CX(ga=KWZv-HOF!k~3kaz?X&?#26w zE}}lZ6mI5?_<6fa^()MW_TpkHMn6VI0=&czxw5!1^zt=S`tTk_rBt~vAE;1}P>^yH zCJW}_rmrVX+T9k(s)BU%KqFZ{<5t>MU!C`zJ2ISYn2VAC6!2Q5M@}F##HcKpx7duN zthaHw#RP`bvEiZ+a{4(g2ByvZ#UW7Xb)%|D_$b?S2H_6tbk4Fd(_D4gx)%y98Gqhz zFcJLN&`!iz%w;Nv=LtcGMAM+6_9;NS59Gx)WL2x*?f9@!v2-Co@PN_l!_3Sh`ieF7ah3jxAkD7ONr*% z%7~imx>!IYPRy5d{1zoOFeTk(HcKMeZz%$R{DC+jVOwEyB?J?vUyqb6Ukoj-8!=9e z7%ygdPt3L2hVi6KtTzU(=4Vr>7PLV9}4+pMdqXckZ($rh zMiMDaIn*!hlyysLXqa>1;UQGa7$yph9}}EmJD=}kVgUCJiMsn^n#+=|T$373eXnCR zviFd>F9qg*f0I}~-6Ji526r)xaHf`|)oaFCfAz|2pLPET`_U)|V!^fx4G$If+i!hi z)?T-#XRC>&SoY0b%CbE332HO=oWMj9Jy*B+C4aJ@L6FQ8RNrFco?P9>-6U5FuCdFR znK|!?Yw$?Gk3|hNqmG4T>03n}fpa`)#r$0^MM}q6Pl%m36e0l_+73qT8j)>{`{?KE z5Hz)LDS$Fj78JFFuv<#o6J_;>!yuI4S~{Ah_f!8Lp@#)F^QlcN&qr^i2Rmwt82giS z6cZ|z$Lu5)8_V4U$Sp|?KiB0;Z6u_|oi_eJp(HtNjO5YhRY4OVZLKr|YrPmM^$L z1u#8oCDk99V1})5SZxjrX*AY8V9DC=5xW_Al4)-xc<7Ix_v(RKs^0H?X+&tG6Qz0Jr#P7;CRF=3vJCDjiG0 zN6(`$hu(hHOUqcE#Xa3Ye|-u86LVU{PF6SxrEo7k^V_&uJxFVYxD)GGYVduT?Cb4_)xy3+Hoaz=yO!<#xJl z&hR}tD|G(9ec}GC^1-sNyq-1e#|AVuDHb>rN{RN;3_<;O28(|T7Q;Tod7!t%=@)z* z{*Xc(Oo!rbSOP_D;}+#ZHI(YI6PZ&R`&pu0RUWxbR_)16R;*5&pW^#~Za^6mg`e}S z3uDu8|Mz?G!!6CuI zve;b@t>eE1qmhS9d9afNtix>#Y&`RuSdOoG9y~@O=4mj^tgEyZ+$Kch) zR*X^56;MdGqebIH{!lr1yVHf}Gik@qEPc%iOUqDM5dV+*AyC-$0DSYA|C_DVY8b|? z#Jr|=EOA?yqr1T-uLse_h~#fo*HGoaXu@AIpWIY!&S(xQJgj5 zR{rwVfj|{lunY@rqR7@Wa2O~w*=d6O!K;BgR@Gu_WU-%It`EGw@4aDA+q4Xv)ryp; zyg(AM;Yk73->l;j`U2R(?3lk6Mli`MH%x@+KHJym+yQr?F|d(n3%D2yCr#Z(UOGr6 z$(~T^MEW;EDKo9F*1Ar{3RT>=_f9;u`jPWy!f!u;?iD`AHE}R`l6*zzIa`jTon|y; z*I1*5to|f{t3o5El27A|OjdPQF;Z;kiwJR{TYtv8-kHvW2^yTm>eAX`7svx@r6!2k zlIjBvdB~g?`0+IeKD1iS@wuq;^Kw3u)_l#iu*dyW#Umh>C$fMNFGHpJHFZNSy6AnH zdlLU@FttZ^lBs7&_mlqEc-jTxoQZ%FN!=`tR+Knz1Oe|8ORTVSQT@E{D4oH~GxN^a zt49ynwQ2fvpcr=fm^xS%%zEo}H$~2TS<<{`^IIL)6r*j%dX_65&>(5ciT9BR_WKC_ zgzCQ1c`CLFKEg*?-pmtkp_`U#_uwQhhbElTw{1H`*Ly@F?%(*CHu+4+YI#|XU&4x{B!@X1bBq^_ zi@&iV=jL)xe&?dVxyyA*+nO`vRHYnf!M30)BTXV<)Zin|^uA-4LNww)9y-gAYWT1k z#7rCCwC3f~{iz~Ke0tHGQIi(`?WKYTERaX=W1V9&T&dAbXr~^$TYlYmR`q%@Z|#^Y zwPItTjySx8QrPy>%grY^rU^XoEt3(+HSpi2uZKSR(`GM#Zh1ZMA~HG$M?oM!3Hw?q zD={f#z2*SrYb8Kbn`LrzPVDCmgX}UIEnl1GRiVmO0RbPqeV8Ivx2)6$nHqc9y)Xhk z)Z1Zl-`_P@uDwjp8}hUcvkK(FTY(o}V#o0ks_?yWOY-`xcE<9xa*Is_N*uQR4u6%D zR8xT1tay(W9W`mrRoF1J(C|~{$iRG}E)XWmxdA z6pDwAt;A;@Z^7p#J+`?osXRn3t@a`3HU>8_Mmvk?yM8NKo6?dbaUi3Mbv!~;#l zJ+!N!&`cDPjn+f?P7z#O?HJM2fc@FS9L6w2ivc>^sSK5MMtba1K-}6VmV-vvWDf9o zXpWCW)G#w^=EYmyB-NW%xg|EUd$VT_hq75i?If^wfwigS;M3e1&IzA@& z_u&(M0s&wxs8T^S$UB#>s2Wo{u?E!&sKJG^EA=#JS)j#!p;ob!=k3UG@}bJTV*de< zK|)d#9E6JWo*|R*no8DbaL$-WH)>k+^}Ub0RFn^xMifjaMvvKYMdB+OR|lrE;g3tk z;d8P!s2l!>BVB+v$Rr~f00+DJB9G69_(?tk=x*}v|FnxKK~C#3D^C%8B{d;_3B#*q zHq(~C!O@cUzJ$|60yps`hUfd-K{{+)vIKksVPRurpvs7xr4nkOTq>JOf1Rw}$L_Mr zpSxSK>wj&Y*a5Qn39>)b1j&TI!r>?<`1wR71fB+uFvm5LhAv;$1)>AK9U-=e=Q^>X zeSMy(TP)yeqhz`1U#Mt?7!NYD>D7H5NTCk8Th6gFUT2k!PMg<+XAYKbc~TW`GK~Po zfp0rMcD5_H^3v!6d<;)UPfE7UFc%GHlb9t0DMjiKq8 zKDt`u;z<9g*&U04WoEUJKeBv2IFn$=xizxs-ZTouibN!{IFyt^A1$UahOPgO{N)_k zAM1Ew>h*z}p8xPo6s8`0i$$ArOslnJ7<+JN2ko2+q6pYId7`=Z z^!$z|>3UmD*iBsFihO7~v5cSMsMa=03F5RFnR`WDuXvT|cC%`izZk%9ye|I=o%HsE z>B-X({QSqPn_PU21?oG+M2SO`Ap{Mp0fK9CF*GY_H$oUl-|D|Q&BV%xWrDKjm*aCi3JJi(F zw|x}K{>q?2Yoa~&W%jXm=Z26J)S3>(`>W0|!O+a#bp(Zl`X>tZg6Ya({(1Z1T|Tn? zcUer(9x6#dF9!LI&Ttw#W|V=CUOrq*e|W6FE0I!)8l#7jw1|-mm#8+K;?(wg)r3?Z zRwKDNycdR=zeGHP>IEEgrFKrk2|X4=)r}IZu8M`0^A!l)f+r$^_9?AUY24e`7hWgU zjBK4IG)_ZL9Lq@Y-_F)&-EwiP(^d(eJ3NHz0+{K4;OGyO6|Gkq z4cAMn`1c!L%lkB2;Si{+Aa=easy3bZg}iRnj#uwUs!HupG#vH#sg{0gED{%RvqL zZ($W*auih;ZtkIpUI!yD&gPFd3&b+OKQ!E&>L5G5xLPI+(hknQMeo2T`Tj~|>fyMt zy#;!kw#&YnjT=FMgOM8_UVA~NZ*Mk-M`O!8Oc||FF2LpAMUl;q3MG#vjS8kw^M_-A z*#?JEbu*J><;&_^`h|YMdDku%{gA*I6VTa-6v*KwKeZ5!6@A}tboWQIUjL7Dp%|LC zFiI#6HtjHN5lX~wLaj?KPW7M=6B2qOIk{**<0t9xO{vk!)C3%Ov)J^EURr81_P*}W zd>GlkC_SNluJy(E-_#qpQ!Xzp?a7QcUPV7$-Mrz-eO>=6++ss~x=;1%h>-qsVxdK( z)7WBPA{zywOh+es)_mCY-plv#;Z;Eb6Q(Jnbh6l{2sEAe6nH`QAyQl#8FHg(hhyA2 z{l$JS>2i`kX^Z7r-)7t2CZ2z7u)p=np1dq|e)+j&S>G9WVLtO&elac;WmW ztO!maJgTY*J zo1orTH@7^9z$L$C(jo1@K>Mto*mL4NhEQ!Z{kwigdV0ZvAG))=fd#PQMVGiHJB9#B zQ?5oLc6^zBxafC<&KVtw#2~3Z&Y@)3hj~vbH$PvZy4GdBEO~uxR4_@YYl!uIAESbw zc=II9`DS&z_xY%JE~*0k7ekvlA2+82b-nhCIoLGLbb!hyG%XLFbF2HezTtbnceQ#1 z+e2A;Nvj2AdG)3wZrW*_*$bBk1Zv$@{s5Z-eRb)tFJJtbMyPB^xsJy<^TmGUcC!xW zW?-upfcP?PgV&yoYCDyaBS(dl=>B+{11= z!%1c6H~};S`<2&@V)Vxi1KTs+_bpK;3Zt80ym0>vR2fk*hMmag8cD4+;U2r9B_pNp zpMSXG{aB+20445@*{zV@DlwEY1J3-K2wo7Fc~AUs%`UQsDO}7I;oY+01dI#92simh zB^R-VG+0^zdVNFy{z=ZQjc?|peL1_Q>ptN<%(x^wR5n4otSA&7SCq|-W2()-clzAH zYlUD@`0AvUqPkz*6C(Px984P(YpRY=_?Xcy9WZM|YxsBXE+j!?4+QdftWexFWWCSp z<-xlP=W7|H5^enro*z8K*F0g`c$kokX*Q}r8KbdOSc*t+Y3Y$!?Z|8CP2^FTqc*7N z40UG#e`_dydjuGSaIMruRDaMGvu%4a{+h?r)N6S!(YlJxXBbX4FlwR2p`=^GyjWgm zUyJi>PL9p)(u{A}XNTv-Ws5A|Pzy?hEwPb8 z_h=Z89qG0-vBt-ECd%JdI8gs``#xs>rM&w2DgSA|xB^y2rPs#i)W6f2JdlQAg2rZ85>=l*fh`dRpNT;Xo~4Ui zDIdMU%GgSyFS25p_e$D&Q6{>-`-*NivX^rEyHT^aq3LkaX_R=uk9}(VWAH~r!sWJ* z5R$}G*$yoDb?@)kmjxozzN$p|A36Yl5@aFk$1L}iAS=X;0u}WwB2=oO$e^gPWASC_ zPeQ4}3TN0?rpV=2U&Oi`;9lJ7u08SWi>Nl%kwVfR?_Xrk{a~SE>o!tkhy#6SsCLFW z_hsA(VUPw*31Q+{ByRX%K9~t-!5oSrQBm0w{Fej~voQR$?-v&jH9f7*{~ z0|$EX#Ml=JW{!r+{y!utd-~s^Sk)BJ)HeV5kNBD~IueYqv}l4nr3#RT5(hGaz&@T4 zDz6A&4B#J$9Wrc&|NJSI`l#1`Q9Im5 zj@grdfebh%#gsTd8Zwx@eu|F?hys>VxlZI|Cm5YLnU7j%^;eFi46IP zxM7+}3}lXjpnoJnIODp1J!wV*2UBl}6j1#j-~Y4Zy=Ic9rIUR2xQck|j5n zaI)9bzQ92(S;acE=>f>3um6$!fR@hxWjEh{xxn||R$~DFVN(rwhy)AHGXlt%D4Ypp zAxw*f#fKVdktFs+1mXWblHNZdLVEf?2$3oY@Q>LLMixvD;8KkR!$IUs%{j&;3z_52 z*gp~~Ozq;op2}rJcSirig&oEJv?{*#A2#(}{ge9zCrs|QCs&yK$C!V*MO&zaaA*V< z0Dgp!+{8qA1^y$}ko5m@A^pFt4ubx}Ce8m>Ww~o2P|o0o?|gp~;Q93zDU6W)%w`pJ z_$?$Iez$%&`vncc2X9{L>5NGUh}KVn9Ek1SAiA3Lp7PxKr%(yhlP%`I0YTslE>K8* zY89ZK>iEBi0g_%cxD2U;gVMBOA0E@>nMM{{_je^y{C{HsJYjyMAzMJE?8foB<9-RL zX4DH<_k=y3tN5D5s98{1CuFZ2?rzwO&mK|M0hPncfasO(P>AtvWcsb4?oL{Plyw>Z^HKV({=00mm zudW`Oqn)jxQuv-`CdiAUjEI^kvH<{SxPrv6h z#WzOTIer@bHhJ!@U(^bXuQNkrcH_nckaW^qNpHI}ggzN6ztoKB$8*~Zf@rp+9s16- zmU>ISm2BBAm3}Wnh!%N!p!3PQZ809gkyTU-0Ia$8ZVK&Ts%C|LzG|>p7k2tAW?b>T zO#N=%0kqNy$;w>1r|huU?vK3K3TH2+e1F)+ELP2vu6!D_ia|Rhyd7J75bQpM?wctnE0DBsiIu|p;CN}i;9j8J}xdU+x_;aCH6GtQL#D}umnEH4&eo;$fm{sCX(EX;{dfW zw}&$_?IVX>s!eLU>U<{f;dtC?J7IVqVV0Y0?kzvmx2?qDxx5-3XVJh<%?P(H{XQVp z&~u}ExnK9GcGy=I5ObVoCr5I*=O^(O&;cXxQ;J|I+V5GPUBRmVj0Kn(8LdWRoUaaf z2BP4Tb&#SEvY~lB-+fp2xuzJ{zP%A;6xr*dT!w7_hU%`dtv@7R>={N9ykW#pV_*sv zXJbZJqgL1j+oSa^Zrqk6hZC6*?^j*2>`n5+S82&(0q$?iRbWGrT`FG?uT`4%bsIx| zLI|tXQY(nQ{p8_W&{MzHtZvT&o&D)j!_nV4&*(*+^I^Ji%D~}eVfY!-{nLf(t+$mJ zfHUG71a-*1Tx=CT1%R!#G#Hqyf^yj9w}x!YNtMi+Bp1iM8kn}KEBv^F>x9`7g7wh@ zf@X9w_p2^WI?G}zH_+YYv-|l0DW8E1Lkh%^5iIM*S)?vVv#>LY3&|Wc=evpps&lh8O9KXaop1!Z?okhiSV3eX*^g4tuO0k#`mWCXTP zf`_3H)vP>q|_aMCk44w*TVW@ik( zv?VgpfSRsHn$yk^+aPyL>Hd9e%u8~^d^!k&yfi^MV!)4ZicQ@HV>tatiA8teMez?2D zEOSRjhLRYFKNl(=Xhsz&rsmof-2T}Kf9{@`s@T3^p^tv?>`&roy&e}xj2iZ6;-3Gl zGVkTR&HxeB)>(qM8D#;Xmp=kJn_RX&y=1ZjW#@`+EZ|p_{qAt`jemgA``as9sFroV z@@a$RBG*88D93cxAJX>4Qc)B}WESm9!No7v+M4CR*i(1~$~f$Ab$_9ihcVbpg@F;k|loXN8TqnRPsmnM_6eJuevZiIVEof_#zlS-TMCA5nT#Oy-1Y zU0xMNXP$i8^VhMl9U1$_^~JFJG2L!jv&?bs0iU#a5uZ#s`1K-&2TokfYj?++aR#qY z!Q|0XPOZ(m2Q5=CW#?hD zxCJtQ8Ghb)la7It!lAxp#ZoUCq0A|1kW#qYEBk_M(`)7&~bZFI?mORk%@2Eq}sS9D=I zfoAh`Fr>E~0p`<=G*$2~i^z=Xr-HO*pP8I>lK`IT5L4&4s-9+-FYQ(5zPj3w5cX0r zP@%nhxCj7GW$q8!_q$V$#4p0fKJN{s%@1_nd;G4*GdnPjycK!%%Ex7qc?Q)z@=*Rr zv*R%uJ7<&`ie~H!#qbpzO9LhUW@(zhxk^I4+Ow3P-kn@5>Coyn%5lca%VpQsfH08u zHp{7CG9JC(Q^k6SG{ZWEc^+ch0c)MC7Hg#*362TGYvrq#85$HMD>bV0E*6ZU7ZbnR z{A6pvhw^#Qss5Pw7+KQR@Jk(>t_3bxk1`%)$s0FRXi37u=6112(xPyjS5#gfKgW_G zEr@Aasj7t70uZH}$7th^UB;_oHNY)auBFB0{3N%F5(ZgP6bA=+)>@r@KRj$lv&&hE%fmmKRq3=$Q*4xVl7Zb?G&{+) zmc=3a>D-6ihr>7dwNc-NDED(kIU|!PmQL|C`_3LerSX+NU`{p9ESlYsue*QC2V2tM zRkjTdh|grh>fTz0T?d8>i~cdhU?gmje@o6UdnKZqoUJ7p=2(OnVg;0V9#ndtVa*Li z3CDm%|4? z8HQH(Nq#Nzg9IrNbSIO;jgFM%Yh#I9V=`Y`706sWx|X%C_9~Wlb~V>-L@gnD*2%rhrfL+S>c4 z^p4Q2TZkbRk3Bp5oDWyZ&ghI=q0r#h5j^ej1O2EKmNDZ$bxj#8 zvCB@R9Dd8Uug<0pdT@fN-fcNK;gY?t?+CL{@wHv+HH6f~NRss)}MdeB05NgT;8Af)vvndjiV7 z&iU0o$UaeM=yBuNvImEG#{2C&sl{iVToIJP9&?ujZc)(e0R?lvd#J3?y9u=|JL-J2 zSW(gK4+QUe2f~BW-6Ym-6z!t6Y6NSjS{x#Gg#1khXZq+MLTBDhUCt{XDbL?6jIO*R z(e-V}9S7xcVtJq04z=THz7OfgajOi0>Zaw#??{x-q}OEjKC7w>g4zP;STM)u8X2DO zqlcD1tIIG)+$nx2sjnoDWrzyT>lFB)vnTQ8tkcbB_81ey>Itcbm7KZdl9#vRJ*S{> zrVX(V5d>a$l^DWwTOCzPj*&kzxyqwP#?c*o%5;UE`j8_k+p3d@a&aSEiN9}A0=24< zI&r<=P<4T1ucT++`nBzA z=Gf)umczVCq@!BVq&_-@q~bWiaXd>E*{UWZ$W%paXkD&UD}H=3nVl1&)2!NzA=Ee< z=20{23y6f?+i`F&ftntJ;7eDi9SAVviw@a?8^ARfBPRCgV@T2Ks}YBnrcq$khjy`Z z`gXxYJ>~FW<0+|a;}*vxzBuE#*r<`yYPPWg81pO)p8t=%w~C4@=(dIN;I6?NcMHMY z-Q7J28XN+RySqCC2oAv;2`<6it#NmU+xhbO8k?dm+fx)=x1Yd zp@wj*I6vvgeMv!f!uOmQK_Len%BNJgiA`AWc`pGpUum-vo*vx;H+?6oy0#~^rdQYw^G)_0WyKVAM>*x=T*-aFTsxbu#xxh zI=BApJorIhazm#_XLmS_;xjX0$Kkqn^p$m5$wjuUO>f{;Z=lRkeEr2Y)bM7)kdeYa z413xLo2QVPGyosEIor7&?^_0)|;ljs(F&3;cur z2>%29{TCK>g%4%hO_JBkVGh1mxSazd+d)t-l#|D#^$!S;Uk7lgOpkU;LqMR~eHCwV zLI|pb677{}jKBfGlHf1FBa<%(5SXtk1Nl%E6N1R#_a!k&ToeDpipDf$WyJ9yaGto^ zp4IAO1Oyoa7P8ofFhYhc;J@ZPdyy1{fRvC+NHFOCdBBkW@d5uINcsOjN+Afi`+pRq z{6B+e8z(hKN2AIrD~A>rQlI4Be|J3Hp!u&@efD{Je>Z1}g?5yTfn z$Z8bHL_L(B1VC2fZw)5cYbrQbz9`5!tIa}JH@%lEG)KULHEPH}UOp1wIQL8Ix~M=_ z2QHvnl7f;F8X1=!ii3lLZzyX>&vd->W?2Lalka-ME5R6^0J3iXaOZJ;e3ukWl}DYCNvHZ`R&jr!D0LhO1{es{DnrKW)J@}s!*`uDoag|@47n+^ zfZOyXlsF^?`O_-2eXK=H-;Ew3ZdK%W%c8!LCYM)I3W*|MV^k1}z@|B=HinK0b~VJj zfQA7F`W($N?1qvM5PX=LnsVP79T~yqyZ)0coAf6P5{)j#%A<$gAz1r$7heJyJUL7( zIC^I-JF&U1Pf}`qdU{%u6CU7Hs*)FqL#JFu$lE4+n5=+_2}eTJ-6O@}07=@x=KYZj z+tS1Gg@uLAeV~Z!VMc5?=YM0LIVGte!K_1=i9IxZ=NgK50XV?~E}Om>eN+e>364%D z*V_p1p5kd$ot|(;f(i7uL5xp{*xY)&cFma~X!i z846dS^W0MFLr>C?G%k_9Vj zCsJ&ZLcYcN7L5lAOsg2=WAKy>+rUAeV_ry)8zw?dIu z{|!KEyB-gjMw7-|oZjdErXU&DV=%=g%fG(>puI85VSUkJUAJJ? zV%=5U=GCbfUW|er%T%oR{fNU+aq}h}a{umZi^InM-2atC79)@xGSX(A1yR8{)`jMH zb|MceUiU5xy#iSYpUnc>28Nn91tJ_ejO&9~$!B_7rZgn>B%YCNMrV9p7e=955y&EV zaa1mr`R}vOiPpy981y{@iV;f{pf54 z&}{P`)y3;VBZOuM84Skbtav{l$e)VOMQ(jOPo(Aulu+Uy>72%(LF**VBm}A2!3`*q ziTeduvoxDwg9L^g_Csxm`>iv6{0walI{Uvd?0SmZp(7xxa<7*7+P^0pgtbGp?S8o} zRi;~KE{d+t&w}`X1M>lcEV!kGS8CI>SuTOgFO%j_A^%^xKj5N&#en-aj#+APTtF;j zoNSRl8MiDatF112xIq%~yZ^$3dh50==%#jc_L&B_VU^HN1iv9m1pah(r2D_e(EQO18E{O!V!ae(uQFMULZmAU z=oXhv<-rzj8xqn3Gn3PaWg`Eh83TclR{lVL$YLc|40NSDz?$5Ru1|4U%_<70Navw+ z6zih3^nZ+ouz`=LEuRPy|M92b-=q20M3714gJ776A_v(u{pZpv5@gr(^7V5u+n-3t zYpETtj2_MV7pF>>;ydA(AbBv*&DzQkItVftG?&)&6x6>Bnm?UN8yWk*hxn^0Cjg-~YyMKL??ZCq1zprHTz!xm&#xNk8K_VzuO{LlzOF=rVeD%PV;?L)?R z^g%9$rGz|<-v56I!e&0=%6@=Q%#aP8FZ|HM^*`qNMO1GY(G4=#e_f(}nO^Mh7 zxv792*+264e`E<2{_(5ufd3u8iuivw)thnt^YLWKB|aem-OS7^%@x4Kh4U|i*45Q@ z9_Rngwt;MyN4e;C#R5goFo)vCSeckmL|@rZdM-w%`u%p^Lqh~QC$?<34mD!)?B9ii z$ESWK3e18v{>4RbSoWdN$RdcSXZ{6Mk0we?a=#kt`{lZu?ioHD3}(PfY6Dd8P(AC$ z$}#j}106J=rU|sk&}VVVzXlxzy>sUA;1p+VCNDEB9i%R0)LNNiAqXJR<9&r zJpC&>=*)vX6bq+{C(l}&@{8*UnIXWv{9fv01JkC1%LdhNi>ssECi?=AT4VSsKAlj@ z0X@5zwO;#xhi9FO2U$G(mi<2XmiBzGR@Q1bQq z*ay1C&t}Dqk=JAlma-8oXl0_P^r4|Xq-Pi3ymBz#l>>%|0!K1T&{i?{XC7k(%!Bn@T~r6A2^Y6j7EWQy+O#!&tgjeg2j_RL5>U#lhf z$(`{tw|r-Js^|KG*4rHwm||;$j`048a7c*@(%#hg%MDESvMaLsg~H%y?_gU;%)TJL zdBJ5;Ou=)-nvsG3n&6Z?9~V#6(8`a7O}hZurPwu9FK{8Jmy%6Ivoqj0Gofv4!*dL zHVE)F(pB}Zx;1ZdnGwvu8}}WyjyNqX(lO3vN@|U$b^6$DFT{JoCfKU5gppMpt6RJT zg(!HJ({M5!uFA^;?e8fU0jD|FY>Vr0F(8*{OSRL?i?GQ(edc!9Gh5_4S*o%ja@XHC zc+MowsV^z$dRqtx?%1I$V5h8ERBKB!>Oz@oNz3Nfi@V7ev)=8&*WLHCcMB24R$%1X zm30U_vmo|@0>8ewz-%ZG<#}m`=I7ccw7cqMltV0DVL{xXYZMO36RH`ig44;SNiXsJ zQNW4zNQT5!YVQ?(EEtW&QF6V{o^v8CQgG4_b4Z2nI#2CcM&(}#fFM|gEf}=>VPra# zViN|MiASKGzUtM9g*sP0I4Xi*>m}jZ$@yK5H`j?o`CJ5zK%I@ANmWxDS8|7dVbqR; z3P!?@%FpZRzW;Ajq^6PI`K{AL@sE7nM{_=;Z2Xx?Xqr z$l1erk}CBh>+_X%ff_tvokMVMhUIpX+pu=}j30XTng_o6Wz^I0>_|BcJ%>ngcrKvi zpjuY4dc4Xle_ueTKo`Dm6+c?pZ=>t02x#%4--puCO<+FcZR4hS;*sgpI3PF==PCrW z#{PoP;=6qsVCBWtt`xbebQgdr+CB2hoW7}XcDu|&E;;=pr>ldsCIvhe`Q|Mk+$us0 z$qn@%|5a|8-df9Oa{;WMQgU+KilwwgO_+&t&YEzn9M!DP9`ESomBDQ(yVW~^C@TOv%yIk}Q6Oavx2 zDtrWwC37UQcpx2Pn|_-qR~{zT!x#qO8Svr{oDzP3SHRQk9f`S(LE1Uet1vsWaG_-3 zWLXI;CQ4kq`Z=`nqyQ->Znmmn4GIY&_{f@|_Ju|QC9^)54^d)<$7UUuO)oTgAyC#( zl2Ec(N_0y;WNRuUPCUi7LeyKIUs75MMsJ;#I~SY^`d*GGlsd7_W*{)$=g;Ek#y=nM zW^&(*v!~pMJkE2ceS$SW%8H8qw;-Ltlg8Xj#^Wn}y<)=IqNXF0GonZ%k z8^|smeK8YipjxZoyl=ZCG;<_WIorLv$^?1Md}zM6o&w5J!dBO?Um-ZFkITLnQ#n$~ zsAv=Y3Yt_h8p_fEF!G?lc*XgrWRWsWFe;Bp1F}nuR2Gw=j`yp>Ho9I&-&UUU06ukF zb-HW_MY%)E5*Xd@Q_{()2?zw2$Ejk?-5qRe6|$jc(sKA+;bn-654Y0kQ}zgbA^l>y z`h>VOMkE9F#AyOo*2i5-PpK=J+#^S+iRe4kB&C%O8Cxx_^qp3-eLJIgUOOCYIrX@y zs3zvR1qV^9h(wNA@H1zhBmAON^@O9N&PrQ2Kc;_&wy4}7-LK~N*v^2avCBS301$nSLsM*W zwGLON5!*BhuLDTtAXP0y=laoMP%e+($oW^ zYO53>#~kCIEAY*fs>_ceVd>SY!{sH#?@a_OBWsthN*1&B+%?#K{<=K0FVC`j&Hr3( z-@%F{BAFm>0O=A0Jyy^=gl_JI^4MGgwRw)H+nV>dv&d%H0(IW;4s8euKW|kquGPbY z03_ckI9eaK2Cdi~*L@ybuJL8%v>S6wzqc*fUn+8kn%dwLG)(vKY{Q<%R5(r z5KZj$?BUGCnumhfQvGhN{ffJ6Pu)lS-jkT%^);y|+yDf$BZ=ob9vhhV@1m(f#$ zI=W9VCB_E|_@$BYjcq=6G6@9IAkm9vb~mChsL z;(~mM9FmXWHJB6y*6<5iGeYiS7fs>C&X=RGIIWMtf4{v{uAk6b6g#YK zZjX$(sW1_a)#;r~Llo~%o_NfewTQ8_Bq4zhQhCEezf+)V^|r=svcs-!M|wB=g9=<5 zry-e!g=;IYP8vJumO5axrfH{)uiAEbw(x zhqrCFOdiwX1UMjE&g>-FR~Vh%mC3UZ9I%0=sa<TctEy0>8ZzWf@G6~N(} zZ5W)Z5MjwqG)i?%7J$(FNmY-B?fch$wxr6$Ocs*6$R(=(0sW4XQWba`PUyKD@{-J@@@%>5TdJHBIB)Nu>L_RAvFT?+sixQH z+&Ud19ZLq=Rr#ugh4UuRbBijAT|gr{~IgZG)m)GS))cw z4!l7Ez5fYw+6E6RJdOI6G~K>UpDUy>!ea>CK&`Gcgn5_R6kC)h57EmCfiTV8Jwkrt z20a}b7yV|LY_)L}?9Qm1S1s;W89G;;P-RW#V-xoDt-YtJ{W?P~{Ut0%^{OM7%^PW} z;3oc$idfC_S)()319<6CeqA@CG%4IEDK>T%J$z(BSqgF)~ZA+TZqM#udw>qCA>+n#L$xHunatS9bV89{7f@cJUh#>#VZXvtt zp(?#eGbpE4O0T(XsLC4 zZzNLY*YN!2HJu8a`016k`FeIPA)TDipTy!SDb5gEqa4Q1Zh8sjNS;;kT$Rk_5(c7m zWSps>^gZfJ=O&&OJt>{Vg)7sWatVR)9lBHWZVJ1kS^s1e9jG#&)4t<fSb^7e z)dF$$okpFl*18?jjn3CQh}HFGnTm8J@j$$Ww#|!2=?Z_(V%>yUhi0^lhxjII80eB; z(CLCAn&dVcApNaM)r%tWi^k_~9TQ7(+V#&rP8%0`u6G6tzyHLJg+?-z#HF?;Mpp~_ z+5L!%T9j{RxLlHIs|$x4qY|3TSR?K;Mi=9ex5wA#`*N_OVm30HmaH{H;`oJRb7$r| zTgPRnJNxNe+VzbSpk_hEAur7lJoXa!~H_uVFh1$ ztD!^d0%NQ2%=aDEeM>Wv$pQG=^9h|0%BXGq@KPaug-pbnxze~%9tp9lOpblDaN}Ag z&lkC-^M$kW;Zmm2e+{XH|L^mzm)93kAt)-mCNFo);*fcw3Il2p$=~ z8>a~QLIih{c%?Z^ztNV@H~X!=D5-n*K>Ol!yT8`#m7n)&11n4X6Nn@%Ihc*rh7)GF9Tl2K0!~H!$$4tDT=>^E0iI5A^<}P20*F zXF*rO#?xS)-yXI+6Hj|`A1TwSPtjJ@i= zy@37WLR@847>s3H&k>UDOg=${pQaMeuQAzii^?HOehIXAd3n>Xy#HzdHPP0AjI;_EGIyt7p17tbe44WavyEM^fv!p&cCCrXkoi@(3(r1SY^ z@|U^abWT82QXMW_ZO-TOWow zgvOHN3>Qj8_4GWt6hO-2mG5OL7D7`dCJ3$ua5}ATzwHwE;4ogh3N1gI>E88Xs3NV= z`aOJw@9XOmtsxUKJI&-~_W{cuTt8T_pS5yaTz()Io!W*tg^c9+(^|f^xm~PRE?r#x z{u)WmtFKyKUOrj*-GVnFihadLNnR@$fO8HKSlJ=K_XAFxtxZ;2Rgjc2eL`0^Z0(Z# zc=)M%t)Fc&EP?&?w>dncQs`mY3Zqfmr^;sw98_;}3g)LHCMo{w<&mLM*Rf2VFqO^+ zl1!roxY+!VC}q|TM+JZbCM7JWmj#fh-rExN8Y9gbI0|f<}y5Q zPtv+16bnsn+#AoX51)MV%V*qvB{g^0PG0SG+j9b_V8~2fTCBjQy=3GcFa2pD&PESv zNWt+`j&^^h*1zhtUnW0q+{Sx(ez44b#E5v8+Wm?A9i9?o36EIgLldm!2xC@aA7=U! z=c~$m%4;rI0xGPlCnQqdH$xc{-=x1HuDzX)@AT7&NISx)1C%DiD#>Nx68j6}GO$%!O`Dnn z97?W!i!h%+W z5MdFD<}XdP*O&7y0jn^7Vpm;D$}ZL5&a$acPj$W zhxibM!m48gC_y2k6p2Id&)<>Pze#u3&ty7ar3E(EitKfn2CG@Uceyx#xa$s@eG2#R zl$n*o5b=G2tXj=8c`%pF#8O07>qIQ4$u9}KF~-5X0&c~wQr)imjU4#KZ}Y0&M$5w{ zly1HXvQo`noMe;DMtanw%*@!x13x9c1*EFBJj&nZ0(h|I@4eUFF$Yi_nsCh4P$qbJ zr7!?}hbd{L-D8A7`-cS%z?!U4`w8c(0hFEnJYe7;*nYwHf3 zG$rMvfzPDp7NXG7fXxebyf~k+=|m@03+ODj7g+4DB|!DN)ARg&zH=1fYO}u*P-)8j zZ9bT7Z#-ms=xl66h~dEApeeXF41Gloi|r?R^I9ltM$HeQa-<)3Wp4*4T1dFDU%@UD zegPhBFcR)pF$Hq-N1SAYeDrc_w$W?=Vb_joxEGMPRQ#=*q|o`rlyhnG{nsY-!*+v z@ujYCvPH(CSlJJi}qw7f+ zlcAt8tPqi5SKqRuHqz4946p$L2g(h?6B1vl^ht3A4SRhvvLLwdi_&UqC0x3!w(WxY4Q-uk23mt2gc`a)K1bcu#Rl8Ud0on zD$0RUAKn7a(P_FryQ+;w$M)Xinx@SQ9AwkKVW|`q3N@^&?t1fah{n5sxg5iAoOSJ&>^m40I9h3*uFGpOXio4OKgr-x;4a_V9PXik; zSuDD!7^p@G^)TL^Y>AoQ+>r$Nocu>{MC&wMC)_-BmoSA4WqF<;yI9l7Bo$GJTxp9r zG&_61lNn%JeB*a@Sn7oPe6i|M1koh6V;ZfVk6-<4!arrt1Zq>H~g-xw2prK?V2ERMRv+bC_ z#`#&3HXv6o@R#$|~|U`Qzy_o@2R z683Yuo&DpwSkc%Gk>ld3Tmu@}lbA@O@95a+R!Mo1(EQ-F6CZe`h?&l@_+c6@7U)jx zb9XuTEpmv)=`8S36$@!3U9D&ilXiC0PkjTeCNg*74X#qa#zx8YVdu8kjlJiCWL z<9r&(ejj!3AOTO(UZt&8^hf>fkGUJuYnZhj3XTgG(o@Yz;Ax8+5PS7~J#F?M>f}-% z?;@XUCiiLok^tcgoG$zo9x62%BS2Vcd>#iRmLbHKHcL|g1pg7_Pz&V=*)Ws`JaPhz zDl$j-=D3#EMAXDc|0(O>`4adYg6@~PYf|hME+U)Fy+Oy&59^+y{ecYAGWbR>fSxbH zs7_MrnvuS%K5I_5eRwlE-8w;?fj4EYMY&natLI8gkn7U6+wIC=*Pl`hAMc3Um@1#-K8wG19IEyDa$!68SE(IPmC0NU{_WI` zo-TWrz44Fpvf;~@D29DK7Yy`4ShB0;g%OqIQgDY4InvsOk-KOLgB}V9BeZ9*2T#aj zWMLPdC4}e`*5#4oDN}M_nt>5CntZ{aKLH$T7KkNO z2{OAFL)Lz++to+^?CEGYzFO~eM; zs7+A9$Pgqnqjfn3%a8#PXR2)s=qGadP^c!KzdY0yM3!CN&gq!!bh=^YdKQXQ8Z?vb zjc0|)AK^Z4YXP)ydee_t;eB+jZ9s9VQ&! z50Jce!Va_nPo#%5nq>a2iG6+4yUQcTV`}NB$LzS*T@P6`sywVIJ00iYpo9VuWZOXO z0CIjX1GQtv$rEZbxAlqV?>=COEY@M|(^d;0d4!k-rMmNovE^?0o#B|xE9*qA`$`-l zInEVAx(jj_dI`HD|48+k#ymzXz=(z9{RTOEa7fZP21J#6a+V-w5Bnx2>YMp4w%`*} z)~gE53F{R;qY;*E&dOEC+72hum~92NGQwIr+!;6HoYC>IdYa2@7hT6EytZ{Wf@soL5}${+=Wmqp zTHtaU1c4Ba&0*rsTAkN6d;|<{WPE_~*(xm>Mu(k;Ob(bAHX=panXUM5?x}ubAl#>% zEC`X8Y1EHB^E#mUgzf*~vlfp5`SX9mTc zC#q5wcQ0@MXVrFP(p_tDf)4ig*L@S+*XxPeyEK#8^M~2U!BOogWsgToNm!IoPOA?%JPg7nyGeY}RXr1@BnLRsJO5q$WeGVi`* zQUyd5-5w-@65wH~6zBAHg4E{}{+yq`=TZ%auRYzN>TmpXbvqPKPt8WyVehp>uOM!} z2Vk|ifvYao`pz$?$$4;|gj&s)%-2VF7%?WQ{j}ZQ#{@PY20w2Gt%}RT#6*g+NHQMX zOlKe%vaI`b1BLu{U;k}5nT3y4)2A9QSwa}WRaHYbCw(++Dam0Jtyrkwh{DNZ( z(jmcG6wbVJShG?LF>K{8Sk#sTvQF|TA+^|L(fyl;t_Yn9NwE_1OA?-6h|me9c0b|Z z)w0*;7IC9@gNZAH9g(K8H5JDl*1-;QOx z8VU&is}^9<=M9&7OH31Ae>Sn9hZ3GcexeU7C%etqM-ZBr{*x79G=mzDC6Mfib(gIi z*kI`^VKTx_{p04Fd~aq-2>X$>_H#x44_L)Q|MN~n1Cn$mK1+BH=r$ttU#|6@lkMZR z)}{!55{kCue!J&LKv2(hZyAObMk=;5@?!QGXd1oIaC)UoLHLMOdd->( z3r_qs@+^y0_o7g=xry`6XEMbG1vm6Z z7<%gmK!D{+lYLN4mtL{?#BgKclh|3tjo$JML}B&g+564|qE>SIXe9D=L4?SbGS-S#C8uq7IqupQzjsi?X~fRF>x-p_Q-TPxm3?qlBJNaZq5w27>1 z_}QwU?{==bKHgNtmA0X*9mOKkykp$%@rwib&z3%1^sDA6Q+eUCbLdqrZLWnw1K|rc z%3+_Y42>~1{+k~R4SrPEc zYMXpx&eu!|+S~v6+1`vGcRXX5IN{`o9#riUHgmD-f?%ZAwDa>CYhG~4{94X>Yny=3 zD%>!32Y@XtC%pcrh4V^nZ-%TbFQgWbdj?|v%4rR-KUEip7_;=GPZg!yD&MZ<^3U@w z71nj#994KiO)6DkJhj?Kr7E*Ht`gp7ROXxbwd>}5$*kgkgY=#WdA$+%$yA@{%8Hbz zOkHr5RLQKP>A{T975*~Gkhyj`A-%26RVz&ZY$Tr~JNvOKoZ_Cg;IF(X47{??<+mu$ z>8?x3+CBzEN%sVg;pAJ=urNdhJt*mb$8ukafL{-;t;o}T)+}X}#7>i_d-fvbN-)il z2fEX{21TcwrE+B7xxM>}CKc)sH z5KA*w4$PCIl)C3}uCg{}+A&5T4-~p(Ou;YTNH7WYBX3{3xLoRI%>zH4=!YVUl&sh= zkqgJGU*dS#3fJ=}B+Oo@cueS6 zJg!0E13VJCA(;pJrB*>oQawOd0?XG%T+Yuvv9>oP@>D5yqO^gMtkswUwV!>k0hz*E zxt>eRj*+0(CE{UX{fl^6sllD++B>iy#M?>B{(X4&VkH1ccD=_A%mTBl<*A*(qr&27 znNb>49PA>q^(fLAN>$FF8%c$uK0F;-t-;_4HCL&qb)Z{EYiI!alq{tJeeFhn2=n4| zzzgaUg4QPEhb0;=$4nwZU|`JCM)mPo;dVOLO2KR0mLwauOy2M^v3OZTj~*lo{lREb z+#cy0;b8rgP+Gk2 zz3gNscsj;N?*&ZVKN>bd*hJn*RKVCAt;0L%pQxW{@Grn0|-LGDt9Fo^>n(_kT2Ah=Iz`lb0uAkXMH72ojBY~H$i!iHS)2I7ze~I z27XVB>h@Ob6K{Oc=eDg5R`eq(yWDPL2*_G^aqQG)rHcK#(`J<*ED^ABzvhd9kVqU* z`LuCpWiDrrH!Vsq_W4S!Fl|QgsfZR}FNL(=1yF4V z=4#J4WFh#mKH?yU5@?o=yQRN#~SxgLrcT*j(ejgFt^ovsZdr{M4!I2RgtEKxrnG5rT zyNL8mutvD}{`hhLNac+Abet4ypZ)aM=&%hK4Fd*X(U8B{qZR)$Y>Rn+El&NJH?E3b zd^U&*Dl%nw$t7!FJk}()=c6x5m^gTAV@h7e#c}qf_ZKPV_j7YdWtCyO>N1PtXgo;B zlFpIC(yKxWT=i_#rt~qDM`|77gx_A4)SMu@enA2IjCnS2H4wbF*w4jozDDOn(!n>a zF^PvAVZfO9d!(*^YZ&264x<|D2f>l5QTL1ndEnIq(9^l!nl~psbm=QcE2jSCWjH*< zJBII5m0=G{fCbT;!{Ple)9*Te8GT7)j53<}z8gjrh*$$i4+inR6izQD8^K8~bG9b(!e z{@tTihzm%QL@d{YYCXCgP=}UG@2ILcQe3^#7#Ez9sq7pi^|J6ShB@jyANN*(*gV4& z9eXH7^LFNxaRi_Q9oBTf@Z@6!FO)$%^5uU^XbWZ@m*#n^SMfy-Q}@hmHLc7& zsjfusaY%{G{OJw!a?xz_n=a4kf19};!b?j$SY6vOcB`4DQd~DJe)qjU<&h^#ZX`SB zYyZU{Nw@O@&8Rh}T}4{d@EQ$M?*aMN(0R_M%p;FP`|Aky+gy*A=#r4oR92NSfwsto z`c)IQ33JDfHOJbo*);WSs~+#K7ef|tzalU5%uz}7Lg@Lz#&f-T4m~PGlWdI`G`mNO z+)i%~xi;698n#XjxtbQ!m|O50n8*I`@@wKI@Xz12&%7mmr$O}Tk>lRtU-jXl-U%(=8L-!ISFc~hZY{3Mv{?VE z&FzrH5^yi_KoR`|D2~ny8vx>qYA)1QJRdY&RB*V8&0-lwK4hwr(OpIdUCG68vY$Kq(X2w% zA#Oioghn}vV8EB{87;_AYy+e9inxPIhesNE)X;e9)2D_7+E5A}kvN6hFnVHNBN#%R z>b7163A#NT8VxAwcaE#89NSikt-T;}rPSt&NH5v-Y~6K1N=HcmeFOMlL)hPl08#zOJ)F7&1Q_2+wqgMZ#zP0!I>(nspvjSJ^0e`$9H0S0V}vp3kGO;z3A)nP;mSm$+ALM_hdp4 z;SPS>guJqHfcuxa0yrAi*USC9i{qLuGiV!T=eTp{C)ASnxW0Gyf#?+6ynDPiLxcu{ z#Agwj7>cXAu6Ls%#I?K!kNQqE1bOGVzZFa(yUaCDK7143rPg(u0f6^m8FJCrr`y#% zDbqsfA2ti^XKE!42h&K|WHO(AJB$>OcyoKAX z@BT>gtDsAcc0!tra8EBbTz{H0W2gBFozD3c;3|niLU2CL+l;m--j#$?dxV5XU7!*LpSMGEoS zWzpa)+`vG;co$u+|C@@Zc6JILX5{y_3Izf|7J1F6La`u(NmN_!M(O4@CXOF8SmMG$ z$OufU&4#^?}E1So_&o>{%U~1;<^}$ZiUY^NpVHZGq%xrtf3COjw(;-#mRJa>-Dy zCn44#-IE<8&+a(b?ag(Pa5dwE5f8KJ#fIu8O(S_hcPmsT+wqOl%BioxOES zM)5#92Z4_-M8eE2!Tq&&y^Ye8XHLw0O8B2xLRmP3utw%27XIYLa6-wmGITFXgWrsA zc@!OZh_){j^mgfE(f38*cRb`5SkZ9aGs_HLos%_c-MWo>64_n7w%e@h;4O^wH}ugQ z;Mab-k8W+tA<1fPr^4<}8lgrI>Cd&S>+=pwq8bYj{`y>vY=Cbcu;lUA{ou3Sou8H2 z?0{N)tuXXYLlG$&f3=W?r7LR%Cu~ykD&#PN3f+@>W%j!}Es@qty4s+1vp`G92Ih`@Cs54@! z$PDM@D(CJU=#mSfbQ|If>D3bcc>WB3_Ui*vq>Ha7%*9e5LR}41FazQ7~OY2TN9=%-p&RLxIvNqp!TLQrC|DP+n;cJ+i%=7&ZL%o?Ywq)L(DJi1lh!#B7=whSxK|OIUfG?Kp3qKJiACJ~XaQXvQ0DxON%h0qwhPr;6l)q7?cDEyG6+;+ zx(MAmj!wi+k_vne@)o|0VH5tFA02ahG_S_-^{i5FKp$0SNh3@|V3|x}aKf@@snM2z zFv=}eiyounjs6+jl|*^hjmYv`?EQ7CSq&|9 zM=fyM1?&A$cS-M9l1e~a2d5cd9l?cIJf)gdHOi15<@DYru{h!}Y6s`}=@rcz+PW^y zvN#@*t%3zSS|u`WHcA`IQ*81lYPHh7ZL8^_yaxX@X;5O+k;-z5o;g02D%9%12Iw`l zRI-RT1jK#tL6vbQ3lPT&VU-hi7Je0+bo}Sn*oaKw z+?a<~!7Z1Vc5tja?Hrh`uQ;;OIwt(0;Y{K4zpQsCUPg>HS9-rsIf~FZ8e(rAglMqa zogVv>jq5g}u{eC`XT#UPHFo??uFpJG=ol4t?aH?OJUt9gwy{s8^9IjWz4Bq|ASWT0 z2p(^{(Cm3LD<6uM$=RL5%OLvlUgiQSvyz~4HM4^oa!Lp0a>b#SZCo|h$(zUAely_7si zW7R7T=QD!%E6u0TyW8!L8yz*YS;bjQr29A%Mz)h(FFxo!=|1W~d_U7mSog|U=`zHC#)}lZ`-bra3pZ`+4aGR-k>vk4No~w;uiZ) zOoFm=+0Vc3f1MhAmN>0Cw#Q`gmakDsquYEn)qR8cJMVwYP%d1;zlC1#=J^$TQH0@f}*I zqo}uBXZCpKo`H`8~*!nsCcze z_;&N+%`m&OW6K-6O&!&DMv+j_kjzLT^V{?P{La1Se(!hP@!tF0 z_m1yAufD!<-}gP|{LXpKIluEPm`AwmGq-=VYwC*jZKA^W*rXg|k2TrJX>4v(w;jb@ z`>K1>$uK8xnv_lZ?Nj>dz$VI0=Kv*4-U?Xj_T*XZ=&~Ao`fo_o?Hh19ctQ*v;`yo3 zYfI@6ryGgqD0g{mB`~X73+mFehSDxTbKvfR#Rld$Y?M6_GQIBkvPt)~J-*!U3wiYA zz3%P}rkn+$zNLMKJMyLvjT<*^#MVF``&^7B-%o1o7{5WAA?iI1h~c6I^EmZ{?Qq>2 zdAo-X9v*$f7HU6>_h{|fU(b8z1CB#^9={MHVs?B z@!u(H^|`D8%u&;0iO|ZZ44X=(CUW%2l=G-U>+kppl|WVn;z0nQXuB*WZ*^<%hKL6N zY-^Kw!h@YR!lHu6jpPuxbvTNjw@FZUh&E6AaHw(WuoThXJBKTa(;ktjYxc>lZD7g7hW&=ZEu->s0w zbEwxF**w-C+vQ!HWb!Ibz+G%>T`baQ$JeZR!?<%MZ#p{XHEdcre60~j4ZMEQ@|Qvl$@X%A1MrLQh^nFauP-1@xThg#poA$jd)lRBT<%IPa< z@?DduVl1Z*(7{U>qa2=;d%V+wTJ>l}iTuxanI>;+j+n@&<>-rJ{~V*CjfbkhA+EG< zgEkGKDyLPUuiW^$A=pQgo10MY8NE5AI0u#B1BRa${!CLlPIaky(*a8k@8t+{XAWk6 z95z8R1}uRPAOH$$L>~L*F>0X9vAmm`xLb5IuCx2irWNB>xC~f=D47keYd{YzdPoA6 zcv-!JxvA9T?I?%&FN)`}mBd7=YDG$W|4Vk>w52=o`{FqNId8P3gv>o0SSOy6Ih=N0 z%9OfyuNZG~A`y+HL3&Bg!R?Q%7 zW1J13sY-cH-d$rbr8F993wmNQ{YYu*9*)miOWtbMI>C1cr&gGRrhoVWoyGRL{*X5W zEP>VyU(kH$O*PCZk*^Gw736IvCnQ9YZleHcZIQ1@y7i{cIZ3xZoq$>CGA22mh*Pk9 z-e4Z*jldbz<=S={6U$X?+R&XndN8qW?XdS=#koxG?0%a=9*pD-5s@Zw`hX4`-q@Fo zuO3Ig|Mojw*S4)oSwnr`OLrPOZePmLOOb{_lPv8ccwM`8oYZ@Ydj6Nqo9U)bwn$yt z90q^a_jY9M+ywS1#Agq2lJ12I7jlF}Tb{IVA#d5~+4NMSr|9V4N7;PK7F?rTS~;qB zNjh+QtX}2f_go zIOLomDyn62T(YAd@mZaV>d;fGpW+Y%CAAK|k~d9k<~}=@zJBp*%1dDLNxMIo3fG^P zKy3%Nr5lFcKm!{Lq{G_}+rs)Av~Gxo`OJoAsK(hfd?IfM8ZxA@EjUYwQ~@yCMA`vP z70~tduGG4BF|qY$^4DYfmG-aD)+Jj@=9I%Yw5JNV=z)vq)`_=DssJw_2Xj+dLi3hT zNEMPNcV>#Pq7AJZx@%k{< zykXAAyt#JYk2G~FCyq{M4Fi&Hr>4=JbvPeZz4Jih=nR*&iFKH5%@zebj z_s?Xz>7*uac4u-pDKJq>VeMONlMg7VR(FS`fv?yGdJKf(eU_59sc*cYPMuCV>14a< zWHma4!+8ek!o>CuhdPCp1paV{W(fjk6?xmSeY+xWm?-t;T$|es9x{}A-rLKxsh@+q z-P4;s|7@{}%BVN}SY+zg=TrgTcuo}%4DyC^4_{6^jf;yS0`{O4mtT1$O=XQ1CJ~UL z_SbM;Bd3#8O@xuxuUmIpB2+W}kSIrE@fD|^xSJ!I8q*!cnO7FVtLPM!F;(!Ajv zn!M>~OpBS2J#b$i6%7paDWN6g`;jdE(I*}!MZ(#a)7Xz1i%@Fbpp_chWGEfqdtCX) zfV?1CZVvzB*o9Q5c^&FHyeqZ5zNL~IBZ<=lX=2xj%7hYW0Q6}dh|lrD<8<+@7gOI4 z`xb9bm_UI%t$J@2z24*XV(lA(b_Z3 zi?*DQq#HyyHZPXWx%C{n=fit^I)HFBFJ>LfQc1cq4rC~wK4Wkcv=ce*1{BIW0vDN3)D& zQ{wpBIA}@5IOa%JWu~8cS_z z4fL2QK$}dy`|C_v{ogN?lTpM;$uVK1(94(JOSQ^X@p?}yunzI3n( z%%NCcV3nflgp>jx0oc$Q%`&ma88v#0ico0_uU@>VhPfDmnViDWB%Omvpp4zS|5EM> zAbrX|*?!?O_7d=kGZb_v=k`G?Nc@@wXGSZoGd;%cra=J*iyFx76o(Y%xW@!5{ztAOJ~3 zK~(b-r^-6-yud|E4TKB?;~kcgH&+dS_UenJOO@6PrgTPMB2Nv*gmd7)0hK=tflAt5 za}8a@VL{ymEeWLNP1j42H#W%wxhg^4*tB8Lu%UGKJ?8ziG%@Mh8=L;m9N|ut=8Z`L zl46I8=B*`%5jJD`fi@w6BXBZmo^<&C@vpwZlS?NRG8sv-5zxeF7p()9AeF?k0|)v< z-ZY*sbD)z~CQNihxkTUTiK$WJ#)Qb3Sai{k#%Pnynm3F$e4$RSOqf99cmXv=utdFe zUbGL`o_#Spvm^CIxS35t^ESBAU^@Qyag~qF82%n@$S%kx-eypnKCB^~aHldIbPzcB zvGF9DlbJ(9S`MLuTMw2<0|0EZ`QL!m1L%xaXQ=0(dBXR++&mg^<^UB~Br(or=7@-q zC!Ki`4cR>8gv8apYTi(`ChYSE&!=V2Ei0jYQ`&eYQb^nlV&}*&#|xnDy8%BApi>&1 zV$-VH>+XrX)jGcxJ-zm6H61J+yOb98TUa93oLXz3edFkrDOFNv_#eY5wNk2At+lIp zPmkvxX8u6ax=phMD?u1#d-P9-#+chUc=KSY-oSNQ0=)qk(|Qc;T(#4d0>SPdsDjNf z9&w!as?Wkc)TDirQY95PwnG#DA7*g zHI5Rdy{*Vw5&J5pGJ%U5`zFPmcZn^^q^q<5CK{p5iweFONAa7ca9|JE>E^Q*mfzoP z0h?wvwr_fzOzCuz?){wS?WcRGfRl9d`86e_@rJO$^PDOGW|KJi@He5nElKxZ|1IYv z-6MbS)BJQmvg$Ye@oTGs%$dZTLl_H zT-VZG+C2O)C+t3J;u1P|JdaqcIL~DwebK8Gwc&`kT03VfOUuw3Z%n0DtuCPE&CgLE zzwqKHYRO^uuf6sf^*gj9C!JJ{6JxigZ#jhj5;k|dtH+&GuUew!GpLR;>eKap4-y#8OYmIW0m!Wgatu-!O&T~$ z$r~mPn4BR;+e%K5yn5v-l`;T%Nb2x}_QhAreVQ~f**5pd#~$Zsm5bCOkk9XLzyfhD z=TW)%l1m8wN-6fRyn!TddO-Ze#036Sxp|zoN2zcTWP@|gFnN38>HgIBfd`aOxJzVc z6j=Svp8xAX-*7%W2&0Ywgh{ml=T%#}e3?fXw_5Y2P8eCZKK*PFCk3@FSl%uWqnF2w zWix?SRo|kHD&#*u*yif1Rj7Vkcih^YzFWOo3AFJv3Ai){$@=mOpEh(Fi>t4^injjo zM+s7>Cobe^8#-dR%RFz+j8Ze9IzA#srT&i+;KCafV8V&hnNU1e%dK}`t8HYH>94F1r zKAf#0K%&k%dYy^}$-V-b^k}VH%N{N1u6OP#VbY0qdLnN%&#UPO4D!#Ge`rKgTh2dc z$84<;YxhDsYS+J=BNC@K*)e{u$`|o4LEiXAMShrc)jYo@J-x<8C|zAb z-ilEP=B6@l=yQ}f<3q}2cEGM%6w6{$FqP+zGdam@smL2gu4H0U_|+(i+s1QgCJPlO z>V^wxQHME1C-Xe*iiBy++uv**u)PmQE3k)Wt;8u@s&k~u+IIxR=WXY~CG^YD(RAo& zKIQQI5ML>YT9tpC8dtsAW0LOt!hCxC>*1VFl1)IFOcf-hP_qiB)6?fNskGoalgZnk z2eVbu?!1^ZdWR#<{_lJwPsW^Bk|^r6ud+RxTLBS*eKXEtm|4?NIU z%>yqonfo7;wbMANF`gMPa4_A>c|}{cY@u54^2-xw9}}xS_uZ=!HDjN!ilc9T!+WDA z*?I%PC!m2F&Fg1RUaPRdMU?KBMvbP2AACT?3c&Tz$20iwJDqy7r4KG>(cYc*o=Uz9 zmy~y>O;_m&Zn^m;^*P>&cVoW(kaN&s&AqMrt?IK({w&oVsW4D52UE@hRp-*Y$4sh# z&Yff@ z=>e|^-_&>BrmI=Is-ro25(sVLIFnB!pJHvUv+1IK=iCMJRnQb1D70nsS7WLGcjQe^ zniwm$-gMIm5h#uIGM@T`i_aU(CQFbvV@@(u-mb|}Q6%$@P{Ruw($mYHRuLVcl~L`t zn?k;O-@EkX)Gr-r0CXGkVmXJ}#-Vgt6E@wjzYovt-n5&Bw;ax=+w6SIhj%b#tlF@u zN?@&16KMSW(Y)bVEyIhRTSQ9+FDVguQja}-;;z>Nm}?C9c|Zwml_pDO$eWhC@z;!} zZOgXVh!ysQ*JD>}ZFV)xDD@uJkOTrZ9~TeTgVTY~hJ!WYjj_=hrnqwR+m zC||My9L%vsuu;VhbV^zy$AJnen9I2zw;bL~>3LZ!Haz#_q|>Y|OF6<3*H_aQ?N$f%1 zdUFb$&*pvJ_B48I>}PXEd$91C%1dNDlRZcW;jR{qRE%s67(`4;t%W#8O)+nZehwPqH5}W&ia7#r=LK z9nVf+Ic4Ie*HQH{9K&*sw{mPQS@k|D*Zg&s-#p$wB+z|*A0Xtxi|0dEM2bc3xSMY3 z;`6@VOr68=9!tp^CYpAvZP~qRw=xMp;EMlol0Zy4$j5~s57)A~fBY*Gy<&6yb?p>s zd+doP=%Gg+QTPAAlhiwJzQtJC&WX^0;s zVWrJf_t*ow80Q7u22Eh`*>B5!qscc+R)JJtUZymm`V^8i12D}@t(K~ifx|S@-6iC$ z7@26>RQj{NS^bRGh|lh3V@>PKZUK5-hE}6% zmrf4yHhRoBCRfdaW$#Jh0)!d4(_I3>?MjtOwqS!Z#*ZgiOJ{l-DP zzFV_~_U`>#C1CE-rHk6UJI_h}j)iEGGf(7A^{IUgt;+k}DN|mjMr>83_e%J#ao2M- z?gwG(!{lW1=3msk`mkU3i4OiTal$wi%~Fp!P2P+aLM5LmqKV`A8T%MKdh{6Y!8<7P zOy~Uz>V?CE4?mc$#yChP_6W{yTYfxaTwUO7c#Ka`lI^&Snc2HE9 zwU;th@WD~RQ9kg&V(!XZvXI>q>Qcoke&V$~nb+2MnmuQ(I>gY50Vs#kQuyFw?o6t| z2Oq|RbbrTjxIbqpc|+sp%$m)4z1Gk}k32$1l8ozB_DLGc6B7JuX0Rz5k{8Zq+tu!T znpCl3MP=U$RV@Nrz}INd&>@Q8bh@4+FzO|B5`}Bcci$;OV2sA(j=W*x z)s_k6$QNI5m??VeBn%J?T-b@v<3TfY@Zb{Wb6T_0m?z1XCr%*vR(ZQXUY~sQF-Ib6 zEjXI&EZ`s}O*tIBr$HDD@&+NB;Rkt}^7>?Y@p<;8Kx9IDDhJf}o$1r*N}f1yfpp(* z4+fIY_~O!&9loPK2k>c1`|GbS;c#ul7Ear>p{H zG<_v+dh*13e){o8>elrp<#6tN8fL6l*lXTOR2IPVmofv6r$hON=)O1aqbu%aKNY7m z0PjEiK7Ib`=PH4>CU?07xm2Zg6&kr^Bqb!-9OLmBe9mS(HG>w6SwIyN*mN?t$P)6V z$<&h3OK4I5MJM{9p=ow4BI*Ypv1;{7YCwD;Z^rTP^3^ZX_GQ~CpTofyao#B9M`qtp z!N(9<$5Wb~Lbp%7o$6g+^L^2mahw|ezH;hHn)S#m%FAIBGS;f71mya?&_URvX?7gn zk=i`m#-m1D$XhY8LElswPRdm+oB}kJ7!3aW;_v(Yea@dcx_Aa-@})J8)6s%V&I=jG znlJw1)Bqr6`ApP692uNE2lQ#SfKj*UM4EN+}@&+xP`mzx>2YIuTwqi1ed$=M7 z^2Uev9C;T9Ed1xh%JgQ3GwAMCbvU&Nn@!s13S-svT04)eeGsJ&b51Xg+_!1dX5}-4 zb?EA=+tTUv>pRwLY%ZaBJBPJlxWr!90wj~+`68a7Z`Hdj|x7RcKe4)6Ym z+8g1y{!c&4W|cjZ=FHoLbqb;8VM5uA%Qjxv(~RJVm?&GDc6xiCna_hRXJ#s8;XU3o zZ+Z^KcUYPA@AstAlDZ?yt67^g@`V>^7b`tVxYO9>R+B-H`NbS^A11N-0(txM&uxx~ z*v<+AEc5M{F0#0`l$J+JdYi^*V>wLxS`H$w;+w-j|ek684Qn z>d>LHa(<}OXBTC*cEOe10Hc!0Gqn)QR=XwrbwFoegy;KB>3Uj2G%!o`Ld zbw13eW-uj%{~rja^Q9wKUUS`b^v0W0y_zY8BYD$xTK?rSHks|=5M&*UWZ;m&DzJ+- zbsREq5WV;AyUNs2rx*Y(M)Ct3(a#|0(BS6h7tlctUh@ZIiG<((@~J2M1dYTE6OZnn z=Q*cf_uE-3WY4*UMeC}|TAS9q>3-5AVg9F|(j)gjsK|*i0;RD(BZ|B!z-`1CV?D2s zO>wHG(8%9LQu!+7ZGB;n{Pfr6zi3#?VVvNWPvG<>hh;fCCY$;%?@x^{Z(JfG;?JM| zOyjN}N2xKa9kTnp1=4KNxR}616e0Z6|4A>_zBy^#F1r6B>i%l?6J*O@^7hZ~|IiEP zzCihWY=|hHC5RU01!&0-5imZ6&cE$^^>tc{)2RGO#Zl-Acsw88bC~{I@h7c*bF~UA zlE?pGYO3F-+6ivR??IrCTCHkPzm;ru%zfY~*np6?VyO(~rm_f!QLwB{qPpsVa8zceA!BEpASUPyjKpycPy1R}y?!3W{(LXA?Z-SIe5USo`~R z(2~`Am(o{zmry$U?17wRGqK8I@yTUb0dZ3ay7_wnn^59s8+o&7-h3f%c}f$9AYfb> z19^*K(ig+O!({V3FPJEQ;%2{da8i?mTkvHeP zj2=ggIp`j=Uro+Fi>g$uqWbXn-~UklkKcd4PRS3VOKOeV2_%fy0tXQ?MCFDL*p@9e zpMUL-?c7Fa(-7?u!F6!`i$H)kbmFnbKI8Vhtoj*S9XK^*h4PAYwY%;bs%2Ng=w}ym zz`a)|y{^Wpl6j}YA=Cw57;~S_n?p&gv{5&<~-$hJ3RZ7dF ztdkz1a?K|2yk*M;*NY>xuVIWZ!`Se#De{Uj z1KId@=RvNB8!z_NS>^jFM-S5G51vdJ!jZgb9CovxBho5>D4`7+C!_#?)>Lcbu-H7w z;r8dS=2Zpjvj;iR@6iNIrz@}p;Vm|c)|xt_y*98!xp>!8&pho2B%@6%{UvWGR};7i z<6fo-$RoG^}HqxM9PY%6X~!t?lPF z!lZE_5%N}yMld&(d6Onn+>DPXGZh;Z9)}#TBxS-him6q%RK8_;15^0@TNJzLZAEHg zc)Y}NDvX#8Z*#QCMy}tlqfH)IL9xHGdjY2kDCCV$d=mR>)*nEzr{3T?(1~#{dH5M7 zQHVCV_wXv(xNibw<}lI1p${i1zqtB?bb6(WmDv>T(N{)ZIukPl|B9vmvJTTr-@m~m z4x*CC+Vdo8P`(b`d-io~Hf)>mVTg?%VW4c=AKHGmpDU@qsi>58(b0QI_`}&AuzjXv z33WqQe?EX~$SDGRxjvo0K>3!{sZ(1$_SM&4Q^g7umG(-{3HbYbc8fTVBS+TYMBTWS ze(|LWLV}IEaUTNn{V({i6A`#E_hNJ0p?y0?;IofDnxP_X_vzEyp=J5`=S}o2A7IJ>%s6p@Jc18o+o$ zL{y{p%Shhz-Vb}EDQ~>SQC2Yzu(m8~CuOeS{D-W4&xgf|^C=~3*ORfHCN<#bk{eiL z6P3B-^!MIpA5h+RaRZ%$g=tGG)CCbWuQ6!2t?K(wzBef34V?~6OfS~N0CkYCci*># zRquUwFaGRHWimN}&FmgzU$4*DiE`qkNh(qE$Ppvx?Wu246P{pZ&6}$xDg-MT&1&VQ zJb}JG%I(_<}>tZA)dJxvDS5HM_Y`_t$%#cid&$_kWvngL!g{>AGUFQiLo*zDb#zng%IIWpA;%d~qZU1Yx$DwBBV&^tsiK`mfPaNQ{1ye|n9GF5PubMi^V`2hri0#z62UWor;#U)2Yl`ic{4sYZ@@hI`UQKka4eb#$8XpAXpf3#ry=1j=3ZO$Szj+rDt8`OLL2^lk)ur( zs620oHre{5{?w>y8>N|!v%g>azUgxESd-X)#q)G1H=E}aKFCZ=ruu2MXyk>qL(;N( z`Ld=uubJVBNigLs95C>eyrIr|zoLkYJ#Vlkb+&zEiKrc_nMNY?=9I~F(M1adm)==doW$ zFp+!i;pE_)?xKLL1>!i^$g$OJDJf$)rxc0jS%?oS9fe`tTtr>z=Yv2rp(mAa`J{s=m}~G`yIXU z+Uv>$66SAf*=e@>&9|rlsG|$sdehCcj!oePvR1BVuU^VjvimK_K|?f}H}^N;XTIKf z>n-ZYq@=1-;!53UXx>^g%l~KJKR%HTSD{qlGk_7q z19|w2?}5e(PbVGS!Zt{w2n@O25otu>ox?%VYDv*dPZMMJU4dv^xvS8=}S~`3wBf*-M1U?Pr2$Y(**`T#) z*7V$Y^IjZ73Gco| zM_BX3S=%WtHHON)w}@iuo)HkuTfvI)6#vsC%4Q|M^T_}JAOJ~3K~$4Ke4BHe0jf~>$S#1?+;w9PfG6X8h&>~&A1o9{fMd^$MclF^g5^JWkUcdJNh@u1%2Ja3|KO`h=lBM(1F_ukX1#2&@5_OUxp z`wktIn~u>M$ykIQ3(YvD!j#FARC)0E|Qflx#@ zc4Q^YbHg1`A~XM-KuNnEqazu1bIB4WCJ-2oE(vqV>Q7KwlhF=yNxlDkdHe)=>ut7T zaG`zE@`idLFygy!ze#6s&SZZMNlbK0NN))Wc|!}G!GnG5O`ST^?`(rzf$ha%G70~% zYumJ?t!%S=#`J*IgLoC&DSyg7duzV^))w|3CSx87#PjgEJA-r7A$Uip&Rvw#vTfonj_~KL z&KlE=>o+L>GFO_n7uiG+5k1Ykw`<262?N4e^9HhwMBTm$z+n!f+~?lj$~R3fF3_yO z{|rRcxZoJc(z@@zS7{*N8whh-bxP$146OeMd;|QDN3P?Iog7%YBVagb>cmka_w3#4 zActKP5Jj^6qwVRoakrH)g*^PvVH((AAmwGSNv!s5fqxf=Lm%K;EFi;D8Q2 zXZ56(om;91lo<17E+KElC|N0I&V8ZC+dFTTByaC8CPm%?k>_pY1d7}EN=fo|Gv{kk z<;^0V-3+qi2YZ96)bj;6Eg z0(@ebyj@lESw-ITJV8ac;Q`iM#WQ3NbqfD><9WD ztO?K}B$$*vSLM5k*ca$}hWdTMl(SH`73F&)Z@32``r%`!_Xh5Wm9afqA%L|Qe*QRk z?0a8tdeEL%P#^0_4z zP+n4Fs?hoeUi;vWTYQkAaqE8DowS*`LmclTw2O@Mp#)wDPVp|!!$^=`)~=wDH{%8e zAn#?T+XYQWUi`S;bo9N9>Z&H37 z2ZXQ_T^;rxNxJ)g+uv=`nt&eMd5{(kUP$XcT&Hr|so*1QjpnG23r(B)%&x^$WY5r1 z%4d&*HB@g{ogVo70jgTNs@KB>=Sq&faxCpyz02lLheZ>YofyIw@xJ-@(b?CX?elxR z;kkb1dYXRw^pd&i_#u^KoCB{kypEF`&ws$@gCR5W>3olsa^_rC_>ni|=tXpF3(?nY zl^s=N zf|aDEQRf=1=(dKJstqJIYN5FF+<+*6a09rV{e|=a?kDW4eF+B&!CJ1@16SGz99Ci8 z95Z$tb-(pi<@=@&7fZ}j+)gCmE>|wi5po^RBAVYX93P--*DekSoXc~ye{i*)dkSzA z2z-MnXMw11Y2V?AylJfA!`P4ym|q@0(J{vwHMhn(=zhd^z0XCY^QZbfp;A70I#{+H zFci=|z5CGm^&1`Lg?b#IK1vzE>+PeDJVf_!5EA`9EqZ4yw>4tB(c>h zN~m(7OKm;g13%F_d)%#jLG{7Aa~pvJUug#`*;dr4PYNKi&;J(yR+Ynnlq54v%K;7%B zrcRgZFP@>m(%&{3F}8BCL%8`NXAYs`yN@dpp*3Y*`4`-)GfM~e;P)(_yqwSdoTo=F}ffL{}4^ zZQpLAS1)~4nI7WOriSqx6|Sg45slh6%CzQ9FE(ZlZ#22vICmp`(3Llkc7HitK7x&u zJF^@O`)!y<=!_(B=enJ=dg^NWdBM+=u|LD%C%&T60N6;7Z6@CWSMzB~J8Oi)T+$gEjR8_3z)x=n*V9JaigARN4>=XbjCqMpsb1aGTIp-Qhx-1I zlDAMdUdk5)!T|xR7CSx3t^0T#ecJESV(qL6lRb6n$KiX+RV_z@)(^5Jf<`P2m{4%| z`$=6V(dOlwDTTx6VM5nFa=qW`P3LxD6I;9H4H}kjKm3-aJur<HErrEm2{XYZ!Hv|`cIF_8$YdPKplz$SsV}@AktMT^W>J0)LdnM{vrgcA>RG4K-pzYy z-}-$vO%;6Rw7(1p9Ag=u7fOuRnypFbh zur<|eQZrzSoW^t454-5wH@{UrfJXCC6}%;{h)!v83SIdi{Gn_ovsPYK$(gG)rsNDPlFKI%@mg#Gc#HMBRyw|+o+fEeC*t=l@1_NFi4(BGUFmEW5O zUo{h&HxydHW>q)Of%F3}~e7BxQ zVM>NE)Q8Cz{`L>2s6oWNI((>X^x=h8;8(UNDqp^w$_<;CkVt2q*-%-MAopAE-uI}` z{zzzi#_9E$$g!n@J#fW_jX%=+?|;C_y*n%G6y$P4SqT3Qcadt{)SUG{6_z=%SAR@ZQ%l0L)?RU_3ElafwpbisOXNk%n~=_TsmjY zJeoCU4v*U%N{a`LFCtB1e0iF4jV>ohK)+(;Dn2||uQb3&Es)1CC3JnO@|bMMLG~`< zxCpH;yM(GUSyD1>NMWGrpUx=^>{>rNYb}P_KKI99FOg<9arCerXe^+m{^;Y6RiKsK zJl0{>T&q@1_OrWzZojR&vZM;e+T!b&3B>(E-U5N5yh9Ku1p>iX?BM&Bo55kZ;e+KW zIEGXG9UE!5tyD=Xv9X562IQk4k2OTv0uPZ+`CoFmWQtGb@Z|{3VGl@KHZwYo!#YZ zx^LEfrWf>R!Q4|jgCS?Se9Ot(3if&X(FdP5R80G{{W^kT{+UWS85~f6VeedC<5gq|(slH_*8!*9i%MGm^J$+qO~PKKH3^-pGNf-g@g@dg#IX6*ZR zn)3M^o*#bDt5$u>rjDJd%~e;bu$@8mB5EwVDI6-N2fa%P()OHuaxW^~#Cd-O>VxP-g} zy5K%e6#P0TpJ%+h)yrkj6hjMDP#6m|U*7Ok=0Q5ydmuZ~#k;JQGh>mKlQ&-jmZ+VK zJ&?&^&YSdIM-CXk0R&i+Rmk|qaSDp~b3dlIN-g*{1lsZLCtbvv`4~2B%+5VXtADwV zj_2;DOm^?ggUKWaoR8#92QK+`-zJ*0Wf7B0CW7!8<)8(L328L!+z!;dDr<-NAAEM= zeAL3>ED6uS{L+`9?uxwWzYQ8Zly2cf+)bM_QQiN>l&OlS!4DhPs8M4GCUZ!z4b8_B zPd-JLx4w+Ju;v5TT2Axx>HPT|jgJX6R|({TR&pum113CBu zuFdTKHi;7>gRGr#`swO#qsNS6^UXSR^G%#Kh_8vSyh>Md4&26#Irkr52xQUbs;g8o za5M9;zv387IrF!Ufqc##dBYfseVhuj}SqFJIdj$ng-YcL{k57Sd9VAm9dpU@dlLG;Xe{ z=xS5%>0KxIv)MI$R*-{89#lsE0^JV1soK7FJ56jek>InaeB_)6984zT+3c;&z&6xz zct=wNE|RedMBRfWXQ*pfk~dULX~Q{<#i}k8m$jagX+!(Qn=+WSrq-au<{YI?n@>Vh zr}k2a=WqMbmY)rOvzhE>X8lD!{_!*&$=N{}9OA#$scmT6!LKQc$7?=sTz%2J;eDE1 zP1(GNmL1wcIcz?e!}$$!V-ncBGllvzzL>h!IomND3iRBfs)>3QZxB_O1bcf?XQR*D z@!SF`pJ*F%C)QOg!Bl(u3Z z`yuu1+ean%M&0(XpZ}!SCew2xhAERd5P`Ad#;f2IFoDE9*qA~a_YG^>G}Z#`YYmr8)T}#1)9)`W2F`1LxUB20*3;6qHJqOp@)pj$ zBXx@k1fp%Ri;7MyMI#--Q|4`?H@m!P^S@&sBKW}R3kxQ+ZE<;VbmRCNsm;S}gur>h zJeYFkk-XUngZb1U&v!*$p z#}ntyrNX4jlyt`9lz2)f=4b5Z>gFmc%BB7PE}}pGnoP&Dk5GKM1Ujk0ja0qLWwdJ7 z)3!WsFwstpqc*iXrwVW`0vb46qko)7Yme<=0++8ea9JF2wSa9rn^ri5ZmZvvE;+d- zCHSCy+rz0t7H``}bASDte%_x>Evl8L4{y1EDkOPHBVs2y@OeYP68OC7O*Vo;7|GiY ztRb5{dk&p?Y7Ir05QZJ$(rw}1^A&l!m&w)X?CXXL(JK+vZ}_kw%8U}v7|ELo2F1@J z(&bm2|I|p{KK*n74IMJb7BGtM`Q($Cbd)1IBL^6+HJmSZ-rP^=R5sB>0E^RELx<>p zRuKf_HdYdV!_BETKiD^rZ(=&PtHC z7*5ii#!0&0|BPblvWZ)%^1N|y5ade&dHa#c8z|~x7Qij@vYAg7 z{xO!~{&|%{(?k2l0Y(rdD7J`_m~dsqokj`OE~EGg%_uIV7MFp*L~O>$CX&aGZ>EEX zzM<^Gzvw7usEOnI6O!uDWli6qy+^*IwSNs$`FAU2 zrsuO(E{<~fD?1@cCGoD4T#;Jry=73ILDMdXySuw)z|cBR*O$C=S#ScX@0NBf#->? zgXAAERd4DeQ807#!F%!AV({;LhN#}SB`4jz!5N2PjCdLPmRvdAF=?qTjfS(wh#UNI zg@()g{-$lUK-`%JlykFkV)tVyGc=M*qZh03jj#=B&mP0}_}tjVoW?O_GqYlt_*~Rw zkD-&xhAWOhB=tEJ`qxsZNKl9W1SA_AikH+a6PF`}V_yWC#~NqK99~dIK|qUvw>egm z)y!AeZW%KZujF<7nw#XkM^s=vD_2bp-gR!+9S(~e^}x^KLyE6l0zmYbHDz$bfRPIq zYh?8(fnxqfwg_`x3M)DTt{bzS`XQX^%-5H64k>PIq!vB>_VBpTdarg(4AQf~!X3E; zmJ)10pcp;3S}(XHX0`E+@4E~NJPzYm{ygaP4yong-myK_ z;1NBR6keB%;?>sOQ=*m-!${SC!PcpHx)RHh(WlTuG|*qxAE@MHjFX3VnNr?{on=x3 zzhZ)j@aoeKbv!8dFJEN$ja`<5vbhN)%STFQC!yu7);HNddNZqKH|s18>#G)t(*Nxm zwNG}?HXS91HM%-iyjdjQoC`#-`YoQ(Vxh2O!hp>5H_S|lxyOBSvy=+}<7aE&RmqH-&&!jXttVQfVC3K3Z@A3Xz{7%Ev9e?}x5PX@b@zZ* zvglyQT~?qHc}Z3FUC{#St*11X9b^=n{KIkOP_vB(h#DwE{-kYL}!bza4^AC~Xta)XPF zFmK5n{+6peo#aO6_+g+bQ|!#9 zQ>KZz(F}Y5IzYW>7&echMg3S#)GXUpk`U>IYHkJ4}a^Ds&s*XdBWo@C#92Z~u_N zXZ3B0!2yKZ^a>}9Q7sZitor!wm*)#eX$^UdI$__tiY@B4SKs^d{ri1JMPjf^=iL+W z{Kd#lTTc#lYu(fR#o;9@7q60EJ2P;2=}lC%68q0x4YIjKb)U$@4}RB>iC8~5-!WTS zod2{zWDl6(m|tWWcM+seGryL?_2I+3!!>T?b5mHyPlzTLkSlVo4g^lNrOS?nno@iC z|Cx~a#O?^Xv!^Xq!~AL`)cARcS=g=hQ`UkBH}CDkKpwO!zoMtYUD#dln4rXC;8bDG zi#%U75bFJ?i(n5%!~pFda2oG!YQV@Qws$NsX>r)beEGBgOmA9<$nSrzQF5FVfGPu6$`? z$sFb5mi2I0rCm2uRpw)+$=brSY1x|9685&*rSX>Y-{Epz`?W55_VDLbFX)`0EW#cf z*UFMcJAc_NYrEYoTh1uzx5H>sqhU{E3@ICJwySHBwSJ8^LEKrcH%C5pqlV{84x*)l z?SHnXaMU(%=O!omY0VD)Uf*HW{aden{gmSTyb#5gUHlvaMoB_!7!->nPSKA5^K+xwh6LGZOrG(5zfgs)o#YDVPmcmQl~*WeiK@Ly0d^qG!xN`P z4@v6E;w328)H|0nGKx^%v38F583(jMfKix2ZE#Shg;6eI!jPpX&rD0J&b{cAt0fY5 zoT|KklsphDi?M)}0k99V1S;0zywJLSBns1mBfr`Mp?(lHp((UscYttOhmN5@_K!N2 zLr;PtbfU3dPN}%|_QM;_>8{@SYl=fqpDQY#KO}fa*FRihvdSSf^8T~$%0+ppCY0@- zZKX;9l&#EEF|lJyUQGx1TM#Km*w1J+Hv%fas-Or;R^aF(NUWOR>vu9&qvFGSVzMkj zP%}Xi&F!0*pI~!PGlL6O7!RM)zF)D5?WTRY&57$@<`92W=xB*!5+%WAzs@TDS7D(c zilfGlObPw~niwB1ZfxYj<;T(f&ecw7mySdF1K97#bMZ+wlITz~ZA+5M=<@P?kdR3Y zPDilipLM0h_NuDxe>Sp{U|@jp&^jsHqikTz>SB)1GX~)ifzK!)SqCJzp{OiX*I#BI zDBjJXjF|k#kRU-mjC%TiM)i+A!1ROiHhb?f@0qikYFSx3kyLdNt9Az!+P&c z`0pKkr1(!AMI9&(22o&`z?cC$4=r)02nvwA?8w5}ED94C)_W5yr0%I13Ifz3HHC|X z@Ss`<1%(TpsTjsjGT=pQpcXhS0#u>*dpBVOkjokoEzrdeWnyA`EKF^>M1&^5kivl` zFY)&QLI1}AS#7*`14z%4MtKgwD8ht+!5{O@Azdnh0O;wx$p9m9Knal7NEn#b|JgN_ zB?lY~5UI(jwTWU@Fxxn$CTJp}DS$W96@gphp zbK8B9f3qy}5Qm?kZj%W)Ev5x8IzDd(JhPURma4b9>>usKu>Tk$56r1)J;)Zr-Mtfz zL?bQyp$JkN_=an}P@$;-V2oJz5zG!+(9|)xO$fGx(u|$H_jIwG+W31#z=?TW1iYs5 z4<@U}_4;6fx&u9f{xLc(j(dS2HS}?4e7{1;A}__)4p7cqKCHDkb>Uyqu>|6W`GH+N zDq~5_&PRUv&6}YgMP??os=v&5Zu%eAx5TiuMJ?9o%ZG=D_m(MTu~B2~jem0{FH=ID z_t{tW5238ou^dVuuicwyi6=TF3OpvWpF;3%2HpqwyQud!MOmak&!IJ3 z!1G)ojagd@logFlKYP*rru1JI=l{;pV@S68OTWFoSOc#|vqck=AxW>X!1lj_WpqwM zfN~}x}eO+*k|?=b2RqbpU&GI`pVQx;uk${hXYD>?Z!g*P#Kv05+V!Rb$pzD*}Y^lFPU zwecEoG4h%UD-ljAB*&VAeAt;BdDU0OcCPAB`WXQcQo+FKP(0v zI*d3XPe;_pi`Ag+g@lU3untK`Z)nCoxEWmWP@dCSd3H@0hm*x>nHCx#kSN9rg)?d(0tLL*}&w z8R-G>1;Vl7;2jnXg+mp}juneTodXEtaQuyq!06SxZL#GWdo{gvwl;k)gqs*WJkeJW zQZ%ald#umxo7bg)ZQEf}bX6>ioV6hYs+OifwxnV4NQ$kU;d;9LW2kL9wd{t~y&3rS zVpH@j6ixWG@gJY zq+mk8!!|tkL)~)w^LhKo11|v4pUJnnrt+ z{p&$VXgVTh?HZwNH*^j?;DNvj_0o8blj)d@MG>#->zVu36i!2gEcW{dtUof$dVAvl z#n;ctSjcSd1XSrhYzU563C7r_P_5-1be}%;!L|d#dEYj6NoMDy{_0Aiu*@3*3){Cne&-daqVWqb-qm1MpVFIjZeVmrmSAUS$bpq992J#vd9Cq z4u8B~_d{rdbD-Ut;qIX#h$r{w;-4yA^(@=t7kBGXr(au))u#{Oj`w-y6(-1~hZAne z38igWB@n=7d+A{ACfb#n(;TuTHgyCk?>4mgS#ru9dK&kgu#oVjK_>N;!`o%tw&#rT z=Iz)R3R

FZN|XlE>dm{WigmZ^^1oasFtxTwj7Dx8v6apCY@o>bUf?0LFVRzzH6m z%&lB?&i&G(;s}}Ydf%XdVAvn)XFqAVvwKLacZYNW*=XT!Z|cJiH2zxC*JNHxSEfVv z>2aT3ha3?DjGnKz>#sa$m8tX=eEAbP_vp=eHv-gg^Msyl6{-$^V?0}^{DT-E z;gzuQ&*oZnxBZF`q4sqg#29)lwT%CXNRIMa%LCBVjTMZ^d_ipH;q)0dENs5UrD+Di zu^3${z~tpf2UR;zS<>Gn@v4~$IdQe4J<4xMmA1vnfy2dL1&i_1kE)Ih_uB?m7vJ4< z>P*7>*5Lx-%RfN~S5I)P;(1;t-_>zW8vCHlL;|EQ>JM1En|fg#ut^>KDX7%quIC~M z_-tW3JibUrHVCN^9R*)AYf1QBBoK)dK5u1|u0n1fO&5HQPOQCD57I4N{B%4~+rDir zWDgYK>saXPE+7|CmIU^Jh}EM3wxvlp1hv$htUX7xRZ>mkn3n;UW7jU3gK|_(E1hFa zp(4QAz6hIhYB)*k-kVr3&T%H?YS{3ia_lNb`mm~HxY+EJ0$+KIjp%Z~wo;tsd|{h! zYma_;_bZ+tI&m@TxgJ<#Vg-2HKQ;a#o4LHp+*k_AzMmURkvU!l@C~QeT3wZ+9BhBF z=0z1X{31J(YGbPYfFMC=u}XCG9M+NM47M0NR6I2N{2d1d@1xZBuw6c-m`VOa5_9^~ zQ3dRjxhmZT4I&8A7K!}APkctw)n_rns799H16yGzR+3&HLtuG(fB*jGk~0Fj%MIp- z{ioD4LNo4{I|AZizry>y1j@-;*eZDGTC;^UFu%>EvaA^a=??iegbl93O|Kpfe)(h@ z>^_v4KQZTnjRdyv@Mo@c*6hDPBqQWjfoq(qnAHBc_S*D-`Qobn(G!J9Rjv62XHH(W+qiS`_g2K zcqFYifUC-8Shud}mYy?o?uDxM2L-u#v@2$1KFhwXRF+B78$ap1zTMc+`H+7hEwf5F zAFr6Ga@>%%T5bUxqN>I{RQN!Pr!_v2nf9LsV4SLcT<^Zk@fvN5d%f3e#dHhhgNeYe z7n--m$;-dIsI`a$NuVeRzN592j)1bdZnUHrXtakx2|&@9?3{+sQUiwA(yViV4ZR*L z#UJB&UTwr;Ql+|w5^Dlb75bn*1W3yzuXGwVCBY+&wHo+d2ADOTHO;M!s6wrE0Np~ zvD(l<2y}}-#M-;d+TepRA4d;=Z2qx=*jN!Hppkw1~wgU~j@5yGT5X&#*% z>bzUE(B%a84GhE??N%uk_Yvn$+M>18id5X_Y-cXJ3a8_Z1H|5Q`GU2#ZSlbj)Hu7k z!=T&_Zzx0Q?;*dAwj;^4AO+m8R52*S$at}sNjsI`?|a(gqS52bgL{ID0pE1VHu-!u zkvBEk=Pym!gMKq4HlknP&3=ti)JkXywdoC+f|QhDvs4^W`I^%uIY-wNzbtPfZVH_u zlWGm8!*J3e60!CzLii<2g~GB19Fpvx3+E;RB3?@=P9$Z>0I`R}d?|LPMDo~OrCb5V zIeC&Lv97QC0axLPk1n-M!)evcF*&(Q`6cW9J8>2xS?#C=0c0oB{~oVNPnglTC=&*u zG2cBe)f3pIL8quZtV-3`$7vH=foq(I?W(XRmgL*0YtQQi^NaroQGdthUMo@j0NZf+35fbTo<9GfBjJ_Wj@B@nm)m3Ec#^dt zTz-cRPhV43G9Up+f*CrOK@q_hfGd7C!~pPsW%q*~a}sA@QpzC=Hp@=`s* zIDwzsUK3Pnt`7ln9rd#*v++@qVtDOQX9DG3FqFVJ#B*si<8*-0Q~f$A-oXq5K0`QW zDYsb$FrW7N<=WYb|DH%2bskI%erC`MQ#BI%JCLFsC}UrU0pKx(Kj%tD!~qD30Gh71#cQ{bZhBb>7LfAFFI4LPCuK z0Q^HPnoO)n)df^#)b5X5-){uSqMU(&f$~mM1%=o#qCpZtk1B*cR(g8rq-KEM6|SmY z%oHMKM)lKl2u)62zNomk7;8@vutCr2Hl%rgiUOuarg{_)P*L+r(Gu?M=pfZepdN5m zR+dJy!}_IN+PoY_6a|dO`I>Bbc{$q%Xjm7!apFT0~Fp(YWVP%rCh3?3Q+wzeR|vk zig(p7YbdV3j0tMU3GV1Mo2_1^873whotP-8s$wZ&Ec?<_(KpX*l9c!j8H!R2f^Haq zuhcAt85tQqH%QZfN5g(^=D#55(R$tcdc@LBl zn;SRD4KNt2@c|a2r7seFt~V4u4V{F|PGBDk1_tsEg4K3Nf*ueHm$~eZeruuBk^S`P z6JkDZ5+|W#NKha!V~>#_AzFlocZ8bVYNYeMF*AwNvmcdqFsLIqC-_nP84fCvJyT`J zvlRw_ut}h%>wBkAi%|_9^eek=DWit7_*wSZ#Cu5JO)by=-PB&vSdxtp+y24v4!uJq zmbZcfUtcdw0d)y6Fkr?hAqpdEA0E6)oLxQmaQQhSi`Hlnynq8nVGy{n|9`>18++rV zpFU=HY{Oyj`TbR-3s}s^R2rqg2C^zWUK~)w=JuskfrHpjWUQZo{*z%KH-QPeMrCi+ z!?=Wp1P&dQ6wCL-+Am(83Yzj!6$2-8dnNj4XuI7%e%IsV=4C}fj5LFO3JqfBzn0mECl{>&cZjP86|XH+YA56@$|=GIe~>f+@JuFI zUM{;q`8^f|L4^w3BtQ&Fvu4>6Y7`)2fpGGn+(ClMD3#N)nfMTH0N9&X!7b+I=Bm|S znyBh$)U3rRaxj(}EYVAfiy5FSxj;Dl$fX9!Z4v^&Ad)sh)!t;@a~4=Wvd08}5eW7b zhLoL3{bTq&PIWN-oPL&%1Nk8V_hSj4=aSbDv0=<5;02qKA3vDDKVGD7DWivVhufu@ zRHWXuk(!io0>SoO8{2$~UCdzsJ7JwcT>EaPBKZ!9fnn9g_E1bUltQ>r&itV@K|{>sIKW?$MAD6yGCl&&~fTncbi zD4^K}Bx!-^Q(ZorJ)(HuUS>TdEoDGDQ%}RAol77G$kwps_Brsur zws+i5ASX-uFMoJyilNxYx{qeQR1s>}rsYxtG!^>%S(9bi5|*0EB`axIq!+t?Qnqgw z?+a5gX5KyEnRWWK1dMl0E-ot0#spR;D{x&~M@K?(2{bq$V2}vPP}cJx|BkA6H=7W6 zUb5+VUb_B_J`*U-`Pa(=ZQ!~Sw0WUis##l{7nc-Q8n`9BrLHQuk*U6nNHCLB<@crO zny-32S6mOxY&Q7j>$f=No!+UCK3+UrTs2i=$hV$NU51;62c^2Ye}+QXZ949TpH#`X zYx1Pdh&78Vi8VB!XhLv;XE6$y#!T?3wn~=D-Zi75O^=9}3Xs|lnyi>LJq`K^>f^J| zZvM?JoVdo8KtAo(cvEc z?2ssF;bS-14^!A^4Fs2TP#+0vf(dTZ?i{k>)hT#SVMAw;K`Ym3X`>=QS-}FFA*NU{ zILoP>16=v0M+lDbdqj{!24WJVaW$FLcQx9F7Rx5b0Yn?`JylrI0*WYY=r+dV`9C9* z@}--qbK@Lb^E3qQpzNWILUumGc(2}UsB4_;$sHi2wU44esfuqj67G`m#F5nbk)O#V zl>8gtRaney5toN^=Wf$T;&?8Q60%+V9^Oz{5HlrgQbr+~VdC^;+;W+dT)X#8gPA|l z;KWCugiefe=tBLz9Ba)$igQwiT)$%CAwLosJJDFg#hYH{V^l*aGL672D=d)pzQJU_ zO@Mjd0|gny@kxN{WgU`fj6$4ppAmnDtqd$d>53Wfbto zzu7VHmQd)6FU6%rG?n5DQo{($)vLJeT5!R?68v|Tj@7a>j^&YwuDO}ysuk{YQxtgf z)hC9|6IY8c-=n9xKP3~r@5W{+O5S@0h@P^Z4R~!S?r-58k!zS8=GO&WL7J28PTX#7MvL(}r4$Fse$em;(Uka#knxcbkK@YPsv-&~Nh_&sd3!lA#@ zNZ$O7UUYJJr4K#$m#vgKbx`IJ6#E)6?LqeeIpC&Vosl(3IeYDEZD4!C>xh}#gB#)$ zzOCqTc}6dboXiic(!n|RxC#cv{E1X1`t zx2_)ZOB6AXmLoNX~Q`u&#r`g*HJ@P4D)U$rLyL$U(WpQ%Yn1tRi_iY3|HwrVkW ztVg5FwZ*`l6SXEAp2}FLjk1A(R?!|X{MUK;$AKmK#ZY+k+0is+YalCqvzs7V(MBrl zqe040cnUPCclz9FynJbPP|Ps!VbrYBQN=y`#};^O>PN@Ro6+_5>Z=)n**)9Ei!&v7 zzdo5OXiZX!^Y_2G0B0*rjR7w{D*zkmY_mJRzM)}(05Z@0c)kDt4`;&h4P*}UeA#Ge zY56}4qWiGGRU0ok9R01?Wa~r?&rMk~?kR(PxH6eqAYVYd5ME^9^!PH9r@Q|m)x!~m~mn(s3<&r9UXO)opY#_$*3e~N}*K-dZT zfhI)yU#Qa~4D=$wI#F0CM7$F-Xbp?iLqmaTQe<;%_+NNCw5>PPnADzqu5KRoxyvq2 zt8WffUri10_Bs2aK9&`xYO$9HxtHaOC4AMQE<}tPyzx?kT<3W4l(Q@`i`6%^pswIa zhVw&88J(HK*Zy%A$IMLZP{djD`ci%IR^Z8NuJ(1(xogN`r(!dBuAGX_o!i_0@hE-} zQ+|cZqJ9b16To*0v4h1%vo=~vOH1d9WmBwH8m(`hFS%!I_t|ue>)QfXz87? zkU%C}2JopMX=}aiqkwuiJ;Bi55dNZa^0cNgX%+$jo%*Lwf4T9b)WTl#g?)J@GCzjC zm*SI-zoe|rKz6qUa%sOG;swuD!qBuSqC0A+hrz3k-HMJ~k978HEi;wq{Kqrj#i85w z)Af!vd@o8?nrtiE9zCsDKzcLOyYmohEfzLP-8?+kII2FV{*O7UF`mgoUTN6kS+m_;Rs$xI^adnIhq04ai33>|qtvZpfT%;K z?X1t9#mnxlbQX1qFsc(>lhCf>5q#TJ<{j4<#q%;(Z2Oly()xinlA!3uB>z*N8?@qA zBxVB3*IG=?L3B2@E&oI5Zfr{zqgl`dq{%~juMaLvVlwkIsiIohg=A@-7z8vk)nd0I zp5V#HbS!|qI+C?v7I-y)f3p>eKD$^=Vg;d&<2c%M(ct=>wi>Z-)1Ll7D4dqQ@-PyG zu#k(iWJZFJH=3BoPUYfjqg7JTocsDEyk1Y(j=DloI-2`*{`SRsn|o2g=SZgd?J>BG zIV)(&FgT}&%WW>Ftsb?eRUNb^*nWb&iN}yT>_BB%S;jPqqpqR*ceYE5^KL4hyx}C@ zNjLGeB9aW_OEmr)Od0FbknGrDk;bSLe8J{NQsy99_C<=W*G#vY1w&L}M>fmX=* zeqh<-&2G^FR|wvQT}~C31IOpSeSTK<;e|!td6w+18~Sgi_Qbz}ovwSf{S7cdir|1B z_i^}h*v-FwJaJc69Ix(8OO0RZcgr{0ZO^tB)Jg*?y8?6ZH(c;U>-8{I{18MGzS;1V#*h?K zNuon4i$Y@36cbp2z?9UmTM~nnP*o97gMqTPWtL<;c39b&OUgNd7b^0Q$++_LE-iIXN9q9Rz z=LeL18|3Ap?YM8IREDJ=18Y#n7uSE-rw{l#l|ncgq^GneZFlU2>^0jQCO~SjI@&L6 z$XIh)Bd7ns$U9ycFE*Z+GviI_e|GQtttO;bVTIVG9VkcJ0jvFFE=`Q}RS>wdl)#CK ze1N?9@iFc!0=C8$Im{$43;KCK!%z-Lx-67afj&nrlmGM=kWMJpS&yU^-n8xHogo>i=|u$-(9}PYM~vAZ9W27}_j$*5GTRv##gZNvjMLJ~>a< z!O-sV^M-Y_)6QVi-|IscMM1RMejGl=R9N+2bqo>UD1sT!XeVT#;-fpFoBMIW4MEJ# zaDPGUZ~iFkX^)Z_hscI#=6iFL#6YY^JQPbv4MfSa&OKMMt4oE!Zv;`*_B|n~{A=`Uph;62%-p|!6U#jPXoaub(A>|Q5h(5tFoK&2V+KW_>ZOLX$smH(4 zUQlqb0q3vL3*VnBuxYYTNP(9JLfVu{A^7V*hhk*TZ${jc%QB-t^4va;B3VC)jQtLk z2OwxCXCQ({)MPrbtc$%L?F3}?5)9e(lp%gnn5p5fYRt?oXL)-VGtQ3gmZ-_D$}QL0 z-VKv!JgXf2i&i~p{7&;3`X4VW+KBpy;JsN&J90?^w;4Dk-*cqD2}6+bsAPS~l7`3H zl!%Bwd_ZM!?_?-uz>Cp9Ib~|ar^LYHDf<%Jg+DToT)>x1)RK1A9Kuctqqs!r+nRF6 zDJQ)$oN8v-MAQK>#kmiOG4F~3GL1(b;ZKvOuSHUET%#dMc)4E#;rT>jRykARxk>E~ za)BB%nnw)uFeJ-eD1S^5Hh^2+ThR6f5kuX=26WTpZoqp^yGCHLiRi;e!hj4R{>=o{ zppRX?w{Lml#LEb0mP5t?Pihbi~Nwyd_0{~F$MG2|RxzuONy z=`*Z!ss`w_N^03}u(sckr`#2{iob+719QZ{*ahWM& zGT-$#Ek7f_VcyoRK3mPM zyv)HIC^K2|#A_^tul;4*Dp@d#lG``PVylC{vf`GNNk>bPel+t_EQ2^bY3WANL=Mxu zmc3!W{@NAqcDgXu^*JiDMh-@ycV!8J&y|>8gi)&SR0(5uZj~pn=YljJ zuJno$a=QG-Qc35PnkJvK_^RZEq6ctE>AF9_YRB)ARKWXRV{fwIgB!ZNBY7;0<4}qm-)Y@@VkWutW&2f zs;6O97-U-E*U1&)BAtFgm}AL#P4=6a_Qs%d+rJAG)iCKU>@*VaYZuod3v(agdJ5(h zY5A@8BMqKna<KZzFzl#RLSiC+&?P zx?J<@f~GmmV#yCDP3>lPlP@lKTgXKi>_`G0I#CFojOOva|H=?5#^gA=IASG_Qy_mj zi$kIHKrc581Kd;@p1h%iiT3otow#2#Yf!xcQHY|~P3{s52fDk1ar+?0F4e^W0!G)I zIPto6r4+ijkd)n_1Zm`Q8lVhARv@6iF~O|28dhF%i*0sk1Vp~`Hpnh zAC2AQp8>s91LMWHb$2yHZW~p)AK<;rvodr(FlJION`N-YQy9+S*&C}IkqGWE!|c@iel_ykaFxa1kdlGBiucVMZyu%v&XWUTai_X&*!h__ zcD0pZ5Vej4*3}}F(f^BF*6-m3PkDH+{BFVC3V)%1B)^vz6Mnay9H!#PZT~ZJk@7gVow%1unN}m_I@{gK`d3w*zH+B2cY$ zWWcr)O_>~blbFFg8R~tt4Ms893Bej4mWn>qOIPg%5AunXg2@&qh} zMNxI+lnmz{OrEq=Por8`DHBh^KVMGI_XN<7%?4u$Im}0K z0NgsWRtTaOa%#CdFc38h#e>r_*90F%Xs~7Z3XTDV22X+!;#)!@KXrq2kPLi&Ecxm^ z5Q!l7ot5RZIBtpBx_8l>@i4PxD5m>mf|UhuRy4ZOJD)SD7}Yh++Gd7 zMVDit=pqEv3LF@G7KktvGFmlXb-@P{%rKWE?2U}Nn(4_~k7(o~Rr==q-Udz<1&#OX zp84@v4;l>Bq-NpE%`h=_amwk?RseCx(GT2)<6`{Qpl|jM;h@S7wpNiYakoJ(jY%AJ zs(-TKJDUsYxf#mJbJ2lJ0=p$L-=$SgwN4%j$4L=SvZyp2xzlyMJ$alSgTK^d67&#- zjmvf{o7yT-%a6+~@~zcNAB#`WhSOe2hJ5rO`mU3BwY_ZJ6^c*Fp}e)`tBOk$ zAdDG$3|7^V0Cca0Y{%?KIF$tgZ!1=2IuZ>+-$uJ3#J?i0%}lhjSqDvSSTM`^2b#iOpd4>Lmb7Oz29iZJ#5#QW6fO^CPp@L}eS4VO zWe=qf8e$7!Bgm^dkc$^Hgr0ppO_hu#?NJnyLsl^C7}!Gg$JzSeb8^9t^9j(Ylp3Mv z0Yo!8JR;9m7uY`?`9#JNnX*WXHqc&`vSJNg#*^b*dq5EjPRA;W?ELepF?%W5x>#si z9hI|J)ij0&P8|p6oR&*(ubsaT59m6E=of1u(a2J9bi$iglzP9u`K-~`U7zN9Zm6mv zvXAY+I%w?sKA$!3nic7o#zB#?u=m6bsSqZ!$oMI>Ew70Id^x{@LdMsEWD<0^Scas{ zN^N%Q)nE21FJmiaFy{WxcbZ1#c7^*6SIv;DAuLd7VNiU52NQ$r;>f}9*NZ@4`=jYF zqzTa;Tkg`rzEF5LgjB$d;R``wcnY|!Bd|W7MY4y;`pRC=^L+mG?WiaLN61&aN>t{+ z%cRWWD-RDbDhZzoLPeZ=gDWP53wb;@(p0~ub|Ny)Mb~ppde_#V3mQ(Z?C^uasZh*R zz@MFai<9s``&6PSl);k)%qU7-hz$j#gHaY&QEU1~*Fk_;-h7d9N3N9Y9cVu&zX)kB z$JOG1<+@5kLd&*=y+58KCAT-lHUPF5*tBjk@=Wn{OqnmwX^bFVeIrNycL+GJF-c>0 zBg1JL0W?Ls)V+Q-5pCZKVu%=Nd`DwAs+^sRVPN41>rm059WSO<8ATl=#hv)3lpZJ+ zx^ku)iRqXW zS>yFo2)OTp!Aphzy2Ab^~e*xnf^Sglob`ZkK{~}l0{i5)&7)V8*qSHz2|=|Yh+XC>VfS= z05RFaY#WrMXkumLHwEEn=-Uh8rH}c;3o+z6CsiV!fkxC5j@7aJwFGrAp7g{v1Gsyz zC*rzIpf((Lk6{%|H%(wvXSdj{*XiSq0%0jv(8!&}zZ_Zjkr{4dxGauLcm=+ubGISj zS6VYamrcMRz1_M};`}k)B#EpyYfWb$t$W7UNUQnH z?3dnHu0B$zEGTCkCiisMtoZDmCUkoa!|%kUmK?5sSpstM2R%CL=%-P&a>%?enHM|k zC%f3zttE?0>@)v}*fDU-*9MNl3%1(5(<&qtgEi)x*KHlGPw2XH! z*ATdI9Vm-FUeInI8y{@Fouu4FGAIO*&1OLL%XK-ZsC*O4p_SsR@Kw0KXdbR@$Moo~=(|75maaj3`?Jc`1{KI!EnqdV1ePdHwONbKDki)Wdy(s5eFZUAah zJFtuV?8!E3zUI`85Zxi8$8;T0d&z7n;gWT`MG8JU(C4tZR{E<^@m*syTik|KhsyFN zJ0brWJ&(%C7rsGF12%gBZx(-|zq*Y^A;0Mf**viLdCZVFC>?*3blD=#!))4SG#^%F zaV~5-O#d|IN_4|0us!^`@ViBaZzc9ljx_1ZY>9H|Z$d4COv+8&2J^UP<2wao{|euV zKhrPX1_BGa-MJ6>sNJ%sLcm?a5F{R*GSDQvHe(_cnOQ*x1cQonCbr{b;HROkGv$bI zg_uBxl%_~yUzAqUSAtx3Y=vk?CxVB)*GBwa+PSiScf*HBe=Koxj42U_+ngH@& zz2764fMS`THIh6)Cs^Cgbl|{sBAeTMqr+lfsGL^Cs_tnbt5~KrW zE_yRL;D5WG@-OZ3whkcsNhtBuhTVL0PuArFDZQE;*c$#zDRNZ+`~N*0X^bFzfUkOS zmhj?aLix4u&-PeIe2noYZ3P+>iUK4gkM1wbj?nN?Uj@WsnFx6Q(GjRGvVKLQ;wslI_nU1~ zfGp?oE+|$)goO2LJ`H99?|kFtu-z1w9hzF}{n6c6Ly0ef0FfdClau-v*D;NSfe~Yq z$9tCM5zm{Sc+pWMIGiRCa5Jb&4<5K2W_6GX0;U7KP2W@qb{dr7M=<8$Z6j5hYuMdl z4m3o;Dlb-8hw;K4m6(13e3Bl2S>_`@-w*0JjzUEdxgAfd-cRHTCMe`zg$mp_+w&NB z1ZkFk89=<=@n+ss)s+e=N4yQ0X_{@0&b6!y<_)1f$W{HAWE^<4Y`BTwkQHx~;~)5( zH-asI;gfdY_z`_nhxXj@Pazf6NhvMO9uJ1s zoOJB-B`>_64ny+j`2B zctC5Qs)17d>33j zP~5UNg0Twu-3jyc!DJeba?_%(CFKtP_2J_;u+P-CefOW&P|-J%o+mwXD+BMQ43+5~ z0=RbaTCd>1_MOrEI{%mFh&H_k3ONvX~c|Z2wqQf%FreRd7OMb^VO!yKISro=HrLO^4!V2uMNHYmvtw93{j% zA3(G6=8(5gxG+%?NccH6Y{Q^BmR+N(xLhwfcF6G}hJ7t^MI#vK!WK}>92*D;Iq$NX z%i4Im6JcjXfg(3SJa-XZ_3W{@w-Jgi1+lxoUH<&bhrfyo5A(}k@=i~*)j#vM?^wGY zr^tFf#OqBi(FJdwhwpT)$7m{@DBaa!@K-d_{ajFOy(cd?R{Svgs?rk~QDi zS?0HRraA&54Y#{}+5PFx`->RwvbjeS-Vv@JMLscDH-^ai1U)CII#PKo04KmAu4xW} z3~A#ickZJ*BPXtw(`Rhb!QtfxL6mvd3L1RMAw!8=hmZ^l)5YNe&Idd+7UgYno)PrM z8%^*$*yeWpi-N$?^vLI=Lrjj=$ssabf8N?HxT)1j8;f!Sno#s%uc@~`H7VfhbnJSc zDcQAW!XRDV{yn1$s!^EFBp)CTzu( zAX`y35Sua35ccRjrH(X;dH*HBRAV&!!k9b|U5y6AQavI6D?jnDdlS(*FscD2+t5nc zMaXY3#Ckl7&B9q5x!^R413Hj;U2#&1iNML^?g51K{Rksy!$&e%{@!M03}xwx7(FkY zXVbwY$KF%r@d>}wynid)Y`B#Sx)rQ^lNKJw9RC_IjqWK4iLUno=A zVGLWHk+Uu#t-tGZ98Q0bEkiV@kljG691e>TmBi>*)2cgo4cn~#pp;c3gB z&S%*8F{#suO-oPw1V1W1uX-G>xo(zXg73y%r=$eeu~nef8jXttYS$v~OfngHR~Ug@ zg2;!_3v#h|&DuW(ac~<}tGYj_NgKuVKAd&r)RC-u?>1bdy>7f2U4nZ z=_5UR_KYT_ug?%Sbdt;nffdCoY)cG-;op9Qv z76BI(3B5(TEoAf2M<12(>kBv`1?wXPdjH~ z=UIGX@>==co?kyLSts{(9il9n^Dc%TigzXR+-|=qzi&~IpD$mtU8-~%v+H3URardlqV!U>sqnqjdmo|0#_BhnbXNeQ=xJC>Z9`4 z&f{<_lfmIjhQW!|balqzGP+?V^scog6*gtHC%N;LxvyjPYs}5>C_a zgiK~k5hi1rl#qzzj^oPRxazVBU~S0KgYxIDNC*o7paEQ7B*4@OibU=-nWS*alk$0k z!VWNv5mp(+TGE!||2`zk0IV(~aU40Gk9MI=9Tku$yXm>Ws<3_GV7SnoVKV>#AOJ~3 zK~z}$k+s_HM9yE^3R$FJ=9qbE2HxJD4QYRfvxsX??*LT2mlUa`Jf!_UdWg02oX}6( z?BCk}ESrwqpt8C?zD&P#_9VExi19G!p?opQ44Cad|82L7{1>5{kw*ajobUnzVZ3k{ z?oaE(H7+W7TmbCYUZe+d?H29?s6BNIAkDd`4GBFK{gE@(C9r-awT54)w@|$7PM^CV zjXqr@2T$Rpz!g1o;c=`G7t5Jm3S>zwtM(t4?Z+dbN|nqfTtZ;>Xz|?gCW?jj`)h-I zxn`>*XYq7bmGU^oOOXtzr5N(t)|@;k>rb4Nv`LdWeF!I_AcRdOmoFQTEoWV$>UUT5 zNn+Q%|9*M9&pRrNL1%+2&0U^-PQhINPX|hoB1P1!bWlDWFi`tGQ^riv`^`6G)5eYR zu|VLH^ z7c5l3l{ITtse$~Hy!rCV>rkL$e!{Y4OJ(n#y*i%@BTTgP&1Lj@t=qJgTObv0KusMs z%)=#17E9aKZFGMM2TYwl4LNSFO{%i}Tc0;ur*IT4<2oO{?4}-nEn3S@ACU^zo}e0o#7%yi>Q6rUWSpb_-lnWi@85_AGkl7 z;KT0SyJg;-xw3BEI(2wbq)ee>r5cz@GK~oVq=}tpTyUUlIu!!B8L%-Y+gMtPm$Ylw zPMH)ktG0zgpojKgkz|H38qn8gq^l^nV8H^RTs_-#IU#TUrhCG3mH)90i@>yKpZwgV39Mxua) zy8+z#w7E}SY*r&0(DT1X%X13`oH=2Z!R26H3%0z`hJ;(-s)09i+z&^dlL5c(lb^To zWrVehv-l0CQi)=&Lc0LVZY^A8QyqFl85)1)4NiJ^asU=gmWDT_m3EbLC}<#iM3!X< zia@{o(_XMFh~dZ4N?QAYH}%hxrIZ3`MTQ_xD?lV?G)vpPJU3;N{BsCjogsTfwLd3# zk6~v7L@Yya9{?fQBLYX0%pdpH0r?WZ?E>!q64)Pu2bTFoTZAV~u3>{@uoxTd-HaJK za_YQ%`rAI4x&An=2bSe@BvYOoJ9<`XT*&GJy~vlIok?&eNtp(0%LRG=zT9$i zuGCS0#=W>o`;S;HD5Iyb-u|1O3n7y)-IYP^FBwyu#emnarF&(_--x?Bg`7FJPZeO| z0T)i2KqeQ2myBszGGLhj0`tZQ-Q0PS?Lg=x!$Q^a&JXPz28GXZS;J+(!%lrTe#4jI zVc6`EVXMKG64Ua5>a1qjkl?YG&~HsjrIgNh=89KfCgD>sG%vdy2Tw|~Pk&ZsjcSs| z;pld(SwfoJUQqTOKP_3)q>w)I|CK5K?oviI~ED6poGz8g2okgYoR&|k%o`T+mLvtiWpxBYcW|-oC4d)#*BQ>qzg{ zUe~ciDgECT|1N0}UN{p%5YKdaTIUb%sSb)3^IRM>{46PeRRcIkFTT`6ZoRpzUf1hL z@j4pP1pD>-Xl!l~?lYmxy9R1n)MLc&JTHudV& zQz4T|l`6#u3r+~QGMmntHA_LRDL(Q#2HXg0xd;)gI7!fk!;TzJ1dZAk?s)djC+1`k z*aicp;|iN(o4h$@22d>onqi8R&ZmUF7u@i;P=Gg{htC?`D}k~_2YBtgaN$DD*9{dw z_Vz>h^5s<&#|yShxP)PoojZ4qGjx*bl?*YBt?r*Ugqb1IWNNwCz&^1pG5MH0M=0i5 zLdNFR*w^Oc@-@hHYug{b1Jf=XKPq*q6tYqaXQ$Vu**NLk_gfkC-BJ}ap*V@=xZz)~ zw?LT*%r;3f72o5K;%kAvQ+p1 zD+ea&!a1yf`V<|V0h$r{Or>+Cl?O|v*AO@Vjevt{kjFXKm|W^!!{_ZgQxMk-mlR1Z zX(MBZDJeBs#zGP111OvKdh5W*&_m*bC|#%E!rb9Hz6<$3X4`i z+$Yb@cHl=r9z;~`$`vke)Ml<+l#q*$y+Vj-&sjTV$0-2d$YVNTiKiGk=bil85*7{j zhE4Ki>}jvPFbVuF!rr-4T|`Z~VwxAn%|O?j^+ynHctFC=BFD* zX@4H}Bnp~uJWhk0azi>@bwd`9W}X!QVDmC8Gms-1crrUI2h-Sj+I9&;jTDFRjM|J{ z69Cbd?#LwdZgM~*H-h=8s}4$^UydkqXvf+qoCh8(2G;NEh6SZ$wul%2Hs)O?{F6|; z>{e|%BAvgYlYCxIoI5XN3uTrsTUC}0W0uJLjeDh8r9$#TjgnI9%lUE=bLR~DJ74D1 z()5-B(x_BknTD6a;D0uwAFy};V3#jlgv_i{Njf8pa>}kf(x5;t`RJCCQv0`MvI!tB zB^M)LpQ*Q(ll-Yt#<0siB;i*B^JWE&J9O$OU0peCTF9F??}ULkOu;XZC~wuGr4xFB zpL6HVI+|B(-mgPI#<}JLb?W*;TiNFGSPbAZ!axf@vyie}V9=(0KV92r z=UuXRu{_nXm7Ibj%!Moe`{(iejK`Q5MyIDcYe*?KmM`e}(#t*x{8SOCBpp;v527ZW zN}Y3j;Jl3*1CYMaJ19pwvGa`WWxX#-@Ne6;Et-71ch;Nzz2lAfQxr6L0`}(L+>E&k z#<4QH4!}ajbI(2Jla^-ZL0OT;tNMj_!G)Gf~+h{2-vtXK*hE(Y?9%V!Cc!P zycye;849%d^XFIa!fWunHs0;Gv0ntyRx8iGODuS^^RU(#OB7iD*|UlN=3>+O_3INP z&=QQjt$)cXGGx3a<{rk$Ta%YEu@MoCk z#waOA(4=Xtn`K0=#<2o7FQ8lS=SKPZhov%S**e*|A0hSduYBQ2dL=R%=cHJ^Z1TXJ zH%Y7eD@dt=Io!VF`_*WHt8Kj-*AImSt}1xzHDjwRJ%|B_P&cHb`4}g!|4nehD88_y za1h~i7SEDg$^(=Y&XNM43OQ{I^Kv&j^;SB$4G!e7XW(< zz`BExl($=t9FP9qSu&ML;RA{h){P%FQ`bJIs{!Nf^Qn zH_I=(LC-^3lT4y_Q^tzY{!s1o2b|o`>Yc3K7b&g3-6EIH2PiW@KUlAoM20ZcPRL!DN`_?>CyYFntmLa8_0Hm`h8`xXeboR%f1OLJG=ERq?8K0D1@Ayc>Rk)9QbOFcaJD@HEQlKCr_uMhd2oxj1|Foc#LBFA5%b4F!t0mmCN^ zob}U>3f^Y_^pmtl4x5E?vEB8sg3S<3uGa6_y+`_e>=fW#v0{b3ygzvFeGNI}_TWQk zYx5^@tus;#%w48TnS4{n2i_2B*c8Bo_m~09O#yUe(n*adtJP*{aE9=&;a|IMGpipsH6yf#Hu7pXCtg^rLxT$}YbS*Q2ke_!p+ zg%MuEY)m$)2bjxH<-1vX?0hWrJ#^^MXo48QutNU#j2JP(r+mr&DOs{)G}TtKKSMhSy13X9+h+a1(9`M&%k}2l8JJ}0fJKWI#o1Sv z^QGCC;JanZmfF7tUM+N!EG|nY#5P`Rd~)HZcJ104itS&p+uJr{;O#p6j-l9GWMC-@ zE>Z+LKE3Bn>^!?1`(2G=j-5O$#U6i0_8&P3AJ6gM_>BE)SXX7hyo)n6pB{uF+>W3L z`{^Ql%i<6XTQ>B0Nr`uQ$E4TGf(FSd6_PesVQN$(6h2RReD;Ak5M2d*rY{p^%;f9V7z`(U>=Wn>>{i z%alZ(DxXEl1z^+s&Etf=ab-LDHwLsmUbsu0{flm%mOprNLUizdIPUM>2M0E9T1H_i zhzzvxa_uH=5a?bY>=Epd?i9tmCE*29rIY$aQ^?bI=9Cq?PRKj+_Q?KIxJUrj%8Mn{ zCUBB7AXjc|)8djE;w84(esBD=P3G^wzrbFeXAYp^D+gp5A|`v3F~udB)FNi+A;T8G zM%bbkc)BKV9xAMRS0k&6w^+F6!IS6Z@lk68QHKip{5OKWFyy{qgSEf zr{%<11t3Q}2aMbcKd#UmxMUbt zgX@-)k_f%1JLOmTtmZ9JHVg8+maUQTTX#vbqIu<&n~TUj^Onf&^Ji7y2Hus7C|yFz zrcWD6b}9Rt7!YI!|M6Nkl&}1$93ZS zz8D+x#^(ZS%CS#dAdi#4%pZLCGjR7K?3V?l8Ai>yPSF_V-x+922zWC9#hPdgCG;{> z2HvdLb*OD@=i{{r?!C;T&28qWF<01Z5U3yRJS-JKaLY08Wk_S|djoPdK6y=w!@T+C zo4UA^koyhAJWJ>py&C&ZU9?i__Z%rn0Hzq>bOF%p-tvWHM!$Bxg+^`XWu zYM8BKdIg{F0YsW|C?9-0&*(q)$*0RtDD!Edls205z5#>@QocdSh`br7l<5qMP;a4l z+5P_SKIt`TB_7VFl`=dA2B}H)BGRU2F*yk^N5#*#XRMT8Ht(0NwMt2YTk=WmA@gMq zRu9vH!9IxnOXo;0;~%XoEoUy1MY|75Q`Ca%a%&NZgpf=GYO}rl&w81#eUGF=&fFz3 ztBCC%TDq8&NB$CR?Ooe22ZPR7|BpY>P(un`aPzUdFeY=$-v8izWl^}1^=jUU$iCF5TFocp{W)@w?m_q@oi}6J=y*K}_UW%b|5E4WnP;Dsr=NXBzMuZR zJnMo%GFd;(oukePkF}L{I&pML$jBOz+Kt)V1^X9N(8DtUKOoc~VX*EZPZPpM! zT)zVZ0%iknaBJPRjl2kk#7YHt@689^5O&z~iAF&)Z#)NuL&&NTX!1PfeDZhw2kR-& zU|WnO<1w*ezSN+wvyEBlGzlJ@|l?R9hOD1YCfb0@V6!#2s3@H#ni<^YrU2*yf9 zWk<6>5XQD(>sE39P|P!)$NGr1`(A(jbq!@R5fk2*uB@BDR)B|v7Zgymg&>7YPh1YN zeaD<9ie>yS!JC1a7a`2^!3Q7s#sF_El;DkFlgxu(r~@a7$#3^67X`_)%p`JTDng5$BZ%R4?9l`smmAE{6o>^+BSo&{sKtGQ2y4<^bN z)0YFXoz)cm^H8K7@$zHRwDxVva*-M1^RE7jU&*xJ)N#!JCHOAzZOg`Y@>s^~OaI{^7!q1s6_T2Y7 z@Z^cg7gO+smj;y;FF@#{c)Cj{w*cW5*Qf+APb1uL#Q_#PccuM326q5wI(eu_GU-}1 zrdp@n!&0bWJ{zOVfR`?0{kbGEwsFid1GZQ9=?vWj^9LaA1FmjcSbL12eOXh6$pnN< zT8OQ^{92A%Cu@$Nm(HGs!$?t?sKo=y;S28lX7~|<8&Hhl$^bVuH54zqC9C&Kui^hF zc)P$54ysKe_bpe})RGfwlba5lkRzwhYUzNQrE|%E$0|te&*#a$Q)eU%z}O);<|P5% zCOmeBwD^9h{Ekq{FaXzVX;Vu6^bxY@WTfmtm|vO{0B&U62&|DLJsf#({>+;V1+SB_ z8_I=?7A}HZ{%j$ub^Q1-4Tt56gfiIQPMNIE*UyU zu3(VhVj}9N-Eyl_pUlQC+t3<0R}|Xd+@iQex9%@!q3nJieyD(>HiREO{Vb+xoP}dj z-SWPB98egDd1D*tEY3tYWo%(@)~0rC+A4t6b^*K*&;$x^yIkgt*P*?b8|Rf1x9hVO z9Baby8TWjbF4F0l&f1o^z?(7{upRUAXe*w}%V-b?KZo!twwd9d%n4+UTMhIL`-pup zW$JfQ3JQ4Kyf(6Xf92(#0D)ul`>yC8hRAZGtva-8ujlsmD;<2={EoZsM1IcWP}0m6 zuQO}oF%*beUR`x4U*d$pJ zet)IHCZ7VkOw~zm9kEb^P1=~^A_@UJp96Ley&Kw=J>EhbQWg8+`K6&EQC zcqN2+GuyMI2Mbzza~BOj5@gTD*o+TAd0vM>{b|=`oi;MMUXW$>Ca9x|Y%n0oyAQ)C znMcQhLbg#{yRkymk6^qpUx|sWSHdahv?S6X1!3v;%-Q=h{wD=uyaZN)7>s*fs*f zC&#W6M1lG-R~9zuIFw{Q&Y%(qff~aoFC|MY8Izt>QI$EHz#2iA!B`*r&30Ubbboe) z>5oErffSy0*uA_6nfoPsU`O5yqZeJR4+S>_dH_;9N4AugWDJBuSgf3z0jPK9?m+1T zCxp}|>pxdnc5THRGCnv)+~VR}$2>8SoS^jf3jvld_s8 z7oD}VfA_()y}4@6NgP<_lo^Q7G6Nq!cjwmoQ=tQVOvbYLxQ%#-v2vMf5KkxTwZM43P&tDGNXfKOFDvgnF%YCo zC)gj`_8>2etHECl85$#JieQUE7Y-Of(P=PA21d-OU{XGrH?kf4j3IrL!S}AT(>bB= z2^A8;i@{@OYrtmEKar$yusmW+9>#(H=dW}5q<9t zMU5|PlJ_`}qACbaGl$xKPlGc^;TLX2ixpG*P`rgUV|XgdbIh9kqn1YScXoLW_Ca3& zX_j4JJG;HuT_{9hY#KwqzQlJFuwdw|hH&!<;XVvll7YE}48aY?nGVG~^B*H#pu`+K z3U@GHi*2_pwy;XJ*+2muWFAutb_{eccAbF(2g-H!nYsV~AOJ~3K~%F~kpl@@Fiegt z$(3@6#}+n8@WurL{x;Cd=LXp~vT)uUG+Q}08?p@) z+F+P7#X2r0Y|`vb@JBG{d!M+^FZMw}gb*_voDO&>H!oOyW^oZIu) z7EPE>ICktzq7*OWC;%)gD92joH+}8VUjsL z9Cvr)X9tiMX!|(P7niX13w??Rr*7eCME;ycQDbjA=s~9eW9YB0e&EOnITi^5#CLx9 zFSyryIWp?KoQESE*BE6}X~XM%yK3O=rSG@OAA2!4&x0v&^NpNfLu1|?@WvFkuaWN* z3h>5rpF)mQGk~olZjKa1Dtuv+>`i5~;3T9*hg zH=fX`sOE5KnuCQx-FY##->s8JhArJEKW%lyHoR(w+*_R$w1c^F&)k(WAb@B$aigp~ z1k?sQa_(t)0S}XDVV4gSN+#X>YSvMC^YM|Z<=8n~N5d`&>JaLg5&|s%legzfBONPc zmBMbFH!HU=a``^_a>YpvnY`c@P4~SBT&6KE`i&wVRt~{`qMXD;CGa{7!BD%lcOPY5 zku&%5U72E(88F6k!ixP`SC7EefM@h`z}6T;pV!Z?A}uBgLz@f+`#Ss|#EPDA?!myJ zF?s|*sQ_LkHZLw&sdjP&w@|$7{@r|7p8I$)`Vc^zV~il4gNT41Pi`tmvWNK{Ln96fxgTFT#vqjO~-&YX$XkfonLZ@!Lm)(iV=@aGD)2)fu;EP_k$W<{sXxw5cH zQz@K6@CIkgu4((Ve6j`DzJ=NzO3J^`y}OECy#3Z&+7{MC`VRHe{GB1o+gXgz#U^g( zVr6w{KFf~ z#!n(dmI{X4_XU7AmO9V^w%E);rtk?vIR8QE0oH{31!1aZ0o*TOOtFR??>U$nW1*O5 zfgS_?PBQoT^Uptp;df*^m=|c9R|1}&@mOPzYNGbtPY}Zi1m0|1ko6%eWT9<52j>&V zQh@>m6j*rsKU9w`E@Ti`5*&HiZJwY1W?m%~8iN7R!GJeww^zu6%%9^NI`;5mlAM@iM()5k7n#bnUYD*5u|LW4K zr6c)oA(Qt#KUiij-{cnX27`vbX7z0?wJH`57`{kQb4R;RWa*k+c-3BT7&k7MvQ8~Z z^vvzw5ekyoHH6afiDMEA2Eqtrs&*VG+xBAN3!WiM)37u_hkE7Z({2saTr71^>$$-I zKNgOt7ehW?2Ps|1DMH`sopCaG(Z7gX3X{`VC?E4`BdJ}fgnl<`@fvB=dn}@~9MApS zMF+%=d!rei!2Bu-0lZ^QfWPJ_6+jNXHQF70I7dA?w0$ye5t-NoiT#L zGZ(_8WBrN{T%k>!_bBhJ3RF9npxwMCZE)^VQfX29RvFyAq0~ff;mXZ>L6*1*>K=)aO`!Mm-PH8xw~+Bd8kxIxfOti zo87=@v0k63mj!3V$xV9_U z619tS4J|W}6os)p3XxGSYlnqvGk%?{I*h+jV!#XDlzHO`k*u2^N|B?%TCxphGqR

S@lZ|+wr*;T@>=&T&_VKSQ~VMX77mzE?g) zE{_kqF;9iyZNdB)%$pay@tmO*KZ28xsln6D&@HM;`U9%oCWffVhrxkF+!Y z&Gu11h2ti+oVUVQ?Vl9~cr&M)c9;Zqf*Wh8u|{2-VUw4^ycu}&vI2Jf4p0jG5jE8~ zah^f$AC(=sg$leCMcCvIGk#FJQk;ePeY6{$+=-JXMNj6rxP*Fy zi>r^X^gN4ieEl!J@A27m`Cu>?}i4-^d!h-@o5V>QpYN%uiCFAu>`Wp6DY7j-CWK zqJW4<{u~)(_1GRzn2f$4Z1Z*8oIe4Ez5!ErE?P+=mKAvQ$?Ed@Q}+dZdoK)}Dt*WQ zs;r*_0-fhB`}eQmU8O|+?8+RLYW%Kj*$0OXx&!Kg!(SZVyOA`gURLkr))swb)z$-A zg#1F1l=4Q)>hem9TH20xN6eO&ht36gaZ3MvDJjx(+SQV7jjQ|YP_5Iavhbg6nwsNd zVd$qIooH&GCsEd*ls-o)wfRJrtw(K9_+-vNOfD6R=ak>Se9kv7H|{*B?L9}~C)WqQ zgvheFpR|)|<%&p|rtizDZ3o?R(D~6x?cA5H(1!!OrIJH4-wxW2*XoW}4ZQ6@=v`X? z52_*{t{#Nh-rO8{fC6#yUlCEd$t^+(`t~UfWu>l%7@2mDiO7|H%7RBWJ`V84dFEWH<*fH`uo6ZK_HJlm+c8X`K@*r5Tw(Z+#j+%u{(%~ZWmK;JHIz`=wb+F*A)sqODgy0Fc_MMRP1^~sE zL?h%%F$9R$=Fe-WZ}w3A`aZ^jEJI%iGfbH@*#Q=@s7p@g7;~nDXd*Ldi3a^0b@eJ8?R;eL$yVUhL9cevQyrENep=N z_7|tDAu4}O4#CvZ&A^yW_dKET}IplM5!-fr7E+iCNpz+BP z2;ETfgWF6fkllP$Y{;fVOl0Q7PT@JvoMwLcBKb;nBNG$vA|4a%=I{SQ+)x-Wup#Q-7DXA5I2;$VeVM7vO!AK*@y_!(ULqT)t&L`D^1IwV(R_*a=p27zP)- zr37w{ocrG8bo(`R%vA$#w8B@5_sWC~@S0B^a#SC6ctzj%SvYc^D&$Nq&)k_^vZsw& zv2dLJs8ZN=q-D@~8J~rlj!9n>#6Er+%O8+jCGa57(ai;JPM%d-DVpx0^s84ubIbw( zH)D?$Z9XQiL+Q|1ujnn;tT89Q4J!kjG|$|TRURw_*FF?KtgvcRCUdxa)}TnNXJc{p$40J&iXrYj z0OP2v84H@%&MB24uo7saV(1WrZpx`+F~Fa8E5i&hnMXG&0;NdD`M2$Z1v_Q(daMFj zO+MPq1#dj|A|61QAaXLfMX6|Y1cNZoW$lvKmz_B)-TMEIkbx5b)(`>W0~=uL9PTYW z?tj9owPeE2n`8`rnp7(+_uZ05ntiqiWd#tk4+iZF43HBH+_2`Ar+DDk039UDJMh!zPO$e3T5NZ!C7{D&L_d6`fzM?z0ce005*x9jV>ONm>i%*TV12 z0X7v(I_T|Mx6!ddr-$DY022JsX`cnK76}0zI+F~2orJJi%iD=-0w#8zUDo90^TZFx z5qz*-mzROFzvHSS=FNL!9WHb7tz8DJD1Pz~>JBCrz*%T3#V+X7ds#PL)4(Hhs3@Ty z?DZ}OyzzQ=elEN)gpZ3X{)MRR`Qx=oLYOyS-Vs7wsj%oDB5L>i+H0=~e}XBE5ZAb| z>rs`rEb?|uk(Zx)Lnc`~oB3*?VpP3t@aXNG~miMG6(fqoM)FiKQHIF za$uwW+OSI(!Oy{u$&)RetO3X%*sz7}uco8i%m;`%fT}o42{5#B;Ij`(hez&+rZQ>I z%zroUk+RJprlp}k6i_2jWAs^anTb%#s<#%7(Nr%(%`cs2jY#FTV6oOA^b*j8?J;sh z-)Hbk3nBOIpOG(W-r2ODSII*!4OeDO2@*h~Mm5XG=+~RdqQ5pt)y{(yfGX1tSX3&1 zRxo$n6eQl>`eRwP)(P=EhX}Ib`7+DjBf9G%IiE*w5C2Je4V|M!%+D)HC@tJy8W6u=Bj}=%RhQ`);9U$AchzU zb<_9?$T)(c#i}|r!_q)J7Uw!z z1X~d~LmJab@${GE&3kjBFtH=~9B4&7O88{EEQ0WfHv7?;;uyzMx0rZ_9eUb?u~1;N@r0H4eN zKDpB2A?BFXx2ZFX502rcHy_3W%_A}r^Qm-Ei=!vy1B{pB?Qtg;_-+Gpdz5*LIEViW z+c_6x{t@0+EE^JUJtybi>mXL5c6X(B^|YS0@}ZJCd6*PIu3r?hXvU1{W!j3XSoKPz zl}F0N4zS}Jr-^kc8UN0N-O;l)$fzH;BVW-eo51!RLQS^^s}@AD&XRI(j~_H#G7{r$ z?pt-_y=kjt)`}gHF?~wehX-b>yNk*r05fpa8d8uT9q@S^rLr3-4o{kr1-l8f;f z!-~0x1rYNp3VlC-&#e``MsE#o<+m2YKchGdH*>x% zCxk*SOO`BACN@|=<=b8CSgKU1lCn@<5Mzr3{EUkXm9g-0xd4vWvAIyAMh%&dXOcfaPHf1e^_iCe zj%|F}vytG8bsKF_#q7bp=QDX1l!N2iuEacx^MY65%e?h_%PvHuFsGkFAh#9EDNDdO zakH?=UMQ^n&gg{>D~0ew7UrHmu!G!vOW|lB+?YCtd4j?u!W=CU%Y;2{DVj$G;?%{$ zH80?rMRI47;jc7O*3X^;>@%-Oioc!&eJKJ-f|{jhZ@J=mIHltYiN@YKh$e4WEw@@-D0aOsyQIA0>q zRTw2#_L5f(yxDEB*4Kbv_QNCgcg!&Eiu0E&H#i^g^*!&ekmnIr{N zhj1hvEPY7FGu|$sGyua~m1Sd}c)|_&Z+#izI$o+u4!+zD*L!D) zhUnnU+IJClV>qe<5Tlfmv<>c=)!NA8^S>t^OTerU-q3{GSnCM_xFpHE3k1=@8^dmy zBbWrahTougx<9*@kZGIPmz|B(?R!s1+joCfAqU;?#i=vr<)&hpWzY+EORM)6$l|s8 z^>rO%jA{;58o#BNBt~2 z7&ZaWugQaWSwtZGa?*npQTRQun2WveSFKzn!@mAnK^YYYO=y7XWmT%)t?GhQDl`X( zV5=oc1@QQ+*|N%fln(G$n&5-{%dfgmvlh+eRjicT z_4$(9zHOU)G58CaJ7+E&;(huZg)dmMtV72R8rE1JHOn}lSkG-9^6yyZ?Z;WOq*MEj z+Qz*3@@g%*xHhxJ0XW?sA{=ueN+ukEdg3_%I?C`97@1RQZKt3>^TrO4N}VRPEQI~i zXNUrCw*P|p3$!GHf?d=@V_O2{ytM_BH4pd6tySw*@{(J~o3-%%{&R&s>|X8tnlwYW zBinE<3Yl-&yhZ2M#{;4ROuIG(Sh_yf#kn>Z)%m!#39#5be>ZuGj6p~p<@5=*=|qxY ztzEmeumJn(uf8Tzz!Y(_dUEDOE(iNDbjT1{wse_-Y>w-29M2H4s@1A$L2uRv3>Jx)K!fLJ z{u>t^jKPcx5L%nP&OSjT!!XHk`3qT?d_6-v$pR(>Bqj#Dv7L4;hGestHpNm5zRd2=66{N2f$qeu#YBU z{GMC&oWK6PS*mmzq=X4|B;ajIpLVse$h-^@Li+&tC^nKr>QpQu(?4vZ$4p(cO6tGt zuzRN_AF24ZT&7+AjaYNm{V6E zrxVV49`>sN2J^9+QEIZm&Zlc_zpD=3%pmhO9F@^44hx7KrHu&^RGHCO7X=9zc=$jS z@xf??5!7%s5Q&hKRaIvl%uhIg1J!vhvS>C$H8`+*37hV4R%Qagh+EN&Nu+tX%m~p- ztN)G*xEagvc)D;p z5);*-!=1=(2gvEm#{I1dMxM==Ul<*aIeXwZU6*nna#4px?ab@rafIh-3VEwWHo2!* z^diy;o#df#D(wZ(d&n~R?cbwlpd;c!dz}P;>(=ZRBva>;f41P^UNDnP`fZE!o$?RD z*pk7PxMw^_$;iFg;8wZ2cy?I{@Kz2AltUJ8ltI65fMhS&agIf*O@eiMtYAiYzI>55 zb3(l#J`4xivwIIh=+Nf?P6S3)eA~Mp>tt=jYC3I9&{pcEQZXD^I`{v=!F5=fOJH6~ z1XLU3d93aho!H#SRk6)`O)BW@y;W44&C)-LTW|sdhoHeFKyU~IcL=V*b#R#A?hqs- zxJ!b&4em||Ft|&?;O?AHgpLG%^i4+BH}1gRqFA!}*s@Fw{A@<^e73>o>#d4i+5UJgSnyLt zkvrK@Y-zXe8pSnxZ}*C(ltmiFa}8V9tTo!=%cv`r`oEG%N}_AtK)eW>!;#{R0>*sj z92cV3I0Q~N=fp6Lw~1JtHroN;oQKlm4o}c`N1N; z-ieLtv~ENyrlS3o#jYv5Wt_}{CIyMMZ^Ej67Ll%S3U<>$!(yF7|Mh2O`JdYVi3N~U zJn%7+j=ifX148}T-b&50n3(SFHh*tz{?vz&S=44PG8^nL~YV(!(Aj4Tbr1_JP z>Ztb%t*YJyYm9Vl3WTe3CvNE^$v&C<+h(Qn^}&(YrT*n9lsa`43bU4j2% za|irZNYtbkF+N)FU`|ruITa#b7*>`3YCA%I`f$ohC_G{pk~`9fvqNs0m9xUN!)IpIh^ZQ^JttxR@-;Y1!1@_Sl@u~FI?q_r=E8Mo^Ynh*l7fj+sj_; z?JBQGZ=^>^r+yPsR{QYmBnrr-Zx~k=ZVyzfy(*vMZYU`nSJ+Wp4Z^ag6L&1FhK4 zYK4#Jf}r<*wmi^ELh_gJhTuj0FBOm2sN(ghLGea(a$N{hmPpaYZr(b2(9*3JlCcON zI!3oy8sCskXlNI%oXt~bId=(+RvU~&|9A>T@~Y+Rt$js)#z&r{3yT{PYgM7v#OLtB zh*m8O%T*dDP=$|OWIF`$yM^RYv%lif8gu$8{V=u8n2r!YjM86VfB4qLKl0qgGuKVD z>Sg-7$W%RIQKwiB0=~i7Gmm=#gbv>QqDU!v7 zn_Fxy48mV`e4S11D)4xxeUkXwEBLA`N7GNWtMj;aUohe?nC|CAHuk!5wW1==zP0ml z%M6l3HNe+gO5muk$%nUnEk4^x*+5!`X`sO!C$XF4~!s&#&6>c^zbc@^PhEyDi=}L0)rO!5tK=z zQcOBCy5K*`TYmc_FqIo{N2A+t#@S7W+YtTG{iY*b_(#Ejvl&$dH3=?9WlD?{@3%W7 z`F$Kq-fI^J(otullrH}#mIQr#Od$fr*2-{Sw7nInHs+nPdS^+R^5vbfFTYIj+=8qe zCEH1vXMj@&YXWn74jl;KsUay7la~2rk|7{>pT3vo?2!FB`Mdi<%n+ykt9M@5%sEIN z&Rn{d*$K2V7L26xkvA@f0btBCt=9Iv4D{rLpHW_h=$^V;(2I2y#;w=o%B$bQ(mvG{Tr?4SG+&eZD5k%#tQpBfWYD-kNY zp0D;xs6mVarnKw}v{=ZdS#~*f^S+aAH`WMyA24jm@-tt`k^~rZD#x$<7)X<6kjQ3F zrX_cEs~nigI(uBmD)L zg@IccI@3iXGOPC%`Dpf?x$2gl7ASroe~r&V8Khn;!hZW1TMYBg^}G2aSsXJ2WSG6i zw7Z(d*tI6ese$%Y0rBt6CtX_A-^}%z!{7MyRx+M0Hut7~qm7Y=siPi;SCa(gMtyGD z3RT}cVA@Dw!y@1G3K-LZYcg*9FdRrQE+2%daQ)|0=j59)V%wThddN|5r{;oRxLpf` zt-||UR9HQzdMI}3XHE0#0=Qw(s_}u1gM^xk<2P_vuNk-J$h~j26g86)M^2BpiJIKc^CS`XkGtjEPN$XvsL1M~ z@7M~tPtU@(^eXTA&3J6+8xs@J7WZm0T>AC}vbNuq~b1V+<8Te7d^QN4fqZvXolN~RD@YLP$ImM?2ZeZA03 zWscx$Iv!gR9taA}+%>D6rU`;KQLHP&rJ(u^P~0fq=R;r6~w=5tABWO9^kl z@O|Nnd0t?j9D1%uR47qUz{fI6Abj^>+S zvsN5^H0sF>9h1XPoST7nocrl(Uokmh@TUfRvx@Y;Zv!uBnCgy|;t+v^!D=r=>QBT` z5_5RkPtgFYKQeXmsnm>Zb)llzhREm=-dIPcqftrI`h0aibNPOZi~BR5#Z_hoKeCCU z?^+tO>o9J>_24qS`Nkq!{s+b?v8+RWU7*JOu@V2v1f9?Wn$6x&mls{mS*S%mH*m=u zN69=o`wV1%2y~o#sDb1j9H?>R;Z0;O$r3#IE*@q}@= zncP^{KSiBw=C`>q3C3(b6~_h)?^Axm+N_CGHi5Pmt=#k-d7Jl=DxPoR_EATV5Qs+< zIrSGzbr|^#A*sc+NNNpB%tlT-b^kf18;GM;Y?L@jo+`9;jA5J&*_1! z|4>u=t*Zhtf%|emspfcMxrr+3*d#YN^qp>gJXaZ#HIg%CrY3H9cqW1Xr#bT!0XXIVw-+%xH*(9_m^A14TvM(Cf9R^P9r(! z#-SUsm9W4{SUzwLHBp8(`#e9 zJWlY$o5k9dM#C5jB6ys5vRoZW4=Iylw=rc4NXcTku{MlP)ed|OUwXu$kq+d4U>LjG z?@=VgNK}9Npa>Cp2@hDwF;#Nod|9v2+FxA6)O9mYim#2WcgXX8QtvY#`{a`uX^!&k=ly(G%KSpH^N~|sfR_~$sD^IrVv@a z1Ht{R_#YFJPYNs(L;ETV^0A9iKY`3%xRf2LCIy?tdPL%OJ-@QUIGo^F4b5iF5J&nD z=It6Nj;p=T)7h!(>5VVXPmlgkrgbzN9izYbRf27S~KM@e&QG_);qEC*H9=s1<9t1pm;5!3PmBFQSL0Tk_m#NiU@P+nG^ z_AgM?g==}pKBR0dD>$LXYsT-!^#Fbz0(t+#-8eMgKK<8%Fpsz=QHC11s3U5=BK zmDG=3B3+m9gw zL_TT;a>ca!i2H|4d3QXnL;Edn7JK3F2gC;8?UeATwMtZJBu0xaP!$q5-=@fCJhGbF zn^8ABUhVL!cNPGd5F}zJR7Yz~b|(ETsM62Qp5~}|vq<*0AS427>@RutkI&d~Op4tjf#l0v>zolI(k2tEN$N~ zr~MF@eT-B@XKl=WozT36Vs4>p&R}8wYF!#>J%~D}33rf_bM#Rxo9E2;ohvxyZuD%) zsx|x0#oRK>d*W1dceE}0?jh?4s&XUDc!gH7xOLAjn&6$1##tz&kME&h`n@{O&_kB7 z*~eVrG5+ZO&`|d@7rY3%46Kj1HAEc;@kyvC&lT%f#!+TCf9uR~^kD1nGfYuFf7Ptj zU|;(6r)|>iL=%NRcDd!JrZCU9U9v5yK90*uFXf>v)<^WM%42OqokZI&;jiG$Qs-5W z;TugK!iYxRL)p3A{;0M#+Tl=1yq%h<)`X_6m{rMOdXM2E(4q)dZA(XCPBv+sB4IjJD=5@vl#^TJLzLkii+Hb>T%DG;X}nAn|Q-eK3LwkvZw#H zpk+Lf1omFAv8Z7ss@R3&rAeN8PpLC5X_T^-_^_F|Mr>>m1p8!jb=J6a#Q&Y# zE&O}PDy|sXcI7OGojcEVrfauV%-uHs!ahl#&;4xt&Is*9+sdBbd}0(Tdpw5-r;#)% zwC8V!m&D_0kH3vvi^C_(PUPB^eb2l3Ue|4Pf-2;J5|9|Rzkb8`p=WL0ue#z6bVrv- zT%JmY>`~E?a-ps5ZM(7&dhEORgVVKMm-Q~wR+sf1cYDb)3sU1DY13yZqs_|L`~rIr z|7dByGfLVOy;F!yfFNvr<}`dn6s99_dnI({xA!u}YveA$1T~Q%$9tFI-Y=H*vD4m> z_vXo-8IhJ}cq~1on0>+c*6pteL1H}klF0Yv|O+)4BC=7;T^ zHX*_oFO8hpk4-bkv?SW5^#+Ta;(yQZl|E;HCH(M@&%&BSaDE&VCd7ryH>%*u|1MT( zym@f}->YQ5PYANDR_7N+!$@{MY~H=~G17tEg$E& zVv+ok)^EAMF@WroSW;Hkgx4(z8PY}o@^9}PET;#_-hNHwym1z!193WewD$#w&IEIJ zHlpZpHPJ>xa8}p5pCP+at$p?BCVLAt{;__>5cgBQ&S&?d*CUk8YL|yIiIja4 z&MQ}xyh9aAbiZel;Yq7{q9kY?h?bmFGn_{`t&R*7e!`PgyDV^w=^O0__FA^8r^|rr z<%XC0q(*!WvrD%|k3=muG3F*tOiY%jwMmHkAMTE5KtPg%UW==`x4&9q(>aplO~*aQ zYiJ%WuC34b{_=cgew|w{09w)wA1X_4|Dj0Hx>!+B76leP_peUZW zT2Ct$ly@3+neSS)%%)~H5Dg2oV2iF8u=OaZeo-0o)4pX zrn`o?Mc8|UC`lfeZyLBsEWmtLhePoSo zth+-&b;y{t(wnMZI_K@qiyC;PWcZ(tppD<|-8P_Qt(*6upuvZZo8rjc*oUO@6rp?w zWz%1S`N~zwgyxV2BT?V1GG6wyK?>A_@HSV(t3!4%o*S4d(QPy}d)3s}wB2|2Z6&-% ztCi%wbulFFUejRkc8u|`KJ3YA*dFWEW{XXOpvn7AQU9wZNVech5Lhf^{SXdeHUo6o z>RYwERAo<%4Pj$W@)hJ8sch$3imI*@yGSXk;C?+;5bcBrIKjowEYZO_S>(0Z`@d%j z2&}8itj^cQ*z=oob!E{Dyfd3|PSA{kBI0F{1C~r8N+Ow(ykrAS0={B-w|yZ#*h*4q zP(%C87PS=axvUVn+7*i5rHbo>AXjgC@jgT@q%u4rAk+d#?d&pV8a@=sn8oB|rwJb_ z+9KC>=CJqYm4uFDM1dqEP@?xkId3O+S*0vumD76n;B{~{26vNPHwAv=Ou{wU`Xx~1 z@EC2}Ey|2-nvK8#ydFxtPi*krG<2G&!~~%$>_qHhTEpR7y=l0?)33-ob9*tv4&?oj z?&&guwgXvR69)8>?SnQ4X?GG0aP;p2u)^=$_kNe#zJ;}NB+e^K^$Y|0u^%lC+e6g% z-An&4L#bwT$Av~AgtiyN6Xthn#2}AUhVk2O*#H=8O#Zm2RT>;r_b#$Dfb8r^vE+eR zTRuEbFdY?|9!9N(#kPK-k8SVG#LH0v8&$5SiWf3;eC4WcU}swg#}|n}PJhXxNuhOgjJ#>7X< z0(xp2LnCerBikxVr%dih>!hN=phCFP2x^3AoFjxe=pzaIVR}4_@tk25H5cfUakWuxQxvwy`ox8g{1SW zGs*yKm?ex<5F*i?GU+XQd1R?SA{bcxz_(A2+Z=E9PtvCXOEesE-pwhh@`XS_kp_zm zSw!06QZGJu`PFue?LSb}c=F#2)#q>TZ-!ev2r#-cvv$#opY-2+xk93D--{VK81Dcj zuU0PDCMtiE*JyM8YCi^2H(2OGVd%g)VXE0Gvp&aS%}3Da?Y|wYNmoh;c4R24ArMe{H4*!VYu|iKs=7v> zhZjJkJwaB7-7ZFpd=W_2rHC%7F@pqykB$OqWvT79%`D9e9n@H2zLfmf8#8$zO|@FL!8}@tgkojU&cJ zzA{8?-r6aZD?pl<(;w3_{PGsyADDlB>tioh#%H)pKb|)CpeTS*&$>w`1eZ2bR^W#c zrkGY-%&F&Mj`0Bj1TmY)vWf%7wxd&91UwOZaWq%En@THwk|Ho$7KxMx{3wp#->ZX2 z*lboD=tBk~9j-8ZRO1>+Eed{wLIA&zGVZ@u^KpzWA~1!{*k8ntk(gG3tVT&?12$q^ z1CVMI18~0TQo?CbVF3S-`^wUTdj$AJjWAqU7T`SFB&QBs16m?aeCPcF2PN5sLq}2p z*0@)HSs;=%`1v&}!J4E+3TnVico5KCrm)n^QEs3UtT=F`jPNV_?@<5Epe`OuEV@qj zegScWE`l7d7eMYzYIwbs=hq`h*Q%-8@Fo|z)Sd1`QKpuRwTb|Z5yZ|tIbaau@c$2k zNZ|)w4|2ncYFdKfbbOi6oH9c{eV-p<1!#<%!c~2K<`Xc^=Uw!FL6J?ER8^C?bDdm5>9`2Q0N5H|GZSH1;M zJyL)NQLEC3?ODY$^nS;nlt$;d*W)lBIWIDZKJ^QtL?sp`i+h?F5AP)<6Txwt1BLh4&30= zExv3stauV0Y0I%RdZ6^F!Zea!7BV2zbpsB|)u|Bf&i|TG=~T}FHR(lkt4snlwY5hy zR7B{3DwpkTtjilw8*Fz3)T8`DXpKOUg9UKhH@tAl&m|OJtg?Nzw954?J{Z)IsqjfF zL4T~UR(lY!8@#f?nBBj>IYa(*&`Y2abMH5qBXNoHM?3jOgo6z8w0WGfvQ{xxpZlst zu|D#`JO$YGpS?CM)-wcL^N1SUVOn=2MyJ{zKlYU!2fbjgx++jg=aT~p*U|2>8MaKX z6g&MH`Ot{FRH^z1IA+)+!!YGk!gb{J&{ZLYgjT1a>+AIZ3a&={MRd;N@6-oApxF6I zitq}g=`}EjHaV$UI#{qt1?1r@ivMV9ikJo{$rNGBRlqcI=z@7BID&Lzeq@nqO#mAd zhOekKNj1x)@V`H~+%CHNB&JthYc|09QKX2sRJtazQNOZoRPVUlGU#em zCGzR-0gd>{ETydtgy9!rw&+rKCjMd4XHiP)Z14`r9S^n+8-zVYXUj>*`~GC0983Dxjj|RmpkOfGCUn=$Kx#&$M;q5 zl9u*|44hJ}tHb5(_PfK{Gg2a$&=EvKI+oO8x3L0!RKJYiDwY=MyWRz-OOwa`T(v`Q z%51g$Jhwj4_&kt!7g-F3ci>==S6{YquwOgj`1{iOBA?Q|GG@l=$h`^Wkez?YNiyWc zxNey+8b*rg<$y%|7-Z?ljGYtv-o+`HJz&oMNXDp!KU#bijVb0eV^t2%*J(GNQPtA zzaCo-3Ml8VS^*||%(3Srs&!-IQ}ad?3(^Gmv+;=|OyF#khxQ7p^BNCI>9rQvz|Je4 zql6o`ryF)RaecDs>AH-W^6BxL@=3}zms`0Vpzu0%``TAfy`@y)<+BL%I4e#w-8NO5OQ7UPq6BeuF(;nLrsl z^vD?2nWce76N585q2hN+01HB(na`Jr=kr$k6(@yL$*k#I+p?XaZw_epT5cPH(3U^- zlYjM9r}2&i+XCP3+qRx~!Xu0krZ=T-z=@qp>NfMkk{R6MjL3J~QzB{PE{%$akjf0( zWK7vbk2?`P*kElRpoG9pPR0z}v#i}l;pK_VM~cY~n#ztx!R21{3|0fHHwZDV&3dCt z#A%VXJu3~54|k38c393uSC9(B)`f3b%+qddKXQIw@ORmM!W#Ckffhgf%0aJgwq z$;^Y|$azbCbBd7gC<8vI#c7$J#}+TL;TJEQFtV}vcaTTL3p5nHt7r*>4=)Du5eDn8 z_YAHo&?8>%Q+Hd5=quARi^)~(UROPm2 z2uuR{b>+$AuOuf|eTI#pQ6)(Rs$JS$o-wpwT+UhubvHA6|wxqYPcdendKJIXuU06y2`SG zZ@BCGW|N&vyLT^|SQu_?j1j34rlmJH5({JH$%kDT5rK?j?_PHwiA;dNx12BRI)MyS z?dixOJjoc1L>0B?@#T7Ds@$v}9g$9@>cM@aJ7P=8AI-tpyZ0;nZC5?k9Jezgl1ES- zXjMxW@^29%-;0khAnBlZ-mj$#fxdKqEfOJu7Z(%$kG1zZ{2kSfdR`G9J>ztKF|>6+ z8(&wGmV-7PBKZF`#L-vZ5y~U@TN@Cg`C~W$@n>%Q9X3vU(2sAYBqckzF5VwNWA>r+%p9wJ5hs{7M zb}@MQyqW!rT&ag?>Gv((=v-$8+g>>WXGMxHBuUeGq=$<8O_oD zYW`i8f(?leTLzuVhM)qzPMQiJbMK?r{xvL{#^c04e~Ysn2|Ah7&fDM8z8TzT8lMs> zJ2X1b0juPsA@#tdpLb^o; zSl3qvFycg-`Bxz3y*S(q_?8iS;WIk8^OjF|0n=#A|M9v)^R$;xLN8ZDNBS&4BhqecFM)S3kFd zPI2|m07v8on?`}zGy%(8P*w%b$6M*iqgcg}i?m#K=RW-4>R~89b3|u2?Fv;QEA3e8 zUXM?0j{>~TY~IZY4Dl-&xT0-??N@uMk7s&sLHuRFPuQ}1(&L_XM~l8mm=1j0sUugP zjnngK*v~cZt(yl&JM=mInvFxNV9Q$VhP$hLa&JvgVItyZ-;1jbbA)86Qo|#EFE9z4 zd=r`4)SVCUm|l_hu}SP%(akU7hvWU@&(=0+h#=}VFUP09xw>A3s*vr@?|UJ$vPp<2 zm{sLdKAMg%MLIUHprzS$VN$cn2-mE$lr1{jFiGmm)v*i#4eA}Qf^hL?*{TpGord(W z$(-7S5Lw*Q0P~(l3LRHs&@2j!5nrr>Dy$7r8_Lfty&KB|x0HkvSDaz6`*!WEaD35t#bKBq z$!}=PGy=;l@`)|PT4Ooh22Ct*&ph!bjuQv{0I2}k};&>f7QCGTcpQ<#It1ewx+n|lHsk&5$lAX zjj6F!Dk*%&6?Yw~9fqTUW4BS0;|GPQOj*bc8_sU7kFUVbD?P}xz|;L1Pv{R5*g7!N zlMEP#x%FPq4`mB8_1v>Jw>7IU^&eL+gjrf93J387El0mEq!y04B zw-{uwxl-ypGWlEUHZUDbouYMtRPl;g!n$$7r?MOc3N@BT2Fs1i1_U zbU=p5LRV2+6(@X`DF=)%wP38JTOP3G%V;P3b}<1sz{Qej?h67#r@K~X@JPirCZMA~ z<1YmPfcj5(yjEj(BN4#gdjE>Cfx8319k#Eot1o~@Fjlet$nsGTVE5dJ_}Tly;{q9v z;7}3_2f+Xt0Wm3i6X1~`OMrwqu}cJG=w|_{wn=Z6+Hh2Px|!#g#b;SKEp4% z44vV_&+rTmGB6lbOdg?Bi)VWh0D)AV2k|eE=>KLAb&%)RDd@+!+|Tfg={+I zOa*xU3<4!R^XXqW)c=`JaBTlS8=j%G|MzT=i*D@im!5BSEe5u~Aqoy9xz7s#-Fx{C z-Y|rakWk&gz}|atdb-zLYi)D<(Bj!Vw*=QVI6Y&CyG^ywRQk1w+QdZsyL}x^&6o9f zA3lEkIED+VudmmFV3EFMsx7y0pi>P%0=&qynS$<#r>CcU8?mr5IMj5*VNzhll;v9W z(YyxWw7f1$?ON$hLo2z+MhYR1!OlR$+F2(pMTF4zCI<`kQ`KezCC@EYrBfdU`GA)8 z@o+F4MBoc&4ENo42ZTdG1*Vk-hee`l$a-##|I;JanA`l}D;Q&-4>jD?AW*LyTP z#>Ny7e$A#HdPcul`B_?8?p0b@+1T}xdac2J{&hN^Lyh;E(^_s4D*6|3H-wg^s)2F< zt(eM}!C_g#Eh{K1D;vw=2e#d7d}}ITpqI{@AL+h4OF47v6NBexVldVeC&}0kxFB3B zRlllsiO%&G?8(=uz#j&PAyn0vW|K*gwIA1}wO`PqK~u$2grku@;Q`WS=^sd}IK-Vu z8$tq0BLr2ouXo|61crs7+=;Y%eSt&2riIH5tDKrrw{t#oe77xlHjrSIATA7u$qcH0 z$S>Kt@lSZn>L+0Y)N99uIH}i0I4#O9i3y#DNK5>*$7)()77vu#e*{{>05Oy#ODzEW z!O+RZJl!NE@a-VC-|qR@P+4u$#u&?_W~W*eJwLUu&=S&H?k^16aTceu?xFq{EK6H+ z-uTBV;?UFzjtZWqeJ=*|Yx++2oo_EEXag?^j$6keygDQLZ%@Kj)gbl$;$9ZlXn!Ql zU!Uw4g4SvKD3iqZkTwobofLL`S>0z@X&}JlcMDzagLlxb4XqaP_#Bfe2s`cY*04BT zg9p=2!vk8^mNLj!j?Ri@?BAdR{P>@bcm;k$1V5qOVHQ8MjYV6i(<4E=0ln!^A9zqH zAiQ{B{y-;=@V}oMCF(?G@!!bQ@|4{fr7&$j&-il2z)}3p5uAbr@o6n3f;GK$?nTmB z>HoH&Q0GedoAn*5>?%pG$z7Dtuk-Cy4SyLkZ@`JC65+69R|K}6f zO{-exu*jFbivx4mLl*@jX1Z66Qa{Cg)53Syq*)|04oHH6t?<^I2Yqf^ZR=mZzS zb(a?vNduLVv~vtC8fB?nk$|$X9ACeF)$05!+GL)OZc$MWxC&$g{_~-xLVF49c3yry zKE0C$vjJ=pP7`s_x^xG+zzl|C06~-k+}7n@`7m>396~ag|BehSN&R}o*3PbP%O^=9 z7-d^&b6ypH@FN0HOCdoyQ&0{J7ULrB$F?vD6CY0p8lZhcy{hZ~+x80%pl+R- zCeQUlo}DS4lKm)2|Lw&1v)kZ?<`d#s0&t*-#*r>6{&z&p&)&Vpf$;JXjc1~QPWF8N zAj%04C8s3kWAg`&g|6JEd_|WxSP*aK%P%~XwN|ZKZq*h14M^RP4iDP0ix~k z)Qc+rASwe8z3LBPV}=8WB6CLUBm9Htt7oEQxO~N&z(@)E-_;QRgQ&tYQJc4ODM5fv zsKqHc6!(wqFb*hmBdNJ2k{FQyQJPo=V4?nR+hz%XD1U!oITPnIQNEmg?0*o2dnWn{ zi7)r~ndr**8kzrXNcY77p)0a+$1Q98LM$4k=%uLr($u~iSc10RvD5xA_BZLRvL3Se(_{+zsAjw@yv9|H>!p1(HGN9l zv+y?1(;0jDz-Wu4HzW6&j+gC#KlQ2z|Hs1y+MBzZSoIp2s|;c%=0U(OLwmM6(fImD zMa8&BM_t`?R3jqxJMk1YeM^Xv{uP>~iM)7DZac_llMEp`=jzY&k0kr0CN)9N!;HY$ zms*4`L7}`^`&j?rBtqbHpMELKtOs@^yR2O-S6rWN;VRcA8_xaaP?P2Z+WVq&w$K=%l~PwN*>aOv|5LW=^dX-}q{6x&56N4fiM7MGxH1n$#op#D=9!MiF6IRIQ%r?dJX zxo_--+>DVmVhy zGX;|*-_LUMFL(;wbo@6{Yz!VX(zy>dpG-L)M_i1 ze`Ssh-{R-~hGAt&V`)W;yx;BbY-vIesQre44Rr7dDXP&!e3Sm4+t(SrfPH0y5On#2 zmjspy1iz7{gkElt?Tlun)@#WO2YnT^GtZHmHvZ73+OrhWEN?=`&) z#pJ2buC>8!(X=m2+2;PLb(XLe>_qkBeqdQ0qCJr!&LBW^F4?!(@p!@U?3XJq#LB}$ zy@afL9e1H_*Nm`_{K?`6+~$vhfH60aLjMoZtb<->45^PQ2(vhffLv9-ZNEx*3z{>} zQ%tMGQG9HOk15^MqFsu)Gy5Hvsjdk%>5i={fM;qje&>d2@WO9-SIg?M$(_4(`zME} zgy2|nm(&&KUH3jZ11aqz?_FIQpD#1-fd2s9^a2xw;%(r<1Ql!MlW=We4tZU>jnkKB zf2I=uG94M8eU%cRQI{&Fy_I87$=VlMpc|gzP{0#7UHG+o2|&p1`HybWLz>^zu}t!} zi)-Uj{?-W%xLt$-mbbt9Jm|lR0WD7au2+^Ryv4uI_^-5(rPf6lif){R zqgR57CLW;rD@4V-F!zmAGh{2`ItxA0XEjfleB#0vmmw|HZHWyn4@d9Tas9Pha9TZd zVo0r4gvX_(I;{_CkdEntjn$GCE4Y#SxLI`XF1wXB1fQrbyZ>os9Py7 z)QN!0>Cj6J90zZ1vrU)Yt3Rl$IWsde4IIFgq1yK?{{B4vdN@j%YudOl_m`ld7|aR! z|J5Ji@sA~`!6&n}?u&kZF@N}+SJu>zMlcZwn>U(iGdIp1sA^E0XH_>{lyM}caWTsF zZSo(<)8pDbks=hnTCX`6X_OsUeNgzx990NYPC~n1GAlg$y$aGk#`kT?=5t6BKItV$ zfTQ#iZ`mz@Q4D)uuZM3pZN}A7ap=}awLe}|%AA(0dy=l7+yR%s4ZPMvTMxj$`FwBx z7B`d`Kc0R&&3?Kcz`zz!UG=*=V0?B;8+aeoYpJdaZpGd$u0C2KXX%w4-_b~h4F(~< zz>jKdyIdH-Am0Mt%~bkXTy5v#SVwz5yIoxNeiUOMWSgi^VT+*;7;-&3Dl{m)_qo82 zH@gm*)|K9!Z7(GZ#|2@m&DR^z7M%g`2R~6$dI2R`Gt2i<7kqcni2Ui7h@i*5wQiQ* z?OzXJY>{)-JLj64E2fo1Ltf$Rf37yXz^y(qO7qou>P4K}qPHYhAIOeH6MY^&Hmdz6 z7C=bNeSA00DY(=e2d(i;l!Z`5dEO#Wo_cA|$m?U;c!$#7&!2_WBtwPK2kkAVg;xrH zP4DR0)~3@Ce+5t8{C$_@eJCJsQ1{zjO*TZtcDkgbZw2t{YI!vi(Gy~-h&|LPX1x0q zMJi%&C0k#$Uh8|ctbN$=HZTNdF|I#4$n^t%(YVB~@2b!_)4x5&KpA?C0GX&s;3ZtiBCTGNF-7Z@+3g`f$2M zU!&Lkkr07Ijn~draWZ4Jr(A}u;+7R@ur!l3wN7$-ap@_m3(#Y>sKPU zAj6v*Oa3Tyi1{PK-FM1+Rg=}wbJ?BSZkCk=^DU`4_it&$2=SczzbgJUuQ3JyM~XF@ zCNAclI!5WMz3YyrJ24iYI;>lcke=i>8v7ScG`&ae{<=t3cAdD%$i#}%Pr%}u#dz&w zBQ}kI`eOEUdxSu5#iT3PTDRWzBb{Io|97;7oQDtDle-gsm$j#(JQH(v`MPU$ppKg{ zP?`I2l8R`xqJj2=HROU^sn%&qj&Z(Vx6S)1PJd#mP+>b!fxY^nGxsmaYsa>%;iZ!v zfxDxQ4#LWwaGWa(H7RFcpO(GKQuwb*O}7>+wy+zm@indJ#^Fah_pfEfb&90zUeRyA*CAf)B4-F2@&CQt$!Ay= zcNr@DsM-0G$NO@>X#+^xdMTu5E_FW+SWH0@gC7V$&vyh3d@pA3Ov7A(@nsKv)ixrj zX1Bq`N-lkm((${6M*i3v?VV0Bbr$t&&6mv^4t-ICTz%=<(@l4m2AyDkBL0z0K1 zoU%g-@$Qb7M4X1NbHgbUL4IfHcxro5=JjDeR6jhVy)&qkSuWWfq>WGRxZmbTFmSUC z{C&2@C;0i?8fLk-1C{Js`D_6H!+W_-%n8d7h(>V#alVN!f3kDOfuKV7PeCR}ZF7RM zu6cHoj+8*>iOh7OuRlW1`IFbqDX}&_Ww)v|VO0vH$fO@FCwDAqOe-S$_9z$@Y2~N^q}s@lp;$1&p#6nN`aFev|6p@eR3O zZ#v3b377pI95La<(M3W>=I*+{xul)LWf!(E$GiI|{>IG~f0Ykpy_lDw4y)}VpT4>+ zZeqNPP>WEnxXxe9s&cw(X=mfRng+gdz6r&W|{eak`DebF`TJzpT zKz=aJ!BE!WN|Rxh=+!*dlhh<)eh{BS*X-H^%Ie}5?h>pL*EWkYK?MmM&TLBzo4PAy zn9{wIv-i$TI}x;TUtpCTMsWf=uhP@8B^0Wt4l7y@Yb+%8|Evf>nIqsBWhmS==}a&# z(`7-&`bW?^J+1}gAbF$WStC8TA!kHw0ijn{E`WcbLCz+?*B@mcL((zpvbtIs0gu^bFRC%{)`jA{&t@cM^w{6#$R znWHnsD#;MR}BZE1m9khl$oNZR%;4^t@ph zvRjcnDUF%M6bzY9*ZIlnD7|Lrp0dK%JqX(j-m|EFXv{j_u|=;7l4TTdZ9%JEV- z<-k*6{Kr0%l?C>hR{ayS@H#VA9p|JdE9vCtiOv%Bl1H2%kPXzc^ySxdh`FBuP|YY2 zWs`Znu33IhUMUc(e`8l~sm^cO;h7!GqKekGC4@l*h89qyI}Bt9>7heuq$QM) z83ZX2WayAa5Gg?r5QgqhYDf`5LTczP2Lu!Z-!iA*tOv8MuFt7i#a~rjC4e9QCjQv8OyS*ONg1Uv$Ws+|d?ynvq-NMRS1cd^l4c z-t?%8ERRqRLjf0T-L?69mBM(-!h4aqdb)%ML)x=^HuX#Ao=aO11VNta3C%n+He<8r1D^l> zzk7Z)Gsv7B1C>SF!`KC|tCgJO$Py0zjnfvAr z*v3981ll}$sobH`Bz(X_OS(aIwC38YITmxhS(-Y-17{)AYE7BHzxyYM$N2U2H}Y9i zV91OrmI6A}k|-W56nl-H?+T=pz*Ck^6Uu!k9q`)B@Ao2=Q$~%O2~W0GhT9j3xI3=h z^;(Y8_bA3`1o)~==ms7ckBK7FZ??EY?Y_^4EKHR0Dr^))7VJB%4HX2Gq)U0446Id! z9fPo}I^eSR4C^2sXJ4(qU>JsFM%1>vC-leA?A5iHd>tuyz9=AO#4_$LX;v0O2qd-6 z>;BB#Yx10$HRu(}9=Sw~h?+`4A^J1;5a&#S+|C?&VaR-7e{R!L@g6m~wzWBzlH*+& zc+ZO?B9I%e_Rfv`=#bJSovn5#9Ab(zjjO7~P$8Dyv2*Z8LbX5H)IBxd3rqYtam#L0 zt*WQ-@5zyCD;bY*VGQ$ELP7#r4%QA3^*J46EA@^pb?%U$P))`zRQk!%bd=`xPsFC54CC~e@q zluKayc3BBUcPkrf6Y1K(%A2l)JO@(iCs`A=*^F)dYX5>){fXnbVjH~##OBD^`7E?p zB28hC8k$fI%=QajOnMab$2@=C##IOL`G*cC_K|dmL96F%TGU&rF7d{Dsd|x`zxz7M zNBhER=6v;Nko=QY4uQ;^f=GTDGb2FWG)2@Yfy3UaQ(yJOERuU;SzYi22~rlXx6gkL zs5FENL*hp*UrZfbe%JKk@6q%`?VUq1D!f5NO8&$@mjA|>yWnsP+b?TV2B$f-QBg~HLX$qKd$y70nyEpYg#(zaFNH4p*|4R zK*^IIp|ZyBv9yqUqfYYahJs7156fzV*!$=+7fGwKcjaNX2uLhedipDs;Vt%MHO z{N5LRv&WMXsSWz-qctxl7A&NF7Z)Q8b4;H7n2DA~beS$~De5_03Yw_-xF^K?lH-j} z)-~LFZ>$wx;A<3@hEigL(kE;6b;U|0WAx;)V|N&glQy(jEpb#+Hq=T%Sk$>@Nj1>w zsQa!Td(E_816?hmQgx~`lvU8h2=4VlddPgJFKe0ZF~rpskUb}G@{FYsEdd@Q_a9Y} zy$x6|{kxSNDO~P7Yqc85eF-O}MKp7vx~E57;&A1ib6sA#hlwrHyr@7Gw{&n9+Rt&u z2LI5&CvD44x{r$S#R>>T#lwGd?bOQG4qt_7_ApEeV^m*X?VO{hLI{kiPvxuW zRz8UY`$vAf1f%8i*!9yZMh>!m~Hd+scB z@3dVzx%co(n!_if8%#o(sqYzG&ZolsH33K2nNw{86X;GwRyjrFvoXU|u$79a53&tr zEAj?};_5U&AvL^wC=IA{@pIm1^8`OKP9f|E<=5A@OVCN_P-NFGy*E>cfL-LXNaM+9 z2|0e&$$HOdrzf{g9K`r-+ddrR_zig~iTx=tsKtcwGZuP^joX$z6`ezSESwDPEgt=G z{l0mJ4gs5bnlK6Cmxe{tZ<7zaYD10v1z4i(yHf#Gt*Y_CCptkSq?*dPpLzf7W?3f- z*-@}G5+AAe#l5%gDe?Lyjm#(AV=K4ICd#JSV`IvJO1+ z{g9dMEs)RzU@j(!To0wwqU;k1&D5dT?_{ei=; zNrRUrFP6IRvY+;u93WN@m4lgx*6`!Bi}_pS9FK^>paldG9Qz3u=K1eWRusw_b;Cx- z^5b_--tXTB!H9*vA-g%Vrer`Fk6NdSmN&ZfI^%Wz$*tOXiWE_YK}4$-(G%9O+xtd) zTT@cSPGIpbDW?V*kokPdR!SYZeo&SzyS7cWLPXEu`69y-_nsYMcni6pDWZZFd2?Nl zD$Lg&YF66A7K{=I$v)3E(Eq^U&hgK0=eIJUHlUX5{K3d6+vs}RBCP`Th=#LbBTnTl z9lQ-L?V{x#%vKy|Dk!c@vsBiT>!tA!1InAHWi0|({O+%!l;!D5p60vF_1+7@aYb9d zTNsK^xs(*K{K$Bg-snyNW^;2-?1L@N=UOktI{%mn0#19lqV7ceioK$rVy?_@m*s~P76{uE8=TU02r5B9Gm$(^|x$;w5DO*K0ILkh_iR^}kMx$?l>s-i5J zN-9H{g+N8uC&&z#aWOQd3<&kZ~d-D=gxK%2zTzjk<;lo^0?PXXp z($6$~+`RQfz+yMei2YF`4E@5_Htx zZGzu2ly3c!sGI~O%Yh3tu)0c<(IIE*#ey!q8nq^i;DPfDW>z#xK5mH)rl4yKkK5Ac z@LKcw?&4!&LEK2_ALr5{d2w$_2yBBa=-%M|+?Bqj5Xc*7Hh&l(Lp?ZXD!ANU*F&Um znZ7ea+DG{VE`obQ8cv9)eX4NhV0-#gl)oM>gg5>>&2qi{+Ap@Q?u#OgR^P~0X3?bd z!y3YM?e9&Ry_(sRU;4jW?CbBDkvMBNx}mBjjafg|M6i#uRX%bXbcHw*Mu;ZUYAkoK zJBSZKYGfH_s~=HX`b;uSA9XGGHK?hB;Q38WbLG1P2hwa)kNZzO8-kdNOup|1aen1b z`k7@(BXc+9CMKd`bD~DY^@VpjG6UYTF@jEw z3L+crgU7+#Spes5_fgn#{J*N1fD6F(RTyXZw@^vtkEoE2S#M-td~9wdIZ&;=Snpm> z*2?B$r55n|5ac+{dx#&Du)%DLWtBz;S$$&%7EuW7IotNt_QH|8%=zb9RM*|t^jnfm z+I~*s6)GQp=8BB%>KA>e9fBmTFl4LGErV(3#b<%$j=6e~T$8sm{SF-%#a-SWnD4Yc1U%tB|OBg-Z*WbGMAIr=>*~_W8 zF!_VxC$6j5Lpet&T8-rKhhw2R)S>^Av?=k0fmLpWAY?ir`4I>%G=Og$^hAHR^f!d+Hve6q; z(2&`W?w=qrs%5*$X~xhL$7s;ZwmfkF0KBk8-QHoD+P$~3CdtZ4?ft(9lP3@jef8G$ zh$s_EUGe9ePL>s$_geDUq3(5clatdAj!PpaL)w{kuNmQ@bju3j=Y3RbNfW3V%Ly=>oy8~N43U2wY<{d7^Xpre2(t=;FN zX^7wShb@yLfhEEXwOa^J?x2w4HFi7QqnWC~*>Vw1J9oXZKvMUd^Zs6LQxwPkKQjP> z8Nw9?Gsez^!)ir7R;(V?tY@A%4$Oyv`Ev{hIqF0u0y$Fzl zB)i@uC3qJ&9R2FBGj&o40FsRlRMD2f=7K2F8$su;eJ3II5At5nCY4PoyUJ(Y!5#Trcj*wKt zjbs?KIsL213qH(I0n0QN>R=-;aMLi-`5m|FV(SYH1Mj$}w?<%auOhji&O?%2QG}-B zvwRoRqpYEf6Y~NupLfqq9rLifnN7&+0Ik+Ef1 zRW;0pFtZmYV;3gIAA#uR_z{F8K-&M;6?&d4OwNM>@%o?5pRMKw%m_dWag!I#8G6EL zc*c)?pTP^;Hm`kY%aEL*dx_-TQFdkazwx8zfl}vcM*|L-p?|r3v2yZ!_v2uw#hIh%q4=3 zHGyxzWI4J$D-u>3x;F09JZEH3UA@PxM7PwNcz>u)H=^DneongnAH0D4nBO=5($5zn zk$hNBO1`xLE{B#SLS43ZbyI(ucikw8jKId(t#+AJV7%?9Khz72ql#lRAC)!wWA$lR z`e4!q?P6ghD+D#X$=SvNFfBA?(j9pEkpI;+u`uGHE~31w12zh(OYJ^C*M>Dl(58=q zFUx#7+GClokEeo@~r)*gR{@{ za`JXm{s}4YJw)n0$8MIl)!wBaWp*;@yujD6smph?l%i|<(KBZ0%{YBwIu)W%s*gq3 zctx{P9U|W_)dzrM;+2eA@8;JgZN*}jK2Fi_gf{F!YFRoPFK-^fB3&&&K?pgB4Mg^E!U zoWB4!g=}`;gskUZ*2BFHW+bG&AJpLmVPUE{c5>IK#Njcv#IFv_;0>@&6jI=M>!%!>RtrtUfC*9#50dFL(ysBs4C?d=fWG7QaRqcV!H-hTB)B$J5Av- zlk9!WVrm0BRl3jTjdI$)>o`y$>qNE8*m4ZZ;hTXFRKCJbwGAIVf}nE!_7*giVVx~J zJku?K3Y=N?i_a3nwx*UyD^1S>i}Pkr@|&PVUx}!JKm9Fn7S$~$)}=uf`!{SpS#{~P zDerr1>%=dV6j9GzAo>9Q)Y8O;bpFCv?mJrd8^W<2?2m*TU19`pS$@wPu{FE#AXpJA z&M}@d64V(QTwJf*ZoFYNEO4s<`%>u3Lu%ftqiOZrrH%pJ8@mxnemr&kLQH}%d!_R0 zHtE|YNusnL56ob4q%wMJ#2T#dcA29M8?>Uu`s<{3I#-8}cTbMClL6bzqlR;M<(g~C z1Isk#rkjR`;Gie1D&qRh1LZ``;8bSKcGGBPty`}o&8RM;)1D3~;*!=NxMycVebkg_ z;FBh&4kB5FERK4%$&PrLq2QesS=Dd9j&k#h67wMKYr;w=(3H>ab4FQdzi+<93_zZ%DQy0>Ow`W{S2XlptE7kgKOz!6Pqh$!dE+9 zsHyWbDeX@#idp|f>&=o}0Xp#I4Xn_x_H{I)SvZgZ=cSJlWg01XF$MNE9cJqb@e|5f4JHD2r zIxlOa9;h~AKBjL=6kF?qbwQ+4dEQpi(v`GA@-hBqwFVz6{N=ct`uYz7)6}4blWEHF zn!)Pcsg2S4dR2mzWL7yPBrLvLp{L__Y3u_e6Gd3LJ!tf&U{?uAf!hCSm8nsrkbs~3 z%#-c5@Yz)I?7l?KjWG?8t6)0aiV!N46(%*{G2Igvl%Q*{8T;3ISz&lKT50#qb8qDw z1s5$)P*x5dQrt~dqIq2W`dcEg<>6rR0uZ%Th8F|#F%YCNy-bT-wdsyO8|auoY?T|m z?O`<$<@Cwj)zJxJI{s9`O6V5#R>^zyBIJm~80r1jfBr1X)*Q_|wKQcHR;BarN@Os9 z8Ylg#RfwwWryr2@T4V@Doi*heb5S$LR6j`Q?>Bmt2^yC+D`&yg{qM^BhI?#HIvkYk ztqdrVT)NE4q5LwoqF=^TTY69b$VclV+Gpm^@aGC$&_*QbAI(WlSHuigEL|^iD2n(3 zo=BM$bN%QHhc$%x$0Pn?psxZ@nd}_;U|%L@zFuR(v}% z#tX*eaOFQr@nh><71L+N@HDWQ@8t_ne6J1MOvU<{F}$k9zq$Oo3x4e2SH;1ZF=8;r znqU6;D}L;lmi64}@=}!oW0f3sC{9)dT*v)0WNFS=1el{$?w%dhW-x*CqGy00YE>Ax z>?w^MKm&1j`N^4^;fbW@-j=KWZ}*AQHOD#yrfEM*} z1HKR9>A6$z`z&Pt)VciseIrx=0}_M5kd=WT*Ufd@A+H<2n@;_{zZa8fEao%5V%Ot8 zv@drjaBIa&ZPhAP-o^I*zy23^Ht9{9j!T)jJC(LzvXTUEWSn6fbH;|rwM4?gQj9x^ zDDSWWCKBjpP0e`2niyl6J=WUq3zo)iR0Pkz0S%n@e#{Bchh7w}m0Rl16frV;68t$s zdV;5TC2wFSiTYzeH;BzI zua4qN;1v)1;_(31o9|c|JNMwq3Rl2X44Ah&T}X0-H^_pt8}WTCBE%qNi@nJ;R16jX zR=pd1iKUB2JI&r)$b2?*edZa^=R8W0aC?7=<<9pDus>UPZU97l3K_B^-1Jj-b>N)} zysK)t(f-{y3vOwjX0w$*e$`p_5Pzh|>S#&k=Cx>tZ|}PSw|{e6Ifg|D&-W2DtNpsw z;C3+ld($VY$RMIdfl+Ng@YJNbhRWr)PXJ`(A9=C!MD=|X$``RUONTUX@OTB{^qR6S z1(0xV?9=VEq_G;L<?UQ*AW-g#!RgLs0HhMi%k2bC80X6N3!~()aDLe11m5lGm3Rt;<`3BwjC_In z#ti5Exv$+|q7DQE?>fZJ&^Qq^kSMUyQg1!do;=4@JoDoMgXd+?UE7#Iqtp@s%I}7M zX5_+7yi{&3dmnUpV^XK}32=u6t%4T(=eH%sqwN*Mb*63h_-c~x{T$KxKO#5okZ_vNx%<$3}ai+HOPVi|!BorEzR43KI3&<+5@MX-Khusl1wo}|q$C@2G zBUqWm(RfD(q5ngx@b)c+c|Qf;{6vh&y7C+suANyXlDkKc#pBi+J zEKLM$530jm$#iPGaC2ST#~BRHj$8s*>phxJqaOMO24f$x>q3Dj9&tbXFk_+@^I{#} z8DTEv?7ldG&{leVgHLwlhFsq-qcC17o#r3bCs393PkST|Rk$YcZ4#2W_OU=>W>TM^ z*q~?IdzBiAz)CGVYoih{I49#K^wJMw%I~`ad*!w+7*R9HL|Gkjy0$N%qGdZW`J7bckkGDLlyS&uuAEY!KV0}C_RG>mSdl&{w!33VWh;N#omc5TgSbCXz z{A+bv!ZU5SRuLs$ZSI?!dfB*M3}_tZ`b7Ub_o;*HLK2=A!UixgAwT%PU3*aXTLU0FG!0KFldFxB;a)yt# zw$x_j+YjVQb(M|y=3Z)NUKxmiV5A3Y!-aN$1|XJc3i$E1z&-GhD?fQsruq81Jg88_ z=}O3(Z#ZAWm>#O?HRi@}X`DJC0&07_VES_qU=VbJ4n{@8&OM2IJ41SRyYb#-t1ccn zhn~8+7~BAYv-zC=3~XSb_W#?sX=hMFL~R?}gkEPggG_NGx%V7Pe+fOx^2ryN!`tMg zRAdrQ=5t;AYXo%tn;sj+pn@0wb7SnAMYdfNXdmR-cYE}E=94r}tT&zonF26$yvhS` z8<|c%C^8f=p*VD{}D2yiAWCvoRnW)rsA8Pka4bA7fVt1nUee_{lu;5PDG8_ zHMZ)Q(+#u}1770#>%LUjU7(Vn8Cxg8ogW}3xNX}N+tmg3HehHaHtzHY7&gnhn0t)b z1x(M1pZuD)1C4S#u1&_Nu<%`E5KaVi13BQ6cm#}!A9G-5K6(!u7ZAKu&yj=eJ*MDzFsn+3Oa+H3lQZ+}b;t&gS)^mzD0M3RA?5k*&>pU-=(q4kVq z2VyS>2qH&N_sXgrzHti#=zsP;DEdu>WZGz37b7D$@2QEDgA!BJffxgAKwg|`LK-wZ zS?9)~cRoqpc*0D+U~}kZm0-zAkR9b*KU!)aqeg#%8U5u-$^i-n&`y{3qB9Yu0?=ah zU6K&g8JOg3@Yt+RUF4<1d2Ez*_SCztXOk$%n*;i1a?p%m9fySp35V&8Y{lO+JT7oh_l~u6n*2Yv$5&-s44zK(;OwvRH1H zLMn)w!l=<(=;@ZkGvCJjX7e2YJiG#Dk#$GV32dCy*a{*Gs*~rU06KiU+cAFXg5Q>c zj3fm&G>Iq+86h`|U!Nm4gm3667E5lV1m52Msz_OdGLCmo&GB;jYn``ju!A-GMbV3r$ z1p6!WEJV!eOROM*&8}0f_cBOyn4zZQv|!MHRB&k|wYzI*(w-y!y$B7ynY4nN7h`m` zymC07=Zk%&=PZI@1{?N7zYH8)a~d;#jjXYkSDSRmYSDy}ya3O&&tVp518nh4uOC*a zkF?xA)Q#|@g~lFxPWv7p3J51@&z0$(uk74;Z%u1W!$$(FjP-g<^>W|DN3R0H@oyFp zun5>m_*p`|>NV88NEmtHwif9OrZ7Mi?d($3xK+PiluLfp^X1O2ro?iZ+v5yzBbTh_ z3zBX`!E%?_g_`l~b?#L}j8~+=37wG{8)%9}_b1Y0_D414r@)C^n3|{0ga!_18$Ze# z2>zQWb7(HA?Jl7_l~r^6+ph96Rdk35SlU0LrF~df@rlxGx&7%(K={B{u@rY2t_p$= zR=>-io*Cl+UtN!uJh~zdJ~UHR5S|&k3cmVEE+LghhEHqXewI9S0<31hE41FmZa9bF zQ(1^;A=~N1L#ayV&}C&hAUWp)$Ar{)_u|FIIm&VvQzfO~#Kc5ZU~p;a=yDBtOHNTY z(7N$9D$J`8jjoz=27TnRNLs%n$Vozq?pG*Fx;u97f(fC>7CaNhs=kf#nkcggy>vx9 zRv$c@E@!F7rpfB}G7gMs#1-$efJ?J5p%Q4uQ-Me*ZIo}^+n&zcGEZaUj3q?9f6wg= z`sGZ7!D=-Joo}AcZ%jNjg{pEzUTtj;&CSz?hK7taW+{et`2k_jg7MD3$*HP|BmSifm)H0=WwwJ^}*_?inT~rj3JNbMl0oGco6am_Dj&XrM7sI^4%k=I@{N{U;E3JF|AEe_L{d z#PbqTbDm>@#rw1DGg~%oGCF#_)}BT Kh8HVX1^f@JYT%3j From a7dfe46fb1c49a1a09d71c6e650f214622899c3d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 29 Jun 2023 03:38:06 +0200 Subject: [PATCH 0007/1009] 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 6b031ff7142f6f..8ac929e13b6d69 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 afd38bf763695f..84c0f769c7cec8 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -500,6 +500,34 @@ def __call__(self, data: Any) -> Any: 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 f95da5c6e66e84..c518ad227a76f9 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 dfe7c5ebed452d12f48bc5d53301218a9fd1faf3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Jun 2023 20:39:31 -0500 Subject: [PATCH 0008/1009] Refactor ESPHome connection management logic into a class (#95457) * Refactor ESPHome setup logic into a class Avoids all the nonlocals and fixes the C901 * cleanup * touch ups * touch ups * touch ups * make easier to read * stale --- homeassistant/components/esphome/__init__.py | 327 +++++++++++-------- 1 file changed, 195 insertions(+), 132 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index afaefe117bacae..271b0b9aa16551 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -137,57 +137,60 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry -) -> bool: - """Set up the esphome component.""" - host = entry.data[CONF_HOST] - port = entry.data[CONF_PORT] - password = entry.data[CONF_PASSWORD] - noise_psk = entry.data.get(CONF_NOISE_PSK) - device_id: str = None # type: ignore[assignment] - - zeroconf_instance = await zeroconf.async_get_instance(hass) - - cli = APIClient( - host, - port, - password, - client_info=f"Home Assistant {ha_version}", - zeroconf_instance=zeroconf_instance, - noise_psk=noise_psk, - ) - - services_issue = f"service_calls_not_enabled-{entry.unique_id}" - if entry.options.get(CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS): - async_delete_issue(hass, DOMAIN, services_issue) - - domain_data = DomainData.get(hass) - entry_data = RuntimeEntryData( - client=cli, - entry_id=entry.entry_id, - store=domain_data.get_or_create_store(hass, entry), - original_options=dict(entry.options), +class ESPHomeManager: + """Class to manage an ESPHome connection.""" + + __slots__ = ( + "hass", + "host", + "password", + "entry", + "cli", + "device_id", + "domain_data", + "voice_assistant_udp_server", + "reconnect_logic", + "zeroconf_instance", + "entry_data", ) - domain_data.set_entry_data(entry, entry_data) - async def on_stop(event: Event) -> None: + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + host: str, + password: str | None, + cli: APIClient, + zeroconf_instance: zeroconf.HaZeroconf, + domain_data: DomainData, + entry_data: RuntimeEntryData, + ) -> None: + """Initialize the esphome manager.""" + self.hass = hass + self.host = host + self.password = password + self.entry = entry + self.cli = cli + self.device_id: str | None = None + self.domain_data = domain_data + self.voice_assistant_udp_server: VoiceAssistantUDPServer | None = None + self.reconnect_logic: ReconnectLogic | None = None + self.zeroconf_instance = zeroconf_instance + self.entry_data = entry_data + + async def on_stop(self, event: Event) -> None: """Cleanup the socket client on HA stop.""" - await _cleanup_instance(hass, entry) - - # Use async_listen instead of async_listen_once so that we don't deregister - # the callback twice when shutting down Home Assistant. - # "Unable to remove unknown listener - # .onetime_listener>" - entry_data.cleanup_callbacks.append( - hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop) - ) + await _cleanup_instance(self.hass, self.entry) + + @property + def services_issue(self) -> str: + """Return the services issue name for this entry.""" + return f"service_calls_not_enabled-{self.entry.unique_id}" @callback - def async_on_service_call(service: HomeassistantServiceCall) -> None: + def async_on_service_call(self, service: HomeassistantServiceCall) -> None: """Call service when user automation in ESPHome config is triggered.""" - device_info = entry_data.device_info - assert device_info is not None + hass = self.hass domain, service_name = service.service.split(".", 1) service_data = service.data @@ -201,15 +204,16 @@ def async_on_service_call(service: HomeassistantServiceCall) -> None: template.render_complex(data_template, service.variables) ) except TemplateError as ex: - _LOGGER.error("Error rendering data template for %s: %s", host, ex) + _LOGGER.error("Error rendering data template for %s: %s", self.host, ex) return if service.is_event: + device_id = self.device_id # ESPHome uses service call packet for both events and service calls # Ensure the user can only send events of form 'esphome.xyz' if domain != "esphome": _LOGGER.error( - "Can only generate events under esphome domain! (%s)", host + "Can only generate events under esphome domain! (%s)", self.host ) return @@ -226,17 +230,21 @@ def async_on_service_call(service: HomeassistantServiceCall) -> None: **service_data, }, ) - elif entry.options.get(CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS): + elif self.entry.options.get( + CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS + ): hass.async_create_task( hass.services.async_call( domain, service_name, service_data, blocking=True ) ) else: + device_info = self.entry_data.device_info + assert device_info is not None async_create_issue( hass, DOMAIN, - services_issue, + self.services_issue, is_fixable=False, severity=IssueSeverity.WARNING, translation_key="service_calls_not_allowed", @@ -256,7 +264,7 @@ def async_on_service_call(service: HomeassistantServiceCall) -> None: ) async def _send_home_assistant_state( - entity_id: str, attribute: str | None, state: State | None + self, entity_id: str, attribute: str | None, state: State | None ) -> None: """Forward Home Assistant states to ESPHome.""" if state is None or (attribute and attribute not in state.attributes): @@ -271,102 +279,102 @@ async def _send_home_assistant_state( else: send_state = attr_val - await cli.send_home_assistant_state(entity_id, attribute, str(send_state)) + await self.cli.send_home_assistant_state(entity_id, attribute, str(send_state)) @callback def async_on_state_subscription( - entity_id: str, attribute: str | None = None + self, entity_id: str, attribute: str | None = None ) -> None: """Subscribe and forward states for requested entities.""" + hass = self.hass async def send_home_assistant_state_event(event: Event) -> None: """Forward Home Assistant states updates to ESPHome.""" + event_data = event.data + new_state: State | None = event_data.get("new_state") + old_state: State | None = event_data.get("old_state") + + if new_state is None or old_state is None: + return # Only communicate changes to the state or attribute tracked - if event.data.get("new_state") is None or ( - event.data.get("old_state") is not None - and "new_state" in event.data - and ( - ( - not attribute - and event.data["old_state"].state - == event.data["new_state"].state - ) - or ( - attribute - and attribute in event.data["old_state"].attributes - and attribute in event.data["new_state"].attributes - and event.data["old_state"].attributes[attribute] - == event.data["new_state"].attributes[attribute] - ) - ) + if (not attribute and old_state.state == new_state.state) or ( + attribute + and old_state.attributes.get(attribute) + == new_state.attributes.get(attribute) ): return - await _send_home_assistant_state( - event.data["entity_id"], attribute, event.data.get("new_state") + await self._send_home_assistant_state( + event.data["entity_id"], attribute, new_state ) - unsub = async_track_state_change_event( - hass, [entity_id], send_home_assistant_state_event + self.entry_data.disconnect_callbacks.append( + async_track_state_change_event( + hass, [entity_id], send_home_assistant_state_event + ) ) - entry_data.disconnect_callbacks.append(unsub) # Send initial state hass.async_create_task( - _send_home_assistant_state(entity_id, attribute, hass.states.get(entity_id)) + self._send_home_assistant_state( + entity_id, attribute, hass.states.get(entity_id) + ) ) - voice_assistant_udp_server: VoiceAssistantUDPServer | None = None - def _handle_pipeline_event( - event_type: VoiceAssistantEventType, data: dict[str, str] | None + self, event_type: VoiceAssistantEventType, data: dict[str, str] | None ) -> None: - cli.send_voice_assistant_event(event_type, data) + self.cli.send_voice_assistant_event(event_type, data) - def _handle_pipeline_finished() -> None: - nonlocal voice_assistant_udp_server + def _handle_pipeline_finished(self) -> None: + self.entry_data.async_set_assist_pipeline_state(False) - entry_data.async_set_assist_pipeline_state(False) + if self.voice_assistant_udp_server is not None: + self.voice_assistant_udp_server.close() + self.voice_assistant_udp_server = None - if voice_assistant_udp_server is not None: - voice_assistant_udp_server.close() - voice_assistant_udp_server = None - - async def _handle_pipeline_start(conversation_id: str, use_vad: bool) -> int | None: + async def _handle_pipeline_start( + self, conversation_id: str, use_vad: bool + ) -> int | None: """Start a voice assistant pipeline.""" - nonlocal voice_assistant_udp_server - - if voice_assistant_udp_server is not None: + if self.voice_assistant_udp_server is not None: return None + hass = self.hass voice_assistant_udp_server = VoiceAssistantUDPServer( - hass, entry_data, _handle_pipeline_event, _handle_pipeline_finished + hass, + self.entry_data, + self._handle_pipeline_event, + self._handle_pipeline_finished, ) port = await voice_assistant_udp_server.start_server() + assert self.device_id is not None, "Device ID must be set" hass.async_create_background_task( voice_assistant_udp_server.run_pipeline( - device_id=device_id, + device_id=self.device_id, conversation_id=conversation_id or None, use_vad=use_vad, ), "esphome.voice_assistant_udp_server.run_pipeline", ) - entry_data.async_set_assist_pipeline_state(True) + self.entry_data.async_set_assist_pipeline_state(True) return port - async def _handle_pipeline_stop() -> None: + async def _handle_pipeline_stop(self) -> None: """Stop a voice assistant pipeline.""" - nonlocal voice_assistant_udp_server + if self.voice_assistant_udp_server is not None: + self.voice_assistant_udp_server.stop() - if voice_assistant_udp_server is not None: - voice_assistant_udp_server.stop() - - async def on_connect() -> None: + async def on_connect(self) -> None: """Subscribe to states and list entities on successful API login.""" - nonlocal device_id + entry = self.entry + entry_data = self.entry_data + reconnect_logic = self.reconnect_logic + hass = self.hass + cli = self.cli try: device_info = await cli.device_info() @@ -389,6 +397,7 @@ async def on_connect() -> None: entry_data.api_version = cli.api_version entry_data.available = True if entry_data.device_info.name: + assert reconnect_logic is not None, "Reconnect logic must be set" reconnect_logic.name = entry_data.device_info.name if device_info.bluetooth_proxy_feature_flags_compat(cli.api_version): @@ -396,37 +405,38 @@ async def on_connect() -> None: await async_connect_scanner(hass, entry, cli, entry_data) ) - device_id = _async_setup_device_registry( - hass, entry, entry_data.device_info - ) + _async_setup_device_registry(hass, entry, entry_data.device_info) entry_data.async_update_device_state(hass) entity_infos, services = await cli.list_entities_services() await entry_data.async_update_static_infos(hass, entry, entity_infos) await _setup_services(hass, entry_data, services) await cli.subscribe_states(entry_data.async_update_state) - await cli.subscribe_service_calls(async_on_service_call) - await cli.subscribe_home_assistant_states(async_on_state_subscription) + await cli.subscribe_service_calls(self.async_on_service_call) + await cli.subscribe_home_assistant_states(self.async_on_state_subscription) if device_info.voice_assistant_version: entry_data.disconnect_callbacks.append( await cli.subscribe_voice_assistant( - _handle_pipeline_start, - _handle_pipeline_stop, + self._handle_pipeline_start, + self._handle_pipeline_stop, ) ) hass.async_create_task(entry_data.async_save_to_store()) except APIConnectionError as err: - _LOGGER.warning("Error getting initial data for %s: %s", host, err) + _LOGGER.warning("Error getting initial data for %s: %s", self.host, err) # Re-connection logic will trigger after this await cli.disconnect() else: _async_check_firmware_version(hass, device_info, entry_data.api_version) - _async_check_using_api_password(hass, device_info, bool(password)) + _async_check_using_api_password(hass, device_info, bool(self.password)) - async def on_disconnect(expected_disconnect: bool) -> None: + async def on_disconnect(self, expected_disconnect: bool) -> None: """Run disconnect callbacks on API disconnect.""" + entry_data = self.entry_data + hass = self.hass + host = self.host name = entry_data.device_info.name if entry_data.device_info else host _LOGGER.debug( "%s: %s disconnected (expected=%s), running disconnected callbacks", @@ -453,7 +463,7 @@ async def on_disconnect(expected_disconnect: bool) -> None: # will be cleared anyway. entry_data.async_update_device_state(hass) - async def on_connect_error(err: Exception) -> None: + async def on_connect_error(self, err: Exception) -> None: """Start reauth flow if appropriate connect error type.""" if isinstance( err, @@ -463,32 +473,85 @@ async def on_connect_error(err: Exception) -> None: InvalidAuthAPIError, ), ): - entry.async_start_reauth(hass) + self.entry.async_start_reauth(self.hass) + + async def async_start(self) -> None: + """Start the esphome connection manager.""" + hass = self.hass + entry = self.entry + entry_data = self.entry_data + + if entry.options.get(CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS): + async_delete_issue(hass, DOMAIN, self.services_issue) + + # Use async_listen instead of async_listen_once so that we don't deregister + # the callback twice when shutting down Home Assistant. + # "Unable to remove unknown listener + # .onetime_listener>" + entry_data.cleanup_callbacks.append( + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.on_stop) + ) - reconnect_logic = ReconnectLogic( - client=cli, - on_connect=on_connect, - on_disconnect=on_disconnect, - zeroconf_instance=zeroconf_instance, - name=host, - on_connect_error=on_connect_error, - ) + reconnect_logic = ReconnectLogic( + client=self.cli, + on_connect=self.on_connect, + on_disconnect=self.on_disconnect, + zeroconf_instance=self.zeroconf_instance, + name=self.host, + on_connect_error=self.on_connect_error, + ) + self.reconnect_logic = reconnect_logic - infos, services = await entry_data.async_load_from_store() - await entry_data.async_update_static_infos(hass, entry, infos) - await _setup_services(hass, entry_data, services) + infos, services = await entry_data.async_load_from_store() + await entry_data.async_update_static_infos(hass, entry, infos) + await _setup_services(hass, entry_data, services) - if entry_data.device_info is not None and entry_data.device_info.name: - reconnect_logic.name = entry_data.device_info.name - if entry.unique_id is None: - hass.config_entries.async_update_entry( - entry, unique_id=format_mac(entry_data.device_info.mac_address) - ) + if entry_data.device_info is not None and entry_data.device_info.name: + reconnect_logic.name = entry_data.device_info.name + if entry.unique_id is None: + hass.config_entries.async_update_entry( + entry, unique_id=format_mac(entry_data.device_info.mac_address) + ) + + await reconnect_logic.start() + entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback) + + entry.async_on_unload( + entry.add_update_listener(entry_data.async_update_listener) + ) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the esphome component.""" + host = entry.data[CONF_HOST] + port = entry.data[CONF_PORT] + password = entry.data[CONF_PASSWORD] + noise_psk = entry.data.get(CONF_NOISE_PSK) - await reconnect_logic.start() - entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback) + zeroconf_instance = await zeroconf.async_get_instance(hass) + + cli = APIClient( + host, + port, + password, + client_info=f"Home Assistant {ha_version}", + zeroconf_instance=zeroconf_instance, + noise_psk=noise_psk, + ) - entry.async_on_unload(entry.add_update_listener(entry_data.async_update_listener)) + domain_data = DomainData.get(hass) + entry_data = RuntimeEntryData( + client=cli, + entry_id=entry.entry_id, + store=domain_data.get_or_create_store(hass, entry), + original_options=dict(entry.options), + ) + domain_data.set_entry_data(entry, entry_data) + + manager = ESPHomeManager( + hass, entry, host, password, cli, zeroconf_instance, domain_data, entry_data + ) + await manager.async_start() return True From 54255331d5b1e79227ec0c3fe04afa3da07f1112 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Jun 2023 20:40:03 -0500 Subject: [PATCH 0009/1009] Small cleanups to bluetooth manager advertisement processing (#95453) Avoid a few lookups that are rarely used now --- homeassistant/components/bluetooth/manager.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index f1221290c74105..d1fcb115180cbb 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -413,23 +413,20 @@ def scanner_adv_received(self, service_info: BluetoothServiceInfoBleak) -> None: # Pre-filter noisy apple devices as they can account for 20-35% of the # traffic on a typical network. - advertisement_data = service_info.advertisement - manufacturer_data = advertisement_data.manufacturer_data if ( - len(manufacturer_data) == 1 - and (apple_data := manufacturer_data.get(APPLE_MFR_ID)) - and apple_data[0] not in APPLE_START_BYTES_WANTED - and not advertisement_data.service_data + (manufacturer_data := service_info.manufacturer_data) + and APPLE_MFR_ID in manufacturer_data + and manufacturer_data[APPLE_MFR_ID][0] not in APPLE_START_BYTES_WANTED + and len(manufacturer_data) == 1 + and not service_info.service_data ): return - device = service_info.device - address = device.address + address = service_info.device.address all_history = self._all_history connectable = service_info.connectable connectable_history = self._connectable_history old_connectable_service_info = connectable and connectable_history.get(address) - source = service_info.source # This logic is complex due to the many combinations of scanners # that are supported. @@ -544,13 +541,17 @@ def scanner_adv_received(self, service_info: BluetoothServiceInfoBleak) -> None: "%s: %s %s match: %s", self._async_describe_source(service_info), address, - advertisement_data, + service_info.advertisement, matched_domains, ) - if connectable or old_connectable_service_info: + if (connectable or old_connectable_service_info) and ( + bleak_callbacks := self._bleak_callbacks + ): # Bleak callbacks must get a connectable device - for callback_filters in self._bleak_callbacks: + device = service_info.device + advertisement_data = service_info.advertisement + for callback_filters in bleak_callbacks: _dispatch_bleak_callback(*callback_filters, device, advertisement_data) for match in self._callback_index.match_callbacks(service_info): From 33c7cdcdb361c658e144b652d1a0f517b9e6f853 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 28 Jun 2023 20:41:11 -0500 Subject: [PATCH 0010/1009] 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 345480da363f47..594abc69c13b64 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 32cfbd70337167..3d0681a847566c 100644 --- a/homeassistant/components/voip/voip.py +++ b/homeassistant/components/voip/voip.py @@ -11,7 +11,13 @@ 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 @@ 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 @@ def __init__(self, hass: HomeAssistant, devices: VoIPDevices) -> None: 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 @@ def __init__( 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 @@ def __init__( width=WIDTH, channels=CHANNELS, opus_payload_type=opus_payload_type, + rtcp_state=rtcp_state, ) self.hass = hass @@ -454,6 +468,7 @@ def __init__( 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 @@ def __init__( 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 79f8926932cf69..b0d6425d442c92 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 5e1af46951c3f9..51f614a685fc45 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 b86b41ebe5db7d73209b09aacc5d79d67433d636 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 03:43:42 +0200 Subject: [PATCH 0011/1009] 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 693d2550f53472..726295448950e5 100644 --- a/homeassistant/components/youtube/coordinator.py +++ b/homeassistant/components/youtube/coordinator.py @@ -65,7 +65,7 @@ async def _get_channels(self, service: Resource) -> list[dict[str, Any]]: 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 1615f3e1fde45b9658edababa24cddfff1870ad4 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 29 Jun 2023 03:45:17 +0200 Subject: [PATCH 0012/1009] 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 cc713d1034c4e9..e8c237114b51ab 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -30,6 +30,7 @@ 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 d95a15738722f2..0ad497a30a261c 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 5e6f856ee44413..5796eae8393e6c 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 c93c3bbdcd610171616bf543c938e5c9001e25ff Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Wed, 28 Jun 2023 21:46:08 -0400 Subject: [PATCH 0013/1009] 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 99a1a4ac2ff889..1b1e51db0358f6 100644 --- a/homeassistant/components/mazda/button.py +++ b/homeassistant/components/mazda/button.py @@ -78,21 +78,25 @@ class MazdaButtonEntityDescription(ButtonEntityDescription): 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 535ddcc963b62f..ba80c10b38d81d 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"), + [ + (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"), [ - ("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"), + (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 b0c0b58340842c4ed34cb3e5e4f870f8d1d1d18e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 04:21:50 +0200 Subject: [PATCH 0014/1009] Remove statement in iss config flow (#95472) Remove conf name --- homeassistant/components/iss/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/iss/config_flow.py b/homeassistant/components/iss/config_flow.py index ebfd445f62c8bc..2beffc7c8944f3 100644 --- a/homeassistant/components/iss/config_flow.py +++ b/homeassistant/components/iss/config_flow.py @@ -3,7 +3,7 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_NAME, CONF_SHOW_ON_MAP +from homeassistant.const import CONF_SHOW_ON_MAP from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult @@ -33,7 +33,7 @@ async def async_step_user(self, user_input=None) -> FlowResult: if user_input is not None: return self.async_create_entry( - title=user_input.get(CONF_NAME, DEFAULT_NAME), + title=DEFAULT_NAME, data={}, options={CONF_SHOW_ON_MAP: user_input.get(CONF_SHOW_ON_MAP, False)}, ) From 48049d588cf394bd9bace94206aae2934b43ceff Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 04:22:55 +0200 Subject: [PATCH 0015/1009] Add entity translations to iOS (#95467) --- homeassistant/components/ios/sensor.py | 14 ++++++++------ homeassistant/components/ios/strings.json | 7 +++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ios/sensor.py b/homeassistant/components/ios/sensor.py index f4dab9e301bad8..f3767be9f3d5ed 100644 --- a/homeassistant/components/ios/sensor.py +++ b/homeassistant/components/ios/sensor.py @@ -1,7 +1,11 @@ """Support for Home Assistant iOS app sensors.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant, callback @@ -17,12 +21,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="level", - name="Battery Level", native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, ), SensorEntityDescription( key="state", - name="Battery State", + translation_key="battery_state", ), ) @@ -59,6 +63,7 @@ class IOSSensor(SensorEntity): """Representation of an iOS sensor.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__( self, device_name, device, description: SensorEntityDescription @@ -67,9 +72,6 @@ def __init__( self.entity_description = description self._device = device - device_name = device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_NAME] - self._attr_name = f"{device_name} {description.key}" - device_id = device[ios.ATTR_DEVICE_ID] self._attr_unique_id = f"{description.key}_{device_id}" diff --git a/homeassistant/components/ios/strings.json b/homeassistant/components/ios/strings.json index 2b486cc0c04834..6c77209e3171b1 100644 --- a/homeassistant/components/ios/strings.json +++ b/homeassistant/components/ios/strings.json @@ -8,5 +8,12 @@ "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } + }, + "entity": { + "sensor": { + "battery_state": { + "name": "Battery state" + } + } } } From 3c7912f7a4d8792435c78b65087d062e67da1d64 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 11:09:45 +0200 Subject: [PATCH 0016/1009] 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 a738952d2ca123..0145d6f0906082 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 e6c4f9835415b695ccafa7d7847b426ea7bbd5b4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 11:24:59 +0200 Subject: [PATCH 0017/1009] 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 d5122862e2c0bf..c2c9c207c02b0e 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 182d0dfd85d645..72216057affc8c 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 bcb973270061ad..6b3b84ba349eaa 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 59cfee3506c7b6..210cc5c75180d7 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 458a2681186799..6d09ba4314cc87 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 44b3494ca7736d..3ab4c3568c4d67 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -79,6 +79,7 @@ class TuyaLightEntityDescription(LightEntityDescription): "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 @@ class TuyaLightEntityDescription(LightEntityDescription): "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 @@ class TuyaLightEntityDescription(LightEntityDescription): "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 @@ class TuyaLightEntityDescription(LightEntityDescription): "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 @@ class TuyaLightEntityDescription(LightEntityDescription): # Some ceiling fan lights use LIGHT for DPCode instead of SWITCH_LED TuyaLightEntityDescription( key=DPCode.LIGHT, + name=None, ), ), # Ambient Light @@ -135,6 +140,7 @@ class TuyaLightEntityDescription(LightEntityDescription): "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 @@ class TuyaLightEntityDescription(LightEntityDescription): "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 @@ class TuyaLightEntityDescription(LightEntityDescription): "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 @@ class TuyaLightEntityDescription(LightEntityDescription): "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 @@ class TuyaLightEntityDescription(LightEntityDescription): "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 @@ class TuyaLightEntityDescription(LightEntityDescription): "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 @@ class TuyaLightEntityDescription(LightEntityDescription): "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 @@ class TuyaLightEntityDescription(LightEntityDescription): "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 @@ class TuyaLightEntityDescription(LightEntityDescription): "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 a60e24eca861a9..c2dc8cea99ba15 100644 --- a/homeassistant/components/tuya/siren.py +++ b/homeassistant/components/tuya/siren.py @@ -27,7 +27,6 @@ "dgnbj": ( SirenEntityDescription( key=DPCode.ALARM_SWITCH, - name="Siren", ), ), # Siren Alarm @@ -35,7 +34,6 @@ "sgbj": ( SirenEntityDescription( key=DPCode.ALARM_SWITCH, - name="Siren", ), ), # Smart Camera @@ -43,7 +41,6 @@ "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 7827fb061eadf8..a2961a55d7872f 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 9fb3d4de30e55ad33dde1f4d0ba7fe08a999ed90 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 11:25:39 +0200 Subject: [PATCH 0018/1009] 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 da6c710fed8386..1da879cb02bfd3 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 e5f1e3c46b4de6..5b53b410208269 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 5dc99c5d6f1f3e..53b40bbf780968 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 69ce0aa4750d73..7710cde12a9da7 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 25b76ea24cb96c..f62e4d3f918a40 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 2205f62cc1af7f600a7719ab2285339ce8954cba Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Thu, 29 Jun 2023 04:29:54 -0500 Subject: [PATCH 0019/1009] 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 7df6d84c79471c..a5f625f9e73ddd 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 072658044d83b9..003bfa3cf396d1 100644 --- a/tests/components/matter/test_door_lock.py +++ b/tests/components/matter/test_door_lock.py @@ -11,7 +11,7 @@ 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 ed16fffa7950d854e4880c98b4a87674a6571967 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jun 2023 11:59:36 +0200 Subject: [PATCH 0020/1009] 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 fb8f57b4d5a2e6..f2097178a95f3f 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 90bc5b7d50e069..553af2f0c866d2 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 2f4f20045d7743..d00926eb0ee5f3 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 c164c6636fa1c6..1b865827e697c9 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 5beb06eea9dd90..8fbafd7a4d168a 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 d2834b8991b76b..6f3d48fc1bbd4e 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -292,7 +292,7 @@ def async_configure(self, **kwargs) -> None: 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 3e8ed2afbdc626..f44d736b426cf7 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 57be995a4892cb..60a332446cef36 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -312,7 +312,7 @@ def perform_set_alarm_detection_sensibility( 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 def8f77994eff7..b922d98f25e1e2 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 08179df5b7e09a..b4776b19c50db9 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 34ac5414936e867425985906db1973888bf9ec7c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jun 2023 12:00:13 +0200 Subject: [PATCH 0021/1009] 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 7c0dc8ff0a6af1..4ea4da602e37c8 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 @@ 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 bfa6c299b57b76..7153dac1bcba73 100644 --- a/homeassistant/components/workday/config_flow.py +++ b/homeassistant/components/workday/config_flow.py @@ -155,6 +155,33 @@ def async_get_options_flow( """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 5af69e29a8baa9..e6753b39dcefaf 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 d2ae9895544fe8..71dd23c19a31ee 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -4,7 +4,9 @@ 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 @@ ) +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 ce4dd1277783c5..7e28471c78cfac 100644 --- a/tests/components/workday/test_config_flow.py +++ b/tests/components/workday/test_config_flow.py @@ -13,6 +13,7 @@ CONF_REMOVE_HOLIDAYS, CONF_WORKDAYS, DEFAULT_EXCLUDES, + DEFAULT_NAME, DEFAULT_OFFSET, DEFAULT_WORKDAYS, DOMAIN, @@ -23,6 +24,8 @@ 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 06d47185fea1ff5e4c37d046979795729044cca3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jun 2023 12:00:41 +0200 Subject: [PATCH 0022/1009] 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 479d1d648b8b88..896d3f8b5a8ace 100644 --- a/homeassistant/components/snapcast/config_flow.py +++ b/homeassistant/components/snapcast/config_flow.py @@ -51,3 +51,13 @@ async def async_step_user(self, user_input=None) -> FlowResult: 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 377a3d1e2b6b72..096e3829bc7ba8 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 @@ 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 766bca634955d6..0087b70d8204e0 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 bb07eae2140d05..b6ff43503a6328 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 a6cfef3029822cc7e8ac36711caf5734890925ff Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jun 2023 12:01:26 +0200 Subject: [PATCH 0023/1009] 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 ac41d03e998a4e..54c47c53895ade 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 @@ 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 @@ async def async_step_user( 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 b6d9fe63e4ba30..15a634cf7a9872 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -14,7 +14,7 @@ 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 @@ 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 @@ ) +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 66c9430911e746..24d1885a9177ac 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 bbfeee20d8a6fe..b7244ccef8d747 100644 --- a/tests/components/qbittorrent/test_config_flow.py +++ b/tests/components/qbittorrent/test_config_flow.py @@ -4,7 +4,7 @@ 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 @@ 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 5d1c1b35d372cd514cb3eb61e7de3d4b97208f1c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 12:02:09 +0200 Subject: [PATCH 0024/1009] 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 932febd80f02a5..5f66338ecc13c6 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 369de1cad3b8a98f33fac7df8bad13512606c856 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 12:03:25 +0200 Subject: [PATCH 0025/1009] 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 c116a1bb63577c..c0fb80971ca1e8 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 a3ffa0aed74f19e0a8e18caca9df61af7bc8a040 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jun 2023 12:03:42 +0200 Subject: [PATCH 0026/1009] Revert "Remove Brottsplatskartan YAML configuration (#94101)" (#95521) --- .../brottsplatskartan/config_flow.py | 15 +++ .../components/brottsplatskartan/sensor.py | 53 ++++++++- .../components/brottsplatskartan/strings.json | 6 + .../brottsplatskartan/test_config_flow.py | 108 ++++++++++++++++++ 4 files changed, 177 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/brottsplatskartan/config_flow.py b/homeassistant/components/brottsplatskartan/config_flow.py index 09d6cd960873c6..1de24ffa76c1ca 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 63af7530b79a07..a70b9c134d0290 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -5,19 +5,62 @@ from datetime import timedelta from brottsplatskartan import ATTRIBUTION, BrottsplatsKartan - -from homeassistant.components.sensor import SensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +import voluptuous as vol + +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 f10120f7884553..8d9677a0af478f 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 efd259fa73cf8a..dd3139dc2b9b90 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.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 9d7007df63815ee56e510c94b5d98dc6292e2fa9 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 14:49:46 +0200 Subject: [PATCH 0027/1009] 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 834202ba58d064..3eceb448fa4e89 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 ab0ce20a9a1a8d..ca975ed055d5f4 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 e9d8fff0dd7c59d9cf57a8f28dc0f72a6193ed7b Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 29 Jun 2023 15:28:34 +0200 Subject: [PATCH 0028/1009] 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 f3a764bc99f7e1..8e76706b7fdb8b 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -96,20 +96,24 @@ def node_removed_callback(event: EventType, node_id: int) -> None: ) 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 d4e90508afac22..0457cfaa8107b4 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -81,7 +81,7 @@ async def async_added_to_hass(self) -> None: 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 @@ async def async_added_to_hass(self) -> None: ) # 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 0bf900c8812878..85434407a10ffb 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 b0d6425d442c92..aab14224dc9643 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 51f614a685fc45..2863b4c90f700e 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 7582b9c415d981..a09351540543b5 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 0d7cfc9c2ede4d..62ed847bf28f8b 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 8e00bd4436faef2dca64d5fa7d5796f333d41515 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 16:40:35 +0200 Subject: [PATCH 0029/1009] 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 c6ca70bdc847db..bdd55bb2dad9d5 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 e3e1bef376744d84eebf1f66cf36299ffe5bc6a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jun 2023 10:35:32 -0500 Subject: [PATCH 0030/1009] 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 9a25a28aa1c962..514c218b1010b8 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -168,7 +168,7 @@ def _has_all_unique_names_and_ports( 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 5c154a50beccb1..112c138a8438a4 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.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 45bbbeee190f5bc1f2a3240db5c66ec135e621bb Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 29 Jun 2023 17:36:39 +0200 Subject: [PATCH 0031/1009] 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 4ea4da602e37c8..51560161faa8fa 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 23e23ae80e8b0c8b0af76a4316a702ed1a6d6798 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 29 Jun 2023 17:39:08 +0200 Subject: [PATCH 0032/1009] 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 8ac929e13b6d69..1a28044dcb5d9f 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 1f840db333bf0c01909b54ad27300e838c3d11e2 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 0033/1009] 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 c1eac31886e875..de6dbdbe0756a7 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -112,8 +112,8 @@ {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 33be262ad72dfd7cbe29120e249c1fb576f7e545 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Thu, 29 Jun 2023 19:53:50 +0300 Subject: [PATCH 0034/1009] 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 862f317ee82d66..5a99805da62b04 100644 --- a/homeassistant/components/androidtv_remote/entity.py +++ b/homeassistant/components/androidtv_remote/entity.py @@ -16,6 +16,7 @@ class AndroidTVRemoteBaseEntity(Entity): """Android TV Remote Base Entity.""" + _attr_name = None _attr_has_entity_name = True _attr_should_poll = False From 9cace8e4bd7b6dd129f80dc83ddbcd91e7974b64 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jun 2023 13:11:17 -0400 Subject: [PATCH 0035/1009] 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 7e6c210a1646b7..526ddd2bcc7483 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -190,6 +190,7 @@ async def async_service_handle(service_call: ServiceCall) -> None: 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 2c68401376575a..1eb8074bbc1546 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -118,7 +118,9 @@ def __init__( # 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 10fd1f2406bfef..34a7b949646878 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 3474f46b09da9537aad5c5b663c6aeac13aecd4d Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 29 Jun 2023 13:13:37 -0400 Subject: [PATCH 0036/1009] 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 3801ccbecc9e8e..90ca13c5146ebe 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 @@ def __init__( 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 39e13412e909e3..baab687e64a3cd 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 d16d7437202d54..a0b3d5be29597a 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} - ), - get_value=lambda data: data.send(RoborockCommand.GET_FLOW_LED_STATUS), - check_support=lambda data: data.api.send_command( - RoborockCommand.GET_FLOW_LED_STATUS + cache_key=CacheableAttribute.flow_led_status, + update_value=lambda cache, value: cache.update_value( + {"status": 1 if value else 0} ), - 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 aab14224dc9643..851359306ad8a9 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 2863b4c90f700e..74612b98385655 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 d9c11bead74af1..eb281076825eda 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -20,11 +20,17 @@ @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 15e69cee9d9df0..6a2e1f4b5f1849 100644 --- a/tests/components/roborock/mock_data.py +++ b/tests/components/roborock/mock_data.py @@ -367,7 +367,12 @@ "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 432bad167cd4ba..eb70e04110f29a 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 3b0ba8183b3a06..bcea4e6246bb5c 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 9c079ef85b6e51..40ecdc267ed49c 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 63218adb65250e1cc3c87a130cfdc7d1e469210f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 29 Jun 2023 19:18:24 +0200 Subject: [PATCH 0037/1009] 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 509983c25f6912..891a97d4d0249f 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 7b3e7fe7c4b31b..c850bb6790b2d3 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 851359306ad8a9..8b905cbe408152 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 74612b98385655..bb50d6f1b57412 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 7252c33df8b903de65643e2168b4a73f39742c5b Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 29 Jun 2023 10:25:25 -0700 Subject: [PATCH 0038/1009] 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 5d0d2526bf2d76..86f61f0ed872e5 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -60,6 +60,7 @@ 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 2d4f0dfe0ba3ca..e667510325bcd5 100644 --- a/homeassistant/components/calendar/const.py +++ b/homeassistant/components/calendar/const.py @@ -41,3 +41,12 @@ class CalendarEntityFeature(IntFlag): } 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 972922218195df..9fdc76abe03f49 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -4,7 +4,7 @@ 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 c8f494a0071be6..7e46dc0d0bdc45 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 decb1a31181e8b2263f7d9b59f726331e57c40e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jun 2023 13:43:13 -0400 Subject: [PATCH 0039/1009] 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 4c9337e54ce57b..8e194ac27b110c 100644 --- a/homeassistant/components/ibeacon/device_tracker.py +++ b/homeassistant/components/ibeacon/device_tracker.py @@ -48,6 +48,8 @@ def _async_device_new( 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 877e58233d5b00..a8c1cf4698c447 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 fceac67a477696..0271e4a0f730f0 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 449109abd5ec731d145094f1e73cdb47903aa9d3 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 0040/1009] 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 6f00568cf2b6ba..5711aff2a4a036 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 93b4e6404bd5784e5dbbc9959ddb8fe291f15d86 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jun 2023 18:03:59 -0500 Subject: [PATCH 0041/1009] 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 2d96897ef9dd78..dbe8ac3f1ab7df 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 de47c904dd7752..404339d4f30dcb 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 b15eb343c063cb..6eaf2885d89306 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 b788ec21052616..cdc270f2e99371 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 c850bb6790b2d3..e6e506273adc44 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 8b905cbe408152..f4bd9865a772b9 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 bb50d6f1b57412..5d216e6c844adf 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 734614bddaffbeec9b1dc6f0ae051fa77c4c7cf9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Jun 2023 18:04:13 -0500 Subject: [PATCH 0042/1009] Fix device_id not set in esphome (#95580) --- homeassistant/components/esphome/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 271b0b9aa16551..fedb2edd899d50 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -405,7 +405,9 @@ async def on_connect(self) -> None: await async_connect_scanner(hass, entry, cli, entry_data) ) - _async_setup_device_registry(hass, entry, entry_data.device_info) + self.device_id = _async_setup_device_registry( + hass, entry, entry_data.device_info + ) entry_data.async_update_device_state(hass) entity_infos, services = await cli.list_entities_services() From f2f0c38fae022a7b91b2227bd3c6b0ba6f6b8154 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 0043/1009] 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 5f426fc49c521d..f52b78b5a5269b 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 1e26d5e211ad43..5cb9e594cb211c 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 b44e15415fd3bf5414bedb564683c5bcab3a0743 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 0044/1009] 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 fa1c382926e778..d7acc9788c458d 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 f4bd9865a772b9..749a064f952bf8 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 5d216e6c844adf..5761609b4b3a5b 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 e77f4191429f3075122011a14baa15d09f23e213 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Jun 2023 22:20:53 -0400 Subject: [PATCH 0045/1009] 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 084acbe73e7c36..216c5d9335e1ce 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 1dcaec4ece62416617f94a6d164cc5a1b266ce13 Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 29 Jun 2023 19:55:51 -0700 Subject: [PATCH 0046/1009] 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 52de921553544e..65d9e0b38943fc 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 749a064f952bf8..bc0d3208b35c6e 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 5761609b4b3a5b..57e7adf7964ec2 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 17ceacd0835fa4bb86f43b22eebf3dc7e2d240f4 Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 29 Jun 2023 20:00:17 -0700 Subject: [PATCH 0047/1009] 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 e2791f6000f3eb..db2a8d9512ed4a 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -18,18 +18,11 @@ ) 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 send_text_command(call: ServiceCall) -> 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 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: 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 @@ def attribution(self): @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 @@ async def async_process( 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 b93a3be93f2fde..b4f617ca029258 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.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 @@ async def async_step_init( 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 c9f86160bb4c13..d63aec0ebd5309 100644 --- a/homeassistant/components/google_assistant_sdk/const.py +++ b/homeassistant/components/google_assistant_sdk/const.py @@ -5,7 +5,6 @@ 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 d4c85be91e50c0..66a2b975b5e15a 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 1d350a8fe4f098..c65477b18b1d31 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 25066f73b6dedf..99f264e4a3a0f2 100644 --- a/tests/components/google_assistant_sdk/test_init.py +++ b/tests/components/google_assistant_sdk/test_init.py @@ -9,8 +9,9 @@ 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 78de5f8e3e8dbfb07be25d802ef248d87f9a996c Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 30 Jun 2023 19:37:57 +1000 Subject: [PATCH 0048/1009] 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 6170bd165e9ada..fa9f609ba10605 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 abf6e0e44d9e9c1fe5567863011f4c9698e98286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 30 Jun 2023 11:39:10 +0200 Subject: [PATCH 0049/1009] Refactor Airzone Cloud _attr_has_entity_name in sensor (#95609) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit airzone_cloud: sensor: refactor _attr_has_entity_name Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone_cloud/sensor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone_cloud/sensor.py b/homeassistant/components/airzone_cloud/sensor.py index 90fbf8493897d8..c33838029b41f2 100644 --- a/homeassistant/components/airzone_cloud/sensor.py +++ b/homeassistant/components/airzone_cloud/sensor.py @@ -141,6 +141,8 @@ def _async_update_attrs(self) -> None: class AirzoneAidooSensor(AirzoneAidooEntity, AirzoneSensor): """Define an Airzone Cloud Aidoo sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: AirzoneUpdateCoordinator, @@ -151,7 +153,6 @@ def __init__( """Initialize.""" super().__init__(coordinator, aidoo_id, aidoo_data) - self._attr_has_entity_name = True self._attr_unique_id = f"{aidoo_id}_{description.key}" self.entity_description = description @@ -161,6 +162,8 @@ def __init__( class AirzoneWebServerSensor(AirzoneWebServerEntity, AirzoneSensor): """Define an Airzone Cloud WebServer sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: AirzoneUpdateCoordinator, @@ -171,7 +174,6 @@ def __init__( """Initialize.""" super().__init__(coordinator, ws_id, ws_data) - self._attr_has_entity_name = True self._attr_unique_id = f"{ws_id}_{description.key}" self.entity_description = description @@ -181,6 +183,8 @@ def __init__( class AirzoneZoneSensor(AirzoneZoneEntity, AirzoneSensor): """Define an Airzone Cloud Zone sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: AirzoneUpdateCoordinator, @@ -191,7 +195,6 @@ def __init__( """Initialize.""" super().__init__(coordinator, zone_id, zone_data) - self._attr_has_entity_name = True self._attr_unique_id = f"{zone_id}_{description.key}" self.entity_description = description From 4ac92d755e4b802aa1a9802b1996938a128b036c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 30 Jun 2023 12:58:07 +0200 Subject: [PATCH 0050/1009] Add config flow for zodiac (#95447) * Add config flow for zodiac * Add config flow for zodiac * Fix feedback --- homeassistant/components/zodiac/__init__.py | 29 +++++++- .../components/zodiac/config_flow.py | 31 ++++++++ homeassistant/components/zodiac/const.py | 1 + homeassistant/components/zodiac/manifest.json | 3 +- homeassistant/components/zodiac/sensor.py | 27 ++++--- homeassistant/components/zodiac/strings.json | 16 +++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 4 +- tests/components/zodiac/test_config_flow.py | 70 +++++++++++++++++++ tests/components/zodiac/test_sensor.py | 12 +++- 10 files changed, 177 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/zodiac/config_flow.py create mode 100644 tests/components/zodiac/test_config_flow.py diff --git a/homeassistant/components/zodiac/__init__.py b/homeassistant/components/zodiac/__init__.py index 35d4d2eefbfb0c..892bcac5bf9177 100644 --- a/homeassistant/components/zodiac/__init__.py +++ b/homeassistant/components/zodiac/__init__.py @@ -1,9 +1,10 @@ """The zodiac component.""" import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -16,8 +17,32 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the zodiac component.""" + + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2024.1.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + hass.async_create_task( - async_load_platform(hass, Platform.SENSOR, DOMAIN, {}, config) + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) ) return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Load a config entry.""" + await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR]) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, [Platform.SENSOR]) diff --git a/homeassistant/components/zodiac/config_flow.py b/homeassistant/components/zodiac/config_flow.py new file mode 100644 index 00000000000000..ebc0a819d1dff8 --- /dev/null +++ b/homeassistant/components/zodiac/config_flow.py @@ -0,0 +1,31 @@ +"""Config flow to configure the Zodiac integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult + +from .const import DEFAULT_NAME, DOMAIN + + +class ZodiacConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for Zodiac.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is not None: + return self.async_create_entry(title=DEFAULT_NAME, data={}) + + return self.async_show_form(step_id="user") + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle import from configuration.yaml.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/zodiac/const.py b/homeassistant/components/zodiac/const.py index c3e7f13d5e372b..f50e108c2aa782 100644 --- a/homeassistant/components/zodiac/const.py +++ b/homeassistant/components/zodiac/const.py @@ -1,5 +1,6 @@ """Constants for Zodiac.""" DOMAIN = "zodiac" +DEFAULT_NAME = "Zodiac" # Signs SIGN_ARIES = "aries" diff --git a/homeassistant/components/zodiac/manifest.json b/homeassistant/components/zodiac/manifest.json index ceacbf1645a254..88f3d7fadef9e9 100644 --- a/homeassistant/components/zodiac/manifest.json +++ b/homeassistant/components/zodiac/manifest.json @@ -2,7 +2,8 @@ "domain": "zodiac", "name": "Zodiac", "codeowners": ["@JulienTant"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zodiac", - "iot_class": "local_polling", + "iot_class": "calculated", "quality_scale": "silver" } diff --git a/homeassistant/components/zodiac/sensor.py b/homeassistant/components/zodiac/sensor.py index f63c844701f087..d9b306da4dd855 100644 --- a/homeassistant/components/zodiac/sensor.py +++ b/homeassistant/components/zodiac/sensor.py @@ -2,14 +2,17 @@ from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.dt import as_local, utcnow from .const import ( ATTR_ELEMENT, ATTR_MODALITY, + DEFAULT_NAME, DOMAIN, ELEMENT_AIR, ELEMENT_EARTH, @@ -159,23 +162,21 @@ } -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Zodiac sensor platform.""" - if discovery_info is None: - return + """Initialize the entries.""" - async_add_entities([ZodiacSensor()], True) + async_add_entities([ZodiacSensor(entry_id=entry.entry_id)], True) class ZodiacSensor(SensorEntity): """Representation of a Zodiac sensor.""" - _attr_name = "Zodiac" + _attr_name = None + _attr_has_entity_name = True _attr_device_class = SensorDeviceClass.ENUM _attr_options = [ SIGN_AQUARIUS, @@ -194,6 +195,14 @@ class ZodiacSensor(SensorEntity): _attr_translation_key = "sign" _attr_unique_id = DOMAIN + def __init__(self, entry_id: str) -> None: + """Initialize Zodiac sensor.""" + self._attr_device_info = DeviceInfo( + name=DEFAULT_NAME, + identifiers={(DOMAIN, entry_id)}, + entry_type=DeviceEntryType.SERVICE, + ) + async def async_update(self) -> None: """Get the time and updates the state.""" today = as_local(utcnow()).date() diff --git a/homeassistant/components/zodiac/strings.json b/homeassistant/components/zodiac/strings.json index cbae6ead433394..8cf0e22237e24f 100644 --- a/homeassistant/components/zodiac/strings.json +++ b/homeassistant/components/zodiac/strings.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "user": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + }, "entity": { "sensor": { "sign": { @@ -18,5 +28,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "The Zodiac YAML configuration is being removed", + "description": "Configuring Zodiac using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Zodiac YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3145c5cdc49446..2925ea3425cc53 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -534,6 +534,7 @@ "zerproc", "zeversolar", "zha", + "zodiac", "zwave_js", "zwave_me", ], diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 26833d623683aa..98571b6905e48f 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6541,8 +6541,8 @@ "zodiac": { "name": "Zodiac", "integration_type": "hub", - "config_flow": false, - "iot_class": "local_polling" + "config_flow": true, + "iot_class": "calculated" }, "zoneminder": { "name": "ZoneMinder", diff --git a/tests/components/zodiac/test_config_flow.py b/tests/components/zodiac/test_config_flow.py new file mode 100644 index 00000000000000..18a512e0b455d0 --- /dev/null +++ b/tests/components/zodiac/test_config_flow.py @@ -0,0 +1,70 @@ +"""Tests for the Zodiac config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.zodiac.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_full_user_flow(hass: HomeAssistant) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "user" + + with patch( + "homeassistant.components.zodiac.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + + assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("title") == "Zodiac" + assert result.get("data") == {} + assert result.get("options") == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) +async def test_single_instance_allowed( + hass: HomeAssistant, + source: str, +) -> None: + """Test we abort if already setup.""" + mock_config_entry = MockConfigEntry(domain=DOMAIN) + + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source} + ) + + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "single_instance_allowed" + + +async def test_import_flow( + hass: HomeAssistant, +) -> None: + """Test the import configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={}, + ) + + assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("title") == "Zodiac" + assert result.get("data") == {} + assert result.get("options") == {} diff --git a/tests/components/zodiac/test_sensor.py b/tests/components/zodiac/test_sensor.py index dbb1d2739a5da6..9fa151c87d567d 100644 --- a/tests/components/zodiac/test_sensor.py +++ b/tests/components/zodiac/test_sensor.py @@ -24,6 +24,8 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.common import MockConfigEntry + DAY1 = datetime(2020, 11, 15, tzinfo=dt_util.UTC) DAY2 = datetime(2020, 4, 20, tzinfo=dt_util.UTC) DAY3 = datetime(2020, 4, 21, tzinfo=dt_util.UTC) @@ -37,13 +39,17 @@ (DAY3, SIGN_TAURUS, ELEMENT_EARTH, MODALITY_FIXED), ], ) -async def test_zodiac_day(hass: HomeAssistant, now, sign, element, modality) -> None: +async def test_zodiac_day( + hass: HomeAssistant, now: datetime, sign: str, element: str, modality: str +) -> None: """Test the zodiac sensor.""" hass.config.set_time_zone("UTC") - config = {DOMAIN: {}} + MockConfigEntry( + domain=DOMAIN, + ).add_to_hass(hass) with patch("homeassistant.components.zodiac.sensor.utcnow", return_value=now): - assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() state = hass.states.get("sensor.zodiac") From 8510d3ad69db55413123a9919c1cfb2cec5eb20d 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 0051/1009] 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 9afdff43ef0c85..4ff63a254553f4 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 522d2496dff09a01f0309de0856609cb09667890 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 30 Jun 2023 15:00:15 +0200 Subject: [PATCH 0052/1009] Update typing-extensions to 4.7.0 (#95539) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 4 ++-- requirements.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e6e506273adc44..98e2df2d97b844 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -46,7 +46,7 @@ PyYAML==6.0 requests==2.31.0 scapy==2.5.0 SQLAlchemy==2.0.15 -typing_extensions>=4.6.3,<5.0 +typing-extensions>=4.7.0,<5.0 ulid-transform==0.7.2 voluptuous-serialize==2.6.0 voluptuous==0.13.1 diff --git a/pyproject.toml b/pyproject.toml index e857abd31e55d5..8256ce2d06060d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ "python-slugify==4.0.1", "PyYAML==6.0", "requests==2.31.0", - "typing_extensions>=4.6.3,<5.0", + "typing-extensions>=4.7.0,<5.0", "ulid-transform==0.7.2", "voluptuous==0.13.1", "voluptuous-serialize==2.6.0", @@ -190,7 +190,7 @@ disable = [ "invalid-character-nul", # PLE2514 "invalid-character-sub", # PLE2512 "invalid-character-zero-width-space", # PLE2515 - "logging-too-few-args", # PLE1206 + "logging-too-few-args", # PLE1206 "logging-too-many-args", # PLE1205 "missing-format-string-key", # F524 "mixed-format-string", # F506 diff --git a/requirements.txt b/requirements.txt index f4f2608b597200..31e5812dadf759 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ pip>=21.3.1,<23.2 python-slugify==4.0.1 PyYAML==6.0 requests==2.31.0 -typing_extensions>=4.6.3,<5.0 +typing-extensions>=4.7.0,<5.0 ulid-transform==0.7.2 voluptuous==0.13.1 voluptuous-serialize==2.6.0 From 39e0662fc88b4fa9fdaa76bf40695fac35006001 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 30 Jun 2023 08:35:19 -0600 Subject: [PATCH 0053/1009] Add ability to configure map icons for PurpleAir (#86124) --- .../components/purpleair/__init__.py | 29 +++++++++++---- .../components/purpleair/config_flow.py | 36 ++++++++++++++++--- homeassistant/components/purpleair/const.py | 1 + .../components/purpleair/strings.json | 9 ++++- .../components/purpleair/test_config_flow.py | 26 ++++++++++++++ 5 files changed, 90 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/purpleair/__init__.py b/homeassistant/components/purpleair/__init__.py index c90f4c9031ce2f..f5c4090dc87077 100644 --- a/homeassistant/components/purpleair/__init__.py +++ b/homeassistant/components/purpleair/__init__.py @@ -1,6 +1,9 @@ """The PurpleAir integration.""" from __future__ import annotations +from collections.abc import Mapping +from typing import Any + from aiopurpleair.models.sensors import SensorModel from homeassistant.config_entries import ConfigEntry @@ -9,7 +12,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN +from .const import CONF_SHOW_ON_MAP, DOMAIN from .coordinator import PurpleAirDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] @@ -60,16 +63,30 @@ def __init__( self._attr_device_info = DeviceInfo( configuration_url=self.coordinator.async_get_map_url(sensor_index), hw_version=self.sensor_data.hardware, - identifiers={(DOMAIN, str(self._sensor_index))}, + identifiers={(DOMAIN, str(sensor_index))}, manufacturer="PurpleAir, Inc.", model=self.sensor_data.model, name=self.sensor_data.name, sw_version=self.sensor_data.firmware_version, ) - self._attr_extra_state_attributes = { - ATTR_LATITUDE: self.sensor_data.latitude, - ATTR_LONGITUDE: self.sensor_data.longitude, - } + self._entry = entry + + @property + def extra_state_attributes(self) -> Mapping[str, Any]: + """Return entity specific state attributes.""" + attrs = {} + + # Displaying the geography on the map relies upon putting the latitude/longitude + # in the entity attributes with "latitude" and "longitude" as the keys. + # Conversely, we can hide the location on the map by using other keys, like + # "lati" and "long": + if self._entry.options.get(CONF_SHOW_ON_MAP): + attrs[ATTR_LATITUDE] = self.sensor_data.latitude + attrs[ATTR_LONGITUDE] = self.sensor_data.longitude + else: + attrs["lati"] = self.sensor_data.latitude + attrs["long"] = self.sensor_data.longitude + return attrs @property def sensor_data(self) -> SensorModel: diff --git a/homeassistant/components/purpleair/config_flow.py b/homeassistant/components/purpleair/config_flow.py index 604bcb28c0e3e9..c7988c02e6ae0a 100644 --- a/homeassistant/components/purpleair/config_flow.py +++ b/homeassistant/components/purpleair/config_flow.py @@ -31,7 +31,7 @@ SelectSelectorMode, ) -from .const import CONF_SENSOR_INDICES, DOMAIN, LOGGER +from .const import CONF_SENSOR_INDICES, CONF_SHOW_ON_MAP, DOMAIN, LOGGER CONF_DISTANCE = "distance" CONF_NEARBY_SENSOR_OPTIONS = "nearby_sensor_options" @@ -318,6 +318,22 @@ def __init__(self, config_entry: ConfigEntry) -> None: self._flow_data: dict[str, Any] = {} self.config_entry = config_entry + @property + def settings_schema(self) -> vol.Schema: + """Return the settings schema.""" + return vol.Schema( + { + vol.Optional( + CONF_SHOW_ON_MAP, + description={ + "suggested_value": self.config_entry.options.get( + CONF_SHOW_ON_MAP + ) + }, + ): bool + } + ) + async def async_step_add_sensor( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -352,7 +368,7 @@ async def async_step_add_sensor( async def async_step_choose_sensor( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Handle the selection of a sensor.""" + """Choose a sensor.""" if user_input is None: options = self._flow_data.pop(CONF_NEARBY_SENSOR_OPTIONS) return self.async_show_form( @@ -375,13 +391,13 @@ async def async_step_init( """Manage the options.""" return self.async_show_menu( step_id="init", - menu_options=["add_sensor", "remove_sensor"], + menu_options=["add_sensor", "remove_sensor", "settings"], ) async def async_step_remove_sensor( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Add a sensor.""" + """Remove a sensor.""" if user_input is None: return self.async_show_form( step_id="remove_sensor", @@ -437,3 +453,15 @@ def async_device_entity_state_changed(_: Event) -> None: options[CONF_SENSOR_INDICES].remove(removed_sensor_index) return self.async_create_entry(data=options) + + async def async_step_settings( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage settings.""" + if user_input is None: + return self.async_show_form( + step_id="settings", data_schema=self.settings_schema + ) + + options = deepcopy({**self.config_entry.options}) + return self.async_create_entry(data=options | user_input) diff --git a/homeassistant/components/purpleair/const.py b/homeassistant/components/purpleair/const.py index 60f51a9e7ddf9c..e3ea7807a21f00 100644 --- a/homeassistant/components/purpleair/const.py +++ b/homeassistant/components/purpleair/const.py @@ -7,3 +7,4 @@ CONF_READ_KEY = "read_key" CONF_SENSOR_INDICES = "sensor_indices" +CONF_SHOW_ON_MAP = "show_on_map" diff --git a/homeassistant/components/purpleair/strings.json b/homeassistant/components/purpleair/strings.json index 3d18fef3906468..836496d0ca8380 100644 --- a/homeassistant/components/purpleair/strings.json +++ b/homeassistant/components/purpleair/strings.json @@ -79,7 +79,8 @@ "init": { "menu_options": { "add_sensor": "Add sensor", - "remove_sensor": "Remove sensor" + "remove_sensor": "Remove sensor", + "settings": "Settings" } }, "remove_sensor": { @@ -90,6 +91,12 @@ "data_description": { "sensor_device_id": "The sensor to remove" } + }, + "settings": { + "title": "Settings", + "data": { + "show_on_map": "Show configured sensor locations on the map" + } } }, "error": { diff --git a/tests/components/purpleair/test_config_flow.py b/tests/components/purpleair/test_config_flow.py index ce911183dfd956..503ba23e052d36 100644 --- a/tests/components/purpleair/test_config_flow.py +++ b/tests/components/purpleair/test_config_flow.py @@ -302,3 +302,29 @@ async def test_options_remove_sensor( # Unload to make sure the update does not run after the # mock is removed. await hass.config_entries.async_unload(config_entry.entry_id) + + +async def test_options_settings( + hass: HomeAssistant, config_entry, setup_config_entry +) -> None: + """Test setting settings via the options flow.""" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"next_step_id": "settings"} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "settings" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"show_on_map": True} + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == { + "sensor_indices": [TEST_SENSOR_INDEX1], + "show_on_map": True, + } + + assert config_entry.options["show_on_map"] is True From 0f1f3bce871cf6dc89c81ea79dcf528f16eb7eb9 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 0054/1009] 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 5ebd7e34409ad1..046ea237560233 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 beac3c713bba0d54eaa53e253f9e08475623e1bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jun 2023 10:21:10 -0500 Subject: [PATCH 0055/1009] 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 d93d7c48823b15..fdd7537e9e1cb0 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 7eb26cb9c9f56bdfb238a032205cfdd273c24063 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 30 Jun 2023 17:33:50 +0200 Subject: [PATCH 0056/1009] 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 2546f7629122dc..66a2e3b0db59e9 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 17d7b820d45307..afe017bfcc714f 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 507b1284362f61..d504040ee90297 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 be69897431fb34..539b89a5546898 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 039b24230998b7..c110b3fd558fff 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 11821773938920..1e238783221b63 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 ab83e3a20c1c3a..14bdf4e0caf784 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 bbc8738c4ae7a4..9e4f92e8c98baa 100644 --- a/homeassistant/components/advantage_air/entity.py +++ b/homeassistant/components/advantage_air/entity.py @@ -84,6 +84,8 @@ def _zone(self) -> dict[str, Any]: 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 13a77d5cab352f..7815354dd928f4 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 2ab23ff2d37b6b..038e71750ddeb5 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -80,6 +80,7 @@ def __init__( 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 009536a9adb1b5..b87448658981c2 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 a70b9c134d0290..5512bcd1176ff8 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 47afdf1539b2bd..dc403611da2e2e 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 eafd1e63b1fef4..d2608ed43c7a02 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 @@ 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 24b1d3545deafd..9b96e58da60062 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 47da87306a34b5..f74ec04476ff6f 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 bec72b5d32df92..cddcabc841ec2b 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 dd33da562978b0..db31baa53a6cdf 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 2025e1a2a6cd52..bcd8e97582324b 100644 --- a/homeassistant/components/jellyfin/media_player.py +++ b/homeassistant/components/jellyfin/media_player.py @@ -90,6 +90,7 @@ def __init__( 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 1957adfc6eb12d..cd0e9ab21a23b0 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 e33eef74c4872e..45f797a5aaa7e1 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 9cf691abdb8659eb3bb7cba23a3bcc6db25ee849 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 30 Jun 2023 17:34:35 +0200 Subject: [PATCH 0057/1009] 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 a2854479abdf56..e5f70bc25a0504 100644 --- a/homeassistant/components/melnor/switch.py +++ b/homeassistant/components/melnor/switch.py @@ -45,6 +45,7 @@ class MelnorSwitchEntityDescription( 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 2d06b20a30a358..b23abb54f8b6ce 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 22bffad44d83ea..54c2d16fb2b35e 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 f7941d1f02d9b1..36626c2324e46a 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 4f93fd3407e675..1ee4274e5bb021 100644 --- a/homeassistant/components/prusalink/sensor.py +++ b/homeassistant/components/prusalink/sensor.py @@ -47,6 +47,7 @@ class 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 38f3c03fb03479..e915c52c9dc221 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -67,6 +67,7 @@ def __init__( 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 120ab77c3b3f36..c439f647da5414 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 0edd6f82195776..3544abcfdd1ad5 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 57919ed1feba6b..3ef3bbdc5ae626 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 a6083e51430249..77776704a6053e 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 376c61c34b68c0822cd8b647677428ee8ca7da94 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Jun 2023 10:37:04 -0500 Subject: [PATCH 0058/1009] 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 404339d4f30dcb..085437fb02e24b 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 bc0d3208b35c6e..a58b9f7e0d1954 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 57e7adf7964ec2..b2989389fc2dc2 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 d4e40ed73f5df1a698e2f0855aa2aed6978f22b6 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 1 Jul 2023 03:52:52 +1000 Subject: [PATCH 0059/1009] 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 27eaef09b43b6a..4c440610838f6a 100644 --- a/homeassistant/components/advantage_air/diagnostics.py +++ b/homeassistant/components/advantage_air/diagnostics.py @@ -9,17 +9,30 @@ 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 00000000000000..a472d4fa1fccff --- /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 00000000000000..ebd026c6cc7ae1 --- /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 9280dc69aecafb9b98415afeb61e80ec3c6f2534 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Jun 2023 13:54:20 -0400 Subject: [PATCH 0060/1009] Default device name to config entry title (#95547) * Default device name to config entry title * Only apply name default if device info provided * Fix logic detecting type of device info --- homeassistant/helpers/entity_platform.py | 25 +++++++----- tests/helpers/test_entity_platform.py | 50 ++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 675d368873af59..66a74edf8f91a1 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -608,19 +608,12 @@ async def _async_add_entity( # noqa: C901 entity.add_to_platform_abort() return - if self.config_entry is not None: - config_entry_id: str | None = self.config_entry.entry_id - else: - config_entry_id = None - device_info = entity.device_info device_id = None device = None - if config_entry_id is not None and device_info is not None: - processed_dev_info: dict[str, str | None] = { - "config_entry_id": config_entry_id - } + if self.config_entry and device_info is not None: + processed_dev_info: dict[str, str | None] = {} for key in ( "connections", "default_manufacturer", @@ -641,6 +634,17 @@ async def _async_add_entity( # noqa: C901 key # type: ignore[literal-required] ] + if ( + # device info that is purely meant for linking doesn't need default name + any( + key not in {"identifiers", "connections"} + for key in (processed_dev_info) + ) + and "default_name" not in processed_dev_info + and not processed_dev_info.get("name") + ): + processed_dev_info["name"] = self.config_entry.title + if "configuration_url" in device_info: if device_info["configuration_url"] is None: processed_dev_info["configuration_url"] = None @@ -660,7 +664,8 @@ async def _async_add_entity( # noqa: C901 try: device = device_registry.async_get_or_create( - **processed_dev_info # type: ignore[arg-type] + config_entry_id=self.config_entry.entry_id, + **processed_dev_info, # type: ignore[arg-type] ) device_id = device.id except RequiredParameterMissing: diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 46806510f4026e..df4f4d1c6439a9 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1827,3 +1827,53 @@ async def async_setup_entry(hass, config_entry, async_add_entities): assert len(hass.states.async_entity_ids()) == 1 assert registry.async_get(expected_entity_id) is not None + + +@pytest.mark.parametrize( + ("entity_device_name", "entity_device_default_name", "expected_device_name"), + [ + (None, None, "Mock Config Entry Title"), + ("", None, "Mock Config Entry Title"), + (None, "Hello", "Hello"), + ("Mock Device Name", None, "Mock Device Name"), + ], +) +async def test_device_name_defaulting_config_entry( + hass: HomeAssistant, + entity_device_name: str, + entity_device_default_name: str, + expected_device_name: str, +) -> None: + """Test setting the device name based on input info.""" + device_info = { + "identifiers": {("hue", "1234")}, + "name": entity_device_name, + } + + if entity_device_default_name: + device_info["default_name"] = entity_device_default_name + + class DeviceNameEntity(Entity): + _attr_unique_id = "qwer" + _attr_device_info = device_info + + async def async_setup_entry(hass, config_entry, async_add_entities): + """Mock setup entry method.""" + async_add_entities([DeviceNameEntity()]) + return True + + platform = MockPlatform(async_setup_entry=async_setup_entry) + config_entry = MockConfigEntry( + title="Mock Config Entry Title", entry_id="super-mock-id" + ) + entity_platform = MockEntityPlatform( + hass, platform_name=config_entry.domain, platform=platform + ) + + assert await entity_platform.async_setup_entry(config_entry) + await hass.async_block_till_done() + + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_device({("hue", "1234")}) + assert device is not None + assert device.name == expected_device_name From 958359260eabab3fa6e68f3a8a9024595b6d6ce4 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 30 Jun 2023 19:55:03 +0200 Subject: [PATCH 0061/1009] 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 891a97d4d0249f..4a1edd4096e302 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 98e2df2d97b844..84b67ded6ae299 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 a58b9f7e0d1954..ef07e91eb7d105 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 b2989389fc2dc2..9b22fd45f8816b 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 6b8ae0ec8630d7d9d4a6e69e018bc76e8e12e801 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 30 Jun 2023 13:06:26 -0500 Subject: [PATCH 0062/1009] 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 5bd270ccfd500e..b64b74c5fa69ba 100644 --- a/homeassistant/components/conversation/trigger.py +++ b/homeassistant/components/conversation/trigger.py @@ -3,6 +3,7 @@ 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 .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 74a5e4df8e2f0f..522162fa457df9 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 11146ff40b410523907d6988bf639d1d36068dd9 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 0063/1009] 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 793e8edc7699c0..af04da27406c28 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 513e9597572b30..2d1d7a93afc169 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 0431e031bafadeb1ebcda8abcd26a5e4f9c786b1 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 0064/1009] 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 cf0e6e91ffb341..cf2a6da9e08afe 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 c472ead4fd5246a279791bea4f15a476f548688f 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 0065/1009] 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 f7b8c9c097c7b9..09f928303bf383 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 2180d0aed7f278..e26781029c5d7a 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 c6210b68bd2661f0e5e0b3ddcf9a50dafdb0e5d7 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 0066/1009] 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 ad0f96dd540aa0..af4248e5e3ba45 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 5b3734bd1be91b..a552d401681b3f 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 982a52b91d7a9b9d4c68b7c875d1f726e4d3b1cc Mon Sep 17 00:00:00 2001 From: Dave Pearce Date: Fri, 30 Jun 2023 15:04:23 -0400 Subject: [PATCH 0067/1009] Add unique_id to Wirelesstag entities. (#95631) * Add unique_id to Wirelesstag entities. * Update homeassistant/components/wirelesstag/binary_sensor.py Co-authored-by: Paulus Schoutsen * Update homeassistant/components/wirelesstag/sensor.py Co-authored-by: Paulus Schoutsen * Update homeassistant/components/wirelesstag/switch.py Co-authored-by: Paulus Schoutsen --------- Co-authored-by: Paulus Schoutsen --- homeassistant/components/wirelesstag/binary_sensor.py | 1 + homeassistant/components/wirelesstag/sensor.py | 1 + homeassistant/components/wirelesstag/switch.py | 1 + 3 files changed, 3 insertions(+) diff --git a/homeassistant/components/wirelesstag/binary_sensor.py b/homeassistant/components/wirelesstag/binary_sensor.py index 82c3a25590adf2..711c2987735b9d 100644 --- a/homeassistant/components/wirelesstag/binary_sensor.py +++ b/homeassistant/components/wirelesstag/binary_sensor.py @@ -100,6 +100,7 @@ def __init__(self, api, tag, sensor_type): super().__init__(api, tag) self._sensor_type = sensor_type self._name = f"{self._tag.name} {self.event.human_readable_name}" + self._attr_unique_id = f"{self.tag_id}_{self._sensor_type}" async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/homeassistant/components/wirelesstag/sensor.py b/homeassistant/components/wirelesstag/sensor.py index e4505e59666a06..fd9a7898f920f8 100644 --- a/homeassistant/components/wirelesstag/sensor.py +++ b/homeassistant/components/wirelesstag/sensor.py @@ -100,6 +100,7 @@ def __init__(self, api, tag, description): self._sensor_type = description.key self.entity_description = description self._name = self._tag.name + self._attr_unique_id = f"{self.tag_id}_{self._sensor_type}" # I want to see entity_id as: # sensor.wirelesstag_bedroom_temperature diff --git a/homeassistant/components/wirelesstag/switch.py b/homeassistant/components/wirelesstag/switch.py index 26c7d9384a6b6a..df0f72aca186c1 100644 --- a/homeassistant/components/wirelesstag/switch.py +++ b/homeassistant/components/wirelesstag/switch.py @@ -82,6 +82,7 @@ def __init__(self, api, tag, description: SwitchEntityDescription) -> None: super().__init__(api, tag) self.entity_description = description self._name = f"{self._tag.name} {description.name}" + self._attr_unique_id = f"{self.tag_id}_{description.key}" def turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" From 8ddc7f208909484cdd6484ffc92e6f96244d1822 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 0068/1009] 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 d7acc9788c458d..293822987c30ae 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 ef07e91eb7d105..fe24b288f63106 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 9b22fd45f8816b..306606bc46c6be 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 3fbc026d5a3dbee78778ed42548a0c2d9c0d026c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Jun 2023 16:13:22 -0400 Subject: [PATCH 0069/1009] Remove passing MAC as an identifier for Fritz (#95648) --- homeassistant/components/fritz/switch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 5b8c40485306cf..1352d9cb42e08e 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -518,7 +518,6 @@ def __init__(self, avm_wrapper: AvmWrapper, device: FritzDevice) -> None: default_manufacturer="AVM", default_model="FRITZ!Box Tracked device", default_name=device.hostname, - identifiers={(DOMAIN, self._mac)}, via_device=( DOMAIN, avm_wrapper.unique_id, From 8b159d0f479b889d82be8f88ded85f444a2fbc46 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sat, 1 Jul 2023 05:59:01 +0200 Subject: [PATCH 0070/1009] 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 f1e3d96a21906b..16ea12a5d9669d 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -119,3 +119,5 @@ def __init__( # 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 591f1ee338af83209efd595f5d94407bc8dc3e0a 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 0071/1009] 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 37225fc052fbf3..96ef152307d1e7 100644 --- a/homeassistant/components/bmw_connected_drive/const.py +++ b/homeassistant/components/bmw_connected_drive/const.py @@ -21,3 +21,9 @@ "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 f6354422312d4c..4a586aab3730e7 100644 --- a/homeassistant/components/bmw_connected_drive/coordinator.py +++ b/homeassistant/components/bmw_connected_drive/coordinator.py @@ -15,10 +15,8 @@ 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 @@ def __init__(self, hass: HomeAssistant, *, entry: ConfigEntry) -> 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 c8d4225117131af867f0d88548c45f5049abec02 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 1 Jul 2023 06:05:28 -0400 Subject: [PATCH 0072/1009] Met: use correct device info keys (#95644) --- homeassistant/components/met/weather.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 05642c12991cd0..20822dc99732ae 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -218,7 +218,7 @@ def forecast(self) -> list[Forecast] | None: def device_info(self) -> DeviceInfo: """Device info.""" return DeviceInfo( - default_name="Forecast", + name="Forecast", entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN,)}, # type: ignore[arg-type] manufacturer="Met.no", From 2191fb21fa6641e6d6ae4a78ded4d54ce68dd04a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 1 Jul 2023 06:06:01 -0400 Subject: [PATCH 0073/1009] Rainbird: use correct device info keys (#95645) --- homeassistant/components/rainbird/coordinator.py | 2 +- homeassistant/components/rainbird/switch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rainbird/coordinator.py b/homeassistant/components/rainbird/coordinator.py index 14598921a61c6a..b503e72d3a6e4c 100644 --- a/homeassistant/components/rainbird/coordinator.py +++ b/homeassistant/components/rainbird/coordinator.py @@ -69,7 +69,7 @@ def serial_number(self) -> str: def device_info(self) -> DeviceInfo: """Return information about the device.""" return DeviceInfo( - default_name=f"{MANUFACTURER} Controller", + name=f"{MANUFACTURER} Controller", identifiers={(DOMAIN, self._serial_number)}, manufacturer=MANUFACTURER, ) diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index e915c52c9dc221..ceca9c71c36480 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -73,7 +73,7 @@ def __init__( self._duration_minutes = duration_minutes self._attr_unique_id = f"{coordinator.serial_number}-{zone}" self._attr_device_info = DeviceInfo( - default_name=f"{MANUFACTURER} Sprinkler {zone}", + name=f"{MANUFACTURER} Sprinkler {zone}", identifiers={(DOMAIN, self._attr_unique_id)}, manufacturer=MANUFACTURER, via_device=(DOMAIN, coordinator.serial_number), From 62ac7973c29c2987882ea22951b27d4eaced9a6a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 1 Jul 2023 06:06:27 -0400 Subject: [PATCH 0074/1009] VeSync: use correct device info keys (#95646) --- homeassistant/components/vesync/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index 752a65ff051dca..f0684b6b01dfa7 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -85,7 +85,7 @@ def device_info(self) -> DeviceInfo: identifiers={(DOMAIN, self.base_unique_id)}, name=self.base_name, model=self.device.device_type, - default_manufacturer="VeSync", + manufacturer="VeSync", sw_version=self.device.current_firm_version, ) From 923677dae379a7552b0b0821d7a1a554271f08ed Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 1 Jul 2023 06:06:46 -0400 Subject: [PATCH 0075/1009] Tesla Wall Connector: use correct device info keys (#95647) --- homeassistant/components/tesla_wall_connector/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tesla_wall_connector/__init__.py b/homeassistant/components/tesla_wall_connector/__init__.py index 179576334a97e2..dfb439133f6b23 100644 --- a/homeassistant/components/tesla_wall_connector/__init__.py +++ b/homeassistant/components/tesla_wall_connector/__init__.py @@ -148,10 +148,10 @@ def device_info(self) -> DeviceInfo: """Return information about the device.""" return DeviceInfo( identifiers={(DOMAIN, self.wall_connector_data.serial_number)}, - default_name=WALLCONNECTOR_DEVICE_NAME, + name=WALLCONNECTOR_DEVICE_NAME, model=self.wall_connector_data.part_number, sw_version=self.wall_connector_data.firmware_version, - default_manufacturer="Tesla", + manufacturer="Tesla", ) From 432bfffef93514d1e1ca954a3e4213222cd3cd7a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 1 Jul 2023 12:12:24 +0200 Subject: [PATCH 0076/1009] Update ruff pre-commit repo (#95603) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d6cd3f43b105c0..c662c6754f4c79 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: - - repo: https://github.com/charliermarsh/ruff-pre-commit + - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.0.272 hooks: - id: ruff From c81b6255c2d29a64de94782829637de356130e7d Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 1 Jul 2023 13:16:45 +0200 Subject: [PATCH 0077/1009] Use `async_on_remove` for KNX entities removal (#95658) * Use `async_on_remove` for KNX entities removal * review --- homeassistant/components/knx/knx_entity.py | 7 ++----- homeassistant/components/knx/sensor.py | 11 ++++++----- tests/components/knx/test_interface_device.py | 8 ++++---- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py index fff7f9b9f4f68c..9545510e635de0 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -42,8 +42,5 @@ async def after_update_callback(self, device: XknxDevice) -> None: async def async_added_to_hass(self) -> None: """Store register state change callback.""" self._device.register_device_updated_cb(self.after_update_callback) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect device object when removed.""" - # will also remove all callbacks - self._device.shutdown() + # will remove all callbacks and xknx tasks + self.async_on_remove(self._device.shutdown) diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 4400c30419356f..dbfe8e9bd5e1ff 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -4,6 +4,7 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta +from functools import partial from typing import Any from xknx import XKNX @@ -221,9 +222,9 @@ async def async_added_to_hass(self) -> None: self.knx.xknx.connection_manager.register_connection_state_changed_cb( self.after_update_callback ) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect device object when removed.""" - self.knx.xknx.connection_manager.unregister_connection_state_changed_cb( - self.after_update_callback + self.async_on_remove( + partial( + self.knx.xknx.connection_manager.unregister_connection_state_changed_cb, + self.after_update_callback, + ) ) diff --git a/tests/components/knx/test_interface_device.py b/tests/components/knx/test_interface_device.py index 9fb21b9f9b47fe..12ae0ac7d0ea06 100644 --- a/tests/components/knx/test_interface_device.py +++ b/tests/components/knx/test_interface_device.py @@ -99,11 +99,11 @@ async def test_removed_entity( hass: HomeAssistant, knx: KNXTestKit, entity_registry: er.EntityRegistry ) -> None: """Test unregister callback when entity is removed.""" - await knx.setup_integration({}) - - with patch.object( - knx.xknx.connection_manager, "unregister_connection_state_changed_cb" + with patch( + "xknx.core.connection_manager.ConnectionManager.unregister_connection_state_changed_cb" ) as unregister_mock: + await knx.setup_integration({}) + entity_registry.async_update_entity( "sensor.knx_interface_connection_established", disabled_by=er.RegistryEntryDisabler.USER, From 8108a0f947f5c1c6146bd8b2d87d096ea61bd636 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sat, 1 Jul 2023 13:55:28 +0200 Subject: [PATCH 0078/1009] Add Bridge module to AsusWRT (#84152) * Add Bridge module to AsusWRT * Requested changes * Requested changes * Requested changes * Add check on router attributes value --- homeassistant/components/asuswrt/bridge.py | 273 ++++++++++++++++++ .../components/asuswrt/config_flow.py | 19 +- homeassistant/components/asuswrt/const.py | 4 + homeassistant/components/asuswrt/router.py | 217 +++----------- homeassistant/components/asuswrt/sensor.py | 4 +- tests/components/asuswrt/test_config_flow.py | 5 +- tests/components/asuswrt/test_sensor.py | 24 +- 7 files changed, 332 insertions(+), 214 deletions(-) create mode 100644 homeassistant/components/asuswrt/bridge.py diff --git a/homeassistant/components/asuswrt/bridge.py b/homeassistant/components/asuswrt/bridge.py new file mode 100644 index 00000000000000..9e6da0ea8f779c --- /dev/null +++ b/homeassistant/components/asuswrt/bridge.py @@ -0,0 +1,273 @@ +"""aioasuswrt and pyasuswrt bridge classes.""" +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections import namedtuple +import logging +from typing import Any, cast + +from aioasuswrt.asuswrt import AsusWrt as AsusWrtLegacy + +from homeassistant.const import ( + CONF_HOST, + CONF_MODE, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.update_coordinator import UpdateFailed + +from .const import ( + CONF_DNSMASQ, + CONF_INTERFACE, + CONF_REQUIRE_IP, + CONF_SSH_KEY, + DEFAULT_DNSMASQ, + DEFAULT_INTERFACE, + KEY_METHOD, + KEY_SENSORS, + PROTOCOL_TELNET, + SENSORS_BYTES, + SENSORS_LOAD_AVG, + SENSORS_RATES, + SENSORS_TEMPERATURES, +) + +SENSORS_TYPE_BYTES = "sensors_bytes" +SENSORS_TYPE_COUNT = "sensors_count" +SENSORS_TYPE_LOAD_AVG = "sensors_load_avg" +SENSORS_TYPE_RATES = "sensors_rates" +SENSORS_TYPE_TEMPERATURES = "sensors_temperatures" + +WrtDevice = namedtuple("WrtDevice", ["ip", "name", "connected_to"]) + +_LOGGER = logging.getLogger(__name__) + + +def _get_dict(keys: list, values: list) -> dict[str, Any]: + """Create a dict from a list of keys and values.""" + return dict(zip(keys, values)) + + +class AsusWrtBridge(ABC): + """The Base Bridge abstract class.""" + + @staticmethod + def get_bridge( + hass: HomeAssistant, conf: dict[str, Any], options: dict[str, Any] | None = None + ) -> AsusWrtBridge: + """Get Bridge instance.""" + return AsusWrtLegacyBridge(conf, options) + + def __init__(self, host: str) -> None: + """Initialize Bridge.""" + self._host = host + self._firmware: str | None = None + self._label_mac: str | None = None + self._model: str | None = None + + @property + def host(self) -> str: + """Return hostname.""" + return self._host + + @property + def firmware(self) -> str | None: + """Return firmware information.""" + return self._firmware + + @property + def label_mac(self) -> str | None: + """Return label mac information.""" + return self._label_mac + + @property + def model(self) -> str | None: + """Return model information.""" + return self._model + + @property + @abstractmethod + def is_connected(self) -> bool: + """Get connected status.""" + + @abstractmethod + async def async_connect(self) -> None: + """Connect to the device.""" + + @abstractmethod + async def async_disconnect(self) -> None: + """Disconnect to the device.""" + + @abstractmethod + async def async_get_connected_devices(self) -> dict[str, WrtDevice]: + """Get list of connected devices.""" + + @abstractmethod + async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]: + """Return a dictionary of available sensors for this bridge.""" + + +class AsusWrtLegacyBridge(AsusWrtBridge): + """The Bridge that use legacy library.""" + + def __init__( + self, conf: dict[str, Any], options: dict[str, Any] | None = None + ) -> None: + """Initialize Bridge.""" + super().__init__(conf[CONF_HOST]) + self._protocol: str = conf[CONF_PROTOCOL] + self._api: AsusWrtLegacy = self._get_api(conf, options) + + @staticmethod + def _get_api( + conf: dict[str, Any], options: dict[str, Any] | None = None + ) -> AsusWrtLegacy: + """Get the AsusWrtLegacy API.""" + opt = options or {} + + return AsusWrtLegacy( + conf[CONF_HOST], + conf.get(CONF_PORT), + conf[CONF_PROTOCOL] == PROTOCOL_TELNET, + conf[CONF_USERNAME], + conf.get(CONF_PASSWORD, ""), + conf.get(CONF_SSH_KEY, ""), + conf[CONF_MODE], + opt.get(CONF_REQUIRE_IP, True), + interface=opt.get(CONF_INTERFACE, DEFAULT_INTERFACE), + dnsmasq=opt.get(CONF_DNSMASQ, DEFAULT_DNSMASQ), + ) + + @property + def is_connected(self) -> bool: + """Get connected status.""" + return cast(bool, self._api.is_connected) + + async def async_connect(self) -> None: + """Connect to the device.""" + await self._api.connection.async_connect() + + # get main router properties + if self._label_mac is None: + await self._get_label_mac() + if self._firmware is None: + await self._get_firmware() + if self._model is None: + await self._get_model() + + async def async_disconnect(self) -> None: + """Disconnect to the device.""" + if self._api is not None and self._protocol == PROTOCOL_TELNET: + self._api.connection.disconnect() + + async def async_get_connected_devices(self) -> dict[str, WrtDevice]: + """Get list of connected devices.""" + try: + api_devices = await self._api.async_get_connected_devices() + except OSError as exc: + raise UpdateFailed(exc) from exc + return { + format_mac(mac): WrtDevice(dev.ip, dev.name, None) + for mac, dev in api_devices.items() + } + + async def _get_nvram_info(self, info_type: str) -> dict[str, Any]: + """Get AsusWrt router info from nvram.""" + info = {} + try: + info = await self._api.async_get_nvram(info_type) + except OSError as exc: + _LOGGER.warning( + "Error calling method async_get_nvram(%s): %s", info_type, exc + ) + + return info + + async def _get_label_mac(self) -> None: + """Get label mac information.""" + label_mac = await self._get_nvram_info("LABEL_MAC") + if label_mac and "label_mac" in label_mac: + self._label_mac = format_mac(label_mac["label_mac"]) + + async def _get_firmware(self) -> None: + """Get firmware information.""" + firmware = await self._get_nvram_info("FIRMWARE") + if firmware and "firmver" in firmware: + firmver: str = firmware["firmver"] + if "buildno" in firmware: + firmver += f" (build {firmware['buildno']})" + self._firmware = firmver + + async def _get_model(self) -> None: + """Get model information.""" + model = await self._get_nvram_info("MODEL") + if model and "model" in model: + self._model = model["model"] + + async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]: + """Return a dictionary of available sensors for this bridge.""" + sensors_temperatures = await self._get_available_temperature_sensors() + sensors_types = { + SENSORS_TYPE_BYTES: { + KEY_SENSORS: SENSORS_BYTES, + KEY_METHOD: self._get_bytes, + }, + SENSORS_TYPE_LOAD_AVG: { + KEY_SENSORS: SENSORS_LOAD_AVG, + KEY_METHOD: self._get_load_avg, + }, + SENSORS_TYPE_RATES: { + KEY_SENSORS: SENSORS_RATES, + KEY_METHOD: self._get_rates, + }, + SENSORS_TYPE_TEMPERATURES: { + KEY_SENSORS: sensors_temperatures, + KEY_METHOD: self._get_temperatures, + }, + } + return sensors_types + + async def _get_available_temperature_sensors(self) -> list[str]: + """Check which temperature information is available on the router.""" + availability = await self._api.async_find_temperature_commands() + return [SENSORS_TEMPERATURES[i] for i in range(3) if availability[i]] + + async def _get_bytes(self) -> dict[str, Any]: + """Fetch byte information from the router.""" + try: + datas = await self._api.async_get_bytes_total() + except (IndexError, OSError, ValueError) as exc: + raise UpdateFailed(exc) from exc + + return _get_dict(SENSORS_BYTES, datas) + + async def _get_rates(self) -> dict[str, Any]: + """Fetch rates information from the router.""" + try: + rates = await self._api.async_get_current_transfer_rates() + except (IndexError, OSError, ValueError) as exc: + raise UpdateFailed(exc) from exc + + return _get_dict(SENSORS_RATES, rates) + + async def _get_load_avg(self) -> dict[str, Any]: + """Fetch load average information from the router.""" + try: + avg = await self._api.async_get_loadavg() + except (IndexError, OSError, ValueError) as exc: + raise UpdateFailed(exc) from exc + + return _get_dict(SENSORS_LOAD_AVG, avg) + + async def _get_temperatures(self) -> dict[str, Any]: + """Fetch temperatures information from the router.""" + try: + temperatures: dict[str, Any] = await self._api.async_get_temperature() + except (OSError, ValueError) as exc: + raise UpdateFailed(exc) from exc + + return temperatures diff --git a/homeassistant/components/asuswrt/config_flow.py b/homeassistant/components/asuswrt/config_flow.py index 6b0056b14faab9..56569d4f23bc5a 100644 --- a/homeassistant/components/asuswrt/config_flow.py +++ b/homeassistant/components/asuswrt/config_flow.py @@ -25,13 +25,13 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.schema_config_entry_flow import ( SchemaCommonFlowHandler, SchemaFlowFormStep, SchemaOptionsFlowHandler, ) +from .bridge import AsusWrtBridge from .const import ( CONF_DNSMASQ, CONF_INTERFACE, @@ -47,7 +47,6 @@ PROTOCOL_SSH, PROTOCOL_TELNET, ) -from .router import get_api, get_nvram_info LABEL_MAC = "LABEL_MAC" @@ -143,16 +142,15 @@ def _show_setup_form( errors=errors or {}, ) - @staticmethod async def _async_check_connection( - user_input: dict[str, Any] + self, user_input: dict[str, Any] ) -> tuple[str, str | None]: """Attempt to connect the AsusWrt router.""" host: str = user_input[CONF_HOST] - api = get_api(user_input) + api = AsusWrtBridge.get_bridge(self.hass, user_input) try: - await api.connection.async_connect() + await api.async_connect() except OSError: _LOGGER.error("Error connecting to the AsusWrt router at %s", host) @@ -168,14 +166,9 @@ async def _async_check_connection( _LOGGER.error("Error connecting to the AsusWrt router at %s", host) return RESULT_CONN_ERROR, None - label_mac = await get_nvram_info(api, LABEL_MAC) - conf_protocol = user_input[CONF_PROTOCOL] - if conf_protocol == PROTOCOL_TELNET: - api.connection.disconnect() + unique_id = api.label_mac + await api.async_disconnect() - unique_id = None - if label_mac and "label_mac" in label_mac: - unique_id = format_mac(label_mac["label_mac"]) return RESULT_SUCCESS, unique_id async def async_step_user( diff --git a/homeassistant/components/asuswrt/const.py b/homeassistant/components/asuswrt/const.py index f80643f078d4d2..1733d4c09c3765 100644 --- a/homeassistant/components/asuswrt/const.py +++ b/homeassistant/components/asuswrt/const.py @@ -13,6 +13,10 @@ DEFAULT_INTERFACE = "eth0" DEFAULT_TRACK_UNKNOWN = False +KEY_COORDINATOR = "coordinator" +KEY_METHOD = "method" +KEY_SENSORS = "sensors" + MODE_AP = "ap" MODE_ROUTER = "router" diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index 4291c21d0ed1ac..c782a8f0f3b036 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -6,22 +6,12 @@ import logging from typing import Any -from aioasuswrt.asuswrt import AsusWrt, Device as WrtDevice - from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, DOMAIN as TRACKER_DOMAIN, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_MODE, - CONF_PASSWORD, - CONF_PORT, - CONF_PROTOCOL, - CONF_USERNAME, -) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry as er @@ -32,55 +22,36 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util +from .bridge import AsusWrtBridge, WrtDevice from .const import ( CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP, - CONF_SSH_KEY, CONF_TRACK_UNKNOWN, DEFAULT_DNSMASQ, DEFAULT_INTERFACE, DEFAULT_TRACK_UNKNOWN, DOMAIN, - PROTOCOL_TELNET, - SENSORS_BYTES, + KEY_COORDINATOR, + KEY_METHOD, + KEY_SENSORS, SENSORS_CONNECTED_DEVICE, - SENSORS_LOAD_AVG, - SENSORS_RATES, - SENSORS_TEMPERATURES, ) CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP] DEFAULT_NAME = "Asuswrt" -KEY_COORDINATOR = "coordinator" -KEY_SENSORS = "sensors" - SCAN_INTERVAL = timedelta(seconds=30) -SENSORS_TYPE_BYTES = "sensors_bytes" SENSORS_TYPE_COUNT = "sensors_count" -SENSORS_TYPE_LOAD_AVG = "sensors_load_avg" -SENSORS_TYPE_RATES = "sensors_rates" -SENSORS_TYPE_TEMPERATURES = "sensors_temperatures" _LOGGER = logging.getLogger(__name__) -def _get_dict(keys: list, values: list) -> dict[str, Any]: - """Create a dict from a list of keys and values.""" - ret_dict: dict[str, Any] = dict.fromkeys(keys) - - for index, key in enumerate(ret_dict): - ret_dict[key] = values[index] - - return ret_dict - - class AsusWrtSensorDataHandler: """Data handler for AsusWrt sensor.""" - def __init__(self, hass: HomeAssistant, api: AsusWrt) -> None: + def __init__(self, hass: HomeAssistant, api: AsusWrtBridge) -> None: """Initialize a AsusWrt sensor data handler.""" self._hass = hass self._api = api @@ -90,42 +61,6 @@ async def _get_connected_devices(self) -> dict[str, int]: """Return number of connected devices.""" return {SENSORS_CONNECTED_DEVICE[0]: self._connected_devices} - async def _get_bytes(self) -> dict[str, Any]: - """Fetch byte information from the router.""" - try: - datas = await self._api.async_get_bytes_total() - except (OSError, ValueError) as exc: - raise UpdateFailed(exc) from exc - - return _get_dict(SENSORS_BYTES, datas) - - async def _get_rates(self) -> dict[str, Any]: - """Fetch rates information from the router.""" - try: - rates = await self._api.async_get_current_transfer_rates() - except (OSError, ValueError) as exc: - raise UpdateFailed(exc) from exc - - return _get_dict(SENSORS_RATES, rates) - - async def _get_load_avg(self) -> dict[str, Any]: - """Fetch load average information from the router.""" - try: - avg = await self._api.async_get_loadavg() - except (OSError, ValueError) as exc: - raise UpdateFailed(exc) from exc - - return _get_dict(SENSORS_LOAD_AVG, avg) - - async def _get_temperatures(self) -> dict[str, Any]: - """Fetch temperatures information from the router.""" - try: - temperatures: dict[str, Any] = await self._api.async_get_temperature() - except (OSError, ValueError) as exc: - raise UpdateFailed(exc) from exc - - return temperatures - def update_device_count(self, conn_devices: int) -> bool: """Update connected devices attribute.""" if self._connected_devices == conn_devices: @@ -134,19 +69,17 @@ def update_device_count(self, conn_devices: int) -> bool: return True async def get_coordinator( - self, sensor_type: str, should_poll: bool = True + self, + sensor_type: str, + update_method: Callable[[], Any] | None = None, ) -> DataUpdateCoordinator: """Get the coordinator for a specific sensor type.""" + should_poll = True if sensor_type == SENSORS_TYPE_COUNT: + should_poll = False method = self._get_connected_devices - elif sensor_type == SENSORS_TYPE_BYTES: - method = self._get_bytes - elif sensor_type == SENSORS_TYPE_LOAD_AVG: - method = self._get_load_avg - elif sensor_type == SENSORS_TYPE_RATES: - method = self._get_rates - elif sensor_type == SENSORS_TYPE_TEMPERATURES: - method = self._get_temperatures + elif update_method is not None: + method = update_method else: raise RuntimeError(f"Invalid sensor type: {sensor_type}") @@ -226,12 +159,6 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: self.hass = hass self._entry = entry - self._api: AsusWrt = None - self._protocol: str = entry.data[CONF_PROTOCOL] - self._host: str = entry.data[CONF_HOST] - self._model: str = "Asus Router" - self._sw_v: str | None = None - self._devices: dict[str, AsusWrtDevInfo] = {} self._connected_devices: int = 0 self._connect_error: bool = False @@ -248,26 +175,19 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: } self._options.update(entry.options) + self._api: AsusWrtBridge = AsusWrtBridge.get_bridge( + self.hass, dict(self._entry.data), self._options + ) + async def setup(self) -> None: """Set up a AsusWrt router.""" - self._api = get_api(dict(self._entry.data), self._options) - try: - await self._api.connection.async_connect() - except OSError as exp: - raise ConfigEntryNotReady from exp - + await self._api.async_connect() + except OSError as exc: + raise ConfigEntryNotReady from exc if not self._api.is_connected: raise ConfigEntryNotReady - # System - model = await get_nvram_info(self._api, "MODEL") - if model and "model" in model: - self._model = model["model"] - firmware = await get_nvram_info(self._api, "FIRMWARE") - if firmware and "firmver" in firmware and "buildno" in firmware: - self._sw_v = f"{firmware['firmver']} (build {firmware['buildno']})" - # Load tracked entities from registry entity_reg = er.async_get(self.hass) track_entries = er.async_entries_for_config_entry( @@ -312,24 +232,24 @@ async def update_all(self, now: datetime | None = None) -> None: async def update_devices(self) -> None: """Update AsusWrt devices tracker.""" new_device = False - _LOGGER.debug("Checking devices for ASUS router %s", self._host) + _LOGGER.debug("Checking devices for ASUS router %s", self.host) try: - api_devices = await self._api.async_get_connected_devices() - except OSError as exc: + wrt_devices = await self._api.async_get_connected_devices() + except UpdateFailed as exc: if not self._connect_error: self._connect_error = True _LOGGER.error( "Error connecting to ASUS router %s for device update: %s", - self._host, + self.host, exc, ) return if self._connect_error: self._connect_error = False - _LOGGER.info("Reconnected to ASUS router %s", self._host) + _LOGGER.info("Reconnected to ASUS router %s", self.host) - self._connected_devices = len(api_devices) + self._connected_devices = len(wrt_devices) consider_home: int = self._options.get( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() ) @@ -337,7 +257,6 @@ async def update_devices(self) -> None: CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN ) - wrt_devices = {format_mac(mac): dev for mac, dev in api_devices.items()} for device_mac, device in self._devices.items(): dev_info = wrt_devices.pop(device_mac, None) device.update(dev_info, consider_home) @@ -363,19 +282,14 @@ async def init_sensors_coordinator(self) -> None: self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api) self._sensors_data_handler.update_device_count(self._connected_devices) - sensors_types: dict[str, list[str]] = { - SENSORS_TYPE_BYTES: SENSORS_BYTES, - SENSORS_TYPE_COUNT: SENSORS_CONNECTED_DEVICE, - SENSORS_TYPE_LOAD_AVG: SENSORS_LOAD_AVG, - SENSORS_TYPE_RATES: SENSORS_RATES, - SENSORS_TYPE_TEMPERATURES: await self._get_available_temperature_sensors(), - } + sensors_types = await self._api.async_get_available_sensors() + sensors_types[SENSORS_TYPE_COUNT] = {KEY_SENSORS: SENSORS_CONNECTED_DEVICE} - for sensor_type, sensor_names in sensors_types.items(): - if not sensor_names: + for sensor_type, sensor_def in sensors_types.items(): + if not (sensor_names := sensor_def.get(KEY_SENSORS)): continue coordinator = await self._sensors_data_handler.get_coordinator( - sensor_type, sensor_type != SENSORS_TYPE_COUNT + sensor_type, update_method=sensor_def.get(KEY_METHOD) ) self._sensors_coordinator[sensor_type] = { KEY_COORDINATOR: coordinator, @@ -392,31 +306,10 @@ async def _update_unpolled_sensors(self) -> None: if self._sensors_data_handler.update_device_count(self._connected_devices): await coordinator.async_refresh() - async def _get_available_temperature_sensors(self) -> list[str]: - """Check which temperature information is available on the router.""" - try: - availability = await self._api.async_find_temperature_commands() - available_sensors = [ - SENSORS_TEMPERATURES[i] for i in range(3) if availability[i] - ] - except Exception as exc: # pylint: disable=broad-except - _LOGGER.debug( - ( - "Failed checking temperature sensor availability for ASUS router" - " %s. Exception: %s" - ), - self._host, - exc, - ) - return [] - - return available_sensors - async def close(self) -> None: """Close the connection.""" - if self._api is not None and self._protocol == PROTOCOL_TELNET: - self._api.connection.disconnect() - self._api = None + if self._api is not None: + await self._api.async_disconnect() for func in self._on_close: func() @@ -443,14 +336,17 @@ def update_options(self, new_options: dict[str, Any]) -> bool: @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return DeviceInfo( + info = DeviceInfo( identifiers={(DOMAIN, self.unique_id or "AsusWRT")}, - name=self._host, - model=self._model, + name=self.host, + model=self._api.model or "Asus Router", manufacturer="Asus", - sw_version=self._sw_v, - configuration_url=f"http://{self._host}", + configuration_url=f"http://{self.host}", ) + if self._api.firmware: + info["sw_version"] = self._api.firmware + + return info @property def signal_device_new(self) -> str: @@ -465,7 +361,7 @@ def signal_device_update(self) -> str: @property def host(self) -> str: """Return router hostname.""" - return self._host + return self._api.host @property def unique_id(self) -> str | None: @@ -475,7 +371,7 @@ def unique_id(self) -> str | None: @property def name(self) -> str: """Return router name.""" - return self._host if self.unique_id else DEFAULT_NAME + return self.host if self.unique_id else DEFAULT_NAME @property def devices(self) -> dict[str, AsusWrtDevInfo]: @@ -486,32 +382,3 @@ def devices(self) -> dict[str, AsusWrtDevInfo]: def sensors_coordinator(self) -> dict[str, Any]: """Return sensors coordinators.""" return self._sensors_coordinator - - -async def get_nvram_info(api: AsusWrt, info_type: str) -> dict[str, Any]: - """Get AsusWrt router info from nvram.""" - info = {} - try: - info = await api.async_get_nvram(info_type) - except OSError as exc: - _LOGGER.warning("Error calling method async_get_nvram(%s): %s", info_type, exc) - - return info - - -def get_api(conf: dict[str, Any], options: dict[str, Any] | None = None) -> AsusWrt: - """Get the AsusWrt API.""" - opt = options or {} - - return AsusWrt( - conf[CONF_HOST], - conf.get(CONF_PORT), - conf[CONF_PROTOCOL] == PROTOCOL_TELNET, - conf[CONF_USERNAME], - conf.get(CONF_PASSWORD, ""), - conf.get(CONF_SSH_KEY, ""), - conf[CONF_MODE], - opt.get(CONF_REQUIRE_IP, True), - interface=opt.get(CONF_INTERFACE, DEFAULT_INTERFACE), - dnsmasq=opt.get(CONF_DNSMASQ, DEFAULT_DNSMASQ), - ) diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 95724ec3bb5599..accd1eba59bece 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -26,13 +26,15 @@ from .const import ( DATA_ASUSWRT, DOMAIN, + KEY_COORDINATOR, + KEY_SENSORS, SENSORS_BYTES, SENSORS_CONNECTED_DEVICE, SENSORS_LOAD_AVG, SENSORS_RATES, SENSORS_TEMPERATURES, ) -from .router import KEY_COORDINATOR, KEY_SENSORS, AsusWrtRouter +from .router import AsusWrtRouter @dataclass diff --git a/tests/components/asuswrt/test_config_flow.py b/tests/components/asuswrt/test_config_flow.py index aabf5d6d46bb75..bdee4f82f90f83 100644 --- a/tests/components/asuswrt/test_config_flow.py +++ b/tests/components/asuswrt/test_config_flow.py @@ -62,7 +62,7 @@ def mock_unique_id_fixture(): @pytest.fixture(name="connect") def mock_controller_connect(mock_unique_id): """Mock a successful connection.""" - with patch("homeassistant.components.asuswrt.router.AsusWrt") as service_mock: + with patch("homeassistant.components.asuswrt.bridge.AsusWrtLegacy") as service_mock: service_mock.return_value.connection.async_connect = AsyncMock() service_mock.return_value.is_connected = True service_mock.return_value.connection.disconnect = Mock() @@ -236,11 +236,12 @@ async def test_on_connect_failed(hass: HomeAssistant, side_effect, error) -> Non ) with PATCH_GET_HOST, patch( - "homeassistant.components.asuswrt.router.AsusWrt" + "homeassistant.components.asuswrt.bridge.AsusWrtLegacy" ) as asus_wrt: asus_wrt.return_value.connection.async_connect = AsyncMock( side_effect=side_effect ) + asus_wrt.return_value.async_get_nvram = AsyncMock(return_value={}) asus_wrt.return_value.is_connected = False result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index 553902b66fd911..c28d71c1a293e6 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -32,7 +32,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed -ASUSWRT_LIB = "homeassistant.components.asuswrt.router.AsusWrt" +ASUSWRT_LIB = "homeassistant.components.asuswrt.bridge.AsusWrtLegacy" HOST = "myrouter.asuswrt.com" IP_ADDRESS = "192.168.1.1" @@ -311,28 +311,6 @@ async def test_loadavg_sensors( assert hass.states.get(f"{sensor_prefix}_load_avg_15m").state == "1.3" -async def test_temperature_sensors_fail( - hass: HomeAssistant, - connect, - mock_available_temps, -) -> None: - """Test fail creating AsusWRT temperature sensors.""" - config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA, SENSORS_TEMP) - config_entry.add_to_hass(hass) - - # Only length of 3 booleans is valid. Checking the exception handling. - mock_available_temps.pop(2) - - # initial devices setup - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - # assert temperature availability exception is handled correctly - assert not hass.states.get(f"{sensor_prefix}_2_4ghz_temperature") - assert not hass.states.get(f"{sensor_prefix}_5ghz_temperature") - assert not hass.states.get(f"{sensor_prefix}_cpu_temperature") - - async def test_temperature_sensors( hass: HomeAssistant, connect, From cac6dc0eae21228532e1087f1d3ba526bbbb01ec Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 1 Jul 2023 10:53:47 -0600 Subject: [PATCH 0079/1009] 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 1e7be48979b036..9ce59eb3b56ff2 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 @@ def __init__(self, simplisafe: SimpliSafe, system: SystemV3, lock: Lock) -> None additional_websocket_events=WEBSOCKET_EVENTS_TO_LISTEN_FOR, ) - self._device: Lock - async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" try: From e4f617e92e9b01dbf2f62cf6e9002aef3dee5148 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 1 Jul 2023 18:04:03 -0400 Subject: [PATCH 0080/1009] Update log message when referenced entity not found (#95577) * Update log message when referenced entity not found * Update homeassistant/helpers/service.py Co-authored-by: Martin Hjelmare * Update test --------- Co-authored-by: Martin Hjelmare --- homeassistant/helpers/service.py | 2 +- tests/helpers/test_service.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index fa0e57d501c4e3..715a960de5dc03 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -240,7 +240,7 @@ def log_missing(self, missing_entities: set[str]) -> None: return _LOGGER.warning( - "Unable to find referenced %s or it is/they are currently not available", + "Referenced %s are missing or not currently available", ", ".join(parts), ) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index f6299312b5391b..291a1744d20976 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -1238,9 +1238,9 @@ async def test_entity_service_call_warn_referenced( ) await service.entity_service_call(hass, {}, "", call) assert ( - "Unable to find referenced areas non-existent-area, devices" - " non-existent-device, entities non.existent" in caplog.text - ) + "Referenced areas non-existent-area, devices non-existent-device, " + "entities non.existent are missing or not currently available" + ) in caplog.text async def test_async_extract_entities_warn_referenced( @@ -1259,9 +1259,9 @@ async def test_async_extract_entities_warn_referenced( extracted = await service.async_extract_entities(hass, {}, call) assert len(extracted) == 0 assert ( - "Unable to find referenced areas non-existent-area, devices" - " non-existent-device, entities non.existent" in caplog.text - ) + "Referenced areas non-existent-area, devices non-existent-device, " + "entities non.existent are missing or not currently available" + ) in caplog.text async def test_async_extract_config_entry_ids(hass: HomeAssistant) -> None: From 33cd44ddb77b45ed85a4329829eac15ac1e6bd3c Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Sun, 2 Jul 2023 09:28:18 -0400 Subject: [PATCH 0081/1009] 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 2c2aafa960e932..01f77cb2d38a4f 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 fe24b288f63106..619a87215528f4 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 306606bc46c6be..b1b5b9f2cbe006 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 79a122e1e52578cd712f695a7e1ebe8665e9e4dc Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 2 Jul 2023 13:28:41 +0000 Subject: [PATCH 0082/1009] 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 1f684ce137c41a..ac01033f2c73ff 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 f0cb03e6314bb04ebcc758fa063b5db057a16f6f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Jul 2023 09:29:45 -0500 Subject: [PATCH 0083/1009] 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 11deb5bb486b07..53c8577be44c72 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -40,6 +40,8 @@ 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 @@ def _name(self, value: str) -> None: 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 @@ async def fetch_device_info(self) -> str | None: 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 @@ async def fetch_device_info(self) -> str | None: 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 @@ async def _retrieve_encryption_key_from_dashboard(self) -> bool: 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 085437fb02e24b..8f5e6b95c39097 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 619a87215528f4..7241667fb82f07 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 b1b5b9f2cbe006..ab6fd014adc080 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 affe65949b2e92..4a99de77c1aab3 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 @@ RequiresEncryptionAPIError, ResolveAPIError, ) +import aiohttp import pytest from homeassistant import config_entries, data_entry_flow @@ -35,6 +37,7 @@ 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 86912d240946924a3787be24d35372fd6eefd5e7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 2 Jul 2023 10:31:30 -0400 Subject: [PATCH 0084/1009] Met Eireann: fix device info (#95683) --- homeassistant/components/met_eireann/weather.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index cce35731c728d1..bf0d7214c6efc1 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -159,7 +159,7 @@ def forecast(self): def device_info(self): """Device info.""" return DeviceInfo( - default_name="Forecast", + name="Forecast", entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN,)}, manufacturer="Met Éireann", From b314e2b1a14a528f2fd95bdbb4b98104c0f5bc86 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 2 Jul 2023 16:32:43 +0200 Subject: [PATCH 0085/1009] Fix songpal test_setup_failed test (#95712) --- tests/components/songpal/test_media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/songpal/test_media_player.py b/tests/components/songpal/test_media_player.py index d5e89e887d1fc3..534e2e6e9e69bf 100644 --- a/tests/components/songpal/test_media_player.py +++ b/tests/components/songpal/test_media_player.py @@ -103,8 +103,8 @@ async def test_setup_failed( await hass.async_block_till_done() all_states = hass.states.async_all() assert len(all_states) == 0 - warning_records = [x for x in caplog.records if x.levelno == logging.WARNING] - assert len(warning_records) == 2 + assert "[name(http://0.0.0.0:10000/sony)] Unable to connect" in caplog.text + assert "Platform songpal not ready yet: Unable to do POST request" in caplog.text assert not any(x.levelno == logging.ERROR for x in caplog.records) caplog.clear() From 7026ea643e63123c3e9c81c45a48c35d8d9699ac Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Sun, 2 Jul 2023 18:51:11 +0300 Subject: [PATCH 0086/1009] Add action attribute to generic hygrostat (#95675) * add action attribute to generic hygrostat * Simplified initialization --- .../components/generic_hygrostat/humidifier.py | 11 +++++++++++ tests/components/generic_hygrostat/test_humidifier.py | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index 01945f9e242daf..959b0a8e8dfcdd 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -9,6 +9,7 @@ MODE_AWAY, MODE_NORMAL, PLATFORM_SCHEMA, + HumidifierAction, HumidifierDeviceClass, HumidifierEntity, HumidifierEntityFeature, @@ -158,6 +159,7 @@ def __init__( self._is_away = False if not self._device_class: self._device_class = HumidifierDeviceClass.HUMIDIFIER + self._attr_action = HumidifierAction.IDLE async def async_added_to_hass(self): """Run when entity about to be added.""" @@ -361,6 +363,15 @@ def _async_switch_changed(self, entity_id, old_state, new_state): """Handle humidifier switch state changes.""" if new_state is None: return + + if new_state.state == STATE_ON: + if self._device_class == HumidifierDeviceClass.DEHUMIDIFIER: + self._attr_action = HumidifierAction.DRYING + else: + self._attr_action = HumidifierAction.HUMIDIFYING + else: + self._attr_action = HumidifierAction.IDLE + self.async_schedule_update_ha_state() async def _async_update_humidity(self, humidity): diff --git a/tests/components/generic_hygrostat/test_humidifier.py b/tests/components/generic_hygrostat/test_humidifier.py index 341571fe9ad4f6..dcb1608b7102ef 100644 --- a/tests/components/generic_hygrostat/test_humidifier.py +++ b/tests/components/generic_hygrostat/test_humidifier.py @@ -121,6 +121,7 @@ async def test_humidifier_input_boolean(hass: HomeAssistant, setup_comp_1) -> No await hass.async_block_till_done() assert hass.states.get(humidifier_switch).state == STATE_ON + assert hass.states.get(ENTITY).attributes.get("action") == "humidifying" async def test_humidifier_switch( @@ -165,6 +166,7 @@ async def test_humidifier_switch( await hass.async_block_till_done() assert hass.states.get(humidifier_switch).state == STATE_ON + assert hass.states.get(ENTITY).attributes.get("action") == "humidifying" def _setup_sensor(hass, humidity): @@ -277,6 +279,7 @@ async def test_default_setup_params(hass: HomeAssistant, setup_comp_2) -> None: assert state.attributes.get("min_humidity") == 0 assert state.attributes.get("max_humidity") == 100 assert state.attributes.get("humidity") == 0 + assert state.attributes.get("action") == "idle" async def test_default_setup_params_dehumidifier( @@ -287,6 +290,7 @@ async def test_default_setup_params_dehumidifier( assert state.attributes.get("min_humidity") == 0 assert state.attributes.get("max_humidity") == 100 assert state.attributes.get("humidity") == 100 + assert state.attributes.get("action") == "idle" async def test_get_modes(hass: HomeAssistant, setup_comp_2) -> None: @@ -648,6 +652,7 @@ async def test_set_target_humidity_dry_off(hass: HomeAssistant, setup_comp_3) -> assert call.domain == HASS_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH + assert hass.states.get(ENTITY).attributes.get("action") == "drying" async def test_turn_away_mode_on_drying(hass: HomeAssistant, setup_comp_3) -> None: @@ -799,6 +804,7 @@ async def test_running_when_operating_mode_is_off_2( assert call.domain == HASS_DOMAIN assert call.service == SERVICE_TURN_OFF assert call.data["entity_id"] == ENT_SWITCH + assert hass.states.get(ENTITY).attributes.get("action") == "off" async def test_no_state_change_when_operation_mode_off_2( @@ -818,6 +824,7 @@ async def test_no_state_change_when_operation_mode_off_2( _setup_sensor(hass, 45) await hass.async_block_till_done() assert len(calls) == 0 + assert hass.states.get(ENTITY).attributes.get("action") == "off" @pytest.fixture From 99badceecc4df7bf869381dca27dd251b2a6adf0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Jul 2023 12:09:20 -0500 Subject: [PATCH 0087/1009] 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 6683e7e458308a..eaa1acc11bf4c8 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 7241667fb82f07..c14527f7348eca 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 ab6fd014adc080..7076fab3e953b2 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 953bd60296e47a62459454733274799b70e821f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Jul 2023 12:23:41 -0500 Subject: [PATCH 0088/1009] 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 9134a92c799086..1c5d25dfb3d2b8 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 84b67ded6ae299..47bd964c00265f 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 c14527f7348eca..87f74d66523cc9 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 7076fab3e953b2..86e22c3644c3fb 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 23a16666c039ca7372d91b25857dbe3e37012a21 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 2 Jul 2023 19:25:39 +0200 Subject: [PATCH 0089/1009] Remove obsolete entity name from Lametric (#95688) Remove obsolete name --- homeassistant/components/lametric/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/lametric/sensor.py b/homeassistant/components/lametric/sensor.py index 0c26d2c7dd5892..6cddf81b2bf3f6 100644 --- a/homeassistant/components/lametric/sensor.py +++ b/homeassistant/components/lametric/sensor.py @@ -39,7 +39,6 @@ class LaMetricSensorEntityDescription( LaMetricSensorEntityDescription( key="rssi", translation_key="rssi", - name="Wi-Fi signal", icon="mdi:wifi", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, From 65f67669d26432140a29c928b73c8d266a9d0375 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 2 Jul 2023 19:27:29 +0200 Subject: [PATCH 0090/1009] Use device info object in LaCrosse View (#95687) Use device info object --- homeassistant/components/lacrosse_view/sensor.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index e001450fab053e..833c47dffb0dc1 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -23,6 +23,7 @@ UnitOfTemperature, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -206,13 +207,13 @@ def __init__( self.entity_description = description self._attr_unique_id = f"{sensor.sensor_id}-{description.key}" - self._attr_device_info = { - "identifiers": {(DOMAIN, sensor.sensor_id)}, - "name": sensor.name, - "manufacturer": "LaCrosse Technology", - "model": sensor.model, - "via_device": (DOMAIN, sensor.location.id), - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, sensor.sensor_id)}, + name=sensor.name, + manufacturer="LaCrosse Technology", + model=sensor.model, + via_device=(DOMAIN, sensor.location.id), + ) self.index = index @property From 2aff138b92536d6a47a2751e60646b85643ae154 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Jul 2023 12:33:25 -0500 Subject: [PATCH 0091/1009] Small improvements to websocket api performance (#95693) --- .../components/websocket_api/commands.py | 109 ++++++++++++------ .../components/websocket_api/connection.py | 14 +++ .../components/websocket_api/http.py | 20 +++- 3 files changed, 103 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 619fc913e09296..c733a96ca9d275 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -3,12 +3,13 @@ from collections.abc import Callable import datetime as dt -from functools import lru_cache +from functools import lru_cache, partial import json from typing import Any, cast import voluptuous as vol +from homeassistant.auth.models import User from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_READ from homeassistant.const import ( EVENT_STATE_CHANGED, @@ -88,6 +89,32 @@ def pong_message(iden: int) -> dict[str, Any]: return {"id": iden, "type": "pong"} +def _forward_events_check_permissions( + send_message: Callable[[str | dict[str, Any] | Callable[[], str]], None], + user: User, + msg_id: int, + event: Event, +) -> None: + """Forward state changed events to websocket.""" + # We have to lookup the permissions again because the user might have + # changed since the subscription was created. + permissions = user.permissions + if not permissions.access_all_entities( + POLICY_READ + ) and not permissions.check_entity(event.data["entity_id"], POLICY_READ): + return + send_message(messages.cached_event_message(msg_id, event)) + + +def _forward_events_unconditional( + send_message: Callable[[str | dict[str, Any] | Callable[[], str]], None], + msg_id: int, + event: Event, +) -> None: + """Forward events to websocket.""" + send_message(messages.cached_event_message(msg_id, event)) + + @callback @decorators.websocket_command( { @@ -109,26 +136,18 @@ def handle_subscribe_events( raise Unauthorized if event_type == EVENT_STATE_CHANGED: - user = connection.user - - @callback - def forward_events(event: Event) -> None: - """Forward state changed events to websocket.""" - # We have to lookup the permissions again because the user might have - # changed since the subscription was created. - permissions = user.permissions - if not permissions.access_all_entities( - POLICY_READ - ) and not permissions.check_entity(event.data["entity_id"], POLICY_READ): - return - connection.send_message(messages.cached_event_message(msg["id"], event)) - + forward_events = callback( + partial( + _forward_events_check_permissions, + connection.send_message, + connection.user, + msg["id"], + ) + ) else: - - @callback - def forward_events(event: Event) -> None: - """Forward events to websocket.""" - connection.send_message(messages.cached_event_message(msg["id"], event)) + forward_events = callback( + partial(_forward_events_unconditional, connection.send_message, msg["id"]) + ) connection.subscriptions[msg["id"]] = hass.bus.async_listen( event_type, forward_events, run_immediately=True @@ -280,6 +299,27 @@ def _send_handle_get_states_response( connection.send_message(construct_result_message(msg_id, f"[{joined_states}]")) +def _forward_entity_changes( + send_message: Callable[[str | dict[str, Any] | Callable[[], str]], None], + entity_ids: set[str], + user: User, + msg_id: int, + event: Event, +) -> None: + """Forward entity state changed events to websocket.""" + entity_id = event.data["entity_id"] + if entity_ids and entity_id not in entity_ids: + return + # We have to lookup the permissions again because the user might have + # changed since the subscription was created. + permissions = user.permissions + if not permissions.access_all_entities( + POLICY_READ + ) and not permissions.check_entity(event.data["entity_id"], POLICY_READ): + return + send_message(messages.cached_state_diff_message(msg_id, event)) + + @callback @decorators.websocket_command( { @@ -292,29 +332,22 @@ def handle_subscribe_entities( ) -> None: """Handle subscribe entities command.""" entity_ids = set(msg.get("entity_ids", [])) - user = connection.user - - @callback - def forward_entity_changes(event: Event) -> None: - """Forward entity state changed events to websocket.""" - entity_id = event.data["entity_id"] - if entity_ids and entity_id not in entity_ids: - return - # We have to lookup the permissions again because the user might have - # changed since the subscription was created. - permissions = user.permissions - if not permissions.access_all_entities( - POLICY_READ - ) and not permissions.check_entity(event.data["entity_id"], POLICY_READ): - return - connection.send_message(messages.cached_state_diff_message(msg["id"], event)) - # We must never await between sending the states and listening for # state changed events or we will introduce a race condition # where some states are missed states = _async_get_allowed_states(hass, connection) connection.subscriptions[msg["id"]] = hass.bus.async_listen( - EVENT_STATE_CHANGED, forward_entity_changes, run_immediately=True + EVENT_STATE_CHANGED, + callback( + partial( + _forward_entity_changes, + connection.send_message, + entity_ids, + connection.user, + msg["id"], + ) + ), + run_immediately=True, ) connection.send_result(msg["id"]) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 319188dae213ac..a554001970b5ac 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -33,6 +33,20 @@ class ActiveConnection: """Handle an active websocket client connection.""" + __slots__ = ( + "logger", + "hass", + "send_message", + "user", + "refresh_token_id", + "subscriptions", + "last_id", + "can_coalesce", + "supported_features", + "handlers", + "binary_handlers", + ) + def __init__( self, logger: WebSocketAdapter, diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 6ac0e10a76cfe5..728405b5d969e5 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -63,6 +63,21 @@ def process(self, msg: str, kwargs: Any) -> tuple[str, Any]: class WebSocketHandler: """Handle an active websocket client connection.""" + __slots__ = ( + "_hass", + "_request", + "_wsock", + "_handle_task", + "_writer_task", + "_closing", + "_authenticated", + "_logger", + "_peak_checker_unsub", + "_connection", + "_message_queue", + "_ready_future", + ) + def __init__(self, hass: HomeAssistant, request: web.Request) -> None: """Initialize an active connection.""" self._hass = hass @@ -201,8 +216,9 @@ def _send_message(self, message: str | dict[str, Any] | Callable[[], str]) -> No return message_queue.append(message) - if self._ready_future and not self._ready_future.done(): - self._ready_future.set_result(None) + ready_future = self._ready_future + if ready_future and not ready_future.done(): + ready_future.set_result(None) peak_checker_active = self._peak_checker_unsub is not None From 2807b6cabc8ff82827f6d5ca69c335a55e0687bc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 2 Jul 2023 19:35:05 +0200 Subject: [PATCH 0092/1009] Add entity translations to kaleidescape (#95625) --- .../components/kaleidescape/entity.py | 9 ++-- .../components/kaleidescape/media_player.py | 1 + .../components/kaleidescape/remote.py | 2 + .../components/kaleidescape/sensor.py | 33 ++++++------ .../components/kaleidescape/strings.json | 52 +++++++++++++++++++ tests/components/kaleidescape/test_sensor.py | 4 +- 6 files changed, 78 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/kaleidescape/entity.py b/homeassistant/components/kaleidescape/entity.py index cab55c20c020a0..87a9fa4da0eecf 100644 --- a/homeassistant/components/kaleidescape/entity.py +++ b/homeassistant/components/kaleidescape/entity.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING from homeassistant.core import callback from homeassistant.helpers.entity import DeviceInfo, Entity @@ -19,18 +19,19 @@ class KaleidescapeEntity(Entity): """Defines a base Kaleidescape entity.""" + _attr_has_entity_name = True + _attr_should_poll = False + def __init__(self, device: KaleidescapeDevice) -> None: """Initialize entity.""" self._device = device - self._attr_should_poll = False self._attr_unique_id = device.serial_number - self._attr_name = f"{KALEIDESCAPE_NAME} {device.system.friendly_name}" self._attr_device_info = DeviceInfo( identifiers={(KALEIDESCAPE_DOMAIN, self._device.serial_number)}, # Instead of setting the device name to the entity name, kaleidescape # should be updated to set has_entity_name = True - name=cast(str | None, self.name), + name=f"{KALEIDESCAPE_NAME} {device.system.friendly_name}", model=self._device.system.type, manufacturer=KALEIDESCAPE_NAME, sw_version=f"{self._device.system.kos_version}", diff --git a/homeassistant/components/kaleidescape/media_player.py b/homeassistant/components/kaleidescape/media_player.py index cbae7f0df76591..7751f6b6a29762 100644 --- a/homeassistant/components/kaleidescape/media_player.py +++ b/homeassistant/components/kaleidescape/media_player.py @@ -56,6 +56,7 @@ class KaleidescapeMediaPlayer(KaleidescapeEntity, MediaPlayerEntity): | MediaPlayerEntityFeature.NEXT_TRACK | MediaPlayerEntityFeature.PREVIOUS_TRACK ) + _attr_name = None async def async_turn_on(self) -> None: """Send leave standby command.""" diff --git a/homeassistant/components/kaleidescape/remote.py b/homeassistant/components/kaleidescape/remote.py index 61080052ee58d5..2d35ad2787fda7 100644 --- a/homeassistant/components/kaleidescape/remote.py +++ b/homeassistant/components/kaleidescape/remote.py @@ -47,6 +47,8 @@ async def async_setup_entry( class KaleidescapeRemote(KaleidescapeEntity, RemoteEntity): """Representation of a Kaleidescape device.""" + _attr_name = None + @property def is_on(self) -> bool: """Return true if device is on.""" diff --git a/homeassistant/components/kaleidescape/sensor.py b/homeassistant/components/kaleidescape/sensor.py index 23d40684c13cc4..183036f3973dfc 100644 --- a/homeassistant/components/kaleidescape/sensor.py +++ b/homeassistant/components/kaleidescape/sensor.py @@ -39,67 +39,67 @@ class KaleidescapeSensorEntityDescription( SENSOR_TYPES: tuple[KaleidescapeSensorEntityDescription, ...] = ( KaleidescapeSensorEntityDescription( key="media_location", - name="Media Location", + translation_key="media_location", icon="mdi:monitor", value_fn=lambda device: device.automation.movie_location, ), KaleidescapeSensorEntityDescription( key="play_status", - name="Play Status", + translation_key="play_status", icon="mdi:monitor", value_fn=lambda device: device.movie.play_status, ), KaleidescapeSensorEntityDescription( key="play_speed", - name="Play Speed", + translation_key="play_speed", icon="mdi:monitor", value_fn=lambda device: device.movie.play_speed, ), KaleidescapeSensorEntityDescription( key="video_mode", - name="Video Mode", + translation_key="video_mode", icon="mdi:monitor-screenshot", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.automation.video_mode, ), KaleidescapeSensorEntityDescription( key="video_color_eotf", - name="Video Color EOTF", + translation_key="video_color_eotf", icon="mdi:monitor-eye", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.automation.video_color_eotf, ), KaleidescapeSensorEntityDescription( key="video_color_space", - name="Video Color Space", + translation_key="video_color_space", icon="mdi:monitor-eye", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.automation.video_color_space, ), KaleidescapeSensorEntityDescription( key="video_color_depth", - name="Video Color Depth", + translation_key="video_color_depth", icon="mdi:monitor-eye", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.automation.video_color_depth, ), KaleidescapeSensorEntityDescription( key="video_color_sampling", - name="Video Color Sampling", + translation_key="video_color_sampling", icon="mdi:monitor-eye", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.automation.video_color_sampling, ), KaleidescapeSensorEntityDescription( key="screen_mask_ratio", - name="Screen Mask Ratio", + translation_key="screen_mask_ratio", icon="mdi:monitor-screenshot", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.automation.screen_mask_ratio, ), KaleidescapeSensorEntityDescription( key="screen_mask_top_trim_rel", - name="Screen Mask Top Trim Rel", + translation_key="screen_mask_top_trim_rel", icon="mdi:monitor-screenshot", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, @@ -107,7 +107,7 @@ class KaleidescapeSensorEntityDescription( ), KaleidescapeSensorEntityDescription( key="screen_mask_bottom_trim_rel", - name="Screen Mask Bottom Trim Rel", + translation_key="screen_mask_bottom_trim_rel", icon="mdi:monitor-screenshot", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, @@ -115,14 +115,14 @@ class KaleidescapeSensorEntityDescription( ), KaleidescapeSensorEntityDescription( key="screen_mask_conservative_ratio", - name="Screen Mask Conservative Ratio", + translation_key="screen_mask_conservative_ratio", icon="mdi:monitor-screenshot", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.automation.screen_mask_conservative_ratio, ), KaleidescapeSensorEntityDescription( key="screen_mask_top_mask_abs", - name="Screen Mask Top Mask Abs", + translation_key="screen_mask_top_mask_abs", icon="mdi:monitor-screenshot", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, @@ -130,7 +130,7 @@ class KaleidescapeSensorEntityDescription( ), KaleidescapeSensorEntityDescription( key="screen_mask_bottom_mask_abs", - name="Screen Mask Bottom Mask Abs", + translation_key="screen_mask_bottom_mask_abs", icon="mdi:monitor-screenshot", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, @@ -138,14 +138,14 @@ class KaleidescapeSensorEntityDescription( ), KaleidescapeSensorEntityDescription( key="cinemascape_mask", - name="Cinemascape Mask", + translation_key="cinemascape_mask", icon="mdi:monitor-star", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.automation.cinemascape_mask, ), KaleidescapeSensorEntityDescription( key="cinemascape_mode", - name="Cinemascape Mode", + translation_key="cinemascape_mode", icon="mdi:monitor-star", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.automation.cinemascape_mode, @@ -177,7 +177,6 @@ def __init__( super().__init__(device) self.entity_description = entity_description self._attr_unique_id = f"{self._attr_unique_id}-{entity_description.key}" - self._attr_name = f"{self._attr_name} {entity_description.name}" @property def native_value(self) -> StateType: diff --git a/homeassistant/components/kaleidescape/strings.json b/homeassistant/components/kaleidescape/strings.json index 92b9c931acddc6..30c22a8ca0ee11 100644 --- a/homeassistant/components/kaleidescape/strings.json +++ b/homeassistant/components/kaleidescape/strings.json @@ -21,5 +21,57 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unsupported": "Unsupported device" } + }, + "entity": { + "sensor": { + "media_location": { + "name": "Media location" + }, + "play_status": { + "name": "Play status" + }, + "play_speed": { + "name": "Play speed" + }, + "video_mode": { + "name": "Video mode" + }, + "video_color_eotf": { + "name": "Video color EOTF" + }, + "video_color_space": { + "name": "Video color space" + }, + "video_color_depth": { + "name": "Video color depth" + }, + "video_color_sampling": { + "name": "Video color sampling" + }, + "screen_mask_ratio": { + "name": "Screen mask ratio" + }, + "screen_mask_top_trim_rel": { + "name": "Screen mask top trim relative" + }, + "screen_mask_bottom_trim_rel": { + "name": "Screen mask bottom trim relative" + }, + "screen_mask_conservative_ratio": { + "name": "Screen mask conservative ratio" + }, + "screen_mask_top_mask_abs": { + "name": "Screen mask top mask absolute" + }, + "screen_mask_bottom_mask_abs": { + "name": "Screen mask bottom mask absolute" + }, + "cinemascape_mask": { + "name": "Cinemascape mask" + }, + "cinemascape_mode": { + "name": "Cinemascape mode" + } + } } } diff --git a/tests/components/kaleidescape/test_sensor.py b/tests/components/kaleidescape/test_sensor.py index 0ae2dc15619531..3fbff29e3e9e98 100644 --- a/tests/components/kaleidescape/test_sensor.py +++ b/tests/components/kaleidescape/test_sensor.py @@ -27,7 +27,7 @@ async def test_sensors( assert entity assert entity.state == "none" assert ( - entity.attributes.get(ATTR_FRIENDLY_NAME) == f"{FRIENDLY_NAME} Media Location" + entity.attributes.get(ATTR_FRIENDLY_NAME) == f"{FRIENDLY_NAME} Media location" ) assert entry assert entry.unique_id == f"{MOCK_SERIAL}-media_location" @@ -36,7 +36,7 @@ async def test_sensors( entry = er.async_get(hass).async_get(f"{ENTITY_ID}_play_status") assert entity assert entity.state == "none" - assert entity.attributes.get(ATTR_FRIENDLY_NAME) == f"{FRIENDLY_NAME} Play Status" + assert entity.attributes.get(ATTR_FRIENDLY_NAME) == f"{FRIENDLY_NAME} Play status" assert entry assert entry.unique_id == f"{MOCK_SERIAL}-play_status" From c1b8e4a3e587250a2172262f5f6f69e006a25818 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 2 Jul 2023 12:13:18 -0600 Subject: [PATCH 0093/1009] Add mold risk sensor to Notion (#95643) Add mold detection sensor to Notion --- homeassistant/components/notion/const.py | 1 + homeassistant/components/notion/sensor.py | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/notion/const.py b/homeassistant/components/notion/const.py index 5e89767d0e0498..0961b7c10c5d4f 100644 --- a/homeassistant/components/notion/const.py +++ b/homeassistant/components/notion/const.py @@ -9,6 +9,7 @@ SENSOR_GARAGE_DOOR = "garage_door" SENSOR_LEAK = "leak" SENSOR_MISSING = "missing" +SENSOR_MOLD = "mold" SENSOR_SAFE = "safe" SENSOR_SLIDING = "sliding" SENSOR_SMOKE_CO = "alarm" diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index e6ff3eaab6941c..6f011523a2ae63 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import NotionEntity -from .const import DOMAIN, SENSOR_TEMPERATURE +from .const import DOMAIN, SENSOR_MOLD, SENSOR_TEMPERATURE from .model import NotionEntityDescriptionMixin @@ -25,6 +25,12 @@ class NotionSensorDescription(SensorEntityDescription, NotionEntityDescriptionMi SENSOR_DESCRIPTIONS = ( + NotionSensorDescription( + key=SENSOR_MOLD, + name="Mold risk", + icon="mdi:liquid-spot", + listener_kind=ListenerKind.MOLD, + ), NotionSensorDescription( key=SENSOR_TEMPERATURE, name="Temperature", @@ -76,11 +82,11 @@ def native_unit_of_measurement(self) -> str | None: @property def native_value(self) -> str | None: - """Return the value reported by the sensor. - - The Notion API only returns a localized string for temperature (e.g. "70°"); we - simply remove the degree symbol: - """ + """Return the value reported by the sensor.""" if not self.listener.status_localized: return None - return self.listener.status_localized.state[:-1] + if self.listener.listener_kind == ListenerKind.TEMPERATURE: + # The Notion API only returns a localized string for temperature (e.g. + # "70°"); we simply remove the degree symbol: + return self.listener.status_localized.state[:-1] + return self.listener.status_localized.state From 0ff38360837aa7064be799332d50fa44a5b05e25 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Jul 2023 16:35:57 -0500 Subject: [PATCH 0094/1009] Use a normal tuple for the EventBus jobs (#95731) --- homeassistant/core.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 47b52d9ff76df4..a30aed22322229 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -31,7 +31,6 @@ TYPE_CHECKING, Any, Generic, - NamedTuple, ParamSpec, TypeVar, cast, @@ -964,12 +963,11 @@ def __repr__(self) -> str: return f"" -class _FilterableJob(NamedTuple): - """Event listener job to be executed with optional filter.""" - - job: HassJob[[Event], Coroutine[Any, Any, None] | None] - event_filter: Callable[[Event], bool] | None - run_immediately: bool +_FilterableJobType = tuple[ + HassJob[[Event], Coroutine[Any, Any, None] | None], # job + Callable[[Event], bool] | None, # event_filter + bool, # run_immediately +] class EventBus: @@ -977,8 +975,8 @@ class EventBus: def __init__(self, hass: HomeAssistant) -> None: """Initialize a new event bus.""" - self._listeners: dict[str, list[_FilterableJob]] = {} - self._match_all_listeners: list[_FilterableJob] = [] + self._listeners: dict[str, list[_FilterableJobType]] = {} + self._match_all_listeners: list[_FilterableJobType] = [] self._listeners[MATCH_ALL] = self._match_all_listeners self._hass = hass @@ -1105,14 +1103,12 @@ def async_listen( raise HomeAssistantError(f"Event listener {listener} is not a callback") return self._async_listen_filterable_job( event_type, - _FilterableJob( - HassJob(listener, f"listen {event_type}"), event_filter, run_immediately - ), + (HassJob(listener, f"listen {event_type}"), event_filter, run_immediately), ) @callback def _async_listen_filterable_job( - self, event_type: str, filterable_job: _FilterableJob + self, event_type: str, filterable_job: _FilterableJobType ) -> CALLBACK_TYPE: self._listeners.setdefault(event_type, []).append(filterable_job) @@ -1159,7 +1155,7 @@ def async_listen_once( This method must be run in the event loop. """ - filterable_job: _FilterableJob | None = None + filterable_job: _FilterableJobType | None = None @callback def _onetime_listener(event: Event) -> None: @@ -1181,7 +1177,7 @@ def _onetime_listener(event: Event) -> None: _onetime_listener, listener, ("__name__", "__qualname__", "__module__"), [] ) - filterable_job = _FilterableJob( + filterable_job = ( HassJob(_onetime_listener, f"onetime listen {event_type} {listener}"), None, False, @@ -1191,7 +1187,7 @@ def _onetime_listener(event: Event) -> None: @callback def _async_remove_listener( - self, event_type: str, filterable_job: _FilterableJob + self, event_type: str, filterable_job: _FilterableJobType ) -> None: """Remove a listener of a specific event_type. From 1ead95f5ea1ee646471bf6d3478a5307a2976f38 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jul 2023 03:10:29 +0200 Subject: [PATCH 0095/1009] Use device class naming for Nest (#95742) --- homeassistant/components/nest/sensor_sdm.py | 2 -- homeassistant/components/nest/strings.json | 10 ---------- 2 files changed, 12 deletions(-) diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index 8eb607b2056650..a74d0f3a54b9af 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -79,7 +79,6 @@ class TemperatureSensor(SensorBase): _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS - _attr_translation_key = "temperature" @property def native_value(self) -> float: @@ -96,7 +95,6 @@ class HumiditySensor(SensorBase): _attr_device_class = SensorDeviceClass.HUMIDITY _attr_native_unit_of_measurement = PERCENTAGE - _attr_translation_key = "humidity" @property def native_value(self) -> int: diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 2578437acf4792..a452d015a2b946 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -86,15 +86,5 @@ "title": "Legacy Works With Nest is being removed", "description": "Legacy Works With Nest is being removed from Home Assistant.\n\nYou must take action to use the SDM API. Remove all `nest` configuration from `configuration.yaml` and restart Home Assistant, then see the Nest [integration instructions]({documentation_url}) for set up instructions and supported devices." } - }, - "entity": { - "sensor": { - "temperature": { - "name": "[%key:component::sensor::entity_component::temperature::name%]" - }, - "humidity": { - "name": "[%key:component::sensor::entity_component::humidity::name%]" - } - } } } From caaeb28cbbbfb975561d7e2641dcc75f78715377 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 2 Jul 2023 18:26:31 -0700 Subject: [PATCH 0096/1009] Add Opower integration for getting electricity/gas usage and cost for many utilities (#90489) * Create Opower integration * fix tests * Update config_flow.py * Update coordinator.py * Update sensor.py * Update sensor.py * Update coordinator.py * Bump opower==0.0.4 * Ignore errors for "recent" PGE accounts * Add type for forecasts * Bump opower to 0.0.5 * Bump opower to 0.0.6 * Bump opower to 0.0.7 * Update requirements_all.txt * Update requirements_test_all.txt * Update coordinator Fix exception caused by https://github.com/home-assistant/core/pull/92095 {} is dict but the function expects a set so change it to set() * Improve exceptions handling * Bump opower==0.0.9 * Bump opower to 0.0.10 * Bump opower to 0.0.11 * fix issue when integration hasn't run for 30 days use last stat time instead of now when fetching recent usage/cost * Allow username to be changed in reauth * Don't allow changing username in reauth flow --- .coveragerc | 3 + CODEOWNERS | 2 + homeassistant/components/opower/__init__.py | 31 +++ .../components/opower/config_flow.py | 112 +++++++++ homeassistant/components/opower/const.py | 5 + .../components/opower/coordinator.py | 220 ++++++++++++++++++ homeassistant/components/opower/manifest.json | 10 + homeassistant/components/opower/sensor.py | 219 +++++++++++++++++ homeassistant/components/opower/strings.json | 28 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/opower/__init__.py | 1 + tests/components/opower/conftest.py | 25 ++ tests/components/opower/test_config_flow.py | 204 ++++++++++++++++ 16 files changed, 873 insertions(+) create mode 100644 homeassistant/components/opower/__init__.py create mode 100644 homeassistant/components/opower/config_flow.py create mode 100644 homeassistant/components/opower/const.py create mode 100644 homeassistant/components/opower/coordinator.py create mode 100644 homeassistant/components/opower/manifest.json create mode 100644 homeassistant/components/opower/sensor.py create mode 100644 homeassistant/components/opower/strings.json create mode 100644 tests/components/opower/__init__.py create mode 100644 tests/components/opower/conftest.py create mode 100644 tests/components/opower/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 6a2a0db3ea4e72..75402c713252d0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -859,6 +859,9 @@ omit = homeassistant/components/openweathermap/sensor.py homeassistant/components/openweathermap/weather_update_coordinator.py homeassistant/components/opnsense/__init__.py + homeassistant/components/opower/__init__.py + homeassistant/components/opower/coordinator.py + homeassistant/components/opower/sensor.py homeassistant/components/opnsense/device_tracker.py homeassistant/components/opple/light.py homeassistant/components/oru/* diff --git a/CODEOWNERS b/CODEOWNERS index 3f8f27187f3f1b..7e09c3c8147157 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -897,6 +897,8 @@ build.json @home-assistant/supervisor /tests/components/openweathermap/ @fabaff @freekode @nzapponi /homeassistant/components/opnsense/ @mtreinish /tests/components/opnsense/ @mtreinish +/homeassistant/components/opower/ @tronikos +/tests/components/opower/ @tronikos /homeassistant/components/oralb/ @bdraco @Lash-L /tests/components/oralb/ @bdraco @Lash-L /homeassistant/components/oru/ @bvlaicu diff --git a/homeassistant/components/opower/__init__.py b/homeassistant/components/opower/__init__.py new file mode 100644 index 00000000000000..f4fca22c9b4424 --- /dev/null +++ b/homeassistant/components/opower/__init__.py @@ -0,0 +1,31 @@ +"""The Opower integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import OpowerCoordinator + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Opower from a config entry.""" + + coordinator = OpowerCoordinator(hass, entry.data) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/opower/config_flow.py b/homeassistant/components/opower/config_flow.py new file mode 100644 index 00000000000000..fdf007c3b681a4 --- /dev/null +++ b/homeassistant/components/opower/config_flow.py @@ -0,0 +1,112 @@ +"""Config flow for Opower integration.""" +from __future__ import annotations + +from collections.abc import Mapping +import logging +from typing import Any + +from opower import CannotConnect, InvalidAuth, Opower, get_supported_utility_names +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_create_clientsession + +from .const import CONF_UTILITY, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_UTILITY): vol.In(get_supported_utility_names()), + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +async def _validate_login( + hass: HomeAssistant, login_data: dict[str, str] +) -> dict[str, str]: + """Validate login data and return any errors.""" + api = Opower( + async_create_clientsession(hass), + login_data[CONF_UTILITY], + login_data[CONF_USERNAME], + login_data[CONF_PASSWORD], + ) + errors: dict[str, str] = {} + try: + await api.async_login() + except InvalidAuth: + errors["base"] = "invalid_auth" + except CannotConnect: + errors["base"] = "cannot_connect" + return errors + + +class OpowerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Opower.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize a new OpowerConfigFlow.""" + self.reauth_entry: config_entries.ConfigEntry | None = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + self._async_abort_entries_match( + { + CONF_UTILITY: user_input[CONF_UTILITY], + CONF_USERNAME: user_input[CONF_USERNAME], + } + ) + errors = await _validate_login(self.hass, user_input) + if not errors: + return self.async_create_entry( + title=f"{user_input[CONF_UTILITY]} ({user_input[CONF_USERNAME]})", + data=user_input, + ) + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Dialog that informs the user that reauth is required.""" + assert self.reauth_entry + errors: dict[str, str] = {} + if user_input is not None: + data = {**self.reauth_entry.data, **user_input} + errors = await _validate_login(self.hass, data) + if not errors: + self.hass.config_entries.async_update_entry( + self.reauth_entry, data=data + ) + await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME): self.reauth_entry.data[CONF_USERNAME], + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/opower/const.py b/homeassistant/components/opower/const.py new file mode 100644 index 00000000000000..b996a214a05916 --- /dev/null +++ b/homeassistant/components/opower/const.py @@ -0,0 +1,5 @@ +"""Constants for the Opower integration.""" + +DOMAIN = "opower" + +CONF_UTILITY = "utility" diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py new file mode 100644 index 00000000000000..4d40bb3356bfee --- /dev/null +++ b/homeassistant/components/opower/coordinator.py @@ -0,0 +1,220 @@ +"""Coordinator to handle Opower connections.""" +from datetime import datetime, timedelta +import logging +from types import MappingProxyType +from typing import Any, cast + +from opower import ( + Account, + AggregateType, + CostRead, + Forecast, + InvalidAuth, + MeterType, + Opower, +) + +from homeassistant.components.recorder import get_instance +from homeassistant.components.recorder.models import StatisticData, StatisticMetaData +from homeassistant.components.recorder.statistics import ( + async_add_external_statistics, + get_last_statistics, + statistics_during_period, +) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, UnitOfEnergy, UnitOfVolume +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import CONF_UTILITY, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class OpowerCoordinator(DataUpdateCoordinator): + """Handle fetching Opower data, updating sensors and inserting statistics.""" + + def __init__( + self, + hass: HomeAssistant, + entry_data: MappingProxyType[str, Any], + ) -> None: + """Initialize the data handler.""" + super().__init__( + hass, + _LOGGER, + name="Opower", + # Data is updated daily on Opower. + # Refresh every 12h to be at most 12h behind. + update_interval=timedelta(hours=12), + ) + self.api = Opower( + aiohttp_client.async_get_clientsession(hass), + entry_data[CONF_UTILITY], + entry_data[CONF_USERNAME], + entry_data[CONF_PASSWORD], + ) + + async def _async_update_data( + self, + ) -> dict[str, Forecast]: + """Fetch data from API endpoint.""" + try: + # Login expires after a few minutes. + # Given the infrequent updating (every 12h) + # assume previous session has expired and re-login. + await self.api.async_login() + except InvalidAuth as err: + raise ConfigEntryAuthFailed from err + forecasts: list[Forecast] = await self.api.async_get_forecast() + _LOGGER.debug("Updating sensor data with: %s", forecasts) + await self._insert_statistics([forecast.account for forecast in forecasts]) + return {forecast.account.utility_account_id: forecast for forecast in forecasts} + + async def _insert_statistics(self, accounts: list[Account]) -> None: + """Insert Opower statistics.""" + for account in accounts: + id_prefix = "_".join( + ( + self.api.utility.subdomain(), + account.meter_type.name.lower(), + account.utility_account_id, + ) + ) + cost_statistic_id = f"{DOMAIN}:{id_prefix}_energy_cost" + consumption_statistic_id = f"{DOMAIN}:{id_prefix}_energy_consumption" + _LOGGER.debug( + "Updating Statistics for %s and %s", + cost_statistic_id, + consumption_statistic_id, + ) + + last_stat = await get_instance(self.hass).async_add_executor_job( + get_last_statistics, self.hass, 1, consumption_statistic_id, True, set() + ) + if not last_stat: + _LOGGER.debug("Updating statistic for the first time") + cost_reads = await self._async_get_all_cost_reads(account) + cost_sum = 0.0 + consumption_sum = 0.0 + last_stats_time = None + else: + cost_reads = await self._async_get_recent_cost_reads( + account, last_stat[consumption_statistic_id][0]["start"] + ) + if not cost_reads: + _LOGGER.debug("No recent usage/cost data. Skipping update") + continue + stats = await get_instance(self.hass).async_add_executor_job( + statistics_during_period, + self.hass, + cost_reads[0].start_time, + None, + {cost_statistic_id, consumption_statistic_id}, + "hour" if account.meter_type == MeterType.ELEC else "day", + None, + {"sum"}, + ) + cost_sum = cast(float, stats[cost_statistic_id][0]["sum"]) + consumption_sum = cast(float, stats[consumption_statistic_id][0]["sum"]) + last_stats_time = stats[cost_statistic_id][0]["start"] + + cost_statistics = [] + consumption_statistics = [] + + for cost_read in cost_reads: + start = cost_read.start_time + if last_stats_time is not None and start.timestamp() <= last_stats_time: + continue + cost_sum += cost_read.provided_cost + consumption_sum += cost_read.consumption + + cost_statistics.append( + StatisticData( + start=start, state=cost_read.provided_cost, sum=cost_sum + ) + ) + consumption_statistics.append( + StatisticData( + start=start, state=cost_read.consumption, sum=consumption_sum + ) + ) + + name_prefix = " ".join( + ( + "Opower", + self.api.utility.subdomain(), + account.meter_type.name.lower(), + account.utility_account_id, + ) + ) + cost_metadata = StatisticMetaData( + has_mean=False, + has_sum=True, + name=f"{name_prefix} cost", + source=DOMAIN, + statistic_id=cost_statistic_id, + unit_of_measurement=None, + ) + consumption_metadata = StatisticMetaData( + has_mean=False, + has_sum=True, + name=f"{name_prefix} consumption", + source=DOMAIN, + statistic_id=consumption_statistic_id, + unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR + if account.meter_type == MeterType.ELEC + else UnitOfVolume.CENTUM_CUBIC_FEET, + ) + + async_add_external_statistics(self.hass, cost_metadata, cost_statistics) + async_add_external_statistics( + self.hass, consumption_metadata, consumption_statistics + ) + + async def _async_get_all_cost_reads(self, account: Account) -> list[CostRead]: + """Get all cost reads since account activation but at different resolutions depending on age. + + - month resolution for all years (since account activation) + - day resolution for past 3 years + - hour resolution for past 2 months, only for electricity, not gas + """ + cost_reads = [] + start = None + end = datetime.now() - timedelta(days=3 * 365) + cost_reads += await self.api.async_get_cost_reads( + account, AggregateType.BILL, start, end + ) + start = end if not cost_reads else cost_reads[-1].end_time + end = ( + datetime.now() - timedelta(days=2 * 30) + if account.meter_type == MeterType.ELEC + else datetime.now() + ) + cost_reads += await self.api.async_get_cost_reads( + account, AggregateType.DAY, start, end + ) + if account.meter_type == MeterType.ELEC: + start = end if not cost_reads else cost_reads[-1].end_time + end = datetime.now() + cost_reads += await self.api.async_get_cost_reads( + account, AggregateType.HOUR, start, end + ) + return cost_reads + + async def _async_get_recent_cost_reads( + self, account: Account, last_stat_time: float + ) -> list[CostRead]: + """Get cost reads within the past 30 days to allow corrections in data from utilities. + + Hourly for electricity, daily for gas. + """ + return await self.api.async_get_cost_reads( + account, + AggregateType.HOUR + if account.meter_type == MeterType.ELEC + else AggregateType.DAY, + datetime.fromtimestamp(last_stat_time) - timedelta(days=30), + datetime.now(), + ) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json new file mode 100644 index 00000000000000..969583f050a0a5 --- /dev/null +++ b/homeassistant/components/opower/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "opower", + "name": "Opower", + "codeowners": ["@tronikos"], + "config_flow": true, + "dependencies": ["recorder"], + "documentation": "https://www.home-assistant.io/integrations/opower", + "iot_class": "cloud_polling", + "requirements": ["opower==0.0.11"] +} diff --git a/homeassistant/components/opower/sensor.py b/homeassistant/components/opower/sensor.py new file mode 100644 index 00000000000000..e28dcbd0661a75 --- /dev/null +++ b/homeassistant/components/opower/sensor.py @@ -0,0 +1,219 @@ +"""Support for Opower sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from opower import Forecast, MeterType, UnitOfMeasure + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfEnergy, UnitOfVolume +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import OpowerCoordinator + + +@dataclass +class OpowerEntityDescriptionMixin: + """Mixin values for required keys.""" + + value_fn: Callable[[Forecast], str | float] + + +@dataclass +class OpowerEntityDescription(SensorEntityDescription, OpowerEntityDescriptionMixin): + """Class describing Opower sensors entities.""" + + +# suggested_display_precision=0 for all sensors since +# Opower provides 0 decimal points for all these. +# (for the statistics in the energy dashboard Opower does provide decimal points) +ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = ( + OpowerEntityDescription( + key="elec_usage_to_date", + name="Current bill electric usage to date", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=0, + value_fn=lambda data: data.usage_to_date, + ), + OpowerEntityDescription( + key="elec_forecasted_usage", + name="Current bill electric forecasted usage", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=0, + value_fn=lambda data: data.forecasted_usage, + ), + OpowerEntityDescription( + key="elec_typical_usage", + name="Typical monthly electric usage", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=0, + value_fn=lambda data: data.typical_usage, + ), + OpowerEntityDescription( + key="elec_cost_to_date", + name="Current bill electric cost to date", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=0, + value_fn=lambda data: data.cost_to_date, + ), + OpowerEntityDescription( + key="elec_forecasted_cost", + name="Current bill electric forecasted cost", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=0, + value_fn=lambda data: data.forecasted_cost, + ), + OpowerEntityDescription( + key="elec_typical_cost", + name="Typical monthly electric cost", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=0, + value_fn=lambda data: data.typical_cost, + ), +) +GAS_SENSORS: tuple[OpowerEntityDescription, ...] = ( + OpowerEntityDescription( + key="gas_usage_to_date", + name="Current bill gas usage to date", + device_class=SensorDeviceClass.GAS, + native_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET, + suggested_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=0, + value_fn=lambda data: data.usage_to_date, + ), + OpowerEntityDescription( + key="gas_forecasted_usage", + name="Current bill gas forecasted usage", + device_class=SensorDeviceClass.GAS, + native_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET, + suggested_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=0, + value_fn=lambda data: data.forecasted_usage, + ), + OpowerEntityDescription( + key="gas_typical_usage", + name="Typical monthly gas usage", + device_class=SensorDeviceClass.GAS, + native_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET, + suggested_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=0, + value_fn=lambda data: data.typical_usage, + ), + OpowerEntityDescription( + key="gas_cost_to_date", + name="Current bill gas cost to date", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=0, + value_fn=lambda data: data.cost_to_date, + ), + OpowerEntityDescription( + key="gas_forecasted_cost", + name="Current bill gas forecasted cost", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=0, + value_fn=lambda data: data.forecasted_cost, + ), + OpowerEntityDescription( + key="gas_typical_cost", + name="Typical monthly gas cost", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=0, + value_fn=lambda data: data.typical_cost, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Opower sensor.""" + + coordinator: OpowerCoordinator = hass.data[DOMAIN][entry.entry_id] + entities: list[OpowerSensor] = [] + forecasts: list[Forecast] = coordinator.data.values() + for forecast in forecasts: + device_id = f"{coordinator.api.utility.subdomain()}_{forecast.account.utility_account_id}" + device = DeviceInfo( + identifiers={(DOMAIN, device_id)}, + name=f"{forecast.account.meter_type.name} account {forecast.account.utility_account_id}", + manufacturer="Opower", + model=coordinator.api.utility.name(), + ) + sensors: tuple[OpowerEntityDescription, ...] = () + if ( + forecast.account.meter_type == MeterType.ELEC + and forecast.unit_of_measure == UnitOfMeasure.KWH + ): + sensors = ELEC_SENSORS + elif ( + forecast.account.meter_type == MeterType.GAS + and forecast.unit_of_measure == UnitOfMeasure.THERM + ): + sensors = GAS_SENSORS + for sensor in sensors: + entities.append( + OpowerSensor( + coordinator, + sensor, + forecast.account.utility_account_id, + device, + device_id, + ) + ) + + async_add_entities(entities) + + +class OpowerSensor(SensorEntity, CoordinatorEntity[OpowerCoordinator]): + """Representation of an Opower sensor.""" + + def __init__( + self, + coordinator: OpowerCoordinator, + description: OpowerEntityDescription, + utility_account_id: str, + device: DeviceInfo, + device_id: str, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self.entity_description: OpowerEntityDescription = description + self._attr_unique_id = f"{device_id}_{description.key}" + self._attr_device_info = device + self.utility_account_id = utility_account_id + + @property + def native_value(self) -> StateType: + """Return the state.""" + if self.coordinator.data is not None: + return self.entity_description.value_fn( + self.coordinator.data[self.utility_account_id] + ) + return None diff --git a/homeassistant/components/opower/strings.json b/homeassistant/components/opower/strings.json new file mode 100644 index 00000000000000..79d8bf80feeb37 --- /dev/null +++ b/homeassistant/components/opower/strings.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "data": { + "utility": "Utility name", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2925ea3425cc53..d8ffa25b765da0 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -325,6 +325,7 @@ "opentherm_gw", "openuv", "openweathermap", + "opower", "oralb", "otbr", "overkiz", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 98571b6905e48f..314e8ffa092f05 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4016,6 +4016,12 @@ "config_flow": false, "iot_class": "local_polling" }, + "opower": { + "name": "Opower", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "opple": { "name": "Opple", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 87f74d66523cc9..74c0b80ad57027 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1362,6 +1362,9 @@ openwrt-luci-rpc==1.1.16 # homeassistant.components.ubus openwrt-ubus-rpc==0.0.2 +# homeassistant.components.opower +opower==0.0.11 + # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 86e22c3644c3fb..09706afca237d6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1028,6 +1028,9 @@ openerz-api==0.2.0 # homeassistant.components.openhome openhomedevice==2.2.0 +# homeassistant.components.opower +opower==0.0.11 + # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/tests/components/opower/__init__.py b/tests/components/opower/__init__.py new file mode 100644 index 00000000000000..71aea27a69835b --- /dev/null +++ b/tests/components/opower/__init__.py @@ -0,0 +1 @@ +"""Tests for the Opower integration.""" diff --git a/tests/components/opower/conftest.py b/tests/components/opower/conftest.py new file mode 100644 index 00000000000000..17c6896593b030 --- /dev/null +++ b/tests/components/opower/conftest.py @@ -0,0 +1,25 @@ +"""Fixtures for the Opower integration tests.""" +import pytest + +from homeassistant.components.opower.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Return the default mocked config entry.""" + config_entry = MockConfigEntry( + title="Pacific Gas & Electric (test-username)", + domain=DOMAIN, + data={ + "utility": "Pacific Gas and Electric Company (PG&E)", + "username": "test-username", + "password": "test-password", + }, + state=ConfigEntryState.LOADED, + ) + config_entry.add_to_hass(hass) + return config_entry diff --git a/tests/components/opower/test_config_flow.py b/tests/components/opower/test_config_flow.py new file mode 100644 index 00000000000000..7f6a847f52e3b4 --- /dev/null +++ b/tests/components/opower/test_config_flow.py @@ -0,0 +1,204 @@ +"""Test the Opower config flow.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +from opower import CannotConnect, InvalidAuth +import pytest + +from homeassistant import config_entries +from homeassistant.components.opower.const import DOMAIN +from homeassistant.components.recorder import Recorder +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +@pytest.fixture(autouse=True, name="mock_setup_entry") +def override_async_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.opower.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_unload_entry() -> Generator[AsyncMock, None, None]: + """Mock unloading a config entry.""" + with patch( + "homeassistant.components.opower.async_unload_entry", + return_value=True, + ) as mock_unload_entry: + yield mock_unload_entry + + +async def test_form( + recorder_mock: Recorder, hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert not result["errors"] + + with patch( + "homeassistant.components.opower.config_flow.Opower.async_login", + ) as mock_login: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "utility": "Pacific Gas and Electric Company (PG&E)", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Pacific Gas and Electric Company (PG&E) (test-username)" + assert result2["data"] == { + "utility": "Pacific Gas and Electric Company (PG&E)", + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + assert mock_login.call_count == 1 + + +@pytest.mark.parametrize( + ("api_exception", "expected_error"), + [ + (InvalidAuth(), "invalid_auth"), + (CannotConnect(), "cannot_connect"), + ], +) +async def test_form_exceptions( + recorder_mock: Recorder, hass: HomeAssistant, api_exception, expected_error +) -> None: + """Test we handle exceptions.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.opower.config_flow.Opower.async_login", + side_effect=api_exception, + ) as mock_login: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "utility": "Pacific Gas and Electric Company (PG&E)", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": expected_error} + assert mock_login.call_count == 1 + + +async def test_form_already_configured( + recorder_mock: Recorder, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test user input for config_entry that already exists.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.opower.config_flow.Opower.async_login", + ) as mock_login: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "utility": "Pacific Gas and Electric Company (PG&E)", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + assert mock_login.call_count == 0 + + +async def test_form_not_already_configured( + recorder_mock: Recorder, + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test user input for config_entry different than the existing one.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.opower.config_flow.Opower.async_login", + ) as mock_login: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "utility": "Pacific Gas and Electric Company (PG&E)", + "username": "test-username2", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert ( + result2["title"] == "Pacific Gas and Electric Company (PG&E) (test-username2)" + ) + assert result2["data"] == { + "utility": "Pacific Gas and Electric Company (PG&E)", + "username": "test-username2", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 2 + assert mock_login.call_count == 1 + + +async def test_form_valid_reauth( + recorder_mock: Recorder, + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_unload_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that we can handle a valid reauth.""" + mock_config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + result = flows[0] + assert result["step_id"] == "reauth_confirm" + assert result["context"]["source"] == "reauth" + assert result["context"]["title_placeholders"] == {"name": mock_config_entry.title} + + with patch( + "homeassistant.components.opower.config_flow.Opower.async_login", + ) as mock_login: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-username", "password": "test-password2"}, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + await hass.async_block_till_done() + assert hass.config_entries.async_entries(DOMAIN)[0].data == { + "utility": "Pacific Gas and Electric Company (PG&E)", + "username": "test-username", + "password": "test-password2", + } + assert len(mock_unload_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert mock_login.call_count == 1 From 4ff158a105e815c2323d02cf163bc7d193f319d8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jul 2023 03:32:32 +0200 Subject: [PATCH 0097/1009] Remove NAM translations handled by device class (#95740) Remove translations handled by device class --- homeassistant/components/nam/button.py | 1 - homeassistant/components/nam/sensor.py | 1 - homeassistant/components/nam/strings.json | 8 -------- 3 files changed, 10 deletions(-) diff --git a/homeassistant/components/nam/button.py b/homeassistant/components/nam/button.py index a55215962086e5..a280369e7c8582 100644 --- a/homeassistant/components/nam/button.py +++ b/homeassistant/components/nam/button.py @@ -23,7 +23,6 @@ RESTART_BUTTON: ButtonEntityDescription = ButtonEntityDescription( key="restart", - translation_key="restart", device_class=ButtonDeviceClass.RESTART, entity_category=EntityCategory.CONFIG, ) diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index 3f9821a1e34f00..3c0b8bc9ba4784 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -338,7 +338,6 @@ class NAMSensorEntityDescription(SensorEntityDescription, NAMSensorRequiredKeysM ), NAMSensorEntityDescription( key=ATTR_SIGNAL_STRENGTH, - translation_key="signal_strength", suggested_display_precision=0, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, diff --git a/homeassistant/components/nam/strings.json b/homeassistant/components/nam/strings.json index e60855b882c1a3..e443a398984493 100644 --- a/homeassistant/components/nam/strings.json +++ b/homeassistant/components/nam/strings.json @@ -39,11 +39,6 @@ } }, "entity": { - "button": { - "restart": { - "name": "[%key:component::button::entity_component::restart::name%]" - } - }, "sensor": { "bme280_humidity": { "name": "BME280 humidity" @@ -153,9 +148,6 @@ "dht22_temperature": { "name": "DHT22 temperature" }, - "signal_strength": { - "name": "[%key:component::sensor::entity_component::signal_strength::name%]" - }, "last_restart": { "name": "Last restart" } From 4a5a8cdc29dfbfbf18581ff765bb6aba8feee534 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jul 2023 03:34:58 +0200 Subject: [PATCH 0098/1009] Add entity translations to minecraft server (#95737) --- .../components/minecraft_server/__init__.py | 1 - .../minecraft_server/binary_sensor.py | 2 ++ .../components/minecraft_server/sensor.py | 12 +++++++++ .../components/minecraft_server/strings.json | 27 +++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index dad8ebe7f11502..801b27ee9716f2 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -237,7 +237,6 @@ def __init__( ) -> None: """Initialize base entity.""" self._server = server - self._attr_name = type_name self._attr_icon = icon self._attr_unique_id = f"{self._server.unique_id}-{type_name}" self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/minecraft_server/binary_sensor.py b/homeassistant/components/minecraft_server/binary_sensor.py index 0bf4cdab859057..ecf7d747770345 100644 --- a/homeassistant/components/minecraft_server/binary_sensor.py +++ b/homeassistant/components/minecraft_server/binary_sensor.py @@ -29,6 +29,8 @@ async def async_setup_entry( class MinecraftServerStatusBinarySensor(MinecraftServerEntity, BinarySensorEntity): """Representation of a Minecraft Server status binary sensor.""" + _attr_translation_key = "status" + def __init__(self, server: MinecraftServer) -> None: """Initialize status binary sensor.""" super().__init__( diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index 2499dd8b75b523..5d056d98dd117a 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -74,6 +74,8 @@ def available(self) -> bool: class MinecraftServerVersionSensor(MinecraftServerSensorEntity): """Representation of a Minecraft Server version sensor.""" + _attr_translation_key = "version" + def __init__(self, server: MinecraftServer) -> None: """Initialize version sensor.""" super().__init__(server=server, type_name=NAME_VERSION, icon=ICON_VERSION) @@ -86,6 +88,8 @@ async def async_update(self) -> None: class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity): """Representation of a Minecraft Server protocol version sensor.""" + _attr_translation_key = "protocol_version" + def __init__(self, server: MinecraftServer) -> None: """Initialize protocol version sensor.""" super().__init__( @@ -102,6 +106,8 @@ async def async_update(self) -> None: class MinecraftServerLatencyTimeSensor(MinecraftServerSensorEntity): """Representation of a Minecraft Server latency time sensor.""" + _attr_translation_key = "latency" + def __init__(self, server: MinecraftServer) -> None: """Initialize latency time sensor.""" super().__init__( @@ -119,6 +125,8 @@ async def async_update(self) -> None: class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity): """Representation of a Minecraft Server online players sensor.""" + _attr_translation_key = "players_online" + def __init__(self, server: MinecraftServer) -> None: """Initialize online players sensor.""" super().__init__( @@ -144,6 +152,8 @@ async def async_update(self) -> None: class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity): """Representation of a Minecraft Server maximum number of players sensor.""" + _attr_translation_key = "players_max" + def __init__(self, server: MinecraftServer) -> None: """Initialize maximum number of players sensor.""" super().__init__( @@ -161,6 +171,8 @@ async def async_update(self) -> None: class MinecraftServerMOTDSensor(MinecraftServerSensorEntity): """Representation of a Minecraft Server MOTD sensor.""" + _attr_translation_key = "motd" + def __init__(self, server: MinecraftServer) -> None: """Initialize MOTD sensor.""" super().__init__( diff --git a/homeassistant/components/minecraft_server/strings.json b/homeassistant/components/minecraft_server/strings.json index 9e546a3cdfa171..b4d68bc611744d 100644 --- a/homeassistant/components/minecraft_server/strings.json +++ b/homeassistant/components/minecraft_server/strings.json @@ -18,5 +18,32 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + }, + "entity": { + "binary_sensor": { + "status": { + "name": "Status" + } + }, + "sensor": { + "version": { + "name": "Version" + }, + "protocol_version": { + "name": "Protocol version" + }, + "latency": { + "name": "Latency" + }, + "players_online": { + "name": "Players online" + }, + "players_max": { + "name": "Players max" + }, + "motd": { + "name": "World message" + } + } } } From 259455b32dc4ceacd3479e70e2c68e128407d30e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jul 2023 03:36:27 +0200 Subject: [PATCH 0099/1009] Add entity translations to melnor (#95734) --- homeassistant/components/melnor/number.py | 6 ++-- homeassistant/components/melnor/sensor.py | 7 ++-- homeassistant/components/melnor/strings.json | 34 ++++++++++++++++++++ homeassistant/components/melnor/switch.py | 2 +- homeassistant/components/melnor/time.py | 2 +- 5 files changed, 42 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/melnor/number.py b/homeassistant/components/melnor/number.py index 79b80a6d7b515e..e0f9c7d3bf67a8 100644 --- a/homeassistant/components/melnor/number.py +++ b/homeassistant/components/melnor/number.py @@ -48,7 +48,7 @@ class MelnorZoneNumberEntityDescription( native_min_value=1, icon="mdi:timer-cog-outline", key="manual_minutes", - name="Manual Duration", + translation_key="manual_minutes", native_unit_of_measurement=UnitOfTime.MINUTES, set_num_fn=lambda valve, value: valve.set_manual_watering_minutes(value), state_fn=lambda valve: valve.manual_watering_minutes, @@ -59,7 +59,7 @@ class MelnorZoneNumberEntityDescription( native_min_value=1, icon="mdi:calendar-refresh-outline", key="frequency_interval_hours", - name="Schedule Interval", + translation_key="frequency_interval_hours", native_unit_of_measurement=UnitOfTime.HOURS, set_num_fn=lambda valve, value: valve.set_frequency_interval_hours(value), state_fn=lambda valve: valve.frequency.interval_hours, @@ -70,7 +70,7 @@ class MelnorZoneNumberEntityDescription( native_min_value=1, icon="mdi:timer-outline", key="frequency_duration_minutes", - name="Schedule Duration", + translation_key="frequency_duration_minutes", native_unit_of_measurement=UnitOfTime.MINUTES, set_num_fn=lambda valve, value: valve.set_frequency_duration_minutes(value), state_fn=lambda valve: valve.frequency.duration_minutes, diff --git a/homeassistant/components/melnor/sensor.py b/homeassistant/components/melnor/sensor.py index b4a1d44a291762..edb906cc80f41e 100644 --- a/homeassistant/components/melnor/sensor.py +++ b/homeassistant/components/melnor/sensor.py @@ -87,7 +87,6 @@ class MelnorSensorEntityDescription( device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, key="battery", - name="Battery", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, state_fn=lambda device: device.battery_level, @@ -97,7 +96,7 @@ class MelnorSensorEntityDescription( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, key="rssi", - name="RSSI", + translation_key="rssi", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, state_class=SensorStateClass.MEASUREMENT, state_fn=lambda device: device.rssi, @@ -108,13 +107,13 @@ class MelnorSensorEntityDescription( MelnorZoneSensorEntityDescription( device_class=SensorDeviceClass.TIMESTAMP, key="manual_cycle_end", - name="Manual Cycle End", + translation_key="manual_cycle_end", state_fn=watering_seconds_left, ), MelnorZoneSensorEntityDescription( device_class=SensorDeviceClass.TIMESTAMP, key="next_cycle", - name="Next Cycle", + translation_key="next_cycle", state_fn=next_cycle, ), ] diff --git a/homeassistant/components/melnor/strings.json b/homeassistant/components/melnor/strings.json index 2fefa32b6bc1f2..51ca18b0b3d1c0 100644 --- a/homeassistant/components/melnor/strings.json +++ b/homeassistant/components/melnor/strings.json @@ -10,5 +10,39 @@ "title": "Discovered Melnor Bluetooth valve" } } + }, + "entity": { + "number": { + "manual_minutes": { + "name": "Manual duration" + }, + "frequency_interval_hours": { + "name": "Schedule interval" + }, + "frequency_duration_minutes": { + "name": "Schedule duration" + } + }, + "sensor": { + "rssi": { + "name": "RSSI" + }, + "manual_cycle_end": { + "name": "Manual cycle end" + }, + "next_cycle": { + "name": "Next cycle" + } + }, + "switch": { + "frequency": { + "name": "Schedule" + } + }, + "time": { + "frequency_start_time": { + "name": "Schedule start time" + } + } } } diff --git a/homeassistant/components/melnor/switch.py b/homeassistant/components/melnor/switch.py index e5f70bc25a0504..03bd28faa9d98e 100644 --- a/homeassistant/components/melnor/switch.py +++ b/homeassistant/components/melnor/switch.py @@ -53,7 +53,7 @@ class MelnorSwitchEntityDescription( device_class=SwitchDeviceClass.SWITCH, icon="mdi:calendar-sync-outline", key="frequency", - name="Schedule", + translation_key="frequency", on_off_fn=lambda valve, bool: valve.set_frequency_enabled(bool), state_fn=lambda valve: valve.schedule_enabled, ), diff --git a/homeassistant/components/melnor/time.py b/homeassistant/components/melnor/time.py index 7abdf62e20c910..943a7996aeb250 100644 --- a/homeassistant/components/melnor/time.py +++ b/homeassistant/components/melnor/time.py @@ -42,7 +42,7 @@ class MelnorZoneTimeEntityDescription( MelnorZoneTimeEntityDescription( entity_category=EntityCategory.CONFIG, key="frequency_start_time", - name="Schedule Start Time", + translation_key="frequency_start_time", set_time_fn=lambda valve, value: valve.set_frequency_start_time(value), state_fn=lambda valve: valve.frequency.start_time, ), From b2611b595e82ffe8cbafb49cce7301610aff4b01 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jul 2023 03:36:53 +0200 Subject: [PATCH 0100/1009] Use DeviceInfo object for Meater (#95733) Use DeviceInfo object --- homeassistant/components/meater/sensor.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index 0a1240c74716ee..88df3b3b615ddf 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -16,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -192,15 +193,15 @@ def __init__( """Initialise the sensor.""" super().__init__(coordinator) self._attr_name = f"Meater Probe {description.name}" - self._attr_device_info = { - "identifiers": { + self._attr_device_info = DeviceInfo( + identifiers={ # Serial numbers are unique identifiers within a specific domain (DOMAIN, device_id) }, - "manufacturer": "Apption Labs", - "model": "Meater Probe", - "name": f"Meater Probe {device_id}", - } + manufacturer="Apption Labs", + model="Meater Probe", + name=f"Meater Probe {device_id}", + ) self._attr_unique_id = f"{device_id}-{description.key}" self.device_id = device_id From 33bc1f01a4246366096f84baa0d076c4651cc9ad Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jul 2023 03:42:02 +0200 Subject: [PATCH 0101/1009] Add entity translations for lifx (#95727) --- .../components/lifx/binary_sensor.py | 4 +--- homeassistant/components/lifx/button.py | 4 +--- homeassistant/components/lifx/entity.py | 2 ++ homeassistant/components/lifx/light.py | 2 +- homeassistant/components/lifx/select.py | 8 ++------ homeassistant/components/lifx/sensor.py | 4 +--- homeassistant/components/lifx/strings.json | 20 +++++++++++++++++++ 7 files changed, 28 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/lifx/binary_sensor.py b/homeassistant/components/lifx/binary_sensor.py index 110661b1c5ca63..5719c881d1faa8 100644 --- a/homeassistant/components/lifx/binary_sensor.py +++ b/homeassistant/components/lifx/binary_sensor.py @@ -18,7 +18,7 @@ HEV_CYCLE_STATE_SENSOR = BinarySensorEntityDescription( key=HEV_CYCLE_STATE, - name="Clean Cycle", + translation_key="clean_cycle", entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.RUNNING, ) @@ -39,8 +39,6 @@ async def async_setup_entry( class LIFXHevCycleBinarySensorEntity(LIFXEntity, BinarySensorEntity): """LIFX HEV cycle state binary sensor.""" - _attr_has_entity_name = True - def __init__( self, coordinator: LIFXUpdateCoordinator, diff --git a/homeassistant/components/lifx/button.py b/homeassistant/components/lifx/button.py index 00d216351a0810..86e3bc569b148a 100644 --- a/homeassistant/components/lifx/button.py +++ b/homeassistant/components/lifx/button.py @@ -17,7 +17,6 @@ RESTART_BUTTON_DESCRIPTION = ButtonEntityDescription( key=RESTART, - name="Restart", device_class=ButtonDeviceClass.RESTART, entity_category=EntityCategory.CONFIG, ) @@ -45,8 +44,7 @@ async def async_setup_entry( class LIFXButton(LIFXEntity, ButtonEntity): """Base LIFX button.""" - _attr_has_entity_name: bool = True - _attr_should_poll: bool = False + _attr_should_poll = False def __init__(self, coordinator: LIFXUpdateCoordinator) -> None: """Initialise a LIFX button.""" diff --git a/homeassistant/components/lifx/entity.py b/homeassistant/components/lifx/entity.py index a86bda53cfdc86..5f08b6e7884478 100644 --- a/homeassistant/components/lifx/entity.py +++ b/homeassistant/components/lifx/entity.py @@ -14,6 +14,8 @@ class LIFXEntity(CoordinatorEntity[LIFXUpdateCoordinator]): """Representation of a LIFX entity with a coordinator.""" + _attr_has_entity_name = True + def __init__(self, coordinator: LIFXUpdateCoordinator) -> None: """Initialise the light.""" super().__init__(coordinator) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index cb901dcbe477f9..0e56155832ffeb 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -112,6 +112,7 @@ class LIFXLight(LIFXEntity, LightEntity): """Representation of a LIFX light.""" _attr_supported_features = LightEntityFeature.TRANSITION | LightEntityFeature.EFFECT + _attr_name = None def __init__( self, @@ -131,7 +132,6 @@ def __init__( self.postponed_update: CALLBACK_TYPE | None = None self.entry = entry self._attr_unique_id = self.coordinator.serial_number - self._attr_name = self.bulb.label self._attr_min_color_temp_kelvin = bulb_features["min_kelvin"] self._attr_max_color_temp_kelvin = bulb_features["max_kelvin"] if bulb_features["min_kelvin"] != bulb_features["max_kelvin"]: diff --git a/homeassistant/components/lifx/select.py b/homeassistant/components/lifx/select.py index 9ad457e0270551..183e31dec1fde2 100644 --- a/homeassistant/components/lifx/select.py +++ b/homeassistant/components/lifx/select.py @@ -23,14 +23,14 @@ INFRARED_BRIGHTNESS_ENTITY = SelectEntityDescription( key=INFRARED_BRIGHTNESS, - name="Infrared brightness", + translation_key="infrared_brightness", entity_category=EntityCategory.CONFIG, options=list(INFRARED_BRIGHTNESS_VALUES_MAP.values()), ) THEME_ENTITY = SelectEntityDescription( key=ATTR_THEME, - name="Theme", + translation_key="theme", entity_category=EntityCategory.CONFIG, options=THEME_NAMES, ) @@ -58,8 +58,6 @@ async def async_setup_entry( class LIFXInfraredBrightnessSelectEntity(LIFXEntity, SelectEntity): """LIFX Nightvision infrared brightness configuration entity.""" - _attr_has_entity_name = True - def __init__( self, coordinator: LIFXUpdateCoordinator, @@ -90,8 +88,6 @@ async def async_select_option(self, option: str) -> None: class LIFXThemeSelectEntity(LIFXEntity, SelectEntity): """Theme entity for LIFX multizone devices.""" - _attr_has_entity_name = True - def __init__( self, coordinator: LIFXUpdateCoordinator, diff --git a/homeassistant/components/lifx/sensor.py b/homeassistant/components/lifx/sensor.py index 654b528575601c..e10f9579bc31fd 100644 --- a/homeassistant/components/lifx/sensor.py +++ b/homeassistant/components/lifx/sensor.py @@ -22,7 +22,7 @@ RSSI_SENSOR = SensorEntityDescription( key=ATTR_RSSI, - name="RSSI", + translation_key="rssi", device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, @@ -41,8 +41,6 @@ async def async_setup_entry( class LIFXRssiSensor(LIFXEntity, SensorEntity): """LIFX RSSI sensor.""" - _attr_has_entity_name = True - def __init__( self, coordinator: LIFXUpdateCoordinator, diff --git a/homeassistant/components/lifx/strings.json b/homeassistant/components/lifx/strings.json index 93d3bd62abea74..69055d6bbc67cb 100644 --- a/homeassistant/components/lifx/strings.json +++ b/homeassistant/components/lifx/strings.json @@ -25,5 +25,25 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } + }, + "entity": { + "binary_sensor": { + "clean_cycle": { + "name": "Clean cycle" + } + }, + "select": { + "infrared_brightness": { + "name": "Infrared brightness" + }, + "theme": { + "name": "Theme" + } + }, + "sensor": { + "rssi": { + "name": "RSSI" + } + } } } From ab500699180b51c80d9d63b9c35796842addac08 Mon Sep 17 00:00:00 2001 From: Mike Woudenberg Date: Mon, 3 Jul 2023 03:52:52 +0200 Subject: [PATCH 0102/1009] 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 c757d2f0080ae7..5eecc0b3f59a89 100644 --- a/homeassistant/components/loqed/config_flow.py +++ b/homeassistant/components/loqed/config_flow.py @@ -44,9 +44,9 @@ async def validate_input( ) 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 @@ async def async_step_user( 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 0374e72d5f0978..6b1c0311a2d049 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 ec7d5467b49a90..507debc02ab9ba 100644 --- a/homeassistant/components/loqed/coordinator.py +++ b/homeassistant/components/loqed/coordinator.py @@ -8,8 +8,8 @@ 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 @@ def __init__( ) -> 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 @@ async def ensure_webhooks(self) -> None: 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 1b1731815b46fd..978fe844d619cb 100644 --- a/homeassistant/components/loqed/entity.py +++ b/homeassistant/components/loqed/entity.py @@ -23,7 +23,7 @@ def __init__(self, coordinator: LoqedDataCoordinator) -> None: 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 5a7540ba89e1fb..d34df19e2d11c3 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 5448f01b7f90dc..6f3316b283f11f 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 a961f10cb1b607..00000000000000 --- 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 89b67ee325855c..960ad9def6b97e 100644 --- a/tests/components/loqed/test_init.py +++ b/tests/components/loqed/test_init.py @@ -15,31 +15,6 @@ 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 422b7ab6830511..59e70212f92b22 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 b24c6adc75b97825339776e56c800046bdd5647a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Jul 2023 20:53:50 -0500 Subject: [PATCH 0103/1009] Avoid regex for negative zero check in sensor (#95691) * Avoid regex for negative zero check in sensor We can avoid calling the regex for every sensor value since most of the time values are not negative zero * tweak * tweak * Apply suggestions from code review * simpler * cover * safer and still fast * safer and still fast * prep for py3.11 * fix check * add missing cover * more coverage * coverage * coverage --- homeassistant/components/sensor/__init__.py | 47 ++++++------ tests/components/sensor/test_init.py | 79 ++++++++++++++++++++- tests/helpers/test_template.py | 6 ++ 3 files changed, 110 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index ad09a1b5fdb4a8..2477e849666008 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -10,6 +10,7 @@ import logging from math import ceil, floor, log10 import re +import sys from typing import Any, Final, cast, final from typing_extensions import Self @@ -92,6 +93,8 @@ NEGATIVE_ZERO_PATTERN = re.compile(r"^-(0\.?0*)$") +PY_311 = sys.version_info >= (3, 11, 0) + SCAN_INTERVAL: Final = timedelta(seconds=30) __all__ = [ @@ -638,10 +641,12 @@ def state(self) -> Any: ) precision = precision + floor(ratio_log) - value = f"{converted_numerical_value:.{precision}f}" - # This can be replaced with adding the z option when we drop support for - # Python 3.10 - value = NEGATIVE_ZERO_PATTERN.sub(r"\1", value) + if PY_311: + value = f"{converted_numerical_value:z.{precision}f}" + else: + value = f"{converted_numerical_value:.{precision}f}" + if value.startswith("-0") and NEGATIVE_ZERO_PATTERN.match(value): + value = value[1:] else: value = converted_numerical_value @@ -883,29 +888,31 @@ def async_update_suggested_units(hass: HomeAssistant) -> None: ) +def _display_precision(hass: HomeAssistant, entity_id: str) -> int | None: + """Return the display precision.""" + if not (entry := er.async_get(hass).async_get(entity_id)) or not ( + sensor_options := entry.options.get(DOMAIN) + ): + return None + if (display_precision := sensor_options.get("display_precision")) is not None: + return cast(int, display_precision) + return sensor_options.get("suggested_display_precision") + + @callback def async_rounded_state(hass: HomeAssistant, entity_id: str, state: State) -> str: """Return the state rounded for presentation.""" - - def display_precision() -> int | None: - """Return the display precision.""" - if not (entry := er.async_get(hass).async_get(entity_id)) or not ( - sensor_options := entry.options.get(DOMAIN) - ): - return None - if (display_precision := sensor_options.get("display_precision")) is not None: - return cast(int, display_precision) - return sensor_options.get("suggested_display_precision") - value = state.state - if (precision := display_precision()) is None: + if (precision := _display_precision(hass, entity_id)) is None: return value with suppress(TypeError, ValueError): numerical_value = float(value) - value = f"{numerical_value:.{precision}f}" - # This can be replaced with adding the z option when we drop support for - # Python 3.10 - value = NEGATIVE_ZERO_PATTERN.sub(r"\1", value) + if PY_311: + value = f"{numerical_value:z.{precision}f}" + else: + value = f"{numerical_value:.{precision}f}" + if value.startswith("-0") and NEGATIVE_ZERO_PATTERN.match(value): + value = value[1:] return value diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index d1da0a8166f6a9..b5d425029d08fc 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -17,6 +17,7 @@ SensorEntity, SensorEntityDescription, SensorStateClass, + async_rounded_state, async_update_suggested_units, ) from homeassistant.config_entries import ConfigEntry, ConfigFlow @@ -557,6 +558,22 @@ async def test_restore_sensor_restore_state( 100, "38", ), + ( + SensorDeviceClass.ATMOSPHERIC_PRESSURE, + UnitOfPressure.INHG, + UnitOfPressure.HPA, + UnitOfPressure.HPA, + -0.00, + "0.0", + ), + ( + SensorDeviceClass.ATMOSPHERIC_PRESSURE, + UnitOfPressure.INHG, + UnitOfPressure.HPA, + UnitOfPressure.HPA, + -0.00001, + "0", + ), ], ) async def test_custom_unit( @@ -592,10 +609,15 @@ async def test_custom_unit( assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() - state = hass.states.get(entity0.entity_id) + entity_id = entity0.entity_id + state = hass.states.get(entity_id) assert state.state == custom_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit + assert ( + async_rounded_state(hass, entity_id, hass.states.get(entity_id)) == custom_state + ) + @pytest.mark.parametrize( ( @@ -902,7 +924,7 @@ async def test_custom_unit_change( "1000000", "1093613", SensorDeviceClass.DISTANCE, - ), + ) ], ) async def test_unit_conversion_priority( @@ -1130,6 +1152,9 @@ async def test_unit_conversion_priority_precision( "sensor": {"suggested_display_precision": 2}, "sensor.private": {"suggested_unit_of_measurement": automatic_unit}, } + assert float(async_rounded_state(hass, entity0.entity_id, state)) == pytest.approx( + round(automatic_state, 2) + ) # Unregistered entity -> Follow native unit state = hass.states.get(entity1.entity_id) @@ -1172,6 +1197,20 @@ async def test_unit_conversion_priority_precision( assert float(state.state) == pytest.approx(custom_state) assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit + # Set a display_precision, this should have priority over suggested_display_precision + entity_registry.async_update_entity_options( + entity0.entity_id, + "sensor", + {"suggested_display_precision": 2, "display_precision": 4}, + ) + entry0 = entity_registry.async_get(entity0.entity_id) + assert entry0.options["sensor"]["suggested_display_precision"] == 2 + assert entry0.options["sensor"]["display_precision"] == 4 + await hass.async_block_till_done() + assert float(async_rounded_state(hass, entity0.entity_id, state)) == pytest.approx( + round(custom_state, 4) + ) + @pytest.mark.parametrize( ( @@ -2362,3 +2401,39 @@ async def async_setup_entry_platform( state = hass.states.get(entity4.entity_id) assert state.attributes == {"device_class": "battery", "friendly_name": "Battery"} + + +def test_async_rounded_state_unregistered_entity_is_passthrough( + hass: HomeAssistant, +) -> None: + """Test async_rounded_state on unregistered entity is passthrough.""" + hass.states.async_set("sensor.test", "1.004") + state = hass.states.get("sensor.test") + assert async_rounded_state(hass, "sensor.test", state) == "1.004" + hass.states.async_set("sensor.test", "-0.0") + state = hass.states.get("sensor.test") + assert async_rounded_state(hass, "sensor.test", state) == "-0.0" + + +def test_async_rounded_state_registered_entity_with_display_precision( + hass: HomeAssistant, +) -> None: + """Test async_rounded_state on registered with display precision. + + The -0 should be dropped. + """ + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get_or_create("sensor", "test", "very_unique") + entity_registry.async_update_entity_options( + entry.entity_id, + "sensor", + {"suggested_display_precision": 2, "display_precision": 4}, + ) + entity_id = entry.entity_id + hass.states.async_set(entity_id, "1.004") + state = hass.states.get(entity_id) + assert async_rounded_state(hass, entity_id, state) == "1.0040" + hass.states.async_set(entity_id, "-0.0") + state = hass.states.get(entity_id) + assert async_rounded_state(hass, entity_id, state) == "0.0000" diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 73854147372357..cd0b5a2ab88996 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -3889,6 +3889,8 @@ def test_state_with_unit_and_rounding(hass: HomeAssistant) -> None: hass.states.async_set("sensor.test", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"}) hass.states.async_set("sensor.test2", "23", {ATTR_UNIT_OF_MEASUREMENT: "beers"}) + hass.states.async_set("sensor.test3", "-0.0", {ATTR_UNIT_OF_MEASUREMENT: "beers"}) + hass.states.async_set("sensor.test4", "-0", {ATTR_UNIT_OF_MEASUREMENT: "beers"}) # state_with_unit property tpl = template.Template("{{ states.sensor.test.state_with_unit }}", hass) @@ -3905,6 +3907,8 @@ def test_state_with_unit_and_rounding(hass: HomeAssistant) -> None: # AllStates.__call__ and rounded=True tpl7 = template.Template("{{ states('sensor.test', rounded=True) }}", hass) tpl8 = template.Template("{{ states('sensor.test2', rounded=True) }}", hass) + tpl9 = template.Template("{{ states('sensor.test3', rounded=True) }}", hass) + tpl10 = template.Template("{{ states('sensor.test4', rounded=True) }}", hass) assert tpl.async_render() == "23.00 beers" assert tpl2.async_render() == "23 beers" @@ -3914,6 +3918,8 @@ def test_state_with_unit_and_rounding(hass: HomeAssistant) -> None: assert tpl6.async_render() == "23 beers" assert tpl7.async_render() == 23.0 assert tpl8.async_render() == 23 + assert tpl9.async_render() == 0.0 + assert tpl10.async_render() == 0 hass.states.async_set("sensor.test", "23.015", {ATTR_UNIT_OF_MEASUREMENT: "beers"}) hass.states.async_set("sensor.test2", "23.015", {ATTR_UNIT_OF_MEASUREMENT: "beers"}) From 4551954c85e3d3764ecd29a653fa698ed4959b44 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jul 2023 03:56:37 +0200 Subject: [PATCH 0104/1009] Add entity translations to LaCrosse View (#95686) --- .../components/lacrosse_view/sensor.py | 20 ++++++--------- .../components/lacrosse_view/strings.json | 25 +++++++++++++++++++ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index 833c47dffb0dc1..547772cad09ff1 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -67,7 +67,6 @@ def get_value(sensor: Sensor, field: str) -> float | int | str | None: "Temperature": LaCrosseSensorEntityDescription( key="Temperature", device_class=SensorDeviceClass.TEMPERATURE, - name="Temperature", state_class=SensorStateClass.MEASUREMENT, value_fn=get_value, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -75,22 +74,20 @@ def get_value(sensor: Sensor, field: str) -> float | int | str | None: "Humidity": LaCrosseSensorEntityDescription( key="Humidity", device_class=SensorDeviceClass.HUMIDITY, - name="Humidity", state_class=SensorStateClass.MEASUREMENT, value_fn=get_value, native_unit_of_measurement=PERCENTAGE, ), "HeatIndex": LaCrosseSensorEntityDescription( key="HeatIndex", + translation_key="heat_index", device_class=SensorDeviceClass.TEMPERATURE, - name="Heat index", state_class=SensorStateClass.MEASUREMENT, value_fn=get_value, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, ), "WindSpeed": LaCrosseSensorEntityDescription( key="WindSpeed", - name="Wind speed", state_class=SensorStateClass.MEASUREMENT, value_fn=get_value, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, @@ -98,7 +95,6 @@ def get_value(sensor: Sensor, field: str) -> float | int | str | None: ), "Rain": LaCrosseSensorEntityDescription( key="Rain", - name="Rain", state_class=SensorStateClass.MEASUREMENT, value_fn=get_value, native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, @@ -106,23 +102,23 @@ def get_value(sensor: Sensor, field: str) -> float | int | str | None: ), "WindHeading": LaCrosseSensorEntityDescription( key="WindHeading", - name="Wind heading", + translation_key="wind_heading", value_fn=get_value, native_unit_of_measurement=DEGREE, ), "WetDry": LaCrosseSensorEntityDescription( key="WetDry", - name="Wet/Dry", + translation_key="wet_dry", value_fn=get_value, ), "Flex": LaCrosseSensorEntityDescription( key="Flex", - name="Flex", + translation_key="flex", value_fn=get_value, ), "BarometricPressure": LaCrosseSensorEntityDescription( key="BarometricPressure", - name="Barometric pressure", + translation_key="barometric_pressure", state_class=SensorStateClass.MEASUREMENT, value_fn=get_value, device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE, @@ -130,7 +126,7 @@ def get_value(sensor: Sensor, field: str) -> float | int | str | None: ), "FeelsLike": LaCrosseSensorEntityDescription( key="FeelsLike", - name="Feels like", + translation_key="feels_like", state_class=SensorStateClass.MEASUREMENT, value_fn=get_value, device_class=SensorDeviceClass.TEMPERATURE, @@ -138,7 +134,7 @@ def get_value(sensor: Sensor, field: str) -> float | int | str | None: ), "WindChill": LaCrosseSensorEntityDescription( key="WindChill", - name="Wind chill", + translation_key="wind_chill", state_class=SensorStateClass.MEASUREMENT, value_fn=get_value, device_class=SensorDeviceClass.TEMPERATURE, @@ -193,7 +189,7 @@ class LaCrosseViewSensor( """LaCrosse View sensor.""" entity_description: LaCrosseSensorEntityDescription - _attr_has_entity_name: bool = True + _attr_has_entity_name = True def __init__( self, diff --git a/homeassistant/components/lacrosse_view/strings.json b/homeassistant/components/lacrosse_view/strings.json index 160517793d8f9c..8dc27ba259e622 100644 --- a/homeassistant/components/lacrosse_view/strings.json +++ b/homeassistant/components/lacrosse_view/strings.json @@ -17,5 +17,30 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "entity": { + "sensor": { + "heat_index": { + "name": "Heat index" + }, + "wind_heading": { + "name": "Wind heading" + }, + "wet_dry": { + "name": "Wet/Dry" + }, + "flex": { + "name": "Flex" + }, + "barometric_pressure": { + "name": "Barometric pressure" + }, + "feels_like": { + "name": "Feels like" + }, + "wind_chill": { + "name": "Wind chill" + } + } } } From 792525b7a202cd6f76b0f1af9a4dd02534556673 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jul 2023 04:41:46 +0200 Subject: [PATCH 0105/1009] Add entity translations for Meater (#95732) * Add entity translations for Meater * Update homeassistant/components/meater/sensor.py --------- Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/components/meater/sensor.py | 17 ++++++------ homeassistant/components/meater/strings.json | 28 ++++++++++++++++++++ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index 88df3b3b615ddf..cf71455a81bb47 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -64,8 +64,8 @@ def _remaining_time_to_timestamp(probe: MeaterProbe) -> datetime | None: # Ambient temperature MeaterSensorEntityDescription( key="ambient", + translation_key="ambient", device_class=SensorDeviceClass.TEMPERATURE, - name="Ambient", native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, available=lambda probe: probe is not None, @@ -74,8 +74,8 @@ def _remaining_time_to_timestamp(probe: MeaterProbe) -> datetime | None: # Internal temperature (probe tip) MeaterSensorEntityDescription( key="internal", + translation_key="internal", device_class=SensorDeviceClass.TEMPERATURE, - name="Internal", native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, available=lambda probe: probe is not None, @@ -84,7 +84,7 @@ def _remaining_time_to_timestamp(probe: MeaterProbe) -> datetime | None: # Name of selected meat in user language or user given custom name MeaterSensorEntityDescription( key="cook_name", - name="Cooking", + translation_key="cook_name", available=lambda probe: probe is not None and probe.cook is not None, value=lambda probe: probe.cook.name if probe.cook else None, ), @@ -92,15 +92,15 @@ def _remaining_time_to_timestamp(probe: MeaterProbe) -> datetime | None: # Slightly Underdone, Finished, Slightly Overdone, OVERCOOK!. Not translated. MeaterSensorEntityDescription( key="cook_state", - name="Cook state", + translation_key="cook_state", available=lambda probe: probe is not None and probe.cook is not None, value=lambda probe: probe.cook.state if probe.cook else None, ), # Target temperature MeaterSensorEntityDescription( key="cook_target_temp", + translation_key="cook_target_temp", device_class=SensorDeviceClass.TEMPERATURE, - name="Target", native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, available=lambda probe: probe is not None and probe.cook is not None, @@ -111,8 +111,8 @@ def _remaining_time_to_timestamp(probe: MeaterProbe) -> datetime | None: # Peak temperature MeaterSensorEntityDescription( key="cook_peak_temp", + translation_key="cook_peak_temp", device_class=SensorDeviceClass.TEMPERATURE, - name="Peak", native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, available=lambda probe: probe is not None and probe.cook is not None, @@ -124,8 +124,8 @@ def _remaining_time_to_timestamp(probe: MeaterProbe) -> datetime | None: # Exposed as a TIMESTAMP sensor where the timestamp is current time + remaining time. MeaterSensorEntityDescription( key="cook_time_remaining", + translation_key="cook_time_remaining", device_class=SensorDeviceClass.TIMESTAMP, - name="Remaining time", available=lambda probe: probe is not None and probe.cook is not None, value=_remaining_time_to_timestamp, ), @@ -133,8 +133,8 @@ def _remaining_time_to_timestamp(probe: MeaterProbe) -> datetime | None: # where the timestamp is current time - elapsed time. MeaterSensorEntityDescription( key="cook_time_elapsed", + translation_key="cook_time_elapsed", device_class=SensorDeviceClass.TIMESTAMP, - name="Elapsed time", available=lambda probe: probe is not None and probe.cook is not None, value=_elapsed_time_to_timestamp, ), @@ -192,7 +192,6 @@ def __init__( ) -> None: """Initialise the sensor.""" super().__init__(coordinator) - self._attr_name = f"Meater Probe {description.name}" self._attr_device_info = DeviceInfo( identifiers={ # Serial numbers are unique identifiers within a specific domain diff --git a/homeassistant/components/meater/strings.json b/homeassistant/components/meater/strings.json index 7f4a97a5b19c5a..279841bb14777c 100644 --- a/homeassistant/components/meater/strings.json +++ b/homeassistant/components/meater/strings.json @@ -26,5 +26,33 @@ "unknown_auth_error": "[%key:common::config_flow::error::unknown%]", "service_unavailable_error": "The API is currently unavailable, please try again later." } + }, + "entity": { + "sensor": { + "ambient": { + "name": "Ambient temperature" + }, + "internal": { + "name": "Internal temperature" + }, + "cook_name": { + "name": "Cooking" + }, + "cook_state": { + "name": "Cook state" + }, + "cook_target_temp": { + "name": "Target temperature" + }, + "cook_peak_temp": { + "name": "Peak temperature" + }, + "cook_time_remaining": { + "name": "Time remaining" + }, + "cook_time_elapsed": { + "name": "Time elapsed" + } + } } } From 7d6595f755fe4232df60822806a756694ac26a80 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 2 Jul 2023 19:42:39 -0700 Subject: [PATCH 0106/1009] Delete the local calendar store when removing the config entry (#95599) * Delete calendar store when removing the config entry * Unlink file on remove with tests --- .../components/local_calendar/__init__.py | 11 +++++++++++ tests/components/local_calendar/test_init.py | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 tests/components/local_calendar/test_init.py diff --git a/homeassistant/components/local_calendar/__init__.py b/homeassistant/components/local_calendar/__init__.py index 33ad67cc81a6ef..7c1d2f09b04b80 100644 --- a/homeassistant/components/local_calendar/__init__.py +++ b/homeassistant/components/local_calendar/__init__.py @@ -39,3 +39,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle removal of an entry.""" + key = slugify(entry.data[CONF_CALENDAR_NAME]) + path = Path(hass.config.path(STORAGE_PATH.format(key=key))) + + def unlink(path: Path) -> None: + path.unlink(missing_ok=True) + + await hass.async_add_executor_job(unlink, path) diff --git a/tests/components/local_calendar/test_init.py b/tests/components/local_calendar/test_init.py new file mode 100644 index 00000000000000..e5ca209e8a667e --- /dev/null +++ b/tests/components/local_calendar/test_init.py @@ -0,0 +1,18 @@ +"""Tests for init platform of local calendar.""" + +from unittest.mock import patch + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_remove_config_entry( + hass: HomeAssistant, setup_integration: None, config_entry: MockConfigEntry +) -> None: + """Test removing a config entry.""" + + with patch("homeassistant.components.local_calendar.Path.unlink") as unlink_mock: + assert await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + unlink_mock.assert_called_once() From 7fdbc7b75d01fb64b7f0e8e5b67b80a4e214005c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jul 2023 04:43:14 +0200 Subject: [PATCH 0107/1009] Clean up solarlog const file (#95542) Move platform specifics to their own file --- homeassistant/components/solarlog/const.py | 196 ------------------- homeassistant/components/solarlog/sensor.py | 199 +++++++++++++++++++- 2 files changed, 197 insertions(+), 198 deletions(-) diff --git a/homeassistant/components/solarlog/const.py b/homeassistant/components/solarlog/const.py index 059dab3da78fb1..d8ba49adbec2cb 100644 --- a/homeassistant/components/solarlog/const.py +++ b/homeassistant/components/solarlog/const.py @@ -1,204 +1,8 @@ """Constants for the Solar-Log integration.""" from __future__ import annotations -from collections.abc import Callable -from dataclasses import dataclass -from datetime import datetime - -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import ( - PERCENTAGE, - UnitOfElectricPotential, - UnitOfEnergy, - UnitOfPower, -) -from homeassistant.util.dt import as_local - DOMAIN = "solarlog" # Default config for solarlog. DEFAULT_HOST = "http://solar-log" DEFAULT_NAME = "solarlog" - - -@dataclass -class SolarLogSensorEntityDescription(SensorEntityDescription): - """Describes Solarlog sensor entity.""" - - value: Callable[[float | int], float] | Callable[[datetime], datetime] | None = None - - -SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( - SolarLogSensorEntityDescription( - key="time", - name="last update", - device_class=SensorDeviceClass.TIMESTAMP, - value=as_local, - ), - SolarLogSensorEntityDescription( - key="power_ac", - name="power AC", - icon="mdi:solar-power", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SolarLogSensorEntityDescription( - key="power_dc", - name="power DC", - icon="mdi:solar-power", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SolarLogSensorEntityDescription( - key="voltage_ac", - name="voltage AC", - native_unit_of_measurement=UnitOfElectricPotential.VOLT, - device_class=SensorDeviceClass.VOLTAGE, - state_class=SensorStateClass.MEASUREMENT, - ), - SolarLogSensorEntityDescription( - key="voltage_dc", - name="voltage DC", - native_unit_of_measurement=UnitOfElectricPotential.VOLT, - device_class=SensorDeviceClass.VOLTAGE, - state_class=SensorStateClass.MEASUREMENT, - ), - SolarLogSensorEntityDescription( - key="yield_day", - name="yield day", - icon="mdi:solar-power", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - value=lambda value: round(value / 1000, 3), - ), - SolarLogSensorEntityDescription( - key="yield_yesterday", - name="yield yesterday", - icon="mdi:solar-power", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - value=lambda value: round(value / 1000, 3), - ), - SolarLogSensorEntityDescription( - key="yield_month", - name="yield month", - icon="mdi:solar-power", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - value=lambda value: round(value / 1000, 3), - ), - SolarLogSensorEntityDescription( - key="yield_year", - name="yield year", - icon="mdi:solar-power", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - value=lambda value: round(value / 1000, 3), - ), - SolarLogSensorEntityDescription( - key="yield_total", - name="yield total", - icon="mdi:solar-power", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, - value=lambda value: round(value / 1000, 3), - ), - SolarLogSensorEntityDescription( - key="consumption_ac", - name="consumption AC", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SolarLogSensorEntityDescription( - key="consumption_day", - name="consumption day", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - value=lambda value: round(value / 1000, 3), - ), - SolarLogSensorEntityDescription( - key="consumption_yesterday", - name="consumption yesterday", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - value=lambda value: round(value / 1000, 3), - ), - SolarLogSensorEntityDescription( - key="consumption_month", - name="consumption month", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - value=lambda value: round(value / 1000, 3), - ), - SolarLogSensorEntityDescription( - key="consumption_year", - name="consumption year", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - value=lambda value: round(value / 1000, 3), - ), - SolarLogSensorEntityDescription( - key="consumption_total", - name="consumption total", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, - value=lambda value: round(value / 1000, 3), - ), - SolarLogSensorEntityDescription( - key="total_power", - name="installed peak power", - icon="mdi:solar-power", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - ), - SolarLogSensorEntityDescription( - key="alternator_loss", - name="alternator loss", - icon="mdi:solar-power", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SolarLogSensorEntityDescription( - key="capacity", - name="capacity", - icon="mdi:solar-power", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.POWER_FACTOR, - state_class=SensorStateClass.MEASUREMENT, - value=lambda value: round(value * 100, 1), - ), - SolarLogSensorEntityDescription( - key="efficiency", - name="efficiency", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.POWER_FACTOR, - state_class=SensorStateClass.MEASUREMENT, - value=lambda value: round(value * 100, 1), - ), - SolarLogSensorEntityDescription( - key="power_available", - name="power available", - icon="mdi:solar-power", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SolarLogSensorEntityDescription( - key="usage", - name="usage", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.POWER_FACTOR, - state_class=SensorStateClass.MEASUREMENT, - value=lambda value: round(value * 100, 1), - ), -) diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index 4180d48cdef28c..906d9aee629a85 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -1,13 +1,208 @@ """Platform for solarlog sensors.""" -from homeassistant.components.sensor import SensorEntity +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + PERCENTAGE, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util.dt import as_local from . import SolarlogData -from .const import DOMAIN, SENSOR_TYPES, SolarLogSensorEntityDescription +from .const import DOMAIN + + +@dataclass +class SolarLogSensorEntityDescription(SensorEntityDescription): + """Describes Solarlog sensor entity.""" + + value: Callable[[float | int], float] | Callable[[datetime], datetime] | None = None + + +SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( + SolarLogSensorEntityDescription( + key="time", + name="last update", + device_class=SensorDeviceClass.TIMESTAMP, + value=as_local, + ), + SolarLogSensorEntityDescription( + key="power_ac", + name="power AC", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SolarLogSensorEntityDescription( + key="power_dc", + name="power DC", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SolarLogSensorEntityDescription( + key="voltage_ac", + name="voltage AC", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SolarLogSensorEntityDescription( + key="voltage_dc", + name="voltage DC", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SolarLogSensorEntityDescription( + key="yield_day", + name="yield day", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + value=lambda value: round(value / 1000, 3), + ), + SolarLogSensorEntityDescription( + key="yield_yesterday", + name="yield yesterday", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + value=lambda value: round(value / 1000, 3), + ), + SolarLogSensorEntityDescription( + key="yield_month", + name="yield month", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + value=lambda value: round(value / 1000, 3), + ), + SolarLogSensorEntityDescription( + key="yield_year", + name="yield year", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + value=lambda value: round(value / 1000, 3), + ), + SolarLogSensorEntityDescription( + key="yield_total", + name="yield total", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + value=lambda value: round(value / 1000, 3), + ), + SolarLogSensorEntityDescription( + key="consumption_ac", + name="consumption AC", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SolarLogSensorEntityDescription( + key="consumption_day", + name="consumption day", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + value=lambda value: round(value / 1000, 3), + ), + SolarLogSensorEntityDescription( + key="consumption_yesterday", + name="consumption yesterday", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + value=lambda value: round(value / 1000, 3), + ), + SolarLogSensorEntityDescription( + key="consumption_month", + name="consumption month", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + value=lambda value: round(value / 1000, 3), + ), + SolarLogSensorEntityDescription( + key="consumption_year", + name="consumption year", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + value=lambda value: round(value / 1000, 3), + ), + SolarLogSensorEntityDescription( + key="consumption_total", + name="consumption total", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + value=lambda value: round(value / 1000, 3), + ), + SolarLogSensorEntityDescription( + key="total_power", + name="installed peak power", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + ), + SolarLogSensorEntityDescription( + key="alternator_loss", + name="alternator loss", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SolarLogSensorEntityDescription( + key="capacity", + name="capacity", + icon="mdi:solar-power", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + value=lambda value: round(value * 100, 1), + ), + SolarLogSensorEntityDescription( + key="efficiency", + name="efficiency", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + value=lambda value: round(value * 100, 1), + ), + SolarLogSensorEntityDescription( + key="power_available", + name="power available", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SolarLogSensorEntityDescription( + key="usage", + name="usage", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + value=lambda value: round(value * 100, 1), + ), +) async def async_setup_entry( From 75bdb0336367656c7f073f6a0f53b8fec509692a 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 0108/1009] 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 11e58fca775c8a..ffe6d7f5433bb3 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -10,7 +10,11 @@ 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_reset_meters(service_call): 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 302d3879a04b1f..88a77407c07ff6 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.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 7bdd64a3f7b56481d3d3b0fe7b2ff3e2918f3204 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Jul 2023 21:47:25 -0500 Subject: [PATCH 0109/1009] 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 53c8577be44c72..731743e48c8eeb 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -3,6 +3,7 @@ from collections import OrderedDict from collections.abc import Mapping +import json import logging from typing import Any @@ -408,6 +409,11 @@ async def _retrieve_encryption_key_from_dashboard(self) -> bool: 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 4a99de77c1aab3..662816a53d8d26 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 2b66480894127c8807baccb43a6a76c45295935b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Jul 2023 22:00:33 -0500 Subject: [PATCH 0110/1009] Speed up routing URLs (#95721) alternative to #95717 --- homeassistant/components/http/__init__.py | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index fda8717c3ddead..c8fa05b2730751 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -18,6 +18,11 @@ from aiohttp.web_exceptions import HTTPMovedPermanently, HTTPRedirection from aiohttp.web_log import AccessLogger from aiohttp.web_protocol import RequestHandler +from aiohttp.web_urldispatcher import ( + AbstractResource, + UrlDispatcher, + UrlMappingMatchInfo, +) from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa @@ -303,6 +308,10 @@ def __init__( "max_field_size": MAX_LINE_SIZE, }, ) + # By default aiohttp does a linear search for routing rules, + # we have a lot of routes, so use a dict lookup with a fallback + # to the linear search. + self.app._router = FastUrlDispatcher() # pylint: disable=protected-access self.hass = hass self.ssl_certificate = ssl_certificate self.ssl_peer_certificate = ssl_peer_certificate @@ -565,3 +574,40 @@ async def start_http_server_and_save_config( ] store.async_delay_save(lambda: conf, SAVE_DELAY) + + +class FastUrlDispatcher(UrlDispatcher): + """UrlDispatcher that uses a dict lookup for resolving.""" + + def __init__(self) -> None: + """Initialize the dispatcher.""" + super().__init__() + self._resource_index: dict[str, AbstractResource] = {} + + def register_resource(self, resource: AbstractResource) -> None: + """Register a resource.""" + super().register_resource(resource) + canonical = resource.canonical + if "{" in canonical: # strip at the first { to allow for variables + canonical = canonical.split("{")[0] + canonical = canonical.rstrip("/") + self._resource_index[canonical] = resource + + async def resolve(self, request: web.Request) -> UrlMappingMatchInfo: + """Resolve a request.""" + url_parts = request.rel_url.raw_parts + resource_index = self._resource_index + # Walk the url parts looking for candidates + for i in range(len(url_parts), 1, -1): + url_part = "/" + "/".join(url_parts[1:i]) + if (resource_candidate := resource_index.get(url_part)) is not None and ( + match_dict := (await resource_candidate.resolve(request))[0] + ) is not None: + return match_dict + # Next try the index view if we don't have a match + if (index_view_candidate := resource_index.get("/")) is not None and ( + match_dict := (await index_view_candidate.resolve(request))[0] + ) is not None: + return match_dict + # Finally, fallback to the linear search + return await super().resolve(request) From cdea33d191037b3c6c1a26ab940e751f6a5dcd48 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 0111/1009] 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 d30198bdc122a3..82426fbce08336 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 74c0b80ad57027..95a0ad68a75572 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 09706afca237d6..7d22f6021c0ad3 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 de7677b28d59ee2e227a69166adfe523a0804d2c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 3 Jul 2023 03:30:53 -0400 Subject: [PATCH 0112/1009] Small zwave_js code cleanup (#95745) --- homeassistant/components/zwave_js/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index b847b76ca17776..8c1dd9b2197fa2 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -317,7 +317,9 @@ def __init__(self, hass: HomeAssistant, driver_events: DriverEvents) -> None: self.discovered_value_ids: dict[str, set[str]] = defaultdict(set) self.driver_events = driver_events self.dev_reg = driver_events.dev_reg - self.registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict(dict) + self.registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict( + lambda: defaultdict(set) + ) self.node_events = NodeEvents(hass, self) @callback @@ -488,9 +490,6 @@ async def async_on_node_ready(self, node: ZwaveNode) -> None: LOGGER.debug("Processing node %s", node) # register (or update) node in device registry device = self.controller_events.register_node_in_dev_reg(node) - # We only want to create the defaultdict once, even on reinterviews - if device.id not in self.controller_events.registered_unique_ids: - self.controller_events.registered_unique_ids[device.id] = defaultdict(set) # Remove any old value ids if this is a reinterview. self.controller_events.discovered_value_ids.pop(device.id, None) From 367acd043314e9c16c345c987cdf2892b631d8fe Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Mon, 3 Jul 2023 05:23:32 -0400 Subject: [PATCH 0113/1009] 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 8fba07198f2f96..4a8a9dec5874c2 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 95a0ad68a75572..81e71a831a2f2e 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 7d22f6021c0ad3..007cb5fdea76cc 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 3bd8955e0e119ab010bfc68ed54097195f300f96 Mon Sep 17 00:00:00 2001 From: hidaris Date: Mon, 3 Jul 2023 18:33:50 +0800 Subject: [PATCH 0114/1009] 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 00000000000000..6da88533edc065 --- /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 28f5b6b7f90168..0b4bacf00ca343 100644 --- a/homeassistant/components/matter/discovery.py +++ b/homeassistant/components/matter/discovery.py @@ -10,6 +10,7 @@ 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 @@ 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 56c51d144d8a91..e1fb4464b83122 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -77,6 +77,7 @@ def _update_from_device(self) -> None: 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 00000000000000..85ac42e5429153 --- /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 00000000000000..ec8453b5c560a1 --- /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 4ee7ea3cba398d2ad27b4530af83a053bb4d938a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jul 2023 14:01:58 +0200 Subject: [PATCH 0115/1009] Use DeviceInfo object for Nobo hub (#95753) --- homeassistant/components/nobo_hub/climate.py | 15 ++++++------- homeassistant/components/nobo_hub/sensor.py | 22 +++++++++----------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py index d1661dce0fa55a..f55dc9344ab65d 100644 --- a/homeassistant/components/nobo_hub/climate.py +++ b/homeassistant/components/nobo_hub/climate.py @@ -18,10 +18,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_IDENTIFIERS, ATTR_NAME, - ATTR_SUGGESTED_AREA, - ATTR_VIA_DEVICE, PRECISION_TENTHS, UnitOfTemperature, ) @@ -95,12 +92,12 @@ def __init__(self, zone_id, hub: nobo, override_type) -> None: self._attr_hvac_mode = HVACMode.AUTO self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.AUTO] self._override_type = override_type - self._attr_device_info: DeviceInfo = { - ATTR_IDENTIFIERS: {(DOMAIN, f"{hub.hub_serial}:{zone_id}")}, - ATTR_NAME: hub.zones[zone_id][ATTR_NAME], - ATTR_VIA_DEVICE: (DOMAIN, hub.hub_info[ATTR_SERIAL]), - ATTR_SUGGESTED_AREA: hub.zones[zone_id][ATTR_NAME], - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{hub.hub_serial}:{zone_id}")}, + name=hub.zones[zone_id][ATTR_NAME], + via_device=(DOMAIN, hub.hub_info[ATTR_SERIAL]), + suggested_area=hub.zones[zone_id][ATTR_NAME], + ) self._read_state() async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/nobo_hub/sensor.py b/homeassistant/components/nobo_hub/sensor.py index 3bb1fa373a5675..c5536bad6ea2e7 100644 --- a/homeassistant/components/nobo_hub/sensor.py +++ b/homeassistant/components/nobo_hub/sensor.py @@ -10,12 +10,8 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, - ATTR_SUGGESTED_AREA, - ATTR_VIA_DEVICE, UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback @@ -60,16 +56,18 @@ def __init__(self, serial: str, hub: nobo) -> None: self._attr_unique_id = component[ATTR_SERIAL] self._attr_name = "Temperature" self._attr_has_entity_name = True - self._attr_device_info: DeviceInfo = { - ATTR_IDENTIFIERS: {(DOMAIN, component[ATTR_SERIAL])}, - ATTR_NAME: component[ATTR_NAME], - ATTR_MANUFACTURER: NOBO_MANUFACTURER, - ATTR_MODEL: component[ATTR_MODEL].name, - ATTR_VIA_DEVICE: (DOMAIN, hub.hub_info[ATTR_SERIAL]), - } zone_id = component[ATTR_ZONE_ID] + suggested_area = None if zone_id != "-1": - self._attr_device_info[ATTR_SUGGESTED_AREA] = hub.zones[zone_id][ATTR_NAME] + suggested_area = hub.zones[zone_id][ATTR_NAME] + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, component[ATTR_SERIAL])}, + name=component[ATTR_NAME], + manufacturer=NOBO_MANUFACTURER, + model=component[ATTR_MODEL].name, + via_device=(DOMAIN, hub.hub_info[ATTR_SERIAL]), + suggested_area=suggested_area, + ) self._read_state() async def async_added_to_hass(self) -> None: From 78cc11ebc47b767de4026d501a990671d80d9a08 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jul 2023 14:02:49 +0200 Subject: [PATCH 0116/1009] Use device class naming for Nuki (#95756) --- homeassistant/components/nuki/sensor.py | 1 - homeassistant/components/nuki/strings.json | 5 ----- 2 files changed, 6 deletions(-) diff --git a/homeassistant/components/nuki/sensor.py b/homeassistant/components/nuki/sensor.py index c4578c7d14d1df..06cfa065c54bb8 100644 --- a/homeassistant/components/nuki/sensor.py +++ b/homeassistant/components/nuki/sensor.py @@ -29,7 +29,6 @@ class NukiBatterySensor(NukiEntity[NukiDevice], SensorEntity): """Representation of a Nuki Lock Battery sensor.""" _attr_has_entity_name = True - _attr_translation_key = "battery" _attr_native_unit_of_measurement = PERCENTAGE _attr_device_class = SensorDeviceClass.BATTERY _attr_entity_category = EntityCategory.DIAGNOSTIC diff --git a/homeassistant/components/nuki/strings.json b/homeassistant/components/nuki/strings.json index f139124e961420..4629f6a2a3b534 100644 --- a/homeassistant/components/nuki/strings.json +++ b/homeassistant/components/nuki/strings.json @@ -44,11 +44,6 @@ } } } - }, - "sensor": { - "battery": { - "name": "[%key:component::sensor::entity_component::battery::name%]" - } } } } From 8062a0a3bdcf62bcaab499e100d2c52a7e4699b8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jul 2023 14:03:24 +0200 Subject: [PATCH 0117/1009] Use device info object for Nuki (#95757) --- homeassistant/components/nuki/__init__.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index b0bfe18614e670..d237303e7c98a7 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -30,6 +30,7 @@ entity_registry as er, issue_registry as ir, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -368,13 +369,13 @@ def __init__(self, coordinator: NukiCoordinator, nuki_device: _NukiDeviceT) -> N self._nuki_device = nuki_device @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info for Nuki entities.""" - return { - "identifiers": {(DOMAIN, parse_id(self._nuki_device.nuki_id))}, - "name": self._nuki_device.name, - "manufacturer": "Nuki Home Solutions GmbH", - "model": self._nuki_device.device_model_str.capitalize(), - "sw_version": self._nuki_device.firmware_version, - "via_device": (DOMAIN, self.coordinator.bridge_id), - } + return DeviceInfo( + identifiers={(DOMAIN, parse_id(self._nuki_device.nuki_id))}, + name=self._nuki_device.name, + manufacturer="Nuki Home Solutions GmbH", + model=self._nuki_device.device_model_str.capitalize(), + sw_version=self._nuki_device.firmware_version, + via_device=(DOMAIN, self.coordinator.bridge_id), + ) From 935242e64e2e201efe65540b55117d7e805f780f Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 3 Jul 2023 14:04:17 +0200 Subject: [PATCH 0118/1009] Use device info object for Discovergy (#95764) --- homeassistant/components/discovergy/sensor.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/discovergy/sensor.py b/homeassistant/components/discovergy/sensor.py index 35955a6b1890a1..3f4069752f2ed0 100644 --- a/homeassistant/components/discovergy/sensor.py +++ b/homeassistant/components/discovergy/sensor.py @@ -11,16 +11,13 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, UnitOfElectricPotential, UnitOfEnergy, UnitOfPower, UnitOfVolume, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -198,12 +195,12 @@ def __init__( self.entity_description = description self._attr_unique_id = f"{meter.full_serial_number}-{data_key}" - self._attr_device_info = { - ATTR_IDENTIFIERS: {(DOMAIN, meter.get_meter_id())}, - ATTR_NAME: f"{meter.measurement_type.capitalize()} {meter.location.street} {meter.location.street_number}", - ATTR_MODEL: f"{meter.type} {meter.full_serial_number}", - ATTR_MANUFACTURER: MANUFACTURER, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, meter.get_meter_id())}, + name=f"{meter.measurement_type.capitalize()} {meter.location.street} {meter.location.street_number}", + model=f"{meter.type} {meter.full_serial_number}", + manufacturer=MANUFACTURER, + ) @property def native_value(self) -> StateType: From 266522267a8c6b12c2bb940b4fb5de55e4ab0cc0 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 3 Jul 2023 14:19:05 +0200 Subject: [PATCH 0119/1009] 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 f3008d9f1c1987..1ef87e8493354e 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 81e71a831a2f2e..a1d6eade29935c 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 007cb5fdea76cc..9f12920bbc1de1 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 bd6f70c236314ea2f61b9ffe11682d302071ba5b Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 3 Jul 2023 05:19:40 -0700 Subject: [PATCH 0120/1009] Bump opower to 0.0.12 (#95748) --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 969583f050a0a5..3b48e96a35194c 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["recorder"], "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", - "requirements": ["opower==0.0.11"] + "requirements": ["opower==0.0.12"] } diff --git a/requirements_all.txt b/requirements_all.txt index a1d6eade29935c..c87482c2fef635 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1363,7 +1363,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.11 +opower==0.0.12 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f12920bbc1de1..ee2ae9f67c5244 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1029,7 +1029,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.11 +opower==0.0.12 # homeassistant.components.oralb oralb-ble==0.17.6 From 4e7d8b579a1d19986e64024a3383fe6f97c6c271 Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 3 Jul 2023 05:53:44 -0700 Subject: [PATCH 0121/1009] Address Opower review comments (#95763) * Address comments * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Update sensor.py --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/opower/sensor.py | 20 ++++++++++++++++++-- homeassistant/components/opower/strings.json | 2 +- tests/components/opower/conftest.py | 2 -- tests/components/opower/test_config_flow.py | 2 ++ 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/opower/sensor.py b/homeassistant/components/opower/sensor.py index e28dcbd0661a75..ef8d8eb884fd73 100644 --- a/homeassistant/components/opower/sensor.py +++ b/homeassistant/components/opower/sensor.py @@ -15,6 +15,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfEnergy, UnitOfVolume from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -71,6 +72,8 @@ class OpowerEntityDescription(SensorEntityDescription, OpowerEntityDescriptionMi key="elec_cost_to_date", name="Current bill electric cost to date", device_class=SensorDeviceClass.MONETARY, + native_unit_of_measurement="USD", + suggested_unit_of_measurement="USD", state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.cost_to_date, @@ -79,6 +82,8 @@ class OpowerEntityDescription(SensorEntityDescription, OpowerEntityDescriptionMi key="elec_forecasted_cost", name="Current bill electric forecasted cost", device_class=SensorDeviceClass.MONETARY, + native_unit_of_measurement="USD", + suggested_unit_of_measurement="USD", state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.forecasted_cost, @@ -87,6 +92,8 @@ class OpowerEntityDescription(SensorEntityDescription, OpowerEntityDescriptionMi key="elec_typical_cost", name="Typical monthly electric cost", device_class=SensorDeviceClass.MONETARY, + native_unit_of_measurement="USD", + suggested_unit_of_measurement="USD", state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.typical_cost, @@ -127,6 +134,8 @@ class OpowerEntityDescription(SensorEntityDescription, OpowerEntityDescriptionMi key="gas_cost_to_date", name="Current bill gas cost to date", device_class=SensorDeviceClass.MONETARY, + native_unit_of_measurement="USD", + suggested_unit_of_measurement="USD", state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.cost_to_date, @@ -135,6 +144,8 @@ class OpowerEntityDescription(SensorEntityDescription, OpowerEntityDescriptionMi key="gas_forecasted_cost", name="Current bill gas forecasted cost", device_class=SensorDeviceClass.MONETARY, + native_unit_of_measurement="USD", + suggested_unit_of_measurement="USD", state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.forecasted_cost, @@ -143,6 +154,8 @@ class OpowerEntityDescription(SensorEntityDescription, OpowerEntityDescriptionMi key="gas_typical_cost", name="Typical monthly gas cost", device_class=SensorDeviceClass.MONETARY, + native_unit_of_measurement="USD", + suggested_unit_of_measurement="USD", state_class=SensorStateClass.TOTAL, suggested_display_precision=0, value_fn=lambda data: data.typical_cost, @@ -165,6 +178,7 @@ async def async_setup_entry( name=f"{forecast.account.meter_type.name} account {forecast.account.utility_account_id}", manufacturer="Opower", model=coordinator.api.utility.name(), + entry_type=DeviceEntryType.SERVICE, ) sensors: tuple[OpowerEntityDescription, ...] = () if ( @@ -191,9 +205,11 @@ async def async_setup_entry( async_add_entities(entities) -class OpowerSensor(SensorEntity, CoordinatorEntity[OpowerCoordinator]): +class OpowerSensor(CoordinatorEntity[OpowerCoordinator], SensorEntity): """Representation of an Opower sensor.""" + entity_description: OpowerEntityDescription + def __init__( self, coordinator: OpowerCoordinator, @@ -204,7 +220,7 @@ def __init__( ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self.entity_description: OpowerEntityDescription = description + self.entity_description = description self._attr_unique_id = f"{device_id}_{description.key}" self._attr_device_info = device self.utility_account_id = utility_account_id diff --git a/homeassistant/components/opower/strings.json b/homeassistant/components/opower/strings.json index 79d8bf80feeb37..037983eb6ffd49 100644 --- a/homeassistant/components/opower/strings.json +++ b/homeassistant/components/opower/strings.json @@ -21,7 +21,7 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } diff --git a/tests/components/opower/conftest.py b/tests/components/opower/conftest.py index 17c6896593b030..0ee910f84f4be4 100644 --- a/tests/components/opower/conftest.py +++ b/tests/components/opower/conftest.py @@ -2,7 +2,6 @@ import pytest from homeassistant.components.opower.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -19,7 +18,6 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: "username": "test-username", "password": "test-password", }, - state=ConfigEntryState.LOADED, ) config_entry.add_to_hass(hass) return config_entry diff --git a/tests/components/opower/test_config_flow.py b/tests/components/opower/test_config_flow.py index 7f6a847f52e3b4..6a45a0dcc561a3 100644 --- a/tests/components/opower/test_config_flow.py +++ b/tests/components/opower/test_config_flow.py @@ -8,6 +8,7 @@ from homeassistant import config_entries from homeassistant.components.opower.const import DOMAIN from homeassistant.components.recorder import Recorder +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -172,6 +173,7 @@ async def test_form_valid_reauth( mock_config_entry: MockConfigEntry, ) -> None: """Test that we can handle a valid reauth.""" + mock_config_entry.state = ConfigEntryState.LOADED mock_config_entry.async_start_reauth(hass) await hass.async_block_till_done() From e5eb5dace574f480cd7b1b4c1dd1eb2f4d67f321 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 3 Jul 2023 16:41:51 +0200 Subject: [PATCH 0122/1009] 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 b9d0e63ecbac36..d2c196dbfdd395 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 3fa1e1215213712caa14d9c63dc78fe6076883ca Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 Jul 2023 17:38:03 +0200 Subject: [PATCH 0123/1009] 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 e4ecbd9d866d9f..f4d1e51b171eed 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 430a1bcb3d56ab73d3423f38697f4f5715cee036 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 3 Jul 2023 17:38:54 +0200 Subject: [PATCH 0124/1009] 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 9615404a9a6ce5..284b8d6b00aee2 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 62e9bdf6cf80b0..6c3dcd81295dfe 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 0a165bb35acbacb9eb5a57e67dfad68b02ad6e1f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 3 Jul 2023 17:43:52 +0200 Subject: [PATCH 0125/1009] Improve opower generic typing (#95758) --- homeassistant/components/opower/coordinator.py | 2 +- homeassistant/components/opower/sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index 4d40bb3356bfee..c331f45bc4977c 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -32,7 +32,7 @@ _LOGGER = logging.getLogger(__name__) -class OpowerCoordinator(DataUpdateCoordinator): +class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): """Handle fetching Opower data, updating sensors and inserting statistics.""" def __init__( diff --git a/homeassistant/components/opower/sensor.py b/homeassistant/components/opower/sensor.py index ef8d8eb884fd73..36f88a36e8aba1 100644 --- a/homeassistant/components/opower/sensor.py +++ b/homeassistant/components/opower/sensor.py @@ -170,7 +170,7 @@ async def async_setup_entry( coordinator: OpowerCoordinator = hass.data[DOMAIN][entry.entry_id] entities: list[OpowerSensor] = [] - forecasts: list[Forecast] = coordinator.data.values() + forecasts = coordinator.data.values() for forecast in forecasts: device_id = f"{coordinator.api.utility.subdomain()}_{forecast.account.utility_account_id}" device = DeviceInfo( From f0eb0849088aa843d3ab8b479f2a734eeb3d6414 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jul 2023 18:31:07 +0200 Subject: [PATCH 0126/1009] Add entity translations to Notion (#95755) * Add entity translations to Notion * Use device class translations * Use device class translations --- .../components/notion/binary_sensor.py | 13 ++++-------- homeassistant/components/notion/sensor.py | 3 +-- homeassistant/components/notion/strings.json | 21 +++++++++++++++++++ 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index f70af18c3e1518..ff58d566a34776 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -52,7 +52,6 @@ class NotionBinarySensorDescription( BINARY_SENSOR_DESCRIPTIONS = ( NotionBinarySensorDescription( key=SENSOR_BATTERY, - name="Low battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, listener_kind=ListenerKind.BATTERY, @@ -60,28 +59,24 @@ class NotionBinarySensorDescription( ), NotionBinarySensorDescription( key=SENSOR_DOOR, - name="Door", device_class=BinarySensorDeviceClass.DOOR, listener_kind=ListenerKind.DOOR, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_GARAGE_DOOR, - name="Garage door", device_class=BinarySensorDeviceClass.GARAGE_DOOR, listener_kind=ListenerKind.GARAGE_DOOR, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_LEAK, - name="Leak detector", device_class=BinarySensorDeviceClass.MOISTURE, listener_kind=ListenerKind.LEAK_STATUS, on_state="leak", ), NotionBinarySensorDescription( key=SENSOR_MISSING, - name="Missing", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, listener_kind=ListenerKind.CONNECTED, @@ -89,28 +84,28 @@ class NotionBinarySensorDescription( ), NotionBinarySensorDescription( key=SENSOR_SAFE, - name="Safe", + translation_key="safe", device_class=BinarySensorDeviceClass.DOOR, listener_kind=ListenerKind.SAFE, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_SLIDING, - name="Sliding door/window", + translation_key="sliding_door_window", device_class=BinarySensorDeviceClass.DOOR, listener_kind=ListenerKind.SLIDING_DOOR_OR_WINDOW, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_SMOKE_CO, - name="Smoke/Carbon monoxide detector", + translation_key="smoke_carbon_monoxide_detector", device_class=BinarySensorDeviceClass.SMOKE, listener_kind=ListenerKind.SMOKE, on_state="alarm", ), NotionBinarySensorDescription( key=SENSOR_WINDOW_HINGED, - name="Hinged window", + translation_key="hinged_window", listener_kind=ListenerKind.HINGED_WINDOW, on_state="open", ), diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index 6f011523a2ae63..4777cc94fbfd24 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -27,13 +27,12 @@ class NotionSensorDescription(SensorEntityDescription, NotionEntityDescriptionMi SENSOR_DESCRIPTIONS = ( NotionSensorDescription( key=SENSOR_MOLD, - name="Mold risk", + translation_key="mold_risk", icon="mdi:liquid-spot", listener_kind=ListenerKind.MOLD, ), NotionSensorDescription( key=SENSOR_TEMPERATURE, - name="Temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/notion/strings.json b/homeassistant/components/notion/strings.json index 49721568ff23f5..24a06d7ee714e5 100644 --- a/homeassistant/components/notion/strings.json +++ b/homeassistant/components/notion/strings.json @@ -24,5 +24,26 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "entity": { + "binary_sensor": { + "safe": { + "name": "Safe" + }, + "sliding_door_window": { + "name": "Sliding door/window" + }, + "smoke_carbon_monoxide_detector": { + "name": "Smoke/Carbon monoxide detector" + }, + "hinged_window": { + "name": "Hinged window" + } + }, + "sensor": { + "mold_risk": { + "name": "Mold risk" + } + } } } From 27e4bca1b39e1114f90f984a116f41ce5034d6ad Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jul 2023 18:36:37 +0200 Subject: [PATCH 0127/1009] 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 2c06621d079ac8..cfacadce528fb0 100644 --- a/homeassistant/components/growatt_server/sensor_types/inverter.py +++ b/homeassistant/components/growatt_server/sensor_types/inverter.py @@ -161,7 +161,7 @@ ), 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 5712d12c42e417c6265fa66d45941a0d0751e06c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 3 Jul 2023 18:37:18 +0200 Subject: [PATCH 0128/1009] Remove unsupported services from tuya vacuum (#95790) --- homeassistant/components/tuya/vacuum.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index a2961a55d7872f..62ff1d63ca0968 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -153,14 +153,6 @@ def state(self) -> str | None: return None return TUYA_STATUS_TO_HA.get(status) - def turn_on(self, **kwargs: Any) -> None: - """Turn the device on.""" - self._send_command([{"code": DPCode.POWER, "value": True}]) - - def turn_off(self, **kwargs: Any) -> None: - """Turn the device off.""" - self._send_command([{"code": DPCode.POWER, "value": False}]) - def start(self, **kwargs: Any) -> None: """Start the device.""" self._send_command([{"code": DPCode.POWER_GO, "value": True}]) From 5f9da06e49ae819abe840d8b1ed8702ff80b1834 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 3 Jul 2023 18:53:21 +0200 Subject: [PATCH 0129/1009] Fix flaky websocket_api test (#95786) --- tests/components/websocket_api/test_commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 7e46dc0d0bdc45..00d27035464326 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1739,6 +1739,7 @@ async def test_execute_script_complex_response( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test testing a condition.""" + await async_setup_component(hass, "homeassistant", {}) await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) await hass.async_block_till_done() ws_client = await hass_ws_client(hass) From aed0c39bc8f823d56f62927ff16d7197793146d2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 3 Jul 2023 20:17:24 +0200 Subject: [PATCH 0130/1009] 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 4a1edd4096e302..f0875bc15d74a8 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 47bd964c00265f..3c861d8a389c18 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 c87482c2fef635..df524f01ae2e37 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 ee2ae9f67c5244..0aa356165bf5e1 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 73f90035bb9c0bc1d7b9b2427ffa529de46458d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Jul 2023 13:19:41 -0500 Subject: [PATCH 0131/1009] Bump aioesphomeapi to 15.1.2 (#95792) changelog: https://github.com/esphome/aioesphomeapi/compare/v15.1.1...v15.1.2 intentionally not tagged for beta to give it more time in dev since we are near the end of the beta cycle --- 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 8f5e6b95c39097..8fc16926e563c9 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.1.1", + "aioesphomeapi==15.1.2", "bluetooth-data-tools==1.3.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index df524f01ae2e37..8a19ee9dd76fa4 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.1.1 +aioesphomeapi==15.1.2 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0aa356165bf5e1..e5feb50105c456 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.1.1 +aioesphomeapi==15.1.2 # homeassistant.components.flo aioflo==2021.11.0 From 3f9d5a0192c04a8c0d840a6da92e6285fa691c78 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Jul 2023 13:20:23 -0500 Subject: [PATCH 0132/1009] Use the converter factory in sensor.recorder._normalize_states (#95785) We have a factory to create converters now which avoids the overhead of calling convert to create the converter every time --- homeassistant/components/sensor/recorder.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index f9fdc2525371c8..2b75c1114cecc4 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections import defaultdict -from collections.abc import Iterable, MutableMapping +from collections.abc import Callable, Iterable, MutableMapping import datetime import itertools import logging @@ -224,6 +224,8 @@ def _normalize_states( converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER[statistics_unit] valid_fstates: list[tuple[float, State]] = [] + convert: Callable[[float], float] + last_unit: str | None | object = object() for fstate, state in fstates: state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -247,15 +249,13 @@ def _normalize_states( LINK_DEV_STATISTICS, ) continue + if state_unit != last_unit: + # The unit of measurement has changed since the last state change + # recreate the converter factory + convert = converter.converter_factory(state_unit, statistics_unit) + last_unit = state_unit - valid_fstates.append( - ( - converter.convert( - fstate, from_unit=state_unit, to_unit=statistics_unit - ), - state, - ) - ) + valid_fstates.append((convert(fstate), state)) return statistics_unit, valid_fstates From 78880f0c9ddf36531b7f091a9f5804de011de395 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 3 Jul 2023 20:21:01 +0200 Subject: [PATCH 0133/1009] 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 c733a96ca9d275..ea00de33390aaf 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -704,10 +704,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 00d27035464326..232362ce96f280 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.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_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. @@ -1775,6 +1803,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 4d3662d4da44579ba0ba860786d1ea06056615e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Jul 2023 13:21:59 -0500 Subject: [PATCH 0134/1009] Tune httpx keep alives for polling integrations (#95782) * Tune keep alives for polling integrations aiohttp closes the connection after 15s by default, and httpx closes the connection after 5s by default. We have a lot of integrations that poll every 10-60s which create and tear down connections over and over. Set the keep alive time to 65s to maximize connection reuse and avoid tls negotiation overhead * Apply suggestions from code review * adjust --- homeassistant/helpers/httpx_client.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/helpers/httpx_client.py b/homeassistant/helpers/httpx_client.py index beb084d8c1c63f..93b199b1db56cf 100644 --- a/homeassistant/helpers/httpx_client.py +++ b/homeassistant/helpers/httpx_client.py @@ -19,8 +19,13 @@ from .frame import warn_use +# We have a lot of integrations that poll every 10-30 seconds +# and we want to keep the connection open for a while so we +# don't have to reconnect every time so we use 15s to match aiohttp. +KEEP_ALIVE_TIMEOUT = 15 DATA_ASYNC_CLIENT = "httpx_async_client" DATA_ASYNC_CLIENT_NOVERIFY = "httpx_async_client_noverify" +DEFAULT_LIMITS = limits = httpx.Limits(keepalive_expiry=KEEP_ALIVE_TIMEOUT) SERVER_SOFTWARE = "{0}/{1} httpx/{2} Python/{3[0]}.{3[1]}".format( APPLICATION_NAME, __version__, httpx.__version__, sys.version_info ) @@ -78,6 +83,7 @@ def create_async_httpx_client( client = HassHttpXAsyncClient( verify=ssl_context, headers={USER_AGENT: SERVER_SOFTWARE}, + limits=DEFAULT_LIMITS, **kwargs, ) From 0f725a24bdd504382baa125e94e72cc862e760c2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Jul 2023 14:56:21 -0400 Subject: [PATCH 0135/1009] Remove the weak ref for tracking update listeners (#95798) --- homeassistant/config_entries.py | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a52b869b8300dd..9e27f6efb3e995 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -10,9 +10,8 @@ import functools import logging from random import randint -from types import MappingProxyType, MethodType +from types import MappingProxyType from typing import TYPE_CHECKING, Any, TypeVar, cast -import weakref from typing_extensions import Self @@ -303,9 +302,7 @@ def __init__( self.supports_remove_device: bool | None = None # Listeners to call on update - self.update_listeners: list[ - weakref.ReferenceType[UpdateListenerType] | weakref.WeakMethod - ] = [] + self.update_listeners: list[UpdateListenerType] = [] # Reason why config entry is in a failed state self.reason: str | None = None @@ -653,16 +650,8 @@ def add_update_listener(self, listener: UpdateListenerType) -> CALLBACK_TYPE: Returns function to unlisten. """ - weak_listener: Any - # weakref.ref is not applicable to a bound method, e.g., - # method of a class instance, as reference will die immediately. - if hasattr(listener, "__self__"): - weak_listener = weakref.WeakMethod(cast(MethodType, listener)) - else: - weak_listener = weakref.ref(listener) - self.update_listeners.append(weak_listener) - - return lambda: self.update_listeners.remove(weak_listener) + self.update_listeners.append(listener) + return lambda: self.update_listeners.remove(listener) def as_dict(self) -> dict[str, Any]: """Return dictionary version of this entry.""" @@ -1348,12 +1337,11 @@ def async_update_entry( if not changed: return False - for listener_ref in entry.update_listeners: - if (listener := listener_ref()) is not None: - self.hass.async_create_task( - listener(self.hass, entry), - f"config entry update listener {entry.title} {entry.domain} {entry.domain}", - ) + for listener in entry.update_listeners: + self.hass.async_create_task( + listener(self.hass, entry), + f"config entry update listener {entry.title} {entry.domain} {entry.domain}", + ) self._async_schedule_save() self._async_dispatch(ConfigEntryChange.UPDATED, entry) From 2f73be0e509ecf24625160a317df53e8a1c77665 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 3 Jul 2023 12:05:02 -0700 Subject: [PATCH 0136/1009] 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 86f61f0ed872e5..b22ac98b0dce82 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 4d6b5adfde701b..5d506d67c6f991 100644 --- a/tests/components/calendar/conftest.py +++ b/tests/components/calendar/conftest.py @@ -9,3 +9,11 @@ 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 9fdc76abe03f49..463e075d16973c 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -4,8 +4,9 @@ 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 05c7d95d8ad976..45dd9d6afe146f 100644 --- a/tests/components/calendar/test_trigger.py +++ b/tests/components/calendar/test_trigger.py @@ -121,14 +121,6 @@ async def fire_until(self, end: datetime.datetime) -> None: 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 4581c3664879a848da111a2caa7dbeebebd1da71 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 3 Jul 2023 21:22:22 +0200 Subject: [PATCH 0137/1009] 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 b22ac98b0dce82..3286dd152e849d 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -264,8 +264,8 @@ def _validate_rrule(value: Any) -> str: 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 73b45a556404f7..92dbf8d47b8060 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -67,6 +67,14 @@ async def async_get_events( 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 04be7677a97f6ae2b24159ed913c9631b4377c8e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 3 Jul 2023 23:00:12 +0200 Subject: [PATCH 0138/1009] Add entity translations for Open UV (#95810) --- .../components/openuv/binary_sensor.py | 2 +- homeassistant/components/openuv/sensor.py | 20 +++++----- homeassistant/components/openuv/strings.json | 39 +++++++++++++++++++ 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 1e69af66eec530..e9f9ee99ff6bc1 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -19,7 +19,7 @@ BINARY_SENSOR_DESCRIPTION_PROTECTION_WINDOW = BinarySensorEntityDescription( key=TYPE_PROTECTION_WINDOW, - name="Protection window", + translation_key="protection_window", icon="mdi:sunglasses", ) diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index 44bde8341a0bc8..90eefac594a7f9 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -49,67 +49,67 @@ SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=TYPE_CURRENT_OZONE_LEVEL, - name="Current ozone level", + translation_key="current_ozone_level", native_unit_of_measurement="du", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_CURRENT_UV_INDEX, - name="Current UV index", + translation_key="current_uv_index", icon="mdi:weather-sunny", native_unit_of_measurement=UV_INDEX, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_CURRENT_UV_LEVEL, - name="Current UV level", + translation_key="current_uv_level", icon="mdi:weather-sunny", ), SensorEntityDescription( key=TYPE_MAX_UV_INDEX, - name="Max UV index", + translation_key="max_uv_index", icon="mdi:weather-sunny", native_unit_of_measurement=UV_INDEX, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_1, - name="Skin type 1 safe exposure time", + translation_key="skin_type_1_safe_exposure_time", icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_2, - name="Skin type 2 safe exposure time", + translation_key="skin_type_2_safe_exposure_time", icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_3, - name="Skin type 3 safe exposure time", + translation_key="skin_type_3_safe_exposure_time", icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_4, - name="Skin type 4 safe exposure time", + translation_key="skin_type_4_safe_exposure_time", icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_5, - name="Skin type 5 safe exposure time", + translation_key="skin_type_5_safe_exposure_time", icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_6, - name="Skin type 6 safe exposure time", + translation_key="skin_type_6_safe_exposure_time", icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/openuv/strings.json b/homeassistant/components/openuv/strings.json index 9542cb8b1a7142..4aa29d11fcf26c 100644 --- a/homeassistant/components/openuv/strings.json +++ b/homeassistant/components/openuv/strings.json @@ -46,5 +46,44 @@ "title": "The {deprecated_service} service is being removed", "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with `{alternate_targets}` as the target." } + }, + "entity": { + "binary_sensor": { + "protection_window": { + "name": "Protection window" + } + }, + "sensor": { + "current_ozone_level": { + "name": "Current ozone level" + }, + "current_uv_index": { + "name": "Current UV index" + }, + "current_uv_level": { + "name": "Current UV level" + }, + "max_uv_index": { + "name": "Max UV index" + }, + "skin_type_1_safe_exposure_time": { + "name": "Skin type 1 safe exposure time" + }, + "skin_type_2_safe_exposure_time": { + "name": "Skin type 2 safe exposure time" + }, + "skin_type_3_safe_exposure_time": { + "name": "Skin type 3 safe exposure time" + }, + "skin_type_4_safe_exposure_time": { + "name": "Skin type 4 safe exposure time" + }, + "skin_type_5_safe_exposure_time": { + "name": "Skin type 5 safe exposure time" + }, + "skin_type_6_safe_exposure_time": { + "name": "Skin type 6 safe exposure time" + } + } } } From fe1430d04b34d2517c97de643e08997fcf251d5f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 3 Jul 2023 23:55:23 +0200 Subject: [PATCH 0139/1009] 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 f48191e471a18c..9bfb01e5a88336 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 8a19ee9dd76fa4..da1672698c7d6d 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 e5feb50105c456..d8ca0291a8467d 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 234ebdcb840e96203ad1107c0dfcfc0d067462a3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 4 Jul 2023 08:39:24 +0200 Subject: [PATCH 0140/1009] Add entity translations for P1 Monitor (#95811) --- homeassistant/components/p1_monitor/sensor.py | 75 +++++++--------- .../components/p1_monitor/strings.json | 88 +++++++++++++++++++ tests/components/p1_monitor/test_sensor.py | 31 ++++--- 3 files changed, 142 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/p1_monitor/sensor.py b/homeassistant/components/p1_monitor/sensor.py index f192dd44300866..21a878fa18743a 100644 --- a/homeassistant/components/p1_monitor/sensor.py +++ b/homeassistant/components/p1_monitor/sensor.py @@ -4,7 +4,6 @@ from typing import Literal from homeassistant.components.sensor import ( - DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -39,7 +38,7 @@ SENSORS_SMARTMETER: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="gas_consumption", - name="Gas Consumption", + translation_key="gas_consumption", entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, device_class=SensorDeviceClass.GAS, @@ -47,49 +46,49 @@ ), SensorEntityDescription( key="power_consumption", - name="Power Consumption", + translation_key="power_consumption", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="energy_consumption_high", - name="Energy Consumption - High Tariff", + translation_key="energy_consumption_high", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="energy_consumption_low", - name="Energy Consumption - Low Tariff", + translation_key="energy_consumption_low", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="power_production", - name="Power Production", + translation_key="power_production", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="energy_production_high", - name="Energy Production - High Tariff", + translation_key="energy_production_high", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="energy_production_low", - name="Energy Production - Low Tariff", + translation_key="energy_production_low", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="energy_tariff_period", - name="Energy Tariff Period", + translation_key="energy_tariff_period", icon="mdi:calendar-clock", ), ) @@ -97,84 +96,84 @@ SENSORS_PHASES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="voltage_phase_l1", - name="Voltage Phase L1", + translation_key="voltage_phase_l1", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="voltage_phase_l2", - name="Voltage Phase L2", + translation_key="voltage_phase_l2", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="voltage_phase_l3", - name="Voltage Phase L3", + translation_key="voltage_phase_l3", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="current_phase_l1", - name="Current Phase L1", + translation_key="current_phase_l1", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="current_phase_l2", - name="Current Phase L2", + translation_key="current_phase_l2", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="current_phase_l3", - name="Current Phase L3", + translation_key="current_phase_l3", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="power_consumed_phase_l1", - name="Power Consumed Phase L1", + translation_key="power_consumed_phase_l1", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="power_consumed_phase_l2", - name="Power Consumed Phase L2", + translation_key="power_consumed_phase_l2", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="power_consumed_phase_l3", - name="Power Consumed Phase L3", + translation_key="power_consumed_phase_l3", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="power_produced_phase_l1", - name="Power Produced Phase L1", + translation_key="power_produced_phase_l1", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="power_produced_phase_l2", - name="Power Produced Phase L2", + translation_key="power_produced_phase_l2", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="power_produced_phase_l3", - name="Power Produced Phase L3", + translation_key="power_produced_phase_l3", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -184,32 +183,32 @@ SENSORS_SETTINGS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="gas_consumption_price", - name="Gas Consumption Price", + translation_key="gas_consumption_price", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfVolume.CUBIC_METERS}", ), SensorEntityDescription( key="energy_consumption_price_low", - name="Energy Consumption Price - Low", + translation_key="energy_consumption_price_low", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", ), SensorEntityDescription( key="energy_consumption_price_high", - name="Energy Consumption Price - High", + translation_key="energy_consumption_price_high", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", ), SensorEntityDescription( key="energy_production_price_low", - name="Energy Production Price - Low", + translation_key="energy_production_price_low", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", ), SensorEntityDescription( key="energy_production_price_high", - name="Energy Production Price - High", + translation_key="energy_production_price_high", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", ), @@ -218,21 +217,21 @@ SENSORS_WATERMETER: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="consumption_day", - name="Consumption Day", + translation_key="consumption_day", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfVolume.LITERS, device_class=SensorDeviceClass.WATER, ), SensorEntityDescription( key="consumption_total", - name="Consumption Total", + translation_key="consumption_total", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, device_class=SensorDeviceClass.WATER, ), SensorEntityDescription( key="pulse_count", - name="Pulse Count", + translation_key="pulse_count", ), ) @@ -248,7 +247,6 @@ async def async_setup_entry( coordinator=coordinator, description=description, name="SmartMeter", - service_key="smartmeter", service=SERVICE_SMARTMETER, ) for description in SENSORS_SMARTMETER @@ -258,7 +256,6 @@ async def async_setup_entry( coordinator=coordinator, description=description, name="Phases", - service_key="phases", service=SERVICE_PHASES, ) for description in SENSORS_PHASES @@ -268,7 +265,6 @@ async def async_setup_entry( coordinator=coordinator, description=description, name="Settings", - service_key="settings", service=SERVICE_SETTINGS, ) for description in SENSORS_SETTINGS @@ -279,7 +275,6 @@ async def async_setup_entry( coordinator=coordinator, description=description, name="WaterMeter", - service_key="watermeter", service=SERVICE_WATERMETER, ) for description in SENSORS_WATERMETER @@ -292,30 +287,28 @@ class P1MonitorSensorEntity( ): """Defines an P1 Monitor sensor.""" + _attr_has_entity_name = True + def __init__( self, *, coordinator: P1MonitorDataUpdateCoordinator, description: SensorEntityDescription, - service_key: Literal["smartmeter", "watermeter", "phases", "settings"], name: str, - service: str, + service: Literal["smartmeter", "watermeter", "phases", "settings"], ) -> None: """Initialize P1 Monitor sensor.""" super().__init__(coordinator=coordinator) - self._service_key = service_key + self._service_key = service - self.entity_id = f"{SENSOR_DOMAIN}.{service}_{description.key}" self.entity_description = description self._attr_unique_id = ( - f"{coordinator.config_entry.entry_id}_{service_key}_{description.key}" + f"{coordinator.config_entry.entry_id}_{service}_{description.key}" ) self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, - identifiers={ - (DOMAIN, f"{coordinator.config_entry.entry_id}_{service_key}") - }, + identifiers={(DOMAIN, f"{coordinator.config_entry.entry_id}_{service}")}, configuration_url=f"http://{coordinator.config_entry.data[CONF_HOST]}", manufacturer="P1 Monitor", name=name, diff --git a/homeassistant/components/p1_monitor/strings.json b/homeassistant/components/p1_monitor/strings.json index 0c745554e9dbc8..781ca109235196 100644 --- a/homeassistant/components/p1_monitor/strings.json +++ b/homeassistant/components/p1_monitor/strings.json @@ -14,5 +14,93 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } + }, + "entity": { + "sensor": { + "gas_consumption": { + "name": "Gas consumption" + }, + "power_consumption": { + "name": "Power consumption" + }, + "energy_consumption_high": { + "name": "Energy consumption - High tariff" + }, + "energy_consumption_low": { + "name": "Energy consumption - Low tariff" + }, + "power_production": { + "name": "Power production" + }, + "energy_production_high": { + "name": "Energy production - High tariff" + }, + "energy_production_low": { + "name": "Energy production - Low tariff" + }, + "energy_tariff_period": { + "name": "Energy tariff period" + }, + "voltage_phase_l1": { + "name": "Voltage phase L1" + }, + "voltage_phase_l2": { + "name": "Voltage phase L2" + }, + "voltage_phase_l3": { + "name": "Voltage phase L3" + }, + "current_phase_l1": { + "name": "Current phase L1" + }, + "current_phase_l2": { + "name": "Current phase L2" + }, + "current_phase_l3": { + "name": "Current phase L3" + }, + "power_consumed_phase_l1": { + "name": "Power consumed phase L1" + }, + "power_consumed_phase_l2": { + "name": "Power consumed phase L2" + }, + "power_consumed_phase_l3": { + "name": "Power consumed phase L3" + }, + "power_produced_phase_l1": { + "name": "Power produced phase L1" + }, + "power_produced_phase_l2": { + "name": "Power produced phase L2" + }, + "power_produced_phase_l3": { + "name": "Power produced phase L3" + }, + "gas_consumption_price": { + "name": "Gas consumption price" + }, + "energy_consumption_price_low": { + "name": "Energy consumption price - Low" + }, + "energy_consumption_price_high": { + "name": "Energy consumption price - High" + }, + "energy_production_price_low": { + "name": "Energy production price - Low" + }, + "energy_production_price_high": { + "name": "Energy production price - High" + }, + "consumption_day": { + "name": "Consumption day" + }, + "consumption_total": { + "name": "Consumption total" + }, + "pulse_count": { + "name": "Pulse count" + } + } } } diff --git a/tests/components/p1_monitor/test_sensor.py b/tests/components/p1_monitor/test_sensor.py index 14ff3b1e519513..f84df458d4b76e 100644 --- a/tests/components/p1_monitor/test_sensor.py +++ b/tests/components/p1_monitor/test_sensor.py @@ -43,20 +43,23 @@ async def test_smartmeter( assert state assert entry.unique_id == f"{entry_id}_smartmeter_power_consumption" assert state.state == "877" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Consumption" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "SmartMeter Power consumption" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.smartmeter_energy_consumption_high") - entry = entity_registry.async_get("sensor.smartmeter_energy_consumption_high") + state = hass.states.get("sensor.smartmeter_energy_consumption_high_tariff") + entry = entity_registry.async_get( + "sensor.smartmeter_energy_consumption_high_tariff" + ) assert entry assert state assert entry.unique_id == f"{entry_id}_smartmeter_energy_consumption_high" assert state.state == "2770.133" assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Consumption - High Tariff" + state.attributes.get(ATTR_FRIENDLY_NAME) + == "SmartMeter Energy consumption - High tariff" ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR @@ -69,7 +72,7 @@ async def test_smartmeter( assert state assert entry.unique_id == f"{entry_id}_smartmeter_energy_tariff_period" assert state.state == "high" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Tariff Period" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "SmartMeter Energy tariff period" assert state.attributes.get(ATTR_ICON) == "mdi:calendar-clock" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes assert ATTR_DEVICE_CLASS not in state.attributes @@ -100,7 +103,7 @@ async def test_phases( assert state assert entry.unique_id == f"{entry_id}_phases_voltage_phase_l1" assert state.state == "233.6" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Voltage Phase L1" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Phases Voltage phase L1" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricPotential.VOLT @@ -114,7 +117,7 @@ async def test_phases( assert state assert entry.unique_id == f"{entry_id}_phases_current_phase_l1" assert state.state == "1.6" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Current Phase L1" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Phases Current phase L1" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricCurrent.AMPERE @@ -128,7 +131,7 @@ async def test_phases( assert state assert entry.unique_id == f"{entry_id}_phases_power_consumed_phase_l1" assert state.state == "315" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Consumed Phase L1" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Phases Power consumed phase L1" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER @@ -160,7 +163,10 @@ async def test_settings( assert state assert entry.unique_id == f"{entry_id}_settings_energy_consumption_price_low" assert state.state == "0.20522" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Consumption Price - Low" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Settings Energy consumption price - Low" + ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -173,7 +179,10 @@ async def test_settings( assert state assert entry.unique_id == f"{entry_id}_settings_energy_production_price_low" assert state.state == "0.20522" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Production Price - Low" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Settings Energy production price - Low" + ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -205,7 +214,7 @@ async def test_watermeter( assert state assert entry.unique_id == f"{entry_id}_watermeter_consumption_day" assert state.state == "112.0" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Consumption Day" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "WaterMeter Consumption day" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.LITERS From e0c77fba22b36d058585bdff2de62f8c17e73f15 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 4 Jul 2023 08:48:16 +0200 Subject: [PATCH 0141/1009] Fix siren.toggle service schema (#95770) --- homeassistant/components/siren/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index 0f82918d82a5f3..a8907ba3b687a7 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -131,7 +131,7 @@ async def async_handle_turn_on_service( SERVICE_TOGGLE, {}, "async_toggle", - [SirenEntityFeature.TURN_ON & SirenEntityFeature.TURN_OFF], + [SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF], ) return True From 10e9b9f813a840c8d492df137dbda67b0b730977 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 4 Jul 2023 09:16:40 +0200 Subject: [PATCH 0142/1009] Fix ring siren test (#95825) --- tests/components/ring/test_siren.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/ring/test_siren.py b/tests/components/ring/test_siren.py index fbbd14aaf4e61b..916da5d24fb002 100644 --- a/tests/components/ring/test_siren.py +++ b/tests/components/ring/test_siren.py @@ -59,10 +59,10 @@ async def test_default_ding_chime_can_be_played( assert state.state == "unknown" -async def test_toggle_plays_default_chime( +async def test_turn_on_plays_default_chime( hass: HomeAssistant, requests_mock: requests_mock.Mocker ) -> None: - """Tests the play chime request is sent correctly when toggled.""" + """Tests the play chime request is sent correctly when turned on.""" await setup_platform(hass, Platform.SIREN) # Mocks the response for playing a test sound @@ -72,7 +72,7 @@ async def test_toggle_plays_default_chime( ) await hass.services.async_call( "siren", - "toggle", + "turn_on", {"entity_id": "siren.downstairs_siren"}, blocking=True, ) From dc34d91da4a0cf401385b086a99d8825085999e0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 4 Jul 2023 11:03:40 +0200 Subject: [PATCH 0143/1009] Update roomba vacuum supported features (#95828) --- homeassistant/components/roomba/irobot_base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py index 8ec91acf96540c..317209886bd2b6 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -39,7 +39,6 @@ | VacuumEntityFeature.SEND_COMMAND | VacuumEntityFeature.START | VacuumEntityFeature.STATE - | VacuumEntityFeature.STATUS | VacuumEntityFeature.STOP | VacuumEntityFeature.LOCATE ) From 8f2a21d270696807d7e8008fdfa2678888976dae Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 4 Jul 2023 11:51:42 +0200 Subject: [PATCH 0144/1009] Update sharkiq vacuum supported features (#95829) --- homeassistant/components/sharkiq/vacuum.py | 1 - tests/components/sharkiq/test_vacuum.py | 1 - 2 files changed, 2 deletions(-) diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index 9121811af3cf4e..ca24212a96c741 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -77,7 +77,6 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum | VacuumEntityFeature.RETURN_HOME | VacuumEntityFeature.START | VacuumEntityFeature.STATE - | VacuumEntityFeature.STATUS | VacuumEntityFeature.STOP | VacuumEntityFeature.LOCATE ) diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index cfd62c9deafc53..4a54b900be1894 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -64,7 +64,6 @@ | VacuumEntityFeature.RETURN_HOME | VacuumEntityFeature.START | VacuumEntityFeature.STATE - | VacuumEntityFeature.STATUS | VacuumEntityFeature.STOP | VacuumEntityFeature.LOCATE ) From 91087392fe12bfc68d1667c7c749230af76b2c80 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 4 Jul 2023 12:52:04 +0200 Subject: [PATCH 0145/1009] 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 0567c551d981bb..a45204351619db 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 c84dacf2fc98af97401ec9d4edbb1291188557df Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 4 Jul 2023 13:19:16 +0200 Subject: [PATCH 0146/1009] Update tuya vacuum supported features (#95832) --- homeassistant/components/tuya/vacuum.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index 62ff1d63ca0968..a35fb4640ccf9c 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -86,7 +86,9 @@ def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> Non self._attr_fan_speed_list = [] - self._attr_supported_features |= VacuumEntityFeature.SEND_COMMAND + self._attr_supported_features = ( + VacuumEntityFeature.SEND_COMMAND | VacuumEntityFeature.STATE + ) if self.find_dpcode(DPCode.PAUSE, prefer_function=True): self._attr_supported_features |= VacuumEntityFeature.PAUSE @@ -102,11 +104,6 @@ def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> Non if self.find_dpcode(DPCode.SEEK, prefer_function=True): self._attr_supported_features |= VacuumEntityFeature.LOCATE - if self.find_dpcode(DPCode.STATUS, prefer_function=True): - self._attr_supported_features |= ( - VacuumEntityFeature.STATE | VacuumEntityFeature.STATUS - ) - if self.find_dpcode(DPCode.POWER, prefer_function=True): self._attr_supported_features |= ( VacuumEntityFeature.TURN_ON | VacuumEntityFeature.TURN_OFF From 081e4e03a7348fcdaff16516e294276b5887314c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 4 Jul 2023 13:26:48 +0200 Subject: [PATCH 0147/1009] 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 e27ff57f03f284..b428018cd9ec36 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -726,6 +726,10 @@ async def async_init_single_device(dev: Device) -> None: 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 b3e1a3f624f8e6b46d90417c3667686b6d4f9add Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 4 Jul 2023 13:40:22 +0200 Subject: [PATCH 0148/1009] 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 df5bf968ae13ee..75ad26665c3bd8 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -79,6 +79,10 @@ async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: 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 c26dc0940ce03cd66406543b18350a649fe0f4e5 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 4 Jul 2023 13:52:01 +0200 Subject: [PATCH 0149/1009] Use common translations for `On`, `Off`, `Open` and `Closed` (#95779) * Use common translations for On and Off * Used common translations for open and closed * Update homeassistant/components/sensibo/strings.json Co-authored-by: Joost Lekkerkerker * Only update state translations --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/climate/strings.json | 2 +- homeassistant/components/demo/strings.json | 2 +- homeassistant/components/humidifier/strings.json | 2 +- homeassistant/components/media_player/strings.json | 2 +- homeassistant/components/mqtt/strings.json | 2 +- homeassistant/components/overkiz/strings.json | 8 ++++---- homeassistant/components/reolink/strings.json | 4 ++-- homeassistant/components/roborock/strings.json | 4 ++-- homeassistant/components/sensibo/strings.json | 4 ++-- homeassistant/components/tuya/strings.json | 2 +- homeassistant/components/xiaomi_miio/strings.json | 2 +- homeassistant/components/yamaha_musiccast/strings.json | 2 +- 12 files changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index 73ac4d6fbc4769..5879c44db83b62 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -53,7 +53,7 @@ "hvac_action": { "name": "Current action", "state": { - "off": "Off", + "off": "[%key:common::state::off%]", "preheating": "Preheating", "heating": "Heating", "cooling": "Cooling", diff --git a/homeassistant/components/demo/strings.json b/homeassistant/components/demo/strings.json index add04c236e76f6..60db0322717d81 100644 --- a/homeassistant/components/demo/strings.json +++ b/homeassistant/components/demo/strings.json @@ -81,7 +81,7 @@ "2": "2", "3": "3", "auto": "Auto", - "off": "Off" + "off": "[%key:common::state::off%]" } } } diff --git a/homeassistant/components/humidifier/strings.json b/homeassistant/components/humidifier/strings.json index 3b4c0bf2dab823..7a2e371024fb78 100644 --- a/homeassistant/components/humidifier/strings.json +++ b/homeassistant/components/humidifier/strings.json @@ -34,7 +34,7 @@ "humidifying": "Humidifying", "drying": "Drying", "idle": "Idle", - "off": "Off" + "off": "[%key:common::state::off%]" } }, "available_modes": { diff --git a/homeassistant/components/media_player/strings.json b/homeassistant/components/media_player/strings.json index 4c33d1f27ef661..eed54ef58c3a96 100644 --- a/homeassistant/components/media_player/strings.json +++ b/homeassistant/components/media_player/strings.json @@ -122,7 +122,7 @@ "name": "Repeat", "state": { "all": "All", - "off": "Off", + "off": "[%key:common::state::off%]", "one": "One" } }, diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index b06794c9b329cb..c1eff29e3bef09 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -130,7 +130,7 @@ "selector": { "set_ca_cert": { "options": { - "off": "Off", + "off": "[%key:common::state::off%]", "auto": "Auto", "custom": "Custom" } diff --git a/homeassistant/components/overkiz/strings.json b/homeassistant/components/overkiz/strings.json index 41405780124e0c..a82284c24af2d6 100644 --- a/homeassistant/components/overkiz/strings.json +++ b/homeassistant/components/overkiz/strings.json @@ -59,9 +59,9 @@ "select": { "open_closed_pedestrian": { "state": { - "open": "Open", + "open": "[%key:common::state::open%]", "pedestrian": "Pedestrian", - "closed": "Closed" + "closed": "[%key:common::state::closed%]" } }, "memorized_simple_volume": { @@ -121,8 +121,8 @@ }, "three_way_handle_direction": { "state": { - "closed": "Closed", - "open": "Open", + "closed": "[%key:common::state::closed%]", + "open": "[%key:common::state::open%]", "tilt": "Tilt" } } diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index f208e3e4035f59..7dc89ddbaf3f55 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -60,7 +60,7 @@ "select": { "floodlight_mode": { "state": { - "off": "Off", + "off": "[%key:common::state::off%]", "auto": "Auto", "schedule": "Schedule" } @@ -74,7 +74,7 @@ }, "auto_quick_reply_message": { "state": { - "off": "Off" + "off": "[%key:common::state::off%]" } }, "auto_track_method": { diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index f711ceaf74ac84..72a83850f93bcb 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -95,7 +95,7 @@ "mop_intensity": { "name": "Mop intensity", "state": { - "off": "Off", + "off": "[%key:common::state::off%]", "low": "Low", "mild": "Mild", "medium": "Medium", @@ -126,7 +126,7 @@ "balanced": "Balanced", "custom": "Custom", "gentle": "Gentle", - "off": "Off", + "off": "[%key:common::state::off%]", "max": "Max", "max_plus": "Max plus", "medium": "Medium", diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json index fb3559de91a18f..b00c4200836905 100644 --- a/homeassistant/components/sensibo/strings.json +++ b/homeassistant/components/sensibo/strings.json @@ -62,9 +62,9 @@ }, "light": { "state": { - "on": "On", + "on": "[%key:common::state::on%]", "dim": "Dim", - "off": "Off" + "off": "[%key:common::state::off%]" } } } diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 534ff1dc9eccab..15e41043f5ae0f 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -93,7 +93,7 @@ "low": "Low", "middle": "Middle", "high": "High", - "closed": "Closed" + "closed": "[%key:common::state::closed%]" } }, "vacuum_collection": { diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index dfcb503182ce59..15c89498bc7b45 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -69,7 +69,7 @@ "state": { "bright": "Bright", "dim": "Dim", - "off": "Off" + "off": "[%key:common::state::off%]" } }, "display_orientation": { diff --git a/homeassistant/components/yamaha_musiccast/strings.json b/homeassistant/components/yamaha_musiccast/strings.json index 9905a8af74ba21..af26ed13b38779 100644 --- a/homeassistant/components/yamaha_musiccast/strings.json +++ b/homeassistant/components/yamaha_musiccast/strings.json @@ -29,7 +29,7 @@ }, "zone_sleep": { "state": { - "off": "Off", + "off": "[%key:common::state::off%]", "30_min": "30 Minutes", "60_min": "60 Minutes", "90_min": "90 Minutes", From 2ca648584d3c0f7cec77f4de6c21cbefed447bb0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 4 Jul 2023 14:18:42 +0200 Subject: [PATCH 0150/1009] Update mqtt vacuum supported features (#95830) * Update mqtt vacuum supported features * Update test --- homeassistant/components/mqtt/vacuum/schema_state.py | 3 +-- tests/components/mqtt/test_state_vacuum.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 385d60a3886625..fef185687db971 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -64,7 +64,6 @@ VacuumEntityFeature.START | VacuumEntityFeature.STOP | VacuumEntityFeature.RETURN_HOME - | VacuumEntityFeature.STATUS | VacuumEntityFeature.BATTERY | VacuumEntityFeature.CLEAN_SPOT ) @@ -199,7 +198,7 @@ def config_schema() -> vol.Schema: def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" supported_feature_strings: list[str] = config[CONF_SUPPORTED_FEATURES] - self._attr_supported_features = strings_to_services( + self._attr_supported_features = VacuumEntityFeature.STATE | strings_to_services( supported_feature_strings, STRING_TO_SERVICE ) self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST] diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 203f5d55b95258..dd15399f67088b 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -112,7 +112,7 @@ async def test_default_supported_features( entity = hass.states.get("vacuum.mqtttest") entity_features = entity.attributes.get(mqttvacuum.CONF_SUPPORTED_FEATURES, 0) assert sorted(services_to_strings(entity_features, SERVICE_TO_STRING)) == sorted( - ["start", "stop", "return_home", "battery", "status", "clean_spot"] + ["start", "stop", "return_home", "battery", "clean_spot"] ) From 52d57efcbf58644ff21c229d8917902beb9a3046 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 4 Jul 2023 14:41:19 +0200 Subject: [PATCH 0151/1009] 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 891264d4290954..4ead41e86e9758 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 ccf35e384bb9bc..6b5676c4a25c16 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -251,6 +251,21 @@ "_airplay._tcp.local.": [ { "domain": "apple_tv", + "properties": { + "model": "appletv*", + }, + }, + { + "domain": "apple_tv", + "properties": { + "model": "audioaccessory*", + }, + }, + { + "domain": "apple_tv", + "properties": { + "am": "airport*", + }, }, { "domain": "samsungtv", From 6964a2112ae6cfc7dcf30e55f81f8e576c9ad7ea Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 4 Jul 2023 14:42:44 +0200 Subject: [PATCH 0152/1009] Revert "Remove unsupported services from tuya vacuum" (#95845) Revert "Remove unsupported services from tuya vacuum (#95790)" This reverts commit 5712d12c42e417c6265fa66d45941a0d0751e06c. --- homeassistant/components/tuya/vacuum.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index a35fb4640ccf9c..3c6ede66c69abf 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -150,6 +150,14 @@ def state(self) -> str | None: return None return TUYA_STATUS_TO_HA.get(status) + def turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + self._send_command([{"code": DPCode.POWER, "value": True}]) + + def turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + self._send_command([{"code": DPCode.POWER, "value": False}]) + def start(self, **kwargs: Any) -> None: """Start the device.""" self._send_command([{"code": DPCode.POWER_GO, "value": True}]) From 02192ddf82ee439d60253a60e218160fc2a0d765 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 4 Jul 2023 14:54:37 +0200 Subject: [PATCH 0153/1009] Set Matter battery sensors as diagnostic (#95794) * Set matter battery sensor to diagnostic * Update tests * Use new eve contact sensor dump across the board * Assert entity category * Complete typing --- .../components/matter/binary_sensor.py | 3 +- homeassistant/components/matter/sensor.py | 2 + tests/components/matter/conftest.py | 21 ++ .../matter/fixtures/nodes/contact-sensor.json | 90 ----- .../fixtures/nodes/eve-contact-sensor.json | 343 ++++++++++++++++++ tests/components/matter/test_binary_sensor.py | 70 +++- tests/components/matter/test_door_lock.py | 9 - tests/components/matter/test_sensor.py | 29 ++ 8 files changed, 451 insertions(+), 116 deletions(-) delete mode 100644 tests/components/matter/fixtures/nodes/contact-sensor.json create mode 100644 tests/components/matter/fixtures/nodes/eve-contact-sensor.json diff --git a/homeassistant/components/matter/binary_sensor.py b/homeassistant/components/matter/binary_sensor.py index 7c94c07c8cd87a..aabfc12eefbf6a 100644 --- a/homeassistant/components/matter/binary_sensor.py +++ b/homeassistant/components/matter/binary_sensor.py @@ -13,7 +13,7 @@ BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import EntityCategory, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -99,6 +99,7 @@ def _update_from_device(self) -> None: entity_description=MatterBinarySensorEntityDescription( key="BatteryChargeLevel", device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, measurement_to_ha=lambda x: x != clusters.PowerSource.Enums.BatChargeLevelEnum.kOk, ), diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index 027dcda65a7985..5021ed7fa0d016 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( LIGHT_LUX, PERCENTAGE, + EntityCategory, Platform, UnitOfPressure, UnitOfTemperature, @@ -127,6 +128,7 @@ def _update_from_device(self) -> None: key="PowerSource", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, # value has double precision measurement_to_ha=lambda x: int(x / 2), ), diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index e4af252fccb55d..6a14148585a23e 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -5,12 +5,15 @@ from collections.abc import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch +from matter_server.client.models.node import MatterNode from matter_server.common.const import SCHEMA_VERSION from matter_server.common.models import ServerInfoMessage import pytest from homeassistant.core import HomeAssistant +from .common import setup_integration_with_node_fixture + from tests.common import MockConfigEntry MOCK_FABRIC_ID = 12341234 @@ -210,3 +213,21 @@ def update_addon_fixture() -> Generator[AsyncMock, None, None]: "homeassistant.components.hassio.addon_manager.async_update_addon" ) as update_addon: yield update_addon + + +@pytest.fixture(name="door_lock") +async def door_lock_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a door lock node.""" + return await setup_integration_with_node_fixture(hass, "door-lock", matter_client) + + +@pytest.fixture(name="eve_contact_sensor_node") +async def eve_contact_sensor_node_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a contact sensor node.""" + return await setup_integration_with_node_fixture( + hass, "eve-contact-sensor", matter_client + ) diff --git a/tests/components/matter/fixtures/nodes/contact-sensor.json b/tests/components/matter/fixtures/nodes/contact-sensor.json deleted file mode 100644 index 909f7be2ebea50..00000000000000 --- a/tests/components/matter/fixtures/nodes/contact-sensor.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "node_id": 1, - "date_commissioned": "2022-11-29T21:23:48.485051", - "last_interview": "2022-11-29T21:23:48.485057", - "interview_version": 2, - "attributes": { - "0/29/0": [ - { - "deviceType": 22, - "revision": 1 - } - ], - "0/29/1": [ - 4, 29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 53, 54, 55, 59, 60, 62, 63, - 64, 65 - ], - "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/40/0": 1, - "0/40/1": "Nabu Casa", - "0/40/2": 65521, - "0/40/3": "Mock ContactSensor", - "0/40/4": 32768, - "0/40/5": "Mock Contact sensor", - "0/40/6": "XX", - "0/40/7": 0, - "0/40/8": "v1.0", - "0/40/9": 1, - "0/40/10": "v1.0", - "0/40/11": "20221206", - "0/40/12": "", - "0/40/13": "", - "0/40/14": "", - "0/40/15": "TEST_SN", - "0/40/16": false, - "0/40/17": true, - "0/40/18": "mock-contact-sensor", - "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, 13, 14, 15, 16, 17, 18, 19, - 65528, 65529, 65531, 65532, 65533 - ], - "1/3/0": 0, - "1/3/1": 2, - "1/3/65532": 0, - "1/3/65533": 4, - "1/3/65528": [], - "1/3/65529": [0, 64], - "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], - "1/29/0": [ - { - "deviceType": 21, - "revision": 1 - } - ], - "1/29/1": [ - 3, 4, 5, 6, 7, 8, 15, 29, 30, 37, 47, 59, 64, 65, 69, 80, 257, 258, 259, - 512, 513, 514, 516, 768, 1024, 1026, 1027, 1028, 1029, 1030, 1283, 1284, - 1285, 1286, 1287, 1288, 1289, 1290, 1291, 1292, 1293, 1294, 2820, - 4294048773 - ], - "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/69/0": true, - "1/69/65532": 0, - "1/69/65533": 1, - "1/69/65528": [], - "1/69/65529": [], - "1/69/65531": [0, 65528, 65529, 65531, 65532, 65533] - }, - "available": true, - "attribute_subscriptions": [] -} diff --git a/tests/components/matter/fixtures/nodes/eve-contact-sensor.json b/tests/components/matter/fixtures/nodes/eve-contact-sensor.json new file mode 100644 index 00000000000000..b0eacfb621c17c --- /dev/null +++ b/tests/components/matter/fixtures/nodes/eve-contact-sensor.json @@ -0,0 +1,343 @@ +{ + "node_id": 1, + "date_commissioned": "2023-07-02T14:06:45.190550", + "last_interview": "2023-07-02T14:06:45.190553", + "interview_version": 4, + "available": true, + "is_bridge": false, + "attributes": { + "0/53/65532": 15, + "0/53/11": 26, + "0/53/3": 4895, + "0/53/47": 0, + "0/53/8": [ + { + "extAddress": 12872547289273451492, + "rloc16": 1024, + "routerId": 1, + "nextHop": 0, + "pathCost": 0, + "LQIIn": 3, + "LQIOut": 3, + "age": 142, + "allocated": true, + "linkEstablished": true + } + ], + "0/53/29": 1556, + "0/53/9": 2040160480, + "0/53/15": 1, + "0/53/40": 519, + "0/53/7": [ + { + "extAddress": 12872547289273451492, + "age": 654, + "rloc16": 1024, + "linkFrameCounter": 738, + "mleFrameCounter": 418, + "lqi": 3, + "averageRssi": -50, + "lastRssi": -51, + "frameErrorRate": 5, + "messageErrorRate": 0, + "rxOnWhenIdle": true, + "fullThreadDevice": true, + "fullNetworkData": true, + "isChild": false + } + ], + "0/53/33": 66, + "0/53/18": 1, + "0/53/45": 0, + "0/53/21": 0, + "0/53/36": 0, + "0/53/44": 0, + "0/53/50": 0, + "0/53/60": "AB//wA==", + "0/53/10": 68, + "0/53/53": 0, + "0/53/65528": [], + "0/53/4": 5980345540157460411, + "0/53/19": 1, + "0/53/62": [0, 0, 0, 0], + "0/53/54": 2, + "0/53/49": 0, + "0/53/23": 2597, + "0/53/20": 0, + "0/53/28": 1059, + "0/53/24": 17, + "0/53/22": 2614, + "0/53/17": 0, + "0/53/32": 0, + "0/53/14": 1, + "0/53/26": 2597, + "0/53/37": 0, + "0/53/65529": [0], + "0/53/34": 1, + "0/53/2": "MyHome1425454932", + "0/53/6": 0, + "0/53/43": 0, + "0/53/25": 2597, + "0/53/30": 0, + "0/53/41": 1, + "0/53/55": 4, + "0/53/42": 520, + "0/53/52": 0, + "0/53/61": { + "activeTimestampPresent": true, + "pendingTimestampPresent": false, + "masterKeyPresent": true, + "networkNamePresent": true, + "extendedPanIdPresent": true, + "meshLocalPrefixPresent": true, + "delayPresent": false, + "panIdPresent": true, + "channelPresent": true, + "pskcPresent": true, + "securityPolicyPresent": true, + "channelMaskPresent": true + }, + "0/53/48": 3, + "0/53/39": 529, + "0/53/35": 0, + "0/53/38": 0, + "0/53/31": 0, + "0/53/51": 0, + "0/53/65533": 1, + "0/53/59": { + "rotationTime": 672, + "flags": 8335 + }, + "0/53/46": 0, + "0/53/5": "QP1S/nSVYwAA", + "0/53/13": 1, + "0/53/27": 17, + "0/53/1": 2, + "0/53/0": 25, + "0/53/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 59, + 60, 61, 62, 65528, 65529, 65531, 65532, 65533 + ], + "0/53/12": 121, + "0/53/16": 0, + "0/42/0": [ + { + "providerNodeID": 1773685588, + "endpoint": 0, + "fabricIndex": 1 + } + ], + "0/42/65528": [], + "0/42/65533": 1, + "0/42/1": true, + "0/42/2": 1, + "0/42/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/42/3": null, + "0/42/65532": 0, + "0/42/65529": [0], + "0/48/65532": 0, + "0/48/65528": [1, 3, 5], + "0/48/1": { + "failSafeExpiryLengthSeconds": 60, + "maxCumulativeFailsafeSeconds": 900 + }, + "0/48/4": true, + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/48/2": 0, + "0/48/0": 0, + "0/48/3": 0, + "0/48/65529": [0, 2, 4], + "0/48/65533": 1, + "0/31/4": 3, + "0/31/65529": [], + "0/31/3": 3, + "0/31/65533": 1, + "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/31/1": [], + "0/31/0": [ + { + "privilege": 0, + "authMode": 0, + "subjects": null, + "targets": null, + "fabricIndex": 1 + }, + { + "privilege": 0, + "authMode": 0, + "subjects": null, + "targets": null, + "fabricIndex": 2 + }, + { + "privilege": 5, + "authMode": 2, + "subjects": [112233], + "targets": null, + "fabricIndex": 3 + } + ], + "0/31/65532": 0, + "0/31/65528": [], + "0/31/2": 4, + "0/49/2": 10, + "0/49/65528": [1, 5, 7], + "0/49/65533": 1, + "0/49/1": [ + { + "networkID": "Uv50lWMtT7s=", + "connected": true + } + ], + "0/49/3": 20, + "0/49/7": null, + "0/49/0": 1, + "0/49/6": null, + "0/49/65529": [0, 3, 4, 6, 8], + "0/49/5": 0, + "0/49/4": true, + "0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533], + "0/49/65532": 2, + "0/63/0": [], + "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/63/65528": [2, 5], + "0/63/1": [], + "0/63/3": 3, + "0/63/65532": 0, + "0/63/65533": 1, + "0/63/65529": [0, 1, 3, 4], + "0/63/2": 3, + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/29/65529": [], + "0/29/65532": 0, + "0/29/3": [1], + "0/29/2": [41], + "0/29/65533": 1, + "0/29/0": [ + { + "deviceType": 22, + "revision": 1 + } + ], + "0/29/1": [29, 31, 40, 42, 46, 48, 49, 51, 53, 60, 62, 63], + "0/29/65528": [], + "0/51/65531": [0, 1, 2, 3, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533], + "0/51/0": [ + { + "name": "ieee802154", + "isOperational": true, + "offPremiseServicesReachableIPv4": null, + "offPremiseServicesReachableIPv6": null, + "hardwareAddress": "YtmXHFJ/dhk=", + "IPv4Addresses": [], + "IPv6Addresses": [ + "/RG+U41GAABynlpPU50e5g==", + "/oAAAAAAAABg2ZccUn92GQ==", + "/VL+dJVjAAB1cwmi02rvTA==" + ], + "type": 4 + } + ], + "0/51/65529": [0], + "0/51/7": [], + "0/51/3": 0, + "0/51/65533": 1, + "0/51/2": 653, + "0/51/6": [], + "0/51/1": 1, + "0/51/8": false, + "0/51/65532": 0, + "0/51/65528": [], + "0/51/5": [], + "0/40/9": 6650, + "0/40/65529": [], + "0/40/4": 77, + "0/40/1": "Eve Systems", + "0/40/5": "", + "0/40/15": "QV26L1A16199", + "0/40/8": "1.1", + "0/40/6": "**REDACTED**", + "0/40/3": "Eve Door", + "0/40/19": { + "caseSessionsPerFabric": 3, + "subscriptionsPerFabric": 3 + }, + "0/40/2": 4874, + "0/40/65532": 0, + "0/40/65528": [], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 18, 19, 65528, 65529, 65531, 65532, + 65533 + ], + "0/40/7": 1, + "0/40/10": "3.2.1", + "0/40/0": 1, + "0/40/65533": 1, + "0/40/18": "4D97F6015F8E39C1", + "0/46/65529": [], + "0/46/0": [1], + "0/46/65528": [], + "0/46/65531": [0, 65528, 65529, 65531, 65532, 65533], + "0/46/65532": 0, + "0/46/65533": 1, + "0/60/65532": 0, + "0/60/0": 0, + "0/60/65528": [], + "0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/60/2": null, + "0/60/65529": [0, 1, 2], + "0/60/1": null, + "0/60/65533": 1, + "1/69/65529": [], + "1/69/65528": [], + "1/69/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/69/65533": 1, + "1/69/65532": 0, + "1/69/0": false, + "1/29/65529": [], + "1/29/1": [3, 29, 47, 69, 319486977], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/29/65533": 1, + "1/29/0": [ + { + "deviceType": 21, + "revision": 1 + } + ], + "1/29/65528": [], + "1/29/65532": 0, + "1/29/2": [], + "1/29/3": [], + "1/47/65531": [ + 0, 1, 2, 11, 12, 14, 15, 16, 18, 19, 25, 65528, 65529, 65531, 65532, 65533 + ], + "1/47/15": false, + "1/47/25": 1, + "1/47/2": "Battery", + "1/47/18": [], + "1/47/1": 0, + "1/47/14": 0, + "1/47/65533": 1, + "1/47/12": 200, + "1/47/19": "", + "1/47/11": 3558, + "1/47/65528": [], + "1/47/65529": [], + "1/47/0": 1, + "1/47/16": 2, + "1/47/65532": 10, + "1/3/65528": [], + "1/3/65529": [0], + "1/3/1": 2, + "1/3/0": 0, + "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/3/65532": 0, + "1/3/65533": 4 + }, + "attribute_subscriptions": [ + [1, 69, 0], + [1, 47, 12] + ] +} diff --git a/tests/components/matter/test_binary_sensor.py b/tests/components/matter/test_binary_sensor.py index d7982e1d5aedd4..4dbb3b27b9c6ef 100644 --- a/tests/components/matter/test_binary_sensor.py +++ b/tests/components/matter/test_binary_sensor.py @@ -1,10 +1,16 @@ """Test Matter binary sensors.""" -from unittest.mock import MagicMock +from collections.abc import Generator +from unittest.mock import MagicMock, patch from matter_server.client.models.node import MatterNode import pytest +from homeassistant.components.matter.binary_sensor import ( + DISCOVERY_SCHEMAS as BINARY_SENSOR_SCHEMAS, +) +from homeassistant.const import EntityCategory, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .common import ( set_node_attribute, @@ -13,14 +19,16 @@ ) -@pytest.fixture(name="contact_sensor_node") -async def contact_sensor_node_fixture( - hass: HomeAssistant, matter_client: MagicMock -) -> MatterNode: - """Fixture for a contact sensor node.""" - return await setup_integration_with_node_fixture( - hass, "contact-sensor", matter_client - ) +@pytest.fixture(autouse=True) +def binary_sensor_platform() -> Generator[None, None, None]: + """Load only the binary sensor platform.""" + with patch( + "homeassistant.components.matter.discovery.DISCOVERY_SCHEMAS", + new={ + Platform.BINARY_SENSOR: BINARY_SENSOR_SCHEMAS, + }, + ): + yield # This tests needs to be adjusted to remove lingering tasks @@ -28,21 +36,22 @@ async def contact_sensor_node_fixture( async def test_contact_sensor( hass: HomeAssistant, matter_client: MagicMock, - contact_sensor_node: MatterNode, + eve_contact_sensor_node: MatterNode, ) -> None: """Test contact sensor.""" - state = hass.states.get("binary_sensor.mock_contact_sensor_door") + entity_id = "binary_sensor.eve_door_door" + state = hass.states.get(entity_id) assert state - assert state.state == "off" + assert state.state == "on" - set_node_attribute(contact_sensor_node, 1, 69, 0, False) + set_node_attribute(eve_contact_sensor_node, 1, 69, 0, True) await trigger_subscription_callback( - hass, matter_client, data=(contact_sensor_node.node_id, "1/69/0", False) + hass, matter_client, data=(eve_contact_sensor_node.node_id, "1/69/0", True) ) - state = hass.states.get("binary_sensor.mock_contact_sensor_door") + state = hass.states.get(entity_id) assert state - assert state.state == "on" + assert state.state == "off" @pytest.fixture(name="occupancy_sensor_node") @@ -75,3 +84,32 @@ async def test_occupancy_sensor( state = hass.states.get("binary_sensor.mock_occupancy_sensor_occupancy") assert state assert state.state == "off" + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_battery_sensor( + hass: HomeAssistant, + matter_client: MagicMock, + door_lock: MatterNode, +) -> None: + """Test battery sensor.""" + entity_id = "binary_sensor.mock_door_lock_battery" + state = hass.states.get(entity_id) + assert state + assert state.state == "off" + + set_node_attribute(door_lock, 1, 47, 14, 1) + await trigger_subscription_callback( + hass, matter_client, data=(door_lock.node_id, "1/47/14", 1) + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == "on" + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get(entity_id) + + assert entry + assert entry.entity_category == EntityCategory.DIAGNOSTIC diff --git a/tests/components/matter/test_door_lock.py b/tests/components/matter/test_door_lock.py index 003bfa3cf396d1..3eba65dc8ab2ba 100644 --- a/tests/components/matter/test_door_lock.py +++ b/tests/components/matter/test_door_lock.py @@ -16,19 +16,10 @@ from .common import ( set_node_attribute, - setup_integration_with_node_fixture, trigger_subscription_callback, ) -@pytest.fixture(name="door_lock") -async def door_lock_fixture( - hass: HomeAssistant, matter_client: MagicMock -) -> MatterNode: - """Fixture for a door lock node.""" - return await setup_integration_with_node_fixture(hass, "door-lock", matter_client) - - # This tests needs to be adjusted to remove lingering tasks @pytest.mark.parametrize("expected_lingering_tasks", [True]) async def test_lock( diff --git a/tests/components/matter/test_sensor.py b/tests/components/matter/test_sensor.py index a2e97e188f63f1..2650f2b1a6ff36 100644 --- a/tests/components/matter/test_sensor.py +++ b/tests/components/matter/test_sensor.py @@ -4,7 +4,9 @@ from matter_server.client.models.node import MatterNode import pytest +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .common import ( set_node_attribute, @@ -179,3 +181,30 @@ async def test_temperature_sensor( state = hass.states.get("sensor.mock_temperature_sensor_temperature") assert state assert state.state == "25.0" + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_battery_sensor( + hass: HomeAssistant, + matter_client: MagicMock, + eve_contact_sensor_node: MatterNode, +) -> None: + """Test battery sensor.""" + entity_id = "sensor.eve_door_battery" + state = hass.states.get(entity_id) + assert state + assert state.state == "100" + + set_node_attribute(eve_contact_sensor_node, 1, 47, 12, 100) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get(entity_id) + assert state + assert state.state == "50" + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get(entity_id) + + assert entry + assert entry.entity_category == EntityCategory.DIAGNOSTIC From c46495a731021094b3aa4c283307b54545cfcfc0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 4 Jul 2023 17:58:15 +0200 Subject: [PATCH 0154/1009] 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 cfc44029e2340d..52d5aca070aa20 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 ea160c2badb1b8bb2472cc45bcb30299364e5888 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Jul 2023 12:13:52 -0500 Subject: [PATCH 0155/1009] 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 5f6152b7bc79be..4fc89bc918b790 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -8,10 +8,10 @@ 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 60e2ee86b2237ad62bdd662751f6f2b83dd8daa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 4 Jul 2023 21:29:14 +0200 Subject: [PATCH 0156/1009] Add Airzone Cloud Zone running binary sensor (#95606) --- homeassistant/components/airzone_cloud/binary_sensor.py | 6 +++++- tests/components/airzone_cloud/test_binary_sensor.py | 8 +++++++- tests/components/airzone_cloud/util.py | 3 +++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airzone_cloud/binary_sensor.py b/homeassistant/components/airzone_cloud/binary_sensor.py index 052318b6b109a4..29b550463d0f82 100644 --- a/homeassistant/components/airzone_cloud/binary_sensor.py +++ b/homeassistant/components/airzone_cloud/binary_sensor.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from typing import Any, Final -from aioairzone_cloud.const import AZD_PROBLEMS, AZD_WARNINGS, AZD_ZONES +from aioairzone_cloud.const import AZD_ACTIVE, AZD_PROBLEMS, AZD_WARNINGS, AZD_ZONES from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -29,6 +29,10 @@ class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription): ZONE_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = ( + AirzoneBinarySensorEntityDescription( + device_class=BinarySensorDeviceClass.RUNNING, + key=AZD_ACTIVE, + ), AirzoneBinarySensorEntityDescription( attributes={ "warnings": AZD_WARNINGS, diff --git a/tests/components/airzone_cloud/test_binary_sensor.py b/tests/components/airzone_cloud/test_binary_sensor.py index b2c9ee173b77e5..37357bf59da94a 100644 --- a/tests/components/airzone_cloud/test_binary_sensor.py +++ b/tests/components/airzone_cloud/test_binary_sensor.py @@ -1,6 +1,6 @@ """The binary sensor tests for the Airzone Cloud platform.""" -from homeassistant.const import STATE_OFF +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from .util import async_init_integration @@ -16,6 +16,12 @@ async def test_airzone_create_binary_sensors(hass: HomeAssistant) -> None: assert state.state == STATE_OFF assert state.attributes.get("warnings") is None + state = hass.states.get("binary_sensor.dormitorio_running") + assert state.state == STATE_OFF + state = hass.states.get("binary_sensor.salon_problem") assert state.state == STATE_OFF assert state.attributes.get("warnings") is None + + state = hass.states.get("binary_sensor.salon_running") + assert state.state == STATE_ON diff --git a/tests/components/airzone_cloud/util.py b/tests/components/airzone_cloud/util.py index 4eab870297b53b..80c0b4ae02731e 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -4,6 +4,7 @@ from unittest.mock import patch from aioairzone_cloud.const import ( + API_ACTIVE, API_AZ_AIDOO, API_AZ_SYSTEM, API_AZ_ZONE, @@ -177,6 +178,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: } if device.get_id() == "zone2": return { + API_ACTIVE: False, API_HUMIDITY: 24, API_IS_CONNECTED: True, API_WS_CONNECTED: True, @@ -187,6 +189,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: API_WARNINGS: [], } return { + API_ACTIVE: True, API_HUMIDITY: 30, API_IS_CONNECTED: True, API_WS_CONNECTED: True, From 26f2fabd853115b3cf1851f43fc3bc0313c7ea3d Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 4 Jul 2023 23:25:03 -0700 Subject: [PATCH 0157/1009] 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 3286dd152e849d..d56b2b0ddfae67 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 92dbf8d47b8060..73b45a556404f7 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -67,14 +67,6 @@ async def async_get_events( 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 463e075d16973c..e0fbbf0cdeb879 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 cfe6185c1c7af6bd0c07d7fe669844b440b90f0f Mon Sep 17 00:00:00 2001 From: Emilv2 Date: Wed, 5 Jul 2023 08:35:02 +0200 Subject: [PATCH 0158/1009] 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 81307c47bbad7c..d25dab4234ec0c 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 da1672698c7d6d..a91ce36593f241 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1627,7 +1627,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 436cda148973ae03a525cf0d33a6dfe63c75f1ee Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 5 Jul 2023 08:35:32 +0200 Subject: [PATCH 0159/1009] Make local calendar integration title translatable (#95805) --- homeassistant/components/local_calendar/strings.json | 1 + homeassistant/generated/integrations.json | 2 +- script/hassfest/translations.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/local_calendar/strings.json b/homeassistant/components/local_calendar/strings.json index f49c92e5438e0b..c6eb36ee88f0eb 100644 --- a/homeassistant/components/local_calendar/strings.json +++ b/homeassistant/components/local_calendar/strings.json @@ -1,4 +1,5 @@ { + "title": "Local Calendar", "config": { "step": { "user": { diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 314e8ffa092f05..9964bfe148ccd0 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3010,7 +3010,6 @@ "iot_class": "cloud_push" }, "local_calendar": { - "name": "Local Calendar", "integration_type": "hub", "config_flow": true, "iot_class": "local_polling" @@ -6677,6 +6676,7 @@ "input_text", "integration", "islamic_prayer_times", + "local_calendar", "local_ip", "min_max", "mobile_app", diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 9efe01cf962346..9f464fd4147d6f 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -34,6 +34,7 @@ "google_travel_time", "homekit_controller", "islamic_prayer_times", + "local_calendar", "local_ip", "nmap_tracker", "rpi_power", From 659281aab67bb810428bcf47726570572fd05476 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Jul 2023 01:35:40 -0500 Subject: [PATCH 0160/1009] 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 669241b05aafaa..639f47272d91e8 100644 --- a/homeassistant/components/esphome/alarm_control_panel.py +++ b/homeassistant/components/esphome/alarm_control_panel.py @@ -33,6 +33,7 @@ from .entity import ( EsphomeEntity, + esphome_state_property, platform_async_setup_entry, ) from .enum_mapper import EsphomeEnumMapper @@ -111,6 +112,7 @@ def _on_static_info_update(self, static_info: EntityInfo) -> None: 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 ddca7bf60ac9c0..90d7bde521582e 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.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 1dfa2f3c6b35bcb409a04abaa43e246def476f2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Jul 2023 01:44:00 -0500 Subject: [PATCH 0161/1009] Use slots in TraceElement (#95877) --- homeassistant/helpers/trace.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index c1d22157a317af..fd7a3081f7a345 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -17,6 +17,17 @@ class TraceElement: """Container for trace data.""" + __slots__ = ( + "_child_key", + "_child_run_id", + "_error", + "path", + "_result", + "reuse_by_child", + "_timestamp", + "_variables", + ) + def __init__(self, variables: TemplateVarsType, path: str) -> None: """Container for trace data.""" self._child_key: str | None = None From b7221bfe090b02725278d12655a24fd02802f4fc 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 0162/1009] 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 72a7f8de946ce2..3ff7612d47ebec 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 a91ce36593f241..bbda184c5615a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2732,7 +2732,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 d8ca0291a8467d..e3e39ea557da20 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2005,7 +2005,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 9109b5fead502cec768f1466e8052135230b5429 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Jul 2023 01:55:25 -0500 Subject: [PATCH 0163/1009] Bump protobuf to 4.23.3 (#95875) --- homeassistant/package_constraints.txt | 2 +- script/gen_requirements_all.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3c861d8a389c18..eee176be186957 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -153,7 +153,7 @@ pyOpenSSL>=23.1.0 # protobuf must be in package constraints for the wheel # builder to build binary wheels -protobuf==4.23.1 +protobuf==4.23.3 # faust-cchardet: Ensure we have a version we can build wheels # 2.1.18 is the first version that works with our wheel builder diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 0bbbd97c926d8a..d36b3f61d9db85 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -157,7 +157,7 @@ # protobuf must be in package constraints for the wheel # builder to build binary wheels -protobuf==4.23.1 +protobuf==4.23.3 # faust-cchardet: Ensure we have a version we can build wheels # 2.1.18 is the first version that works with our wheel builder From 91f334ca5951eb2b58cf682547ba8ce6eac08399 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Jul 2023 02:25:38 -0500 Subject: [PATCH 0164/1009] Small cleanups to service calls (#95873) --- homeassistant/core.py | 85 ++++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index a30aed22322229..f01f8188bc0b38 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1693,7 +1693,7 @@ def __init__( class ServiceCall: """Representation of a call to a service.""" - __slots__ = ["domain", "service", "data", "context", "return_response"] + __slots__ = ("domain", "service", "data", "context", "return_response") def __init__( self, @@ -1704,8 +1704,8 @@ def __init__( return_response: bool = False, ) -> None: """Initialize a service call.""" - self.domain = domain.lower() - self.service = service.lower() + self.domain = domain + self.service = service self.data = ReadOnlyDict(data or {}) self.context = context or Context() self.return_response = return_response @@ -1890,15 +1890,20 @@ async def async_call( This method is a coroutine. """ - domain = domain.lower() - service = service.lower() context = context or Context() service_data = service_data or {} try: handler = self._services[domain][service] except KeyError: - raise ServiceNotFound(domain, service) from None + # Almost all calls are already lower case, so we avoid + # calling lower() on the arguments in the common case. + domain = domain.lower() + service = service.lower() + try: + handler = self._services[domain][service] + except KeyError: + raise ServiceNotFound(domain, service) from None if return_response: if not blocking: @@ -1938,8 +1943,8 @@ async def async_call( self._hass.bus.async_fire( EVENT_CALL_SERVICE, { - ATTR_DOMAIN: domain.lower(), - ATTR_SERVICE: service.lower(), + ATTR_DOMAIN: domain, + ATTR_SERVICE: service, ATTR_SERVICE_DATA: service_data, }, context=context, @@ -1947,7 +1952,10 @@ async def async_call( coro = self._execute_service(handler, service_call) if not blocking: - self._run_service_in_background(coro, service_call) + self._hass.async_create_task( + self._run_service_call_catch_exceptions(coro, service_call), + f"service call background {service_call.domain}.{service_call.service}", + ) return None response_data = await coro @@ -1959,49 +1967,42 @@ async def async_call( ) return response_data - def _run_service_in_background( + async def _run_service_call_catch_exceptions( self, coro_or_task: Coroutine[Any, Any, Any] | asyncio.Task[Any], service_call: ServiceCall, ) -> None: """Run service call in background, catching and logging any exceptions.""" - - async def catch_exceptions() -> None: - try: - await coro_or_task - except Unauthorized: - _LOGGER.warning( - "Unauthorized service called %s/%s", - service_call.domain, - service_call.service, - ) - except asyncio.CancelledError: - _LOGGER.debug("Service was cancelled: %s", service_call) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error executing service: %s", service_call) - - self._hass.async_create_task( - catch_exceptions(), - f"service call background {service_call.domain}.{service_call.service}", - ) + try: + await coro_or_task + except Unauthorized: + _LOGGER.warning( + "Unauthorized service called %s/%s", + service_call.domain, + service_call.service, + ) + except asyncio.CancelledError: + _LOGGER.debug("Service was cancelled: %s", service_call) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error executing service: %s", service_call) async def _execute_service( self, handler: Service, service_call: ServiceCall ) -> ServiceResponse: """Execute a service.""" - if handler.job.job_type == HassJobType.Coroutinefunction: - return await cast( - Callable[[ServiceCall], Awaitable[ServiceResponse]], - handler.job.target, - )(service_call) - if handler.job.job_type == HassJobType.Callback: - return cast(Callable[[ServiceCall], ServiceResponse], handler.job.target)( - service_call - ) - return await self._hass.async_add_executor_job( - cast(Callable[[ServiceCall], ServiceResponse], handler.job.target), - service_call, - ) + job = handler.job + target = job.target + if job.job_type == HassJobType.Coroutinefunction: + if TYPE_CHECKING: + target = cast(Callable[..., Coroutine[Any, Any, _R]], target) + return await target(service_call) + if job.job_type == HassJobType.Callback: + if TYPE_CHECKING: + target = cast(Callable[..., _R], target) + return target(service_call) + if TYPE_CHECKING: + target = cast(Callable[..., _R], target) + return await self._hass.async_add_executor_job(target, service_call) class Config: From 85e8eee94ed8ff23d160ca36d2912f35307c7547 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 Jul 2023 09:54:23 +0200 Subject: [PATCH 0165/1009] 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 f0875bc15d74a8..9f53aef8165836 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 eee176be186957..39d241bd55d96e 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 bbda184c5615a0..5e99d5774ce971 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 e3e39ea557da20..dccc608c1aaa73 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 39dcb5a2b57001689877a71cb47f7c58a7ef1ddf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 5 Jul 2023 12:53:07 +0200 Subject: [PATCH 0166/1009] Adjust services and properties supported by roborock vacuum (#95789) * Update supported features * Raise issue when vacuum.start_pause is called --- .../components/roborock/strings.json | 6 ++++ homeassistant/components/roborock/vacuum.py | 17 +++++---- tests/components/roborock/test_vacuum.py | 36 ++++++++++++++++++- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 72a83850f93bcb..1cd95914808b8e 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -139,5 +139,11 @@ } } } + }, + "issues": { + "service_deprecation_start_pause": { + "title": "Roborock vaccum support for vacuum.start_pause is being removed", + "description": "Roborock vaccum support for the vacuum.start_pause service is deprecated and will be removed in Home Assistant 2024.2; Please adjust any automation or script that uses the service to instead call vacuum.pause or vacuum.start and select submit below to mark this issue as resolved." + } } } diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index 5f66338ecc13c6..804c082657882a 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -16,6 +16,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify @@ -75,7 +76,6 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): | VacuumEntityFeature.RETURN_HOME | VacuumEntityFeature.FAN_SPEED | VacuumEntityFeature.BATTERY - | VacuumEntityFeature.STATUS | VacuumEntityFeature.SEND_COMMAND | VacuumEntityFeature.LOCATE | VacuumEntityFeature.CLEAN_SPOT @@ -110,11 +110,6 @@ def fan_speed(self) -> str | None: """Return the fan speed of the vacuum cleaner.""" return self._device_status.fan_power.name - @property - def status(self) -> str | None: - """Return the status of the vacuum cleaner.""" - return self._device_status.state.name - async def async_start(self) -> None: """Start the vacuum.""" await self.send(RoborockCommand.APP_START) @@ -152,6 +147,16 @@ async def async_start_pause(self) -> None: await self.async_pause() else: await self.async_start() + ir.async_create_issue( + self.hass, + DOMAIN, + "service_deprecation_start_pause", + breaks_in_ha_version="2024.2.0", + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="service_deprecation_start_pause", + ) async def async_send_command( self, diff --git a/tests/components/roborock/test_vacuum.py b/tests/components/roborock/test_vacuum.py index 80fbd4092c0550..080893f1d95925 100644 --- a/tests/components/roborock/test_vacuum.py +++ b/tests/components/roborock/test_vacuum.py @@ -20,7 +20,7 @@ ) from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import entity_registry as er, issue_registry as ir from tests.common import MockConfigEntry @@ -88,3 +88,37 @@ async def test_commands( assert mock_send_command.call_count == 1 assert mock_send_command.call_args[0][0] == command assert mock_send_command.call_args[0][1] == called_params + + +@pytest.mark.parametrize( + ("service", "issue_id"), + [ + (SERVICE_START_PAUSE, "service_deprecation_start_pause"), + ], +) +async def test_issues( + hass: HomeAssistant, + bypass_api_fixture, + setup_entry: MockConfigEntry, + service: str, + issue_id: str, +) -> None: + """Test issues raised by calling deprecated services.""" + vacuum = hass.states.get(ENTITY_ID) + assert vacuum + + data = {ATTR_ENTITY_ID: ENTITY_ID} + with patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_command" + ): + await hass.services.async_call( + Platform.VACUUM, + service, + data, + blocking=True, + ) + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue("roborock", issue_id) + assert issue.is_fixable is True + assert issue.is_persistent is True From b2e708834fe6340887eed00b9f279bfd53a4c97c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Jul 2023 07:00:37 -0500 Subject: [PATCH 0167/1009] Add slots to the StateMachine class (#95849) --- homeassistant/components/recorder/core.py | 8 ++--- homeassistant/core.py | 2 ++ tests/components/recorder/test_init.py | 37 ++++++++++++++--------- tests/helpers/test_restore_state.py | 25 ++++++++++++--- tests/helpers/test_template.py | 23 ++++++++------ 5 files changed, 63 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 5023393dc5e2e1..d4a026cfefc921 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -553,10 +553,10 @@ def _adjust_lru_size(self) -> None: If the number of entities has increased, increase the size of the LRU cache to avoid thrashing. """ - new_size = self.hass.states.async_entity_ids_count() * 2 - self.state_attributes_manager.adjust_lru_size(new_size) - self.states_meta_manager.adjust_lru_size(new_size) - self.statistics_meta_manager.adjust_lru_size(new_size) + if new_size := self.hass.states.async_entity_ids_count() * 2: + self.state_attributes_manager.adjust_lru_size(new_size) + self.states_meta_manager.adjust_lru_size(new_size) + self.statistics_meta_manager.adjust_lru_size(new_size) @callback def async_periodic_statistics(self) -> None: diff --git a/homeassistant/core.py b/homeassistant/core.py index f01f8188bc0b38..252abdb28d4d74 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1410,6 +1410,8 @@ def __repr__(self) -> str: class StateMachine: """Helper class that tracks the state of different entities.""" + __slots__ = ("_states", "_reservations", "_bus", "_loop") + def __init__(self, bus: EventBus, loop: asyncio.events.AbstractEventLoop) -> None: """Initialize state machine.""" self._states: dict[str, State] = {} diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 0bb315365b5d00..4e9a0261ec2063 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -56,6 +56,10 @@ SERVICE_PURGE, SERVICE_PURGE_ENTITIES, ) +from homeassistant.components.recorder.table_managers import ( + state_attributes as state_attributes_table_manager, + states_meta as states_meta_table_manager, +) from homeassistant.components.recorder.util import session_scope from homeassistant.const import ( EVENT_COMPONENT_LOADED, @@ -93,6 +97,15 @@ from tests.typing import RecorderInstanceGenerator +@pytest.fixture +def small_cache_size() -> None: + """Patch the default cache size to 8.""" + with patch.object(state_attributes_table_manager, "CACHE_SIZE", 8), patch.object( + states_meta_table_manager, "CACHE_SIZE", 8 + ): + yield + + def _default_recorder(hass): """Return a recorder with reasonable defaults.""" return Recorder( @@ -2022,13 +2035,10 @@ def test_deduplication_event_data_inside_commit_interval( assert all(event.data_id == first_data_id for event in events) -# Patch CACHE_SIZE since otherwise -# the CI can fail because the test takes too long to run -@patch( - "homeassistant.components.recorder.table_managers.state_attributes.CACHE_SIZE", 5 -) def test_deduplication_state_attributes_inside_commit_interval( - hass_recorder: Callable[..., HomeAssistant], caplog: pytest.LogCaptureFixture + small_cache_size: None, + hass_recorder: Callable[..., HomeAssistant], + caplog: pytest.LogCaptureFixture, ) -> None: """Test deduplication of state attributes inside the commit interval.""" hass = hass_recorder() @@ -2306,16 +2316,15 @@ async def test_excluding_attributes_by_integration( async def test_lru_increases_with_many_entities( - recorder_mock: Recorder, hass: HomeAssistant + small_cache_size: None, recorder_mock: Recorder, hass: HomeAssistant ) -> None: """Test that the recorder's internal LRU cache increases with many entities.""" - # We do not actually want to record 4096 entities so we mock the entity count - mock_entity_count = 4096 - with patch.object( - hass.states, "async_entity_ids_count", return_value=mock_entity_count - ): - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) - await async_wait_recording_done(hass) + mock_entity_count = 16 + for idx in range(mock_entity_count): + hass.states.async_set(f"test.entity{idx}", "on") + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) + await async_wait_recording_done(hass) assert ( recorder_mock.state_attributes_manager._id_map.get_size() diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index 56e931b4345390..fa0a14b8fbbddd 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -232,17 +232,21 @@ async def test_hass_starting(hass: HomeAssistant) -> None: entity.hass = hass entity.entity_id = "input_boolean.b1" + all_states = hass.states.async_all() + assert len(all_states) == 0 + hass.states.async_set("input_boolean.b1", "on") + # Mock that only b1 is present this run - states = [State("input_boolean.b1", "on")] with patch( "homeassistant.helpers.restore_state.Store.async_save" - ) as mock_write_data, patch.object(hass.states, "async_all", return_value=states): + ) as mock_write_data: state = await entity.async_get_last_state() await hass.async_block_till_done() assert state is not None assert state.entity_id == "input_boolean.b1" assert state.state == "on" + hass.states.async_remove("input_boolean.b1") # Assert that no data was written yet, since hass is still starting. assert not mock_write_data.called @@ -293,15 +297,20 @@ async def test_dump_data(hass: HomeAssistant) -> None: "input_boolean.b5": StoredState(State("input_boolean.b5", "off"), None, now), } + for state in states: + hass.states.async_set(state.entity_id, state.state, state.attributes) + with patch( "homeassistant.helpers.restore_state.Store.async_save" - ) as mock_write_data, patch.object(hass.states, "async_all", return_value=states): + ) as mock_write_data: await data.async_dump_states() assert mock_write_data.called args = mock_write_data.mock_calls[0][1] written_states = args[0] + for state in states: + hass.states.async_remove(state.entity_id) # b0 should not be written, since it didn't extend RestoreEntity # b1 should be written, since it is present in the current run # b2 should not be written, since it is not registered with the helper @@ -319,9 +328,12 @@ async def test_dump_data(hass: HomeAssistant) -> None: # Test that removed entities are not persisted await entity.async_remove() + for state in states: + hass.states.async_set(state.entity_id, state.state, state.attributes) + with patch( "homeassistant.helpers.restore_state.Store.async_save" - ) as mock_write_data, patch.object(hass.states, "async_all", return_value=states): + ) as mock_write_data: await data.async_dump_states() assert mock_write_data.called @@ -355,10 +367,13 @@ async def test_dump_error(hass: HomeAssistant) -> None: data = async_get(hass) + for state in states: + hass.states.async_set(state.entity_id, state.state, state.attributes) + with patch( "homeassistant.helpers.restore_state.Store.async_save", side_effect=HomeAssistantError, - ) as mock_write_data, patch.object(hass.states, "async_all", return_value=states): + ) as mock_write_data: await data.async_dump_states() assert mock_write_data.called diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index cd0b5a2ab88996..0c3f0e4469ad35 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -4533,20 +4533,22 @@ async def test_render_to_info_with_exception(hass: HomeAssistant) -> None: async def test_lru_increases_with_many_entities(hass: HomeAssistant) -> None: """Test that the template internal LRU cache increases with many entities.""" # We do not actually want to record 4096 entities so we mock the entity count - mock_entity_count = 4096 + mock_entity_count = 16 assert template.CACHED_TEMPLATE_LRU.get_size() == template.CACHED_TEMPLATE_STATES assert ( template.CACHED_TEMPLATE_NO_COLLECT_LRU.get_size() == template.CACHED_TEMPLATE_STATES ) + template.CACHED_TEMPLATE_LRU.set_size(8) + template.CACHED_TEMPLATE_NO_COLLECT_LRU.set_size(8) template.async_setup(hass) - with patch.object( - hass.states, "async_entity_ids_count", return_value=mock_entity_count - ): - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) - await hass.async_block_till_done() + for i in range(mock_entity_count): + hass.states.async_set(f"sensor.sensor{i}", "on") + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) + await hass.async_block_till_done() assert template.CACHED_TEMPLATE_LRU.get_size() == int( round(mock_entity_count * template.ENTITY_COUNT_GROWTH_FACTOR) @@ -4556,9 +4558,12 @@ async def test_lru_increases_with_many_entities(hass: HomeAssistant) -> None: ) await hass.async_stop() - with patch.object(hass.states, "async_entity_ids_count", return_value=8192): - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) - await hass.async_block_till_done() + + for i in range(mock_entity_count): + hass.states.async_set(f"sensor.sensor_add_{i}", "on") + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) + await hass.async_block_till_done() assert template.CACHED_TEMPLATE_LRU.get_size() == int( round(mock_entity_count * template.ENTITY_COUNT_GROWTH_FACTOR) From f028d1a1cae05598bdb9b7c4cd156d9f8af510e5 Mon Sep 17 00:00:00 2001 From: Aaron Collins Date: Thu, 6 Jul 2023 00:12:18 +1200 Subject: [PATCH 0168/1009] 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 75402c713252d0..f2092abef63c44 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 481a072bdb3c73..b0097f607d5809 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -15,8 +15,9 @@ 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 @@ def device_info(self) -> DeviceInfo: 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 6f90b0cf5efa6b..c6334dfaeca417 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 37b3ec45c4cb29..847f030fae5aba 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 5e99d5774ce971..5f32008df46cab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1618,7 +1618,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 dccc608c1aaa73..3585613fe0cb46 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1200,7 +1200,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 00000000000000..8145a7a1e99b7a --- /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 505f8fa363b610d31dc1d7819041698a94b5aab6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Jul 2023 07:17:28 -0500 Subject: [PATCH 0169/1009] Fix ESPHome camera not accepting the same exact image bytes (#95822) --- .coveragerc | 1 - .../components/esphome/entry_data.py | 4 +- tests/components/esphome/test_camera.py | 316 ++++++++++++++++++ 3 files changed, 319 insertions(+), 2 deletions(-) create mode 100644 tests/components/esphome/test_camera.py diff --git a/.coveragerc b/.coveragerc index f2092abef63c44..5ba8575979d8cc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -306,7 +306,6 @@ omit = homeassistant/components/escea/discovery.py homeassistant/components/esphome/__init__.py homeassistant/components/esphome/bluetooth/* - homeassistant/components/esphome/camera.py homeassistant/components/esphome/domain_data.py homeassistant/components/esphome/entry_data.py homeassistant/components/etherscan/sensor.py diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index a7c81543a9404b..3391d02a829aeb 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -14,6 +14,7 @@ APIVersion, BinarySensorInfo, CameraInfo, + CameraState, ClimateInfo, CoverInfo, DeviceInfo, @@ -339,8 +340,9 @@ def async_update_state(self, state: EntityState) -> None: if ( current_state == state and subscription_key not in stale_state + and state_type is not CameraState and not ( - type(state) is SensorState # pylint: disable=unidiomatic-typecheck + state_type is SensorState # pylint: disable=unidiomatic-typecheck and (platform_info := self.info.get(SensorInfo)) and (entity_info := platform_info.get(state.key)) and (cast(SensorInfo, entity_info)).force_update diff --git a/tests/components/esphome/test_camera.py b/tests/components/esphome/test_camera.py new file mode 100644 index 00000000000000..f856a9dd15caa5 --- /dev/null +++ b/tests/components/esphome/test_camera.py @@ -0,0 +1,316 @@ +"""Test ESPHome cameras.""" +from collections.abc import Awaitable, Callable + +from aioesphomeapi import ( + APIClient, + CameraInfo, + CameraState, + EntityInfo, + EntityState, + UserService, +) + +from homeassistant.components.camera import ( + STATE_IDLE, +) +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant + +from .conftest import MockESPHomeDevice + +from tests.typing import ClientSessionGenerator + +SMALLEST_VALID_JPEG = ( + "ffd8ffe000104a46494600010101004800480000ffdb00430003020202020203020202030303030406040404040408060" + "6050609080a0a090809090a0c0f0c0a0b0e0b09090d110d0e0f101011100a0c12131210130f101010ffc9000b08000100" + "0101011100ffcc000600101005ffda0008010100003f00d2cf20ffd9" +) +SMALLEST_VALID_JPEG_BYTES = bytes.fromhex(SMALLEST_VALID_JPEG) + + +async def test_camera_single_image( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], + hass_client: ClientSessionGenerator, +) -> None: + """Test a generic camera single image request.""" + entity_info = [ + CameraInfo( + object_id="mycamera", + key=1, + name="my camera", + unique_id="my_camera", + ) + ] + states = [] + user_service = [] + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("camera.test_my_camera") + assert state is not None + assert state.state == STATE_IDLE + + async def _mock_camera_image(): + mock_device.set_state(CameraState(key=1, data=SMALLEST_VALID_JPEG_BYTES)) + + mock_client.request_single_image = _mock_camera_image + + client = await hass_client() + resp = await client.get("/api/camera_proxy/camera.test_my_camera") + await hass.async_block_till_done() + state = hass.states.get("camera.test_my_camera") + assert state is not None + assert state.state == STATE_IDLE + + assert resp.status == 200 + assert resp.content_type == "image/jpeg" + assert resp.content_length == len(SMALLEST_VALID_JPEG_BYTES) + assert await resp.read() == SMALLEST_VALID_JPEG_BYTES + + +async def test_camera_single_image_unavailable_before_requested( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], + hass_client: ClientSessionGenerator, +) -> None: + """Test a generic camera that goes unavailable before the request.""" + entity_info = [ + CameraInfo( + object_id="mycamera", + key=1, + name="my camera", + unique_id="my_camera", + ) + ] + states = [] + user_service = [] + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("camera.test_my_camera") + assert state is not None + assert state.state == STATE_IDLE + await mock_device.mock_disconnect(False) + + client = await hass_client() + resp = await client.get("/api/camera_proxy/camera.test_my_camera") + await hass.async_block_till_done() + state = hass.states.get("camera.test_my_camera") + assert state is not None + assert state.state == STATE_UNAVAILABLE + + assert resp.status == 500 + + +async def test_camera_single_image_unavailable_during_request( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], + hass_client: ClientSessionGenerator, +) -> None: + """Test a generic camera that goes unavailable before the request.""" + entity_info = [ + CameraInfo( + object_id="mycamera", + key=1, + name="my camera", + unique_id="my_camera", + ) + ] + states = [] + user_service = [] + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("camera.test_my_camera") + assert state is not None + assert state.state == STATE_IDLE + + async def _mock_camera_image(): + await mock_device.mock_disconnect(False) + # Currently there is a bug where the camera will block + # forever if we don't send a response + mock_device.set_state(CameraState(key=1, data=SMALLEST_VALID_JPEG_BYTES)) + + mock_client.request_single_image = _mock_camera_image + + client = await hass_client() + resp = await client.get("/api/camera_proxy/camera.test_my_camera") + await hass.async_block_till_done() + state = hass.states.get("camera.test_my_camera") + assert state is not None + assert state.state == STATE_UNAVAILABLE + + assert resp.status == 500 + + +async def test_camera_stream( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], + hass_client: ClientSessionGenerator, +) -> None: + """Test a generic camera stream.""" + entity_info = [ + CameraInfo( + object_id="mycamera", + key=1, + name="my camera", + unique_id="my_camera", + ) + ] + states = [] + user_service = [] + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("camera.test_my_camera") + assert state is not None + assert state.state == STATE_IDLE + remaining_responses = 3 + + async def _mock_camera_image(): + nonlocal remaining_responses + if remaining_responses == 0: + return + remaining_responses -= 1 + mock_device.set_state(CameraState(key=1, data=SMALLEST_VALID_JPEG_BYTES)) + + mock_client.request_image_stream = _mock_camera_image + mock_client.request_single_image = _mock_camera_image + + client = await hass_client() + resp = await client.get("/api/camera_proxy_stream/camera.test_my_camera") + await hass.async_block_till_done() + state = hass.states.get("camera.test_my_camera") + assert state is not None + assert state.state == STATE_IDLE + + assert resp.status == 200 + assert resp.content_type == "multipart/x-mixed-replace" + assert resp.content_length is None + raw_stream = b"" + async for data in resp.content.iter_any(): + raw_stream += data + if len(raw_stream) > 300: + break + + assert b"image/jpeg" in raw_stream + + +async def test_camera_stream_unavailable( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], + hass_client: ClientSessionGenerator, +) -> None: + """Test a generic camera stream when the device is disconnected.""" + entity_info = [ + CameraInfo( + object_id="mycamera", + key=1, + name="my camera", + unique_id="my_camera", + ) + ] + states = [] + user_service = [] + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("camera.test_my_camera") + assert state is not None + assert state.state == STATE_IDLE + + await mock_device.mock_disconnect(False) + + client = await hass_client() + await client.get("/api/camera_proxy_stream/camera.test_my_camera") + await hass.async_block_till_done() + state = hass.states.get("camera.test_my_camera") + assert state is not None + assert state.state == STATE_UNAVAILABLE + + +async def test_camera_stream_with_disconnection( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], + hass_client: ClientSessionGenerator, +) -> None: + """Test a generic camera stream that goes unavailable during the request.""" + entity_info = [ + CameraInfo( + object_id="mycamera", + key=1, + name="my camera", + unique_id="my_camera", + ) + ] + states = [] + user_service = [] + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("camera.test_my_camera") + assert state is not None + assert state.state == STATE_IDLE + remaining_responses = 3 + + async def _mock_camera_image(): + nonlocal remaining_responses + if remaining_responses == 0: + return + if remaining_responses == 2: + await mock_device.mock_disconnect(False) + remaining_responses -= 1 + mock_device.set_state(CameraState(key=1, data=SMALLEST_VALID_JPEG_BYTES)) + + mock_client.request_image_stream = _mock_camera_image + mock_client.request_single_image = _mock_camera_image + + client = await hass_client() + await client.get("/api/camera_proxy_stream/camera.test_my_camera") + await hass.async_block_till_done() + state = hass.states.get("camera.test_my_camera") + assert state is not None + assert state.state == STATE_UNAVAILABLE From c75c79962a76a86939361ffd9aafbe22b6629907 Mon Sep 17 00:00:00 2001 From: gigatexel <65073191+gigatexel@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:31:27 +0200 Subject: [PATCH 0170/1009] Clarify GPS coordinates for device_tracker.see (#95847) --- homeassistant/components/device_tracker/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index c6c2d212e2d04e..22d89b42253c68 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -30,7 +30,7 @@ see: text: gps: name: GPS coordinates - description: GPS coordinates where device is located (latitude, longitude). + description: GPS coordinates where device is located, specified by latitude and longitude. example: "[51.509802, -0.086692]" selector: object: From bd7057f7b13aa6144500d2323ec8fb56c44ff84c Mon Sep 17 00:00:00 2001 From: Florent Thiery Date: Wed, 5 Jul 2023 15:09:12 +0200 Subject: [PATCH 0171/1009] Add raid array degraded state binary sensor to freebox sensors (#95242) Add raid array degraded state binary sensor --- .../components/freebox/binary_sensor.py | 100 ++++++++++ homeassistant/components/freebox/const.py | 1 + homeassistant/components/freebox/router.py | 11 ++ tests/components/freebox/conftest.py | 2 + tests/components/freebox/const.py | 174 ++++++++++++++---- .../components/freebox/test_binary_sensor.py | 44 +++++ 6 files changed, 296 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/freebox/binary_sensor.py create mode 100644 tests/components/freebox/test_binary_sensor.py diff --git a/homeassistant/components/freebox/binary_sensor.py b/homeassistant/components/freebox/binary_sensor.py new file mode 100644 index 00000000000000..aabd07366b40f7 --- /dev/null +++ b/homeassistant/components/freebox/binary_sensor.py @@ -0,0 +1,100 @@ +"""Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + EntityCategory, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .router import FreeboxRouter + +_LOGGER = logging.getLogger(__name__) + +RAID_SENSORS: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key="raid_degraded", + name="degraded", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the binary sensors.""" + router: FreeboxRouter = hass.data[DOMAIN][entry.unique_id] + + _LOGGER.debug("%s - %s - %s raid(s)", router.name, router.mac, len(router.raids)) + + binary_entities = [ + FreeboxRaidDegradedSensor(router, raid, description) + for raid in router.raids.values() + for description in RAID_SENSORS + ] + + if binary_entities: + async_add_entities(binary_entities, True) + + +class FreeboxRaidDegradedSensor(BinarySensorEntity): + """Representation of a Freebox raid sensor.""" + + _attr_should_poll = False + _attr_has_entity_name = True + + def __init__( + self, + router: FreeboxRouter, + raid: dict[str, Any], + description: BinarySensorEntityDescription, + ) -> None: + """Initialize a Freebox raid degraded sensor.""" + self.entity_description = description + self._router = router + self._attr_device_info = router.device_info + self._raid = raid + self._attr_name = f"Raid array {raid['id']} {description.name}" + self._attr_unique_id = ( + f"{router.mac} {description.key} {raid['name']} {raid['id']}" + ) + + @callback + def async_update_state(self) -> None: + """Update the Freebox Raid sensor.""" + self._raid = self._router.raids[self._raid["id"]] + + @property + def is_on(self) -> bool: + """Return true if degraded.""" + return self._raid["degraded"] + + @callback + def async_on_demand_update(self): + """Update state.""" + self.async_update_state() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Register state update callback.""" + self.async_update_state() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self._router.signal_sensor_update, + self.async_on_demand_update, + ) + ) diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index 767cb94de48afc..5a7c7863b4e0b8 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -20,6 +20,7 @@ Platform.BUTTON, Platform.DEVICE_TRACKER, Platform.SENSOR, + Platform.BINARY_SENSOR, Platform.SWITCH, Platform.CAMERA, ] diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 5622da48e673a0..4a9c22847aebfe 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -72,6 +72,7 @@ def __init__( self.devices: dict[str, dict[str, Any]] = {} self.disks: dict[int, dict[str, Any]] = {} + self.raids: dict[int, dict[str, Any]] = {} self.sensors_temperature: dict[str, int] = {} self.sensors_connection: dict[str, float] = {} self.call_list: list[dict[str, Any]] = [] @@ -145,6 +146,8 @@ async def update_sensors(self) -> None: await self._update_disks_sensors() + await self._update_raids_sensors() + async_dispatcher_send(self.hass, self.signal_sensor_update) async def _update_disks_sensors(self) -> None: @@ -155,6 +158,14 @@ async def _update_disks_sensors(self) -> None: for fbx_disk in fbx_disks: self.disks[fbx_disk["id"]] = fbx_disk + async def _update_raids_sensors(self) -> None: + """Update Freebox raids.""" + # None at first request + fbx_raids: list[dict[str, Any]] = await self._api.storage.get_raids() or [] + + for fbx_raid in fbx_raids: + self.raids[fbx_raid["id"]] = fbx_raid + async def update_home_devices(self) -> None: """Update Home devices (alarm, light, sensor, switch, remote ...).""" if not self.home_granted: diff --git a/tests/components/freebox/conftest.py b/tests/components/freebox/conftest.py index 7bf1cbfe7a4a5f..b950d44508dd4b 100644 --- a/tests/components/freebox/conftest.py +++ b/tests/components/freebox/conftest.py @@ -11,6 +11,7 @@ DATA_HOME_GET_NODES, DATA_LAN_GET_HOSTS_LIST, DATA_STORAGE_GET_DISKS, + DATA_STORAGE_GET_RAIDS, DATA_SYSTEM_GET_CONFIG, WIFI_GET_GLOBAL_CONFIG, ) @@ -56,6 +57,7 @@ def mock_router(mock_device_registry_devices): # sensor instance.call.get_calls_log = AsyncMock(return_value=DATA_CALL_GET_CALLS_LOG) instance.storage.get_disks = AsyncMock(return_value=DATA_STORAGE_GET_DISKS) + instance.storage.get_raids = AsyncMock(return_value=DATA_STORAGE_GET_RAIDS) # home devices instance.home.get_home_nodes = AsyncMock(return_value=DATA_HOME_GET_NODES) instance.connection.get_status = AsyncMock( diff --git a/tests/components/freebox/const.py b/tests/components/freebox/const.py index 96fe96c19c5fa7..7028366d02b241 100644 --- a/tests/components/freebox/const.py +++ b/tests/components/freebox/const.py @@ -93,75 +93,177 @@ { "idle_duration": 0, "read_error_requests": 0, - "read_requests": 110, + "read_requests": 1815106, "spinning": True, - # "table_type": "ms-dos", API returns without dash, but codespell isn't agree - "firmware": "SC1D", - "type": "internal", - "idle": False, - "connector": 0, - "id": 0, + "table_type": "raid", + "firmware": "0001", + "type": "sata", + "idle": True, + "connector": 2, + "id": 1000, "write_error_requests": 0, - "state": "enabled", - "write_requests": 2708929, - "total_bytes": 250050000000, - "model": "ST9250311CS", + "time_before_spindown": 600, + "state": "disabled", + "write_requests": 80386151, + "total_bytes": 2000000000000, + "model": "ST2000LM015-2E8174", "active_duration": 0, - "temp": 40, - "serial": "6VCQY907", + "temp": 30, + "serial": "ZDZLBFHC", "partitions": [ { - "fstype": "ext4", - "total_bytes": 244950000000, - "label": "Disque dur", - "id": 2, - "internal": True, + "fstype": "raid", + "total_bytes": 0, + "label": "Volume 2000Go", + "id": 1000, + "internal": False, "fsck_result": "no_run_yet", - "state": "mounted", - "disk_id": 0, - "free_bytes": 227390000000, - "used_bytes": 5090000000, - "path": "L0Rpc3F1ZSBkdXI=", + "state": "umounted", + "disk_id": 1000, + "free_bytes": 0, + "used_bytes": 0, + "path": "L1ZvbHVtZSAyMDAwR28=", } ], }, { - "idle_duration": 8290, + "idle_duration": 0, "read_error_requests": 0, - "read_requests": 2326826, - "spinning": False, - "table_type": "gpt", + "read_requests": 3622038, + "spinning": True, + "table_type": "raid", "firmware": "0001", "type": "sata", "idle": True, "connector": 0, "id": 2000, "write_error_requests": 0, - "state": "enabled", - "write_requests": 122733632, + "time_before_spindown": 600, + "state": "disabled", + "write_requests": 80386151, "total_bytes": 2000000000000, "model": "ST2000LM015-2E8174", "active_duration": 0, + "temp": 31, + "serial": "ZDZLEJXE", + "partitions": [ + { + "fstype": "raid", + "total_bytes": 0, + "label": "Volume 2000Go 1", + "id": 2000, + "internal": False, + "fsck_result": "no_run_yet", + "state": "umounted", + "disk_id": 2000, + "free_bytes": 0, + "used_bytes": 0, + "path": "L1ZvbHVtZSAyMDAwR28gMQ==", + } + ], + }, + { + "idle_duration": 0, + "read_error_requests": 0, + "read_requests": 0, + "spinning": False, + "table_type": "superfloppy", + "firmware": "", + "type": "raid", + "idle": False, + "connector": 0, + "id": 3000, + "write_error_requests": 0, + "state": "enabled", + "write_requests": 0, + "total_bytes": 2000000000000, + "model": "", + "active_duration": 0, "temp": 0, - "serial": "WDZYJ27Q", + "serial": "", "partitions": [ { "fstype": "ext4", "total_bytes": 1960000000000, - "label": "Disque 2", - "id": 2001, + "label": "Freebox", + "id": 3000, "internal": False, "fsck_result": "no_run_yet", "state": "mounted", - "disk_id": 2000, - "free_bytes": 1880000000000, - "used_bytes": 85410000000, - "path": "L0Rpc3F1ZSAy", + "disk_id": 3000, + "free_bytes": 1730000000000, + "used_bytes": 236910000000, + "path": "L0ZyZWVib3g=", } ], }, ] +DATA_STORAGE_GET_RAIDS = [ + { + "degraded": False, + "raid_disks": 2, # Number of members that should be in this array + "next_check": 0, # Unix timestamp of next check in seconds. Might be 0 if check_interval is 0 + "sync_action": "idle", # values: idle, resync, recover, check, repair, reshape, frozen + "level": "raid1", # values: basic, raid0, raid1, raid5, raid10 + "uuid": "dc8679f8-13f9-11ee-9106-38d547790df8", + "sysfs_state": "clear", # values: clear, inactive, suspended, readonly, read_auto, clean, active, write_pending, active_idle + "id": 0, + "sync_completed_pos": 0, # Current position of sync process + "members": [ + { + "total_bytes": 2000000000000, + "active_device": 1, + "id": 1000, + "corrected_read_errors": 0, + "array_id": 0, + "disk": { + "firmware": "0001", + "temp": 29, + "serial": "ZDZLBFHC", + "model": "ST2000LM015-2E8174", + }, + "role": "active", # values: active, faulty, spare, missing + "sct_erc_supported": False, + "sct_erc_enabled": False, + "dev_uuid": "fca8720e-13f9-11ee-9106-38d547790df8", + "device_location": "sata-internal-p2", + "set_name": "Freebox", + "set_uuid": "dc8679f8-13f9-11ee-9106-38d547790df8", + }, + { + "total_bytes": 2000000000000, + "active_device": 0, + "id": 2000, + "corrected_read_errors": 0, + "array_id": 0, + "disk": { + "firmware": "0001", + "temp": 30, + "serial": "ZDZLEJXE", + "model": "ST2000LM015-2E8174", + }, + "role": "active", + "sct_erc_supported": False, + "sct_erc_enabled": False, + "dev_uuid": "16bf00d6-13fa-11ee-9106-38d547790df8", + "device_location": "sata-internal-p0", + "set_name": "Freebox", + "set_uuid": "dc8679f8-13f9-11ee-9106-38d547790df8", + }, + ], + "array_size": 2000000000000, # Size of array in bytes + "state": "running", # stopped, running, error + "sync_speed": 0, # Sync speed in bytes per second + "name": "Freebox", + "check_interval": 0, # Check interval in seconds + "disk_id": 3000, + "last_check": 1682884357, # Unix timestamp of last check in seconds + "sync_completed_end": 0, # End position of sync process: total of bytes to sync + "sync_completed_percent": 0, # Percentage of sync completion + } +] + # switch WIFI_GET_GLOBAL_CONFIG = {"enabled": True, "mac_filter_state": "disabled"} diff --git a/tests/components/freebox/test_binary_sensor.py b/tests/components/freebox/test_binary_sensor.py new file mode 100644 index 00000000000000..08ecfca379487a --- /dev/null +++ b/tests/components/freebox/test_binary_sensor.py @@ -0,0 +1,44 @@ +"""Tests for the Freebox sensors.""" +from copy import deepcopy +from datetime import timedelta +from unittest.mock import Mock + +from homeassistant.components.freebox.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from .const import DATA_STORAGE_GET_RAIDS, MOCK_HOST, MOCK_PORT + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_raid_array_degraded(hass: HomeAssistant, router: Mock) -> None: + """Test raid array degraded binary sensor.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, + unique_id=MOCK_HOST, + ) + entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + assert ( + hass.states.get("binary_sensor.freebox_server_r2_raid_array_0_degraded").state + == "off" + ) + + # Now simulate we degraded + DATA_STORAGE_GET_RAIDS_DEGRADED = deepcopy(DATA_STORAGE_GET_RAIDS) + DATA_STORAGE_GET_RAIDS_DEGRADED[0]["degraded"] = True + router().storage.get_raids.return_value = DATA_STORAGE_GET_RAIDS_DEGRADED + # Simulate an update + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=60)) + # To execute the save + await hass.async_block_till_done() + assert ( + hass.states.get("binary_sensor.freebox_server_r2_raid_array_0_degraded").state + == "on" + ) From ea57f78392c495e84abf7395a3a6dbc02fff6924 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Jul 2023 08:59:36 -0500 Subject: [PATCH 0172/1009] Add slots to the service registry (#95857) --- homeassistant/core.py | 2 + .../google_assistant/test_smart_home.py | 154 +++++------------- tests/components/group/test_light.py | 70 ++++---- tests/components/vesync/test_init.py | 5 +- tests/components/zha/conftest.py | 6 +- 5 files changed, 79 insertions(+), 158 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 252abdb28d4d74..60485a678b086c 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1726,6 +1726,8 @@ def __repr__(self) -> str: class ServiceRegistry: """Offer the services over the eventbus.""" + __slots__ = ("_services", "_hass") + def __init__(self, hass: HomeAssistant) -> None: """Initialize a service registry.""" self._services: dict[str, dict[str, Service]] = {} diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 849f9e38a68be6..f471e6f862c574 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -1,7 +1,7 @@ """Test Google Smart Home.""" import asyncio from types import SimpleNamespace -from unittest.mock import ANY, call, patch +from unittest.mock import ANY, patch import pytest from pytest_unordered import unordered @@ -488,76 +488,41 @@ async def test_execute( events = async_capture_events(hass, EVENT_COMMAND_RECEIVED) service_events = async_capture_events(hass, EVENT_CALL_SERVICE) - with patch.object( - hass.services, "async_call", wraps=hass.services.async_call - ) as call_service_mock: - result = await sh.async_handle_message( - hass, - MockConfig(should_report_state=report_state), - None, - { - "requestId": REQ_ID, - "inputs": [ - { - "intent": "action.devices.EXECUTE", - "payload": { - "commands": [ - { - "devices": [ - {"id": "light.non_existing"}, - {"id": "light.ceiling_lights"}, - {"id": "light.kitchen_lights"}, - ], - "execution": [ - { - "command": "action.devices.commands.OnOff", - "params": {"on": True}, - }, - { - "command": "action.devices.commands.BrightnessAbsolute", - "params": {"brightness": 20}, - }, - ], - } - ] - }, - } - ], - }, - const.SOURCE_CLOUD, - ) - assert call_service_mock.call_count == 4 - expected_calls = [ - call( - "light", - "turn_on", - {"entity_id": "light.ceiling_lights"}, - blocking=not report_state, - context=ANY, - ), - call( - "light", - "turn_on", - {"entity_id": "light.kitchen_lights"}, - blocking=not report_state, - context=ANY, - ), - call( - "light", - "turn_on", - {"entity_id": "light.ceiling_lights", "brightness_pct": 20}, - blocking=not report_state, - context=ANY, - ), - call( - "light", - "turn_on", - {"entity_id": "light.kitchen_lights", "brightness_pct": 20}, - blocking=not report_state, - context=ANY, - ), - ] - call_service_mock.assert_has_awaits(expected_calls, any_order=True) + result = await sh.async_handle_message( + hass, + MockConfig(should_report_state=report_state), + None, + { + "requestId": REQ_ID, + "inputs": [ + { + "intent": "action.devices.EXECUTE", + "payload": { + "commands": [ + { + "devices": [ + {"id": "light.non_existing"}, + {"id": "light.ceiling_lights"}, + {"id": "light.kitchen_lights"}, + ], + "execution": [ + { + "command": "action.devices.commands.OnOff", + "params": {"on": True}, + }, + { + "command": "action.devices.commands.BrightnessAbsolute", + "params": {"brightness": 20}, + }, + ], + } + ] + }, + } + ], + }, + const.SOURCE_CLOUD, + ) await hass.async_block_till_done() assert result == { @@ -682,11 +647,7 @@ async def slow_turn_on(*args, **kwargs): # Make DemoLigt.async_turn_on hang waiting for the turn_on_wait event await turn_on_wait.wait() - with patch.object( - hass.services, "async_call", wraps=hass.services.async_call - ) as call_service_mock, patch.object( - DemoLight, "async_turn_on", wraps=slow_turn_on - ): + with patch.object(DemoLight, "async_turn_on", wraps=slow_turn_on): result = await sh.async_handle_message( hass, MockConfig(should_report_state=report_state), @@ -722,51 +683,10 @@ async def slow_turn_on(*args, **kwargs): }, const.SOURCE_CLOUD, ) - # Only the two first calls are executed - assert call_service_mock.call_count == 2 - expected_calls = [ - call( - "light", - "turn_on", - {"entity_id": "light.ceiling_lights"}, - blocking=not report_state, - context=ANY, - ), - call( - "light", - "turn_on", - {"entity_id": "light.kitchen_lights"}, - blocking=not report_state, - context=ANY, - ), - ] - call_service_mock.assert_has_awaits(expected_calls, any_order=True) turn_on_wait.set() await hass.async_block_till_done() await hass.async_block_till_done() - # The remaining two calls should now have executed - assert call_service_mock.call_count == 4 - expected_calls.extend( - [ - call( - "light", - "turn_on", - {"entity_id": "light.ceiling_lights", "brightness_pct": 20}, - blocking=not report_state, - context=ANY, - ), - call( - "light", - "turn_on", - {"entity_id": "light.kitchen_lights", "brightness_pct": 20}, - blocking=not report_state, - context=ANY, - ), - ] - ) - call_service_mock.assert_has_awaits(expected_calls, any_order=True) - await hass.async_block_till_done() assert result == { "requestId": REQ_ID, diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index f50d4486b39590..539a8c61414fe5 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -1,5 +1,4 @@ """The tests for the Group Light platform.""" -import unittest.mock from unittest.mock import MagicMock, patch import async_timeout @@ -16,7 +15,6 @@ ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, ATTR_EFFECT_LIST, - ATTR_FLASH, ATTR_HS_COLOR, ATTR_MAX_COLOR_TEMP_KELVIN, ATTR_MIN_COLOR_TEMP_KELVIN, @@ -26,7 +24,6 @@ ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_WHITE, - ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, SERVICE_TOGGLE, SERVICE_TURN_OFF, @@ -39,16 +36,17 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + EVENT_CALL_SERVICE, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from tests.common import get_fixture_path +from tests.common import async_capture_events, get_fixture_path async def test_default_state(hass: HomeAssistant) -> None: @@ -1443,6 +1441,7 @@ async def test_invalid_service_calls(hass: HomeAssistant) -> None: await group.async_setup_platform( hass, {"name": "test", "entities": ["light.test1", "light.test2"]}, add_entities ) + await async_setup_component(hass, "light", {}) await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -1451,35 +1450,38 @@ async def test_invalid_service_calls(hass: HomeAssistant) -> None: grouped_light = add_entities.call_args[0][0][0] grouped_light.hass = hass - with unittest.mock.patch.object(hass.services, "async_call") as mock_call: - await grouped_light.async_turn_on(brightness=150, four_oh_four="404") - data = {ATTR_ENTITY_ID: ["light.test1", "light.test2"], ATTR_BRIGHTNESS: 150} - mock_call.assert_called_once_with( - LIGHT_DOMAIN, SERVICE_TURN_ON, data, blocking=True, context=None - ) - mock_call.reset_mock() - - await grouped_light.async_turn_off(transition=4, four_oh_four="404") - data = {ATTR_ENTITY_ID: ["light.test1", "light.test2"], ATTR_TRANSITION: 4} - mock_call.assert_called_once_with( - LIGHT_DOMAIN, SERVICE_TURN_OFF, data, blocking=True, context=None - ) - mock_call.reset_mock() - - data = { - ATTR_BRIGHTNESS: 150, - ATTR_XY_COLOR: (0.5, 0.42), - ATTR_RGB_COLOR: (80, 120, 50), - ATTR_COLOR_TEMP_KELVIN: 1234, - ATTR_EFFECT: "Sunshine", - ATTR_TRANSITION: 4, - ATTR_FLASH: "long", - } - await grouped_light.async_turn_on(**data) - data[ATTR_ENTITY_ID] = ["light.test1", "light.test2"] - mock_call.assert_called_once_with( - LIGHT_DOMAIN, SERVICE_TURN_ON, data, blocking=True, context=None - ) + service_call_events = async_capture_events(hass, EVENT_CALL_SERVICE) + + await grouped_light.async_turn_on(brightness=150, four_oh_four="404") + data = {ATTR_ENTITY_ID: ["light.test1", "light.test2"], ATTR_BRIGHTNESS: 150} + assert len(service_call_events) == 1 + service_event_call: Event = service_call_events[0] + assert service_event_call.data["domain"] == LIGHT_DOMAIN + assert service_event_call.data["service"] == SERVICE_TURN_ON + assert service_event_call.data["service_data"] == data + service_call_events.clear() + + await grouped_light.async_turn_off(transition=4, four_oh_four="404") + data = {ATTR_ENTITY_ID: ["light.test1", "light.test2"], ATTR_TRANSITION: 4} + assert len(service_call_events) == 1 + service_event_call: Event = service_call_events[0] + assert service_event_call.data["domain"] == LIGHT_DOMAIN + assert service_event_call.data["service"] == SERVICE_TURN_OFF + assert service_event_call.data["service_data"] == data + service_call_events.clear() + + data = { + ATTR_BRIGHTNESS: 150, + ATTR_COLOR_TEMP_KELVIN: 1234, + ATTR_TRANSITION: 4, + } + await grouped_light.async_turn_on(**data) + data[ATTR_ENTITY_ID] = ["light.test1", "light.test2"] + service_event_call: Event = service_call_events[0] + assert service_event_call.data["domain"] == LIGHT_DOMAIN + assert service_event_call.data["service"] == SERVICE_TURN_ON + assert service_event_call.data["service_data"] == data + service_call_events.clear() async def test_reload(hass: HomeAssistant) -> None: diff --git a/tests/components/vesync/test_init.py b/tests/components/vesync/test_init.py index 0f77c9cbf35e55..c643e2bda1980b 100644 --- a/tests/components/vesync/test_init.py +++ b/tests/components/vesync/test_init.py @@ -33,15 +33,12 @@ async def test_async_setup_entry__not_login( hass.config_entries, "async_forward_entry_setup" ) as setup_mock, patch( "homeassistant.components.vesync.async_process_devices" - ) as process_mock, patch.object( - hass.services, "async_register" - ) as register_mock: + ) as process_mock: assert not await async_setup_entry(hass, config_entry) await hass.async_block_till_done() assert setups_mock.call_count == 0 assert setup_mock.call_count == 0 assert process_mock.call_count == 0 - assert register_mock.call_count == 0 assert manager.login.call_count == 1 assert DOMAIN not in hass.data diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 271108496b218d..e3a12703640fa7 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -332,8 +332,8 @@ def _zha_device( @pytest.fixture def hass_disable_services(hass): - """Mock service register.""" - with patch.object(hass.services, "async_register"), patch.object( - hass.services, "has_service", return_value=True + """Mock services.""" + with patch.object( + hass, "services", MagicMock(has_service=MagicMock(return_value=True)) ): yield hass From c7f6d8405863f9dbea6e926e79c767aa27d25868 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 5 Jul 2023 16:51:28 +0200 Subject: [PATCH 0173/1009] Warn when changing multipan channel if there are not 2 known users (#95898) * Warn when changing multipan channel if there are not 2 known users * Add test * Improve messages * Tweak translation string * Adjust message * Remove unused translation placeholders --- .../silabs_multiprotocol_addon.py | 32 ++++- .../homeassistant_hardware/strings.json | 15 ++- .../homeassistant_sky_connect/strings.json | 15 ++- .../homeassistant_yellow/strings.json | 15 ++- .../test_silabs_multiprotocol_addon.py | 120 +++++++++++++++++- 5 files changed, 181 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py index c5f7049e54f497..e4d9902346c167 100644 --- a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py +++ b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py @@ -138,6 +138,17 @@ async def async_change_channel( return tasks + async def async_active_platforms(self) -> list[str]: + """Return a list of platforms using the multipan radio.""" + active_platforms: list[str] = [] + + for integration_domain, platform in self._platforms.items(): + if not await platform.async_using_multipan(self._hass): + continue + active_platforms.append(integration_domain) + + return active_platforms + @callback def async_get_channel(self) -> int | None: """Get the channel.""" @@ -510,7 +521,26 @@ async def async_step_reconfigure_addon( ) -> FlowResult: """Reconfigure the addon.""" multipan_manager = await get_addon_manager(self.hass) + active_platforms = await multipan_manager.async_active_platforms() + if set(active_platforms) != {"otbr", "zha"}: + return await self.async_step_notify_unknown_multipan_user() + return await self.async_step_change_channel() + + async def async_step_notify_unknown_multipan_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Notify that there may be unknown multipan platforms.""" + if user_input is None: + return self.async_show_form( + step_id="notify_unknown_multipan_user", + ) + return await self.async_step_change_channel() + async def async_step_change_channel( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Change the channel.""" + multipan_manager = await get_addon_manager(self.hass) if user_input is None: channels = [str(x) for x in range(11, 27)] suggested_channel = DEFAULT_CHANNEL @@ -529,7 +559,7 @@ async def async_step_reconfigure_addon( } ) return self.async_show_form( - step_id="reconfigure_addon", data_schema=data_schema + step_id="change_channel", data_schema=data_schema ) # Change the shared channel diff --git a/homeassistant/components/homeassistant_hardware/strings.json b/homeassistant/components/homeassistant_hardware/strings.json index 60501397557270..06221fc7b97f91 100644 --- a/homeassistant/components/homeassistant_hardware/strings.json +++ b/homeassistant/components/homeassistant_hardware/strings.json @@ -18,6 +18,12 @@ "uninstall_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::title%]" } }, + "change_channel": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::title%]", + "data": { + "channel": "Channel" + } + }, "install_addon": { "title": "The Silicon Labs Multiprotocol add-on installation has started" }, @@ -25,11 +31,12 @@ "title": "Channel change initiated", "description": "A Zigbee and Thread channel change has been initiated and will finish in {delay_minutes} minutes." }, + "notify_unknown_multipan_user": { + "title": "Manual configuration may be needed", + "description": "Home Assistant can automatically change the channels for otbr and zha. If you have configured another integration to use the radio, for example Zigbee2MQTT, you will have to reconfigure the channel in that integration after completing this guide." + }, "reconfigure_addon": { - "title": "Reconfigure IEEE 802.15.4 radio multiprotocol support", - "data": { - "channel": "Channel" - } + "title": "Reconfigure IEEE 802.15.4 radio multiprotocol support" }, "show_revert_guide": { "title": "Multiprotocol support is enabled for this device", diff --git a/homeassistant/components/homeassistant_sky_connect/strings.json b/homeassistant/components/homeassistant_sky_connect/strings.json index 415df2092a15f2..047130e787cac1 100644 --- a/homeassistant/components/homeassistant_sky_connect/strings.json +++ b/homeassistant/components/homeassistant_sky_connect/strings.json @@ -17,6 +17,12 @@ "uninstall_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_menu::menu_options::uninstall_addon%]" } }, + "change_channel": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::change_channel::title%]", + "data": { + "channel": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::change_channel::data::channel%]" + } + }, "install_addon": { "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_addon::title%]" }, @@ -24,11 +30,12 @@ "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_channel_change::title%]", "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_channel_change::description%]" }, + "notify_unknown_multipan_user": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_unknown_multipan_user::title%]", + "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_unknown_multipan_user::description%]" + }, "reconfigure_addon": { - "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::title%]", - "data": { - "channel": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::data::channel%]" - } + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::title%]" }, "show_revert_guide": { "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::title%]", diff --git a/homeassistant/components/homeassistant_yellow/strings.json b/homeassistant/components/homeassistant_yellow/strings.json index c1069a7e755a51..617e61336a5ef1 100644 --- a/homeassistant/components/homeassistant_yellow/strings.json +++ b/homeassistant/components/homeassistant_yellow/strings.json @@ -17,6 +17,12 @@ "uninstall_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::addon_menu::menu_options::uninstall_addon%]" } }, + "change_channel": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::change_channel::title%]", + "data": { + "channel": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::change_channel::data::channel%]" + } + }, "hardware_settings": { "title": "Configure hardware settings", "data": { @@ -38,6 +44,10 @@ "multipan_settings": "Configure IEEE 802.15.4 radio multiprotocol support" } }, + "notify_unknown_multipan_user": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_unknown_multipan_user::title%]", + "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_unknown_multipan_user::description%]" + }, "reboot_menu": { "title": "Reboot required", "description": "The settings have changed, but the new settings will not take effect until the system is rebooted", @@ -47,10 +57,7 @@ } }, "reconfigure_addon": { - "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::title%]", - "data": { - "channel": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::data::channel%]" - } + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::title%]" }, "show_revert_guide": { "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::title%]", diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index 83702adcc3aa4c..a956214c098add 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -27,6 +27,7 @@ ) TEST_DOMAIN = "test" +TEST_DOMAIN_2 = "test_2" class FakeConfigFlow(ConfigFlow): @@ -456,7 +457,7 @@ async def test_option_flow_addon_installed_other_device( @pytest.mark.parametrize( ("configured_channel", "suggested_channel"), [(None, "15"), (11, "11")] ) -async def test_option_flow_addon_installed_same_device_reconfigure( +async def test_option_flow_addon_installed_same_device_reconfigure_unexpected_users( hass: HomeAssistant, addon_info, addon_store_info, @@ -465,7 +466,7 @@ async def test_option_flow_addon_installed_same_device_reconfigure( configured_channel: int | None, suggested_channel: int, ) -> None: - """Test installing the multi pan addon.""" + """Test reconfiguring the multi pan addon.""" mock_integration(hass, MockModule("hassio")) addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" @@ -494,7 +495,11 @@ async def test_option_flow_addon_installed_same_device_reconfigure( {"next_step_id": "reconfigure_addon"}, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "reconfigure_addon" + assert result["step_id"] == "notify_unknown_multipan_user" + + result = await hass.config_entries.options.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "change_channel" assert get_suggested(result["data_schema"].schema, "channel") == suggested_channel result = await hass.config_entries.options.async_configure( @@ -508,6 +513,79 @@ async def test_option_flow_addon_installed_same_device_reconfigure( assert result["type"] == FlowResultType.CREATE_ENTRY assert mock_multiprotocol_platform.change_channel_calls == [(14, 300)] + assert multipan_manager._channel == 14 + + +@pytest.mark.parametrize( + ("configured_channel", "suggested_channel"), [(None, "15"), (11, "11")] +) +async def test_option_flow_addon_installed_same_device_reconfigure_expected_users( + hass: HomeAssistant, + addon_info, + addon_store_info, + addon_installed, + configured_channel: int | None, + suggested_channel: int, +) -> None: + """Test reconfiguring the multi pan addon.""" + mock_integration(hass, MockModule("hassio")) + addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + + multipan_manager = await silabs_multiprotocol_addon.get_addon_manager(hass) + multipan_manager._channel = configured_channel + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + mock_multiprotocol_platforms = {} + for domain in ["otbr", "zha"]: + mock_multiprotocol_platform = MockMultiprotocolPlatform() + mock_multiprotocol_platforms[domain] = mock_multiprotocol_platform + mock_multiprotocol_platform.channel = configured_channel + mock_multiprotocol_platform.using_multipan = True + + hass.config.components.add(domain) + mock_platform( + hass, f"{domain}.silabs_multiprotocol", mock_multiprotocol_platform + ) + hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: domain}) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "addon_menu" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "reconfigure_addon"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "change_channel" + assert get_suggested(result["data_schema"].schema, "channel") == suggested_channel + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"channel": "14"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "notify_channel_change" + assert result["description_placeholders"] == {"delay_minutes": "5"} + + result = await hass.config_entries.options.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.CREATE_ENTRY + + for domain in ["otbr", "zha"]: + assert mock_multiprotocol_platforms[domain].change_channel_calls == [(14, 300)] + assert multipan_manager._channel == 14 async def test_option_flow_addon_installed_same_device_uninstall( @@ -1007,3 +1085,39 @@ async def test_load_preferences(hass: HomeAssistant) -> None: await multipan_manager2.async_setup() assert multipan_manager._channel == multipan_manager2._channel + + +@pytest.mark.parametrize( + ( + "multipan_platforms", + "active_platforms", + ), + [ + ({}, []), + ({TEST_DOMAIN: False}, []), + ({TEST_DOMAIN: True}, [TEST_DOMAIN]), + ({TEST_DOMAIN: True, TEST_DOMAIN_2: False}, [TEST_DOMAIN]), + ({TEST_DOMAIN: True, TEST_DOMAIN_2: True}, [TEST_DOMAIN, TEST_DOMAIN_2]), + ], +) +async def test_active_plaforms( + hass: HomeAssistant, + multipan_platforms: dict[str, bool], + active_platforms: list[str], +) -> None: + """Test async_active_platforms.""" + multipan_manager = await silabs_multiprotocol_addon.get_addon_manager(hass) + + for domain, platform_using_multipan in multipan_platforms.items(): + mock_multiprotocol_platform = MockMultiprotocolPlatform() + mock_multiprotocol_platform.channel = 11 + mock_multiprotocol_platform.using_multipan = platform_using_multipan + + hass.config.components.add(domain) + mock_platform( + hass, f"{domain}.silabs_multiprotocol", mock_multiprotocol_platform + ) + hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: domain}) + + await hass.async_block_till_done() + assert await multipan_manager.async_active_platforms() == active_platforms From d9721702af611049c04d2700764e8267718cde01 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 5 Jul 2023 16:59:10 +0200 Subject: [PATCH 0174/1009] Address late review of freebox tests (#95910) Use lower case for local variables --- tests/components/freebox/test_binary_sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/freebox/test_binary_sensor.py b/tests/components/freebox/test_binary_sensor.py index 08ecfca379487a..ec504a514adebc 100644 --- a/tests/components/freebox/test_binary_sensor.py +++ b/tests/components/freebox/test_binary_sensor.py @@ -31,9 +31,9 @@ async def test_raid_array_degraded(hass: HomeAssistant, router: Mock) -> None: ) # Now simulate we degraded - DATA_STORAGE_GET_RAIDS_DEGRADED = deepcopy(DATA_STORAGE_GET_RAIDS) - DATA_STORAGE_GET_RAIDS_DEGRADED[0]["degraded"] = True - router().storage.get_raids.return_value = DATA_STORAGE_GET_RAIDS_DEGRADED + data_storage_get_raids_degraded = deepcopy(DATA_STORAGE_GET_RAIDS) + data_storage_get_raids_degraded[0]["degraded"] = True + router().storage.get_raids.return_value = data_storage_get_raids_degraded # Simulate an update async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=60)) # To execute the save From 20dc9203dd4715c4cb555ed228e995070f8f5812 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 5 Jul 2023 18:20:10 +0200 Subject: [PATCH 0175/1009] Update frontend to 20230705.1 (#95913) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 9f53aef8165836..07c5585833dd20 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230705.0"] + "requirements": ["home-assistant-frontend==20230705.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 39d241bd55d96e..93cdc1eb3d7f97 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 5f32008df46cab..5b100318336bf1 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 3585613fe0cb46..70b6da6bb7100b 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 dc5ee71d7ae74066a98aa55d1dd8277812af0c11 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Jul 2023 11:47:24 -0500 Subject: [PATCH 0176/1009] Add slots to core EventBus (#95856) --- homeassistant/core.py | 2 + tests/components/datadog/test_init.py | 36 +- tests/components/ffmpeg/test_init.py | 32 +- tests/components/google_pubsub/test_init.py | 48 +-- tests/components/influxdb/test_init.py | 366 +++++++------------- tests/components/logentries/test_init.py | 46 +-- tests/components/prometheus/test_init.py | 50 +-- tests/components/statsd/test_init.py | 50 ++- 8 files changed, 218 insertions(+), 412 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 60485a678b086c..dbc8769bb6f440 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -973,6 +973,8 @@ def __repr__(self) -> str: class EventBus: """Allow the firing of and listening for events.""" + __slots__ = ("_listeners", "_match_all_listeners", "_hass") + def __init__(self, hass: HomeAssistant) -> None: """Initialize a new event bus.""" self._listeners: dict[str, list[_FilterableJobType]] = {} diff --git a/tests/components/datadog/test_init.py b/tests/components/datadog/test_init.py index c42d532b8009a0..76956874e737f4 100644 --- a/tests/components/datadog/test_init.py +++ b/tests/components/datadog/test_init.py @@ -1,15 +1,13 @@ """The tests for the Datadog component.""" from unittest import mock -from unittest.mock import MagicMock, patch +from unittest.mock import patch import homeassistant.components.datadog as datadog from homeassistant.const import ( EVENT_LOGBOOK_ENTRY, - EVENT_STATE_CHANGED, STATE_OFF, STATE_ON, ) -import homeassistant.core as ha from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -27,7 +25,6 @@ async def test_invalid_config(hass: HomeAssistant) -> None: async def test_datadog_setup_full(hass: HomeAssistant) -> None: """Test setup with all data.""" config = {datadog.DOMAIN: {"host": "host", "port": 123, "rate": 1, "prefix": "foo"}} - hass.bus.listen = MagicMock() with patch("homeassistant.components.datadog.initialize") as mock_init, patch( "homeassistant.components.datadog.statsd" @@ -37,15 +34,9 @@ async def test_datadog_setup_full(hass: HomeAssistant) -> None: assert mock_init.call_count == 1 assert mock_init.call_args == mock.call(statsd_host="host", statsd_port=123) - assert hass.bus.listen.called - assert hass.bus.listen.call_args_list[0][0][0] == EVENT_LOGBOOK_ENTRY - assert hass.bus.listen.call_args_list[1][0][0] == EVENT_STATE_CHANGED - async def test_datadog_setup_defaults(hass: HomeAssistant) -> None: """Test setup with defaults.""" - hass.bus.listen = mock.MagicMock() - with patch("homeassistant.components.datadog.initialize") as mock_init, patch( "homeassistant.components.datadog.statsd" ): @@ -63,13 +54,10 @@ async def test_datadog_setup_defaults(hass: HomeAssistant) -> None: assert mock_init.call_count == 1 assert mock_init.call_args == mock.call(statsd_host="host", statsd_port=8125) - assert hass.bus.listen.called async def test_logbook_entry(hass: HomeAssistant) -> None: """Test event listener.""" - hass.bus.listen = mock.MagicMock() - with patch("homeassistant.components.datadog.initialize"), patch( "homeassistant.components.datadog.statsd" ) as mock_statsd: @@ -79,16 +67,14 @@ async def test_logbook_entry(hass: HomeAssistant) -> None: {datadog.DOMAIN: {"host": "host", "rate": datadog.DEFAULT_RATE}}, ) - assert hass.bus.listen.called - handler_method = hass.bus.listen.call_args_list[0][0][1] - event = { "domain": "automation", "entity_id": "sensor.foo.bar", "message": "foo bar biz", "name": "triggered something", } - handler_method(mock.MagicMock(data=event)) + hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, event) + await hass.async_block_till_done() assert mock_statsd.event.call_count == 1 assert mock_statsd.event.call_args == mock.call( @@ -102,8 +88,6 @@ async def test_logbook_entry(hass: HomeAssistant) -> None: async def test_state_changed(hass: HomeAssistant) -> None: """Test event listener.""" - hass.bus.listen = mock.MagicMock() - with patch("homeassistant.components.datadog.initialize"), patch( "homeassistant.components.datadog.statsd" ) as mock_statsd: @@ -119,9 +103,6 @@ async def test_state_changed(hass: HomeAssistant) -> None: }, ) - assert hass.bus.listen.called - handler_method = hass.bus.listen.call_args_list[1][0][1] - valid = {"1": 1, "1.0": 1.0, STATE_ON: 1, STATE_OFF: 0} attributes = {"elevation": 3.2, "temperature": 5.0, "up": True, "down": False} @@ -129,12 +110,12 @@ async def test_state_changed(hass: HomeAssistant) -> None: for in_, out in valid.items(): state = mock.MagicMock( domain="sensor", - entity_id="sensor.foo.bar", + entity_id="sensor.foobar", state=in_, attributes=attributes, ) - handler_method(mock.MagicMock(data={"new_state": state})) - + hass.states.async_set(state.entity_id, state.state, state.attributes) + await hass.async_block_till_done() assert mock_statsd.gauge.call_count == 5 for attribute, value in attributes.items(): @@ -160,7 +141,6 @@ async def test_state_changed(hass: HomeAssistant) -> None: mock_statsd.gauge.reset_mock() for invalid in ("foo", "", object): - handler_method( - mock.MagicMock(data={"new_state": ha.State("domain.test", invalid, {})}) - ) + hass.states.async_set("domain.test", invalid, {}) + await hass.async_block_till_done() assert not mock_statsd.gauge.called diff --git a/tests/components/ffmpeg/test_init.py b/tests/components/ffmpeg/test_init.py index 521ac732e5b782..0c6ce300d01b70 100644 --- a/tests/components/ffmpeg/test_init.py +++ b/tests/components/ffmpeg/test_init.py @@ -8,7 +8,11 @@ SERVICE_START, SERVICE_STOP, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ( + ATTR_ENTITY_ID, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component, setup_component @@ -54,7 +58,7 @@ def __init__(self, hass, initial_state=True, entity_id="test.ffmpeg_device"): self.hass = hass self.entity_id = entity_id - self.ffmpeg = MagicMock + self.ffmpeg = MagicMock() self.called_stop = False self.called_start = False self.called_restart = False @@ -104,12 +108,18 @@ async def test_setup_component_test_register(hass: HomeAssistant) -> None: with assert_setup_component(1): await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) - hass.bus.async_listen_once = MagicMock() ffmpeg_dev = MockFFmpegDev(hass) + ffmpeg_dev._async_stop_ffmpeg = AsyncMock() + ffmpeg_dev._async_start_ffmpeg = AsyncMock() await ffmpeg_dev.async_added_to_hass() - assert hass.bus.async_listen_once.called - assert hass.bus.async_listen_once.call_count == 2 + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert len(ffmpeg_dev._async_start_ffmpeg.mock_calls) == 2 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert len(ffmpeg_dev._async_stop_ffmpeg.mock_calls) == 2 async def test_setup_component_test_register_no_startup(hass: HomeAssistant) -> None: @@ -117,12 +127,18 @@ async def test_setup_component_test_register_no_startup(hass: HomeAssistant) -> with assert_setup_component(1): await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) - hass.bus.async_listen_once = MagicMock() ffmpeg_dev = MockFFmpegDev(hass, False) + ffmpeg_dev._async_stop_ffmpeg = AsyncMock() + ffmpeg_dev._async_start_ffmpeg = AsyncMock() await ffmpeg_dev.async_added_to_hass() - assert hass.bus.async_listen_once.called - assert hass.bus.async_listen_once.call_count == 1 + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert len(ffmpeg_dev._async_start_ffmpeg.mock_calls) == 1 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert len(ffmpeg_dev._async_stop_ffmpeg.mock_calls) == 2 async def test_setup_component_test_service_start(hass: HomeAssistant) -> None: diff --git a/tests/components/google_pubsub/test_init.py b/tests/components/google_pubsub/test_init.py index 38c3da79524d80..0a1d474126811c 100644 --- a/tests/components/google_pubsub/test_init.py +++ b/tests/components/google_pubsub/test_init.py @@ -8,8 +8,7 @@ import homeassistant.components.google_pubsub as google_pubsub from homeassistant.components.google_pubsub import DateTimeJSONEncoder as victim -from homeassistant.const import EVENT_STATE_CHANGED -from homeassistant.core import HomeAssistant, split_entity_id +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component GOOGLE_PUBSUB_PATH = "homeassistant.components.google_pubsub" @@ -60,9 +59,8 @@ def mock_is_file_fixture(): @pytest.fixture(autouse=True) -def mock_bus_and_json(hass, monkeypatch): +def mock_json(hass, monkeypatch): """Mock the event bus listener and os component.""" - hass.bus.listen = mock.MagicMock() monkeypatch.setattr( f"{GOOGLE_PUBSUB_PATH}.json.dumps", mock.Mock(return_value=mock.MagicMock()) ) @@ -80,8 +78,6 @@ async def test_minimal_config(hass: HomeAssistant, mock_client) -> None: } assert await async_setup_component(hass, google_pubsub.DOMAIN, config) await hass.async_block_till_done() - assert hass.bus.listen.called - assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED assert mock_client.from_service_account_json.call_count == 1 assert mock_client.from_service_account_json.call_args[0][0] == os.path.join( hass.config.config_dir, "creds" @@ -107,27 +103,12 @@ async def test_full_config(hass: HomeAssistant, mock_client) -> None: } assert await async_setup_component(hass, google_pubsub.DOMAIN, config) await hass.async_block_till_done() - assert hass.bus.listen.called - assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED assert mock_client.from_service_account_json.call_count == 1 assert mock_client.from_service_account_json.call_args[0][0] == os.path.join( hass.config.config_dir, "creds" ) -def make_event(entity_id): - """Make a mock event for test.""" - domain = split_entity_id(entity_id)[0] - state = mock.MagicMock( - state="not blank", - domain=domain, - entity_id=entity_id, - object_id="entity", - attributes={}, - ) - return mock.MagicMock(data={"new_state": state}, time_fired=12345) - - async def _setup(hass, filter_config): """Shared set up for filtering tests.""" config = { @@ -140,12 +121,11 @@ async def _setup(hass, filter_config): } assert await async_setup_component(hass, google_pubsub.DOMAIN, config) await hass.async_block_till_done() - return hass.bus.listen.call_args_list[0][0][1] async def test_allowlist(hass: HomeAssistant, mock_client) -> None: """Test an allowlist only config.""" - handler_method = await _setup( + await _setup( hass, { "include_domains": ["light"], @@ -165,8 +145,8 @@ async def test_allowlist(hass: HomeAssistant, mock_client) -> None: ] for test in tests: - event = make_event(test.id) - handler_method(event) + hass.states.async_set(test.id, "not blank") + await hass.async_block_till_done() was_called = publish_client.publish.call_count == 1 assert test.should_pass == was_called @@ -175,7 +155,7 @@ async def test_allowlist(hass: HomeAssistant, mock_client) -> None: async def test_denylist(hass: HomeAssistant, mock_client) -> None: """Test a denylist only config.""" - handler_method = await _setup( + await _setup( hass, { "exclude_domains": ["climate"], @@ -195,8 +175,8 @@ async def test_denylist(hass: HomeAssistant, mock_client) -> None: ] for test in tests: - event = make_event(test.id) - handler_method(event) + hass.states.async_set(test.id, "not blank") + await hass.async_block_till_done() was_called = publish_client.publish.call_count == 1 assert test.should_pass == was_called @@ -205,7 +185,7 @@ async def test_denylist(hass: HomeAssistant, mock_client) -> None: async def test_filtered_allowlist(hass: HomeAssistant, mock_client) -> None: """Test an allowlist config with a filtering denylist.""" - handler_method = await _setup( + await _setup( hass, { "include_domains": ["light"], @@ -226,8 +206,8 @@ async def test_filtered_allowlist(hass: HomeAssistant, mock_client) -> None: ] for test in tests: - event = make_event(test.id) - handler_method(event) + hass.states.async_set(test.id, "not blank") + await hass.async_block_till_done() was_called = publish_client.publish.call_count == 1 assert test.should_pass == was_called @@ -236,7 +216,7 @@ async def test_filtered_allowlist(hass: HomeAssistant, mock_client) -> None: async def test_filtered_denylist(hass: HomeAssistant, mock_client) -> None: """Test a denylist config with a filtering allowlist.""" - handler_method = await _setup( + await _setup( hass, { "include_entities": ["climate.included", "sensor.excluded_test"], @@ -257,8 +237,8 @@ async def test_filtered_denylist(hass: HomeAssistant, mock_client) -> None: ] for test in tests: - event = make_event(test.id) - handler_method(event) + hass.states.async_set(test.id, "not blank") + await hass.async_block_till_done() was_called = publish_client.publish.call_count == 1 assert test.should_pass == was_called diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index 683c69807b2338..a1234b7a470f88 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -2,14 +2,13 @@ from dataclasses import dataclass import datetime from http import HTTPStatus -from unittest.mock import MagicMock, Mock, call, patch +from unittest.mock import ANY, MagicMock, Mock, call, patch import pytest import homeassistant.components.influxdb as influxdb from homeassistant.components.influxdb.const import DEFAULT_BUCKET from homeassistant.const import ( - EVENT_STATE_CHANGED, PERCENTAGE, STATE_OFF, STATE_ON, @@ -39,7 +38,6 @@ class FilterTest: @pytest.fixture(autouse=True) def mock_batch_timeout(hass, monkeypatch): """Mock the event bus listener and the batch timeout for tests.""" - hass.bus.listen = MagicMock() monkeypatch.setattr( f"{INFLUX_PATH}.InfluxThread.batch_timeout", Mock(return_value=0), @@ -129,8 +127,6 @@ async def test_setup_config_full( assert await async_setup_component(hass, influxdb.DOMAIN, config) await hass.async_block_till_done() - assert hass.bus.listen.called - assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED assert get_write_api(mock_client).call_count == 1 @@ -263,8 +259,6 @@ async def test_setup_config_ssl( assert await async_setup_component(hass, influxdb.DOMAIN, config) await hass.async_block_till_done() - assert hass.bus.listen.called - assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED assert expected_client_args.items() <= mock_client.call_args.kwargs.items() @@ -285,8 +279,6 @@ async def test_setup_minimal_config( assert await async_setup_component(hass, influxdb.DOMAIN, config) await hass.async_block_till_done() - assert hass.bus.listen.called - assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED assert get_write_api(mock_client).call_count == 1 @@ -347,7 +339,6 @@ async def _setup(hass, mock_influx_client, config_ext, get_write_api): # A call is made to the write API during setup to test the connection. # Therefore we reset the write API mock here before the test begins. get_write_api(mock_influx_client).reset_mock() - return hass.bus.listen.call_args_list[0][0][1] @pytest.mark.parametrize( @@ -372,7 +363,7 @@ async def test_event_listener( hass: HomeAssistant, mock_client, config_ext, get_write_api, get_mock_call ) -> None: """Test the event listener.""" - handler_method = await _setup(hass, mock_client, config_ext, get_write_api) + await _setup(hass, mock_client, config_ext, get_write_api) # map of HA State to valid influxdb [state, value] fields valid = { @@ -394,19 +385,11 @@ async def test_event_listener( "updated_at": datetime.datetime(2017, 1, 1, 0, 0), "multi_periods": "0.120.240.2023873", } - state = MagicMock( - state=in_, - domain="fake", - entity_id="fake.entity-id", - object_id="entity", - attributes=attrs, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) body = [ { "measurement": "foobars", - "tags": {"domain": "fake", "entity_id": "entity"}, - "time": 12345, + "tags": {"domain": "fake", "entity_id": "entity_id"}, + "time": ANY, "fields": { "longitude": 1.1, "latitude": 2.2, @@ -427,7 +410,8 @@ async def test_event_listener( if out[1] is not None: body[0]["fields"]["value"] = out[1] - handler_method(event) + hass.states.async_set("fake.entity_id", in_, attrs) + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() write_api = get_write_api(mock_client) @@ -458,30 +442,23 @@ async def test_event_listener_no_units( hass: HomeAssistant, mock_client, config_ext, get_write_api, get_mock_call ) -> None: """Test the event listener for missing units.""" - handler_method = await _setup(hass, mock_client, config_ext, get_write_api) + await _setup(hass, mock_client, config_ext, get_write_api) - for unit in (None, ""): + for unit in ("",): if unit: attrs = {"unit_of_measurement": unit} else: attrs = {} - state = MagicMock( - state=1, - domain="fake", - entity_id="fake.entity-id", - object_id="entity", - attributes=attrs, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) body = [ { - "measurement": "fake.entity-id", - "tags": {"domain": "fake", "entity_id": "entity"}, - "time": 12345, + "measurement": "fake.entity_id", + "tags": {"domain": "fake", "entity_id": "entity_id"}, + "time": ANY, "fields": {"value": 1}, } ] - handler_method(event) + hass.states.async_set("fake.entity_id", 1, attrs) + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() write_api = get_write_api(mock_client) @@ -512,26 +489,19 @@ async def test_event_listener_inf( hass: HomeAssistant, mock_client, config_ext, get_write_api, get_mock_call ) -> None: """Test the event listener with large or invalid numbers.""" - handler_method = await _setup(hass, mock_client, config_ext, get_write_api) + await _setup(hass, mock_client, config_ext, get_write_api) attrs = {"bignumstring": "9" * 999, "nonumstring": "nan"} - state = MagicMock( - state=8, - domain="fake", - entity_id="fake.entity-id", - object_id="entity", - attributes=attrs, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) body = [ { - "measurement": "fake.entity-id", - "tags": {"domain": "fake", "entity_id": "entity"}, - "time": 12345, + "measurement": "fake.entity_id", + "tags": {"domain": "fake", "entity_id": "entity_id"}, + "time": ANY, "fields": {"value": 8}, } ] - handler_method(event) + hass.states.async_set("fake.entity_id", 8, attrs) + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() write_api = get_write_api(mock_client) @@ -561,26 +531,19 @@ async def test_event_listener_states( hass: HomeAssistant, mock_client, config_ext, get_write_api, get_mock_call ) -> None: """Test the event listener against ignored states.""" - handler_method = await _setup(hass, mock_client, config_ext, get_write_api) - - for state_state in (1, "unknown", "", "unavailable", None): - state = MagicMock( - state=state_state, - domain="fake", - entity_id="fake.entity-id", - object_id="entity", - attributes={}, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) + await _setup(hass, mock_client, config_ext, get_write_api) + + for state_state in (1, "unknown", "", "unavailable"): body = [ { - "measurement": "fake.entity-id", - "tags": {"domain": "fake", "entity_id": "entity"}, - "time": 12345, + "measurement": "fake.entity_id", + "tags": {"domain": "fake", "entity_id": "entity_id"}, + "time": ANY, "fields": {"value": 1}, } ] - handler_method(event) + hass.states.async_set("fake.entity_id", state_state) + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() write_api = get_write_api(mock_client) @@ -592,27 +555,20 @@ async def test_event_listener_states( write_api.reset_mock() -def execute_filter_test(hass, tests, handler_method, write_api, get_mock_call): +async def execute_filter_test(hass: HomeAssistant, tests, write_api, get_mock_call): """Execute all tests for a given filtering test.""" for test in tests: domain, entity_id = split_entity_id(test.id) - state = MagicMock( - state=1, - domain=domain, - entity_id=test.id, - object_id=entity_id, - attributes={}, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) body = [ { "measurement": test.id, "tags": {"domain": domain, "entity_id": entity_id}, - "time": 12345, + "time": ANY, "fields": {"value": 1}, } ] - handler_method(event) + hass.states.async_set(test.id, 1) + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() if test.should_pass: @@ -647,14 +603,14 @@ async def test_event_listener_denylist( """Test the event listener against a denylist.""" config = {"exclude": {"entities": ["fake.denylisted"]}, "include": {}} config.update(config_ext) - handler_method = await _setup(hass, mock_client, config, get_write_api) + await _setup(hass, mock_client, config, get_write_api) write_api = get_write_api(mock_client) tests = [ FilterTest("fake.ok", True), FilterTest("fake.denylisted", False), ] - execute_filter_test(hass, tests, handler_method, write_api, get_mock_call) + await execute_filter_test(hass, tests, write_api, get_mock_call) @pytest.mark.parametrize( @@ -681,14 +637,14 @@ async def test_event_listener_denylist_domain( """Test the event listener against a domain denylist.""" config = {"exclude": {"domains": ["another_fake"]}, "include": {}} config.update(config_ext) - handler_method = await _setup(hass, mock_client, config, get_write_api) + await _setup(hass, mock_client, config, get_write_api) write_api = get_write_api(mock_client) tests = [ FilterTest("fake.ok", True), FilterTest("another_fake.denylisted", False), ] - execute_filter_test(hass, tests, handler_method, write_api, get_mock_call) + await execute_filter_test(hass, tests, write_api, get_mock_call) @pytest.mark.parametrize( @@ -715,14 +671,14 @@ async def test_event_listener_denylist_glob( """Test the event listener against a glob denylist.""" config = {"exclude": {"entity_globs": ["*.excluded_*"]}, "include": {}} config.update(config_ext) - handler_method = await _setup(hass, mock_client, config, get_write_api) + await _setup(hass, mock_client, config, get_write_api) write_api = get_write_api(mock_client) tests = [ FilterTest("fake.ok", True), FilterTest("fake.excluded_entity", False), ] - execute_filter_test(hass, tests, handler_method, write_api, get_mock_call) + await execute_filter_test(hass, tests, write_api, get_mock_call) @pytest.mark.parametrize( @@ -749,14 +705,14 @@ async def test_event_listener_allowlist( """Test the event listener against an allowlist.""" config = {"include": {"entities": ["fake.included"]}, "exclude": {}} config.update(config_ext) - handler_method = await _setup(hass, mock_client, config, get_write_api) + await _setup(hass, mock_client, config, get_write_api) write_api = get_write_api(mock_client) tests = [ FilterTest("fake.included", True), FilterTest("fake.excluded", False), ] - execute_filter_test(hass, tests, handler_method, write_api, get_mock_call) + await execute_filter_test(hass, tests, write_api, get_mock_call) @pytest.mark.parametrize( @@ -783,14 +739,14 @@ async def test_event_listener_allowlist_domain( """Test the event listener against a domain allowlist.""" config = {"include": {"domains": ["fake"]}, "exclude": {}} config.update(config_ext) - handler_method = await _setup(hass, mock_client, config, get_write_api) + await _setup(hass, mock_client, config, get_write_api) write_api = get_write_api(mock_client) tests = [ FilterTest("fake.ok", True), FilterTest("another_fake.excluded", False), ] - execute_filter_test(hass, tests, handler_method, write_api, get_mock_call) + await execute_filter_test(hass, tests, write_api, get_mock_call) @pytest.mark.parametrize( @@ -817,14 +773,14 @@ async def test_event_listener_allowlist_glob( """Test the event listener against a glob allowlist.""" config = {"include": {"entity_globs": ["*.included_*"]}, "exclude": {}} config.update(config_ext) - handler_method = await _setup(hass, mock_client, config, get_write_api) + await _setup(hass, mock_client, config, get_write_api) write_api = get_write_api(mock_client) tests = [ FilterTest("fake.included_entity", True), FilterTest("fake.denied", False), ] - execute_filter_test(hass, tests, handler_method, write_api, get_mock_call) + await execute_filter_test(hass, tests, write_api, get_mock_call) @pytest.mark.parametrize( @@ -862,7 +818,7 @@ async def test_event_listener_filtered_allowlist( }, } config.update(config_ext) - handler_method = await _setup(hass, mock_client, config, get_write_api) + await _setup(hass, mock_client, config, get_write_api) write_api = get_write_api(mock_client) tests = [ @@ -874,7 +830,7 @@ async def test_event_listener_filtered_allowlist( FilterTest("fake.excluded_entity", False), FilterTest("another_fake.included_entity", True), ] - execute_filter_test(hass, tests, handler_method, write_api, get_mock_call) + await execute_filter_test(hass, tests, write_api, get_mock_call) @pytest.mark.parametrize( @@ -904,7 +860,7 @@ async def test_event_listener_filtered_denylist( "exclude": {"domains": ["another_fake"], "entity_globs": "*.excluded_*"}, } config.update(config_ext) - handler_method = await _setup(hass, mock_client, config, get_write_api) + await _setup(hass, mock_client, config, get_write_api) write_api = get_write_api(mock_client) tests = [ @@ -914,7 +870,7 @@ async def test_event_listener_filtered_denylist( FilterTest("another_fake.denied", False), FilterTest("fake.excluded_entity", False), ] - execute_filter_test(hass, tests, handler_method, write_api, get_mock_call) + await execute_filter_test(hass, tests, write_api, get_mock_call) @pytest.mark.parametrize( @@ -939,7 +895,7 @@ async def test_event_listener_invalid_type( hass: HomeAssistant, mock_client, config_ext, get_write_api, get_mock_call ) -> None: """Test the event listener when an attribute has an invalid type.""" - handler_method = await _setup(hass, mock_client, config_ext, get_write_api) + await _setup(hass, mock_client, config_ext, get_write_api) # map of HA State to valid influxdb [state, value] fields valid = { @@ -957,19 +913,11 @@ async def test_event_listener_invalid_type( "latitude": "2.2", "invalid_attribute": ["value1", "value2"], } - state = MagicMock( - state=in_, - domain="fake", - entity_id="fake.entity-id", - object_id="entity", - attributes=attrs, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) body = [ { "measurement": "foobars", - "tags": {"domain": "fake", "entity_id": "entity"}, - "time": 12345, + "tags": {"domain": "fake", "entity_id": "entity_id"}, + "time": ANY, "fields": { "longitude": 1.1, "latitude": 2.2, @@ -982,7 +930,8 @@ async def test_event_listener_invalid_type( if out[1] is not None: body[0]["fields"]["value"] = out[1] - handler_method(event) + hass.states.async_set("fake.entity_id", in_, attrs) + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() write_api = get_write_api(mock_client) @@ -1015,25 +964,17 @@ async def test_event_listener_default_measurement( """Test the event listener with a default measurement.""" config = {"default_measurement": "state"} config.update(config_ext) - handler_method = await _setup(hass, mock_client, config, get_write_api) - - state = MagicMock( - state=1, - domain="fake", - entity_id="fake.ok", - object_id="ok", - attributes={}, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) + await _setup(hass, mock_client, config, get_write_api) body = [ { "measurement": "state", "tags": {"domain": "fake", "entity_id": "ok"}, - "time": 12345, + "time": ANY, "fields": {"value": 1}, } ] - handler_method(event) + hass.states.async_set("fake.ok", 1) + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() write_api = get_write_api(mock_client) @@ -1065,26 +1006,19 @@ async def test_event_listener_unit_of_measurement_field( """Test the event listener for unit of measurement field.""" config = {"override_measurement": "state"} config.update(config_ext) - handler_method = await _setup(hass, mock_client, config, get_write_api) + await _setup(hass, mock_client, config, get_write_api) attrs = {"unit_of_measurement": "foobars"} - state = MagicMock( - state="foo", - domain="fake", - entity_id="fake.entity-id", - object_id="entity", - attributes=attrs, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) body = [ { "measurement": "state", - "tags": {"domain": "fake", "entity_id": "entity"}, - "time": 12345, + "tags": {"domain": "fake", "entity_id": "entity_id"}, + "time": ANY, "fields": {"state": "foo", "unit_of_measurement_str": "foobars"}, } ] - handler_method(event) + hass.states.async_set("fake.entity_id", "foo", attrs) + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() write_api = get_write_api(mock_client) @@ -1116,17 +1050,9 @@ async def test_event_listener_tags_attributes( """Test the event listener when some attributes should be tags.""" config = {"tags_attributes": ["friendly_fake"]} config.update(config_ext) - handler_method = await _setup(hass, mock_client, config, get_write_api) + await _setup(hass, mock_client, config, get_write_api) attrs = {"friendly_fake": "tag_str", "field_fake": "field_str"} - state = MagicMock( - state=1, - domain="fake", - entity_id="fake.something", - object_id="something", - attributes=attrs, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) body = [ { "measurement": "fake.something", @@ -1135,11 +1061,12 @@ async def test_event_listener_tags_attributes( "entity_id": "something", "friendly_fake": "tag_str", }, - "time": 12345, + "time": ANY, "fields": {"value": 1, "field_fake_str": "field_str"}, } ] - handler_method(event) + hass.states.async_set("fake.something", 1, attrs) + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() write_api = get_write_api(mock_client) @@ -1179,7 +1106,7 @@ async def test_event_listener_component_override_measurement( "component_config_domain": {"climate": {"override_measurement": "hvac"}}, } config.update(config_ext) - handler_method = await _setup(hass, mock_client, config, get_write_api) + await _setup(hass, mock_client, config, get_write_api) test_components = [ {"domain": "sensor", "id": "fake_humidity", "res": "humidity"}, @@ -1188,23 +1115,16 @@ async def test_event_listener_component_override_measurement( {"domain": "other", "id": "just_fake", "res": "other.just_fake"}, ] for comp in test_components: - state = MagicMock( - state=1, - domain=comp["domain"], - entity_id=f"{comp['domain']}.{comp['id']}", - object_id=comp["id"], - attributes={}, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) body = [ { "measurement": comp["res"], "tags": {"domain": comp["domain"], "entity_id": comp["id"]}, - "time": 12345, + "time": ANY, "fields": {"value": 1}, } ] - handler_method(event) + hass.states.async_set(f"{comp['domain']}.{comp['id']}", 1) + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() write_api = get_write_api(mock_client) @@ -1246,7 +1166,7 @@ async def test_event_listener_component_measurement_attr( "component_config_domain": {"climate": {"override_measurement": "hvac"}}, } config.update(config_ext) - handler_method = await _setup(hass, mock_client, config, get_write_api) + await _setup(hass, mock_client, config, get_write_api) test_components = [ { @@ -1261,23 +1181,16 @@ async def test_event_listener_component_measurement_attr( {"domain": "other", "id": "just_fake", "attrs": {}, "res": "other"}, ] for comp in test_components: - state = MagicMock( - state=1, - domain=comp["domain"], - entity_id=f"{comp['domain']}.{comp['id']}", - object_id=comp["id"], - attributes=comp["attrs"], - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) body = [ { "measurement": comp["res"], "tags": {"domain": comp["domain"], "entity_id": comp["id"]}, - "time": 12345, + "time": ANY, "fields": {"value": 1}, } ] - handler_method(event) + hass.states.async_set(f"{comp['domain']}.{comp['id']}", 1, comp["attrs"]) + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() write_api = get_write_api(mock_client) @@ -1321,7 +1234,7 @@ async def test_event_listener_ignore_attributes( }, } config.update(config_ext) - handler_method = await _setup(hass, mock_client, config, get_write_api) + await _setup(hass, mock_client, config, get_write_api) test_components = [ { @@ -1342,30 +1255,27 @@ async def test_event_listener_ignore_attributes( ] for comp in test_components: entity_id = f"{comp['domain']}.{comp['id']}" - state = MagicMock( - state=1, - domain=comp["domain"], - entity_id=entity_id, - object_id=comp["id"], - attributes={ - "ignore": 1, - "id_ignore": 1, - "glob_ignore": 1, - "domain_ignore": 1, - }, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) fields = {"value": 1} fields.update(comp["attrs"]) body = [ { "measurement": entity_id, "tags": {"domain": comp["domain"], "entity_id": comp["id"]}, - "time": 12345, + "time": ANY, "fields": fields, } ] - handler_method(event) + hass.states.async_set( + entity_id, + 1, + { + "ignore": 1, + "id_ignore": 1, + "glob_ignore": 1, + "domain_ignore": 1, + }, + ) + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() write_api = get_write_api(mock_client) @@ -1401,25 +1311,17 @@ async def test_event_listener_ignore_attributes_overlapping_entities( "component_config_domain": {"sensor": {"ignore_attributes": ["ignore"]}}, } config.update(config_ext) - handler_method = await _setup(hass, mock_client, config, get_write_api) - - state = MagicMock( - state=1, - domain="sensor", - entity_id="sensor.fake", - object_id="fake", - attributes={"ignore": 1}, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) + await _setup(hass, mock_client, config, get_write_api) body = [ { "measurement": "units", "tags": {"domain": "sensor", "entity_id": "fake"}, - "time": 12345, + "time": ANY, "fields": {"value": 1}, } ] - handler_method(event) + hass.states.async_set("sensor.fake", 1, {"ignore": 1}) + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() write_api = get_write_api(mock_client) @@ -1452,22 +1354,14 @@ async def test_event_listener_scheduled_write( """Test the event listener retries after a write failure.""" config = {"max_retries": 1} config.update(config_ext) - handler_method = await _setup(hass, mock_client, config, get_write_api) - - state = MagicMock( - state=1, - domain="fake", - entity_id="entity.id", - object_id="entity", - attributes={}, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) + await _setup(hass, mock_client, config, get_write_api) write_api = get_write_api(mock_client) write_api.side_effect = OSError("foo") # Write fails with patch.object(influxdb.time, "sleep") as mock_sleep: - handler_method(event) + hass.states.async_set("entity.entity_id", 1) + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() assert mock_sleep.called assert write_api.call_count == 2 @@ -1475,7 +1369,8 @@ async def test_event_listener_scheduled_write( # Write works again write_api.side_effect = None with patch.object(influxdb.time, "sleep") as mock_sleep: - handler_method(event) + hass.states.async_set("entity.entity_id", "2") + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() assert not mock_sleep.called assert write_api.call_count == 3 @@ -1503,16 +1398,7 @@ async def test_event_listener_backlog_full( hass: HomeAssistant, mock_client, config_ext, get_write_api, get_mock_call ) -> None: """Test the event listener drops old events when backlog gets full.""" - handler_method = await _setup(hass, mock_client, config_ext, get_write_api) - - state = MagicMock( - state=1, - domain="fake", - entity_id="entity.id", - object_id="entity", - attributes={}, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) + await _setup(hass, mock_client, config_ext, get_write_api) monotonic_time = 0 @@ -1523,7 +1409,8 @@ def fast_monotonic(): return monotonic_time with patch("homeassistant.components.influxdb.time.monotonic", new=fast_monotonic): - handler_method(event) + hass.states.async_set("entity.id", 1) + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() assert get_write_api(mock_client).call_count == 0 @@ -1551,26 +1438,17 @@ async def test_event_listener_attribute_name_conflict( hass: HomeAssistant, mock_client, config_ext, get_write_api, get_mock_call ) -> None: """Test the event listener when an attribute conflicts with another field.""" - handler_method = await _setup(hass, mock_client, config_ext, get_write_api) - - attrs = {"value": "value_str"} - state = MagicMock( - state=1, - domain="fake", - entity_id="fake.something", - object_id="something", - attributes=attrs, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) + await _setup(hass, mock_client, config_ext, get_write_api) body = [ { "measurement": "fake.something", "tags": {"domain": "fake", "entity_id": "something"}, - "time": 12345, + "time": ANY, "fields": {"value": 1, "value__str": "value_str"}, } ] - handler_method(event) + hass.states.async_set("fake.something", 1, {"value": "value_str"}) + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() write_api = get_write_api(mock_client) @@ -1642,7 +1520,6 @@ async def test_connection_failure_on_startup( == 1 ) event_helper.call_later.assert_called_once() - hass.bus.listen.assert_not_called() @pytest.mark.parametrize( @@ -1686,21 +1563,14 @@ async def test_invalid_inputs_error( But Influx is an external service so there may be edge cases that haven't been encountered yet. """ - handler_method = await _setup(hass, mock_client, config_ext, get_write_api) + await _setup(hass, mock_client, config_ext, get_write_api) write_api = get_write_api(mock_client) write_api.side_effect = test_exception - state = MagicMock( - state=1, - domain="fake", - entity_id="fake.something", - object_id="something", - attributes={}, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) with patch(f"{INFLUX_PATH}.time.sleep") as sleep: - handler_method(event) + hass.states.async_set("fake.something", 1) + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() write_api.assert_called_once() @@ -1786,29 +1656,25 @@ async def test_precision( "precision": precision, } config.update(config_ext) - handler_method = await _setup(hass, mock_client, config, get_write_api) + await _setup(hass, mock_client, config, get_write_api) value = "1.9" - attrs = { - "unit_of_measurement": "foobars", - } - state = MagicMock( - state=value, - domain="fake", - entity_id="fake.entity-id", - object_id="entity", - attributes=attrs, - ) - event = MagicMock(data={"new_state": state}, time_fired=12345) body = [ { "measurement": "foobars", - "tags": {"domain": "fake", "entity_id": "entity"}, - "time": 12345, + "tags": {"domain": "fake", "entity_id": "entity_id"}, + "time": ANY, "fields": {"value": float(value)}, } ] - handler_method(event) + hass.states.async_set( + "fake.entity_id", + value, + { + "unit_of_measurement": "foobars", + }, + ) + await hass.async_block_till_done() hass.data[influxdb.DOMAIN].block_till_done() write_api = get_write_api(mock_client) diff --git a/tests/components/logentries/test_init.py b/tests/components/logentries/test_init.py index 0101356e3edb3e..98b171c813f931 100644 --- a/tests/components/logentries/test_init.py +++ b/tests/components/logentries/test_init.py @@ -1,10 +1,10 @@ """The tests for the Logentries component.""" -from unittest.mock import MagicMock, call, patch +from unittest.mock import ANY, call, patch import pytest import homeassistant.components.logentries as logentries -from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -12,19 +12,23 @@ async def test_setup_config_full(hass: HomeAssistant) -> None: """Test setup with all data.""" config = {"logentries": {"token": "secret"}} - hass.bus.listen = MagicMock() assert await async_setup_component(hass, logentries.DOMAIN, config) - assert hass.bus.listen.called - assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED + + with patch("homeassistant.components.logentries.requests.post") as mock_post: + hass.states.async_set("fake.entity", STATE_ON) + await hass.async_block_till_done() + assert len(mock_post.mock_calls) == 1 async def test_setup_config_defaults(hass: HomeAssistant) -> None: """Test setup with defaults.""" config = {"logentries": {"token": "token"}} - hass.bus.listen = MagicMock() assert await async_setup_component(hass, logentries.DOMAIN, config) - assert hass.bus.listen.called - assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED + + with patch("homeassistant.components.logentries.requests.post") as mock_post: + hass.states.async_set("fake.entity", STATE_ON) + await hass.async_block_till_done() + assert len(mock_post.mock_calls) == 1 @pytest.fixture @@ -47,28 +51,24 @@ async def test_event_listener(hass: HomeAssistant, mock_dump, mock_requests) -> mock_post = mock_requests.post mock_requests.exceptions.RequestException = Exception config = {"logentries": {"token": "token"}} - hass.bus.listen = MagicMock() assert await async_setup_component(hass, logentries.DOMAIN, config) - handler_method = hass.bus.listen.call_args_list[0][0][1] valid = {"1": 1, "1.0": 1.0, STATE_ON: 1, STATE_OFF: 0, "foo": "foo"} for in_, out in valid.items(): - state = MagicMock(state=in_, domain="fake", object_id="entity", attributes={}) - event = MagicMock(data={"new_state": state}, time_fired=12345) - body = [ - { - "domain": "fake", - "entity_id": "entity", - "attributes": {}, - "time": "12345", - "value": out, - } - ] payload = { "host": "https://webhook.logentries.com/noformat/logs/token", - "event": body, + "event": [ + { + "domain": "fake", + "entity_id": "entity", + "attributes": {}, + "time": ANY, + "value": out, + } + ], } - handler_method(event) + hass.states.async_set("fake.entity", in_) + await hass.async_block_till_done() assert mock_post.call_count == 1 assert mock_post.call_args == call(payload["host"], data=payload, timeout=10) mock_post.reset_mock() diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index d9231732941edd..8b0acb9c5b043a 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -44,7 +44,6 @@ CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONTENT_TYPE_TEXT_PLAIN, DEGREE, - EVENT_STATE_CHANGED, PERCENTAGE, STATE_CLOSED, STATE_CLOSING, @@ -59,7 +58,7 @@ UnitOfEnergy, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, split_entity_id +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -1568,23 +1567,13 @@ def mock_client_fixture(): yield counter_client -@pytest.fixture -def mock_bus(hass): - """Mock the event bus listener.""" - hass.bus.listen = mock.MagicMock() - - -@pytest.mark.usefixtures("mock_bus") async def test_minimal_config(hass: HomeAssistant, mock_client) -> None: """Test the minimal config and defaults of component.""" config = {prometheus.DOMAIN: {}} assert await async_setup_component(hass, prometheus.DOMAIN, config) await hass.async_block_till_done() - assert hass.bus.listen.called - assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED -@pytest.mark.usefixtures("mock_bus") async def test_full_config(hass: HomeAssistant, mock_client) -> None: """Test the full config of component.""" config = { @@ -1607,21 +1596,6 @@ async def test_full_config(hass: HomeAssistant, mock_client) -> None: } assert await async_setup_component(hass, prometheus.DOMAIN, config) await hass.async_block_till_done() - assert hass.bus.listen.called - assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED - - -def make_event(entity_id): - """Make a mock event for test.""" - domain = split_entity_id(entity_id)[0] - state = mock.MagicMock( - state="not blank", - domain=domain, - entity_id=entity_id, - object_id="entity", - attributes={}, - ) - return mock.MagicMock(data={"new_state": state}, time_fired=12345) async def _setup(hass, filter_config): @@ -1629,13 +1603,11 @@ async def _setup(hass, filter_config): config = {prometheus.DOMAIN: {"filter": filter_config}} assert await async_setup_component(hass, prometheus.DOMAIN, config) await hass.async_block_till_done() - return hass.bus.listen.call_args_list[0][0][1] -@pytest.mark.usefixtures("mock_bus") async def test_allowlist(hass: HomeAssistant, mock_client) -> None: """Test an allowlist only config.""" - handler_method = await _setup( + await _setup( hass, { "include_domains": ["fake"], @@ -1654,18 +1626,17 @@ async def test_allowlist(hass: HomeAssistant, mock_client) -> None: ] for test in tests: - event = make_event(test.id) - handler_method(event) + hass.states.async_set(test.id, "not blank") + await hass.async_block_till_done() was_called = mock_client.labels.call_count == 1 assert test.should_pass == was_called mock_client.labels.reset_mock() -@pytest.mark.usefixtures("mock_bus") async def test_denylist(hass: HomeAssistant, mock_client) -> None: """Test a denylist only config.""" - handler_method = await _setup( + await _setup( hass, { "exclude_domains": ["fake"], @@ -1684,18 +1655,17 @@ async def test_denylist(hass: HomeAssistant, mock_client) -> None: ] for test in tests: - event = make_event(test.id) - handler_method(event) + hass.states.async_set(test.id, "not blank") + await hass.async_block_till_done() was_called = mock_client.labels.call_count == 1 assert test.should_pass == was_called mock_client.labels.reset_mock() -@pytest.mark.usefixtures("mock_bus") async def test_filtered_denylist(hass: HomeAssistant, mock_client) -> None: """Test a denylist config with a filtering allowlist.""" - handler_method = await _setup( + await _setup( hass, { "include_entities": ["fake.included", "test.excluded_test"], @@ -1715,8 +1685,8 @@ async def test_filtered_denylist(hass: HomeAssistant, mock_client) -> None: ] for test in tests: - event = make_event(test.id) - handler_method(event) + hass.states.async_set(test.id, "not blank") + await hass.async_block_till_done() was_called = mock_client.labels.call_count == 1 assert test.should_pass == was_called diff --git a/tests/components/statsd/test_init.py b/tests/components/statsd/test_init.py index 2b94d8c07905a4..1b48b6195e5f9b 100644 --- a/tests/components/statsd/test_init.py +++ b/tests/components/statsd/test_init.py @@ -1,13 +1,12 @@ """The tests for the StatsD feeder.""" from unittest import mock -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest import voluptuous as vol import homeassistant.components.statsd as statsd -from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON -import homeassistant.core as ha +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -32,15 +31,15 @@ def test_invalid_config() -> None: async def test_statsd_setup_full(hass: HomeAssistant) -> None: """Test setup with all data.""" config = {"statsd": {"host": "host", "port": 123, "rate": 1, "prefix": "foo"}} - hass.bus.listen = MagicMock() with patch("statsd.StatsClient") as mock_init: assert await async_setup_component(hass, statsd.DOMAIN, config) assert mock_init.call_count == 1 assert mock_init.call_args == mock.call(host="host", port=123, prefix="foo") - assert hass.bus.listen.called - assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED + hass.states.async_set("domain.test", "on") + await hass.async_block_till_done() + assert len(mock_init.mock_calls) == 3 async def test_statsd_setup_defaults(hass: HomeAssistant) -> None: @@ -50,13 +49,14 @@ async def test_statsd_setup_defaults(hass: HomeAssistant) -> None: config["statsd"][statsd.CONF_PORT] = statsd.DEFAULT_PORT config["statsd"][statsd.CONF_PREFIX] = statsd.DEFAULT_PREFIX - hass.bus.listen = MagicMock() with patch("statsd.StatsClient") as mock_init: assert await async_setup_component(hass, statsd.DOMAIN, config) assert mock_init.call_count == 1 assert mock_init.call_args == mock.call(host="host", port=8125, prefix="hass") - assert hass.bus.listen.called + hass.states.async_set("domain.test", "on") + await hass.async_block_till_done() + assert len(mock_init.mock_calls) == 3 async def test_event_listener_defaults(hass: HomeAssistant, mock_client) -> None: @@ -65,31 +65,27 @@ async def test_event_listener_defaults(hass: HomeAssistant, mock_client) -> None config["statsd"][statsd.CONF_RATE] = statsd.DEFAULT_RATE - hass.bus.listen = MagicMock() await async_setup_component(hass, statsd.DOMAIN, config) - assert hass.bus.listen.called - handler_method = hass.bus.listen.call_args_list[0][0][1] valid = {"1": 1, "1.0": 1.0, "custom": 3, STATE_ON: 1, STATE_OFF: 0} for in_, out in valid.items(): - state = MagicMock(state=in_, attributes={"attribute key": 3.2}) - handler_method(MagicMock(data={"new_state": state})) + hass.states.async_set("domain.test", in_, {"attribute key": 3.2}) + await hass.async_block_till_done() mock_client.gauge.assert_has_calls( - [mock.call(state.entity_id, out, statsd.DEFAULT_RATE)] + [mock.call("domain.test", out, statsd.DEFAULT_RATE)] ) mock_client.gauge.reset_mock() assert mock_client.incr.call_count == 1 assert mock_client.incr.call_args == mock.call( - state.entity_id, rate=statsd.DEFAULT_RATE + "domain.test", rate=statsd.DEFAULT_RATE ) mock_client.incr.reset_mock() for invalid in ("foo", "", object): - handler_method( - MagicMock(data={"new_state": ha.State("domain.test", invalid, {})}) - ) + hass.states.async_set("domain.test", invalid, {}) + await hass.async_block_till_done() assert not mock_client.gauge.called assert mock_client.incr.called @@ -100,19 +96,16 @@ async def test_event_listener_attr_details(hass: HomeAssistant, mock_client) -> config["statsd"][statsd.CONF_RATE] = statsd.DEFAULT_RATE - hass.bus.listen = MagicMock() await async_setup_component(hass, statsd.DOMAIN, config) - assert hass.bus.listen.called - handler_method = hass.bus.listen.call_args_list[0][0][1] valid = {"1": 1, "1.0": 1.0, STATE_ON: 1, STATE_OFF: 0} for in_, out in valid.items(): - state = MagicMock(state=in_, attributes={"attribute key": 3.2}) - handler_method(MagicMock(data={"new_state": state})) + hass.states.async_set("domain.test", in_, {"attribute key": 3.2}) + await hass.async_block_till_done() mock_client.gauge.assert_has_calls( [ - mock.call(f"{state.entity_id}.state", out, statsd.DEFAULT_RATE), - mock.call(f"{state.entity_id}.attribute_key", 3.2, statsd.DEFAULT_RATE), + mock.call("domain.test.state", out, statsd.DEFAULT_RATE), + mock.call("domain.test.attribute_key", 3.2, statsd.DEFAULT_RATE), ] ) @@ -120,13 +113,12 @@ async def test_event_listener_attr_details(hass: HomeAssistant, mock_client) -> assert mock_client.incr.call_count == 1 assert mock_client.incr.call_args == mock.call( - state.entity_id, rate=statsd.DEFAULT_RATE + "domain.test", rate=statsd.DEFAULT_RATE ) mock_client.incr.reset_mock() for invalid in ("foo", "", object): - handler_method( - MagicMock(data={"new_state": ha.State("domain.test", invalid, {})}) - ) + hass.states.async_set("domain.test", invalid, {}) + await hass.async_block_till_done() assert not mock_client.gauge.called assert mock_client.incr.called From 0e428f8d399583e1ede577f01dfaf401dbbdd877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Moreno?= Date: Wed, 5 Jul 2023 21:12:21 +0200 Subject: [PATCH 0177/1009] Deprecate Dry and Fan preset modes in favor of HVAC modes (#95634) * zwave_js: deprecate Dry and Fan preset modes Migrating Dry and Fan presets to HVAC modes * Move consts. Set Dry and Fan as HVAC-first modes. * Update homeassistant/components/zwave_js/climate.py Co-authored-by: Raman Gupta <7243222+raman325@users.noreply.github.com> * Fix tests * Keep track of HA release when deprecation was introduced --------- Co-authored-by: Raman Gupta <7243222+raman325@users.noreply.github.com> --- homeassistant/components/zwave_js/climate.py | 49 +- tests/components/zwave_js/common.py | 1 + tests/components/zwave_js/conftest.py | 18 + ...airzone_aidoo_control_hvac_unit_state.json | 818 ++++++++++++++++++ tests/components/zwave_js/test_climate.py | 80 ++ 5 files changed, 963 insertions(+), 3 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/climate_airzone_aidoo_control_hvac_unit_state.json diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 82c212a99a5291..f38508ec09c780 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -9,8 +9,6 @@ THERMOSTAT_CURRENT_TEMP_PROPERTY, THERMOSTAT_HUMIDITY_PROPERTY, THERMOSTAT_MODE_PROPERTY, - THERMOSTAT_MODE_SETPOINT_MAP, - THERMOSTAT_MODES, THERMOSTAT_OPERATING_STATE_PROPERTY, THERMOSTAT_SETPOINT_PROPERTY, ThermostatMode, @@ -40,7 +38,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.unit_conversion import TemperatureConverter -from .const import DATA_CLIENT, DOMAIN +from .const import DATA_CLIENT, DOMAIN, LOGGER from .discovery import ZwaveDiscoveryInfo from .discovery_data_template import DynamicCurrentTempClimateDataTemplate from .entity import ZWaveBaseEntity @@ -48,6 +46,38 @@ PARALLEL_UPDATES = 0 +THERMOSTAT_MODES = [ + ThermostatMode.OFF, + ThermostatMode.HEAT, + ThermostatMode.COOL, + ThermostatMode.AUTO, + ThermostatMode.AUTO_CHANGE_OVER, + ThermostatMode.FAN, + ThermostatMode.DRY, +] + +THERMOSTAT_MODE_SETPOINT_MAP: dict[int, list[ThermostatSetpointType]] = { + ThermostatMode.OFF: [], + ThermostatMode.HEAT: [ThermostatSetpointType.HEATING], + ThermostatMode.COOL: [ThermostatSetpointType.COOLING], + ThermostatMode.AUTO: [ + ThermostatSetpointType.HEATING, + ThermostatSetpointType.COOLING, + ], + ThermostatMode.AUXILIARY: [ThermostatSetpointType.HEATING], + ThermostatMode.FURNACE: [ThermostatSetpointType.FURNACE], + ThermostatMode.DRY: [ThermostatSetpointType.DRY_AIR], + ThermostatMode.MOIST: [ThermostatSetpointType.MOIST_AIR], + ThermostatMode.AUTO_CHANGE_OVER: [ThermostatSetpointType.AUTO_CHANGEOVER], + ThermostatMode.HEATING_ECON: [ThermostatSetpointType.ENERGY_SAVE_HEATING], + ThermostatMode.COOLING_ECON: [ThermostatSetpointType.ENERGY_SAVE_COOLING], + ThermostatMode.AWAY: [ + ThermostatSetpointType.AWAY_HEATING, + ThermostatSetpointType.AWAY_COOLING, + ], + ThermostatMode.FULL_POWER: [ThermostatSetpointType.FULL_POWER], +} + # Map Z-Wave HVAC Mode to Home Assistant value # Note: We treat "auto" as "heat_cool" as most Z-Wave devices # report auto_changeover as auto without schedule support. @@ -233,9 +263,15 @@ def _set_modes_and_presets(self) -> None: # treat value as hvac mode if hass_mode := ZW_HVAC_MODE_MAP.get(mode_id): all_modes[hass_mode] = mode_id + # Dry and Fan modes are in the process of being migrated from + # presets to hvac modes. In the meantime, we will set them as + # both, presets and hvac modes, to maintain backwards compatibility + if mode_id in (ThermostatMode.DRY, ThermostatMode.FAN): + all_presets[mode_name] = mode_id else: # treat value as hvac preset all_presets[mode_name] = mode_id + self._hvac_modes = all_modes self._hvac_presets = all_presets @@ -487,6 +523,13 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: preset_mode_value = self._hvac_presets.get(preset_mode) if preset_mode_value is None: raise ValueError(f"Received an invalid preset mode: {preset_mode}") + # Dry and Fan preset modes are deprecated as of 2023.8 + # Use Dry and Fan HVAC modes instead + if preset_mode_value in (ThermostatMode.DRY, ThermostatMode.FAN): + LOGGER.warning( + "Dry and Fan preset modes are deprecated and will be removed in a future release. " + "Use the corresponding Dry and Fan HVAC modes instead" + ) await self._async_set_value(self._current_mode, preset_mode_value) diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index 3da63419a4b171..606dda30b243c0 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -35,6 +35,7 @@ CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY = "climate.thermostatic_valve" CLIMATE_FLOOR_THERMOSTAT_ENTITY = "climate.floor_thermostat" CLIMATE_MAIN_HEAT_ACTIONNER = "climate.main_heat_actionner" +CLIMATE_AIDOO_HVAC_UNIT_ENTITY = "climate.aidoo_control_hvac_unit" BULB_6_MULTI_COLOR_LIGHT_ENTITY = "light.bulb_6_multi_color" EATON_RF9640_ENTITY = "light.allloaddimmer" AEON_SMART_SWITCH_LIGHT_ENTITY = "light.smart_switch_6" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 68484111802332..0eb4ec775f932a 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -365,6 +365,14 @@ def climate_adc_t3000_state_fixture(): return json.loads(load_fixture("zwave_js/climate_adc_t3000_state.json")) +@pytest.fixture(name="climate_airzone_aidoo_control_hvac_unit_state", scope="session") +def climate_airzone_aidoo_control_hvac_unit_state_fixture(): + """Load the climate Airzone Aidoo Control HVAC Unit state fixture data.""" + return json.loads( + load_fixture("zwave_js/climate_airzone_aidoo_control_hvac_unit_state.json") + ) + + @pytest.fixture(name="climate_danfoss_lc_13_state", scope="session") def climate_danfoss_lc_13_state_fixture(): """Load Danfoss (LC-13) electronic radiator thermostat node state fixture data.""" @@ -826,6 +834,16 @@ def climate_adc_t3000_missing_fan_mode_states_fixture(client, climate_adc_t3000_ return node +@pytest.fixture(name="climate_airzone_aidoo_control_hvac_unit") +def climate_airzone_aidoo_control_hvac_unit_fixture( + client, climate_airzone_aidoo_control_hvac_unit_state +): + """Mock a climate Airzone Aidoo Control HVAC node.""" + node = Node(client, copy.deepcopy(climate_airzone_aidoo_control_hvac_unit_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="climate_danfoss_lc_13") def climate_danfoss_lc_13_fixture(client, climate_danfoss_lc_13_state): """Mock a climate radio danfoss LC-13 node.""" diff --git a/tests/components/zwave_js/fixtures/climate_airzone_aidoo_control_hvac_unit_state.json b/tests/components/zwave_js/fixtures/climate_airzone_aidoo_control_hvac_unit_state.json new file mode 100644 index 00000000000000..b5afa1131a8754 --- /dev/null +++ b/tests/components/zwave_js/fixtures/climate_airzone_aidoo_control_hvac_unit_state.json @@ -0,0 +1,818 @@ +{ + "nodeId": 12, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 1102, + "productId": 1, + "productType": 4, + "firmwareVersion": "10.20.1", + "zwavePlusVersion": 2, + "deviceConfig": { + "filename": "/data/db/devices/0x044e/AZAI6WSPFU2.json", + "isEmbedded": true, + "manufacturer": "Airzone", + "manufacturerId": 1102, + "label": "AZAI6WSPFU2", + "description": "Aidoo Control HVAC unit", + "devices": [ + { + "productType": 4, + "productId": 1 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "preferred": false, + "compat": { + "overrideFloatEncoding": { + "precision": 1 + } + } + }, + "label": "AZAI6WSPFU2", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 12, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 3, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 2, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 3, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 5, + "isSecure": false + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + } + ] + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + }, + "unit": "\u00b0C", + "stateful": true, + "secret": false + }, + "value": 29 + }, + { + "endpoint": 0, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Thermostat mode", + "min": 0, + "max": 255, + "states": { + "0": "Off", + "1": "Heat", + "2": "Cool", + "6": "Fan", + "8": "Dry", + "10": "Auto changeover" + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "manufacturerData", + "propertyName": "manufacturerData", + "ccVersion": 3, + "metadata": { + "type": "buffer", + "readable": true, + "writeable": false, + "label": "Manufacturer data", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 1, + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Setpoint (Heating)", + "ccSpecific": { + "setpointType": 1 + }, + "unit": "\u00b0C", + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 2, + "propertyName": "setpoint", + "propertyKeyName": "Cooling", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Setpoint (Cooling)", + "ccSpecific": { + "setpointType": 2 + }, + "unit": "\u00b0C", + "stateful": true, + "secret": false + }, + "value": 23 + }, + { + "endpoint": 0, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 8, + "propertyName": "setpoint", + "propertyKeyName": "Dry Air", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Setpoint (Dry Air)", + "ccSpecific": { + "setpointType": 8 + }, + "unit": "\u00b0C", + "stateful": true, + "secret": false + }, + "value": 23 + }, + { + "endpoint": 0, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 10, + "propertyName": "setpoint", + "propertyKeyName": "Auto Changeover", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Setpoint (Auto Changeover)", + "ccSpecific": { + "setpointType": 10 + }, + "unit": "\u00b0C", + "stateful": true, + "secret": false + }, + "value": 32 + }, + { + "endpoint": 0, + "commandClass": 68, + "commandClassName": "Thermostat Fan Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Thermostat fan mode", + "min": 0, + "max": 255, + "states": { + "1": "Low", + "3": "High", + "4": "Auto medium", + "5": "Medium" + }, + "stateful": true, + "secret": false + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "System", + "propertyKey": "Hardware status", + "propertyName": "System", + "propertyKeyName": "Hardware status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Hardware status", + "ccSpecific": { + "notificationType": 9 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "3": "System hardware failure (with failure code)" + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 1102 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + }, + "stateful": true, + "secret": false + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.16" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions", + "stateful": true, + "secret": false + }, + "value": ["10.20"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version", + "stateful": true, + "secret": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "SDK version", + "stateful": true, + "secret": false + }, + "value": "7.16.3" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API version", + "stateful": true, + "secret": false + }, + "value": "10.16.3" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API build number", + "stateful": true, + "secret": false + }, + "value": 297 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API version", + "stateful": true, + "secret": false + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API build number", + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.16.3" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol build number", + "stateful": true, + "secret": false + }, + "value": 297 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application version", + "stateful": true, + "secret": false + }, + "value": "10.20.1" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application build number", + "stateful": true, + "secret": false + }, + "value": 43707 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "value", + "propertyName": "value", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Indicator value", + "ccSpecific": { + "indicatorId": 0 + }, + "min": 0, + "max": 255, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "identify", + "propertyName": "identify", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Identify", + "states": { + "true": "Identify" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "timeout", + "propertyName": "timeout", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "Timeout", + "stateful": true, + "secret": false + } + } + ], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x044e:0x0004:0x0001:10.20.1", + "statistics": { + "commandsTX": 69, + "commandsRX": 497, + "commandsDroppedRX": 0, + "commandsDroppedTX": 2, + "timeoutResponse": 0, + "rtt": 81 + }, + "highestSecurityClass": -1, + "isControllerNode": false, + "keepAwake": false +} diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index d3f38aaa3074bd..753c107c2ee072 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -18,6 +18,7 @@ ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_PRESET_MODE, + ATTR_PRESET_MODES, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN as CLIMATE_DOMAIN, @@ -41,6 +42,7 @@ from homeassistant.core import HomeAssistant from .common import ( + CLIMATE_AIDOO_HVAC_UNIT_ENTITY, CLIMATE_DANFOSS_LC13_ENTITY, CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY, CLIMATE_FLOOR_THERMOSTAT_ENTITY, @@ -694,3 +696,81 @@ async def test_thermostat_unknown_values( state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY) assert ATTR_HVAC_ACTION not in state.attributes + + +async def test_thermostat_dry_and_fan_both_hvac_mode_and_preset( + hass: HomeAssistant, + client, + climate_airzone_aidoo_control_hvac_unit, + integration, +) -> None: + """Test that dry and fan modes are both available as hvac mode and preset.""" + state = hass.states.get(CLIMATE_AIDOO_HVAC_UNIT_ENTITY) + assert state + assert state.attributes[ATTR_HVAC_MODES] == [ + HVACMode.OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.FAN_ONLY, + HVACMode.DRY, + HVACMode.HEAT_COOL, + ] + assert state.attributes[ATTR_PRESET_MODES] == [ + PRESET_NONE, + "Fan", + "Dry", + ] + + +async def test_thermostat_warning_when_setting_dry_preset( + hass: HomeAssistant, + client, + climate_airzone_aidoo_control_hvac_unit, + integration, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test warning when setting Dry preset.""" + state = hass.states.get(CLIMATE_AIDOO_HVAC_UNIT_ENTITY) + assert state + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + { + ATTR_ENTITY_ID: CLIMATE_AIDOO_HVAC_UNIT_ENTITY, + ATTR_PRESET_MODE: "Dry", + }, + blocking=True, + ) + + assert ( + "Dry and Fan preset modes are deprecated and will be removed in a future release. Use the corresponding Dry and Fan HVAC modes instead" + in caplog.text + ) + + +async def test_thermostat_warning_when_setting_fan_preset( + hass: HomeAssistant, + client, + climate_airzone_aidoo_control_hvac_unit, + integration, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test warning when setting Fan preset.""" + state = hass.states.get(CLIMATE_AIDOO_HVAC_UNIT_ENTITY) + assert state + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + { + ATTR_ENTITY_ID: CLIMATE_AIDOO_HVAC_UNIT_ENTITY, + ATTR_PRESET_MODE: "Fan", + }, + blocking=True, + ) + + assert ( + "Dry and Fan preset modes are deprecated and will be removed in a future release. Use the corresponding Dry and Fan HVAC modes instead" + in caplog.text + ) From 186295ef8a41e36673f1aaf1f3f0bbde1b6503a9 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 5 Jul 2023 22:27:03 +0200 Subject: [PATCH 0178/1009] Correct spelling roborock strings (#95919) --- homeassistant/components/roborock/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 1cd95914808b8e..e595b7abff4e8a 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -142,8 +142,8 @@ }, "issues": { "service_deprecation_start_pause": { - "title": "Roborock vaccum support for vacuum.start_pause is being removed", - "description": "Roborock vaccum support for the vacuum.start_pause service is deprecated and will be removed in Home Assistant 2024.2; Please adjust any automation or script that uses the service to instead call vacuum.pause or vacuum.start and select submit below to mark this issue as resolved." + "title": "Roborock vacuum support for vacuum.start_pause is being removed", + "description": "Roborock vacuum support for the vacuum.start_pause service is deprecated and will be removed in Home Assistant 2024.2; Please adjust any automation or script that uses the service to instead call vacuum.pause or vacuum.start and select submit below to mark this issue as resolved." } } } From cb7fa494a432dbcf004b5ffd6e3027cef3c9ed11 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Jul 2023 18:56:09 -0500 Subject: [PATCH 0179/1009] Make SwitchBot no_devices_found message more helpful (#95916) --- homeassistant/components/switchbot/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index 2d31a883e4b2d1..fb9f906527cd1e 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -44,7 +44,7 @@ }, "abort": { "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", - "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "no_devices_found": "No supported SwitchBot devices found in range; If the device is in range, ensure the scanner has active scanning enabled, as SwitchBot devices cannot be discovered with passive scans. Active scans can be disabled once the device is configured. If you need clarification on whether the device is in-range, download the diagnostics for the integration that provides your Bluetooth adapter or proxy and check if the MAC address of the SwitchBot device is present.", "unknown": "[%key:common::config_flow::error::unknown%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "switchbot_unsupported_type": "Unsupported Switchbot Type." From af1cb7be58dde77b1cd4cdb1e1f9832bc2fb7f3a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 6 Jul 2023 08:49:59 +0200 Subject: [PATCH 0180/1009] Migrate from deprecated VacuumEntity to StateVacuumEntity in Ecovacs (#95920) * migrate to StateVacuumEntity * harmoize supported features start and stop * apply suggestions --- homeassistant/components/ecovacs/vacuum.py | 118 +++++++++++---------- 1 file changed, 60 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index f1bf7deb502686..ba922a30b84fa6 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -6,7 +6,15 @@ import sucks -from homeassistant.components.vacuum import VacuumEntity, VacuumEntityFeature +from homeassistant.components.vacuum import ( + STATE_CLEANING, + STATE_DOCKED, + STATE_ERROR, + STATE_IDLE, + STATE_RETURNING, + StateVacuumEntity, + VacuumEntityFeature, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level @@ -34,7 +42,7 @@ def setup_platform( add_entities(vacuums, True) -class EcovacsVacuum(VacuumEntity): +class EcovacsVacuum(StateVacuumEntity): """Ecovacs Vacuums such as Deebot.""" _attr_fan_speed_list = [sucks.FAN_SPEED_NORMAL, sucks.FAN_SPEED_HIGH] @@ -44,10 +52,9 @@ class EcovacsVacuum(VacuumEntity): | VacuumEntityFeature.RETURN_HOME | VacuumEntityFeature.CLEAN_SPOT | VacuumEntityFeature.STOP - | VacuumEntityFeature.TURN_OFF - | VacuumEntityFeature.TURN_ON + | VacuumEntityFeature.START | VacuumEntityFeature.LOCATE - | VacuumEntityFeature.STATUS + | VacuumEntityFeature.STATE | VacuumEntityFeature.SEND_COMMAND | VacuumEntityFeature.FAN_SPEED ) @@ -56,14 +63,13 @@ def __init__(self, device: sucks.VacBot) -> None: """Initialize the Ecovacs Vacuum.""" self.device = device self.device.connect_and_wait_until_ready() - if self.device.vacuum.get("nick") is not None: - self._attr_name = str(self.device.vacuum["nick"]) - else: - # In case there is no nickname defined, use the device id - self._attr_name = str(format(self.device.vacuum["did"])) + vacuum = self.device.vacuum + + self.error = None + self._attr_unique_id = vacuum["did"] + self._attr_name = vacuum.get("nick", vacuum["did"]) - self._error = None - _LOGGER.debug("Vacuum initialized: %s", self.name) + _LOGGER.debug("StateVacuum initialized: %s", self.name) async def async_added_to_hass(self) -> None: """Set up the event listeners now that hass is ready.""" @@ -79,9 +85,9 @@ def on_error(self, error): to change, that will come through as a separate on_status event """ if error == "no_error": - self._error = None + self.error = None else: - self._error = error + self.error = error self.hass.bus.fire( "ecovacs_error", {"entity_id": self.entity_id, "error": error} @@ -89,36 +95,24 @@ def on_error(self, error): self.schedule_update_ha_state() @property - def unique_id(self) -> str: - """Return an unique ID.""" - return self.device.vacuum.get("did") - - @property - def is_on(self) -> bool: - """Return true if vacuum is currently cleaning.""" - return self.device.is_cleaning + def state(self) -> str | None: + """Return the state of the vacuum cleaner.""" + if self.error is not None: + return STATE_ERROR - @property - def is_charging(self) -> bool: - """Return true if vacuum is currently charging.""" - return self.device.is_charging + if self.device.is_cleaning: + return STATE_CLEANING - @property - def status(self) -> str | None: - """Return the status of the vacuum cleaner.""" - return self.device.vacuum_status + if self.device.is_charging: + return STATE_DOCKED - def return_to_base(self, **kwargs: Any) -> None: - """Set the vacuum cleaner to return to the dock.""" + if self.device.vacuum_status == sucks.CLEAN_MODE_STOP: + return STATE_IDLE - self.device.run(sucks.Charge()) + if self.device.vacuum_status == sucks.CHARGE_MODE_RETURNING: + return STATE_RETURNING - @property - def battery_icon(self) -> str: - """Return the battery icon for the vacuum cleaner.""" - return icon_for_battery_level( - battery_level=self.battery_level, charging=self.is_charging - ) + return None @property def battery_level(self) -> int | None: @@ -126,22 +120,42 @@ def battery_level(self) -> int | None: if self.device.battery_status is not None: return self.device.battery_status * 100 - return super().battery_level + return None + + @property + def battery_icon(self) -> str: + """Return the battery icon for the vacuum cleaner.""" + return icon_for_battery_level( + battery_level=self.battery_level, charging=self.device.is_charging + ) @property def fan_speed(self) -> str | None: """Return the fan speed of the vacuum cleaner.""" return self.device.fan_speed - def turn_on(self, **kwargs: Any) -> None: + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the device-specific state attributes of this vacuum.""" + data: dict[str, Any] = {} + data[ATTR_ERROR] = self.error + + for key, val in self.device.components.items(): + attr_name = ATTR_COMPONENT_PREFIX + key + data[attr_name] = int(val * 100) + + return data + + def return_to_base(self, **kwargs: Any) -> None: + """Set the vacuum cleaner to return to the dock.""" + + self.device.run(sucks.Charge()) + + def start(self, **kwargs: Any) -> None: """Turn the vacuum on and start cleaning.""" self.device.run(sucks.Clean()) - def turn_off(self, **kwargs: Any) -> None: - """Turn the vacuum off stopping the cleaning and returning home.""" - self.return_to_base() - def stop(self, **kwargs: Any) -> None: """Stop the vacuum cleaner.""" @@ -159,7 +173,7 @@ def locate(self, **kwargs: Any) -> None: def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set fan speed.""" - if self.is_on: + if self.state == STATE_CLEANING: self.device.run(sucks.Clean(mode=self.device.clean_status, speed=fan_speed)) def send_command( @@ -170,15 +184,3 @@ def send_command( ) -> None: """Send a command to a vacuum cleaner.""" self.device.run(sucks.VacBotCommand(command, params)) - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the device-specific state attributes of this vacuum.""" - data: dict[str, Any] = {} - data[ATTR_ERROR] = self._error - - for key, val in self.device.components.items(): - attr_name = ATTR_COMPONENT_PREFIX + key - data[attr_name] = int(val * 100) - - return data From 8a4085011dce41a73d68b1f956c88202c8c0e5fb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 6 Jul 2023 09:02:32 +0200 Subject: [PATCH 0181/1009] Add missing qnap translation (#95969) --- homeassistant/components/qnap/strings.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/qnap/strings.json b/homeassistant/components/qnap/strings.json index 26ca5dedd34422..36946b81c0ca18 100644 --- a/homeassistant/components/qnap/strings.json +++ b/homeassistant/components/qnap/strings.json @@ -19,5 +19,11 @@ "invalid_auth": "Bad authentication", "unknown": "Unknown error" } + }, + "issues": { + "deprecated_yaml": { + "title": "The QNAP YAML configuration is being removed", + "description": "Configuring QNAP using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the QNAP YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } From 85e4454d450a1ef7d14c62e8352e9a8672a7dd96 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 6 Jul 2023 01:26:10 -0700 Subject: [PATCH 0182/1009] Bump pyrainbird to 2.1.0 (#95968) --- homeassistant/components/rainbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 2216d060f29fc0..a44cfb3ce138ff 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/rainbird", "iot_class": "local_polling", "loggers": ["pyrainbird"], - "requirements": ["pyrainbird==2.0.0"] + "requirements": ["pyrainbird==2.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5b100318336bf1..3fe51080f2813a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1941,7 +1941,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 70b6da6bb7100b..c33cf3f4c99d9d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1442,7 +1442,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 de24860c87b2042fdba548367d6d70dc5f9a38d0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 6 Jul 2023 11:13:43 +0200 Subject: [PATCH 0183/1009] Add filters to calendar/services.yaml (#95853) --- homeassistant/components/calendar/services.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/calendar/services.yaml b/homeassistant/components/calendar/services.yaml index af69882bba5a19..1f4d6aa3152a65 100644 --- a/homeassistant/components/calendar/services.yaml +++ b/homeassistant/components/calendar/services.yaml @@ -4,6 +4,8 @@ create_event: target: entity: domain: calendar + supported_features: + - calendar.CalendarEntityFeature.CREATE_EVENT fields: summary: name: Summary From be01eb5aad13ba26225e4340d4eb9ad8e8fb3c88 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 6 Jul 2023 13:25:34 +0200 Subject: [PATCH 0184/1009] Explicitly use device name as entity name for Xiaomi fan and humidifier (#95986) --- homeassistant/components/xiaomi_miio/fan.py | 2 ++ homeassistant/components/xiaomi_miio/humidifier.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 247b91d1b06401..a3bb28e7a8b7a1 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -292,6 +292,8 @@ async def async_service_handler(service: ServiceCall) -> None: class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): """Representation of a generic Xiaomi device.""" + _attr_name = None + def __init__(self, device, entry, unique_id, coordinator): """Initialize the generic Xiaomi device.""" super().__init__(device, entry, unique_id, coordinator) diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index 82ede87848e7eb..0438b606efd9c1 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -118,6 +118,7 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): _attr_device_class = HumidifierDeviceClass.HUMIDIFIER _attr_supported_features = HumidifierEntityFeature.MODES + _attr_name = None def __init__(self, device, entry, unique_id, coordinator): """Initialize the generic Xiaomi device.""" From 63cb50977be32cb35c275f6212ea803139381a4d Mon Sep 17 00:00:00 2001 From: neocolis Date: Thu, 6 Jul 2023 08:50:51 -0400 Subject: [PATCH 0185/1009] Fix matter exception NoneType in set_brightness for optional min/max level values (#95949) Fix exception NoneType in set_brightness for optional min/max level values --- homeassistant/components/matter/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index facdb6752d3b0a..02919baa8f1a4b 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -128,7 +128,7 @@ async def _set_brightness(self, brightness: int) -> None: renormalize( brightness, (0, 255), - (level_control.minLevel, level_control.maxLevel), + (level_control.minLevel or 1, level_control.maxLevel or 254), ) ) @@ -220,7 +220,7 @@ def _get_brightness(self) -> int: return round( renormalize( level_control.currentLevel, - (level_control.minLevel or 0, level_control.maxLevel or 254), + (level_control.minLevel or 1, level_control.maxLevel or 254), (0, 255), ) ) From 991ff81e4605652a9379804416b149ca8ae6fedc Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 6 Jul 2023 15:01:03 +0200 Subject: [PATCH 0186/1009] Mention automatic issue assignment in issue template (#95987) Co-authored-by: Martin Hjelmare --- .github/ISSUE_TEMPLATE/bug_report.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 237fc2888ab3e9..80291c73e6193c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -59,15 +59,15 @@ body: attributes: label: Integration causing the issue description: > - The name of the integration. For example: Automation, Philips Hue + The name of the integration, for example Automation or Philips Hue. - type: input id: integration_link attributes: label: Link to integration documentation on our website placeholder: "https://www.home-assistant.io/integrations/..." description: | - Providing a link [to the documentation][docs] helps us categorize the - issue, while also providing a useful reference for others. + Providing a link [to the documentation][docs] helps us categorize the issue and might speed up the + investigation by automatically informing a contributor, while also providing a useful reference for others. [docs]: https://www.home-assistant.io/integrations From 966e89a60c1e2807bc9e32bdbaeb45ac1465354b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 6 Jul 2023 16:17:59 +0200 Subject: [PATCH 0187/1009] Use device name for Nuki (#95941) --- homeassistant/components/nuki/lock.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 55560d3bf8c213..a1a75ef8260a82 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -72,6 +72,7 @@ class NukiDeviceEntity(NukiEntity[_NukiDeviceT], LockEntity): _attr_has_entity_name = True _attr_supported_features = LockEntityFeature.OPEN _attr_translation_key = "nuki_lock" + _attr_name = None @property def unique_id(self) -> str | None: From 45ae6b34751de03c9e6ad4ec86a7370a1678e07b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 6 Jul 2023 16:19:42 +0200 Subject: [PATCH 0188/1009] Add explicit device naming for Tuya sensors (#95944) --- homeassistant/components/tuya/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index a2cd2d5fc410b6..afa40f27afd9b4 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -511,6 +511,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "rqbj": ( TuyaSensorEntityDescription( key=DPCode.GAS_SENSOR_VALUE, + name=None, icon="mdi:gas-cylinder", state_class=SensorStateClass.MEASUREMENT, ), @@ -633,6 +634,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "ylcg": ( TuyaSensorEntityDescription( key=DPCode.PRESSURE_VALUE, + name=None, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), From 8015d4c7b49248ff1df628001ea4e13df988d848 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 6 Jul 2023 16:21:15 +0200 Subject: [PATCH 0189/1009] Fix entity name for Flick Electric (#95947) Fix entity name --- homeassistant/components/flick_electric/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/flick_electric/sensor.py b/homeassistant/components/flick_electric/sensor.py index 2210f44bf7a8e1..a0844fe6cdb7c9 100644 --- a/homeassistant/components/flick_electric/sensor.py +++ b/homeassistant/components/flick_electric/sensor.py @@ -34,6 +34,7 @@ class FlickPricingSensor(SensorEntity): _attr_attribution = "Data provided by Flick Electric" _attr_native_unit_of_measurement = f"{CURRENCY_CENT}/{UnitOfEnergy.KILO_WATT_HOUR}" + _attr_has_entity_name = True _attr_translation_key = "power_price" _attributes: dict[str, Any] = {} From 342d07cb92b67ff0280441e0fd4da7d14e008751 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 6 Jul 2023 16:24:34 +0200 Subject: [PATCH 0190/1009] 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 715a960de5dc03..a1dc22ea4a153b 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -678,6 +678,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) hass.data[SERVICE_DESCRIPTION_CACHE][(domain, service)] = description diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 291a1744d20976..a4a9bc5d2b06bb 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 2b0f2227fd464aecfa0e1e46ff0752615967ed82 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 6 Jul 2023 16:28:20 +0200 Subject: [PATCH 0191/1009] Fix state of slimproto players (#96000) --- homeassistant/components/slimproto/media_player.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/slimproto/media_player.py b/homeassistant/components/slimproto/media_player.py index 641d3b8ae4d9f7..c7c6585e0023d5 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -27,8 +27,10 @@ from .const import DEFAULT_NAME, DOMAIN, PLAYER_EVENT STATE_MAPPING = { - PlayerState.IDLE: MediaPlayerState.IDLE, + PlayerState.STOPPED: MediaPlayerState.IDLE, PlayerState.PLAYING: MediaPlayerState.PLAYING, + PlayerState.BUFFER_READY: MediaPlayerState.PLAYING, + PlayerState.BUFFERING: MediaPlayerState.PLAYING, PlayerState.PAUSED: MediaPlayerState.PAUSED, } From 5d9533fb9009e8c6c0de5435c64d2e50dbcb0e30 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 6 Jul 2023 16:48:03 +0200 Subject: [PATCH 0192/1009] Make script services always respond when asked (#95991) * Make script services always respond when asked * Update test_init.py --- homeassistant/components/script/__init__.py | 2 +- tests/components/script/test_init.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index f8d41db0e11616..8530aa3b04c143 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -608,7 +608,7 @@ async def _service_handler(self, service: ServiceCall) -> ServiceResponse: variables=service.data, context=service.context, wait=True ) if service.return_response: - return response + return response or {} return None async def async_added_to_hass(self) -> None: diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 199c3e08942daf..cc41b6c404cd24 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -26,7 +26,7 @@ callback, split_entity_id, ) -from homeassistant.exceptions import HomeAssistantError, ServiceNotFound +from homeassistant.exceptions import ServiceNotFound from homeassistant.helpers import entity_registry as er, template from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import ( @@ -1625,7 +1625,7 @@ async def test_responses(hass: HomeAssistant, response: Any) -> None: ) -async def test_responses_error(hass: HomeAssistant) -> None: +async def test_responses_no_response(hass: HomeAssistant) -> None: """Test response variable not set.""" mock_restore_cache(hass, ()) assert await async_setup_component( @@ -1645,10 +1645,13 @@ async def test_responses_error(hass: HomeAssistant) -> None: }, ) - with pytest.raises(HomeAssistantError): - assert await hass.services.async_call( + # Validate we can call it with return_response + assert ( + await hass.services.async_call( DOMAIN, "test", {"greeting": "world"}, blocking=True, return_response=True ) + == {} + ) # Validate we can also call it without return_response assert ( await hass.services.async_call( From b9c7e7c15ede85ddf7752b59b1148a3cc9f0a80a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 6 Jul 2023 17:14:09 +0200 Subject: [PATCH 0193/1009] Fix not including device_name in friendly name if it is None (#95485) * Omit device_name in friendly name if it is None * Fix test --- homeassistant/helpers/entity.py | 5 +++-- tests/helpers/test_entity.py | 23 ++++++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 3d22e2538a3322..e87eb15b9545ec 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -772,9 +772,10 @@ def _friendly_name_internal(self) -> str | None: ): return name + device_name = device_entry.name_by_user or device_entry.name if self.use_device_name: - return device_entry.name_by_user or device_entry.name - return f"{device_entry.name_by_user or device_entry.name} {name}" + return device_name + return f"{device_name} {name}" if device_name else name @callback def _async_write_ha_state(self) -> None: diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 85a7932aef89f1..7de6f70e793698 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -20,7 +20,7 @@ from homeassistant.core import Context, HomeAssistant, HomeAssistantError from homeassistant.helpers import device_registry as dr, entity, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity -from homeassistant.helpers.typing import UNDEFINED +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from tests.common import ( MockConfigEntry, @@ -989,12 +989,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @pytest.mark.parametrize( - ("has_entity_name", "entity_name", "expected_friendly_name", "warn_implicit_name"), ( - (False, "Entity Blu", "Entity Blu", False), - (False, None, None, False), - (True, "Entity Blu", "Device Bla Entity Blu", False), - (True, None, "Device Bla", False), + "has_entity_name", + "entity_name", + "device_name", + "expected_friendly_name", + "warn_implicit_name", + ), + ( + (False, "Entity Blu", "Device Bla", "Entity Blu", False), + (False, None, "Device Bla", None, False), + (True, "Entity Blu", "Device Bla", "Device Bla Entity Blu", False), + (True, None, "Device Bla", "Device Bla", False), + (True, "Entity Blu", UNDEFINED, "Entity Blu", False), + (True, "Entity Blu", None, "Mock Title Entity Blu", False), ), ) async def test_friendly_name_attr( @@ -1002,6 +1010,7 @@ async def test_friendly_name_attr( caplog: pytest.LogCaptureFixture, has_entity_name: bool, entity_name: str | None, + device_name: str | None | UndefinedType, expected_friendly_name: str | None, warn_implicit_name: bool, ) -> None: @@ -1012,7 +1021,7 @@ async def test_friendly_name_attr( device_info={ "identifiers": {("hue", "1234")}, "connections": {(dr.CONNECTION_NETWORK_MAC, "abcd")}, - "name": "Device Bla", + "name": device_name, }, ) ent._attr_has_entity_name = has_entity_name From b7b8afffd0e87e7e6ebfb93d3a718c1d55bed74d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Jul 2023 05:19:06 -1000 Subject: [PATCH 0194/1009] 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 a1dc22ea4a153b..40bb96506309e8 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", ""), @@ -686,7 +691,7 @@ def async_set_service_schema( } 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 a4a9bc5d2b06bb..bc7a93f0f19d75 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -622,6 +622,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 59645344e7cba63f468a7960b1b2d7ba4fc19b34 Mon Sep 17 00:00:00 2001 From: micha91 Date: Thu, 6 Jul 2023 17:20:20 +0200 Subject: [PATCH 0195/1009] Fix grouping feature for MusicCast (#95958) check the current source for grouping using the source ID instead of the label --- .../yamaha_musiccast/media_player.py | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index cf6feb44fbd772..42549fb20d9164 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -130,14 +130,11 @@ def zone_id(self): @property def _is_netusb(self): - return ( - self.coordinator.data.netusb_input - == self.coordinator.data.zones[self._zone_id].input - ) + return self.coordinator.data.netusb_input == self.source_id @property def _is_tuner(self): - return self.coordinator.data.zones[self._zone_id].input == "tuner" + return self.source_id == "tuner" @property def media_content_id(self): @@ -516,10 +513,15 @@ async def async_select_source(self, source: str) -> None: self._zone_id, self.reverse_source_mapping.get(source, source) ) + @property + def source_id(self): + """ID of the current input source.""" + return self.coordinator.data.zones[self._zone_id].input + @property def source(self): """Name of the current input source.""" - return self.source_mapping.get(self.coordinator.data.zones[self._zone_id].input) + return self.source_mapping.get(self.source_id) @property def source_list(self): @@ -597,7 +599,7 @@ def is_network_client(self) -> bool: return ( self.coordinator.data.group_role == "client" and self.coordinator.data.group_id != NULL_GROUP - and self.source == ATTR_MC_LINK + and self.source_id == ATTR_MC_LINK ) @property @@ -606,7 +608,7 @@ def is_client(self) -> bool: If the media player is not part of a group, False is returned. """ - return self.is_network_client or self.source == ATTR_MAIN_SYNC + return self.is_network_client or self.source_id == ATTR_MAIN_SYNC def get_all_mc_entities(self) -> list[MusicCastMediaPlayer]: """Return all media player entities of the musiccast system.""" @@ -639,11 +641,11 @@ def is_part_of_group(self, group_server) -> bool: and self.coordinator.data.group_id == group_server.coordinator.data.group_id and self.ip_address != group_server.ip_address - and self.source == ATTR_MC_LINK + and self.source_id == ATTR_MC_LINK ) or ( self.ip_address == group_server.ip_address - and self.source == ATTR_MAIN_SYNC + and self.source_id == ATTR_MAIN_SYNC ) ) @@ -859,8 +861,12 @@ async def async_client_leave_group(self, force=False): """ _LOGGER.debug("%s client leave called", self.entity_id) if not force and ( - self.source == ATTR_MAIN_SYNC - or [entity for entity in self.other_zones if entity.source == ATTR_MC_LINK] + self.source_id == ATTR_MAIN_SYNC + or [ + entity + for entity in self.other_zones + if entity.source_id == ATTR_MC_LINK + ] ): await self.coordinator.musiccast.zone_unjoin(self._zone_id) else: From ecc0917e8f40bcf5a8f836d1d422975bf3f70cae 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 0196/1009] Migrate bracketed IP addresses in ZHA config entry (#95917) * Automatically correct IP addresses surrounded by brackets * Simplify regex * Move pattern inline * Maintain old behavior of stripping whitespace --- homeassistant/components/zha/__init__.py | 24 ++++++++++++++++++++---- tests/components/zha/test_init.py | 12 ++++++++++-- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 5607cabffea19f..8a81648b580ae6 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -3,6 +3,7 @@ import copy import logging import os +import re import voluptuous as vol from zhaquirks import setup as setup_quirks @@ -85,19 +86,34 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True +def _clean_serial_port_path(path: str) -> str: + """Clean the serial port path, applying corrections where necessary.""" + + if path.startswith("socket://"): + path = path.strip() + + # Removes extraneous brackets from IP addresses (they don't parse in CPython 3.11.4) + if re.match(r"^socket://\[\d+\.\d+\.\d+\.\d+\]:\d+$", path): + path = path.replace("[", "").replace("]", "") + + return path + + async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up ZHA. Will automatically load components to support devices found on the network. """ - # Strip whitespace around `socket://` URIs, this is no longer accepted by zigpy - # This will be removed in 2023.7.0 + # Remove brackets around IP addresses, this no longer works in CPython 3.11.4 + # This will be removed in 2023.11.0 path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] + cleaned_path = _clean_serial_port_path(path) data = copy.deepcopy(dict(config_entry.data)) - if path.startswith("socket://") and path != path.strip(): - data[CONF_DEVICE][CONF_DEVICE_PATH] = path.strip() + if path != cleaned_path: + _LOGGER.debug("Cleaned serial port path %r -> %r", path, cleaned_path) + data[CONF_DEVICE][CONF_DEVICE_PATH] = cleaned_path hass.config_entries.async_update_entry(config_entry, data=data) zha_data = hass.data.setdefault(DATA_ZHA, {}) diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index 23a76de4c2504a..24ee63fb3d5064 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -114,19 +114,27 @@ async def test_config_depreciation(hass: HomeAssistant, zha_config) -> None: @pytest.mark.parametrize( ("path", "cleaned_path"), [ + # No corrections ("/dev/path1", "/dev/path1"), + ("/dev/path1[asd]", "/dev/path1[asd]"), ("/dev/path1 ", "/dev/path1 "), + ("socket://1.2.3.4:5678", "socket://1.2.3.4:5678"), + # Brackets around URI + ("socket://[1.2.3.4]:5678", "socket://1.2.3.4:5678"), + # Spaces ("socket://dev/path1 ", "socket://dev/path1"), + # Both + ("socket://[1.2.3.4]:5678 ", "socket://1.2.3.4:5678"), ], ) @patch("homeassistant.components.zha.setup_quirks", Mock(return_value=True)) @patch( "homeassistant.components.zha.websocket_api.async_load_api", Mock(return_value=True) ) -async def test_setup_with_v3_spaces_in_uri( +async def test_setup_with_v3_cleaning_uri( hass: HomeAssistant, path: str, cleaned_path: str ) -> None: - """Test migration of config entry from v3 with spaces after `socket://` URI.""" + """Test migration of config entry from v3, applying corrections to the port path.""" config_entry_v3 = MockConfigEntry( domain=DOMAIN, data={ From 23d5fb962211f95cce9dc34458fa321752399b82 Mon Sep 17 00:00:00 2001 From: Guillaume Duveau Date: Thu, 6 Jul 2023 20:26:46 +0200 Subject: [PATCH 0197/1009] Add more device info for SmartThings devices (#95723) * Add more device info for SmartThings devices * Fix binary_sensor test * Fix binary sensor test, try 2 * Fix and add SmartsThings new device info tests --- .../components/smartthings/__init__.py | 6 +- .../smartthings/test_binary_sensor.py | 16 ++++- tests/components/smartthings/test_climate.py | 10 ++- tests/components/smartthings/test_cover.py | 16 ++++- tests/components/smartthings/test_fan.py | 15 +++- tests/components/smartthings/test_light.py | 17 ++++- tests/components/smartthings/test_lock.py | 18 ++++- tests/components/smartthings/test_sensor.py | 72 ++++++++++++++----- tests/components/smartthings/test_switch.py | 18 ++++- 9 files changed, 150 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 024f04b0dc90d1..6606352ffc88e0 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -452,9 +452,11 @@ def device_info(self) -> DeviceInfo: return DeviceInfo( configuration_url="https://account.smartthings.com", identifiers={(DOMAIN, self._device.device_id)}, - manufacturer="Unavailable", - model=self._device.device_type_name, + manufacturer=self._device.status.ocf_manufacturer_name, + model=self._device.status.ocf_model_number, name=self._device.label, + hw_version=self._device.status.ocf_hardware_version, + sw_version=self._device.status.ocf_firmware_version, ) @property diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 0b697786b18054..b6f2159ae13edb 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -51,7 +51,15 @@ async def test_entity_and_device_attributes( """Test the attributes of the entity are correct.""" # Arrange device = device_factory( - "Motion Sensor 1", [Capability.motion_sensor], {Attribute.motion: "inactive"} + "Motion Sensor 1", + [Capability.motion_sensor], + { + Attribute.motion: "inactive", + Attribute.mnmo: "123", + Attribute.mnmn: "Generic manufacturer", + Attribute.mnhw: "v4.56", + Attribute.mnfv: "v7.89", + }, ) entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) @@ -66,8 +74,10 @@ async def test_entity_and_device_attributes( assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label - assert entry.model == device.device_type_name - assert entry.manufacturer == "Unavailable" + assert entry.model == "123" + assert entry.manufacturer == "Generic manufacturer" + assert entry.hw_version == "v4.56" + assert entry.sw_version == "v7.89" async def test_update_from_signal(hass: HomeAssistant, device_factory) -> None: diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 02f6af46655142..fe917504dcd478 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -112,6 +112,10 @@ def thermostat_fixture(device_factory): ], Attribute.thermostat_operating_state: "idle", Attribute.humidity: 34, + Attribute.mnmo: "123", + Attribute.mnmn: "Generic manufacturer", + Attribute.mnhw: "v4.56", + Attribute.mnfv: "v7.89", }, ) device.status.attributes[Attribute.temperature] = Status(70, "F", None) @@ -581,5 +585,7 @@ async def test_entity_and_device_attributes(hass: HomeAssistant, thermostat) -> assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, thermostat.device_id)} assert entry.name == thermostat.label - assert entry.model == thermostat.device_type_name - assert entry.manufacturer == "Unavailable" + assert entry.model == "123" + assert entry.manufacturer == "Generic manufacturer" + assert entry.hw_version == "v4.56" + assert entry.sw_version == "v7.89" diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 715f26beaa7e0a..f8c21166fe1c02 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -33,7 +33,15 @@ async def test_entity_and_device_attributes( """Test the attributes of the entity are correct.""" # Arrange device = device_factory( - "Garage", [Capability.garage_door_control], {Attribute.door: "open"} + "Garage", + [Capability.garage_door_control], + { + Attribute.door: "open", + Attribute.mnmo: "123", + Attribute.mnmn: "Generic manufacturer", + Attribute.mnhw: "v4.56", + Attribute.mnfv: "v7.89", + }, ) entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) @@ -49,8 +57,10 @@ async def test_entity_and_device_attributes( assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label - assert entry.model == device.device_type_name - assert entry.manufacturer == "Unavailable" + assert entry.model == "123" + assert entry.manufacturer == "Generic manufacturer" + assert entry.hw_version == "v4.56" + assert entry.sw_version == "v7.89" async def test_open(hass: HomeAssistant, device_factory) -> None: diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 120a90fb2f45d9..aef9ce319e73a3 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -48,7 +48,14 @@ async def test_entity_and_device_attributes( device = device_factory( "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], - status={Attribute.switch: "on", Attribute.fan_speed: 2}, + status={ + Attribute.switch: "on", + Attribute.fan_speed: 2, + Attribute.mnmo: "123", + Attribute.mnmn: "Generic manufacturer", + Attribute.mnhw: "v4.56", + Attribute.mnfv: "v7.89", + }, ) # Act await setup_platform(hass, FAN_DOMAIN, devices=[device]) @@ -64,8 +71,10 @@ async def test_entity_and_device_attributes( assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label - assert entry.model == device.device_type_name - assert entry.manufacturer == "Unavailable" + assert entry.model == "123" + assert entry.manufacturer == "Generic manufacturer" + assert entry.hw_version == "v4.56" + assert entry.sw_version == "v7.89" async def test_turn_off(hass: HomeAssistant, device_factory) -> None: diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index 713b156fc4f2e0..0e01910c84a05d 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -109,7 +109,16 @@ async def test_entity_and_device_attributes( ) -> None: """Test the attributes of the entity are correct.""" # Arrange - device = device_factory("Light 1", [Capability.switch, Capability.switch_level]) + device = device_factory( + "Light 1", + [Capability.switch, Capability.switch_level], + { + Attribute.mnmo: "123", + Attribute.mnmn: "Generic manufacturer", + Attribute.mnhw: "v4.56", + Attribute.mnfv: "v7.89", + }, + ) entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) # Act @@ -124,8 +133,10 @@ async def test_entity_and_device_attributes( assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label - assert entry.model == device.device_type_name - assert entry.manufacturer == "Unavailable" + assert entry.model == "123" + assert entry.manufacturer == "Generic manufacturer" + assert entry.hw_version == "v4.56" + assert entry.sw_version == "v7.89" async def test_turn_off(hass: HomeAssistant, light_devices) -> None: diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 6c01bc2b6c4af2..0d237cec132da0 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -22,7 +22,17 @@ async def test_entity_and_device_attributes( ) -> None: """Test the attributes of the entity are correct.""" # Arrange - device = device_factory("Lock_1", [Capability.lock], {Attribute.lock: "unlocked"}) + device = device_factory( + "Lock_1", + [Capability.lock], + { + Attribute.lock: "unlocked", + Attribute.mnmo: "123", + Attribute.mnmn: "Generic manufacturer", + Attribute.mnhw: "v4.56", + Attribute.mnfv: "v7.89", + }, + ) entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) # Act @@ -37,8 +47,10 @@ async def test_entity_and_device_attributes( assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label - assert entry.model == device.device_type_name - assert entry.manufacturer == "Unavailable" + assert entry.model == "123" + assert entry.manufacturer == "Generic manufacturer" + assert entry.hw_version == "v4.56" + assert entry.sw_version == "v7.89" async def test_lock(hass: HomeAssistant, device_factory) -> None: diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 01745878bf08a8..cc7b67145c1e0f 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -90,7 +90,17 @@ async def test_entity_and_device_attributes( ) -> None: """Test the attributes of the entity are correct.""" # Arrange - device = device_factory("Sensor 1", [Capability.battery], {Attribute.battery: 100}) + device = device_factory( + "Sensor 1", + [Capability.battery], + { + Attribute.battery: 100, + Attribute.mnmo: "123", + Attribute.mnmn: "Generic manufacturer", + Attribute.mnhw: "v4.56", + Attribute.mnfv: "v7.89", + }, + ) entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) # Act @@ -105,8 +115,10 @@ async def test_entity_and_device_attributes( assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label - assert entry.model == device.device_type_name - assert entry.manufacturer == "Unavailable" + assert entry.model == "123" + assert entry.manufacturer == "Generic manufacturer" + assert entry.hw_version == "v4.56" + assert entry.sw_version == "v7.89" async def test_energy_sensors_for_switch_device( @@ -117,7 +129,15 @@ async def test_energy_sensors_for_switch_device( device = device_factory( "Switch_1", [Capability.switch, Capability.power_meter, Capability.energy_meter], - {Attribute.switch: "off", Attribute.power: 355, Attribute.energy: 11.422}, + { + Attribute.switch: "off", + Attribute.power: 355, + Attribute.energy: 11.422, + Attribute.mnmo: "123", + Attribute.mnmn: "Generic manufacturer", + Attribute.mnhw: "v4.56", + Attribute.mnfv: "v7.89", + }, ) entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) @@ -136,8 +156,10 @@ async def test_energy_sensors_for_switch_device( assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label - assert entry.model == device.device_type_name - assert entry.manufacturer == "Unavailable" + assert entry.model == "123" + assert entry.manufacturer == "Generic manufacturer" + assert entry.hw_version == "v4.56" + assert entry.sw_version == "v7.89" state = hass.states.get("sensor.switch_1_power_meter") assert state @@ -151,8 +173,10 @@ async def test_energy_sensors_for_switch_device( assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label - assert entry.model == device.device_type_name - assert entry.manufacturer == "Unavailable" + assert entry.model == "123" + assert entry.manufacturer == "Generic manufacturer" + assert entry.hw_version == "v4.56" + assert entry.sw_version == "v7.89" async def test_power_consumption_sensor(hass: HomeAssistant, device_factory) -> None: @@ -171,7 +195,11 @@ async def test_power_consumption_sensor(hass: HomeAssistant, device_factory) -> "energySaved": 0, "start": "2021-07-30T16:45:25Z", "end": "2021-07-30T16:58:33Z", - } + }, + Attribute.mnmo: "123", + Attribute.mnmn: "Generic manufacturer", + Attribute.mnhw: "v4.56", + Attribute.mnfv: "v7.89", }, ) entity_registry = er.async_get(hass) @@ -190,8 +218,10 @@ async def test_power_consumption_sensor(hass: HomeAssistant, device_factory) -> assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label - assert entry.model == device.device_type_name - assert entry.manufacturer == "Unavailable" + assert entry.model == "123" + assert entry.manufacturer == "Generic manufacturer" + assert entry.hw_version == "v4.56" + assert entry.sw_version == "v7.89" state = hass.states.get("sensor.refrigerator_power") assert state @@ -206,13 +236,21 @@ async def test_power_consumption_sensor(hass: HomeAssistant, device_factory) -> assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label - assert entry.model == device.device_type_name - assert entry.manufacturer == "Unavailable" + assert entry.model == "123" + assert entry.manufacturer == "Generic manufacturer" + assert entry.hw_version == "v4.56" + assert entry.sw_version == "v7.89" device = device_factory( "vacuum", [Capability.power_consumption_report], - {Attribute.power_consumption: {}}, + { + Attribute.power_consumption: {}, + Attribute.mnmo: "123", + Attribute.mnmn: "Generic manufacturer", + Attribute.mnhw: "v4.56", + Attribute.mnfv: "v7.89", + }, ) entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) @@ -230,8 +268,10 @@ async def test_power_consumption_sensor(hass: HomeAssistant, device_factory) -> assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label - assert entry.model == device.device_type_name - assert entry.manufacturer == "Unavailable" + assert entry.model == "123" + assert entry.manufacturer == "Generic manufacturer" + assert entry.hw_version == "v4.56" + assert entry.sw_version == "v7.89" async def test_update_from_signal(hass: HomeAssistant, device_factory) -> None: diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 81bb8579cfd031..f90395f0064197 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -21,7 +21,17 @@ async def test_entity_and_device_attributes( ) -> None: """Test the attributes of the entity are correct.""" # Arrange - device = device_factory("Switch_1", [Capability.switch], {Attribute.switch: "on"}) + device = device_factory( + "Switch_1", + [Capability.switch], + { + Attribute.switch: "on", + Attribute.mnmo: "123", + Attribute.mnmn: "Generic manufacturer", + Attribute.mnhw: "v4.56", + Attribute.mnfv: "v7.89", + }, + ) entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) # Act @@ -36,8 +46,10 @@ async def test_entity_and_device_attributes( assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} assert entry.name == device.label - assert entry.model == device.device_type_name - assert entry.manufacturer == "Unavailable" + assert entry.model == "123" + assert entry.manufacturer == "Generic manufacturer" + assert entry.hw_version == "v4.56" + assert entry.sw_version == "v7.89" async def test_turn_off(hass: HomeAssistant, device_factory) -> None: From d1e19c3a855b25b0270b8e636c1439d8172785bf Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 6 Jul 2023 22:39:18 +0200 Subject: [PATCH 0198/1009] Add entity translations to Pushbullet (#95943) --- homeassistant/components/pushbullet/sensor.py | 20 +++++------ .../components/pushbullet/strings.json | 34 +++++++++++++++++++ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py index b61469f6b2a811..84d2998e992a70 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -16,50 +16,50 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="application_name", - name="Application name", + translation_key="application_name", entity_registry_enabled_default=False, ), SensorEntityDescription( key="body", - name="Body", + translation_key="body", ), SensorEntityDescription( key="notification_id", - name="Notification ID", + translation_key="notification_id", entity_registry_enabled_default=False, ), SensorEntityDescription( key="notification_tag", - name="Notification tag", + translation_key="notification_tag", entity_registry_enabled_default=False, ), SensorEntityDescription( key="package_name", - name="Package name", + translation_key="package_name", entity_registry_enabled_default=False, ), SensorEntityDescription( key="receiver_email", - name="Receiver email", + translation_key="receiver_email", entity_registry_enabled_default=False, ), SensorEntityDescription( key="sender_email", - name="Sender email", + translation_key="sender_email", entity_registry_enabled_default=False, ), SensorEntityDescription( key="source_device_iden", - name="Sender device ID", + translation_key="source_device_identifier", entity_registry_enabled_default=False, ), SensorEntityDescription( key="title", - name="Title", + translation_key="title", ), SensorEntityDescription( key="type", - name="Type", + translation_key="type", entity_registry_enabled_default=False, ), ) diff --git a/homeassistant/components/pushbullet/strings.json b/homeassistant/components/pushbullet/strings.json index a6571ae7bf0f24..94d4202ea8c0d7 100644 --- a/homeassistant/components/pushbullet/strings.json +++ b/homeassistant/components/pushbullet/strings.json @@ -15,5 +15,39 @@ } } } + }, + "entity": { + "sensor": { + "application_name": { + "name": "Application name" + }, + "body": { + "name": "Body" + }, + "notification_id": { + "name": "Notification ID" + }, + "notification_tag": { + "name": "Notification tag" + }, + "package_name": { + "name": "Package name" + }, + "receiver_email": { + "name": "Receiver email" + }, + "sender_email": { + "name": "Sender email" + }, + "source_device_identifier": { + "name": "Sender device ID" + }, + "title": { + "name": "Title" + }, + "type": { + "name": "Type" + } + } } } From 99430ceb3404cd9e5923a69485826bf0a02f43a1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 6 Jul 2023 22:51:05 +0200 Subject: [PATCH 0199/1009] Add entity translations for PureEnergie (#95935) * Add entity translations for PureEnergie * Fix tests --- homeassistant/components/pure_energie/sensor.py | 7 ++++--- homeassistant/components/pure_energie/strings.json | 13 +++++++++++++ tests/components/pure_energie/test_sensor.py | 6 +++--- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/pure_energie/sensor.py b/homeassistant/components/pure_energie/sensor.py index 7d584c7c1a8af2..9f67665d66c5d5 100644 --- a/homeassistant/components/pure_energie/sensor.py +++ b/homeassistant/components/pure_energie/sensor.py @@ -39,7 +39,7 @@ class PureEnergieSensorEntityDescription( SENSORS: tuple[PureEnergieSensorEntityDescription, ...] = ( PureEnergieSensorEntityDescription( key="power_flow", - name="Power Flow", + translation_key="power_flow", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -47,7 +47,7 @@ class PureEnergieSensorEntityDescription( ), PureEnergieSensorEntityDescription( key="energy_consumption_total", - name="Energy Consumption", + translation_key="energy_consumption_total", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -55,7 +55,7 @@ class PureEnergieSensorEntityDescription( ), PureEnergieSensorEntityDescription( key="energy_production_total", - name="Energy Production", + translation_key="energy_production_total", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -83,6 +83,7 @@ class PureEnergieSensorEntity( ): """Defines an Pure Energie sensor.""" + _attr_has_entity_name = True entity_description: PureEnergieSensorEntityDescription def __init__( diff --git a/homeassistant/components/pure_energie/strings.json b/homeassistant/components/pure_energie/strings.json index a76b4a001e611c..3545f62d667650 100644 --- a/homeassistant/components/pure_energie/strings.json +++ b/homeassistant/components/pure_energie/strings.json @@ -22,5 +22,18 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } + }, + "entity": { + "sensor": { + "power_flow": { + "name": "Power flow" + }, + "energy_consumption_total": { + "name": "Energy consumption" + }, + "energy_production_total": { + "name": "Energy production" + } + } } } diff --git a/tests/components/pure_energie/test_sensor.py b/tests/components/pure_energie/test_sensor.py index 2881bf28d8f3bf..eb0b9634e83f94 100644 --- a/tests/components/pure_energie/test_sensor.py +++ b/tests/components/pure_energie/test_sensor.py @@ -34,7 +34,7 @@ async def test_sensors( assert state assert entry.unique_id == "aabbccddeeff_energy_consumption_total" assert state.state == "17762.1" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Consumption" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home Energy consumption" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY @@ -46,7 +46,7 @@ async def test_sensors( assert state assert entry.unique_id == "aabbccddeeff_energy_production_total" assert state.state == "21214.6" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Production" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home Energy production" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY @@ -58,7 +58,7 @@ async def test_sensors( assert state assert entry.unique_id == "aabbccddeeff_power_flow" assert state.state == "338" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Flow" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home Power flow" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER From e94726ec84c62baa270f71f6dce84b0b0d6c6bc1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 6 Jul 2023 23:01:06 +0200 Subject: [PATCH 0200/1009] 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 1da879cb02bfd3..35083c4b0897c6 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 6c4b5291e1df4f1d1d34ea80341460b7095f55b5 Mon Sep 17 00:00:00 2001 From: lymanepp <4195527+lymanepp@users.noreply.github.com> Date: Thu, 6 Jul 2023 17:05:46 -0400 Subject: [PATCH 0201/1009] Add humidity to NWS forecast (#95575) * Add humidity to NWS forecast to address https://github.com/home-assistant/core/issues/95572 * Use pynws 1.5.0 enhancements for probabilityOfPrecipitation, dewpoint, and relativeHumidity. * Update requirements to match pynws version * test for clear night * update docstring --------- Co-authored-by: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> --- homeassistant/components/nws/manifest.json | 2 +- homeassistant/components/nws/weather.py | 39 +++++++++++++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nws/const.py | 21 ++++++++++-- tests/components/nws/test_weather.py | 19 +++++++++++ 6 files changed, 65 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index ed7d825afff930..7f5d01f9897726 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["metar", "pynws"], "quality_scale": "platinum", - "requirements": ["pynws==1.4.1"] + "requirements": ["pynws==1.5.0"] } diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 9edf6e61751b3a..e8a35ba66f11aa 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -8,6 +8,8 @@ ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_HUMIDITY, + ATTR_FORECAST_NATIVE_DEW_POINT, ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, @@ -52,16 +54,13 @@ PARALLEL_UPDATES = 0 -def convert_condition( - time: str, weather: tuple[tuple[str, int | None], ...] -) -> tuple[str, int | None]: +def convert_condition(time: str, weather: tuple[tuple[str, int | None], ...]) -> str: """Convert NWS codes to HA condition. Choose first condition in CONDITION_CLASSES that exists in weather code. If no match is found, return first condition from NWS """ conditions: list[str] = [w[0] for w in weather] - prec_probs = [w[1] or 0 for w in weather] # Choose condition with highest priority. cond = next( @@ -75,10 +74,10 @@ def convert_condition( if cond == "clear": if time == "day": - return ATTR_CONDITION_SUNNY, max(prec_probs) + return ATTR_CONDITION_SUNNY if time == "night": - return ATTR_CONDITION_CLEAR_NIGHT, max(prec_probs) - return cond, max(prec_probs) + return ATTR_CONDITION_CLEAR_NIGHT + return cond async def async_setup_entry( @@ -219,8 +218,7 @@ def condition(self) -> str | None: time = self.observation.get("iconTime") if weather: - cond, _ = convert_condition(time, weather) - return cond + return convert_condition(time, weather) return None @property @@ -256,16 +254,27 @@ def forecast(self) -> list[Forecast] | None: else: data[ATTR_FORECAST_NATIVE_TEMP] = None + data[ATTR_FORECAST_PRECIPITATION_PROBABILITY] = forecast_entry.get( + "probabilityOfPrecipitation" + ) + + if (dewp := forecast_entry.get("dewpoint")) is not None: + data[ATTR_FORECAST_NATIVE_DEW_POINT] = TemperatureConverter.convert( + dewp, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS + ) + else: + data[ATTR_FORECAST_NATIVE_DEW_POINT] = None + + data[ATTR_FORECAST_HUMIDITY] = forecast_entry.get("relativeHumidity") + if self.mode == DAYNIGHT: data[ATTR_FORECAST_DAYTIME] = forecast_entry.get("isDaytime") + time = forecast_entry.get("iconTime") weather = forecast_entry.get("iconWeather") - if time and weather: - cond, precip = convert_condition(time, weather) - else: - cond, precip = None, None - data[ATTR_FORECAST_CONDITION] = cond - data[ATTR_FORECAST_PRECIPITATION_PROBABILITY] = precip + data[ATTR_FORECAST_CONDITION] = ( + convert_condition(time, weather) if time and weather else None + ) data[ATTR_FORECAST_WIND_BEARING] = forecast_entry.get("windBearing") wind_speed = forecast_entry.get("windSpeedAvg") diff --git a/requirements_all.txt b/requirements_all.txt index 3fe51080f2813a..e214740acec1c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1861,7 +1861,7 @@ pynuki==1.6.2 pynut2==2.1.2 # homeassistant.components.nws -pynws==1.4.1 +pynws==1.5.0 # homeassistant.components.nx584 pynx584==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c33cf3f4c99d9d..583c7c96996626 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1377,7 +1377,7 @@ pynuki==1.6.2 pynut2==2.1.2 # homeassistant.components.nws -pynws==1.4.1 +pynws==1.5.0 # homeassistant.components.nx584 pynx584==0.5 diff --git a/tests/components/nws/const.py b/tests/components/nws/const.py index 2048db2a2c3c5f..106b80998ac863 100644 --- a/tests/components/nws/const.py +++ b/tests/components/nws/const.py @@ -3,6 +3,8 @@ from homeassistant.components.weather import ( ATTR_CONDITION_LIGHTNING_RAINY, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_DEW_POINT, + ATTR_FORECAST_HUMIDITY, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, @@ -59,6 +61,9 @@ "windGust": 20, } +CLEAR_NIGHT_OBSERVATION = DEFAULT_OBSERVATION.copy() +CLEAR_NIGHT_OBSERVATION["iconTime"] = "night" + SENSOR_EXPECTED_OBSERVATION_METRIC = { "dewpoint": "5", "temperature": "10", @@ -183,6 +188,9 @@ "timestamp": "2019-08-12T23:53:00+00:00", "iconTime": "night", "iconWeather": (("lightning-rainy", 40), ("lightning-rainy", 90)), + "probabilityOfPrecipitation": 89, + "dewpoint": 4, + "relativeHumidity": 75, }, ] @@ -192,7 +200,9 @@ ATTR_FORECAST_TEMP: 10, ATTR_FORECAST_WIND_SPEED: 10, ATTR_FORECAST_WIND_BEARING: 180, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 90, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 89, + ATTR_FORECAST_DEW_POINT: 4, + ATTR_FORECAST_HUMIDITY: 75, } EXPECTED_FORECAST_METRIC = { @@ -211,7 +221,14 @@ 2, ), ATTR_FORECAST_WIND_BEARING: 180, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 90, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 89, + ATTR_FORECAST_DEW_POINT: round( + TemperatureConverter.convert( + 4, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS + ), + 1, + ), + ATTR_FORECAST_HUMIDITY: 75, } NONE_FORECAST = [{key: None for key in DEFAULT_FORECAST[0]}] diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index ce268796639f38..06d2c2006d84dd 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -7,6 +7,7 @@ from homeassistant.components import nws from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, ATTR_FORECAST, DOMAIN as WEATHER_DOMAIN, @@ -19,6 +20,7 @@ from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM from .const import ( + CLEAR_NIGHT_OBSERVATION, EXPECTED_FORECAST_IMPERIAL, EXPECTED_FORECAST_METRIC, NONE_FORECAST, @@ -97,6 +99,23 @@ async def test_imperial_metric( assert forecast[0].get(key) == value +async def test_night_clear(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None: + """Test with clear-night in observation.""" + instance = mock_simple_nws.return_value + instance.observation = CLEAR_NIGHT_OBSERVATION + + entry = MockConfigEntry( + domain=nws.DOMAIN, + data=NWS_CONFIG, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("weather.abc_daynight") + assert state.state == ATTR_CONDITION_CLEAR_NIGHT + + async def test_none_values(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None: """Test with none values in observation and forecast dicts.""" instance = mock_simple_nws.return_value From 63bf4b8099d5823fc71f545a32fe1dddd6b355d7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 6 Jul 2023 23:26:21 +0200 Subject: [PATCH 0202/1009] Add entity translations to Purpleair (#95942) * Add entity translations to Purpleair * Add entity translations to Purpleair * Change vocaqi sensor --- homeassistant/components/purpleair/sensor.py | 26 ++++++---------- .../components/purpleair/strings.json | 31 +++++++++++++++++++ 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/purpleair/sensor.py b/homeassistant/components/purpleair/sensor.py index 23370f8a20c2d0..160f529c285ff2 100644 --- a/homeassistant/components/purpleair/sensor.py +++ b/homeassistant/components/purpleair/sensor.py @@ -50,7 +50,6 @@ class PurpleAirSensorEntityDescription( SENSOR_DESCRIPTIONS = [ PurpleAirSensorEntityDescription( key="humidity", - name="Humidity", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -58,7 +57,7 @@ class PurpleAirSensorEntityDescription( ), PurpleAirSensorEntityDescription( key="pm0.3_count_concentration", - name="PM0.3 count concentration", + translation_key="pm0_3_count_concentration", entity_registry_enabled_default=False, icon="mdi:blur", native_unit_of_measurement=CONCENTRATION_PARTICLES_PER_100_MILLILITERS, @@ -67,7 +66,7 @@ class PurpleAirSensorEntityDescription( ), PurpleAirSensorEntityDescription( key="pm0.5_count_concentration", - name="PM0.5 count concentration", + translation_key="pm0_5_count_concentration", entity_registry_enabled_default=False, icon="mdi:blur", native_unit_of_measurement=CONCENTRATION_PARTICLES_PER_100_MILLILITERS, @@ -76,7 +75,7 @@ class PurpleAirSensorEntityDescription( ), PurpleAirSensorEntityDescription( key="pm1.0_count_concentration", - name="PM1.0 count concentration", + translation_key="pm1_0_count_concentration", entity_registry_enabled_default=False, icon="mdi:blur", native_unit_of_measurement=CONCENTRATION_PARTICLES_PER_100_MILLILITERS, @@ -85,7 +84,6 @@ class PurpleAirSensorEntityDescription( ), PurpleAirSensorEntityDescription( key="pm1.0_mass_concentration", - name="PM1.0 mass concentration", device_class=SensorDeviceClass.PM1, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, @@ -93,7 +91,7 @@ class PurpleAirSensorEntityDescription( ), PurpleAirSensorEntityDescription( key="pm10.0_count_concentration", - name="PM10.0 count concentration", + translation_key="pm10_0_count_concentration", entity_registry_enabled_default=False, icon="mdi:blur", native_unit_of_measurement=CONCENTRATION_PARTICLES_PER_100_MILLILITERS, @@ -102,7 +100,6 @@ class PurpleAirSensorEntityDescription( ), PurpleAirSensorEntityDescription( key="pm10.0_mass_concentration", - name="PM10.0 mass concentration", device_class=SensorDeviceClass.PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, @@ -110,7 +107,7 @@ class PurpleAirSensorEntityDescription( ), PurpleAirSensorEntityDescription( key="pm2.5_count_concentration", - name="PM2.5 count concentration", + translation_key="pm2_5_count_concentration", entity_registry_enabled_default=False, icon="mdi:blur", native_unit_of_measurement=CONCENTRATION_PARTICLES_PER_100_MILLILITERS, @@ -119,7 +116,6 @@ class PurpleAirSensorEntityDescription( ), PurpleAirSensorEntityDescription( key="pm2.5_mass_concentration", - name="PM2.5 mass concentration", device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, @@ -127,7 +123,7 @@ class PurpleAirSensorEntityDescription( ), PurpleAirSensorEntityDescription( key="pm5.0_count_concentration", - name="PM5.0 count concentration", + translation_key="pm5_0_count_concentration", entity_registry_enabled_default=False, icon="mdi:blur", native_unit_of_measurement=CONCENTRATION_PARTICLES_PER_100_MILLILITERS, @@ -136,7 +132,6 @@ class PurpleAirSensorEntityDescription( ), PurpleAirSensorEntityDescription( key="pressure", - name="Pressure", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=UnitOfPressure.MBAR, state_class=SensorStateClass.MEASUREMENT, @@ -144,7 +139,7 @@ class PurpleAirSensorEntityDescription( ), PurpleAirSensorEntityDescription( key="rssi", - name="RSSI", + translation_key="rssi", device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -154,7 +149,6 @@ class PurpleAirSensorEntityDescription( ), PurpleAirSensorEntityDescription( key="temperature", - name="Temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, state_class=SensorStateClass.MEASUREMENT, @@ -162,7 +156,7 @@ class PurpleAirSensorEntityDescription( ), PurpleAirSensorEntityDescription( key="uptime", - name="Uptime", + translation_key="uptime", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, device_class=SensorDeviceClass.DURATION, @@ -171,9 +165,9 @@ class PurpleAirSensorEntityDescription( value_fn=lambda sensor: sensor.uptime, ), PurpleAirSensorEntityDescription( + # This sensor is an air quality index for VOCs. More info at https://github.com/home-assistant/core/pull/84896 key="voc", - name="VOC", - device_class=SensorDeviceClass.AQI, + translation_key="voc_aqi", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda sensor: sensor.voc, ), diff --git a/homeassistant/components/purpleair/strings.json b/homeassistant/components/purpleair/strings.json index 836496d0ca8380..5e7c61c182053c 100644 --- a/homeassistant/components/purpleair/strings.json +++ b/homeassistant/components/purpleair/strings.json @@ -107,5 +107,36 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "sensor": { + "pm0_3_count_concentration": { + "name": "PM0.3 count concentration" + }, + "pm0_5_count_concentration": { + "name": "PM0.5 count concentration" + }, + "pm1_0_count_concentration": { + "name": "PM1.0 count concentration" + }, + "pm10_0_count_concentration": { + "name": "PM10.0 count concentration" + }, + "pm2_5_count_concentration": { + "name": "PM2.5 count concentration" + }, + "pm5_0_count_concentration": { + "name": "PM5.0 count concentration" + }, + "rssi": { + "name": "RSSI" + }, + "uptime": { + "name": "Uptime" + }, + "voc_aqi": { + "name": "Volatile organic compounds air quality index" + } + } } } From d2bcb5fa87db774522fc27f8789bdbb97bdbd315 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jul 2023 01:03:01 +0200 Subject: [PATCH 0203/1009] Add entity translations to Rainbird (#96030) * Add entity translations to Rainbird * Add entity translations to Rainbird --- .../components/rainbird/binary_sensor.py | 4 +++- homeassistant/components/rainbird/number.py | 4 ++-- homeassistant/components/rainbird/sensor.py | 6 ++++-- homeassistant/components/rainbird/strings.json | 17 +++++++++++++++++ homeassistant/components/rainbird/switch.py | 2 +- tests/components/rainbird/test_binary_sensor.py | 4 ++-- tests/components/rainbird/test_sensor.py | 4 ++-- 7 files changed, 31 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/rainbird/binary_sensor.py b/homeassistant/components/rainbird/binary_sensor.py index ee5be0e4617b92..139a17f5181969 100644 --- a/homeassistant/components/rainbird/binary_sensor.py +++ b/homeassistant/components/rainbird/binary_sensor.py @@ -20,7 +20,7 @@ RAIN_SENSOR_ENTITY_DESCRIPTION = BinarySensorEntityDescription( key="rainsensor", - name="Rainsensor", + translation_key="rainsensor", icon="mdi:water", ) @@ -38,6 +38,8 @@ async def async_setup_entry( class RainBirdSensor(CoordinatorEntity[RainbirdUpdateCoordinator], BinarySensorEntity): """A sensor implementation for Rain Bird device.""" + _attr_has_entity_name = True + def __init__( self, coordinator: RainbirdUpdateCoordinator, diff --git a/homeassistant/components/rainbird/number.py b/homeassistant/components/rainbird/number.py index ac1ea9618706b1..febb960d652259 100644 --- a/homeassistant/components/rainbird/number.py +++ b/homeassistant/components/rainbird/number.py @@ -32,14 +32,14 @@ async def async_setup_entry( class RainDelayNumber(CoordinatorEntity[RainbirdUpdateCoordinator], NumberEntity): - """A number implemnetaiton for the rain delay.""" + """A number implementation for the rain delay.""" _attr_native_min_value = 0 _attr_native_max_value = 14 _attr_native_step = 1 _attr_native_unit_of_measurement = UnitOfTime.DAYS _attr_icon = "mdi:water-off" - _attr_name = "Rain delay" + _attr_translation_key = "rain_delay" _attr_has_entity_name = True def __init__( diff --git a/homeassistant/components/rainbird/sensor.py b/homeassistant/components/rainbird/sensor.py index de74943baf9c4c..f5cf2390095ad2 100644 --- a/homeassistant/components/rainbird/sensor.py +++ b/homeassistant/components/rainbird/sensor.py @@ -1,4 +1,4 @@ -"""Support for Rain Bird Irrigation system LNK WiFi Module.""" +"""Support for Rain Bird Irrigation system LNK Wi-Fi Module.""" from __future__ import annotations import logging @@ -18,7 +18,7 @@ RAIN_DELAY_ENTITY_DESCRIPTION = SensorEntityDescription( key="raindelay", - name="Raindelay", + translation_key="raindelay", icon="mdi:water-off", ) @@ -42,6 +42,8 @@ async def async_setup_entry( class RainBirdSensor(CoordinatorEntity[RainbirdUpdateCoordinator], SensorEntity): """A sensor implementation for Rain Bird device.""" + _attr_has_entity_name = True + def __init__( self, coordinator: RainbirdUpdateCoordinator, diff --git a/homeassistant/components/rainbird/strings.json b/homeassistant/components/rainbird/strings.json index 3b5ae332dbd436..a98baead976ae6 100644 --- a/homeassistant/components/rainbird/strings.json +++ b/homeassistant/components/rainbird/strings.json @@ -27,5 +27,22 @@ } } } + }, + "entity": { + "binary_sensor": { + "rainsensor": { + "name": "Rainsensor" + } + }, + "number": { + "rain_delay": { + "name": "Rain delay" + } + }, + "sensor": { + "raindelay": { + "name": "Raindelay" + } + } } } diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index ceca9c71c36480..3e2a3115e29390 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -1,4 +1,4 @@ -"""Support for Rain Bird Irrigation system LNK WiFi Module.""" +"""Support for Rain Bird Irrigation system LNK Wi-Fi Module.""" from __future__ import annotations import logging diff --git a/tests/components/rainbird/test_binary_sensor.py b/tests/components/rainbird/test_binary_sensor.py index 816f2a3b969b6a..cfa2c4d2684816 100644 --- a/tests/components/rainbird/test_binary_sensor.py +++ b/tests/components/rainbird/test_binary_sensor.py @@ -31,10 +31,10 @@ async def test_rainsensor( assert await setup_integration() - rainsensor = hass.states.get("binary_sensor.rainsensor") + rainsensor = hass.states.get("binary_sensor.rain_bird_controller_rainsensor") assert rainsensor is not None assert rainsensor.state == expected_state assert rainsensor.attributes == { - "friendly_name": "Rainsensor", + "friendly_name": "Rain Bird Controller Rainsensor", "icon": "mdi:water", } diff --git a/tests/components/rainbird/test_sensor.py b/tests/components/rainbird/test_sensor.py index e9923b1a05296f..049a5f15c45668 100644 --- a/tests/components/rainbird/test_sensor.py +++ b/tests/components/rainbird/test_sensor.py @@ -28,10 +28,10 @@ async def test_sensors( assert await setup_integration() - raindelay = hass.states.get("sensor.raindelay") + raindelay = hass.states.get("sensor.rain_bird_controller_raindelay") assert raindelay is not None assert raindelay.state == expected_state assert raindelay.attributes == { - "friendly_name": "Raindelay", + "friendly_name": "Rain Bird Controller Raindelay", "icon": "mdi:water-off", } From ba1266a893df9002bd3fbfc0a6afb51845e8bf9c Mon Sep 17 00:00:00 2001 From: Mike Woudenberg Date: Fri, 7 Jul 2023 05:09:34 +0200 Subject: [PATCH 0204/1009] Add sensors to LOQED integration for battery percentage and BLE stength (#95726) * Add sensors for battery percentage and BLE stength * Use translatable name for BLE strength, no longer pass enity to sensor --- .coveragerc | 1 + homeassistant/components/loqed/__init__.py | 2 +- homeassistant/components/loqed/sensor.py | 71 +++++++++++++++++++++ homeassistant/components/loqed/strings.json | 7 ++ tests/components/loqed/conftest.py | 1 + 5 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/loqed/sensor.py diff --git a/.coveragerc b/.coveragerc index 5ba8575979d8cc..e69683288a2a0d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -650,6 +650,7 @@ omit = homeassistant/components/lookin/light.py homeassistant/components/lookin/media_player.py homeassistant/components/lookin/sensor.py + homeassistant/components/loqed/sensor.py homeassistant/components/luci/device_tracker.py homeassistant/components/luftdaten/sensor.py homeassistant/components/lupusec/* diff --git a/homeassistant/components/loqed/__init__.py b/homeassistant/components/loqed/__init__.py index 1248c75612fb53..e6c69e0751e629 100644 --- a/homeassistant/components/loqed/__init__.py +++ b/homeassistant/components/loqed/__init__.py @@ -14,7 +14,7 @@ from .const import DOMAIN from .coordinator import LoqedDataCoordinator -PLATFORMS: list[str] = [Platform.LOCK] +PLATFORMS: list[str] = [Platform.LOCK, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/loqed/sensor.py b/homeassistant/components/loqed/sensor.py new file mode 100644 index 00000000000000..ee4fa7ecd74b00 --- /dev/null +++ b/homeassistant/components/loqed/sensor.py @@ -0,0 +1,71 @@ +"""Creates LOQED sensors.""" +from typing import Final + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + EntityCategory, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import LoqedDataCoordinator, StatusMessage +from .entity import LoqedEntity + +SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( + SensorEntityDescription( + key="ble_strength", + translation_key="ble_strength", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="battery_percentage", + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Loqed lock platform.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities(LoqedSensor(coordinator, sensor) for sensor in SENSORS) + + +class LoqedSensor(LoqedEntity, SensorEntity): + """Representation of Sensor state.""" + + def __init__( + self, coordinator: LoqedDataCoordinator, description: SensorEntityDescription + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{self.coordinator.lock.id}_{description.key}" + + @property + def data(self) -> StatusMessage: + """Return data object from DataUpdateCoordinator.""" + return self.coordinator.lock + + @property + def native_value(self) -> int: + """Return state of sensor.""" + return getattr(self.data, self.entity_description.key) diff --git a/homeassistant/components/loqed/strings.json b/homeassistant/components/loqed/strings.json index 6f3316b283f11f..3d31194f5a6ed4 100644 --- a/homeassistant/components/loqed/strings.json +++ b/homeassistant/components/loqed/strings.json @@ -17,5 +17,12 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "sensor": { + "ble_strength": { + "name": "Bluetooth signal" + } + } } } diff --git a/tests/components/loqed/conftest.py b/tests/components/loqed/conftest.py index da7009a5744b34..be57237afdc784 100644 --- a/tests/components/loqed/conftest.py +++ b/tests/components/loqed/conftest.py @@ -48,6 +48,7 @@ def lock_fixture() -> loqed.Lock: mock_lock.name = "LOQED smart lock" mock_lock.getWebhooks = AsyncMock(return_value=webhooks_fixture) mock_lock.bolt_state = "locked" + mock_lock.battery_percentage = 90 return mock_lock From 66a1e5c2c1c811d345073ec81f85377262ad2601 Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Fri, 7 Jul 2023 00:43:46 -0400 Subject: [PATCH 0205/1009] Remove copy/pasted references to GMail in YouTube integration tests (#96048) These were likely used as an example when writing the tests for this component and we missed renaming them. A few unused vars with references to GMail were also removed. --- tests/components/youtube/conftest.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/components/youtube/conftest.py b/tests/components/youtube/conftest.py index 6513c359a7c95a..d87a3c07679c46 100644 --- a/tests/components/youtube/conftest.py +++ b/tests/components/youtube/conftest.py @@ -1,4 +1,4 @@ -"""Configure tests for the Google Mail integration.""" +"""Configure tests for the YouTube integration.""" from collections.abc import Awaitable, Callable, Coroutine import time from typing import Any @@ -20,7 +20,6 @@ ComponentSetup = Callable[[], Awaitable[None]] -BUILD = "homeassistant.components.google_mail.api.build" CLIENT_ID = "1234" CLIENT_SECRET = "5678" GOOGLE_AUTH_URI = "https://accounts.google.com/o/oauth2/v2/auth" @@ -28,7 +27,6 @@ SCOPES = [ "https://www.googleapis.com/auth/youtube.readonly", ] -SENSOR = "sensor.example_gmail_com_vacation_end_date" TITLE = "Google for Developers" TOKEN = "homeassistant.components.youtube.api.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid" @@ -59,7 +57,7 @@ def mock_expires_at() -> int: @pytest.fixture(name="config_entry") def mock_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry: - """Create Google Mail entry in Home Assistant.""" + """Create YouTube entry in Home Assistant.""" return MockConfigEntry( domain=DOMAIN, title=TITLE, @@ -79,7 +77,7 @@ def mock_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry: @pytest.fixture(autouse=True) def mock_connection(aioclient_mock: AiohttpClientMocker) -> None: - """Mock Google Mail connection.""" + """Mock YouTube connection.""" aioclient_mock.post( GOOGLE_TOKEN_URI, json={ From 4bf37209116a471491c7a375ede93dd454559d0e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jul 2023 06:44:09 +0200 Subject: [PATCH 0206/1009] Add entity translations to RFXTRX (#96041) --- homeassistant/components/rfxtrx/sensor.py | 45 ++++++--------- homeassistant/components/rfxtrx/strings.json | 58 ++++++++++++++++++++ 2 files changed, 76 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 3613a640f1abd4..60f35a93d1a89e 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -70,14 +70,12 @@ class RfxtrxSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES = ( RfxtrxSensorEntityDescription( key="Barometer", - name="Barometer", device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.HPA, ), RfxtrxSensorEntityDescription( key="Battery numeric", - name="Battery", device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, @@ -86,49 +84,46 @@ class RfxtrxSensorEntityDescription(SensorEntityDescription): ), RfxtrxSensorEntityDescription( key="Current", - name="Current", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, ), RfxtrxSensorEntityDescription( key="Current Ch. 1", - name="Current Ch. 1", + translation_key="current_ch_1", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, ), RfxtrxSensorEntityDescription( key="Current Ch. 2", - name="Current Ch. 2", + translation_key="current_ch_2", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, ), RfxtrxSensorEntityDescription( key="Current Ch. 3", - name="Current Ch. 3", + translation_key="current_ch_3", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, ), RfxtrxSensorEntityDescription( key="Energy usage", - name="Instantaneous power", + translation_key="instantaneous_power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, ), RfxtrxSensorEntityDescription( key="Humidity", - name="Humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), RfxtrxSensorEntityDescription( key="Rssi numeric", - name="Signal strength", device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -137,108 +132,104 @@ class RfxtrxSensorEntityDescription(SensorEntityDescription): ), RfxtrxSensorEntityDescription( key="Temperature", - name="Temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), RfxtrxSensorEntityDescription( key="Temperature2", - name="Temperature 2", + translation_key="temperature_2", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), RfxtrxSensorEntityDescription( key="Total usage", - name="Total energy usage", + translation_key="total_energy_usage", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, ), RfxtrxSensorEntityDescription( key="Voltage", - name="Voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, ), RfxtrxSensorEntityDescription( key="Wind direction", - name="Wind direction", + translation_key="wind_direction", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=DEGREE, ), RfxtrxSensorEntityDescription( key="Rain rate", - name="Rain rate", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, ), RfxtrxSensorEntityDescription( key="Sound", - name="Sound", + translation_key="sound", ), RfxtrxSensorEntityDescription( key="Sensor Status", - name="Sensor status", + translation_key="sensor_status", ), RfxtrxSensorEntityDescription( key="Count", - name="Count", + translation_key="count", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement="count", ), RfxtrxSensorEntityDescription( key="Counter value", - name="Counter value", + translation_key="counter_value", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement="count", ), RfxtrxSensorEntityDescription( key="Chill", - name="Chill", + translation_key="chill", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), RfxtrxSensorEntityDescription( key="Wind average speed", - name="Wind average speed", + translation_key="wind_average_speed", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, device_class=SensorDeviceClass.WIND_SPEED, ), RfxtrxSensorEntityDescription( key="Wind gust", - name="Wind gust", + translation_key="wind_gust", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, device_class=SensorDeviceClass.WIND_SPEED, ), RfxtrxSensorEntityDescription( key="Rain total", - name="Rain total", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, device_class=SensorDeviceClass.PRECIPITATION, ), RfxtrxSensorEntityDescription( key="Forecast", - name="Forecast status", + translation_key="forecast_status", ), RfxtrxSensorEntityDescription( key="Forecast numeric", - name="Forecast", + translation_key="forecast", ), RfxtrxSensorEntityDescription( key="Humidity status", - name="Humidity status", + translation_key="humidity_status", ), RfxtrxSensorEntityDescription( key="UV", - name="UV index", + translation_key="uv_index", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UV_INDEX, ), diff --git a/homeassistant/components/rfxtrx/strings.json b/homeassistant/components/rfxtrx/strings.json index 4469fd59801f41..7e68f960fca6db 100644 --- a/homeassistant/components/rfxtrx/strings.json +++ b/homeassistant/components/rfxtrx/strings.json @@ -78,5 +78,63 @@ "status": "Received status: {subtype}", "command": "Received command: {subtype}" } + }, + "entity": { + "sensor": { + "current_ch_1": { + "name": "Current Ch. 1" + }, + "current_ch_2": { + "name": "Current Ch. 2" + }, + "current_ch_3": { + "name": "Current Ch. 3" + }, + "instantaneous_power": { + "name": "Instantaneous power" + }, + "temperature_2": { + "name": "Temperature 2" + }, + "total_energy_usage": { + "name": "Total energy usage" + }, + "wind_direction": { + "name": "Wind direction" + }, + "sound": { + "name": "Sound" + }, + "sensor_status": { + "name": "Sensor status" + }, + "count": { + "name": "Count" + }, + "counter_value": { + "name": "Counter value" + }, + "chill": { + "name": "Chill" + }, + "wind_average_speed": { + "name": "Wind average speed" + }, + "wind_gust": { + "name": "Wind gust" + }, + "forecast_status": { + "name": "Forecast status" + }, + "forecast": { + "name": "Forecast" + }, + "humidity_status": { + "name": "Humidity status" + }, + "uv_index": { + "name": "UV index" + } + } } } From 8c5df60cc38530c8647b0de037020dcabd3f6ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Moreno?= Date: Fri, 7 Jul 2023 10:27:28 +0200 Subject: [PATCH 0207/1009] Revert zwave_js change to THERMOSTAT_MODE_SETPOINT_MAP (#96058) Remove THERMOSTAT_MODE_SETPOINT_MAP map Signed-off-by: Adrian Moreno --- homeassistant/components/zwave_js/climate.py | 23 +------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index f38508ec09c780..cb027f32e0a5da 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -9,6 +9,7 @@ THERMOSTAT_CURRENT_TEMP_PROPERTY, THERMOSTAT_HUMIDITY_PROPERTY, THERMOSTAT_MODE_PROPERTY, + THERMOSTAT_MODE_SETPOINT_MAP, THERMOSTAT_OPERATING_STATE_PROPERTY, THERMOSTAT_SETPOINT_PROPERTY, ThermostatMode, @@ -56,28 +57,6 @@ ThermostatMode.DRY, ] -THERMOSTAT_MODE_SETPOINT_MAP: dict[int, list[ThermostatSetpointType]] = { - ThermostatMode.OFF: [], - ThermostatMode.HEAT: [ThermostatSetpointType.HEATING], - ThermostatMode.COOL: [ThermostatSetpointType.COOLING], - ThermostatMode.AUTO: [ - ThermostatSetpointType.HEATING, - ThermostatSetpointType.COOLING, - ], - ThermostatMode.AUXILIARY: [ThermostatSetpointType.HEATING], - ThermostatMode.FURNACE: [ThermostatSetpointType.FURNACE], - ThermostatMode.DRY: [ThermostatSetpointType.DRY_AIR], - ThermostatMode.MOIST: [ThermostatSetpointType.MOIST_AIR], - ThermostatMode.AUTO_CHANGE_OVER: [ThermostatSetpointType.AUTO_CHANGEOVER], - ThermostatMode.HEATING_ECON: [ThermostatSetpointType.ENERGY_SAVE_HEATING], - ThermostatMode.COOLING_ECON: [ThermostatSetpointType.ENERGY_SAVE_COOLING], - ThermostatMode.AWAY: [ - ThermostatSetpointType.AWAY_HEATING, - ThermostatSetpointType.AWAY_COOLING, - ], - ThermostatMode.FULL_POWER: [ThermostatSetpointType.FULL_POWER], -} - # Map Z-Wave HVAC Mode to Home Assistant value # Note: We treat "auto" as "heat_cool" as most Z-Wave devices # report auto_changeover as auto without schedule support. From 84979f8e920889ed8974f215c4252c39c40b7389 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jul 2023 10:34:11 +0200 Subject: [PATCH 0208/1009] Use device class naming in Renault (#96038) --- .../components/renault/binary_sensor.py | 3 - homeassistant/components/renault/sensor.py | 1 - homeassistant/components/renault/strings.json | 12 ---- tests/components/renault/const.py | 6 +- .../renault/snapshots/test_binary_sensor.ambr | 36 +++++------ .../renault/snapshots/test_sensor.ambr | 60 +++++++++---------- 6 files changed, 51 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/renault/binary_sensor.py b/homeassistant/components/renault/binary_sensor.py index 83d86745d90b02..ef2d7196f04852 100644 --- a/homeassistant/components/renault/binary_sensor.py +++ b/homeassistant/components/renault/binary_sensor.py @@ -87,7 +87,6 @@ def icon(self) -> str | None: device_class=BinarySensorDeviceClass.PLUG, on_key="plugStatus", on_value=PlugState.PLUGGED.value, - translation_key="plugged_in", ), RenaultBinarySensorEntityDescription( key="charging", @@ -95,7 +94,6 @@ def icon(self) -> str | None: device_class=BinarySensorDeviceClass.BATTERY_CHARGING, on_key="chargingStatus", on_value=ChargeState.CHARGE_IN_PROGRESS.value, - translation_key="charging", ), RenaultBinarySensorEntityDescription( key="hvac_status", @@ -112,7 +110,6 @@ def icon(self) -> str | None: device_class=BinarySensorDeviceClass.LOCK, on_key="lockStatus", on_value="unlocked", - translation_key="lock_status", ), RenaultBinarySensorEntityDescription( key="hatch_status", diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index 90ad70521df321..050c5a930f6be6 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -165,7 +165,6 @@ def _get_utc_value(entity: RenaultSensor[T]) -> datetime: entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, - translation_key="battery_level", ), RenaultSensorEntityDescription( key="charge_state", diff --git a/homeassistant/components/renault/strings.json b/homeassistant/components/renault/strings.json index 066b49abcc0983..7cf016187be0c9 100644 --- a/homeassistant/components/renault/strings.json +++ b/homeassistant/components/renault/strings.json @@ -34,9 +34,6 @@ }, "entity": { "binary_sensor": { - "charging": { - "name": "[%key:component::binary_sensor::entity_component::battery_charging::name%]" - }, "hatch_status": { "name": "Hatch" }, @@ -46,15 +43,9 @@ "hvac_status": { "name": "HVAC" }, - "lock_status": { - "name": "[%key:component::binary_sensor::entity_component::lock::name%]" - }, "passenger_door_status": { "name": "Passenger door" }, - "plugged_in": { - "name": "[%key:component::binary_sensor::entity_component::plug::name%]" - }, "rear_left_door_status": { "name": "Rear left door" }, @@ -101,9 +92,6 @@ "battery_last_activity": { "name": "Last battery activity" }, - "battery_level": { - "name": "Battery level" - }, "battery_temperature": { "name": "Battery temperature" }, diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index 2aceb5e74894c9..4b2a7dfc72bb46 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -151,7 +151,7 @@ }, { ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, - ATTR_ENTITY_ID: "sensor.reg_number_battery_level", + ATTR_ENTITY_ID: "sensor.reg_number_battery", ATTR_STATE: "60", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_level", @@ -386,7 +386,7 @@ }, { ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, - ATTR_ENTITY_ID: "sensor.reg_number_battery_level", + ATTR_ENTITY_ID: "sensor.reg_number_battery", ATTR_STATE: "50", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_level", @@ -621,7 +621,7 @@ }, { ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, - ATTR_ENTITY_ID: "sensor.reg_number_battery_level", + ATTR_ENTITY_ID: "sensor.reg_number_battery", ATTR_STATE: "60", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_level", diff --git a/tests/components/renault/snapshots/test_binary_sensor.ambr b/tests/components/renault/snapshots/test_binary_sensor.ambr index dc10dd839f0c7c..9625810bedb50e 100644 --- a/tests/components/renault/snapshots/test_binary_sensor.ambr +++ b/tests/components/renault/snapshots/test_binary_sensor.ambr @@ -54,7 +54,7 @@ 'original_name': 'Lock', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'lock_status', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777123_lock_status', 'unit_of_measurement': None, }), @@ -325,7 +325,7 @@ 'original_name': 'Plug', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'plugged_in', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777123_plugged_in', 'unit_of_measurement': None, }), @@ -353,7 +353,7 @@ 'original_name': 'Charging', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'charging', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777123_charging', 'unit_of_measurement': None, }), @@ -381,7 +381,7 @@ 'original_name': 'Lock', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'lock_status', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777123_lock_status', 'unit_of_measurement': None, }), @@ -674,7 +674,7 @@ 'original_name': 'Plug', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'plugged_in', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777999_plugged_in', 'unit_of_measurement': None, }), @@ -702,7 +702,7 @@ 'original_name': 'Charging', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'charging', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777999_charging', 'unit_of_measurement': None, }), @@ -828,7 +828,7 @@ 'original_name': 'Plug', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'plugged_in', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777999_plugged_in', 'unit_of_measurement': None, }), @@ -856,7 +856,7 @@ 'original_name': 'Charging', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'charging', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777999_charging', 'unit_of_measurement': None, }), @@ -912,7 +912,7 @@ 'original_name': 'Lock', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'lock_status', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777999_lock_status', 'unit_of_measurement': None, }), @@ -1216,7 +1216,7 @@ 'original_name': 'Lock', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'lock_status', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777123_lock_status', 'unit_of_measurement': None, }), @@ -1487,7 +1487,7 @@ 'original_name': 'Plug', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'plugged_in', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777123_plugged_in', 'unit_of_measurement': None, }), @@ -1515,7 +1515,7 @@ 'original_name': 'Charging', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'charging', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777123_charging', 'unit_of_measurement': None, }), @@ -1543,7 +1543,7 @@ 'original_name': 'Lock', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'lock_status', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777123_lock_status', 'unit_of_measurement': None, }), @@ -1836,7 +1836,7 @@ 'original_name': 'Plug', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'plugged_in', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777999_plugged_in', 'unit_of_measurement': None, }), @@ -1864,7 +1864,7 @@ 'original_name': 'Charging', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'charging', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777999_charging', 'unit_of_measurement': None, }), @@ -1990,7 +1990,7 @@ 'original_name': 'Plug', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'plugged_in', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777999_plugged_in', 'unit_of_measurement': None, }), @@ -2018,7 +2018,7 @@ 'original_name': 'Charging', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'charging', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777999_charging', 'unit_of_measurement': None, }), @@ -2074,7 +2074,7 @@ 'original_name': 'Lock', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'lock_status', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777999_lock_status', 'unit_of_measurement': None, }), diff --git a/tests/components/renault/snapshots/test_sensor.ambr b/tests/components/renault/snapshots/test_sensor.ambr index 72f9201b7a426f..b4e2f105b3b34d 100644 --- a/tests/components/renault/snapshots/test_sensor.ambr +++ b/tests/components/renault/snapshots/test_sensor.ambr @@ -327,7 +327,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.reg_number_battery_level', + 'entity_id': 'sensor.reg_number_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -337,10 +337,10 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery level', + 'original_name': 'Battery', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'battery_level', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777123_battery_level', 'unit_of_measurement': '%', }), @@ -777,12 +777,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'REG-NUMBER Battery level', + 'friendly_name': 'REG-NUMBER Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.reg_number_battery_level', + 'entity_id': 'sensor.reg_number_battery', 'last_changed': , 'last_updated': , 'state': 'unknown', @@ -1023,7 +1023,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.reg_number_battery_level', + 'entity_id': 'sensor.reg_number_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1033,10 +1033,10 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery level', + 'original_name': 'Battery', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'battery_level', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777999_battery_level', 'unit_of_measurement': '%', }), @@ -1471,12 +1471,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'REG-NUMBER Battery level', + 'friendly_name': 'REG-NUMBER Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.reg_number_battery_level', + 'entity_id': 'sensor.reg_number_battery', 'last_changed': , 'last_updated': , 'state': 'unknown', @@ -1713,7 +1713,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.reg_number_battery_level', + 'entity_id': 'sensor.reg_number_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1723,10 +1723,10 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery level', + 'original_name': 'Battery', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'battery_level', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777999_battery_level', 'unit_of_measurement': '%', }), @@ -2189,12 +2189,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'REG-NUMBER Battery level', + 'friendly_name': 'REG-NUMBER Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.reg_number_battery_level', + 'entity_id': 'sensor.reg_number_battery', 'last_changed': , 'last_updated': , 'state': 'unknown', @@ -2726,7 +2726,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.reg_number_battery_level', + 'entity_id': 'sensor.reg_number_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2736,10 +2736,10 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery level', + 'original_name': 'Battery', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'battery_level', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777123_battery_level', 'unit_of_measurement': '%', }), @@ -3176,12 +3176,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'REG-NUMBER Battery level', + 'friendly_name': 'REG-NUMBER Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.reg_number_battery_level', + 'entity_id': 'sensor.reg_number_battery', 'last_changed': , 'last_updated': , 'state': '60', @@ -3422,7 +3422,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.reg_number_battery_level', + 'entity_id': 'sensor.reg_number_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3432,10 +3432,10 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery level', + 'original_name': 'Battery', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'battery_level', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777999_battery_level', 'unit_of_measurement': '%', }), @@ -3870,12 +3870,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'REG-NUMBER Battery level', + 'friendly_name': 'REG-NUMBER Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.reg_number_battery_level', + 'entity_id': 'sensor.reg_number_battery', 'last_changed': , 'last_updated': , 'state': '60', @@ -4112,7 +4112,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.reg_number_battery_level', + 'entity_id': 'sensor.reg_number_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4122,10 +4122,10 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery level', + 'original_name': 'Battery', 'platform': 'renault', 'supported_features': 0, - 'translation_key': 'battery_level', + 'translation_key': None, 'unique_id': 'vf1aaaaa555777999_battery_level', 'unit_of_measurement': '%', }), @@ -4588,12 +4588,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'REG-NUMBER Battery level', + 'friendly_name': 'REG-NUMBER Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.reg_number_battery_level', + 'entity_id': 'sensor.reg_number_battery', 'last_changed': , 'last_updated': , 'state': '50', From 86a397720f6c1e90bd3ce2cdde64d8d06e3e3402 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 7 Jul 2023 13:31:54 +0200 Subject: [PATCH 0209/1009] Move platform_integration_no_support issue to the homeassistant integration (#95927) * Move platform_integration_no_support issue to the homeassistant integration * Update test * Improve repair text * Update test --- .../components/alarm_control_panel/strings.json | 6 ------ .../components/binary_sensor/strings.json | 6 ------ homeassistant/components/button/strings.json | 6 ------ homeassistant/components/calendar/strings.json | 6 ------ homeassistant/components/camera/strings.json | 6 ------ homeassistant/components/climate/strings.json | 6 ------ homeassistant/components/cover/strings.json | 6 ------ homeassistant/components/date/strings.json | 6 ------ .../components/device_tracker/strings.json | 6 ------ homeassistant/components/fan/strings.json | 6 ------ .../components/homeassistant/strings.json | 4 ++++ homeassistant/components/humidifier/strings.json | 6 ------ homeassistant/components/light/strings.json | 6 ------ homeassistant/components/lock/strings.json | 6 ------ .../components/media_player/strings.json | 6 ------ homeassistant/components/number/strings.json | 6 ------ homeassistant/components/remote/strings.json | 6 ------ homeassistant/components/select/strings.json | 6 ------ homeassistant/components/sensor/strings.json | 6 ------ homeassistant/components/siren/strings.json | 6 ------ homeassistant/components/switch/strings.json | 6 ------ homeassistant/components/text/strings.json | 6 ------ homeassistant/components/time/strings.json | 6 ------ homeassistant/components/update/strings.json | 6 ------ homeassistant/components/vacuum/strings.json | 6 ------ .../components/water_heater/strings.json | 6 ------ homeassistant/components/weather/strings.json | 6 ------ homeassistant/helpers/entity_platform.py | 16 ++++++++++++++-- homeassistant/strings.json | 4 ---- tests/helpers/test_entity_platform.py | 12 ++++++++++-- 30 files changed, 28 insertions(+), 164 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/strings.json b/homeassistant/components/alarm_control_panel/strings.json index 4025bbd4cc4e5c..6b01cab2beccfe 100644 --- a/homeassistant/components/alarm_control_panel/strings.json +++ b/homeassistant/components/alarm_control_panel/strings.json @@ -62,11 +62,5 @@ } } } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index b9c9b19a93c3f0..ee70420fec04a2 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -314,11 +314,5 @@ "smoke": "smoke", "sound": "sound", "vibration": "vibration" - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/button/strings.json b/homeassistant/components/button/strings.json index 006959d1b4cfe5..a92a5a0f38a7ad 100644 --- a/homeassistant/components/button/strings.json +++ b/homeassistant/components/button/strings.json @@ -21,11 +21,5 @@ "update": { "name": "Update" } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/calendar/strings.json b/homeassistant/components/calendar/strings.json index b28f741c3816c2..898953c18acdcc 100644 --- a/homeassistant/components/calendar/strings.json +++ b/homeassistant/components/calendar/strings.json @@ -32,11 +32,5 @@ } } } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/camera/strings.json b/homeassistant/components/camera/strings.json index f67097516b46a7..0722ec1c5e6dd2 100644 --- a/homeassistant/components/camera/strings.json +++ b/homeassistant/components/camera/strings.json @@ -34,11 +34,5 @@ } } } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index 5879c44db83b62..8034799a6d0c5f 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -104,11 +104,5 @@ "temperature": { "name": "Target temperature" } } } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/cover/strings.json b/homeassistant/components/cover/strings.json index 663df02a824063..2f61bd95083e94 100644 --- a/homeassistant/components/cover/strings.json +++ b/homeassistant/components/cover/strings.json @@ -76,11 +76,5 @@ "window": { "name": "Window" } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/date/strings.json b/homeassistant/components/date/strings.json index f2d2e5ef8e18f4..110a4cabb921fb 100644 --- a/homeassistant/components/date/strings.json +++ b/homeassistant/components/date/strings.json @@ -4,11 +4,5 @@ "_": { "name": "[%key:component::date::title%]" } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/device_tracker/strings.json b/homeassistant/components/device_tracker/strings.json index 54e4f922053a2c..c15b9723c972ec 100644 --- a/homeassistant/components/device_tracker/strings.json +++ b/homeassistant/components/device_tracker/strings.json @@ -41,11 +41,5 @@ } } } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index b69068d3d64e07..b16d6da6df5651 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -52,11 +52,5 @@ } } } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 0a41f9c7a99ea6..edb26c3622ef1e 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -19,6 +19,10 @@ "platform_only": { "title": "The {domain} integration does not support YAML configuration under its own key", "description": "The {domain} integration does not support configuration under its own key, it must be configured under its supported platforms.\n\nTo resolve this:\n\n1. Remove `{domain}:` from your YAML configuration file.\n\n2. Restart Home Assistant." + }, + "no_platform_setup": { + "title": "Unused YAML configuration for the {platform} integration", + "description": "It's not possible to configure {platform} {domain} by adding `{platform_key}` to the {domain} configuration. Please check the documentation for more information on how to set up this integration.\n\nTo resolve this:\n1. Remove `{platform_key}` occurences from the `{domain}:` configuration in your YAML configuration file.\n2. Restart Home Assistant.\n\nExample that should be removed:\n{yaml_example}\n" } }, "system_health": { diff --git a/homeassistant/components/humidifier/strings.json b/homeassistant/components/humidifier/strings.json index 7a2e371024fb78..f06bf7ccd598a7 100644 --- a/homeassistant/components/humidifier/strings.json +++ b/homeassistant/components/humidifier/strings.json @@ -74,11 +74,5 @@ "humidifier": { "name": "[%key:component::humidifier::entity_component::_::name%]" } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index f89497b5ef95f9..935e38d33d9647 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -86,11 +86,5 @@ } } } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/lock/strings.json b/homeassistant/components/lock/strings.json index b77bf5e6900a9e..da4b5217b86294 100644 --- a/homeassistant/components/lock/strings.json +++ b/homeassistant/components/lock/strings.json @@ -34,11 +34,5 @@ } } } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/media_player/strings.json b/homeassistant/components/media_player/strings.json index eed54ef58c3a96..2c63a543119669 100644 --- a/homeassistant/components/media_player/strings.json +++ b/homeassistant/components/media_player/strings.json @@ -159,11 +159,5 @@ "receiver": { "name": "Receiver" } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/number/strings.json b/homeassistant/components/number/strings.json index 9af54311129f4b..46db471305c315 100644 --- a/homeassistant/components/number/strings.json +++ b/homeassistant/components/number/strings.json @@ -154,11 +154,5 @@ "wind_speed": { "name": "[%key:component::sensor::entity_component::wind_speed::name%]" } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/remote/strings.json b/homeassistant/components/remote/strings.json index 18a92494242655..f0d2787b658659 100644 --- a/homeassistant/components/remote/strings.json +++ b/homeassistant/components/remote/strings.json @@ -24,11 +24,5 @@ "on": "[%key:common::state::on%]" } } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/select/strings.json b/homeassistant/components/select/strings.json index 53441d365b4ff9..9080b940b2a5f7 100644 --- a/homeassistant/components/select/strings.json +++ b/homeassistant/components/select/strings.json @@ -24,11 +24,5 @@ } } } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index d3dbbc678b0795..c4c1f81109d49a 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -267,11 +267,5 @@ "wind_speed": { "name": "Wind speed" } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/siren/strings.json b/homeassistant/components/siren/strings.json index c3dde16a99fec6..60d8843c1515e5 100644 --- a/homeassistant/components/siren/strings.json +++ b/homeassistant/components/siren/strings.json @@ -13,11 +13,5 @@ } } } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/switch/strings.json b/homeassistant/components/switch/strings.json index 2bb6c82a8c151a..a7934ba420927a 100644 --- a/homeassistant/components/switch/strings.json +++ b/homeassistant/components/switch/strings.json @@ -30,11 +30,5 @@ "outlet": { "name": "Outlet" } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/text/strings.json b/homeassistant/components/text/strings.json index d8f55dbe4e79c3..034f1ab315b820 100644 --- a/homeassistant/components/text/strings.json +++ b/homeassistant/components/text/strings.json @@ -27,11 +27,5 @@ } } } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/time/strings.json b/homeassistant/components/time/strings.json index 9cbcf718d73640..e8d92a30e2eac8 100644 --- a/homeassistant/components/time/strings.json +++ b/homeassistant/components/time/strings.json @@ -4,11 +4,5 @@ "_": { "name": "[%key:component::time::title%]" } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/update/strings.json b/homeassistant/components/update/strings.json index 518b8605aa764f..b69e0acf65e5f2 100644 --- a/homeassistant/components/update/strings.json +++ b/homeassistant/components/update/strings.json @@ -14,11 +14,5 @@ "firmware": { "name": "Firmware" } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index e0db3ba4e47db3..a27a60bba4f30b 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -28,11 +28,5 @@ "returning": "Returning to dock" } } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/water_heater/strings.json b/homeassistant/components/water_heater/strings.json index b0784279667303..6344b5a847a202 100644 --- a/homeassistant/components/water_heater/strings.json +++ b/homeassistant/components/water_heater/strings.json @@ -18,11 +18,5 @@ "performance": "Performance" } } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/components/weather/strings.json b/homeassistant/components/weather/strings.json index 53eca9c7f9193c..26ccd731828267 100644 --- a/homeassistant/components/weather/strings.json +++ b/homeassistant/components/weather/strings.json @@ -74,11 +74,5 @@ } } } - }, - "issues": { - "platform_integration_no_support": { - "title": "[%key:common::issues::platform_integration_no_support_title%]", - "description": "[%key:common::issues::platform_integration_no_support_description%]" - } } } diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 66a74edf8f91a1..55d167ae253c6e 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -19,6 +19,7 @@ ) from homeassistant.core import ( CALLBACK_TYPE, + DOMAIN as HOMEASSISTANT_DOMAIN, CoreState, HomeAssistant, ServiceCall, @@ -216,16 +217,27 @@ async def async_setup( self.platform_name, self.domain, ) + learn_more_url = None + if self.platform and "custom_components" not in self.platform.__file__: # type: ignore[attr-defined] + learn_more_url = ( + f"https://www.home-assistant.io/integrations/{self.platform_name}/" + ) + platform_key = f"platform: {self.platform_name}" + yaml_example = f"```yaml\n{self.domain}:\n - {platform_key}\n```" async_create_issue( self.hass, - self.domain, + HOMEASSISTANT_DOMAIN, f"platform_integration_no_support_{self.domain}_{self.platform_name}", is_fixable=False, + issue_domain=self.platform_name, + learn_more_url=learn_more_url, severity=IssueSeverity.ERROR, - translation_key="platform_integration_no_support", + translation_key="no_platform_setup", translation_placeholders={ "domain": self.domain, "platform": self.platform_name, + "platform_key": platform_key, + "yaml_example": yaml_example, }, ) diff --git a/homeassistant/strings.json b/homeassistant/strings.json index 4da9c25ca107a9..c4cf0593aaeb54 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -87,10 +87,6 @@ "unknown_authorize_url_generation": "Unknown error generating an authorize URL.", "cloud_not_connected": "Not connected to Home Assistant Cloud." } - }, - "issues": { - "platform_integration_no_support_title": "Platform support not supported", - "platform_integration_no_support_description": "The {platform} platform for the {domain} integration does not support platform setup.\n\nPlease remove it from your configuration and restart Home Assistant to fix this issue." } } } diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index df4f4d1c6439a9..711c333c5ff4e9 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1477,11 +1477,19 @@ async def test_platform_with_no_setup( in caplog.text ) issue = issue_registry.async_get_issue( - domain="mock-integration", + domain="homeassistant", issue_id="platform_integration_no_support_mock-integration_mock-platform", ) assert issue - assert issue.translation_key == "platform_integration_no_support" + assert issue.issue_domain == "mock-platform" + assert issue.learn_more_url is None + assert issue.translation_key == "no_platform_setup" + assert issue.translation_placeholders == { + "domain": "mock-integration", + "platform": "mock-platform", + "platform_key": "platform: mock-platform", + "yaml_example": "```yaml\nmock-integration:\n - platform: mock-platform\n```", + } async def test_platforms_sharing_services(hass: HomeAssistant) -> None: From 70445c0edd05d45dd1929a409c6c311949a69858 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jul 2023 14:13:01 +0200 Subject: [PATCH 0210/1009] Add RDW codeowner (#96035) --- CODEOWNERS | 4 ++-- homeassistant/components/rdw/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 7e09c3c8147157..16c0426d87f620 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1002,8 +1002,8 @@ build.json @home-assistant/supervisor /tests/components/rapt_ble/ @sairon /homeassistant/components/raspberry_pi/ @home-assistant/core /tests/components/raspberry_pi/ @home-assistant/core -/homeassistant/components/rdw/ @frenck -/tests/components/rdw/ @frenck +/homeassistant/components/rdw/ @frenck @joostlek +/tests/components/rdw/ @frenck @joostlek /homeassistant/components/recollect_waste/ @bachya /tests/components/recollect_waste/ @bachya /homeassistant/components/recorder/ @home-assistant/core diff --git a/homeassistant/components/rdw/manifest.json b/homeassistant/components/rdw/manifest.json index 0b5640fe3a41eb..5df34652f2b2ed 100644 --- a/homeassistant/components/rdw/manifest.json +++ b/homeassistant/components/rdw/manifest.json @@ -1,7 +1,7 @@ { "domain": "rdw", "name": "RDW", - "codeowners": ["@frenck"], + "codeowners": ["@frenck", "@joostlek"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rdw", "integration_type": "service", From 7d9715259308f7a95466f52cce9cb4db4f673501 Mon Sep 17 00:00:00 2001 From: Barry Williams Date: Fri, 7 Jul 2023 13:24:42 +0100 Subject: [PATCH 0211/1009] 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 53b2478490da75..79653e1c9bc16c 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 @@ class ServiceDetails(NamedTuple): SERVICE_MOBILE_APP, SERVICE_NETGEAR, SERVICE_OCTOPRINT, + "openhome", "philips_hue", SERVICE_SAMSUNG_PRINTER, "sonos", From d202b7c3c77ef28169fd620f89801deb8b21c29f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jul 2023 14:40:22 +0200 Subject: [PATCH 0212/1009] Add entity translations to RDW (#96034) --- homeassistant/components/rdw/binary_sensor.py | 4 ++-- homeassistant/components/rdw/sensor.py | 4 ++-- homeassistant/components/rdw/strings.json | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rdw/binary_sensor.py b/homeassistant/components/rdw/binary_sensor.py index 13a045151435bf..9d895f35eb7c60 100644 --- a/homeassistant/components/rdw/binary_sensor.py +++ b/homeassistant/components/rdw/binary_sensor.py @@ -41,13 +41,13 @@ class RDWBinarySensorEntityDescription( BINARY_SENSORS: tuple[RDWBinarySensorEntityDescription, ...] = ( RDWBinarySensorEntityDescription( key="liability_insured", - name="Liability insured", + translation_key="liability_insured", icon="mdi:shield-car", is_on_fn=lambda vehicle: vehicle.liability_insured, ), RDWBinarySensorEntityDescription( key="pending_recall", - name="Pending recall", + translation_key="pending_recall", device_class=BinarySensorDeviceClass.PROBLEM, is_on_fn=lambda vehicle: vehicle.pending_recall, ), diff --git a/homeassistant/components/rdw/sensor.py b/homeassistant/components/rdw/sensor.py index e262665dd63a1e..2c324ca7093b9d 100644 --- a/homeassistant/components/rdw/sensor.py +++ b/homeassistant/components/rdw/sensor.py @@ -42,13 +42,13 @@ class RDWSensorEntityDescription( SENSORS: tuple[RDWSensorEntityDescription, ...] = ( RDWSensorEntityDescription( key="apk_expiration", - name="APK expiration", + translation_key="apk_expiration", device_class=SensorDeviceClass.DATE, value_fn=lambda vehicle: vehicle.apk_expiration, ), RDWSensorEntityDescription( key="ascription_date", - name="Ascription date", + translation_key="ascription_date", device_class=SensorDeviceClass.DATE, value_fn=lambda vehicle: vehicle.ascription_date, ), diff --git a/homeassistant/components/rdw/strings.json b/homeassistant/components/rdw/strings.json index 840802a12b7a92..cf24ec5115c592 100644 --- a/homeassistant/components/rdw/strings.json +++ b/homeassistant/components/rdw/strings.json @@ -14,5 +14,23 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown_license_plate": "Unknown license plate" } + }, + "entity": { + "binary_sensor": { + "liability_insured": { + "name": "Liability insured" + }, + "pending_recall": { + "name": "Pending recall" + } + }, + "sensor": { + "apk_expiration": { + "name": "APK expiration" + }, + "ascription_date": { + "name": "Ascription date" + } + } } } From 1aecbb9bd5e1cad82e457a690c471cb46bb8aa83 Mon Sep 17 00:00:00 2001 From: Jan Stienstra <65826735+j-stienstra@users.noreply.github.com> Date: Fri, 7 Jul 2023 14:42:02 +0200 Subject: [PATCH 0213/1009] Add full test coverage to Jellyfin (#86974) * Add full test coverage * Remove unreachable exception * Remove comment line. Conflicting with codecov * Use auto fixture and syrupy --- .../components/jellyfin/media_source.py | 6 +- tests/components/jellyfin/conftest.py | 16 +- tests/components/jellyfin/fixtures/album.json | 12 + .../components/jellyfin/fixtures/albums.json | 16 + .../components/jellyfin/fixtures/artist.json | 15 + .../components/jellyfin/fixtures/artists.json | 19 + .../components/jellyfin/fixtures/episode.json | 504 +++++++++++++++++ .../jellyfin/fixtures/episodes.json | 509 ++++++++++++++++++ .../fixtures/get-item-collection.json | 2 +- .../jellyfin/fixtures/media-source-root.json | 23 + .../jellyfin/fixtures/movie-collection.json | 45 ++ tests/components/jellyfin/fixtures/movie.json | 153 ++++++ .../components/jellyfin/fixtures/movies.json | 159 ++++++ .../jellyfin/fixtures/music-collection.json | 45 ++ .../components/jellyfin/fixtures/season.json | 23 + .../components/jellyfin/fixtures/seasons.json | 29 + .../jellyfin/fixtures/series-list.json | 34 ++ .../components/jellyfin/fixtures/series.json | 28 + tests/components/jellyfin/fixtures/track.json | 91 ++++ .../jellyfin/fixtures/tracks-nopath.json | 93 ++++ .../jellyfin/fixtures/tracks-nosource.json | 23 + .../fixtures/tracks-unknown-extension.json | 95 ++++ .../components/jellyfin/fixtures/tracks.json | 95 ++++ .../jellyfin/fixtures/tv-collection.json | 45 ++ .../jellyfin/fixtures/unsupported-item.json | 5 + .../jellyfin/snapshots/test_media_source.ambr | 135 +++++ tests/components/jellyfin/test_init.py | 20 + .../components/jellyfin/test_media_source.py | 303 +++++++++++ 28 files changed, 2536 insertions(+), 7 deletions(-) create mode 100644 tests/components/jellyfin/fixtures/album.json create mode 100644 tests/components/jellyfin/fixtures/albums.json create mode 100644 tests/components/jellyfin/fixtures/artist.json create mode 100644 tests/components/jellyfin/fixtures/artists.json create mode 100644 tests/components/jellyfin/fixtures/episode.json create mode 100644 tests/components/jellyfin/fixtures/episodes.json create mode 100644 tests/components/jellyfin/fixtures/media-source-root.json create mode 100644 tests/components/jellyfin/fixtures/movie-collection.json create mode 100644 tests/components/jellyfin/fixtures/movie.json create mode 100644 tests/components/jellyfin/fixtures/movies.json create mode 100644 tests/components/jellyfin/fixtures/music-collection.json create mode 100644 tests/components/jellyfin/fixtures/season.json create mode 100644 tests/components/jellyfin/fixtures/seasons.json create mode 100644 tests/components/jellyfin/fixtures/series-list.json create mode 100644 tests/components/jellyfin/fixtures/series.json create mode 100644 tests/components/jellyfin/fixtures/track.json create mode 100644 tests/components/jellyfin/fixtures/tracks-nopath.json create mode 100644 tests/components/jellyfin/fixtures/tracks-nosource.json create mode 100644 tests/components/jellyfin/fixtures/tracks-unknown-extension.json create mode 100644 tests/components/jellyfin/fixtures/tracks.json create mode 100644 tests/components/jellyfin/fixtures/tv-collection.json create mode 100644 tests/components/jellyfin/fixtures/unsupported-item.json create mode 100644 tests/components/jellyfin/snapshots/test_media_source.ambr create mode 100644 tests/components/jellyfin/test_media_source.py diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index f9c73443d00ab4..318798fdc5f2d3 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -21,7 +21,6 @@ from .const import ( COLLECTION_TYPE_MOVIES, COLLECTION_TYPE_MUSIC, - COLLECTION_TYPE_TVSHOWS, DOMAIN, ITEM_KEY_COLLECTION_TYPE, ITEM_KEY_ID, @@ -155,10 +154,7 @@ async def _build_library( return await self._build_music_library(library, include_children) if collection_type == COLLECTION_TYPE_MOVIES: return await self._build_movie_library(library, include_children) - if collection_type == COLLECTION_TYPE_TVSHOWS: - return await self._build_tv_library(library, include_children) - - raise BrowseError(f"Unsupported collection type {collection_type}") + return await self._build_tv_library(library, include_children) async def _build_music_library( self, library: dict[str, Any], include_children: bool diff --git a/tests/components/jellyfin/conftest.py b/tests/components/jellyfin/conftest.py index 423e4ad395069b..671c9881ae0529 100644 --- a/tests/components/jellyfin/conftest.py +++ b/tests/components/jellyfin/conftest.py @@ -74,6 +74,8 @@ def mock_api() -> MagicMock: jf_api.sessions.return_value = load_json_fixture("sessions.json") jf_api.artwork.side_effect = api_artwork_side_effect + jf_api.audio_url.side_effect = api_audio_url_side_effect + jf_api.video_url.side_effect = api_video_url_side_effect jf_api.user_items.side_effect = api_user_items_side_effect jf_api.get_item.side_effect = api_get_item_side_effect jf_api.get_media_folders.return_value = load_json_fixture("get-media-folders.json") @@ -86,7 +88,7 @@ def mock_api() -> MagicMock: def mock_config() -> MagicMock: """Return a mocked JellyfinClient.""" jf_config = create_autospec(Config) - jf_config.data = {} + jf_config.data = {"auth.server": "http://localhost"} return jf_config @@ -138,6 +140,18 @@ def api_artwork_side_effect(*args, **kwargs): return f"http://localhost/Items/{item_id}/Images/{art}.{ext}" +def api_audio_url_side_effect(*args, **kwargs): + """Handle variable responses for audio_url method.""" + item_id = args[0] + return f"http://localhost/Audio/{item_id}/universal?UserId=test-username,DeviceId=TEST-UUID,MaxStreamingBitrate=140000000" + + +def api_video_url_side_effect(*args, **kwargs): + """Handle variable responses for video_url method.""" + item_id = args[0] + return f"http://localhost/Videos/{item_id}/stream?static=true,DeviceId=TEST-UUID,api_key=TEST-API-KEY" + + def api_get_item_side_effect(*args): """Handle variable responses for get_item method.""" return load_json_fixture("get-item-collection.json") diff --git a/tests/components/jellyfin/fixtures/album.json b/tests/components/jellyfin/fixtures/album.json new file mode 100644 index 00000000000000..b748b125e4a895 --- /dev/null +++ b/tests/components/jellyfin/fixtures/album.json @@ -0,0 +1,12 @@ +{ + "AlbumArtist": "ARTIST", + "AlbumArtists": [{ "Id": "ARTIST-UUID", "Name": "ARTIST" }], + "Artists": ["ARTIST"], + "Id": "ALBUM-UUID", + "ImageTags": {}, + "IsFolder": true, + "Name": "ALBUM", + "PrimaryImageAspectRatio": 1, + "ServerId": "ServerId", + "Type": "MusicAlbum" +} diff --git a/tests/components/jellyfin/fixtures/albums.json b/tests/components/jellyfin/fixtures/albums.json new file mode 100644 index 00000000000000..e557018a89e90c --- /dev/null +++ b/tests/components/jellyfin/fixtures/albums.json @@ -0,0 +1,16 @@ +{ + "Items": [ + { + "AlbumArtist": "ARTIST", + "AlbumArtists": [{ "Id": "ARTIST-UUID", "Name": "ARTIST" }], + "Artists": ["ARTIST"], + "Id": "ALBUM-UUID", + "ImageTags": {}, + "IsFolder": true, + "Name": "ALBUM", + "PrimaryImageAspectRatio": 1, + "ServerId": "ServerId", + "Type": "MusicAlbum" + } + ] +} diff --git a/tests/components/jellyfin/fixtures/artist.json b/tests/components/jellyfin/fixtures/artist.json new file mode 100644 index 00000000000000..95e59d33820e14 --- /dev/null +++ b/tests/components/jellyfin/fixtures/artist.json @@ -0,0 +1,15 @@ +{ + "AlbumCount": 1, + "Id": "ARTIST-UUID", + "ImageTags": { + "Logo": "string", + "Primary": "string" + }, + "IsFolder": true, + "Name": "ARTIST", + "ParentId": "MUSIC-COLLECTION-FOLDER-UUID", + "Path": "/media/music/artist", + "PrimaryImageAspectRatio": 1, + "ServerId": "string", + "Type": "MusicArtist" +} diff --git a/tests/components/jellyfin/fixtures/artists.json b/tests/components/jellyfin/fixtures/artists.json new file mode 100644 index 00000000000000..bb57ef451a24e4 --- /dev/null +++ b/tests/components/jellyfin/fixtures/artists.json @@ -0,0 +1,19 @@ +{ + "Items": [ + { + "AlbumCount": 1, + "Id": "ARTIST-UUID", + "ImageTags": { + "Logo": "string", + "Primary": "string" + }, + "IsFolder": true, + "Name": "ARTIST", + "ParentId": "MUSIC-COLLECTION-FOLDER-UUID", + "Path": "/media/music/artist", + "PrimaryImageAspectRatio": 1, + "ServerId": "string", + "Type": "MusicArtist" + } + ] +} diff --git a/tests/components/jellyfin/fixtures/episode.json b/tests/components/jellyfin/fixtures/episode.json new file mode 100644 index 00000000000000..49f30434eac108 --- /dev/null +++ b/tests/components/jellyfin/fixtures/episode.json @@ -0,0 +1,504 @@ +{ + "Name": "EPISODE", + "OriginalTitle": "string", + "ServerId": "SERVER-UUID", + "Id": "EPISODE-UUID", + "Etag": "string", + "SourceType": "string", + "PlaylistItemId": "string", + "DateCreated": "2019-08-24T14:15:22Z", + "DateLastMediaAdded": "2019-08-24T14:15:22Z", + "ExtraType": "string", + "AirsBeforeSeasonNumber": 0, + "AirsAfterSeasonNumber": 0, + "AirsBeforeEpisodeNumber": 0, + "CanDelete": true, + "CanDownload": true, + "HasSubtitles": true, + "PreferredMetadataLanguage": "string", + "PreferredMetadataCountryCode": "string", + "SupportsSync": true, + "Container": "string", + "SortName": "string", + "ForcedSortName": "string", + "Video3DFormat": "HalfSideBySide", + "PremiereDate": "2019-08-24T14:15:22Z", + "ExternalUrls": [ + { + "Name": "string", + "Url": "string" + } + ], + "MediaSources": [ + { + "Protocol": "File", + "Id": "string", + "Path": "/media/tvshows/Series/Season 01/S01E01.mp4", + "EncoderPath": "string", + "EncoderProtocol": "File", + "Type": "Default", + "Container": "string", + "Size": 0, + "Name": "string", + "IsRemote": true, + "ETag": "string", + "RunTimeTicks": 0, + "ReadAtNativeFramerate": true, + "IgnoreDts": true, + "IgnoreIndex": true, + "GenPtsInput": true, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "IsInfiniteStream": true, + "RequiresOpening": true, + "OpenToken": "string", + "RequiresClosing": true, + "LiveStreamId": "string", + "BufferMs": 0, + "RequiresLooping": true, + "SupportsProbing": true, + "VideoType": "VideoFile", + "IsoType": "Dvd", + "Video3DFormat": "HalfSideBySide", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "MediaAttachments": [ + { + "Codec": "string", + "CodecTag": "string", + "Comment": "string", + "Index": 0, + "FileName": "string", + "MimeType": "string", + "DeliveryUrl": "string" + } + ], + "Formats": ["string"], + "Bitrate": 0, + "Timestamp": "None", + "RequiredHttpHeaders": { + "property1": "string", + "property2": "string" + }, + "TranscodingUrl": "string", + "TranscodingSubProtocol": "string", + "TranscodingContainer": "string", + "AnalyzeDurationMs": 0, + "DefaultAudioStreamIndex": 0, + "DefaultSubtitleStreamIndex": 0 + } + ], + "CriticRating": 0, + "ProductionLocations": ["string"], + "Path": "string", + "EnableMediaSourceDisplay": true, + "OfficialRating": "string", + "CustomRating": "string", + "ChannelId": "04b0b2a5-93cb-474d-8ea9-3df0f84eb0ff", + "ChannelName": "string", + "Overview": "string", + "Taglines": ["string"], + "Genres": ["string"], + "CommunityRating": 0, + "CumulativeRunTimeTicks": 0, + "RunTimeTicks": 0, + "PlayAccess": "Full", + "AspectRatio": "string", + "ProductionYear": 0, + "IsPlaceHolder": true, + "Number": "string", + "ChannelNumber": "string", + "IndexNumber": 0, + "IndexNumberEnd": 0, + "ParentIndexNumber": 0, + "RemoteTrailers": [ + { + "Url": "string", + "Name": "string" + } + ], + "ProviderIds": { + "property1": "string", + "property2": "string" + }, + "IsHD": true, + "IsFolder": false, + "ParentId": "FOLDER-UUID", + "Type": "Episode", + "People": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "Role": "string", + "Type": "string", + "PrimaryImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + } + } + ], + "Studios": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "GenreItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "ParentLogoItemId": "c78d400f-de5c-421e-8714-4fb05d387233", + "ParentBackdropItemId": "c22fd826-17fc-44f4-9b04-1eb3e8fb9173", + "ParentBackdropImageTags": ["string"], + "LocalTrailerCount": 0, + "UserData": { + "Rating": 0, + "PlayedPercentage": 0, + "UnplayedItemCount": 0, + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": true, + "Likes": true, + "LastPlayedDate": "2019-08-24T14:15:22Z", + "Played": true, + "Key": "string", + "ItemId": "string" + }, + "RecursiveItemCount": 0, + "ChildCount": 0, + "SeriesName": "string", + "SeriesId": "c7b70af4-4902-4a7e-95ab-28349b6c7afc", + "SeasonId": "badb6463-e5b7-45c5-8141-71204420ec8f", + "SpecialFeatureCount": 0, + "DisplayPreferencesId": "string", + "Status": "string", + "AirTime": "string", + "AirDays": ["Sunday"], + "Tags": ["string"], + "PrimaryImageAspectRatio": 0, + "Artists": ["string"], + "ArtistItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "Album": "string", + "CollectionType": "string", + "DisplayOrder": "string", + "AlbumId": "21af9851-8e39-43a9-9c47-513d3b9e99fc", + "AlbumPrimaryImageTag": "string", + "SeriesPrimaryImageTag": "string", + "AlbumArtist": "string", + "AlbumArtists": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "SeasonName": "string", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "VideoType": "VideoFile", + "PartCount": 0, + "MediaSourceCount": 0, + "ImageTags": { + "property1": "string", + "property2": "string" + }, + "BackdropImageTags": ["string"], + "ScreenshotImageTags": ["string"], + "ParentLogoImageTag": "string", + "ParentArtItemId": "10c1875b-b82c-48e8-bae9-939a5e68dc2f", + "ParentArtImageTag": "string", + "SeriesThumbImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + }, + "SeriesStudio": "string", + "ParentThumbItemId": "ae6ff707-333d-4994-be6d-b83ca1b35f46", + "ParentThumbImageTag": "string", + "ParentPrimaryImageItemId": "string", + "ParentPrimaryImageTag": "string", + "Chapters": [ + { + "StartPositionTicks": 0, + "Name": "string", + "ImagePath": "string", + "ImageDateModified": "2019-08-24T14:15:22Z", + "ImageTag": "string" + } + ], + "LocationType": "FileSystem", + "IsoType": "Dvd", + "MediaType": "string", + "EndDate": "2019-08-24T14:15:22Z", + "LockedFields": ["Cast"], + "TrailerCount": 0, + "MovieCount": 0, + "SeriesCount": 0, + "ProgramCount": 0, + "EpisodeCount": 0, + "SongCount": 0, + "AlbumCount": 0, + "ArtistCount": 0, + "MusicVideoCount": 0, + "LockData": true, + "Width": 0, + "Height": 0, + "CameraMake": "string", + "CameraModel": "string", + "Software": "string", + "ExposureTime": 0, + "FocalLength": 0, + "ImageOrientation": "TopLeft", + "Aperture": 0, + "ShutterSpeed": 0, + "Latitude": 0, + "Longitude": 0, + "Altitude": 0, + "IsoSpeedRating": 0, + "SeriesTimerId": "string", + "ProgramId": "string", + "ChannelPrimaryImageTag": "string", + "StartDate": "2019-08-24T14:15:22Z", + "CompletionPercentage": 0, + "IsRepeat": true, + "EpisodeTitle": "string", + "ChannelType": "TV", + "Audio": "Mono", + "IsMovie": true, + "IsSports": true, + "IsSeries": true, + "IsLive": true, + "IsNews": true, + "IsKids": true, + "IsPremiere": true, + "TimerId": "string", + "CurrentProgram": {} +} diff --git a/tests/components/jellyfin/fixtures/episodes.json b/tests/components/jellyfin/fixtures/episodes.json new file mode 100644 index 00000000000000..31b2fe76558ecf --- /dev/null +++ b/tests/components/jellyfin/fixtures/episodes.json @@ -0,0 +1,509 @@ +{ + "Items": [ + { + "Name": "EPISODE", + "OriginalTitle": "string", + "ServerId": "SERVER-UUID", + "Id": "EPISODE-UUID", + "Etag": "string", + "SourceType": "string", + "PlaylistItemId": "string", + "DateCreated": "2019-08-24T14:15:22Z", + "DateLastMediaAdded": "2019-08-24T14:15:22Z", + "ExtraType": "string", + "AirsBeforeSeasonNumber": 0, + "AirsAfterSeasonNumber": 0, + "AirsBeforeEpisodeNumber": 0, + "CanDelete": true, + "CanDownload": true, + "HasSubtitles": true, + "PreferredMetadataLanguage": "string", + "PreferredMetadataCountryCode": "string", + "SupportsSync": true, + "Container": "string", + "SortName": "string", + "ForcedSortName": "string", + "Video3DFormat": "HalfSideBySide", + "PremiereDate": "2019-08-24T14:15:22Z", + "ExternalUrls": [ + { + "Name": "string", + "Url": "string" + } + ], + "MediaSources": [ + { + "Protocol": "File", + "Id": "string", + "Path": "/media/tvshows/Series/Season 01/S01E01.mp4", + "EncoderPath": "string", + "EncoderProtocol": "File", + "Type": "Default", + "Container": "string", + "Size": 0, + "Name": "string", + "IsRemote": true, + "ETag": "string", + "RunTimeTicks": 0, + "ReadAtNativeFramerate": true, + "IgnoreDts": true, + "IgnoreIndex": true, + "GenPtsInput": true, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "IsInfiniteStream": true, + "RequiresOpening": true, + "OpenToken": "string", + "RequiresClosing": true, + "LiveStreamId": "string", + "BufferMs": 0, + "RequiresLooping": true, + "SupportsProbing": true, + "VideoType": "VideoFile", + "IsoType": "Dvd", + "Video3DFormat": "HalfSideBySide", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "MediaAttachments": [ + { + "Codec": "string", + "CodecTag": "string", + "Comment": "string", + "Index": 0, + "FileName": "string", + "MimeType": "string", + "DeliveryUrl": "string" + } + ], + "Formats": ["string"], + "Bitrate": 0, + "Timestamp": "None", + "RequiredHttpHeaders": { + "property1": "string", + "property2": "string" + }, + "TranscodingUrl": "string", + "TranscodingSubProtocol": "string", + "TranscodingContainer": "string", + "AnalyzeDurationMs": 0, + "DefaultAudioStreamIndex": 0, + "DefaultSubtitleStreamIndex": 0 + } + ], + "CriticRating": 0, + "ProductionLocations": ["string"], + "Path": "string", + "EnableMediaSourceDisplay": true, + "OfficialRating": "string", + "CustomRating": "string", + "ChannelId": "04b0b2a5-93cb-474d-8ea9-3df0f84eb0ff", + "ChannelName": "string", + "Overview": "string", + "Taglines": ["string"], + "Genres": ["string"], + "CommunityRating": 0, + "CumulativeRunTimeTicks": 0, + "RunTimeTicks": 0, + "PlayAccess": "Full", + "AspectRatio": "string", + "ProductionYear": 0, + "IsPlaceHolder": true, + "Number": "string", + "ChannelNumber": "string", + "IndexNumber": 0, + "IndexNumberEnd": 0, + "ParentIndexNumber": 0, + "RemoteTrailers": [ + { + "Url": "string", + "Name": "string" + } + ], + "ProviderIds": { + "property1": "string", + "property2": "string" + }, + "IsHD": true, + "IsFolder": false, + "ParentId": "FOLDER-UUID", + "Type": "Episode", + "People": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", + "Role": "string", + "Type": "string", + "PrimaryImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + } + } + ], + "Studios": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "GenreItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "ParentLogoItemId": "c78d400f-de5c-421e-8714-4fb05d387233", + "ParentBackdropItemId": "c22fd826-17fc-44f4-9b04-1eb3e8fb9173", + "ParentBackdropImageTags": ["string"], + "LocalTrailerCount": 0, + "UserData": { + "Rating": 0, + "PlayedPercentage": 0, + "UnplayedItemCount": 0, + "PlaybackPositionTicks": 0, + "PlayCount": 0, + "IsFavorite": true, + "Likes": true, + "LastPlayedDate": "2019-08-24T14:15:22Z", + "Played": true, + "Key": "string", + "ItemId": "string" + }, + "RecursiveItemCount": 0, + "ChildCount": 0, + "SeriesName": "string", + "SeriesId": "c7b70af4-4902-4a7e-95ab-28349b6c7afc", + "SeasonId": "badb6463-e5b7-45c5-8141-71204420ec8f", + "SpecialFeatureCount": 0, + "DisplayPreferencesId": "string", + "Status": "string", + "AirTime": "string", + "AirDays": ["Sunday"], + "Tags": ["string"], + "PrimaryImageAspectRatio": 0, + "Artists": ["string"], + "ArtistItems": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "Album": "string", + "CollectionType": "string", + "DisplayOrder": "string", + "AlbumId": "21af9851-8e39-43a9-9c47-513d3b9e99fc", + "AlbumPrimaryImageTag": "string", + "SeriesPrimaryImageTag": "string", + "AlbumArtist": "string", + "AlbumArtists": [ + { + "Name": "string", + "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43" + } + ], + "SeasonName": "string", + "MediaStreams": [ + { + "Codec": "string", + "CodecTag": "string", + "Language": "string", + "ColorRange": "string", + "ColorSpace": "string", + "ColorTransfer": "string", + "ColorPrimaries": "string", + "DvVersionMajor": 0, + "DvVersionMinor": 0, + "DvProfile": 0, + "DvLevel": 0, + "RpuPresentFlag": 0, + "ElPresentFlag": 0, + "BlPresentFlag": 0, + "DvBlSignalCompatibilityId": 0, + "Comment": "string", + "TimeBase": "string", + "CodecTimeBase": "string", + "Title": "string", + "VideoRange": "string", + "VideoRangeType": "string", + "VideoDoViTitle": "string", + "LocalizedUndefined": "string", + "LocalizedDefault": "string", + "LocalizedForced": "string", + "LocalizedExternal": "string", + "DisplayTitle": "string", + "NalLengthSize": "string", + "IsInterlaced": true, + "IsAVC": true, + "ChannelLayout": "string", + "BitRate": 0, + "BitDepth": 0, + "RefFrames": 0, + "PacketLength": 0, + "Channels": 0, + "SampleRate": 0, + "IsDefault": true, + "IsForced": true, + "Height": 0, + "Width": 0, + "AverageFrameRate": 0, + "RealFrameRate": 0, + "Profile": "string", + "Type": "Audio", + "AspectRatio": "string", + "Index": 0, + "Score": 0, + "IsExternal": true, + "DeliveryMethod": "Encode", + "DeliveryUrl": "string", + "IsExternalUrl": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "string", + "PixelFormat": "string", + "Level": 0, + "IsAnamorphic": true + } + ], + "VideoType": "VideoFile", + "PartCount": 0, + "MediaSourceCount": 0, + "ImageTags": { + "Primary": "string" + }, + "BackdropImageTags": ["string"], + "ScreenshotImageTags": ["string"], + "ParentLogoImageTag": "string", + "ParentArtItemId": "10c1875b-b82c-48e8-bae9-939a5e68dc2f", + "ParentArtImageTag": "string", + "SeriesThumbImageTag": "string", + "ImageBlurHashes": { + "Primary": { + "property1": "string", + "property2": "string" + }, + "Art": { + "property1": "string", + "property2": "string" + }, + "Backdrop": { + "property1": "string", + "property2": "string" + }, + "Banner": { + "property1": "string", + "property2": "string" + }, + "Logo": { + "property1": "string", + "property2": "string" + }, + "Thumb": { + "property1": "string", + "property2": "string" + }, + "Disc": { + "property1": "string", + "property2": "string" + }, + "Box": { + "property1": "string", + "property2": "string" + }, + "Screenshot": { + "property1": "string", + "property2": "string" + }, + "Menu": { + "property1": "string", + "property2": "string" + }, + "Chapter": { + "property1": "string", + "property2": "string" + }, + "BoxRear": { + "property1": "string", + "property2": "string" + }, + "Profile": { + "property1": "string", + "property2": "string" + } + }, + "SeriesStudio": "string", + "ParentThumbItemId": "ae6ff707-333d-4994-be6d-b83ca1b35f46", + "ParentThumbImageTag": "string", + "ParentPrimaryImageItemId": "string", + "ParentPrimaryImageTag": "string", + "Chapters": [ + { + "StartPositionTicks": 0, + "Name": "string", + "ImagePath": "string", + "ImageDateModified": "2019-08-24T14:15:22Z", + "ImageTag": "string" + } + ], + "LocationType": "FileSystem", + "IsoType": "Dvd", + "MediaType": "string", + "EndDate": "2019-08-24T14:15:22Z", + "LockedFields": ["Cast"], + "TrailerCount": 0, + "MovieCount": 0, + "SeriesCount": 0, + "ProgramCount": 0, + "EpisodeCount": 0, + "SongCount": 0, + "AlbumCount": 0, + "ArtistCount": 0, + "MusicVideoCount": 0, + "LockData": true, + "Width": 0, + "Height": 0, + "CameraMake": "string", + "CameraModel": "string", + "Software": "string", + "ExposureTime": 0, + "FocalLength": 0, + "ImageOrientation": "TopLeft", + "Aperture": 0, + "ShutterSpeed": 0, + "Latitude": 0, + "Longitude": 0, + "Altitude": 0, + "IsoSpeedRating": 0, + "SeriesTimerId": "string", + "ProgramId": "string", + "ChannelPrimaryImageTag": "string", + "StartDate": "2019-08-24T14:15:22Z", + "CompletionPercentage": 0, + "IsRepeat": true, + "EpisodeTitle": "string", + "ChannelType": "TV", + "Audio": "Mono", + "IsMovie": true, + "IsSports": true, + "IsSeries": true, + "IsLive": true, + "IsNews": true, + "IsKids": true, + "IsPremiere": true, + "TimerId": "string", + "CurrentProgram": {} + } + ], + "TotalRecordCount": 1, + "StartIndex": 0 +} diff --git a/tests/components/jellyfin/fixtures/get-item-collection.json b/tests/components/jellyfin/fixtures/get-item-collection.json index 90ad63a39e4ce0..c58074d999f3fc 100644 --- a/tests/components/jellyfin/fixtures/get-item-collection.json +++ b/tests/components/jellyfin/fixtures/get-item-collection.json @@ -298,7 +298,7 @@ } ], "Album": "string", - "CollectionType": "string", + "CollectionType": "tvshows", "DisplayOrder": "string", "AlbumId": "21af9851-8e39-43a9-9c47-513d3b9e99fc", "AlbumPrimaryImageTag": "string", diff --git a/tests/components/jellyfin/fixtures/media-source-root.json b/tests/components/jellyfin/fixtures/media-source-root.json new file mode 100644 index 00000000000000..9d8d2a8231ad79 --- /dev/null +++ b/tests/components/jellyfin/fixtures/media-source-root.json @@ -0,0 +1,23 @@ +{ + "title": "Jellyfin", + "media_class": "directory", + "media_content_type": "", + "media_content_id": "media-source://jellyfin", + "children_media_class": "directory", + "can_play": false, + "can_expand": true, + "thumbnail": null, + "not_shown": 0, + "children": [ + { + "title": "COLLECTION FOLDER", + "media_class": "directory", + "media_content_type": "", + "media_content_id": "media-source://jellyfin/COLLECTION-FOLDER-UUID", + "children_media_class": null, + "can_play": false, + "can_expand": true, + "thumbnail": null + } + ] +} diff --git a/tests/components/jellyfin/fixtures/movie-collection.json b/tests/components/jellyfin/fixtures/movie-collection.json new file mode 100644 index 00000000000000..1a3c262440dc5c --- /dev/null +++ b/tests/components/jellyfin/fixtures/movie-collection.json @@ -0,0 +1,45 @@ +{ + "BackdropImageTags": [], + "CanDelete": false, + "CanDownload": false, + "ChannelId": "", + "ChildCount": 1, + "CollectionType": "movies", + "DateCreated": "string", + "DisplayPreferencesId": "string", + "EnableMediaSourceDisplay": true, + "Etag": "string", + "ExternalUrls": [], + "GenreItems": [], + "Genres": [], + "Id": "MOVIE-COLLECTION-FOLDER-UUID", + "ImageBlurHashes": { "Primary": { "string": "string" } }, + "ImageTags": { "Primary": "string" }, + "IsFolder": true, + "LocalTrailerCount": 0, + "LocationType": "FileSystem", + "LockData": false, + "LockedFields": [], + "Name": "Movies", + "ParentId": "string", + "Path": "string", + "People": [], + "PlayAccess": "Full", + "PrimaryImageAspectRatio": 1.7777777777777777, + "ProviderIds": {}, + "RemoteTrailers": [], + "ServerId": "string", + "SortName": "movies", + "SpecialFeatureCount": 0, + "Studios": [], + "Taglines": [], + "Tags": [], + "Type": "CollectionFolder", + "UserData": { + "IsFavorite": false, + "Key": "string", + "PlayCount": 0, + "PlaybackPositionTicks": 0, + "Played": false + } +} diff --git a/tests/components/jellyfin/fixtures/movie.json b/tests/components/jellyfin/fixtures/movie.json new file mode 100644 index 00000000000000..47eaddd4cfcf70 --- /dev/null +++ b/tests/components/jellyfin/fixtures/movie.json @@ -0,0 +1,153 @@ +{ + "BackdropImageTags": ["string"], + "CanDelete": true, + "CanDownload": true, + "ChannelId": "", + "Chapters": [], + "CommunityRating": 0, + "Container": "string", + "CriticRating": 0, + "DateCreated": "string", + "DisplayPreferencesId": "string", + "EnableMediaSourceDisplay": true, + "Etag": "string", + "ExternalUrls": [], + "GenreItems": [], + "Genres": ["string"], + "Height": 0, + "Id": "MOVIE-UUID", + "ImageBlurHashes": { + "Backdrop": { "string": "string" }, + "Primary": { "string": "string" } + }, + "ImageTags": { "Primary": "string" }, + "IsFolder": false, + "IsHD": true, + "LocalTrailerCount": 0, + "LocationType": "FileSystem", + "LockData": false, + "LockedFields": [], + "MediaSources": [ + { + "Bitrate": 0, + "Container": "string", + "DefaultAudioStreamIndex": 1, + "ETag": "string", + "Formats": [], + "GenPtsInput": false, + "Id": "string", + "IgnoreDts": false, + "IgnoreIndex": false, + "IsInfiniteStream": false, + "IsRemote": false, + "MediaAttachments": [], + "MediaStreams": [ + { + "AspectRatio": "string", + "AverageFrameRate": 0, + "BitRate": 0, + "Codec": "string", + "CodecTimeBase": "string", + "ColorPrimaries": "string", + "ColorTransfer": "string", + "DisplayTitle": "string", + "Height": 0, + "Index": 0, + "IsDefault": true, + "IsExternal": false, + "IsForced": false, + "IsInterlaced": false, + "IsTextSubtitleStream": false, + "Level": 0, + "PixelFormat": "string", + "Profile": "Main", + "RealFrameRate": 0, + "RefFrames": 0, + "SupportsExternalStream": false, + "TimeBase": "string", + "Type": "Video", + "VideoRange": "string", + "VideoRangeType": "string", + "Width": 0 + } + ], + "Name": "MOVIE", + "Path": "/media/movies/MOVIE/MOVIE.mp4", + "Protocol": "File", + "ReadAtNativeFramerate": false, + "RequiredHttpHeaders": {}, + "RequiresClosing": false, + "RequiresLooping": false, + "RequiresOpening": false, + "RunTimeTicks": 0, + "Size": 0, + "SupportsDirectPlay": true, + "SupportsDirectStream": true, + "SupportsProbing": true, + "SupportsTranscoding": false, + "Type": "Default", + "VideoType": "VideoFile" + } + ], + "MediaStreams": [ + { + "AspectRatio": "string", + "AverageFrameRate": 0, + "BitRate": 0, + "Codec": "string", + "CodecTimeBase": "string", + "ColorPrimaries": "string", + "ColorTransfer": "string", + "DisplayTitle": "string", + "Height": 0, + "Index": 0, + "IsDefault": true, + "IsExternal": false, + "IsForced": false, + "IsInterlaced": false, + "IsTextSubtitleStream": false, + "Level": 0, + "PixelFormat": "string", + "Profile": "string", + "RealFrameRate": 0, + "RefFrames": 0, + "SupportsExternalStream": false, + "TimeBase": "string", + "Type": "Video", + "VideoRange": "string", + "VideoRangeType": "string", + "Width": 0 + } + ], + "MediaType": "Video", + "Name": "MOVIE", + "OfficialRating": "string", + "OriginalTitle": "MOVIE", + "Overview": "string", + "Path": "/media/movies/MOVIE/MOVIE.mp4", + "People": [], + "PlayAccess": "string", + "PremiereDate": "string", + "PrimaryImageAspectRatio": 0, + "ProductionLocations": ["string"], + "ProductionYear": 0, + "ProviderIds": { "Imdb": "string", "Tmdb": "string" }, + "RemoteTrailers": [], + "RunTimeTicks": 0, + "ServerId": "string", + "SortName": "string", + "SpecialFeatureCount": 0, + "Studios": [], + "Taglines": ["string"], + "Tags": [], + "Type": "Movie", + "UserData": { + "IsFavorite": false, + "Key": "0", + "PlayCount": 0, + "PlaybackPositionTicks": 0, + "Played": false + }, + "VideoType": "VideoFile", + "Width": 0 +} diff --git a/tests/components/jellyfin/fixtures/movies.json b/tests/components/jellyfin/fixtures/movies.json new file mode 100644 index 00000000000000..78706456b9b760 --- /dev/null +++ b/tests/components/jellyfin/fixtures/movies.json @@ -0,0 +1,159 @@ +{ + "Items": [ + { + "BackdropImageTags": ["string"], + "CanDelete": true, + "CanDownload": true, + "ChannelId": "", + "Chapters": [], + "CommunityRating": 0, + "Container": "string", + "CriticRating": 0, + "DateCreated": "string", + "DisplayPreferencesId": "string", + "EnableMediaSourceDisplay": true, + "Etag": "string", + "ExternalUrls": [], + "GenreItems": [], + "Genres": ["string"], + "Height": 0, + "Id": "MOVIE-UUID", + "ImageBlurHashes": { + "Backdrop": { "string": "string" }, + "Primary": { "string": "string" } + }, + "ImageTags": { "Primary": "string" }, + "IsFolder": false, + "IsHD": true, + "LocalTrailerCount": 0, + "LocationType": "FileSystem", + "LockData": false, + "LockedFields": [], + "MediaSources": [ + { + "Bitrate": 0, + "Container": "string", + "DefaultAudioStreamIndex": 1, + "ETag": "string", + "Formats": [], + "GenPtsInput": false, + "Id": "string", + "IgnoreDts": false, + "IgnoreIndex": false, + "IsInfiniteStream": false, + "IsRemote": false, + "MediaAttachments": [], + "MediaStreams": [ + { + "AspectRatio": "string", + "AverageFrameRate": 0, + "BitRate": 0, + "Codec": "string", + "CodecTimeBase": "string", + "ColorPrimaries": "string", + "ColorTransfer": "string", + "DisplayTitle": "string", + "Height": 0, + "Index": 0, + "IsDefault": true, + "IsExternal": false, + "IsForced": false, + "IsInterlaced": false, + "IsTextSubtitleStream": false, + "Level": 0, + "PixelFormat": "string", + "Profile": "Main", + "RealFrameRate": 0, + "RefFrames": 0, + "SupportsExternalStream": false, + "TimeBase": "string", + "Type": "Video", + "VideoRange": "string", + "VideoRangeType": "string", + "Width": 0 + } + ], + "Name": "MOVIE", + "Path": "/media/movies/MOVIE/MOVIE.mp4", + "Protocol": "File", + "ReadAtNativeFramerate": false, + "RequiredHttpHeaders": {}, + "RequiresClosing": false, + "RequiresLooping": false, + "RequiresOpening": false, + "RunTimeTicks": 0, + "Size": 0, + "SupportsDirectPlay": true, + "SupportsDirectStream": true, + "SupportsProbing": true, + "SupportsTranscoding": false, + "Type": "Default", + "VideoType": "VideoFile" + } + ], + "MediaStreams": [ + { + "AspectRatio": "string", + "AverageFrameRate": 0, + "BitRate": 0, + "Codec": "string", + "CodecTimeBase": "string", + "ColorPrimaries": "string", + "ColorTransfer": "string", + "DisplayTitle": "string", + "Height": 0, + "Index": 0, + "IsDefault": true, + "IsExternal": false, + "IsForced": false, + "IsInterlaced": false, + "IsTextSubtitleStream": false, + "Level": 0, + "PixelFormat": "string", + "Profile": "string", + "RealFrameRate": 0, + "RefFrames": 0, + "SupportsExternalStream": false, + "TimeBase": "string", + "Type": "Video", + "VideoRange": "string", + "VideoRangeType": "string", + "Width": 0 + } + ], + "MediaType": "Video", + "Name": "MOVIE", + "OfficialRating": "string", + "OriginalTitle": "MOVIE", + "Overview": "string", + "Path": "/media/movies/MOVIE/MOVIE.mp4", + "People": [], + "PlayAccess": "string", + "PremiereDate": "string", + "PrimaryImageAspectRatio": 0, + "ProductionLocations": ["string"], + "ProductionYear": 0, + "ProviderIds": { "Imdb": "string", "Tmdb": "string" }, + "RemoteTrailers": [], + "RunTimeTicks": 0, + "ServerId": "string", + "SortName": "string", + "SpecialFeatureCount": 0, + "Studios": [], + "Taglines": ["string"], + "Tags": [], + "Type": "Movie", + "UserData": { + "IsFavorite": false, + "Key": "0", + "PlayCount": 0, + "PlaybackPositionTicks": 0, + "Played": false + }, + "VideoType": "VideoFile", + "Width": 0 + } + ], + "StartIndex": 0, + "TotalRecordCount": 1 +} diff --git a/tests/components/jellyfin/fixtures/music-collection.json b/tests/components/jellyfin/fixtures/music-collection.json new file mode 100644 index 00000000000000..0ae91d7badd49f --- /dev/null +++ b/tests/components/jellyfin/fixtures/music-collection.json @@ -0,0 +1,45 @@ +{ + "BackdropImageTags": [], + "CanDelete": false, + "CanDownload": false, + "ChannelId": "", + "ChildCount": 0, + "CollectionType": "music", + "DateCreated": "string", + "DisplayPreferencesId": "string", + "EnableMediaSourceDisplay": true, + "Etag": "string", + "ExternalUrls": [], + "GenreItems": [], + "Genres": [], + "Id": "MUSIC-COLLECTION-FOLDER-UUID", + "ImageBlurHashes": { "Primary": { "string": "string" } }, + "ImageTags": { "Primary": "string" }, + "IsFolder": true, + "LocalTrailerCount": 0, + "LocationType": "FileSystem", + "LockData": false, + "LockedFields": [], + "Name": "Music", + "ParentId": "string", + "Path": "string", + "People": [], + "PlayAccess": "Full", + "PrimaryImageAspectRatio": 1.7777777777777777, + "ProviderIds": {}, + "RemoteTrailers": [], + "ServerId": "string", + "SortName": "music", + "SpecialFeatureCount": 0, + "Studios": [], + "Taglines": [], + "Tags": [], + "Type": "CollectionFolder", + "UserData": { + "IsFavorite": false, + "Key": "string", + "PlayCount": 0, + "PlaybackPositionTicks": 0, + "Played": false + } +} diff --git a/tests/components/jellyfin/fixtures/season.json b/tests/components/jellyfin/fixtures/season.json new file mode 100644 index 00000000000000..b8fb80042f3636 --- /dev/null +++ b/tests/components/jellyfin/fixtures/season.json @@ -0,0 +1,23 @@ +{ + "BackdropImageTags": [], + "ChannelId": "string", + "Id": "SEASON-UUID", + "ImageBlurHashes": {}, + "ImageTags": {}, + "IndexNumber": 0, + "IsFolder": true, + "LocationType": "FileSystem", + "Name": "SEASON", + "SeriesId": "SERIES-UUID", + "SeriesName": "SERIES", + "ServerId": "SEASON-UUID", + "Type": "Season", + "UserData": { + "IsFavorite": false, + "Key": "string", + "PlayCount": 0, + "PlaybackPositionTicks": 0, + "Played": false, + "UnplayedItemCount": 0 + } +} diff --git a/tests/components/jellyfin/fixtures/seasons.json b/tests/components/jellyfin/fixtures/seasons.json new file mode 100644 index 00000000000000..dc070d78352beb --- /dev/null +++ b/tests/components/jellyfin/fixtures/seasons.json @@ -0,0 +1,29 @@ +{ + "Items": [ + { + "BackdropImageTags": [], + "ChannelId": "string", + "Id": "SEASON-UUID", + "ImageBlurHashes": {}, + "ImageTags": {}, + "IndexNumber": 0, + "IsFolder": true, + "LocationType": "FileSystem", + "Name": "SEASON", + "SeriesId": "SERIES-UUID", + "SeriesName": "SERIES", + "ServerId": "SEASON-UUID", + "Type": "Season", + "UserData": { + "IsFavorite": false, + "Key": "string", + "PlayCount": 0, + "PlaybackPositionTicks": 0, + "Played": false, + "UnplayedItemCount": 0 + } + } + ], + "StartIndex": 0, + "TotalRecordCount": 1 +} diff --git a/tests/components/jellyfin/fixtures/series-list.json b/tests/components/jellyfin/fixtures/series-list.json new file mode 100644 index 00000000000000..3209ccfb2c4747 --- /dev/null +++ b/tests/components/jellyfin/fixtures/series-list.json @@ -0,0 +1,34 @@ +{ + "Items": [ + { + "AirDays": ["string"], + "AirTime": "string", + "BackdropImageTags": [], + "ChannelId": "string", + "CommunityRating": 0, + "EndDate": "string", + "Id": "SERIES-UUID", + "ImageBlurHashes": { "Banner": { "string": "string" } }, + "ImageTags": { "Banner": "string" }, + "IsFolder": true, + "LocationType": "FileSystem", + "Name": "SERIES", + "PremiereDate": "string", + "ProductionYear": 0, + "RunTimeTicks": 0, + "ServerId": "string", + "Status": "string", + "Type": "Series", + "UserData": { + "IsFavorite": false, + "Key": "string", + "PlayCount": 0, + "PlaybackPositionTicks": 0, + "Played": false, + "UnplayedItemCount": 0 + } + } + ], + "TotalRecordCount": 1, + "StartIndex": 0 +} diff --git a/tests/components/jellyfin/fixtures/series.json b/tests/components/jellyfin/fixtures/series.json new file mode 100644 index 00000000000000..879680ec591c76 --- /dev/null +++ b/tests/components/jellyfin/fixtures/series.json @@ -0,0 +1,28 @@ +{ + "AirDays": ["string"], + "AirTime": "string", + "BackdropImageTags": [], + "ChannelId": "string", + "CommunityRating": 0, + "EndDate": "string", + "Id": "SERIES-UUID", + "ImageBlurHashes": { "Banner": { "string": "string" } }, + "ImageTags": { "Banner": "string" }, + "IsFolder": true, + "LocationType": "FileSystem", + "Name": "SERIES", + "PremiereDate": "string", + "ProductionYear": 0, + "RunTimeTicks": 0, + "ServerId": "string", + "Status": "string", + "Type": "Series", + "UserData": { + "IsFavorite": false, + "Key": "string", + "PlayCount": 0, + "PlaybackPositionTicks": 0, + "Played": false, + "UnplayedItemCount": 0 + } +} diff --git a/tests/components/jellyfin/fixtures/track.json b/tests/components/jellyfin/fixtures/track.json new file mode 100644 index 00000000000000..e9297549387d61 --- /dev/null +++ b/tests/components/jellyfin/fixtures/track.json @@ -0,0 +1,91 @@ +{ + "Album": "ALBUM_NAME", + "AlbumArtist": "ARTIST", + "AlbumArtists": [{ "Id": "ARTIST-UUID", "Name": "ARTIST" }], + "AlbumId": "ALBUM-UUID", + "AlbumPrimaryImageTag": "string", + "ArtistItems": [{ "Id": "ARTIST-UUID", "Name": "ARTIST" }], + "Artists": ["ARTIST"], + "Id": "TRACK-UUID", + "ImageTags": { "Primary": "string" }, + "IndexNumber": 1, + "IsFolder": false, + "MediaSources": [ + { + "Bitrate": 1, + "Container": "flac", + "DefaultAudioStreamIndex": 0, + "Formats": [], + "GenPtsInput": false, + "Id": "string", + "IgnoreDts": false, + "IgnoreIndex": false, + "IsInfiniteStream": false, + "IsRemote": false, + "MediaAttachments": [], + "MediaStreams": [ + { + "BitDepth": 16, + "ChannelLayout": "stereo", + "Channels": 2, + "Codec": "flac", + "CodecTimeBase": "1/44100", + "DisplayTitle": "FLAC - Stereo", + "Index": 0, + "IsDefault": false, + "IsExternal": false, + "IsForced": false, + "IsInterlaced": false, + "IsTextSubtitleStream": false, + "Level": 0, + "SampleRate": 44100, + "SupportsExternalStream": false, + "TimeBase": "1/44100", + "Type": "Audio" + } + ], + "Name": "string", + "Path": "/media/music/MockArtist/MockAlbum/01 - Track - MockAlbum - MockArtist.flac", + "Protocol": "string", + "ReadAtNativeFramerate": false, + "RequiredHttpHeaders": {}, + "RequiresClosing": false, + "RequiresLooping": false, + "RequiresOpening": false, + "RunTimeTicks": 2954933248, + "Size": 30074476, + "SupportsDirectPlay": true, + "SupportsDirectStream": true, + "SupportsProbing": true, + "SupportsTranscoding": true, + "Type": "Default" + } + ], + "MediaStreams": [ + { + "BitDepth": 16, + "ChannelLayout": "stereo", + "Channels": 2, + "Codec": "flac", + "CodecTimeBase": "1/44100", + "DisplayTitle": "FLAC - Stereo", + "Index": 0, + "IsDefault": false, + "IsExternal": false, + "IsForced": false, + "IsInterlaced": false, + "IsTextSubtitleStream": false, + "Level": 0, + "SampleRate": 44100, + "SupportsExternalStream": false, + "TimeBase": "1/44100", + "Type": "Audio" + } + ], + "MediaType": "Audio", + "Name": "TRACK", + "ParentId": "ALBUM-UUID", + "Path": "/media/music/MockArtist/MockAlbum/01 - Track - MockAlbum - MockArtist.flac", + "ServerId": "string", + "Type": "Audio" +} diff --git a/tests/components/jellyfin/fixtures/tracks-nopath.json b/tests/components/jellyfin/fixtures/tracks-nopath.json new file mode 100644 index 00000000000000..75e87e1a05b67c --- /dev/null +++ b/tests/components/jellyfin/fixtures/tracks-nopath.json @@ -0,0 +1,93 @@ +{ + "Items": [ + { + "Album": "ALBUM_NAME", + "AlbumArtist": "ARTIST", + "AlbumArtists": [{ "Id": "ARTIST-UUID", "Name": "ARTIST" }], + "AlbumId": "ALBUM-UUID", + "AlbumPrimaryImageTag": "string", + "ArtistItems": [{ "Id": "ARTIST-UUID", "Name": "ARTIST" }], + "Artists": ["ARTIST"], + "Id": "TRACK-UUID", + "ImageTags": { "Primary": "string" }, + "IndexNumber": 1, + "IsFolder": false, + "MediaSources": [ + { + "Bitrate": 1, + "Container": "flac", + "DefaultAudioStreamIndex": 0, + "Formats": [], + "GenPtsInput": false, + "Id": "string", + "IgnoreDts": false, + "IgnoreIndex": false, + "IsInfiniteStream": false, + "IsRemote": false, + "MediaAttachments": [], + "MediaStreams": [ + { + "BitDepth": 16, + "ChannelLayout": "stereo", + "Channels": 2, + "Codec": "flac", + "CodecTimeBase": "1/44100", + "DisplayTitle": "FLAC - Stereo", + "Index": 0, + "IsDefault": false, + "IsExternal": false, + "IsForced": false, + "IsInterlaced": false, + "IsTextSubtitleStream": false, + "Level": 0, + "SampleRate": 44100, + "SupportsExternalStream": false, + "TimeBase": "1/44100", + "Type": "Audio" + } + ], + "Name": "string", + "Protocol": "string", + "ReadAtNativeFramerate": false, + "RequiredHttpHeaders": {}, + "RequiresClosing": false, + "RequiresLooping": false, + "RequiresOpening": false, + "RunTimeTicks": 2954933248, + "Size": 30074476, + "SupportsDirectPlay": true, + "SupportsDirectStream": true, + "SupportsProbing": true, + "SupportsTranscoding": true, + "Type": "Default" + } + ], + "MediaStreams": [ + { + "BitDepth": 16, + "ChannelLayout": "stereo", + "Channels": 2, + "Codec": "flac", + "CodecTimeBase": "1/44100", + "DisplayTitle": "FLAC - Stereo", + "Index": 0, + "IsDefault": false, + "IsExternal": false, + "IsForced": false, + "IsInterlaced": false, + "IsTextSubtitleStream": false, + "Level": 0, + "SampleRate": 44100, + "SupportsExternalStream": false, + "TimeBase": "1/44100", + "Type": "Audio" + } + ], + "MediaType": "Audio", + "Name": "TRACK", + "ParentId": "ALBUM-UUID", + "ServerId": "string", + "Type": "Audio" + } + ] +} diff --git a/tests/components/jellyfin/fixtures/tracks-nosource.json b/tests/components/jellyfin/fixtures/tracks-nosource.json new file mode 100644 index 00000000000000..02509f13196e99 --- /dev/null +++ b/tests/components/jellyfin/fixtures/tracks-nosource.json @@ -0,0 +1,23 @@ +{ + "Items": [ + { + "Album": "ALBUM_NAME", + "AlbumArtist": "ARTIST", + "AlbumArtists": [{ "Id": "ARTIST-UUID", "Name": "ARTIST" }], + "AlbumId": "ALBUM-UUID", + "AlbumPrimaryImageTag": "string", + "ArtistItems": [{ "Id": "ARTIST-UUID", "Name": "ARTIST" }], + "Artists": ["ARTIST"], + "Id": "TRACK-UUID", + "ImageTags": { "Primary": "string" }, + "IndexNumber": 1, + "IsFolder": false, + "MediaType": "Audio", + "Name": "TRACK", + "ParentId": "ALBUM-UUID", + "Path": "/media/music/MockArtist/MockAlbum/01 - Track - MockAlbum - MockArtist.flac", + "ServerId": "string", + "Type": "Audio" + } + ] +} diff --git a/tests/components/jellyfin/fixtures/tracks-unknown-extension.json b/tests/components/jellyfin/fixtures/tracks-unknown-extension.json new file mode 100644 index 00000000000000..b3beaa1d75870c --- /dev/null +++ b/tests/components/jellyfin/fixtures/tracks-unknown-extension.json @@ -0,0 +1,95 @@ +{ + "Items": [ + { + "Album": "ALBUM_NAME", + "AlbumArtist": "ARTIST", + "AlbumArtists": [{ "Id": "ARTIST-UUID", "Name": "ARTIST" }], + "AlbumId": "ALBUM-UUID", + "AlbumPrimaryImageTag": "string", + "ArtistItems": [{ "Id": "ARTIST-UUID", "Name": "ARTIST" }], + "Artists": ["ARTIST"], + "Id": "TRACK-UUID", + "ImageTags": { "Primary": "string" }, + "IndexNumber": 1, + "IsFolder": false, + "MediaSources": [ + { + "Bitrate": 1, + "Container": "flac", + "DefaultAudioStreamIndex": 0, + "Formats": [], + "GenPtsInput": false, + "Id": "string", + "IgnoreDts": false, + "IgnoreIndex": false, + "IsInfiniteStream": false, + "IsRemote": false, + "MediaAttachments": [], + "MediaStreams": [ + { + "BitDepth": 16, + "ChannelLayout": "stereo", + "Channels": 2, + "Codec": "flac", + "CodecTimeBase": "1/44100", + "DisplayTitle": "FLAC - Stereo", + "Index": 0, + "IsDefault": false, + "IsExternal": false, + "IsForced": false, + "IsInterlaced": false, + "IsTextSubtitleStream": false, + "Level": 0, + "SampleRate": 44100, + "SupportsExternalStream": false, + "TimeBase": "1/44100", + "Type": "Audio" + } + ], + "Name": "string", + "Path": "/media/music/MockArtist/MockAlbum/01 - Track - MockAlbum - MockArtist.uke", + "Protocol": "string", + "ReadAtNativeFramerate": false, + "RequiredHttpHeaders": {}, + "RequiresClosing": false, + "RequiresLooping": false, + "RequiresOpening": false, + "RunTimeTicks": 2954933248, + "Size": 30074476, + "SupportsDirectPlay": true, + "SupportsDirectStream": true, + "SupportsProbing": true, + "SupportsTranscoding": true, + "Type": "Default" + } + ], + "MediaStreams": [ + { + "BitDepth": 16, + "ChannelLayout": "stereo", + "Channels": 2, + "Codec": "flac", + "CodecTimeBase": "1/44100", + "DisplayTitle": "FLAC - Stereo", + "Index": 0, + "IsDefault": false, + "IsExternal": false, + "IsForced": false, + "IsInterlaced": false, + "IsTextSubtitleStream": false, + "Level": 0, + "SampleRate": 44100, + "SupportsExternalStream": false, + "TimeBase": "1/44100", + "Type": "Audio" + } + ], + "MediaType": "Audio", + "Name": "TRACK", + "ParentId": "ALBUM-UUID", + "Path": "/media/music/MockArtist/MockAlbum/01 - Track - MockAlbum - MockArtist.uke", + "ServerId": "string", + "Type": "Audio" + } + ] +} diff --git a/tests/components/jellyfin/fixtures/tracks.json b/tests/components/jellyfin/fixtures/tracks.json new file mode 100644 index 00000000000000..63a0fd9deaff62 --- /dev/null +++ b/tests/components/jellyfin/fixtures/tracks.json @@ -0,0 +1,95 @@ +{ + "Items": [ + { + "Album": "ALBUM_NAME", + "AlbumArtist": "ARTIST", + "AlbumArtists": [{ "Id": "ARTIST-UUID", "Name": "ARTIST" }], + "AlbumId": "ALBUM-UUID", + "AlbumPrimaryImageTag": "string", + "ArtistItems": [{ "Id": "ARTIST-UUID", "Name": "ARTIST" }], + "Artists": ["ARTIST"], + "Id": "TRACK-UUID", + "ImageTags": { "Primary": "string" }, + "IndexNumber": 1, + "IsFolder": false, + "MediaSources": [ + { + "Bitrate": 1, + "Container": "flac", + "DefaultAudioStreamIndex": 0, + "Formats": [], + "GenPtsInput": false, + "Id": "string", + "IgnoreDts": false, + "IgnoreIndex": false, + "IsInfiniteStream": false, + "IsRemote": false, + "MediaAttachments": [], + "MediaStreams": [ + { + "BitDepth": 16, + "ChannelLayout": "stereo", + "Channels": 2, + "Codec": "flac", + "CodecTimeBase": "1/44100", + "DisplayTitle": "FLAC - Stereo", + "Index": 0, + "IsDefault": false, + "IsExternal": false, + "IsForced": false, + "IsInterlaced": false, + "IsTextSubtitleStream": false, + "Level": 0, + "SampleRate": 44100, + "SupportsExternalStream": false, + "TimeBase": "1/44100", + "Type": "Audio" + } + ], + "Name": "string", + "Path": "/media/music/MockArtist/MockAlbum/01 - Track - MockAlbum - MockArtist.flac", + "Protocol": "string", + "ReadAtNativeFramerate": false, + "RequiredHttpHeaders": {}, + "RequiresClosing": false, + "RequiresLooping": false, + "RequiresOpening": false, + "RunTimeTicks": 2954933248, + "Size": 30074476, + "SupportsDirectPlay": true, + "SupportsDirectStream": true, + "SupportsProbing": true, + "SupportsTranscoding": true, + "Type": "Default" + } + ], + "MediaStreams": [ + { + "BitDepth": 16, + "ChannelLayout": "stereo", + "Channels": 2, + "Codec": "flac", + "CodecTimeBase": "1/44100", + "DisplayTitle": "FLAC - Stereo", + "Index": 0, + "IsDefault": false, + "IsExternal": false, + "IsForced": false, + "IsInterlaced": false, + "IsTextSubtitleStream": false, + "Level": 0, + "SampleRate": 44100, + "SupportsExternalStream": false, + "TimeBase": "1/44100", + "Type": "Audio" + } + ], + "MediaType": "Audio", + "Name": "TRACK", + "ParentId": "ALBUM-UUID", + "Path": "/media/music/MockArtist/MockAlbum/01 - Track - MockAlbum - MockArtist.flac", + "ServerId": "string", + "Type": "Audio" + } + ] +} diff --git a/tests/components/jellyfin/fixtures/tv-collection.json b/tests/components/jellyfin/fixtures/tv-collection.json new file mode 100644 index 00000000000000..0817352edae93c --- /dev/null +++ b/tests/components/jellyfin/fixtures/tv-collection.json @@ -0,0 +1,45 @@ +{ + "BackdropImageTags": [], + "CanDelete": false, + "CanDownload": false, + "ChannelId": "", + "ChildCount": 0, + "CollectionType": "tvshows", + "DateCreated": "string", + "DisplayPreferencesId": "string", + "EnableMediaSourceDisplay": true, + "Etag": "string", + "ExternalUrls": [], + "GenreItems": [], + "Genres": [], + "Id": "TV-COLLECTION-FOLDER-UUID", + "ImageBlurHashes": { "Primary": { "string": "string" } }, + "ImageTags": { "Primary": "string" }, + "IsFolder": true, + "LocalTrailerCount": 0, + "LocationType": "FileSystem", + "LockData": false, + "LockedFields": [], + "Name": "TVShows", + "ParentId": "string", + "Path": "string", + "People": [], + "PlayAccess": "Full", + "PrimaryImageAspectRatio": 1.7777777777777777, + "ProviderIds": {}, + "RemoteTrailers": [], + "ServerId": "string", + "SortName": "music", + "SpecialFeatureCount": 0, + "Studios": [], + "Taglines": [], + "Tags": [], + "Type": "CollectionFolder", + "UserData": { + "IsFavorite": false, + "Key": "string", + "PlayCount": 0, + "PlaybackPositionTicks": 0, + "Played": false + } +} diff --git a/tests/components/jellyfin/fixtures/unsupported-item.json b/tests/components/jellyfin/fixtures/unsupported-item.json new file mode 100644 index 00000000000000..5d97447808a1e8 --- /dev/null +++ b/tests/components/jellyfin/fixtures/unsupported-item.json @@ -0,0 +1,5 @@ +{ + "Id": "Unsupported-UUID", + "Type": "Unsupported", + "MediaType": "Unsupported" +} diff --git a/tests/components/jellyfin/snapshots/test_media_source.ambr b/tests/components/jellyfin/snapshots/test_media_source.ambr new file mode 100644 index 00000000000000..6d629f245a0c8d --- /dev/null +++ b/tests/components/jellyfin/snapshots/test_media_source.ambr @@ -0,0 +1,135 @@ +# serializer version: 1 +# name: test_movie_library + dict({ + 'can_expand': False, + 'can_play': True, + 'children': None, + 'children_media_class': None, + 'domain': 'jellyfin', + 'identifier': 'MOVIE-UUID', + 'media_class': , + 'media_content_id': 'media-source://jellyfin/MOVIE-UUID', + 'media_content_type': 'video/mp4', + 'not_shown': 0, + 'thumbnail': 'http://localhost/Items/MOVIE-UUID/Images/Primary.jpg', + 'title': 'MOVIE', + }) +# --- +# name: test_music_library + dict({ + 'can_expand': True, + 'can_play': False, + 'children': None, + 'children_media_class': None, + 'domain': 'jellyfin', + 'identifier': 'ALBUM-UUID', + 'media_class': , + 'media_content_id': 'media-source://jellyfin/ALBUM-UUID', + 'media_content_type': '', + 'not_shown': 0, + 'thumbnail': None, + 'title': 'ALBUM', + }) +# --- +# name: test_music_library.1 + dict({ + 'can_expand': True, + 'can_play': False, + 'children': None, + 'children_media_class': None, + 'domain': 'jellyfin', + 'identifier': 'ALBUM-UUID', + 'media_class': , + 'media_content_id': 'media-source://jellyfin/ALBUM-UUID', + 'media_content_type': '', + 'not_shown': 0, + 'thumbnail': None, + 'title': 'ALBUM', + }) +# --- +# name: test_music_library.2 + dict({ + 'can_expand': False, + 'can_play': True, + 'children': None, + 'children_media_class': None, + 'domain': 'jellyfin', + 'identifier': 'TRACK-UUID', + 'media_class': , + 'media_content_id': 'media-source://jellyfin/TRACK-UUID', + 'media_content_type': 'audio/flac', + 'not_shown': 0, + 'thumbnail': 'http://localhost/Items/TRACK-UUID/Images/Primary.jpg', + 'title': 'TRACK', + }) +# --- +# name: test_resolve + 'http://localhost/Audio/TRACK-UUID/universal?UserId=test-username,DeviceId=TEST-UUID,MaxStreamingBitrate=140000000' +# --- +# name: test_resolve.1 + 'http://localhost/Videos/MOVIE-UUID/stream?static=true,DeviceId=TEST-UUID,api_key=TEST-API-KEY' +# --- +# name: test_root + dict({ + 'can_expand': True, + 'can_play': False, + 'children': None, + 'children_media_class': None, + 'domain': 'jellyfin', + 'identifier': 'COLLECTION-FOLDER-UUID', + 'media_class': , + 'media_content_id': 'media-source://jellyfin/COLLECTION-FOLDER-UUID', + 'media_content_type': '', + 'not_shown': 0, + 'thumbnail': None, + 'title': 'COLLECTION FOLDER', + }) +# --- +# name: test_tv_library + dict({ + 'can_expand': True, + 'can_play': False, + 'children': None, + 'children_media_class': None, + 'domain': 'jellyfin', + 'identifier': 'SERIES-UUID', + 'media_class': , + 'media_content_id': 'media-source://jellyfin/SERIES-UUID', + 'media_content_type': '', + 'not_shown': 0, + 'thumbnail': None, + 'title': 'SERIES', + }) +# --- +# name: test_tv_library.1 + dict({ + 'can_expand': True, + 'can_play': False, + 'children': None, + 'children_media_class': None, + 'domain': 'jellyfin', + 'identifier': 'SEASON-UUID', + 'media_class': , + 'media_content_id': 'media-source://jellyfin/SEASON-UUID', + 'media_content_type': '', + 'not_shown': 0, + 'thumbnail': None, + 'title': 'SEASON', + }) +# --- +# name: test_tv_library.2 + dict({ + 'can_expand': False, + 'can_play': True, + 'children': None, + 'children_media_class': None, + 'domain': 'jellyfin', + 'identifier': 'EPISODE-UUID', + 'media_class': , + 'media_content_id': 'media-source://jellyfin/EPISODE-UUID', + 'media_content_type': 'video/mp4', + 'not_shown': 0, + 'thumbnail': 'http://localhost/Items/EPISODE-UUID/Images/Primary.jpg', + 'title': 'EPISODE', + }) +# --- diff --git a/tests/components/jellyfin/test_init.py b/tests/components/jellyfin/test_init.py index 542be0736c7881..56e352bd71f984 100644 --- a/tests/components/jellyfin/test_init.py +++ b/tests/components/jellyfin/test_init.py @@ -29,6 +29,26 @@ async def test_config_entry_not_ready( assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY +async def test_invalid_auth( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_jellyfin: MagicMock, + mock_client: MagicMock, +) -> None: + """Test the Jellyfin integration handling invalid credentials.""" + mock_client.auth.connect_to_address.return_value = await async_load_json_fixture( + hass, + "auth-connect-address.json", + ) + mock_client.auth.login.return_value = await async_load_json_fixture( + hass, + "auth-login-failure.json", + ) + + mock_config_entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) + + async def test_load_unload_config_entry( hass: HomeAssistant, mock_config_entry: MockConfigEntry, diff --git a/tests/components/jellyfin/test_media_source.py b/tests/components/jellyfin/test_media_source.py new file mode 100644 index 00000000000000..5f8871e62428ef --- /dev/null +++ b/tests/components/jellyfin/test_media_source.py @@ -0,0 +1,303 @@ +"""Tests for the Jellyfin media_player platform.""" +from unittest.mock import MagicMock + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.jellyfin.const import DOMAIN +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source import ( + DOMAIN as MEDIA_SOURCE_DOMAIN, + URI_SCHEME, + async_browse_media, + async_resolve_media, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import load_json_fixture + +from tests.common import MockConfigEntry + + +@pytest.fixture(autouse=True) +async def setup_component(hass: HomeAssistant) -> None: + """Set up component.""" + assert await async_setup_component(hass, MEDIA_SOURCE_DOMAIN, {}) + + +async def test_resolve( + hass: HomeAssistant, + mock_client: MagicMock, + init_integration: MockConfigEntry, + mock_jellyfin: MagicMock, + mock_api: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Test resolving Jellyfin media items.""" + + # Test resolving a track + mock_api.get_item.side_effect = None + mock_api.get_item.return_value = load_json_fixture("track.json") + + play_media = await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/TRACK-UUID") + + assert play_media.mime_type == "audio/flac" + assert play_media.url == snapshot + + # Test resolving a movie + mock_api.get_item.side_effect = None + mock_api.get_item.return_value = load_json_fixture("movie.json") + + play_media = await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/MOVIE-UUID") + + assert play_media.mime_type == "video/mp4" + assert play_media.url == snapshot + + # Test resolving an unsupported item + mock_api.get_item.side_effect = None + mock_api.get_item.return_value = load_json_fixture("unsupported-item.json") + + with pytest.raises(BrowseError): + await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/UNSUPPORTED-ITEM-UUID") + + +async def test_root( + hass: HomeAssistant, + mock_client: MagicMock, + init_integration: MockConfigEntry, + mock_jellyfin: MagicMock, + mock_api: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Test browsing the Jellyfin root.""" + + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}") + + assert browse.domain == DOMAIN + assert browse.identifier is None + assert browse.title == "Jellyfin" + assert vars(browse.children[0]) == snapshot + + +async def test_tv_library( + hass: HomeAssistant, + mock_client: MagicMock, + init_integration: MockConfigEntry, + mock_jellyfin: MagicMock, + mock_api: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Test browsing a Jellyfin TV Library.""" + + # Test browsing an empty tv library + mock_api.get_item.side_effect = None + mock_api.get_item.return_value = load_json_fixture("tv-collection.json") + mock_api.user_items.side_effect = None + mock_api.user_items.return_value = {"Items": []} + + browse = await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/TV-COLLECTION-FOLDER-UUID" + ) + + assert browse.domain == DOMAIN + assert browse.identifier == "TV-COLLECTION-FOLDER-UUID" + assert browse.title == "TVShows" + assert browse.children == [] + + # Test browsing a tv library containing series + mock_api.user_items.side_effect = None + mock_api.user_items.return_value = load_json_fixture("series-list.json") + + browse = await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/TV-COLLECTION-FOLDER-UUID" + ) + + assert browse.domain == DOMAIN + assert browse.identifier == "TV-COLLECTION-FOLDER-UUID" + assert browse.title == "TVShows" + assert vars(browse.children[0]) == snapshot + + # Test browsing a series + mock_api.get_item.side_effect = None + mock_api.get_item.return_value = load_json_fixture("series.json") + mock_api.user_items.side_effect = None + mock_api.user_items.return_value = load_json_fixture("seasons.json") + + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/SERIES-UUID") + + assert browse.domain == DOMAIN + assert browse.identifier == "SERIES-UUID" + assert browse.title == "SERIES" + assert vars(browse.children[0]) == snapshot + + # Test browsing a season + mock_api.get_item.side_effect = None + mock_api.get_item.return_value = load_json_fixture("season.json") + mock_api.user_items.side_effect = None + mock_api.user_items.return_value = load_json_fixture("episodes.json") + + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/SEASON-UUID") + + assert browse.domain == DOMAIN + assert browse.identifier == "SEASON-UUID" + assert browse.title == "SEASON" + assert vars(browse.children[0]) == snapshot + + +async def test_movie_library( + hass: HomeAssistant, + mock_client: MagicMock, + init_integration: MockConfigEntry, + mock_jellyfin: MagicMock, + mock_api: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Test browsing a Jellyfin Movie Library.""" + + # Test empty movie library + mock_api.get_item.side_effect = None + mock_api.get_item.return_value = load_json_fixture("movie-collection.json") + mock_api.user_items.side_effect = None + mock_api.user_items.return_value = {"Items": []} + + browse = await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/MOVIE-COLLECTION-FOLDER-UUID" + ) + + assert browse.domain == DOMAIN + assert browse.identifier == "MOVIE-COLLECTION-FOLDER-UUID" + assert browse.title == "Movies" + assert browse.children == [] + + # Test browsing a movie library containing movies + mock_api.user_items.side_effect = None + mock_api.user_items.return_value = load_json_fixture("movies.json") + + browse = await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/MOVIE-COLLECTION-FOLDER-UUID" + ) + + assert browse.domain == DOMAIN + assert browse.identifier == "MOVIE-COLLECTION-FOLDER-UUID" + assert browse.title == "Movies" + assert vars(browse.children[0]) == snapshot + + +async def test_music_library( + hass: HomeAssistant, + mock_client: MagicMock, + init_integration: MockConfigEntry, + mock_jellyfin: MagicMock, + mock_api: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Test browsing a Jellyfin Music Library.""" + + # Test browsinng an empty music library + mock_api.get_item.side_effect = None + mock_api.get_item.return_value = load_json_fixture("music-collection.json") + mock_api.user_items.side_effect = None + mock_api.user_items.return_value = {"Items": []} + + browse = await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/MUSIC-COLLECTION-FOLDER-UUID" + ) + + assert browse.domain == DOMAIN + assert browse.identifier == "MUSIC-COLLECTION-FOLDER-UUID" + assert browse.title == "Music" + assert browse.children == [] + + # Test browsing a music library containing albums + mock_api.user_items.side_effect = None + mock_api.user_items.return_value = load_json_fixture("albums.json") + + browse = await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/MUSIC-COLLECTION-FOLDER-UUID" + ) + + assert browse.domain == DOMAIN + assert browse.identifier == "MUSIC-COLLECTION-FOLDER-UUID" + assert browse.title == "Music" + assert vars(browse.children[0]) == snapshot + + # Test browsing an artist + mock_api.get_item.side_effect = None + mock_api.get_item.return_value = load_json_fixture("artist.json") + mock_api.user_items.side_effect = None + mock_api.user_items.return_value = load_json_fixture("albums.json") + + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/ARTIST-UUID") + + assert browse.domain == DOMAIN + assert browse.identifier == "ARTIST-UUID" + assert browse.title == "ARTIST" + assert vars(browse.children[0]) == snapshot + + # Test browsing an album + mock_api.get_item.side_effect = None + mock_api.get_item.return_value = load_json_fixture("album.json") + mock_api.user_items.side_effect = None + mock_api.user_items.return_value = load_json_fixture("tracks.json") + + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/ALBUM-UUID") + + assert browse.domain == DOMAIN + assert browse.identifier == "ALBUM-UUID" + assert browse.title == "ALBUM" + assert vars(browse.children[0]) == snapshot + + # Test browsing an album with a track with no source + mock_api.user_items.side_effect = None + mock_api.user_items.return_value = load_json_fixture("tracks-nosource.json") + + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/ALBUM-UUID") + + assert browse.domain == DOMAIN + assert browse.identifier == "ALBUM-UUID" + assert browse.title == "ALBUM" + + assert browse.children == [] + + # Test browsing an album with a track with no path + mock_api.user_items.side_effect = None + mock_api.user_items.return_value = load_json_fixture("tracks-nopath.json") + + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/ALBUM-UUID") + + assert browse.domain == DOMAIN + assert browse.identifier == "ALBUM-UUID" + assert browse.title == "ALBUM" + + assert browse.children == [] + + # Test browsing an album with a track with an unknown file extension + mock_api.user_items.side_effect = None + mock_api.user_items.return_value = load_json_fixture( + "tracks-unknown-extension.json" + ) + + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/ALBUM-UUID") + + assert browse.domain == DOMAIN + assert browse.identifier == "ALBUM-UUID" + assert browse.title == "ALBUM" + + assert browse.children == [] + + +async def test_browse_unsupported( + hass: HomeAssistant, + mock_client: MagicMock, + init_integration: MockConfigEntry, + mock_jellyfin: MagicMock, + mock_api: MagicMock, +) -> None: + """Test browsing an unsupported item.""" + + mock_api.get_item.side_effect = None + mock_api.get_item.return_value = load_json_fixture("unsupported-item.json") + + with pytest.raises(BrowseError): + await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/UNSUPPORTED-ITEM-UUID") From 25b85934864aba35f85b411d272f3e916a8723a6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 7 Jul 2023 16:00:45 +0200 Subject: [PATCH 0214/1009] Fix missing name in Renault service descriptions (#96075) --- homeassistant/components/renault/services.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/renault/services.yaml b/homeassistant/components/renault/services.yaml index c8c3e9b12ba12e..5911c453c95863 100644 --- a/homeassistant/components/renault/services.yaml +++ b/homeassistant/components/renault/services.yaml @@ -1,4 +1,5 @@ ac_start: + name: Start A/C description: Start A/C on vehicle. fields: vehicle: @@ -25,6 +26,7 @@ ac_start: text: ac_cancel: + name: Cancel A/C description: Cancel A/C on vehicle. fields: vehicle: @@ -36,6 +38,7 @@ ac_cancel: integration: renault charge_set_schedules: + name: Update charge schedule description: Update charge schedule on vehicle. fields: vehicle: From c4c4b6c81bcbf61f38c5f88b16d85f5cd3311bc5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jul 2023 16:03:27 +0200 Subject: [PATCH 0215/1009] Add device class back to Purpleair (#96062) --- homeassistant/components/purpleair/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/purpleair/sensor.py b/homeassistant/components/purpleair/sensor.py index 160f529c285ff2..fffceffa343ce4 100644 --- a/homeassistant/components/purpleair/sensor.py +++ b/homeassistant/components/purpleair/sensor.py @@ -168,6 +168,7 @@ class PurpleAirSensorEntityDescription( # This sensor is an air quality index for VOCs. More info at https://github.com/home-assistant/core/pull/84896 key="voc", translation_key="voc_aqi", + device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda sensor: sensor.voc, ), From 8138c85074894d60993b78a710229057cc0c24d3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 7 Jul 2023 16:12:52 +0200 Subject: [PATCH 0216/1009] Fix missing name in TP-Link service descriptions (#96074) --- homeassistant/components/tplink/services.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/tplink/services.yaml b/homeassistant/components/tplink/services.yaml index 128c5c3a4937bc..16166278565aed 100644 --- a/homeassistant/components/tplink/services.yaml +++ b/homeassistant/components/tplink/services.yaml @@ -1,4 +1,5 @@ sequence_effect: + name: Sequence effect description: Set a sequence effect target: entity: @@ -6,6 +7,7 @@ sequence_effect: domain: light fields: sequence: + name: Sequence description: List of HSV sequences (Max 16) example: | - [340, 20, 50] @@ -15,6 +17,7 @@ sequence_effect: selector: object: segments: + name: Segments description: List of Segments (0 for all) example: 0, 2, 4, 6, 8 default: 0 @@ -22,6 +25,7 @@ sequence_effect: selector: object: brightness: + name: Brightness description: Initial brightness example: 80 default: 100 @@ -33,6 +37,7 @@ sequence_effect: max: 100 unit_of_measurement: "%" duration: + name: Duration description: Duration example: 0 default: 0 @@ -44,6 +49,7 @@ sequence_effect: max: 5000 unit_of_measurement: "ms" repeat_times: + name: Repetitions description: Repetitions (0 for continuous) example: 0 default: 0 @@ -54,6 +60,7 @@ sequence_effect: step: 1 max: 10 transition: + name: Transition description: Transition example: 2000 default: 0 @@ -65,6 +72,7 @@ sequence_effect: max: 6000 unit_of_measurement: "ms" spread: + name: Spread description: Speed of spread example: 1 default: 0 @@ -75,6 +83,7 @@ sequence_effect: step: 1 max: 16 direction: + name: Direction description: Direction example: 1 default: 4 @@ -85,6 +94,7 @@ sequence_effect: step: 1 max: 4 random_effect: + name: Random effect description: Set a random effect target: entity: From 529846d3a25ecc0ebec80f9228b73d66f6ad2505 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 7 Jul 2023 16:19:29 +0200 Subject: [PATCH 0217/1009] 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 c7c6585e0023d5..9bd9f7668c80bd 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 fec40ec25004786e93997350a2b4d86c19102b92 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jul 2023 16:32:59 +0200 Subject: [PATCH 0218/1009] Add entity translations to Recollect waste (#96037) --- homeassistant/components/recollect_waste/sensor.py | 4 ++-- homeassistant/components/recollect_waste/strings.json | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 4883734f47e175..5989fb1cfe347a 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -28,11 +28,11 @@ SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_TYPE_CURRENT_PICKUP, - name="Current pickup", + translation_key=SENSOR_TYPE_CURRENT_PICKUP, ), SensorEntityDescription( key=SENSOR_TYPE_NEXT_PICKUP, - name="Next pickup", + translation_key=SENSOR_TYPE_NEXT_PICKUP, ), ) diff --git a/homeassistant/components/recollect_waste/strings.json b/homeassistant/components/recollect_waste/strings.json index a350b9880fc89b..20aa5982f0d5c4 100644 --- a/homeassistant/components/recollect_waste/strings.json +++ b/homeassistant/components/recollect_waste/strings.json @@ -24,5 +24,15 @@ } } } + }, + "entity": { + "sensor": { + "current_pickup": { + "name": "Current pickup" + }, + "next_pickup": { + "name": "Next pickup" + } + } } } From f205d50ac710cc4cad505295fb72c5a3f84603fd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 7 Jul 2023 16:42:05 +0200 Subject: [PATCH 0219/1009] Fix missing name in FluxLED service descriptions (#96077) --- homeassistant/components/flux_led/services.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/flux_led/services.yaml b/homeassistant/components/flux_led/services.yaml index b17d81f9174076..5d880370818bb2 100644 --- a/homeassistant/components/flux_led/services.yaml +++ b/homeassistant/components/flux_led/services.yaml @@ -1,4 +1,5 @@ set_custom_effect: + name: Set custom effect description: Set a custom light effect. target: entity: @@ -37,6 +38,7 @@ set_custom_effect: - "jump" - "strobe" set_zones: + name: Set zones description: Set strip zones for Addressable v3 controllers (0xA3). target: entity: @@ -78,6 +80,7 @@ set_zones: - "jump" - "breathing" set_music_mode: + name: Set music mode description: Configure music mode on Controller RGB with MIC (0x08), Addressable v2 (0xA2), and Addressable v3 (0xA3) devices that have a built-in microphone. target: entity: From ddd0d3faa240f5b36d27e0730e817317dbf3900f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jul 2023 17:24:41 +0200 Subject: [PATCH 0220/1009] 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 160cd0e8634e01..64f7dafc1b7a49 100644 --- a/homeassistant/components/mystrom/__init__.py +++ b/homeassistant/components/mystrom/__init__.py @@ -22,6 +22,24 @@ _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 f0cc6224191373..8b3e2f8535f2d6 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 01b52d2cb94358..281d7af9947988 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={} - ), patch( - "pymystrom.bulb.MyStromBulb.bulb_type", bulb_type + side_effect=AsyncMock(return_value=get_default_device_response(device_type)), ), patch( - "pymystrom.switch.MyStromSwitch.mac", - new_callable=PropertyMock, - return_value=DEVICE_MAC, + "homeassistant.components.mystrom._get_mystrom_bulb", + return_value=MyStromBulbMock("6001940376EB", get_default_bulb_state()), ), 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 3fe180d55cafdb9f3885d4bf3c808f6bd9c3b31f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 7 Jul 2023 09:25:23 -0600 Subject: [PATCH 0221/1009] 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 a811894a0c2895..f603cf0ccd772c 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 7026ffe0a3e6c4d8e92ed3c909d75e46fcfcc970 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jul 2023 17:25:58 +0200 Subject: [PATCH 0222/1009] 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 47680714d1901c..4a71789423ffd7 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 fe6f07199c4f8f..d1272b7a1f6fb5 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 0f63aaa05bbbbf48fba59dcf7a5c13ab6475e0e7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jul 2023 18:08:33 +0200 Subject: [PATCH 0223/1009] Remove deprecated Pihole binary sensors (#95799) --- .../components/pi_hole/binary_sensor.py | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/homeassistant/components/pi_hole/binary_sensor.py b/homeassistant/components/pi_hole/binary_sensor.py index 7ec1bf40c665ef..5d1419db8b2e01 100644 --- a/homeassistant/components/pi_hole/binary_sensor.py +++ b/homeassistant/components/pi_hole/binary_sensor.py @@ -8,7 +8,6 @@ from hole import Hole from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -39,42 +38,6 @@ class PiHoleBinarySensorEntityDescription( BINARY_SENSOR_TYPES: tuple[PiHoleBinarySensorEntityDescription, ...] = ( - PiHoleBinarySensorEntityDescription( - # Deprecated, scheduled to be removed in 2022.6 - key="core_update_available", - name="Core Update Available", - entity_registry_enabled_default=False, - device_class=BinarySensorDeviceClass.UPDATE, - extra_value=lambda api: { - "current_version": api.versions["core_current"], - "latest_version": api.versions["core_latest"], - }, - state_value=lambda api: bool(api.versions["core_update"]), - ), - PiHoleBinarySensorEntityDescription( - # Deprecated, scheduled to be removed in 2022.6 - key="web_update_available", - name="Web Update Available", - entity_registry_enabled_default=False, - device_class=BinarySensorDeviceClass.UPDATE, - extra_value=lambda api: { - "current_version": api.versions["web_current"], - "latest_version": api.versions["web_latest"], - }, - state_value=lambda api: bool(api.versions["web_update"]), - ), - PiHoleBinarySensorEntityDescription( - # Deprecated, scheduled to be removed in 2022.6 - key="ftl_update_available", - name="FTL Update Available", - entity_registry_enabled_default=False, - device_class=BinarySensorDeviceClass.UPDATE, - extra_value=lambda api: { - "current_version": api.versions["FTL_current"], - "latest_version": api.versions["FTL_latest"], - }, - state_value=lambda api: bool(api.versions["FTL_update"]), - ), PiHoleBinarySensorEntityDescription( key="status", translation_key="status", From f2990d97b2beb83cf93c68755bc47b178d5c403c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 7 Jul 2023 18:10:44 +0200 Subject: [PATCH 0224/1009] Update sentry-sdk to 1.27.1 (#96089) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 336c1cbc7efac0..c3d0852e17ae9b 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/sentry", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["sentry-sdk==1.25.1"] + "requirements": ["sentry-sdk==1.27.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index e214740acec1c0..daf6db5cfda511 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2360,7 +2360,7 @@ sensorpro-ble==0.5.3 sensorpush-ble==1.5.5 # homeassistant.components.sentry -sentry-sdk==1.25.1 +sentry-sdk==1.27.1 # homeassistant.components.sfr_box sfrbox-api==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 583c7c96996626..b2cf1c3df1f8fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1723,7 +1723,7 @@ sensorpro-ble==0.5.3 sensorpush-ble==1.5.5 # homeassistant.components.sentry -sentry-sdk==1.25.1 +sentry-sdk==1.27.1 # homeassistant.components.sfr_box sfrbox-api==0.0.6 From 298ab05470656fbe270da50bada15cc624f31393 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 7 Jul 2023 18:15:06 +0200 Subject: [PATCH 0225/1009] Add missing issue translations to the kitchen_sink integration (#95931) --- homeassistant/components/demo/strings.json | 41 ------------------ .../components/kitchen_sink/strings.json | 43 +++++++++++++++++++ 2 files changed, 43 insertions(+), 41 deletions(-) create mode 100644 homeassistant/components/kitchen_sink/strings.json diff --git a/homeassistant/components/demo/strings.json b/homeassistant/components/demo/strings.json index 60db0322717d81..3794b27cc0ea01 100644 --- a/homeassistant/components/demo/strings.json +++ b/homeassistant/components/demo/strings.json @@ -1,46 +1,5 @@ { "title": "Demo", - "issues": { - "bad_psu": { - "title": "The power supply is not stable", - "fix_flow": { - "step": { - "confirm": { - "title": "The power supply needs to be replaced", - "description": "Press SUBMIT to confirm the power supply has been replaced" - } - } - } - }, - "out_of_blinker_fluid": { - "title": "The blinker fluid is empty and needs to be refilled", - "fix_flow": { - "step": { - "confirm": { - "title": "Blinker fluid needs to be refilled", - "description": "Press SUBMIT when blinker fluid has been refilled" - } - } - } - }, - "cold_tea": { - "title": "The tea is cold", - "fix_flow": { - "step": {}, - "abort": { - "not_tea_time": "Can not re-heat the tea at this time" - } - } - }, - "transmogrifier_deprecated": { - "title": "The transmogrifier component is deprecated", - "description": "The transmogrifier component is now deprecated due to the lack of local control available in the new API" - }, - "unfixable_problem": { - "title": "This is not a fixable problem", - "description": "This issue is never going to give up." - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/kitchen_sink/strings.json b/homeassistant/components/kitchen_sink/strings.json new file mode 100644 index 00000000000000..ce907a3368da12 --- /dev/null +++ b/homeassistant/components/kitchen_sink/strings.json @@ -0,0 +1,43 @@ +{ + "issues": { + "bad_psu": { + "title": "The power supply is not stable", + "fix_flow": { + "step": { + "confirm": { + "title": "The power supply needs to be replaced", + "description": "Press SUBMIT to confirm the power supply has been replaced" + } + } + } + }, + "out_of_blinker_fluid": { + "title": "The blinker fluid is empty and needs to be refilled", + "fix_flow": { + "step": { + "confirm": { + "title": "Blinker fluid needs to be refilled", + "description": "Press SUBMIT when blinker fluid has been refilled" + } + } + } + }, + "cold_tea": { + "title": "The tea is cold", + "fix_flow": { + "step": {}, + "abort": { + "not_tea_time": "Can not re-heat the tea at this time" + } + } + }, + "transmogrifier_deprecated": { + "title": "The transmogrifier component is deprecated", + "description": "The transmogrifier component is now deprecated due to the lack of local control available in the new API" + }, + "unfixable_problem": { + "title": "This is not a fixable problem", + "description": "This issue is never going to give up." + } + } +} From d1cfb6e1a899c35ca78ca2492400883f625b22c1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 7 Jul 2023 18:19:11 +0200 Subject: [PATCH 0226/1009] Remove unreferenced issues (#95976) --- .../components/android_ip_webcam/strings.json | 6 ------ homeassistant/components/apcupsd/strings.json | 6 ------ homeassistant/components/dlink/strings.json | 6 ------ homeassistant/components/unifiprotect/strings.json | 11 ----------- homeassistant/components/zamg/strings.json | 6 ------ 5 files changed, 35 deletions(-) diff --git a/homeassistant/components/android_ip_webcam/strings.json b/homeassistant/components/android_ip_webcam/strings.json index 6f6639cecb41a7..db21a69098477e 100644 --- a/homeassistant/components/android_ip_webcam/strings.json +++ b/homeassistant/components/android_ip_webcam/strings.json @@ -17,11 +17,5 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } - }, - "issues": { - "deprecated_yaml": { - "title": "The Android IP Webcam YAML configuration is being removed", - "description": "Configuring Android IP Webcam using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Android IP Webcam YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } } } diff --git a/homeassistant/components/apcupsd/strings.json b/homeassistant/components/apcupsd/strings.json index aef33a6f8bf805..c7ebf8a0a3bc5b 100644 --- a/homeassistant/components/apcupsd/strings.json +++ b/homeassistant/components/apcupsd/strings.json @@ -16,11 +16,5 @@ "description": "Enter the host and port on which the apcupsd NIS is being served." } } - }, - "issues": { - "deprecated_yaml": { - "title": "The APC UPS Daemon YAML configuration is being removed", - "description": "Configuring APC UPS Daemon using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the APC UPS Daemon YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } } } diff --git a/homeassistant/components/dlink/strings.json b/homeassistant/components/dlink/strings.json index 9ac7453093cb95..ee7abb3e97907d 100644 --- a/homeassistant/components/dlink/strings.json +++ b/homeassistant/components/dlink/strings.json @@ -24,11 +24,5 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } - }, - "issues": { - "deprecated_yaml": { - "title": "The D-Link Smart Plug YAML configuration is being removed", - "description": "Configuring D-Link Smart Plug using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the D-Link Power Plug YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } } } diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index f8d578e1ca48b1..fc50e8141a1660 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -79,17 +79,6 @@ "deprecate_smart_sensor": { "title": "Smart Detection Sensor Deprecated", "description": "The unified \"Detected Object\" sensor for smart detections is now deprecated. It has been replaced with individual smart detection binary sensors for each smart detection type.\n\nBelow are the detected automations or scripts that use one or more of the deprecated entities:\n{items}\nThe above list may be incomplete and it does not include any template usages inside of dashboards. Please update any templates, automations or scripts accordingly." - }, - "deprecated_service_set_doorbell_message": { - "title": "set_doorbell_message is Deprecated", - "fix_flow": { - "step": { - "confirm": { - "title": "set_doorbell_message is Deprecated", - "description": "The `unifiprotect.set_doorbell_message` service is deprecated in favor of the new Doorbell Text entity added to each Doorbell device. It will be removed in v2023.3.0. Please update to use the [`text.set_value` service]({link})." - } - } - } } }, "entity": { diff --git a/homeassistant/components/zamg/strings.json b/homeassistant/components/zamg/strings.json index 6305f68efd933a..f0a607f2da7da8 100644 --- a/homeassistant/components/zamg/strings.json +++ b/homeassistant/components/zamg/strings.json @@ -18,11 +18,5 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "station_not_found": "Station ID not found at zamg" } - }, - "issues": { - "deprecated_yaml": { - "title": "The ZAMG YAML configuration is being removed", - "description": "Configuring ZAMG using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the ZAMG YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } } } From 6402e2c1404efdb51af247bafecf1bf7c06fdf8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Jul 2023 06:25:54 -1000 Subject: [PATCH 0227/1009] Bump aioesphomeapi to 15.1.3 (#95819) --- 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 8fc16926e563c9..1acf0f1154e472 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.1.2", + "aioesphomeapi==15.1.3", "bluetooth-data-tools==1.3.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index daf6db5cfda511..d038879b629475 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.1.2 +aioesphomeapi==15.1.3 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2cf1c3df1f8fd..531ce982f08f25 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.1.2 +aioesphomeapi==15.1.3 # homeassistant.components.flo aioflo==2021.11.0 From 29d7535b7b0b5f7a026281b24f4ed28803687309 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jul 2023 18:27:44 +0200 Subject: [PATCH 0228/1009] Add entity translations to Rainmachine (#96033) --- .../components/rainmachine/binary_sensor.py | 14 ++-- .../components/rainmachine/button.py | 1 - .../components/rainmachine/select.py | 2 +- .../components/rainmachine/sensor.py | 16 ++--- .../components/rainmachine/strings.json | 69 +++++++++++++++++++ .../components/rainmachine/switch.py | 4 +- .../components/rainmachine/update.py | 4 +- 7 files changed, 89 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 33650cfc2fef13..7f93db67c4cb32 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -44,14 +44,14 @@ class RainMachineBinarySensorDescription( BINARY_SENSOR_DESCRIPTIONS = ( RainMachineBinarySensorDescription( key=TYPE_FLOW_SENSOR, - name="Flow sensor", + translation_key=TYPE_FLOW_SENSOR, icon="mdi:water-pump", api_category=DATA_PROVISION_SETTINGS, data_key="useFlowSensor", ), RainMachineBinarySensorDescription( key=TYPE_FREEZE, - name="Freeze restrictions", + translation_key=TYPE_FREEZE, icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_CURRENT, @@ -59,7 +59,7 @@ class RainMachineBinarySensorDescription( ), RainMachineBinarySensorDescription( key=TYPE_HOURLY, - name="Hourly restrictions", + translation_key=TYPE_HOURLY, icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_CURRENT, @@ -67,7 +67,7 @@ class RainMachineBinarySensorDescription( ), RainMachineBinarySensorDescription( key=TYPE_MONTH, - name="Month restrictions", + translation_key=TYPE_MONTH, icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_CURRENT, @@ -75,7 +75,7 @@ class RainMachineBinarySensorDescription( ), RainMachineBinarySensorDescription( key=TYPE_RAINDELAY, - name="Rain delay restrictions", + translation_key=TYPE_RAINDELAY, icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_CURRENT, @@ -83,7 +83,7 @@ class RainMachineBinarySensorDescription( ), RainMachineBinarySensorDescription( key=TYPE_RAINSENSOR, - name="Rain sensor restrictions", + translation_key=TYPE_RAINSENSOR, icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -92,7 +92,7 @@ class RainMachineBinarySensorDescription( ), RainMachineBinarySensorDescription( key=TYPE_WEEKDAY, - name="Weekday restrictions", + translation_key=TYPE_WEEKDAY, icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_CURRENT, diff --git a/homeassistant/components/rainmachine/button.py b/homeassistant/components/rainmachine/button.py index d4ed17c72e9ee6..82829094957048 100644 --- a/homeassistant/components/rainmachine/button.py +++ b/homeassistant/components/rainmachine/button.py @@ -51,7 +51,6 @@ async def _async_reboot(controller: Controller) -> None: BUTTON_DESCRIPTIONS = ( RainMachineButtonDescription( key=BUTTON_KIND_REBOOT, - name="Reboot", api_category=DATA_PROVISION_SETTINGS, push_action=_async_reboot, ), diff --git a/homeassistant/components/rainmachine/select.py b/homeassistant/components/rainmachine/select.py index f482deb4ef4518..2a5bc93f60146f 100644 --- a/homeassistant/components/rainmachine/select.py +++ b/homeassistant/components/rainmachine/select.py @@ -59,7 +59,7 @@ class FreezeProtectionSelectDescription( SELECT_DESCRIPTIONS = ( FreezeProtectionSelectDescription( key=TYPE_FREEZE_PROTECTION_TEMPERATURE, - name="Freeze protection temperature", + translation_key=TYPE_FREEZE_PROTECTION_TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, api_category=DATA_RESTRICTIONS_UNIVERSAL, diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 22943d73fcb4dc..6333dcc82f4699 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -69,7 +69,7 @@ class RainMachineSensorCompletionTimerDescription( SENSOR_DESCRIPTIONS = ( RainMachineSensorDataDescription( key=TYPE_FLOW_SENSOR_CLICK_M3, - name="Flow sensor clicks per cubic meter", + translation_key=TYPE_FLOW_SENSOR_CLICK_M3, icon="mdi:water-pump", native_unit_of_measurement=f"clicks/{UnitOfVolume.CUBIC_METERS}", entity_category=EntityCategory.DIAGNOSTIC, @@ -80,7 +80,7 @@ class RainMachineSensorCompletionTimerDescription( ), RainMachineSensorDataDescription( key=TYPE_FLOW_SENSOR_CONSUMED_LITERS, - name="Flow sensor consumed liters", + translation_key=TYPE_FLOW_SENSOR_CONSUMED_LITERS, icon="mdi:water-pump", device_class=SensorDeviceClass.WATER, entity_category=EntityCategory.DIAGNOSTIC, @@ -92,7 +92,7 @@ class RainMachineSensorCompletionTimerDescription( ), RainMachineSensorDataDescription( key=TYPE_FLOW_SENSOR_LEAK_CLICKS, - name="Flow sensor leak clicks", + translation_key=TYPE_FLOW_SENSOR_LEAK_CLICKS, icon="mdi:pipe-leak", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="clicks", @@ -103,7 +103,7 @@ class RainMachineSensorCompletionTimerDescription( ), RainMachineSensorDataDescription( key=TYPE_FLOW_SENSOR_LEAK_VOLUME, - name="Flow sensor leak volume", + translation_key=TYPE_FLOW_SENSOR_LEAK_VOLUME, icon="mdi:pipe-leak", device_class=SensorDeviceClass.WATER, entity_category=EntityCategory.DIAGNOSTIC, @@ -115,7 +115,7 @@ class RainMachineSensorCompletionTimerDescription( ), RainMachineSensorDataDescription( key=TYPE_FLOW_SENSOR_START_INDEX, - name="Flow sensor start index", + translation_key=TYPE_FLOW_SENSOR_START_INDEX, icon="mdi:water-pump", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="index", @@ -125,7 +125,7 @@ class RainMachineSensorCompletionTimerDescription( ), RainMachineSensorDataDescription( key=TYPE_FLOW_SENSOR_WATERING_CLICKS, - name="Flow sensor clicks", + translation_key=TYPE_FLOW_SENSOR_WATERING_CLICKS, icon="mdi:water-pump", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="clicks", @@ -136,7 +136,7 @@ class RainMachineSensorCompletionTimerDescription( ), RainMachineSensorDataDescription( key=TYPE_LAST_LEAK_DETECTED, - name="Last leak detected", + translation_key=TYPE_LAST_LEAK_DETECTED, icon="mdi:pipe-leak", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -147,7 +147,7 @@ class RainMachineSensorCompletionTimerDescription( ), RainMachineSensorDataDescription( key=TYPE_RAIN_SENSOR_RAIN_START, - name="Rain sensor rain start", + translation_key=TYPE_RAIN_SENSOR_RAIN_START, icon="mdi:weather-pouring", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, diff --git a/homeassistant/components/rainmachine/strings.json b/homeassistant/components/rainmachine/strings.json index 9991fd31e034c5..884d05359a61bb 100644 --- a/homeassistant/components/rainmachine/strings.json +++ b/homeassistant/components/rainmachine/strings.json @@ -28,5 +28,74 @@ } } } + }, + "entity": { + "binary_sensor": { + "flow_sensor": { + "name": "Flow sensor" + }, + "freeze": { + "name": "Freeze restrictions" + }, + "hourly": { + "name": "Hourly restrictions" + }, + "month": { + "name": "Month restrictions" + }, + "raindelay": { + "name": "Rain delay restrictions" + }, + "rainsensor": { + "name": "Rain sensor restrictions" + }, + "weekday": { + "name": "Weekday restrictions" + } + }, + "select": { + "freeze_protection_temperature": { + "name": "Freeze protection temperature" + } + }, + "sensor": { + "flow_sensor_clicks_cubic_meter": { + "name": "Flow sensor clicks per cubic meter" + }, + "flow_sensor_consumed_liters": { + "name": "Flow sensor consumed liters" + }, + "flow_sensor_leak_clicks": { + "name": "Flow sensor leak clicks" + }, + "flow_sensor_leak_volume": { + "name": "Flow sensor leak volume" + }, + "flow_sensor_start_index": { + "name": "Flow sensor start index" + }, + "flow_sensor_watering_clicks": { + "name": "Flow sensor clicks" + }, + "last_leak_detected": { + "name": "Last leak detected" + }, + "rain_sensor_rain_start": { + "name": "Rain sensor rain start" + } + }, + "switch": { + "freeze_protect_enabled": { + "name": "Freeze protection" + }, + "hot_days_extra_watering": { + "name": "Extra water on hot days" + } + }, + "update": { + "firmware": { + "name": "Firmware" + } + } } } diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 60db5085951cf3..e6ed92d04dc25b 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -161,14 +161,14 @@ class RainMachineRestrictionSwitchDescription( RESTRICTIONS_SWITCH_DESCRIPTIONS = ( RainMachineRestrictionSwitchDescription( key=TYPE_RESTRICTIONS_FREEZE_PROTECT_ENABLED, - name="Freeze protection", + translation_key=TYPE_RESTRICTIONS_FREEZE_PROTECT_ENABLED, icon="mdi:snowflake-alert", api_category=DATA_RESTRICTIONS_UNIVERSAL, data_key="freezeProtectEnabled", ), RainMachineRestrictionSwitchDescription( key=TYPE_RESTRICTIONS_HOT_DAYS_EXTRA_WATERING, - name="Extra water on hot days", + translation_key=TYPE_RESTRICTIONS_HOT_DAYS_EXTRA_WATERING, icon="mdi:heat-wave", api_category=DATA_RESTRICTIONS_UNIVERSAL, data_key="hotDaysExtraWatering", diff --git a/homeassistant/components/rainmachine/update.py b/homeassistant/components/rainmachine/update.py index f603cf0ccd772c..372319ba9a0f71 100644 --- a/homeassistant/components/rainmachine/update.py +++ b/homeassistant/components/rainmachine/update.py @@ -44,7 +44,7 @@ class UpdateStates(Enum): UPDATE_DESCRIPTION = RainMachineEntityDescription( key="update", - name="Firmware", + translation_key="firmware", api_category=DATA_MACHINE_FIRMWARE_UPDATE_STATUS, ) @@ -52,7 +52,7 @@ class UpdateStates(Enum): async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up WLED update based on a config entry.""" + """Set up Rainmachine update based on a config entry.""" data: RainMachineData = hass.data[DOMAIN][entry.entry_id] async_add_entities([RainMachineUpdateEntity(entry, data, UPDATE_DESCRIPTION)]) From ac19de98574df7b65484a6538d8e59b6a3b87718 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 7 Jul 2023 18:30:00 +0200 Subject: [PATCH 0229/1009] Make season integration title translatable (#95802) --- homeassistant/components/season/strings.json | 1 + homeassistant/generated/integrations.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/season/strings.json b/homeassistant/components/season/strings.json index bff02df5c6c226..d53d6a0890f1da 100644 --- a/homeassistant/components/season/strings.json +++ b/homeassistant/components/season/strings.json @@ -1,4 +1,5 @@ { + "title": "Season", "config": { "step": { "user": { diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 9964bfe148ccd0..e8271f2cadec17 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4823,7 +4823,6 @@ "iot_class": "local_polling" }, "season": { - "name": "Season", "integration_type": "service", "config_flow": true, "iot_class": "local_polling" @@ -6687,6 +6686,7 @@ "proximity", "rpi_power", "schedule", + "season", "shopping_list", "sun", "switch_as_x", From daa9162ca7478f2c85187ef4779f5939d152ae66 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jul 2023 18:58:55 +0200 Subject: [PATCH 0230/1009] Add entity translations to pvoutput (#96029) --- homeassistant/components/pvoutput/sensor.py | 12 +++++------- .../components/pvoutput/strings.json | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 700757c6d58d38..b681678b098e62 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -45,7 +45,7 @@ class PVOutputSensorEntityDescription( SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( PVOutputSensorEntityDescription( key="energy_consumption", - name="Energy consumed", + translation_key="energy_consumption", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -53,7 +53,7 @@ class PVOutputSensorEntityDescription( ), PVOutputSensorEntityDescription( key="energy_generation", - name="Energy generated", + translation_key="energy_generation", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -61,7 +61,7 @@ class PVOutputSensorEntityDescription( ), PVOutputSensorEntityDescription( key="normalized_output", - name="Efficiency", + translation_key="efficiency", native_unit_of_measurement=( f"{UnitOfEnergy.KILO_WATT_HOUR}/{UnitOfPower.KILO_WATT}" ), @@ -70,7 +70,7 @@ class PVOutputSensorEntityDescription( ), PVOutputSensorEntityDescription( key="power_consumption", - name="Power consumed", + translation_key="power_consumption", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -78,7 +78,7 @@ class PVOutputSensorEntityDescription( ), PVOutputSensorEntityDescription( key="power_generation", - name="Power generated", + translation_key="power_generation", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -86,7 +86,6 @@ class PVOutputSensorEntityDescription( ), PVOutputSensorEntityDescription( key="temperature", - name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -94,7 +93,6 @@ class PVOutputSensorEntityDescription( ), PVOutputSensorEntityDescription( key="voltage", - name="Voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/pvoutput/strings.json b/homeassistant/components/pvoutput/strings.json index 12f30b773d5dcf..06d9897105353e 100644 --- a/homeassistant/components/pvoutput/strings.json +++ b/homeassistant/components/pvoutput/strings.json @@ -23,5 +23,24 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "entity": { + "sensor": { + "energy_consumption": { + "name": "Energy consumed" + }, + "energy_generation": { + "name": "Energy generated" + }, + "efficiency": { + "name": "Efficiency" + }, + "power_consumption": { + "name": "Power consumed" + }, + "power_generation": { + "name": "Power generated" + } + } } } From 1e4f43452caf333b61dba65073e9052b0bee8a12 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 7 Jul 2023 19:00:06 +0200 Subject: [PATCH 0231/1009] Warn when vacuum.turn_on or turn_off is called on Tuya vacuums (#95848) Co-authored-by: Hmmbob <33529490+hmmbob@users.noreply.github.com> --- homeassistant/components/tuya/strings.json | 10 ++++++++++ homeassistant/components/tuya/vacuum.py | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 15e41043f5ae0f..0cab59de2912c8 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -212,5 +212,15 @@ } } } + }, + "issues": { + "service_deprecation_turn_off": { + "title": "Tuya vacuum support for vacuum.turn_off is being removed", + "description": "Tuya vacuum support for the vacuum.turn_off service is deprecated and will be removed in Home Assistant 2024.2; Please adjust any automation or script that uses the service to instead call vacuum.stop and select submit below to mark this issue as resolved." + }, + "service_deprecation_turn_on": { + "title": "Tuya vacuum support for vacuum.turn_on is being removed", + "description": "Tuya vacuum support for the vacuum.turn_on service is deprecated and will be removed in Home Assistant 2024.2; Please adjust any automation or script that uses the service to instead call vacuum.start and select submit below to mark this issue as resolved." + } } } diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index 3c6ede66c69abf..b332be7de2d650 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -15,6 +15,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_IDLE, STATE_PAUSED from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -153,10 +154,30 @@ def state(self) -> str | None: def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._send_command([{"code": DPCode.POWER, "value": True}]) + ir.async_create_issue( + self.hass, + DOMAIN, + "service_deprecation_turn_on", + breaks_in_ha_version="2024.2.0", + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="service_deprecation_turn_on", + ) def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._send_command([{"code": DPCode.POWER, "value": False}]) + ir.async_create_issue( + self.hass, + DOMAIN, + "service_deprecation_turn_off", + breaks_in_ha_version="2024.2.0", + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="service_deprecation_turn_off", + ) def start(self, **kwargs: Any) -> None: """Start the device.""" From 849aa5d9efa77753743aac6b25d8a3fc0b3f1a8d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jul 2023 19:15:41 +0200 Subject: [PATCH 0232/1009] 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 9e97c2f080fb55..0ecf0e7b697482 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 17440c960844d92a3b03ef21b6e40c3911608413 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jul 2023 19:24:52 +0200 Subject: [PATCH 0233/1009] Add entity translations to Rympro (#96087) --- homeassistant/components/rympro/sensor.py | 2 +- homeassistant/components/rympro/strings.json | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rympro/sensor.py b/homeassistant/components/rympro/sensor.py index 80675d9dec8cf9..2c1a3ecee118e8 100644 --- a/homeassistant/components/rympro/sensor.py +++ b/homeassistant/components/rympro/sensor.py @@ -34,7 +34,7 @@ class RymProSensor(CoordinatorEntity[RymProDataUpdateCoordinator], SensorEntity) """Sensor for RymPro meters.""" _attr_has_entity_name = True - _attr_name = "Total consumption" + _attr_translation_key = "total_consumption" _attr_device_class = SensorDeviceClass.WATER _attr_native_unit_of_measurement = UnitOfVolume.CUBIC_METERS _attr_state_class = SensorStateClass.TOTAL_INCREASING diff --git a/homeassistant/components/rympro/strings.json b/homeassistant/components/rympro/strings.json index b6e7adc9631e99..2909d6c1b9b848 100644 --- a/homeassistant/components/rympro/strings.json +++ b/homeassistant/components/rympro/strings.json @@ -16,5 +16,12 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "sensor": { + "total_consumption": { + "name": "Total consumption" + } + } } } From f1db497efeee288dc3436df3c50c7f73c8872fe6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Jul 2023 07:36:38 -1000 Subject: [PATCH 0234/1009] Avoid http route linear search fallback when there are multiple paths (#95776) --- homeassistant/components/http/__init__.py | 29 +++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index c8fa05b2730751..f559b09a1ff992 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -582,32 +582,37 @@ class FastUrlDispatcher(UrlDispatcher): def __init__(self) -> None: """Initialize the dispatcher.""" super().__init__() - self._resource_index: dict[str, AbstractResource] = {} + self._resource_index: dict[str, list[AbstractResource]] = {} def register_resource(self, resource: AbstractResource) -> None: """Register a resource.""" super().register_resource(resource) canonical = resource.canonical if "{" in canonical: # strip at the first { to allow for variables - canonical = canonical.split("{")[0] - canonical = canonical.rstrip("/") - self._resource_index[canonical] = resource + canonical = canonical.split("{")[0].rstrip("/") + # There may be multiple resources for a canonical path + # so we use a list to avoid falling back to a full linear search + self._resource_index.setdefault(canonical, []).append(resource) async def resolve(self, request: web.Request) -> UrlMappingMatchInfo: """Resolve a request.""" url_parts = request.rel_url.raw_parts resource_index = self._resource_index + # Walk the url parts looking for candidates for i in range(len(url_parts), 1, -1): url_part = "/" + "/".join(url_parts[1:i]) - if (resource_candidate := resource_index.get(url_part)) is not None and ( - match_dict := (await resource_candidate.resolve(request))[0] - ) is not None: - return match_dict + if (resource_candidates := resource_index.get(url_part)) is not None: + for candidate in resource_candidates: + if ( + match_dict := (await candidate.resolve(request))[0] + ) is not None: + return match_dict # Next try the index view if we don't have a match - if (index_view_candidate := resource_index.get("/")) is not None and ( - match_dict := (await index_view_candidate.resolve(request))[0] - ) is not None: - return match_dict + if (index_view_candidates := resource_index.get("/")) is not None: + for candidate in index_view_candidates: + if (match_dict := (await candidate.resolve(request))[0]) is not None: + return match_dict + # Finally, fallback to the linear search return await super().resolve(request) From 914fc570c6c4a3cf8f2f7e9bb16c396d7fe6c720 Mon Sep 17 00:00:00 2001 From: Patrick ZAJDA Date: Fri, 7 Jul 2023 19:38:43 +0200 Subject: [PATCH 0235/1009] Set some Switchbot entity names to none (#90846) --- homeassistant/components/switchbot/binary_sensor.py | 6 +++--- homeassistant/components/switchbot/sensor.py | 2 +- homeassistant/components/switchbot/strings.json | 9 --------- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index cb11c64f16ad84..237a2d9766825d 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -25,12 +25,12 @@ ), "motion_detected": BinarySensorEntityDescription( key="pir_state", - translation_key="motion", + name=None, device_class=BinarySensorDeviceClass.MOTION, ), "contact_open": BinarySensorEntityDescription( key="contact_open", - translation_key="door_open", + name=None, device_class=BinarySensorDeviceClass.DOOR, ), "contact_timeout": BinarySensorEntityDescription( @@ -46,7 +46,7 @@ ), "door_open": BinarySensorEntityDescription( key="door_status", - translation_key="door_open", + name=None, device_class=BinarySensorDeviceClass.DOOR, ), "unclosed_alarm": BinarySensorEntityDescription( diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index b5b34bf54ec81f..e9e434bc51ca76 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -67,7 +67,7 @@ ), "temperature": SensorEntityDescription( key="temperature", - translation_key="temperature", + name=None, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index fb9f906527cd1e..c00f2fe79e4e79 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -64,12 +64,6 @@ "calibration": { "name": "Calibration" }, - "motion": { - "name": "[%key:component::binary_sensor::entity_component::motion::name%]" - }, - "door_open": { - "name": "[%key:component::binary_sensor::entity_component::door::name%]" - }, "door_timeout": { "name": "Timeout" }, @@ -102,9 +96,6 @@ "humidity": { "name": "[%key:component::sensor::entity_component::humidity::name%]" }, - "temperature": { - "name": "[%key:component::sensor::entity_component::temperature::name%]" - }, "power": { "name": "[%key:component::sensor::entity_component::power::name%]" } From 372687fe811dbaf784f317f1f48ac0477f725162 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 7 Jul 2023 20:02:47 +0200 Subject: [PATCH 0236/1009] Update PyTurboJPEG to 1.7.1 (#96104) --- homeassistant/components/camera/manifest.json | 2 +- homeassistant/components/stream/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index a0ae9d925a89cf..b1df158a260904 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/camera", "integration_type": "entity", "quality_scale": "internal", - "requirements": ["PyTurboJPEG==1.6.7"] + "requirements": ["PyTurboJPEG==1.7.1"] } diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index 8b8e9b8a427f06..c07a083ac52c62 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["PyTurboJPEG==1.6.7", "ha-av==10.1.0", "numpy==1.23.2"] + "requirements": ["PyTurboJPEG==1.7.1", "ha-av==10.1.0", "numpy==1.23.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 93cdc1eb3d7f97..d4cdafd7ec1981 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -40,7 +40,7 @@ PyNaCl==1.5.0 pyOpenSSL==23.2.0 pyserial==3.5 python-slugify==4.0.1 -PyTurboJPEG==1.6.7 +PyTurboJPEG==1.7.1 pyudev==0.23.2 PyYAML==6.0 requests==2.31.0 diff --git a/requirements_all.txt b/requirements_all.txt index d038879b629475..8d949afdbbd881 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -112,7 +112,7 @@ PyTransportNSW==0.1.1 # homeassistant.components.camera # homeassistant.components.stream -PyTurboJPEG==1.6.7 +PyTurboJPEG==1.7.1 # homeassistant.components.vicare PyViCare==2.25.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 531ce982f08f25..470191cba22682 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -96,7 +96,7 @@ PyTransportNSW==0.1.1 # homeassistant.components.camera # homeassistant.components.stream -PyTurboJPEG==1.6.7 +PyTurboJPEG==1.7.1 # homeassistant.components.vicare PyViCare==2.25.0 From 18ee9f4725b6058a2b5d736c61554d5bd0be80ac Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 7 Jul 2023 20:52:38 +0200 Subject: [PATCH 0237/1009] 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 dbc8769bb6f440..82ea72281570d0 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -16,7 +16,6 @@ ) import concurrent.futures from contextlib import suppress -from contextvars import ContextVar import datetime import enum import functools @@ -155,8 +154,6 @@ class ConfigSource(StrEnum): _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]: @@ -199,16 +196,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 @@ -292,9 +300,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 cea8a866f5c246..e8f1e58615cfe3 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -93,7 +93,7 @@ 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 raise_issue() -> None: # 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 56014d7a556449..922e42c7a7e33f 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 458774b748ce60..5ea6df42349d28 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -12,6 +12,7 @@ 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 8b63eab7b4226a..7e0766c8ac5fbc 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -9,10 +9,12 @@ 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 @@ ServiceResponse, State, SupportsResponse, + callback, ) from homeassistant.exceptions import ( HomeAssistantError, @@ -202,6 +205,184 @@ def job(): 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 7f184e05e34e4394ccef952cb194218345e0315d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 8 Jul 2023 01:36:14 +0200 Subject: [PATCH 0238/1009] 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 d7af3b666885c3..bac4e63e288fe6 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 a0d54e8f4e60d80e003edcaba29edcd11e465e11 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 8 Jul 2023 01:42:19 +0200 Subject: [PATCH 0239/1009] 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 4913d76c0c9712..b895be83f2edf2 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 d31dc5da2828de..34c0ea5ea95b43 100644 --- a/homeassistant/components/simplisafe/binary_sensor.py +++ b/homeassistant/components/simplisafe/binary_sensor.py @@ -111,7 +111,6 @@ def __init__( """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 d8da8bc75927e7..bd60c040f5695d 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 618c21566f7e14..4f230442f8567a 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 c2ccd185289640caf806f7fe6701b842d3a50068 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 7 Jul 2023 21:40:18 -0400 Subject: [PATCH 0240/1009] 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 88bcdd4987b9c8..f1bfc7de876748 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 8d949afdbbd881..977bfcb31fcdfa 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 470191cba22682..3a7dbbf3c0cd83 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 a6fe53f2b34b62e76eed3f46a626d57e2ccc3694 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 8 Jul 2023 08:50:47 +0200 Subject: [PATCH 0241/1009] Fix missing name in Fritz!Box service descriptions (#96076) --- homeassistant/components/fritz/services.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/fritz/services.yaml b/homeassistant/components/fritz/services.yaml index 3c7ed6438417a6..95527257ea9c48 100644 --- a/homeassistant/components/fritz/services.yaml +++ b/homeassistant/components/fritz/services.yaml @@ -1,4 +1,5 @@ reconnect: + name: Reconnect description: Reconnects your FRITZ!Box internet connection fields: device_id: @@ -11,6 +12,7 @@ reconnect: entity: device_class: connectivity reboot: + name: Reboot description: Reboots your FRITZ!Box fields: device_id: @@ -24,6 +26,7 @@ reboot: device_class: connectivity cleanup: + name: Remove stale device tracker entities description: Remove FRITZ!Box stale device_tracker entities fields: device_id: From 8bfac2c46c86dcf1ad73c76f642e1a12019eff59 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 8 Jul 2023 02:52:15 -0400 Subject: [PATCH 0242/1009] Correct Goalzero sensor state class (#96122) --- homeassistant/components/goalzero/sensor.py | 2 +- tests/components/goalzero/test_sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index 61ec45a98f9d02..9001824d678d80 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -72,7 +72,7 @@ name="Wh stored", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="volts", diff --git a/tests/components/goalzero/test_sensor.py b/tests/components/goalzero/test_sensor.py index 47fbb29915b2c6..90b1489803a812 100644 --- a/tests/components/goalzero/test_sensor.py +++ b/tests/components/goalzero/test_sensor.py @@ -66,7 +66,7 @@ async def test_sensors( assert state.state == "1330" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.WATT_HOUR - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL state = hass.states.get(f"sensor.{DEFAULT_NAME}_volts") assert state.state == "12.0" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE From 5d1b4f48e0a9237711f86570e0b4d4864e01a386 Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Sat, 8 Jul 2023 08:59:26 +0200 Subject: [PATCH 0243/1009] Rename 'Switch as X' helper to ... (#96114) --- homeassistant/components/switch_as_x/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch_as_x/manifest.json b/homeassistant/components/switch_as_x/manifest.json index b54509ea5e3103..d14a6bcb390ffa 100644 --- a/homeassistant/components/switch_as_x/manifest.json +++ b/homeassistant/components/switch_as_x/manifest.json @@ -1,6 +1,6 @@ { "domain": "switch_as_x", - "name": "Switch as X", + "name": "Change device type of a switch", "codeowners": ["@home-assistant/core"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/switch_as_x", From 7178e1cefeba616857bed39d51dbb79cb05afa35 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 8 Jul 2023 08:59:34 +0200 Subject: [PATCH 0244/1009] Update apprise to 1.4.5 (#96086) --- homeassistant/components/apprise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index 9a56f5d91ebb16..04dcef052025aa 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/apprise", "iot_class": "cloud_push", "loggers": ["apprise"], - "requirements": ["apprise==1.4.0"] + "requirements": ["apprise==1.4.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 977bfcb31fcdfa..f57bf13ac7dd8f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -414,7 +414,7 @@ anthemav==1.4.1 apcaccess==0.0.13 # homeassistant.components.apprise -apprise==1.4.0 +apprise==1.4.5 # homeassistant.components.aprs aprslib==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a7dbbf3c0cd83..9a2602d6b7eb2c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -377,7 +377,7 @@ anthemav==1.4.1 apcaccess==0.0.13 # homeassistant.components.apprise -apprise==1.4.0 +apprise==1.4.5 # homeassistant.components.aprs aprslib==0.7.0 From 967c4d13d8cc4a707c7b0986f0d211db659e9681 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 8 Jul 2023 09:17:58 +0200 Subject: [PATCH 0245/1009] Update pipdeptree to 2.9.4 (#96115) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 74369507229a31..2ea19443aa40c5 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -16,7 +16,7 @@ pre-commit==3.1.0 pydantic==1.10.9 pylint==2.17.4 pylint-per-file-ignores==1.1.0 -pipdeptree==2.7.0 +pipdeptree==2.9.4 pytest-asyncio==0.20.3 pytest-aiohttp==1.0.4 pytest-cov==3.0.0 From e38f55fdb6f992a2b5c30a0630261d9db84294b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Jul 2023 21:19:44 -1000 Subject: [PATCH 0246/1009] Move ESPHomeManager into its own file (#95870) * Move ESPHomeManager into its own file This is not a functional change. This is only a reorganization ahead of some more test coverage being added so moving tests around can be avoided later. * relos * fixes * merge a portion of new cover since its small and allows us to remove the __init__ from .coveragerc --- .coveragerc | 2 +- homeassistant/components/esphome/__init__.py | 691 +---------------- .../components/esphome/config_flow.py | 3 +- homeassistant/components/esphome/const.py | 12 + homeassistant/components/esphome/manager.py | 696 ++++++++++++++++++ tests/components/esphome/conftest.py | 10 +- tests/components/esphome/test_config_flow.py | 6 +- tests/components/esphome/test_diagnostics.py | 5 +- tests/components/esphome/test_init.py | 16 + tests/components/esphome/test_update.py | 2 +- .../esphome/test_voice_assistant.py | 13 +- 11 files changed, 753 insertions(+), 703 deletions(-) create mode 100644 homeassistant/components/esphome/manager.py diff --git a/.coveragerc b/.coveragerc index e69683288a2a0d..0d44a63633ae43 100644 --- a/.coveragerc +++ b/.coveragerc @@ -304,10 +304,10 @@ omit = homeassistant/components/escea/__init__.py homeassistant/components/escea/climate.py homeassistant/components/escea/discovery.py - homeassistant/components/esphome/__init__.py homeassistant/components/esphome/bluetooth/* homeassistant/components/esphome/domain_data.py homeassistant/components/esphome/entry_data.py + homeassistant/components/esphome/manager.py homeassistant/components/etherscan/sensor.py homeassistant/components/eufy/* homeassistant/components/eufylife_ble/__init__.py diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index fedb2edd899d50..fb13e86dd1d64b 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -1,528 +1,42 @@ """Support for esphome devices.""" from __future__ import annotations -import logging -from typing import Any, NamedTuple, TypeVar - from aioesphomeapi import ( APIClient, - APIConnectionError, - APIVersion, - DeviceInfo as EsphomeDeviceInfo, - HomeassistantServiceCall, - InvalidAuthAPIError, - InvalidEncryptionKeyAPIError, - ReconnectLogic, - RequiresEncryptionAPIError, - UserService, - UserServiceArgType, - VoiceAssistantEventType, ) -from awesomeversion import AwesomeVersion -import voluptuous as vol -from homeassistant.components import tag, zeroconf +from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_DEVICE_ID, CONF_HOST, - CONF_MODE, CONF_PASSWORD, CONF_PORT, - EVENT_HOMEASSISTANT_STOP, __version__ as ha_version, ) -from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback -from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.issue_registry import ( - IssueSeverity, - async_create_issue, - async_delete_issue, -) -from homeassistant.helpers.service import async_set_service_schema -from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType -from .bluetooth import async_connect_scanner from .const import ( - CONF_ALLOW_SERVICE_CALLS, - DEFAULT_ALLOW_SERVICE_CALLS, + CONF_NOISE_PSK, DOMAIN, ) -from .dashboard import async_get_dashboard, async_setup as async_setup_dashboard +from .dashboard import async_setup as async_setup_dashboard from .domain_data import DomainData # Import config flow so that it's added to the registry from .entry_data import RuntimeEntryData -from .voice_assistant import VoiceAssistantUDPServer - -CONF_DEVICE_NAME = "device_name" -CONF_NOISE_PSK = "noise_psk" -_LOGGER = logging.getLogger(__name__) -_R = TypeVar("_R") - -STABLE_BLE_VERSION_STR = "2023.6.0" -STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR) -PROJECT_URLS = { - "esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/", -} -DEFAULT_URL = f"https://esphome.io/changelog/{STABLE_BLE_VERSION_STR}.html" +from .manager import ESPHomeManager, cleanup_instance CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) -@callback -def _async_check_firmware_version( - hass: HomeAssistant, device_info: EsphomeDeviceInfo, api_version: APIVersion -) -> None: - """Create or delete an the ble_firmware_outdated issue.""" - # ESPHome device_info.mac_address is the unique_id - issue = f"ble_firmware_outdated-{device_info.mac_address}" - if ( - not device_info.bluetooth_proxy_feature_flags_compat(api_version) - # If the device has a project name its up to that project - # to tell them about the firmware version update so we don't notify here - or (device_info.project_name and device_info.project_name not in PROJECT_URLS) - or AwesomeVersion(device_info.esphome_version) >= STABLE_BLE_VERSION - ): - async_delete_issue(hass, DOMAIN, issue) - return - async_create_issue( - hass, - DOMAIN, - issue, - is_fixable=False, - severity=IssueSeverity.WARNING, - learn_more_url=PROJECT_URLS.get(device_info.project_name, DEFAULT_URL), - translation_key="ble_firmware_outdated", - translation_placeholders={ - "name": device_info.name, - "version": STABLE_BLE_VERSION_STR, - }, - ) - - -@callback -def _async_check_using_api_password( - hass: HomeAssistant, device_info: EsphomeDeviceInfo, has_password: bool -) -> None: - """Create or delete an the api_password_deprecated issue.""" - # ESPHome device_info.mac_address is the unique_id - issue = f"api_password_deprecated-{device_info.mac_address}" - if not has_password: - async_delete_issue(hass, DOMAIN, issue) - return - async_create_issue( - hass, - DOMAIN, - issue, - is_fixable=False, - severity=IssueSeverity.WARNING, - learn_more_url="https://esphome.io/components/api.html", - translation_key="api_password_deprecated", - translation_placeholders={ - "name": device_info.name, - }, - ) - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the esphome component.""" await async_setup_dashboard(hass) return True -class ESPHomeManager: - """Class to manage an ESPHome connection.""" - - __slots__ = ( - "hass", - "host", - "password", - "entry", - "cli", - "device_id", - "domain_data", - "voice_assistant_udp_server", - "reconnect_logic", - "zeroconf_instance", - "entry_data", - ) - - def __init__( - self, - hass: HomeAssistant, - entry: ConfigEntry, - host: str, - password: str | None, - cli: APIClient, - zeroconf_instance: zeroconf.HaZeroconf, - domain_data: DomainData, - entry_data: RuntimeEntryData, - ) -> None: - """Initialize the esphome manager.""" - self.hass = hass - self.host = host - self.password = password - self.entry = entry - self.cli = cli - self.device_id: str | None = None - self.domain_data = domain_data - self.voice_assistant_udp_server: VoiceAssistantUDPServer | None = None - self.reconnect_logic: ReconnectLogic | None = None - self.zeroconf_instance = zeroconf_instance - self.entry_data = entry_data - - async def on_stop(self, event: Event) -> None: - """Cleanup the socket client on HA stop.""" - await _cleanup_instance(self.hass, self.entry) - - @property - def services_issue(self) -> str: - """Return the services issue name for this entry.""" - return f"service_calls_not_enabled-{self.entry.unique_id}" - - @callback - def async_on_service_call(self, service: HomeassistantServiceCall) -> None: - """Call service when user automation in ESPHome config is triggered.""" - hass = self.hass - domain, service_name = service.service.split(".", 1) - service_data = service.data - - if service.data_template: - try: - data_template = { - key: Template(value) for key, value in service.data_template.items() - } - template.attach(hass, data_template) - service_data.update( - template.render_complex(data_template, service.variables) - ) - except TemplateError as ex: - _LOGGER.error("Error rendering data template for %s: %s", self.host, ex) - return - - if service.is_event: - device_id = self.device_id - # ESPHome uses service call packet for both events and service calls - # Ensure the user can only send events of form 'esphome.xyz' - if domain != "esphome": - _LOGGER.error( - "Can only generate events under esphome domain! (%s)", self.host - ) - return - - # Call native tag scan - if service_name == "tag_scanned" and device_id is not None: - tag_id = service_data["tag_id"] - hass.async_create_task(tag.async_scan_tag(hass, tag_id, device_id)) - return - - hass.bus.async_fire( - service.service, - { - ATTR_DEVICE_ID: device_id, - **service_data, - }, - ) - elif self.entry.options.get( - CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS - ): - hass.async_create_task( - hass.services.async_call( - domain, service_name, service_data, blocking=True - ) - ) - else: - device_info = self.entry_data.device_info - assert device_info is not None - async_create_issue( - hass, - DOMAIN, - self.services_issue, - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="service_calls_not_allowed", - translation_placeholders={ - "name": device_info.friendly_name or device_info.name, - }, - ) - _LOGGER.error( - "%s: Service call %s.%s: with data %s rejected; " - "If you trust this device and want to allow access for it to make " - "Home Assistant service calls, you can enable this " - "functionality in the options flow", - device_info.friendly_name or device_info.name, - domain, - service_name, - service_data, - ) - - async def _send_home_assistant_state( - self, entity_id: str, attribute: str | None, state: State | None - ) -> None: - """Forward Home Assistant states to ESPHome.""" - if state is None or (attribute and attribute not in state.attributes): - return - - send_state = state.state - if attribute: - attr_val = state.attributes[attribute] - # ESPHome only handles "on"/"off" for boolean values - if isinstance(attr_val, bool): - send_state = "on" if attr_val else "off" - else: - send_state = attr_val - - await self.cli.send_home_assistant_state(entity_id, attribute, str(send_state)) - - @callback - def async_on_state_subscription( - self, entity_id: str, attribute: str | None = None - ) -> None: - """Subscribe and forward states for requested entities.""" - hass = self.hass - - async def send_home_assistant_state_event(event: Event) -> None: - """Forward Home Assistant states updates to ESPHome.""" - event_data = event.data - new_state: State | None = event_data.get("new_state") - old_state: State | None = event_data.get("old_state") - - if new_state is None or old_state is None: - return - - # Only communicate changes to the state or attribute tracked - if (not attribute and old_state.state == new_state.state) or ( - attribute - and old_state.attributes.get(attribute) - == new_state.attributes.get(attribute) - ): - return - - await self._send_home_assistant_state( - event.data["entity_id"], attribute, new_state - ) - - self.entry_data.disconnect_callbacks.append( - async_track_state_change_event( - hass, [entity_id], send_home_assistant_state_event - ) - ) - - # Send initial state - hass.async_create_task( - self._send_home_assistant_state( - entity_id, attribute, hass.states.get(entity_id) - ) - ) - - def _handle_pipeline_event( - self, event_type: VoiceAssistantEventType, data: dict[str, str] | None - ) -> None: - self.cli.send_voice_assistant_event(event_type, data) - - def _handle_pipeline_finished(self) -> None: - self.entry_data.async_set_assist_pipeline_state(False) - - if self.voice_assistant_udp_server is not None: - self.voice_assistant_udp_server.close() - self.voice_assistant_udp_server = None - - async def _handle_pipeline_start( - self, conversation_id: str, use_vad: bool - ) -> int | None: - """Start a voice assistant pipeline.""" - if self.voice_assistant_udp_server is not None: - return None - - hass = self.hass - voice_assistant_udp_server = VoiceAssistantUDPServer( - hass, - self.entry_data, - self._handle_pipeline_event, - self._handle_pipeline_finished, - ) - port = await voice_assistant_udp_server.start_server() - - assert self.device_id is not None, "Device ID must be set" - hass.async_create_background_task( - voice_assistant_udp_server.run_pipeline( - device_id=self.device_id, - conversation_id=conversation_id or None, - use_vad=use_vad, - ), - "esphome.voice_assistant_udp_server.run_pipeline", - ) - self.entry_data.async_set_assist_pipeline_state(True) - - return port - - async def _handle_pipeline_stop(self) -> None: - """Stop a voice assistant pipeline.""" - if self.voice_assistant_udp_server is not None: - self.voice_assistant_udp_server.stop() - - async def on_connect(self) -> None: - """Subscribe to states and list entities on successful API login.""" - entry = self.entry - entry_data = self.entry_data - reconnect_logic = self.reconnect_logic - hass = self.hass - cli = self.cli - try: - device_info = await cli.device_info() - - # Migrate config entry to new unique ID if necessary - # This was changed in 2023.1 - if entry.unique_id != format_mac(device_info.mac_address): - hass.config_entries.async_update_entry( - entry, unique_id=format_mac(device_info.mac_address) - ) - - # Make sure we have the correct device name stored - # so we can map the device to ESPHome Dashboard config - if entry.data.get(CONF_DEVICE_NAME) != device_info.name: - hass.config_entries.async_update_entry( - entry, data={**entry.data, CONF_DEVICE_NAME: device_info.name} - ) - - entry_data.device_info = device_info - assert cli.api_version is not None - entry_data.api_version = cli.api_version - entry_data.available = True - if entry_data.device_info.name: - assert reconnect_logic is not None, "Reconnect logic must be set" - reconnect_logic.name = entry_data.device_info.name - - if device_info.bluetooth_proxy_feature_flags_compat(cli.api_version): - entry_data.disconnect_callbacks.append( - await async_connect_scanner(hass, entry, cli, entry_data) - ) - - self.device_id = _async_setup_device_registry( - hass, entry, entry_data.device_info - ) - entry_data.async_update_device_state(hass) - - entity_infos, services = await cli.list_entities_services() - await entry_data.async_update_static_infos(hass, entry, entity_infos) - await _setup_services(hass, entry_data, services) - await cli.subscribe_states(entry_data.async_update_state) - await cli.subscribe_service_calls(self.async_on_service_call) - await cli.subscribe_home_assistant_states(self.async_on_state_subscription) - - if device_info.voice_assistant_version: - entry_data.disconnect_callbacks.append( - await cli.subscribe_voice_assistant( - self._handle_pipeline_start, - self._handle_pipeline_stop, - ) - ) - - hass.async_create_task(entry_data.async_save_to_store()) - except APIConnectionError as err: - _LOGGER.warning("Error getting initial data for %s: %s", self.host, err) - # Re-connection logic will trigger after this - await cli.disconnect() - else: - _async_check_firmware_version(hass, device_info, entry_data.api_version) - _async_check_using_api_password(hass, device_info, bool(self.password)) - - async def on_disconnect(self, expected_disconnect: bool) -> None: - """Run disconnect callbacks on API disconnect.""" - entry_data = self.entry_data - hass = self.hass - host = self.host - name = entry_data.device_info.name if entry_data.device_info else host - _LOGGER.debug( - "%s: %s disconnected (expected=%s), running disconnected callbacks", - name, - host, - expected_disconnect, - ) - for disconnect_cb in entry_data.disconnect_callbacks: - disconnect_cb() - entry_data.disconnect_callbacks = [] - entry_data.available = False - entry_data.expected_disconnect = expected_disconnect - # Mark state as stale so that we will always dispatch - # the next state update of that type when the device reconnects - entry_data.stale_state = { - (type(entity_state), key) - for state_dict in entry_data.state.values() - for key, entity_state in state_dict.items() - } - if not hass.is_stopping: - # Avoid marking every esphome entity as unavailable on shutdown - # since it generates a lot of state changed events and database - # writes when we already know we're shutting down and the state - # will be cleared anyway. - entry_data.async_update_device_state(hass) - - async def on_connect_error(self, err: Exception) -> None: - """Start reauth flow if appropriate connect error type.""" - if isinstance( - err, - ( - RequiresEncryptionAPIError, - InvalidEncryptionKeyAPIError, - InvalidAuthAPIError, - ), - ): - self.entry.async_start_reauth(self.hass) - - async def async_start(self) -> None: - """Start the esphome connection manager.""" - hass = self.hass - entry = self.entry - entry_data = self.entry_data - - if entry.options.get(CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS): - async_delete_issue(hass, DOMAIN, self.services_issue) - - # Use async_listen instead of async_listen_once so that we don't deregister - # the callback twice when shutting down Home Assistant. - # "Unable to remove unknown listener - # .onetime_listener>" - entry_data.cleanup_callbacks.append( - hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.on_stop) - ) - - reconnect_logic = ReconnectLogic( - client=self.cli, - on_connect=self.on_connect, - on_disconnect=self.on_disconnect, - zeroconf_instance=self.zeroconf_instance, - name=self.host, - on_connect_error=self.on_connect_error, - ) - self.reconnect_logic = reconnect_logic - - infos, services = await entry_data.async_load_from_store() - await entry_data.async_update_static_infos(hass, entry, infos) - await _setup_services(hass, entry_data, services) - - if entry_data.device_info is not None and entry_data.device_info.name: - reconnect_logic.name = entry_data.device_info.name - if entry.unique_id is None: - hass.config_entries.async_update_entry( - entry, unique_id=format_mac(entry_data.device_info.mac_address) - ) - - await reconnect_logic.start() - entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback) - - entry.async_on_unload( - entry.add_update_listener(entry_data.async_update_listener) - ) - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the esphome component.""" host = entry.data[CONF_HOST] @@ -558,202 +72,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -@callback -def _async_setup_device_registry( - hass: HomeAssistant, entry: ConfigEntry, device_info: EsphomeDeviceInfo -) -> str: - """Set up device registry feature for a particular config entry.""" - sw_version = device_info.esphome_version - if device_info.compilation_time: - sw_version += f" ({device_info.compilation_time})" - - configuration_url = None - if device_info.webserver_port > 0: - configuration_url = f"http://{entry.data['host']}:{device_info.webserver_port}" - elif dashboard := async_get_dashboard(hass): - configuration_url = f"homeassistant://hassio/ingress/{dashboard.addon_slug}" - - manufacturer = "espressif" - if device_info.manufacturer: - manufacturer = device_info.manufacturer - model = device_info.model - hw_version = None - if device_info.project_name: - project_name = device_info.project_name.split(".") - manufacturer = project_name[0] - model = project_name[1] - hw_version = device_info.project_version - - device_registry = dr.async_get(hass) - device_entry = device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - configuration_url=configuration_url, - connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}, - name=device_info.friendly_name or device_info.name, - manufacturer=manufacturer, - model=model, - sw_version=sw_version, - hw_version=hw_version, - ) - return device_entry.id - - -class ServiceMetadata(NamedTuple): - """Metadata for services.""" - - validator: Any - example: str - selector: dict[str, Any] - description: str | None = None - - -ARG_TYPE_METADATA = { - UserServiceArgType.BOOL: ServiceMetadata( - validator=cv.boolean, - example="False", - selector={"boolean": None}, - ), - UserServiceArgType.INT: ServiceMetadata( - validator=vol.Coerce(int), - example="42", - selector={"number": {CONF_MODE: "box"}}, - ), - UserServiceArgType.FLOAT: ServiceMetadata( - validator=vol.Coerce(float), - example="12.3", - selector={"number": {CONF_MODE: "box", "step": 1e-3}}, - ), - UserServiceArgType.STRING: ServiceMetadata( - validator=cv.string, - example="Example text", - selector={"text": None}, - ), - UserServiceArgType.BOOL_ARRAY: ServiceMetadata( - validator=[cv.boolean], - description="A list of boolean values.", - example="[True, False]", - selector={"object": {}}, - ), - UserServiceArgType.INT_ARRAY: ServiceMetadata( - validator=[vol.Coerce(int)], - description="A list of integer values.", - example="[42, 34]", - selector={"object": {}}, - ), - UserServiceArgType.FLOAT_ARRAY: ServiceMetadata( - validator=[vol.Coerce(float)], - description="A list of floating point numbers.", - example="[ 12.3, 34.5 ]", - selector={"object": {}}, - ), - UserServiceArgType.STRING_ARRAY: ServiceMetadata( - validator=[cv.string], - description="A list of strings.", - example="['Example text', 'Another example']", - selector={"object": {}}, - ), -} - - -async def _register_service( - hass: HomeAssistant, entry_data: RuntimeEntryData, service: UserService -) -> None: - if entry_data.device_info is None: - raise ValueError("Device Info needs to be fetched first") - service_name = f"{entry_data.device_info.name.replace('-', '_')}_{service.name}" - schema = {} - fields = {} - - for arg in service.args: - if arg.type not in ARG_TYPE_METADATA: - _LOGGER.error( - "Can't register service %s because %s is of unknown type %s", - service_name, - arg.name, - arg.type, - ) - return - metadata = ARG_TYPE_METADATA[arg.type] - schema[vol.Required(arg.name)] = metadata.validator - fields[arg.name] = { - "name": arg.name, - "required": True, - "description": metadata.description, - "example": metadata.example, - "selector": metadata.selector, - } - - async def execute_service(call: ServiceCall) -> None: - await entry_data.client.execute_service(service, call.data) - - hass.services.async_register( - DOMAIN, service_name, execute_service, vol.Schema(schema) - ) - - service_desc = { - "description": ( - f"Calls the service {service.name} of the node" - f" {entry_data.device_info.name}" - ), - "fields": fields, - } - - async_set_service_schema(hass, DOMAIN, service_name, service_desc) - - -async def _setup_services( - hass: HomeAssistant, entry_data: RuntimeEntryData, services: list[UserService] -) -> None: - if entry_data.device_info is None: - # Can happen if device has never connected or .storage cleared - return - old_services = entry_data.services.copy() - to_unregister = [] - to_register = [] - for service in services: - if service.key in old_services: - # Already exists - if (matching := old_services.pop(service.key)) != service: - # Need to re-register - to_unregister.append(matching) - to_register.append(service) - else: - # New service - to_register.append(service) - - for service in old_services.values(): - to_unregister.append(service) - - entry_data.services = {serv.key: serv for serv in services} - - for service in to_unregister: - service_name = f"{entry_data.device_info.name}_{service.name}" - hass.services.async_remove(DOMAIN, service_name) - - for service in to_register: - await _register_service(hass, entry_data, service) - - -async def _cleanup_instance( - hass: HomeAssistant, entry: ConfigEntry -) -> RuntimeEntryData: - """Cleanup the esphome client if it exists.""" - domain_data = DomainData.get(hass) - data = domain_data.pop_entry_data(entry) - data.available = False - for disconnect_cb in data.disconnect_callbacks: - disconnect_cb() - data.disconnect_callbacks = [] - for cleanup_callback in data.cleanup_callbacks: - cleanup_callback() - await data.async_cleanup() - await data.client.disconnect() - return data - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an esphome config entry.""" - entry_data = await _cleanup_instance(hass, entry) + entry_data = await cleanup_instance(hass, entry) return await hass.config_entries.async_unload_platforms( entry, entry_data.loaded_platforms ) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 731743e48c8eeb..ecd49718559b2b 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -27,9 +27,10 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac -from . import CONF_DEVICE_NAME, CONF_NOISE_PSK from .const import ( CONF_ALLOW_SERVICE_CALLS, + CONF_DEVICE_NAME, + CONF_NOISE_PSK, DEFAULT_ALLOW_SERVICE_CALLS, DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS, DOMAIN, diff --git a/homeassistant/components/esphome/const.py b/homeassistant/components/esphome/const.py index a53bb2db8edbba..f0e3972f197745 100644 --- a/homeassistant/components/esphome/const.py +++ b/homeassistant/components/esphome/const.py @@ -1,7 +1,19 @@ """ESPHome constants.""" +from awesomeversion import AwesomeVersion DOMAIN = "esphome" CONF_ALLOW_SERVICE_CALLS = "allow_service_calls" +CONF_DEVICE_NAME = "device_name" +CONF_NOISE_PSK = "noise_psk" + DEFAULT_ALLOW_SERVICE_CALLS = True DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False + + +STABLE_BLE_VERSION_STR = "2023.6.0" +STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR) +PROJECT_URLS = { + "esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/", +} +DEFAULT_URL = f"https://esphome.io/changelog/{STABLE_BLE_VERSION_STR}.html" diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py new file mode 100644 index 00000000000000..b87d3ac38992a5 --- /dev/null +++ b/homeassistant/components/esphome/manager.py @@ -0,0 +1,696 @@ +"""Manager for esphome devices.""" +from __future__ import annotations + +import logging +from typing import Any, NamedTuple + +from aioesphomeapi import ( + APIClient, + APIConnectionError, + APIVersion, + DeviceInfo as EsphomeDeviceInfo, + HomeassistantServiceCall, + InvalidAuthAPIError, + InvalidEncryptionKeyAPIError, + ReconnectLogic, + RequiresEncryptionAPIError, + UserService, + UserServiceArgType, + VoiceAssistantEventType, +) +from awesomeversion import AwesomeVersion +import voluptuous as vol + +from homeassistant.components import tag, zeroconf +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_DEVICE_ID, + CONF_MODE, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback +from homeassistant.exceptions import TemplateError +from homeassistant.helpers import template +import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) +from homeassistant.helpers.service import async_set_service_schema +from homeassistant.helpers.template import Template + +from .bluetooth import async_connect_scanner +from .const import ( + CONF_ALLOW_SERVICE_CALLS, + CONF_DEVICE_NAME, + DEFAULT_ALLOW_SERVICE_CALLS, + DEFAULT_URL, + DOMAIN, + PROJECT_URLS, + STABLE_BLE_VERSION, + STABLE_BLE_VERSION_STR, +) +from .dashboard import async_get_dashboard +from .domain_data import DomainData + +# Import config flow so that it's added to the registry +from .entry_data import RuntimeEntryData +from .voice_assistant import VoiceAssistantUDPServer + +_LOGGER = logging.getLogger(__name__) + + +@callback +def _async_check_firmware_version( + hass: HomeAssistant, device_info: EsphomeDeviceInfo, api_version: APIVersion +) -> None: + """Create or delete an the ble_firmware_outdated issue.""" + # ESPHome device_info.mac_address is the unique_id + issue = f"ble_firmware_outdated-{device_info.mac_address}" + if ( + not device_info.bluetooth_proxy_feature_flags_compat(api_version) + # If the device has a project name its up to that project + # to tell them about the firmware version update so we don't notify here + or (device_info.project_name and device_info.project_name not in PROJECT_URLS) + or AwesomeVersion(device_info.esphome_version) >= STABLE_BLE_VERSION + ): + async_delete_issue(hass, DOMAIN, issue) + return + async_create_issue( + hass, + DOMAIN, + issue, + is_fixable=False, + severity=IssueSeverity.WARNING, + learn_more_url=PROJECT_URLS.get(device_info.project_name, DEFAULT_URL), + translation_key="ble_firmware_outdated", + translation_placeholders={ + "name": device_info.name, + "version": STABLE_BLE_VERSION_STR, + }, + ) + + +@callback +def _async_check_using_api_password( + hass: HomeAssistant, device_info: EsphomeDeviceInfo, has_password: bool +) -> None: + """Create or delete an the api_password_deprecated issue.""" + # ESPHome device_info.mac_address is the unique_id + issue = f"api_password_deprecated-{device_info.mac_address}" + if not has_password: + async_delete_issue(hass, DOMAIN, issue) + return + async_create_issue( + hass, + DOMAIN, + issue, + is_fixable=False, + severity=IssueSeverity.WARNING, + learn_more_url="https://esphome.io/components/api.html", + translation_key="api_password_deprecated", + translation_placeholders={ + "name": device_info.name, + }, + ) + + +class ESPHomeManager: + """Class to manage an ESPHome connection.""" + + __slots__ = ( + "hass", + "host", + "password", + "entry", + "cli", + "device_id", + "domain_data", + "voice_assistant_udp_server", + "reconnect_logic", + "zeroconf_instance", + "entry_data", + ) + + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + host: str, + password: str | None, + cli: APIClient, + zeroconf_instance: zeroconf.HaZeroconf, + domain_data: DomainData, + entry_data: RuntimeEntryData, + ) -> None: + """Initialize the esphome manager.""" + self.hass = hass + self.host = host + self.password = password + self.entry = entry + self.cli = cli + self.device_id: str | None = None + self.domain_data = domain_data + self.voice_assistant_udp_server: VoiceAssistantUDPServer | None = None + self.reconnect_logic: ReconnectLogic | None = None + self.zeroconf_instance = zeroconf_instance + self.entry_data = entry_data + + async def on_stop(self, event: Event) -> None: + """Cleanup the socket client on HA stop.""" + await cleanup_instance(self.hass, self.entry) + + @property + def services_issue(self) -> str: + """Return the services issue name for this entry.""" + return f"service_calls_not_enabled-{self.entry.unique_id}" + + @callback + def async_on_service_call(self, service: HomeassistantServiceCall) -> None: + """Call service when user automation in ESPHome config is triggered.""" + hass = self.hass + domain, service_name = service.service.split(".", 1) + service_data = service.data + + if service.data_template: + try: + data_template = { + key: Template(value) for key, value in service.data_template.items() + } + template.attach(hass, data_template) + service_data.update( + template.render_complex(data_template, service.variables) + ) + except TemplateError as ex: + _LOGGER.error("Error rendering data template for %s: %s", self.host, ex) + return + + if service.is_event: + device_id = self.device_id + # ESPHome uses service call packet for both events and service calls + # Ensure the user can only send events of form 'esphome.xyz' + if domain != "esphome": + _LOGGER.error( + "Can only generate events under esphome domain! (%s)", self.host + ) + return + + # Call native tag scan + if service_name == "tag_scanned" and device_id is not None: + tag_id = service_data["tag_id"] + hass.async_create_task(tag.async_scan_tag(hass, tag_id, device_id)) + return + + hass.bus.async_fire( + service.service, + { + ATTR_DEVICE_ID: device_id, + **service_data, + }, + ) + elif self.entry.options.get( + CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS + ): + hass.async_create_task( + hass.services.async_call( + domain, service_name, service_data, blocking=True + ) + ) + else: + device_info = self.entry_data.device_info + assert device_info is not None + async_create_issue( + hass, + DOMAIN, + self.services_issue, + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="service_calls_not_allowed", + translation_placeholders={ + "name": device_info.friendly_name or device_info.name, + }, + ) + _LOGGER.error( + "%s: Service call %s.%s: with data %s rejected; " + "If you trust this device and want to allow access for it to make " + "Home Assistant service calls, you can enable this " + "functionality in the options flow", + device_info.friendly_name or device_info.name, + domain, + service_name, + service_data, + ) + + async def _send_home_assistant_state( + self, entity_id: str, attribute: str | None, state: State | None + ) -> None: + """Forward Home Assistant states to ESPHome.""" + if state is None or (attribute and attribute not in state.attributes): + return + + send_state = state.state + if attribute: + attr_val = state.attributes[attribute] + # ESPHome only handles "on"/"off" for boolean values + if isinstance(attr_val, bool): + send_state = "on" if attr_val else "off" + else: + send_state = attr_val + + await self.cli.send_home_assistant_state(entity_id, attribute, str(send_state)) + + @callback + def async_on_state_subscription( + self, entity_id: str, attribute: str | None = None + ) -> None: + """Subscribe and forward states for requested entities.""" + hass = self.hass + + async def send_home_assistant_state_event(event: Event) -> None: + """Forward Home Assistant states updates to ESPHome.""" + event_data = event.data + new_state: State | None = event_data.get("new_state") + old_state: State | None = event_data.get("old_state") + + if new_state is None or old_state is None: + return + + # Only communicate changes to the state or attribute tracked + if (not attribute and old_state.state == new_state.state) or ( + attribute + and old_state.attributes.get(attribute) + == new_state.attributes.get(attribute) + ): + return + + await self._send_home_assistant_state( + event.data["entity_id"], attribute, new_state + ) + + self.entry_data.disconnect_callbacks.append( + async_track_state_change_event( + hass, [entity_id], send_home_assistant_state_event + ) + ) + + # Send initial state + hass.async_create_task( + self._send_home_assistant_state( + entity_id, attribute, hass.states.get(entity_id) + ) + ) + + def _handle_pipeline_event( + self, event_type: VoiceAssistantEventType, data: dict[str, str] | None + ) -> None: + self.cli.send_voice_assistant_event(event_type, data) + + def _handle_pipeline_finished(self) -> None: + self.entry_data.async_set_assist_pipeline_state(False) + + if self.voice_assistant_udp_server is not None: + self.voice_assistant_udp_server.close() + self.voice_assistant_udp_server = None + + async def _handle_pipeline_start( + self, conversation_id: str, use_vad: bool + ) -> int | None: + """Start a voice assistant pipeline.""" + if self.voice_assistant_udp_server is not None: + return None + + hass = self.hass + voice_assistant_udp_server = VoiceAssistantUDPServer( + hass, + self.entry_data, + self._handle_pipeline_event, + self._handle_pipeline_finished, + ) + port = await voice_assistant_udp_server.start_server() + + assert self.device_id is not None, "Device ID must be set" + hass.async_create_background_task( + voice_assistant_udp_server.run_pipeline( + device_id=self.device_id, + conversation_id=conversation_id or None, + use_vad=use_vad, + ), + "esphome.voice_assistant_udp_server.run_pipeline", + ) + self.entry_data.async_set_assist_pipeline_state(True) + + return port + + async def _handle_pipeline_stop(self) -> None: + """Stop a voice assistant pipeline.""" + if self.voice_assistant_udp_server is not None: + self.voice_assistant_udp_server.stop() + + async def on_connect(self) -> None: + """Subscribe to states and list entities on successful API login.""" + entry = self.entry + entry_data = self.entry_data + reconnect_logic = self.reconnect_logic + hass = self.hass + cli = self.cli + try: + device_info = await cli.device_info() + + # Migrate config entry to new unique ID if necessary + # This was changed in 2023.1 + if entry.unique_id != format_mac(device_info.mac_address): + hass.config_entries.async_update_entry( + entry, unique_id=format_mac(device_info.mac_address) + ) + + # Make sure we have the correct device name stored + # so we can map the device to ESPHome Dashboard config + if entry.data.get(CONF_DEVICE_NAME) != device_info.name: + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_DEVICE_NAME: device_info.name} + ) + + entry_data.device_info = device_info + assert cli.api_version is not None + entry_data.api_version = cli.api_version + entry_data.available = True + if entry_data.device_info.name: + assert reconnect_logic is not None, "Reconnect logic must be set" + reconnect_logic.name = entry_data.device_info.name + + if device_info.bluetooth_proxy_feature_flags_compat(cli.api_version): + entry_data.disconnect_callbacks.append( + await async_connect_scanner(hass, entry, cli, entry_data) + ) + + self.device_id = _async_setup_device_registry( + hass, entry, entry_data.device_info + ) + entry_data.async_update_device_state(hass) + + entity_infos, services = await cli.list_entities_services() + await entry_data.async_update_static_infos(hass, entry, entity_infos) + await _setup_services(hass, entry_data, services) + await cli.subscribe_states(entry_data.async_update_state) + await cli.subscribe_service_calls(self.async_on_service_call) + await cli.subscribe_home_assistant_states(self.async_on_state_subscription) + + if device_info.voice_assistant_version: + entry_data.disconnect_callbacks.append( + await cli.subscribe_voice_assistant( + self._handle_pipeline_start, + self._handle_pipeline_stop, + ) + ) + + hass.async_create_task(entry_data.async_save_to_store()) + except APIConnectionError as err: + _LOGGER.warning("Error getting initial data for %s: %s", self.host, err) + # Re-connection logic will trigger after this + await cli.disconnect() + else: + _async_check_firmware_version(hass, device_info, entry_data.api_version) + _async_check_using_api_password(hass, device_info, bool(self.password)) + + async def on_disconnect(self, expected_disconnect: bool) -> None: + """Run disconnect callbacks on API disconnect.""" + entry_data = self.entry_data + hass = self.hass + host = self.host + name = entry_data.device_info.name if entry_data.device_info else host + _LOGGER.debug( + "%s: %s disconnected (expected=%s), running disconnected callbacks", + name, + host, + expected_disconnect, + ) + for disconnect_cb in entry_data.disconnect_callbacks: + disconnect_cb() + entry_data.disconnect_callbacks = [] + entry_data.available = False + entry_data.expected_disconnect = expected_disconnect + # Mark state as stale so that we will always dispatch + # the next state update of that type when the device reconnects + entry_data.stale_state = { + (type(entity_state), key) + for state_dict in entry_data.state.values() + for key, entity_state in state_dict.items() + } + if not hass.is_stopping: + # Avoid marking every esphome entity as unavailable on shutdown + # since it generates a lot of state changed events and database + # writes when we already know we're shutting down and the state + # will be cleared anyway. + entry_data.async_update_device_state(hass) + + async def on_connect_error(self, err: Exception) -> None: + """Start reauth flow if appropriate connect error type.""" + if isinstance( + err, + ( + RequiresEncryptionAPIError, + InvalidEncryptionKeyAPIError, + InvalidAuthAPIError, + ), + ): + self.entry.async_start_reauth(self.hass) + + async def async_start(self) -> None: + """Start the esphome connection manager.""" + hass = self.hass + entry = self.entry + entry_data = self.entry_data + + if entry.options.get(CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS): + async_delete_issue(hass, DOMAIN, self.services_issue) + + # Use async_listen instead of async_listen_once so that we don't deregister + # the callback twice when shutting down Home Assistant. + # "Unable to remove unknown listener + # .onetime_listener>" + entry_data.cleanup_callbacks.append( + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.on_stop) + ) + + reconnect_logic = ReconnectLogic( + client=self.cli, + on_connect=self.on_connect, + on_disconnect=self.on_disconnect, + zeroconf_instance=self.zeroconf_instance, + name=self.host, + on_connect_error=self.on_connect_error, + ) + self.reconnect_logic = reconnect_logic + + infos, services = await entry_data.async_load_from_store() + await entry_data.async_update_static_infos(hass, entry, infos) + await _setup_services(hass, entry_data, services) + + if entry_data.device_info is not None and entry_data.device_info.name: + reconnect_logic.name = entry_data.device_info.name + if entry.unique_id is None: + hass.config_entries.async_update_entry( + entry, unique_id=format_mac(entry_data.device_info.mac_address) + ) + + await reconnect_logic.start() + entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback) + + entry.async_on_unload( + entry.add_update_listener(entry_data.async_update_listener) + ) + + +@callback +def _async_setup_device_registry( + hass: HomeAssistant, entry: ConfigEntry, device_info: EsphomeDeviceInfo +) -> str: + """Set up device registry feature for a particular config entry.""" + sw_version = device_info.esphome_version + if device_info.compilation_time: + sw_version += f" ({device_info.compilation_time})" + + configuration_url = None + if device_info.webserver_port > 0: + configuration_url = f"http://{entry.data['host']}:{device_info.webserver_port}" + elif dashboard := async_get_dashboard(hass): + configuration_url = f"homeassistant://hassio/ingress/{dashboard.addon_slug}" + + manufacturer = "espressif" + if device_info.manufacturer: + manufacturer = device_info.manufacturer + model = device_info.model + hw_version = None + if device_info.project_name: + project_name = device_info.project_name.split(".") + manufacturer = project_name[0] + model = project_name[1] + hw_version = device_info.project_version + + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + configuration_url=configuration_url, + connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}, + name=device_info.friendly_name or device_info.name, + manufacturer=manufacturer, + model=model, + sw_version=sw_version, + hw_version=hw_version, + ) + return device_entry.id + + +class ServiceMetadata(NamedTuple): + """Metadata for services.""" + + validator: Any + example: str + selector: dict[str, Any] + description: str | None = None + + +ARG_TYPE_METADATA = { + UserServiceArgType.BOOL: ServiceMetadata( + validator=cv.boolean, + example="False", + selector={"boolean": None}, + ), + UserServiceArgType.INT: ServiceMetadata( + validator=vol.Coerce(int), + example="42", + selector={"number": {CONF_MODE: "box"}}, + ), + UserServiceArgType.FLOAT: ServiceMetadata( + validator=vol.Coerce(float), + example="12.3", + selector={"number": {CONF_MODE: "box", "step": 1e-3}}, + ), + UserServiceArgType.STRING: ServiceMetadata( + validator=cv.string, + example="Example text", + selector={"text": None}, + ), + UserServiceArgType.BOOL_ARRAY: ServiceMetadata( + validator=[cv.boolean], + description="A list of boolean values.", + example="[True, False]", + selector={"object": {}}, + ), + UserServiceArgType.INT_ARRAY: ServiceMetadata( + validator=[vol.Coerce(int)], + description="A list of integer values.", + example="[42, 34]", + selector={"object": {}}, + ), + UserServiceArgType.FLOAT_ARRAY: ServiceMetadata( + validator=[vol.Coerce(float)], + description="A list of floating point numbers.", + example="[ 12.3, 34.5 ]", + selector={"object": {}}, + ), + UserServiceArgType.STRING_ARRAY: ServiceMetadata( + validator=[cv.string], + description="A list of strings.", + example="['Example text', 'Another example']", + selector={"object": {}}, + ), +} + + +async def _register_service( + hass: HomeAssistant, entry_data: RuntimeEntryData, service: UserService +) -> None: + if entry_data.device_info is None: + raise ValueError("Device Info needs to be fetched first") + service_name = f"{entry_data.device_info.name.replace('-', '_')}_{service.name}" + schema = {} + fields = {} + + for arg in service.args: + if arg.type not in ARG_TYPE_METADATA: + _LOGGER.error( + "Can't register service %s because %s is of unknown type %s", + service_name, + arg.name, + arg.type, + ) + return + metadata = ARG_TYPE_METADATA[arg.type] + schema[vol.Required(arg.name)] = metadata.validator + fields[arg.name] = { + "name": arg.name, + "required": True, + "description": metadata.description, + "example": metadata.example, + "selector": metadata.selector, + } + + async def execute_service(call: ServiceCall) -> None: + await entry_data.client.execute_service(service, call.data) + + hass.services.async_register( + DOMAIN, service_name, execute_service, vol.Schema(schema) + ) + + service_desc = { + "description": ( + f"Calls the service {service.name} of the node" + f" {entry_data.device_info.name}" + ), + "fields": fields, + } + + async_set_service_schema(hass, DOMAIN, service_name, service_desc) + + +async def _setup_services( + hass: HomeAssistant, entry_data: RuntimeEntryData, services: list[UserService] +) -> None: + if entry_data.device_info is None: + # Can happen if device has never connected or .storage cleared + return + old_services = entry_data.services.copy() + to_unregister = [] + to_register = [] + for service in services: + if service.key in old_services: + # Already exists + if (matching := old_services.pop(service.key)) != service: + # Need to re-register + to_unregister.append(matching) + to_register.append(service) + else: + # New service + to_register.append(service) + + for service in old_services.values(): + to_unregister.append(service) + + entry_data.services = {serv.key: serv for serv in services} + + for service in to_unregister: + service_name = f"{entry_data.device_info.name}_{service.name}" + hass.services.async_remove(DOMAIN, service_name) + + for service in to_register: + await _register_service(hass, entry_data, service) + + +async def cleanup_instance(hass: HomeAssistant, entry: ConfigEntry) -> RuntimeEntryData: + """Cleanup the esphome client if it exists.""" + domain_data = DomainData.get(hass) + data = domain_data.pop_entry_data(entry) + data.available = False + for disconnect_cb in data.disconnect_callbacks: + disconnect_cb() + data.disconnect_callbacks = [] + for cleanup_callback in data.cleanup_callbacks: + cleanup_callback() + await data.async_cleanup() + await data.client.disconnect() + return data diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index ffd87691b38256..1dcdc559de7b61 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -19,14 +19,14 @@ from zeroconf import Zeroconf from homeassistant.components.esphome import ( - CONF_DEVICE_NAME, - CONF_NOISE_PSK, - DOMAIN, dashboard, ) from homeassistant.components.esphome.const import ( CONF_ALLOW_SERVICE_CALLS, + CONF_DEVICE_NAME, + CONF_NOISE_PSK, DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS, + DOMAIN, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant @@ -234,7 +234,9 @@ async def mock_try_connect(self): try_connect_done.set() return result - with patch("homeassistant.components.esphome.ReconnectLogic", MockReconnectLogic): + with patch( + "homeassistant.components.esphome.manager.ReconnectLogic", MockReconnectLogic + ): assert await hass.config_entries.async_setup(entry.entry_id) await try_connect_done.wait() diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 662816a53d8d26..f5b7795a57bd9d 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -18,15 +18,15 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp, zeroconf from homeassistant.components.esphome import ( - CONF_DEVICE_NAME, - CONF_NOISE_PSK, - DOMAIN, DomainData, dashboard, ) from homeassistant.components.esphome.const import ( CONF_ALLOW_SERVICE_CALLS, + CONF_DEVICE_NAME, + CONF_NOISE_PSK, DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS, + DOMAIN, ) from homeassistant.components.hassio import HassioServiceInfo from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT diff --git a/tests/components/esphome/test_diagnostics.py b/tests/components/esphome/test_diagnostics.py index c1df7c024cd19c..a77fd9b0087597 100644 --- a/tests/components/esphome/test_diagnostics.py +++ b/tests/components/esphome/test_diagnostics.py @@ -1,7 +1,10 @@ """Tests for the diagnostics data provided by the ESPHome integration.""" -from homeassistant.components.esphome import CONF_DEVICE_NAME, CONF_NOISE_PSK +from homeassistant.components.esphome.const import ( + CONF_DEVICE_NAME, + CONF_NOISE_PSK, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant diff --git a/tests/components/esphome/test_init.py b/tests/components/esphome/test_init.py index 84bafc9fd84512..d3d47a40d66fab 100644 --- a/tests/components/esphome/test_init.py +++ b/tests/components/esphome/test_init.py @@ -31,3 +31,19 @@ async def test_unique_id_updated_to_mac( await hass.async_block_till_done() assert entry.unique_id == "11:22:33:44:55:aa" + + +async def test_delete_entry( + hass: HomeAssistant, mock_client, mock_zeroconf: None +) -> None: + """Test we can delete an entry with error.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "test.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, + unique_id="mock-config-name", + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index dd0daf1c455689..53ae72e375e064 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -15,7 +15,7 @@ @pytest.fixture(autouse=True) def stub_reconnect(): """Stub reconnect.""" - with patch("homeassistant.components.esphome.ReconnectLogic.start"): + with patch("homeassistant.components.esphome.manager.ReconnectLogic.start"): yield diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index 08750d06dd0dd7..322e057ec15ca0 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -8,7 +8,6 @@ import async_timeout import pytest -from homeassistant.components import esphome from homeassistant.components.assist_pipeline import PipelineEvent, PipelineEventType from homeassistant.components.esphome import DomainData from homeassistant.components.esphome.voice_assistant import VoiceAssistantUDPServer @@ -103,15 +102,15 @@ async def async_pipeline_from_audio_stream(*args, device_id, **kwargs): ) def handle_event( - event_type: esphome.VoiceAssistantEventType, data: dict[str, str] | None + event_type: VoiceAssistantEventType, data: dict[str, str] | None ) -> None: - if event_type == esphome.VoiceAssistantEventType.VOICE_ASSISTANT_STT_END: + if event_type == VoiceAssistantEventType.VOICE_ASSISTANT_STT_END: assert data is not None assert data["text"] == _TEST_INPUT_TEXT - elif event_type == esphome.VoiceAssistantEventType.VOICE_ASSISTANT_TTS_START: + elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_TTS_START: assert data is not None assert data["text"] == _TEST_OUTPUT_TEXT - elif event_type == esphome.VoiceAssistantEventType.VOICE_ASSISTANT_TTS_END: + elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_TTS_END: assert data is not None assert data["url"] == _TEST_OUTPUT_URL @@ -399,9 +398,9 @@ def is_speech(self, chunk, sample_rate): return sum(chunk) > 0 def handle_event( - event_type: esphome.VoiceAssistantEventType, data: dict[str, str] | None + event_type: VoiceAssistantEventType, data: dict[str, str] | None ) -> None: - assert event_type == esphome.VoiceAssistantEventType.VOICE_ASSISTANT_ERROR + assert event_type == VoiceAssistantEventType.VOICE_ASSISTANT_ERROR assert data is not None assert data["code"] == "speech-timeout" From 51344d566e09372e967804f43ceed7f48e33cce6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Jul 2023 21:23:45 -1000 Subject: [PATCH 0247/1009] Small speed up to cameras (#96124) --- homeassistant/components/camera/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index b22e2996f7e42c..277aa10075e231 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -514,8 +514,8 @@ def frontend_stream_type(self) -> StreamType | None: @property def available(self) -> bool: """Return True if entity is available.""" - if self.stream and not self.stream.available: - return self.stream.available + if (stream := self.stream) and not stream.available: + return False return super().available async def async_create_stream(self) -> Stream | None: From abdbea85227a94ddfb98c69672fe5149a1715a1f Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Sat, 8 Jul 2023 00:26:19 -0700 Subject: [PATCH 0248/1009] 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 3a562296a50307..bb19d2e1655014 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 f57bf13ac7dd8f..cc0922a033781a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2216,7 +2216,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 9a2602d6b7eb2c..e6a3baf8136430 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1624,7 +1624,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 f4ad261f511f6fc8eee9f9ddb1ad480c273e52d4 Mon Sep 17 00:00:00 2001 From: Scott Giminiani Date: Sat, 8 Jul 2023 04:46:34 -0400 Subject: [PATCH 0249/1009] Use global CONF_API_TOKEN constant rather than defining our own (#96120) --- homeassistant/components/amberelectric/__init__.py | 3 ++- tests/components/amberelectric/test_binary_sensor.py | 2 +- tests/components/amberelectric/test_sensor.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/amberelectric/__init__.py b/homeassistant/components/amberelectric/__init__.py index b6901e1b81b31a..9d9eef49b36901 100644 --- a/homeassistant/components/amberelectric/__init__.py +++ b/homeassistant/components/amberelectric/__init__.py @@ -4,9 +4,10 @@ from amberelectric.api import amber_api from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_TOKEN from homeassistant.core import HomeAssistant -from .const import CONF_API_TOKEN, CONF_SITE_ID, DOMAIN, PLATFORMS +from .const import CONF_SITE_ID, DOMAIN, PLATFORMS from .coordinator import AmberUpdateCoordinator diff --git a/tests/components/amberelectric/test_binary_sensor.py b/tests/components/amberelectric/test_binary_sensor.py index 32cec180dbcd8d..fb95cd1c41ef13 100644 --- a/tests/components/amberelectric/test_binary_sensor.py +++ b/tests/components/amberelectric/test_binary_sensor.py @@ -11,11 +11,11 @@ import pytest from homeassistant.components.amberelectric.const import ( - CONF_API_TOKEN, CONF_SITE_ID, CONF_SITE_NAME, DOMAIN, ) +from homeassistant.const import CONF_API_TOKEN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/amberelectric/test_sensor.py b/tests/components/amberelectric/test_sensor.py index 7a35b2c1c7ec23..286345dba10d56 100644 --- a/tests/components/amberelectric/test_sensor.py +++ b/tests/components/amberelectric/test_sensor.py @@ -7,11 +7,11 @@ import pytest from homeassistant.components.amberelectric.const import ( - CONF_API_TOKEN, CONF_SITE_ID, CONF_SITE_NAME, DOMAIN, ) +from homeassistant.const import CONF_API_TOKEN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component From bbf97fdf01d6b4bcf26b5808d1006f043671dd4a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 8 Jul 2023 10:48:14 +0200 Subject: [PATCH 0250/1009] Add entity translations for plugwise (#95808) --- homeassistant/components/plugwise/climate.py | 2 +- homeassistant/components/plugwise/sensor.py | 96 ++++++------ .../components/plugwise/strings.json | 140 ++++++++++++++++++ 3 files changed, 187 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 36626c2324e46a..d0a65799807465 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -39,7 +39,7 @@ async def async_setup_entry( class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): - """Representation of an Plugwise thermostat.""" + """Representation of a Plugwise thermostat.""" _attr_has_entity_name = True _attr_name = None diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 7a504a0db84b08..d18226e5af9baa 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -30,7 +30,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="setpoint", - name="Setpoint", + translation_key="setpoint", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -38,7 +38,7 @@ ), SensorEntityDescription( key="setpoint_high", - name="Cooling setpoint", + translation_key="cooling_setpoint", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -46,7 +46,7 @@ ), SensorEntityDescription( key="setpoint_low", - name="Heating setpoint", + translation_key="heating_setpoint", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -54,7 +54,6 @@ ), SensorEntityDescription( key="temperature", - name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, @@ -62,7 +61,7 @@ ), SensorEntityDescription( key="intended_boiler_temperature", - name="Intended boiler temperature", + translation_key="intended_boiler_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, @@ -70,7 +69,7 @@ ), SensorEntityDescription( key="temperature_difference", - name="Temperature difference", + translation_key="temperature_difference", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, @@ -78,14 +77,14 @@ ), SensorEntityDescription( key="outdoor_temperature", - name="Outdoor temperature", + translation_key="outdoor_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="outdoor_air_temperature", - name="Outdoor air temperature", + translation_key="outdoor_air_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, @@ -93,7 +92,7 @@ ), SensorEntityDescription( key="water_temperature", - name="Water temperature", + translation_key="water_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, @@ -101,7 +100,7 @@ ), SensorEntityDescription( key="return_temperature", - name="Return temperature", + translation_key="return_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, @@ -109,14 +108,14 @@ ), SensorEntityDescription( key="electricity_consumed", - name="Electricity consumed", + translation_key="electricity_consumed", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_produced", - name="Electricity produced", + translation_key="electricity_produced", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -124,28 +123,28 @@ ), SensorEntityDescription( key="electricity_consumed_interval", - name="Electricity consumed interval", + translation_key="electricity_consumed_interval", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_consumed_peak_interval", - name="Electricity consumed peak interval", + translation_key="electricity_consumed_peak_interval", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_consumed_off_peak_interval", - name="Electricity consumed off peak interval", + translation_key="electricity_consumed_off_peak_interval", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_produced_interval", - name="Electricity produced interval", + translation_key="electricity_produced_interval", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, @@ -153,133 +152,133 @@ ), SensorEntityDescription( key="electricity_produced_peak_interval", - name="Electricity produced peak interval", + translation_key="electricity_produced_peak_interval", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_produced_off_peak_interval", - name="Electricity produced off peak interval", + translation_key="electricity_produced_off_peak_interval", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_consumed_point", - name="Electricity consumed point", + translation_key="electricity_consumed_point", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_consumed_off_peak_point", - name="Electricity consumed off peak point", + translation_key="electricity_consumed_off_peak_point", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_consumed_peak_point", - name="Electricity consumed peak point", + translation_key="electricity_consumed_peak_point", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_consumed_off_peak_cumulative", - name="Electricity consumed off peak cumulative", + translation_key="electricity_consumed_off_peak_cumulative", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="electricity_consumed_peak_cumulative", - name="Electricity consumed peak cumulative", + translation_key="electricity_consumed_peak_cumulative", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="electricity_produced_point", - name="Electricity produced point", + translation_key="electricity_produced_point", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_produced_off_peak_point", - name="Electricity produced off peak point", + translation_key="electricity_produced_off_peak_point", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_produced_peak_point", - name="Electricity produced peak point", + translation_key="electricity_produced_peak_point", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_produced_off_peak_cumulative", - name="Electricity produced off peak cumulative", + translation_key="electricity_produced_off_peak_cumulative", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="electricity_produced_peak_cumulative", - name="Electricity produced peak cumulative", + translation_key="electricity_produced_peak_cumulative", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="electricity_phase_one_consumed", - name="Electricity phase one consumed", + translation_key="electricity_phase_one_consumed", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_phase_two_consumed", - name="Electricity phase two consumed", + translation_key="electricity_phase_two_consumed", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_phase_three_consumed", - name="Electricity phase three consumed", + translation_key="electricity_phase_three_consumed", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_phase_one_produced", - name="Electricity phase one produced", + translation_key="electricity_phase_one_produced", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_phase_two_produced", - name="Electricity phase two produced", + translation_key="electricity_phase_two_produced", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_phase_three_produced", - name="Electricity phase three produced", + translation_key="electricity_phase_three_produced", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="voltage_phase_one", - name="Voltage phase one", + translation_key="voltage_phase_one", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, @@ -287,7 +286,7 @@ ), SensorEntityDescription( key="voltage_phase_two", - name="Voltage phase two", + translation_key="voltage_phase_two", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, @@ -295,7 +294,7 @@ ), SensorEntityDescription( key="voltage_phase_three", - name="Voltage phase three", + translation_key="voltage_phase_three", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, @@ -303,35 +302,34 @@ ), SensorEntityDescription( key="gas_consumed_interval", - name="Gas consumed interval", + translation_key="gas_consumed_interval", icon="mdi:meter-gas", native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="gas_consumed_cumulative", - name="Gas consumed cumulative", + translation_key="gas_consumed_cumulative", native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="net_electricity_point", - name="Net electricity point", + translation_key="net_electricity_point", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="net_electricity_cumulative", - name="Net electricity cumulative", + translation_key="net_electricity_cumulative", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="battery", - name="Battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, @@ -339,7 +337,6 @@ ), SensorEntityDescription( key="illuminance", - name="Illuminance", native_unit_of_measurement=LIGHT_LUX, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, @@ -347,7 +344,7 @@ ), SensorEntityDescription( key="modulation_level", - name="Modulation level", + translation_key="modulation_level", icon="mdi:percent", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, @@ -355,7 +352,7 @@ ), SensorEntityDescription( key="valve_position", - name="Valve position", + translation_key="valve_position", icon="mdi:valve", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, @@ -363,7 +360,7 @@ ), SensorEntityDescription( key="water_pressure", - name="Water pressure", + translation_key="water_pressure", native_unit_of_measurement=UnitOfPressure.BAR, device_class=SensorDeviceClass.PRESSURE, entity_category=EntityCategory.DIAGNOSTIC, @@ -371,14 +368,13 @@ ), SensorEntityDescription( key="humidity", - name="Relative humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="dhw_temperature", - name="DHW temperature", + translation_key="dhw_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, @@ -386,7 +382,7 @@ ), SensorEntityDescription( key="domestic_hot_water_setpoint", - name="DHW setpoint", + translation_key="domestic_hot_water_setpoint", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, @@ -394,7 +390,7 @@ ), SensorEntityDescription( key="maximum_boiler_temperature", - name="Maximum boiler temperature", + translation_key="maximum_boiler_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index afc921f1101e72..e1b5b5c40535d8 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -102,6 +102,146 @@ "name": "Thermostat schedule" } }, + "sensor": { + "setpoint": { + "name": "Setpoint" + }, + "cooling_setpoint": { + "name": "Cooling setpoint" + }, + "heating_setpoint": { + "name": "Heating setpoint" + }, + "intended_boiler_temperature": { + "name": "Intended boiler temperature" + }, + "temperature_difference": { + "name": "Temperature difference" + }, + "outdoor_temperature": { + "name": "Outdoor temperature" + }, + "outdoor_air_temperature": { + "name": "Outdoor air temperature" + }, + "water_temperature": { + "name": "Water temperature" + }, + "return_temperature": { + "name": "Return temperature" + }, + "electricity_consumed": { + "name": "Electricity consumed" + }, + "electricity_produced": { + "name": "Electricity produced" + }, + "electricity_consumed_interval": { + "name": "Electricity consumed interval" + }, + "electricity_consumed_peak_interval": { + "name": "Electricity consumed peak interval" + }, + "electricity_consumed_off_peak_interval": { + "name": "Electricity consumed off peak interval" + }, + "electricity_produced_interval": { + "name": "Electricity produced interval" + }, + "electricity_produced_peak_interval": { + "name": "Electricity produced peak interval" + }, + "electricity_produced_off_peak_interval": { + "name": "Electricity produced off peak interval" + }, + "electricity_consumed_point": { + "name": "Electricity consumed point" + }, + "electricity_consumed_off_peak_point": { + "name": "Electricity consumed off peak point" + }, + "electricity_consumed_peak_point": { + "name": "Electricity consumed peak point" + }, + "electricity_consumed_off_peak_cumulative": { + "name": "Electricity consumed off peak cumulative" + }, + "electricity_consumed_peak_cumulative": { + "name": "Electricity consumed peak cumulative" + }, + "electricity_produced_point": { + "name": "Electricity produced point" + }, + "electricity_produced_off_peak_point": { + "name": "Electricity produced off peak point" + }, + "electricity_produced_peak_point": { + "name": "Electricity produced peak point" + }, + "electricity_produced_off_peak_cumulative": { + "name": "Electricity produced off peak cumulative" + }, + "electricity_produced_peak_cumulative": { + "name": "Electricity produced peak cumulative" + }, + "electricity_phase_one_consumed": { + "name": "Electricity phase one consumed" + }, + "electricity_phase_two_consumed": { + "name": "Electricity phase two consumed" + }, + "electricity_phase_three_consumed": { + "name": "Electricity phase three consumed" + }, + "electricity_phase_one_produced": { + "name": "Electricity phase one produced" + }, + "electricity_phase_two_produced": { + "name": "Electricity phase two produced" + }, + "electricity_phase_three_produced": { + "name": "Electricity phase three produced" + }, + "voltage_phase_one": { + "name": "Voltage phase one" + }, + "voltage_phase_two": { + "name": "Voltage phase two" + }, + "voltage_phase_three": { + "name": "Voltage phase three" + }, + "gas_consumed_interval": { + "name": "Gas consumed interval" + }, + "gas_consumed_cumulative": { + "name": "Gas consumed cumulative" + }, + "net_electricity_point": { + "name": "Net electricity point" + }, + "net_electricity_cumulative": { + "name": "Net electricity cumulative" + }, + "modulation_level": { + "name": "Modulation level" + }, + "valve_position": { + "name": "Valve position" + }, + "water_pressure": { + "name": "Water pressure" + }, + "dhw_temperature": { + "name": "DHW temperature" + }, + "domestic_hot_water_setpoint": { + "name": "DHW setpoint" + }, + "maximum_boiler_temperature": { + "name": "Maximum boiler temperature" + } + }, "switch": { "cooling_ena_switch": { "name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]" From 2b4f6ffcd6a630a0c1e750a0d51a90bb460e6b01 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Jul 2023 22:50:39 -1000 Subject: [PATCH 0251/1009] Speed up hassio ingress (#95777) --- homeassistant/components/hassio/http.py | 28 +++--- homeassistant/components/hassio/ingress.py | 104 +++++++++++---------- tests/components/hassio/test_ingress.py | 91 ++++++++++++++++++ 3 files changed, 161 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 2480353c2d386e..34e1d89b8b4ea1 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -85,6 +85,13 @@ # pylint: enable=implicit-str-concat # fmt: on +RESPONSE_HEADERS_FILTER = { + TRANSFER_ENCODING, + CONTENT_LENGTH, + CONTENT_TYPE, + CONTENT_ENCODING, +} + class HassIOView(HomeAssistantView): """Hass.io view to handle base part.""" @@ -170,8 +177,9 @@ async def _handle(self, request: web.Request, path: str) -> web.StreamResponse: ) response.content_type = client.content_type + response.enable_compression() await response.prepare(request) - async for data in client.content.iter_chunked(4096): + async for data in client.content.iter_chunked(8192): await response.write(data) return response @@ -190,21 +198,13 @@ async def _handle(self, request: web.Request, path: str) -> web.StreamResponse: def _response_header(response: aiohttp.ClientResponse, path: str) -> dict[str, str]: """Create response header.""" - headers = {} - - for name, value in response.headers.items(): - if name in ( - TRANSFER_ENCODING, - CONTENT_LENGTH, - CONTENT_TYPE, - CONTENT_ENCODING, - ): - continue - headers[name] = value - + headers = { + name: value + for name, value in response.headers.items() + if name not in RESPONSE_HEADERS_FILTER + } if NO_STORE.match(path): headers[CACHE_CONTROL] = "no-store, max-age=0" - return headers diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index fc92e9309a03d8..2a9d9b7397824e 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -17,11 +17,32 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import UNDEFINED from .const import X_HASS_SOURCE, X_INGRESS_PATH _LOGGER = logging.getLogger(__name__) +INIT_HEADERS_FILTER = { + hdrs.CONTENT_LENGTH, + hdrs.CONTENT_ENCODING, + hdrs.TRANSFER_ENCODING, + hdrs.ACCEPT_ENCODING, # Avoid local compression, as we will compress at the border + hdrs.SEC_WEBSOCKET_EXTENSIONS, + hdrs.SEC_WEBSOCKET_PROTOCOL, + hdrs.SEC_WEBSOCKET_VERSION, + hdrs.SEC_WEBSOCKET_KEY, +} +RESPONSE_HEADERS_FILTER = { + hdrs.TRANSFER_ENCODING, + hdrs.CONTENT_LENGTH, + hdrs.CONTENT_TYPE, + hdrs.CONTENT_ENCODING, +} + +MIN_COMPRESSED_SIZE = 128 +MAX_SIMPLE_RESPONSE_SIZE = 4194000 + @callback def async_setup_ingress_view(hass: HomeAssistant, host: str): @@ -145,28 +166,35 @@ async def _handle_request( skip_auto_headers={hdrs.CONTENT_TYPE}, ) as result: headers = _response_header(result) - + content_length_int = 0 + content_length = result.headers.get(hdrs.CONTENT_LENGTH, UNDEFINED) # Simple request - if ( - hdrs.CONTENT_LENGTH in result.headers - and int(result.headers.get(hdrs.CONTENT_LENGTH, 0)) < 4194000 - ) or result.status in (204, 304): + if result.status in (204, 304) or ( + content_length is not UNDEFINED + and (content_length_int := int(content_length or 0)) + <= MAX_SIMPLE_RESPONSE_SIZE + ): # Return Response body = await result.read() - return web.Response( + simple_response = web.Response( headers=headers, status=result.status, content_type=result.content_type, body=body, ) + if content_length_int > MIN_COMPRESSED_SIZE: + simple_response.enable_compression() + await simple_response.prepare(request) + return simple_response # Stream response response = web.StreamResponse(status=result.status, headers=headers) response.content_type = result.content_type try: + response.enable_compression() await response.prepare(request) - async for data in result.content.iter_chunked(4096): + async for data in result.content.iter_chunked(8192): await response.write(data) except ( @@ -179,24 +207,20 @@ async def _handle_request( return response +@lru_cache(maxsize=32) +def _forwarded_for_header(forward_for: str | None, peer_name: str) -> str: + """Create X-Forwarded-For header.""" + connected_ip = ip_address(peer_name) + return f"{forward_for}, {connected_ip!s}" if forward_for else f"{connected_ip!s}" + + def _init_header(request: web.Request, token: str) -> CIMultiDict | dict[str, str]: """Create initial header.""" - headers = {} - - # filter flags - for name, value in request.headers.items(): - if name in ( - hdrs.CONTENT_LENGTH, - hdrs.CONTENT_ENCODING, - hdrs.TRANSFER_ENCODING, - hdrs.SEC_WEBSOCKET_EXTENSIONS, - hdrs.SEC_WEBSOCKET_PROTOCOL, - hdrs.SEC_WEBSOCKET_VERSION, - hdrs.SEC_WEBSOCKET_KEY, - ): - continue - headers[name] = value - + headers = { + name: value + for name, value in request.headers.items() + if name not in INIT_HEADERS_FILTER + } # Ingress information headers[X_HASS_SOURCE] = "core.ingress" headers[X_INGRESS_PATH] = f"/api/hassio_ingress/{token}" @@ -208,12 +232,7 @@ def _init_header(request: web.Request, token: str) -> CIMultiDict | dict[str, st _LOGGER.error("Can't set forward_for header, missing peername") raise HTTPBadRequest() - connected_ip = ip_address(peername[0]) - if forward_for: - forward_for = f"{forward_for}, {connected_ip!s}" - else: - forward_for = f"{connected_ip!s}" - headers[hdrs.X_FORWARDED_FOR] = forward_for + headers[hdrs.X_FORWARDED_FOR] = _forwarded_for_header(forward_for, peername[0]) # Set X-Forwarded-Host if not (forward_host := request.headers.get(hdrs.X_FORWARDED_HOST)): @@ -223,7 +242,7 @@ def _init_header(request: web.Request, token: str) -> CIMultiDict | dict[str, st # Set X-Forwarded-Proto forward_proto = request.headers.get(hdrs.X_FORWARDED_PROTO) if not forward_proto: - forward_proto = request.url.scheme + forward_proto = request.scheme headers[hdrs.X_FORWARDED_PROTO] = forward_proto return headers @@ -231,31 +250,20 @@ def _init_header(request: web.Request, token: str) -> CIMultiDict | dict[str, st def _response_header(response: aiohttp.ClientResponse) -> dict[str, str]: """Create response header.""" - headers = {} - - for name, value in response.headers.items(): - if name in ( - hdrs.TRANSFER_ENCODING, - hdrs.CONTENT_LENGTH, - hdrs.CONTENT_TYPE, - hdrs.CONTENT_ENCODING, - ): - continue - headers[name] = value - - return headers + return { + name: value + for name, value in response.headers.items() + if name not in RESPONSE_HEADERS_FILTER + } def _is_websocket(request: web.Request) -> bool: """Return True if request is a websocket.""" headers = request.headers - - if ( + return bool( "upgrade" in headers.get(hdrs.CONNECTION, "").lower() and headers.get(hdrs.UPGRADE, "").lower() == "websocket" - ): - return True - return False + ) async def _websocket_forward(ws_from, ws_to): diff --git a/tests/components/hassio/test_ingress.py b/tests/components/hassio/test_ingress.py index 06b7523614c832..6df946ad2cfafb 100644 --- a/tests/components/hassio/test_ingress.py +++ b/tests/components/hassio/test_ingress.py @@ -348,3 +348,94 @@ async def test_forwarding_paths_as_requested( "/api/hassio_ingress/mock-token/hello/%252e./world", ) assert await resp.text() == "test" + + +@pytest.mark.parametrize( + "build_type", + [ + ("a3_vl", "test/beer/ping?index=1"), + ("core", "index.html"), + ("local", "panel/config"), + ("jk_921", "editor.php?idx=3&ping=5"), + ("fsadjf10312", ""), + ], +) +async def test_ingress_request_get_compressed( + hassio_noauth_client, build_type, aioclient_mock: AiohttpClientMocker +) -> None: + """Test ingress compressed.""" + body = "this_is_long_enough_to_be_compressed" * 100 + aioclient_mock.get( + f"http://127.0.0.1/ingress/{build_type[0]}/{build_type[1]}", + text=body, + headers={"Content-Length": len(body)}, + ) + + resp = await hassio_noauth_client.get( + f"/api/hassio_ingress/{build_type[0]}/{build_type[1]}", + headers={"X-Test-Header": "beer", "Accept-Encoding": "gzip, deflate"}, + ) + + # Check we got right response + assert resp.status == HTTPStatus.OK + body = await resp.text() + assert body == body + assert resp.headers["Content-Encoding"] == "deflate" + + # Check we forwarded command + assert len(aioclient_mock.mock_calls) == 1 + assert X_AUTH_TOKEN not in aioclient_mock.mock_calls[-1][3] + assert aioclient_mock.mock_calls[-1][3]["X-Hass-Source"] == "core.ingress" + assert ( + aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] + == f"/api/hassio_ingress/{build_type[0]}" + ) + assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO] + + +@pytest.mark.parametrize( + "build_type", + [ + ("a3_vl", "test/beer/ping?index=1"), + ("core", "index.html"), + ("local", "panel/config"), + ("jk_921", "editor.php?idx=3&ping=5"), + ("fsadjf10312", ""), + ], +) +async def test_ingress_request_get_not_changed( + hassio_noauth_client, build_type, aioclient_mock: AiohttpClientMocker +) -> None: + """Test ingress compressed and not modified.""" + aioclient_mock.get( + f"http://127.0.0.1/ingress/{build_type[0]}/{build_type[1]}", + text="test", + status=HTTPStatus.NOT_MODIFIED, + ) + + resp = await hassio_noauth_client.get( + f"/api/hassio_ingress/{build_type[0]}/{build_type[1]}", + headers={"X-Test-Header": "beer", "Accept-Encoding": "gzip, deflate"}, + ) + + # Check we got right response + assert resp.status == HTTPStatus.NOT_MODIFIED + body = await resp.text() + assert body == "" + assert "Content-Encoding" not in resp.headers # too small to compress + + # Check we forwarded command + assert len(aioclient_mock.mock_calls) == 1 + assert X_AUTH_TOKEN not in aioclient_mock.mock_calls[-1][3] + assert aioclient_mock.mock_calls[-1][3]["X-Hass-Source"] == "core.ingress" + assert ( + aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] + == f"/api/hassio_ingress/{build_type[0]}" + ) + assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO] From 6dae3553f254852d3a4ed380f036139f22259887 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 8 Jul 2023 10:55:25 +0200 Subject: [PATCH 0252/1009] Add MEDIA_ENQUEUE to MediaPlayerEntityFeature (#95905) --- homeassistant/components/forked_daapd/const.py | 1 + homeassistant/components/group/media_player.py | 8 ++++++++ homeassistant/components/heos/media_player.py | 1 + homeassistant/components/media_player/const.py | 1 + homeassistant/components/media_player/services.yaml | 3 +++ homeassistant/components/sonos/media_player.py | 1 + homeassistant/components/squeezebox/media_player.py | 1 + tests/components/group/test_media_player.py | 4 +++- 8 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/forked_daapd/const.py b/homeassistant/components/forked_daapd/const.py index 69438dc17f16b2..5668f941c6e712 100644 --- a/homeassistant/components/forked_daapd/const.py +++ b/homeassistant/components/forked_daapd/const.py @@ -82,6 +82,7 @@ | MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.BROWSE_MEDIA + | MediaPlayerEntityFeature.MEDIA_ENQUEUE ) SUPPORTED_FEATURES_ZONE = ( MediaPlayerEntityFeature.VOLUME_SET diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index 15be22ddfbffc9..fa43ac76ea636c 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -51,6 +51,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType KEY_CLEAR_PLAYLIST = "clear_playlist" +KEY_ENQUEUE = "enqueue" KEY_ON_OFF = "on_off" KEY_PAUSE_PLAY_STOP = "play" KEY_PLAY_MEDIA = "play_media" @@ -116,6 +117,7 @@ def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> Non self._entities = entities self._features: dict[str, set[str]] = { KEY_CLEAR_PLAYLIST: set(), + KEY_ENQUEUE: set(), KEY_ON_OFF: set(), KEY_PAUSE_PLAY_STOP: set(), KEY_PLAY_MEDIA: set(), @@ -192,6 +194,10 @@ def async_update_supported_features( self._features[KEY_VOLUME].add(entity_id) else: self._features[KEY_VOLUME].discard(entity_id) + if new_features & MediaPlayerEntityFeature.MEDIA_ENQUEUE: + self._features[KEY_ENQUEUE].add(entity_id) + else: + self._features[KEY_ENQUEUE].discard(entity_id) async def async_added_to_hass(self) -> None: """Register listeners.""" @@ -434,6 +440,8 @@ def async_update_state(self) -> None: | MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_STEP ) + if self._features[KEY_ENQUEUE]: + supported_features |= MediaPlayerEntityFeature.MEDIA_ENQUEUE self._attr_supported_features = supported_features self.async_write_ha_state() diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 9ad33caf0734fc..3b6f5bcdd2fcc7 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -52,6 +52,7 @@ | MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.GROUPING | MediaPlayerEntityFeature.BROWSE_MEDIA + | MediaPlayerEntityFeature.MEDIA_ENQUEUE ) PLAY_STATE_TO_STATE = { diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 1cc90aa49043c4..f96d2a012c837a 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -199,6 +199,7 @@ class MediaPlayerEntityFeature(IntFlag): BROWSE_MEDIA = 131072 REPEAT_SET = 262144 GROUPING = 524288 + MEDIA_ENQUEUE = 2097152 # These SUPPORT_* constants are deprecated as of Home Assistant 2022.5. diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 5a513e4f3a0500..536d229dbda8a7 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -154,6 +154,9 @@ play_media: enqueue: name: Enqueue description: If the content should be played now or be added to the queue. + filter: + supported_features: + - media_player.MediaPlayerEntityFeature.MEDIA_ENQUEUE required: false selector: select: diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 526ddd2bcc7483..c519d2371009bc 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -195,6 +195,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): MediaPlayerEntityFeature.BROWSE_MEDIA | MediaPlayerEntityFeature.CLEAR_PLAYLIST | MediaPlayerEntityFeature.GROUPING + | MediaPlayerEntityFeature.MEDIA_ENQUEUE | MediaPlayerEntityFeature.NEXT_TRACK | MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index d3fae39bc4d3fc..d57ba8ba49df03 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -234,6 +234,7 @@ class SqueezeBoxEntity(MediaPlayerEntity): | MediaPlayerEntityFeature.CLEAR_PLAYLIST | MediaPlayerEntityFeature.STOP | MediaPlayerEntityFeature.GROUPING + | MediaPlayerEntityFeature.MEDIA_ENQUEUE ) def __init__(self, player): diff --git a/tests/components/group/test_media_player.py b/tests/components/group/test_media_player.py index 4549a7f5fec0bc..3524c0f1e88d38 100644 --- a/tests/components/group/test_media_player.py +++ b/tests/components/group/test_media_player.py @@ -191,7 +191,9 @@ async def test_supported_features(hass: HomeAssistant) -> None: | MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.STOP ) - play_media = MediaPlayerEntityFeature.PLAY_MEDIA + play_media = ( + MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.MEDIA_ENQUEUE + ) volume = ( MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.VOLUME_SET From e0274ec854ceeed7d5c1158f889436dac8f1e656 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 8 Jul 2023 11:39:53 +0200 Subject: [PATCH 0253/1009] Use device class naming for nobo hub v2 (#96022) --- homeassistant/components/nobo_hub/climate.py | 4 ++-- homeassistant/components/nobo_hub/sensor.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py index f55dc9344ab65d..00667c43fdb7c6 100644 --- a/homeassistant/components/nobo_hub/climate.py +++ b/homeassistant/components/nobo_hub/climate.py @@ -73,6 +73,8 @@ class NoboZone(ClimateEntity): controlled as a unity. """ + _attr_name = None + _attr_has_entity_name = True _attr_max_temp = MAX_TEMPERATURE _attr_min_temp = MIN_TEMPERATURE _attr_precision = PRECISION_TENTHS @@ -87,8 +89,6 @@ def __init__(self, zone_id, hub: nobo, override_type) -> None: self._id = zone_id self._nobo = hub self._attr_unique_id = f"{hub.hub_serial}:{zone_id}" - self._attr_name = None - self._attr_has_entity_name = True self._attr_hvac_mode = HVACMode.AUTO self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.AUTO] self._override_type = override_type diff --git a/homeassistant/components/nobo_hub/sensor.py b/homeassistant/components/nobo_hub/sensor.py index c5536bad6ea2e7..9cc957ec1dfc1d 100644 --- a/homeassistant/components/nobo_hub/sensor.py +++ b/homeassistant/components/nobo_hub/sensor.py @@ -46,6 +46,7 @@ class NoboTemperatureSensor(SensorEntity): _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS _attr_state_class = SensorStateClass.MEASUREMENT _attr_should_poll = False + _attr_has_entity_name = True def __init__(self, serial: str, hub: nobo) -> None: """Initialize the temperature sensor.""" @@ -54,8 +55,6 @@ def __init__(self, serial: str, hub: nobo) -> None: self._nobo = hub component = hub.components[self._id] self._attr_unique_id = component[ATTR_SERIAL] - self._attr_name = "Temperature" - self._attr_has_entity_name = True zone_id = component[ATTR_ZONE_ID] suggested_area = None if zone_id != "-1": From 1eb2ddf0103b3795632d6a98d96350593eece9ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 8 Jul 2023 11:41:39 +0200 Subject: [PATCH 0254/1009] Update aioairzone-cloud to v0.2.1 (#96063) --- .../components/airzone_cloud/diagnostics.py | 10 +++++-- .../components/airzone_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../airzone_cloud/test_diagnostics.py | 12 ++++++++- tests/components/airzone_cloud/util.py | 27 +++++++++++-------- 6 files changed, 38 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/airzone_cloud/diagnostics.py b/homeassistant/components/airzone_cloud/diagnostics.py index a86f95d6187b4c..0bce3251d5afa4 100644 --- a/homeassistant/components/airzone_cloud/diagnostics.py +++ b/homeassistant/components/airzone_cloud/diagnostics.py @@ -7,6 +7,7 @@ from aioairzone_cloud.const import ( API_CITY, API_GROUP_ID, + API_GROUPS, API_LOCATION_ID, API_OLD_ID, API_PIN, @@ -29,7 +30,6 @@ TO_REDACT_API = [ API_CITY, - API_GROUP_ID, API_LOCATION_ID, API_OLD_ID, API_PIN, @@ -58,11 +58,17 @@ def gather_ids(api_data: dict[str, Any]) -> dict[str, Any]: ids[dev_id] = f"device{dev_idx}" dev_idx += 1 + group_idx = 1 inst_idx = 1 - for inst_id in api_data[RAW_INSTALLATIONS]: + for inst_id, inst_data in api_data[RAW_INSTALLATIONS].items(): if inst_id not in ids: ids[inst_id] = f"installation{inst_idx}" inst_idx += 1 + for group in inst_data[API_GROUPS]: + group_id = group[API_GROUP_ID] + if group_id not in ids: + ids[group_id] = f"group{group_idx}" + group_idx += 1 ws_idx = 1 for ws_id in api_data[RAW_WEBSERVERS]: diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index 8602dfa14cf20d..289565f0473803 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "iot_class": "cloud_polling", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.2.0"] + "requirements": ["aioairzone-cloud==0.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index cc0922a033781a..a4a4cf09bbd4ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -191,7 +191,7 @@ aio-georss-gdacs==0.8 aioairq==0.2.4 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.2.0 +aioairzone-cloud==0.2.1 # homeassistant.components.airzone aioairzone==0.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6a3baf8136430..1a79cab57c4937 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -169,7 +169,7 @@ aio-georss-gdacs==0.8 aioairq==0.2.4 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.2.0 +aioairzone-cloud==0.2.1 # homeassistant.components.airzone aioairzone==0.6.4 diff --git a/tests/components/airzone_cloud/test_diagnostics.py b/tests/components/airzone_cloud/test_diagnostics.py index 730ac27325aa60..6c8ae366518df9 100644 --- a/tests/components/airzone_cloud/test_diagnostics.py +++ b/tests/components/airzone_cloud/test_diagnostics.py @@ -5,9 +5,11 @@ from aioairzone_cloud.const import ( API_DEVICE_ID, API_DEVICES, + API_GROUP_ID, API_GROUPS, API_WS_ID, AZD_AIDOOS, + AZD_GROUPS, AZD_INSTALLATIONS, AZD_SYSTEMS, AZD_WEBSERVERS, @@ -40,9 +42,10 @@ CONFIG[CONF_ID]: { API_GROUPS: [ { + API_GROUP_ID: "grp1", API_DEVICES: [ { - API_DEVICE_ID: "device1", + API_DEVICE_ID: "dev1", API_WS_ID: WS_ID, }, ], @@ -91,6 +94,12 @@ async def test_config_entry_diagnostics( assert list(diag["api_data"]) >= list(RAW_DATA_MOCK) assert "dev1" not in diag["api_data"][RAW_DEVICES_CONFIG] assert "device1" in diag["api_data"][RAW_DEVICES_CONFIG] + assert ( + diag["api_data"][RAW_INSTALLATIONS]["installation1"][API_GROUPS][0][ + API_GROUP_ID + ] + == "group1" + ) assert "inst1" not in diag["api_data"][RAW_INSTALLATIONS] assert "installation1" in diag["api_data"][RAW_INSTALLATIONS] assert WS_ID not in diag["api_data"][RAW_WEBSERVERS] @@ -111,6 +120,7 @@ async def test_config_entry_diagnostics( assert list(diag["coord_data"]) >= [ AZD_AIDOOS, + AZD_GROUPS, AZD_INSTALLATIONS, AZD_SYSTEMS, AZD_WEBSERVERS, diff --git a/tests/components/airzone_cloud/util.py b/tests/components/airzone_cloud/util.py index 80c0b4ae02731e..a8cb539bb1d253 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -16,6 +16,7 @@ API_DISCONNECTION_DATE, API_ERRORS, API_FAH, + API_GROUP_ID, API_GROUPS, API_HUMIDITY, API_INSTALLATION_ID, @@ -61,6 +62,7 @@ GET_INSTALLATION_MOCK = { API_GROUPS: [ { + API_GROUP_ID: "grp1", API_NAME: "Group", API_DEVICES: [ { @@ -94,6 +96,7 @@ ], }, { + API_GROUP_ID: "grp2", API_NAME: "Aidoo Group", API_DEVICES: [ { @@ -176,6 +179,18 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: API_WS_CONNECTED: True, API_WARNINGS: [], } + if device.get_id() == "zone1": + return { + API_ACTIVE: True, + API_HUMIDITY: 30, + API_IS_CONNECTED: True, + API_WS_CONNECTED: True, + API_LOCAL_TEMP: { + API_FAH: 68, + API_CELSIUS: 20, + }, + API_WARNINGS: [], + } if device.get_id() == "zone2": return { API_ACTIVE: False, @@ -188,17 +203,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: }, API_WARNINGS: [], } - return { - API_ACTIVE: True, - API_HUMIDITY: 30, - API_IS_CONNECTED: True, - API_WS_CONNECTED: True, - API_LOCAL_TEMP: { - API_FAH: 68, - API_CELSIUS: 20, - }, - API_WARNINGS: [], - } + return None def mock_get_webserver(webserver: WebServer, devices: bool) -> dict[str, Any]: From 32b7370321cf51087a0672ea63849e84017f6367 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 8 Jul 2023 11:42:27 +0200 Subject: [PATCH 0255/1009] Add filters to alarm_control_panel/services.yaml (#95850) --- .../components/alarm_control_panel/services.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 0bf3952c4ed341..c3022b87eb7684 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -20,6 +20,8 @@ alarm_arm_custom_bypass: target: entity: domain: alarm_control_panel + supported_features: + - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS fields: code: name: Code @@ -34,6 +36,8 @@ alarm_arm_home: target: entity: domain: alarm_control_panel + supported_features: + - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME fields: code: name: Code @@ -48,6 +52,8 @@ alarm_arm_away: target: entity: domain: alarm_control_panel + supported_features: + - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY fields: code: name: Code @@ -62,6 +68,8 @@ alarm_arm_night: target: entity: domain: alarm_control_panel + supported_features: + - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT fields: code: name: Code @@ -76,6 +84,8 @@ alarm_arm_vacation: target: entity: domain: alarm_control_panel + supported_features: + - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION fields: code: name: Code @@ -90,6 +100,8 @@ alarm_trigger: target: entity: domain: alarm_control_panel + supported_features: + - alarm_control_panel.AlarmControlPanelEntityFeature.TRIGGER fields: code: name: Code From 207721b42134ba271cffd26355e1b9a42dfd7273 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sat, 8 Jul 2023 11:43:14 +0200 Subject: [PATCH 0256/1009] Make generic camera integration title translatable (#95806) --- homeassistant/components/generic/strings.json | 1 + homeassistant/generated/integrations.json | 2 +- script/hassfest/translations.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index 5fddd2d78fe441..0ce8af4f3a6849 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -1,4 +1,5 @@ { + "title": "Generic Camera", "config": { "error": { "unknown": "[%key:common::config_flow::error::unknown%]", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index e8271f2cadec17..84a670d3759be4 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1896,7 +1896,6 @@ "iot_class": "cloud_polling" }, "generic": { - "name": "Generic Camera", "integration_type": "hub", "config_flow": true, "iot_class": "local_push" @@ -6663,6 +6662,7 @@ "emulated_roku", "filesize", "garages_amsterdam", + "generic", "google_travel_time", "group", "growatt_server", diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 9f464fd4147d6f..5f233b4dec8c1e 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -31,6 +31,7 @@ "emulated_roku", "faa_delays", "garages_amsterdam", + "generic", "google_travel_time", "homekit_controller", "islamic_prayer_times", From b8af7fbd5563f45a21e5ec8d5d5bef396bd32e90 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 8 Jul 2023 11:47:49 +0200 Subject: [PATCH 0257/1009] Update template vacuum supported features (#95831) --- homeassistant/components/template/vacuum.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index f95c2660164673..c5705c34076b1d 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -148,7 +148,9 @@ def __init__( self._template = config.get(CONF_VALUE_TEMPLATE) self._battery_level_template = config.get(CONF_BATTERY_LEVEL_TEMPLATE) self._fan_speed_template = config.get(CONF_FAN_SPEED_TEMPLATE) - self._attr_supported_features = VacuumEntityFeature.START + self._attr_supported_features = ( + VacuumEntityFeature.START | VacuumEntityFeature.STATE + ) self._start_script = Script(hass, config[SERVICE_START], friendly_name, DOMAIN) @@ -192,8 +194,6 @@ def __init__( self._battery_level = None self._attr_fan_speed = None - if self._template: - self._attr_supported_features |= VacuumEntityFeature.STATE if self._battery_level_template: self._attr_supported_features |= VacuumEntityFeature.BATTERY From 6f9a640fa30e9ec500c223071dbe4f4d817effe5 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sat, 8 Jul 2023 11:48:15 +0200 Subject: [PATCH 0258/1009] Make workday integration title translatable (#95803) --- homeassistant/components/workday/strings.json | 1 + homeassistant/generated/integrations.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/workday/strings.json b/homeassistant/components/workday/strings.json index e6753b39dcefaf..fcebc7638c6ef3 100644 --- a/homeassistant/components/workday/strings.json +++ b/homeassistant/components/workday/strings.json @@ -1,4 +1,5 @@ { + "title": "Workday", "config": { "abort": { "incorrect_province": "Incorrect subdivision from yaml import", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 84a670d3759be4..c7f842748c20af 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6289,7 +6289,6 @@ "iot_class": "cloud_polling" }, "workday": { - "name": "Workday", "integration_type": "hub", "config_flow": true, "iot_class": "local_polling" @@ -6695,6 +6694,7 @@ "tod", "uptime", "utility_meter", - "waze_travel_time" + "waze_travel_time", + "workday" ] } From 39c386e8b684476d7e5599fb24ebea0dda01e32c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 8 Jul 2023 11:49:09 +0200 Subject: [PATCH 0259/1009] Add filters to fan/services.yaml (#95855) --- homeassistant/components/fan/services.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 52d5aca070aa20..db3bea9cad3a8d 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -5,6 +5,8 @@ set_preset_mode: target: entity: domain: fan + supported_features: + - fan.FanEntityFeature.PRESET_MODE fields: preset_mode: name: Preset mode @@ -20,6 +22,8 @@ set_percentage: target: entity: domain: fan + supported_features: + - fan.FanEntityFeature.SET_SPEED fields: percentage: name: Percentage @@ -41,6 +45,9 @@ turn_on: percentage: name: Percentage description: Percentage speed setting. + filter: + supported_features: + - fan.FanEntityFeature.SET_SPEED selector: number: min: 0 @@ -50,6 +57,9 @@ turn_on: name: Preset mode description: Preset mode setting. example: "auto" + filter: + supported_features: + - fan.FanEntityFeature.PRESET_MODE selector: text: @@ -66,6 +76,8 @@ oscillate: target: entity: domain: fan + supported_features: + - fan.FanEntityFeature.OSCILLATE fields: oscillating: name: Oscillating @@ -87,6 +99,8 @@ set_direction: target: entity: domain: fan + supported_features: + - fan.FanEntityFeature.DIRECTION fields: direction: name: Direction @@ -106,6 +120,8 @@ increase_speed: target: entity: domain: fan + supported_features: + - fan.FanEntityFeature.SET_SPEED fields: percentage_step: advanced: true @@ -123,6 +139,8 @@ decrease_speed: target: entity: domain: fan + supported_features: + - fan.FanEntityFeature.SET_SPEED fields: percentage_step: advanced: true From 602ca5dafe0e9dcbdae5de1795f3b8271d548f66 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 8 Jul 2023 11:49:38 +0200 Subject: [PATCH 0260/1009] Add filters to humidifier/services.yaml (#95859) --- homeassistant/components/humidifier/services.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/humidifier/services.yaml b/homeassistant/components/humidifier/services.yaml index 9c1b748c9acadc..d498f0a2c14cd2 100644 --- a/homeassistant/components/humidifier/services.yaml +++ b/homeassistant/components/humidifier/services.yaml @@ -6,6 +6,8 @@ set_mode: target: entity: domain: humidifier + supported_features: + - humidifier.HumidifierEntityFeature.MODES fields: mode: description: New mode From b5678a12ec7d8a38d5cec079fcdfff4797adc01e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 8 Jul 2023 11:50:13 +0200 Subject: [PATCH 0261/1009] Add filters to lock/services.yaml (#95860) --- homeassistant/components/lock/services.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml index 740d107d625c28..992c58cf5f6c1b 100644 --- a/homeassistant/components/lock/services.yaml +++ b/homeassistant/components/lock/services.yaml @@ -20,6 +20,8 @@ open: target: entity: domain: lock + supported_features: + - lock.LockEntityFeature.OPEN fields: code: name: Code From 3d064b7d6bdfb201fb77ebb7d16b41f19cb9947d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 8 Jul 2023 11:51:02 +0200 Subject: [PATCH 0262/1009] Add filters to cover/services.yaml (#95854) --- homeassistant/components/cover/services.yaml | 22 +++++++++++++++++++ homeassistant/helpers/selector.py | 23 +++++++++++++++----- tests/helpers/test_selector.py | 16 ++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cover/services.yaml b/homeassistant/components/cover/services.yaml index 2f8e20464f3f3a..8ab42c6d3e97c5 100644 --- a/homeassistant/components/cover/services.yaml +++ b/homeassistant/components/cover/services.yaml @@ -6,6 +6,8 @@ open_cover: target: entity: domain: cover + supported_features: + - cover.CoverEntityFeature.OPEN close_cover: name: Close @@ -13,6 +15,8 @@ close_cover: target: entity: domain: cover + supported_features: + - cover.CoverEntityFeature.CLOSE toggle: name: Toggle @@ -20,6 +24,9 @@ toggle: target: entity: domain: cover + supported_features: + - - cover.CoverEntityFeature.CLOSE + - cover.CoverEntityFeature.OPEN set_cover_position: name: Set position @@ -27,6 +34,8 @@ set_cover_position: target: entity: domain: cover + supported_features: + - cover.CoverEntityFeature.SET_POSITION fields: position: name: Position @@ -44,6 +53,8 @@ stop_cover: target: entity: domain: cover + supported_features: + - cover.CoverEntityFeature.STOP open_cover_tilt: name: Open tilt @@ -51,6 +62,8 @@ open_cover_tilt: target: entity: domain: cover + supported_features: + - cover.CoverEntityFeature.OPEN_TILT close_cover_tilt: name: Close tilt @@ -58,6 +71,8 @@ close_cover_tilt: target: entity: domain: cover + supported_features: + - cover.CoverEntityFeature.CLOSE_TILT toggle_cover_tilt: name: Toggle tilt @@ -65,6 +80,9 @@ toggle_cover_tilt: target: entity: domain: cover + supported_features: + - - cover.CoverEntityFeature.CLOSE_TILT + - cover.CoverEntityFeature.OPEN_TILT set_cover_tilt_position: name: Set tilt position @@ -72,6 +90,8 @@ set_cover_tilt_position: target: entity: domain: cover + supported_features: + - cover.CoverEntityFeature.SET_TILT_POSITION fields: tilt_position: name: Tilt position @@ -89,3 +109,5 @@ stop_cover_tilt: target: entity: domain: cover + supported_features: + - cover.CoverEntityFeature.STOP_TILT diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 84c0f769c7cec8..abd4d2e623ee12 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -122,12 +122,9 @@ def _entity_features() -> dict[str, type[IntFlag]]: } -def _validate_supported_feature(supported_feature: int | str) -> int: +def _validate_supported_feature(supported_feature: str) -> int: """Validate a supported feature and resolve an enum string to its value.""" - if isinstance(supported_feature, int): - return supported_feature - known_entity_features = _entity_features() try: @@ -144,6 +141,20 @@ def _validate_supported_feature(supported_feature: int | str) -> int: raise vol.Invalid(f"Unknown supported feature '{supported_feature}'") from exc +def _validate_supported_features(supported_features: int | list[str]) -> int: + """Validate a supported feature and resolve an enum string to its value.""" + + if isinstance(supported_features, int): + return supported_features + + feature_mask = 0 + + for supported_feature in supported_features: + feature_mask |= _validate_supported_feature(supported_feature) + + return feature_mask + + ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema( { # Integration that provided the entity @@ -153,7 +164,9 @@ def _validate_supported_feature(supported_feature: int | str) -> int: # Device class of the entity vol.Optional("device_class"): vol.All(cv.ensure_list, [str]), # Features supported by the entity - vol.Optional("supported_features"): [vol.All(str, _validate_supported_feature)], + vol.Optional("supported_features"): [ + vol.All(cv.ensure_list, [str], _validate_supported_features) + ], } ) diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index c518ad227a76f9..fd2dba4b08459e 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -235,6 +235,22 @@ def test_device_selector_schema(schema, valid_selections, invalid_selections) -> ("light.abc123", "blah.blah", FAKE_UUID), (None,), ), + ( + { + "filter": [ + { + "supported_features": [ + [ + "light.LightEntityFeature.EFFECT", + "light.LightEntityFeature.TRANSITION", + ] + ] + }, + ] + }, + ("light.abc123", "blah.blah", FAKE_UUID), + (None,), + ), ( { "filter": [ From e39f023e3f93b8cc00b1f647e8416bca2dd82d2b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Jul 2023 00:36:40 -1000 Subject: [PATCH 0263/1009] Refactor ESPHome camera to avoid creating tasks (#95818) --- homeassistant/components/esphome/camera.py | 60 +++++++++++++--------- tests/components/esphome/test_camera.py | 3 -- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index 94a9b03b90ce2b..f3fb8b867d8f00 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -2,6 +2,8 @@ from __future__ import annotations import asyncio +from collections.abc import Callable, Coroutine +from functools import partial from typing import Any from aioesphomeapi import CameraInfo, CameraState @@ -40,48 +42,56 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize.""" Camera.__init__(self) EsphomeEntity.__init__(self, *args, **kwargs) - self._image_cond = asyncio.Condition() + self._loop = asyncio.get_running_loop() + self._image_futures: list[asyncio.Future[bool | None]] = [] + + @callback + def _set_futures(self, result: bool) -> None: + """Set futures to done.""" + for future in self._image_futures: + if not future.done(): + future.set_result(result) + self._image_futures.clear() + + @callback + def _on_device_update(self) -> None: + """Handle device going available or unavailable.""" + super()._on_device_update() + if not self.available: + self._set_futures(False) @callback def _on_state_update(self) -> None: """Notify listeners of new image when update arrives.""" super()._on_state_update() - self.hass.async_create_task(self._on_state_update_coro()) - - async def _on_state_update_coro(self) -> None: - async with self._image_cond: - self._image_cond.notify_all() + self._set_futures(True) async def async_camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return single camera image bytes.""" - if not self.available: - return None - await self._client.request_single_image() - async with self._image_cond: - await self._image_cond.wait() - if not self.available: - # Availability can change while waiting for 'self._image.cond' - return None # type: ignore[unreachable] - return self._state.data[:] + return await self._async_request_image(self._client.request_single_image) - async def _async_camera_stream_image(self) -> bytes | None: - """Return a single camera image in a stream.""" + async def _async_request_image( + self, request_method: Callable[[], Coroutine[Any, Any, None]] + ) -> bytes | None: + """Wait for an image to be available and return it.""" if not self.available: return None - await self._client.request_image_stream() - async with self._image_cond: - await self._image_cond.wait() - if not self.available: - # Availability can change while waiting for 'self._image.cond' - return None # type: ignore[unreachable] - return self._state.data[:] + image_future = self._loop.create_future() + self._image_futures.append(image_future) + await request_method() + if not await image_future: + return None + return self._state.data async def handle_async_mjpeg_stream( self, request: web.Request ) -> web.StreamResponse: """Serve an HTTP MJPEG stream from the camera.""" + stream_request = partial( + self._async_request_image, self._client.request_image_stream + ) return await camera.async_get_still_stream( - request, self._async_camera_stream_image, camera.DEFAULT_CONTENT_TYPE, 0.0 + request, stream_request, camera.DEFAULT_CONTENT_TYPE, 0.0 ) diff --git a/tests/components/esphome/test_camera.py b/tests/components/esphome/test_camera.py index f856a9dd15caa5..94ff4c6e7a889c 100644 --- a/tests/components/esphome/test_camera.py +++ b/tests/components/esphome/test_camera.py @@ -149,9 +149,6 @@ async def test_camera_single_image_unavailable_during_request( async def _mock_camera_image(): await mock_device.mock_disconnect(False) - # Currently there is a bug where the camera will block - # forever if we don't send a response - mock_device.set_state(CameraState(key=1, data=SMALLEST_VALID_JPEG_BYTES)) mock_client.request_single_image = _mock_camera_image From 5bf1547ebc8c9958bd93df483a208362e4637ed2 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 8 Jul 2023 14:00:51 +0200 Subject: [PATCH 0264/1009] Update pydantic to 1.10.11 (#96137) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 2ea19443aa40c5..baae4698b1e69b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ freezegun==1.2.2 mock-open==1.4.0 mypy==1.4.1 pre-commit==3.1.0 -pydantic==1.10.9 +pydantic==1.10.11 pylint==2.17.4 pylint-per-file-ignores==1.1.0 pipdeptree==2.9.4 From de211de59879d1f132da93a4eb1f9376116275ea Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 8 Jul 2023 16:49:17 +0200 Subject: [PATCH 0265/1009] Update lxml to 4.9.3 (#96132) --- homeassistant/components/scrape/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 8bfda778c79959..42f9fdb05d5a52 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -6,5 +6,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/scrape", "iot_class": "cloud_polling", - "requirements": ["beautifulsoup4==4.11.1", "lxml==4.9.1"] + "requirements": ["beautifulsoup4==4.11.1", "lxml==4.9.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index a4a4cf09bbd4ef..c724b59ba746aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1157,7 +1157,7 @@ lupupy==0.3.0 lw12==0.9.2 # homeassistant.components.scrape -lxml==4.9.1 +lxml==4.9.3 # homeassistant.components.nmap_tracker mac-vendor-lookup==0.1.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1a79cab57c4937..b0a6df91055e31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ loqedAPI==2.1.7 luftdaten==0.7.4 # homeassistant.components.scrape -lxml==4.9.1 +lxml==4.9.3 # homeassistant.components.nmap_tracker mac-vendor-lookup==0.1.12 From 598610e313850994050f8f883a18442740b9cfe4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 8 Jul 2023 16:50:46 +0200 Subject: [PATCH 0266/1009] Add entity translations to Sensibo (#96091) --- .../components/sensibo/binary_sensor.py | 16 ++- homeassistant/components/sensibo/button.py | 2 +- homeassistant/components/sensibo/entity.py | 6 +- homeassistant/components/sensibo/number.py | 4 +- homeassistant/components/sensibo/select.py | 2 - homeassistant/components/sensibo/sensor.py | 30 ++--- homeassistant/components/sensibo/strings.json | 113 ++++++++++++++++-- homeassistant/components/sensibo/switch.py | 6 +- homeassistant/components/sensibo/update.py | 2 +- .../components/sensibo/test_binary_sensor.py | 4 +- 10 files changed, 130 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/sensibo/binary_sensor.py b/homeassistant/components/sensibo/binary_sensor.py index e57267a1658cb7..08f45b94789eba 100644 --- a/homeassistant/components/sensibo/binary_sensor.py +++ b/homeassistant/components/sensibo/binary_sensor.py @@ -54,8 +54,8 @@ class SensiboDeviceBinarySensorEntityDescription( FILTER_CLEAN_REQUIRED_DESCRIPTION = SensiboDeviceBinarySensorEntityDescription( key="filter_clean", + translation_key="filter_clean", device_class=BinarySensorDeviceClass.PROBLEM, - name="Filter clean required", value_fn=lambda data: data.filter_clean, ) @@ -64,20 +64,18 @@ class SensiboDeviceBinarySensorEntityDescription( key="alive", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, - name="Alive", value_fn=lambda data: data.alive, ), SensiboMotionBinarySensorEntityDescription( key="is_main_sensor", + translation_key="is_main_sensor", entity_category=EntityCategory.DIAGNOSTIC, - name="Main sensor", icon="mdi:connection", value_fn=lambda data: data.is_main_sensor, ), SensiboMotionBinarySensorEntityDescription( key="motion", device_class=BinarySensorDeviceClass.MOTION, - name="Motion", icon="mdi:motion-sensor", value_fn=lambda data: data.motion, ), @@ -86,8 +84,8 @@ class SensiboDeviceBinarySensorEntityDescription( MOTION_DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( SensiboDeviceBinarySensorEntityDescription( key="room_occupied", + translation_key="room_occupied", device_class=BinarySensorDeviceClass.MOTION, - name="Room occupied", icon="mdi:motion-sensor", value_fn=lambda data: data.room_occupied, ), @@ -100,30 +98,30 @@ class SensiboDeviceBinarySensorEntityDescription( PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( SensiboDeviceBinarySensorEntityDescription( key="pure_ac_integration", + translation_key="pure_ac_integration", entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, - name="Pure Boost linked with AC", value_fn=lambda data: data.pure_ac_integration, ), SensiboDeviceBinarySensorEntityDescription( key="pure_geo_integration", + translation_key="pure_geo_integration", entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, - name="Pure Boost linked with presence", value_fn=lambda data: data.pure_geo_integration, ), SensiboDeviceBinarySensorEntityDescription( key="pure_measure_integration", + translation_key="pure_measure_integration", entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, - name="Pure Boost linked with indoor air quality", value_fn=lambda data: data.pure_measure_integration, ), SensiboDeviceBinarySensorEntityDescription( key="pure_prime_integration", + translation_key="pure_prime_integration", entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, - name="Pure Boost linked with outdoor air quality", value_fn=lambda data: data.pure_prime_integration, ), FILTER_CLEAN_REQUIRED_DESCRIPTION, diff --git a/homeassistant/components/sensibo/button.py b/homeassistant/components/sensibo/button.py index 1406d9d26c73ed..b47023f3ec49a8 100644 --- a/homeassistant/components/sensibo/button.py +++ b/homeassistant/components/sensibo/button.py @@ -33,7 +33,7 @@ class SensiboButtonEntityDescription( DEVICE_BUTTON_TYPES = SensiboButtonEntityDescription( key="reset_filter", - name="Reset filter", + translation_key="reset_filter", icon="mdi:air-filter", entity_category=EntityCategory.CONFIG, data_key="filter_clean", diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index 8b46e3e79417df..3696f618fd7687 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -49,6 +49,8 @@ async def wrap_api_call(*args: Any, **kwargs: Any) -> None: class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]): """Representation of a Sensibo Base Entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: SensiboDataUpdateCoordinator, @@ -68,8 +70,6 @@ def device_data(self) -> SensiboDevice: class SensiboDeviceBaseEntity(SensiboBaseEntity): """Representation of a Sensibo Device.""" - _attr_has_entity_name = True - def __init__( self, coordinator: SensiboDataUpdateCoordinator, @@ -93,8 +93,6 @@ def __init__( class SensiboMotionBaseEntity(SensiboBaseEntity): """Representation of a Sensibo Motion Entity.""" - _attr_has_entity_name = True - def __init__( self, coordinator: SensiboDataUpdateCoordinator, diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py index c39026265c7ad5..94765a17a4d5dc 100644 --- a/homeassistant/components/sensibo/number.py +++ b/homeassistant/components/sensibo/number.py @@ -38,8 +38,8 @@ class SensiboNumberEntityDescription( DEVICE_NUMBER_TYPES = ( SensiboNumberEntityDescription( key="calibration_temp", + translation_key="calibration_temperature", remote_key="temperature", - name="Temperature calibration", icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -50,8 +50,8 @@ class SensiboNumberEntityDescription( ), SensiboNumberEntityDescription( key="calibration_hum", + translation_key="calibration_humidity", remote_key="humidity", - name="Humidity calibration", icon="mdi:water", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py index 29ebdc8926108d..cda8a972ede14c 100644 --- a/homeassistant/components/sensibo/select.py +++ b/homeassistant/components/sensibo/select.py @@ -41,7 +41,6 @@ class SensiboSelectEntityDescription( SensiboSelectEntityDescription( key="horizontalSwing", data_key="horizontal_swing_mode", - name="Horizontal swing", icon="mdi:air-conditioner", value_fn=lambda data: data.horizontal_swing_mode, options_fn=lambda data: data.horizontal_swing_modes, @@ -51,7 +50,6 @@ class SensiboSelectEntityDescription( SensiboSelectEntityDescription( key="light", data_key="light_mode", - name="Light", icon="mdi:flashlight", value_fn=lambda data: data.light_mode, options_fn=lambda data: data.light_modes, diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 69d6a8cb78b432..7208902456ea8b 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -67,8 +67,8 @@ class SensiboDeviceSensorEntityDescription( FILTER_LAST_RESET_DESCRIPTION = SensiboDeviceSensorEntityDescription( key="filter_last_reset", + translation_key="filter_last_reset", device_class=SensorDeviceClass.TIMESTAMP, - name="Filter last reset", icon="mdi:timer", value_fn=lambda data: data.filter_last_reset, extra_fn=None, @@ -77,22 +77,22 @@ class SensiboDeviceSensorEntityDescription( MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( SensiboMotionSensorEntityDescription( key="rssi", + translation_key="rssi", device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, state_class=SensorStateClass.MEASUREMENT, - name="rssi", icon="mdi:wifi", value_fn=lambda data: data.rssi, entity_registry_enabled_default=False, ), SensiboMotionSensorEntityDescription( key="battery_voltage", + translation_key="battery_voltage", device_class=SensorDeviceClass.VOLTAGE, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, - name="Battery voltage", icon="mdi:battery", value_fn=lambda data: data.battery_voltage, ), @@ -101,7 +101,6 @@ class SensiboDeviceSensorEntityDescription( device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, - name="Humidity", icon="mdi:water", value_fn=lambda data: data.humidity, ), @@ -109,7 +108,6 @@ class SensiboDeviceSensorEntityDescription( key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - name="Temperature", icon="mdi:thermometer", value_fn=lambda data: data.temperature, ), @@ -120,18 +118,16 @@ class SensiboDeviceSensorEntityDescription( device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, - name="PM2.5", icon="mdi:air-filter", value_fn=lambda data: data.pm25, extra_fn=None, ), SensiboDeviceSensorEntityDescription( key="pure_sensitivity", - name="Pure sensitivity", + translation_key="sensitivity", icon="mdi:air-filter", value_fn=lambda data: data.pure_sensitivity, extra_fn=None, - translation_key="sensitivity", ), FILTER_LAST_RESET_DESCRIPTION, ) @@ -139,35 +135,35 @@ class SensiboDeviceSensorEntityDescription( DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( SensiboDeviceSensorEntityDescription( key="timer_time", + translation_key="timer_time", device_class=SensorDeviceClass.TIMESTAMP, - name="Timer end time", icon="mdi:timer", value_fn=lambda data: data.timer_time, extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on}, ), SensiboDeviceSensorEntityDescription( key="feels_like", + translation_key="feels_like", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - name="Temperature feels like", value_fn=lambda data: data.feelslike, extra_fn=None, entity_registry_enabled_default=False, ), SensiboDeviceSensorEntityDescription( key="climate_react_low", + translation_key="climate_react_low", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - name="Climate React low temperature threshold", value_fn=lambda data: data.smart_low_temp_threshold, extra_fn=lambda data: data.smart_low_state, entity_registry_enabled_default=False, ), SensiboDeviceSensorEntityDescription( key="climate_react_high", + translation_key="climate_react_high", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - name="Climate React high temperature threshold", value_fn=lambda data: data.smart_high_temp_threshold, extra_fn=lambda data: data.smart_high_state, entity_registry_enabled_default=False, @@ -175,7 +171,6 @@ class SensiboDeviceSensorEntityDescription( SensiboDeviceSensorEntityDescription( key="climate_react_type", translation_key="smart_type", - name="Climate React type", value_fn=lambda data: data.smart_type, extra_fn=None, entity_registry_enabled_default=False, @@ -186,19 +181,19 @@ class SensiboDeviceSensorEntityDescription( AIRQ_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( SensiboDeviceSensorEntityDescription( key="airq_tvoc", + translation_key="airq_tvoc", native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, state_class=SensorStateClass.MEASUREMENT, icon="mdi:air-filter", - name="AirQ TVOC", value_fn=lambda data: data.tvoc, extra_fn=None, ), SensiboDeviceSensorEntityDescription( key="airq_co2", + translation_key="airq_co2", device_class=SensorDeviceClass.CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, - name="AirQ CO2", value_fn=lambda data: data.co2, extra_fn=None, ), @@ -210,15 +205,14 @@ class SensiboDeviceSensorEntityDescription( device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, - name="PM 2.5", value_fn=lambda data: data.pm25, extra_fn=None, ), SensiboDeviceSensorEntityDescription( key="tvoc", + translation_key="tvoc", native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, state_class=SensorStateClass.MEASUREMENT, - name="TVOC", value_fn=lambda data: data.tvoc, extra_fn=None, ), @@ -227,7 +221,6 @@ class SensiboDeviceSensorEntityDescription( device_class=SensorDeviceClass.CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, - name="CO2", value_fn=lambda data: data.co2, extra_fn=None, ), @@ -243,7 +236,6 @@ class SensiboDeviceSensorEntityDescription( key="iaq", device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.MEASUREMENT, - name="Air quality", value_fn=lambda data: data.iaq, extra_fn=None, ), diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json index b00c4200836905..2379e2c2b38c83 100644 --- a/homeassistant/components/sensibo/strings.json +++ b/homeassistant/components/sensibo/strings.json @@ -31,23 +31,45 @@ } }, "entity": { - "sensor": { - "sensitivity": { - "state": { - "n": "Normal", - "s": "Sensitive" - } + "binary_sensor": { + "filter_clean": { + "name": "Filter clean required" }, - "smart_type": { - "state": { - "temperature": "Temperature", - "feelslike": "Feels like", - "humidity": "Humidity" - } + "is_main_sensor": { + "name": "Main sensor" + }, + "room_occupied": { + "name": "Room occupied" + }, + "pure_ac_integration": { + "name": "Pure Boost linked with AC" + }, + "pure_geo_integration": { + "name": "Pure Boost linked with presence" + }, + "pure_measure_integration": { + "name": "Pure Boost linked with indoor air quality" + }, + "pure_prime_integration": { + "name": "Pure Boost linked with outdoor air quality" + } + }, + "button": { + "reset_filter": { + "name": "Reset filter" + } + }, + "number": { + "calibration_temperature": { + "name": "Temperature calibration" + }, + "calibration_humidity": { + "name": "Humidity calibration" } }, "select": { "horizontalswing": { + "name": "Horizontal swing", "state": { "stopped": "Stopped", "fixedleft": "Fixed left", @@ -61,12 +83,79 @@ } }, "light": { + "name": "Light", "state": { "on": "[%key:common::state::on%]", "dim": "Dim", "off": "[%key:common::state::off%]" } } + }, + "sensor": { + "filter_last_reset": { + "name": "Filter last reset" + }, + "rssi": { + "name": "RSSI" + }, + "battery_voltage": { + "name": "Battery voltage" + }, + "sensitivity": { + "name": "Pure sensitivity", + "state": { + "n": "Normal", + "s": "Sensitive" + } + }, + "timer_time": { + "name": "Timer end time" + }, + "feels_like": { + "name": "Temperature feels like" + }, + "climate_react_low": { + "name": "Climate React low temperature threshold" + }, + "climate_react_high": { + "name": "Climate React high temperature threshold" + }, + "smart_type": { + "name": "Climate React type", + "state": { + "temperature": "Temperature", + "feelslike": "Feels like", + "humidity": "Humidity" + } + }, + "airq_tvoc": { + "name": "AirQ TVOC" + }, + "airq_co2": { + "name": "AirQ CO2" + }, + "tvoc": { + "name": "TVOC" + }, + "ethanol": { + "name": "Ethanol" + } + }, + "switch": { + "timer_on_switch": { + "name": "Timer" + }, + "climate_react_switch": { + "name": "Climate React" + }, + "pure_boost_switch": { + "name": "Pure Boost" + } + }, + "update": { + "fw_ver_available": { + "name": "Update available" + } } } } diff --git a/homeassistant/components/sensibo/switch.py b/homeassistant/components/sensibo/switch.py index ee9c946268fee8..20167ddd184224 100644 --- a/homeassistant/components/sensibo/switch.py +++ b/homeassistant/components/sensibo/switch.py @@ -45,8 +45,8 @@ class SensiboDeviceSwitchEntityDescription( DEVICE_SWITCH_TYPES: tuple[SensiboDeviceSwitchEntityDescription, ...] = ( SensiboDeviceSwitchEntityDescription( key="timer_on_switch", + translation_key="timer_on_switch", device_class=SwitchDeviceClass.SWITCH, - name="Timer", icon="mdi:timer", value_fn=lambda data: data.timer_on, extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on}, @@ -56,8 +56,8 @@ class SensiboDeviceSwitchEntityDescription( ), SensiboDeviceSwitchEntityDescription( key="climate_react_switch", + translation_key="climate_react_switch", device_class=SwitchDeviceClass.SWITCH, - name="Climate React", icon="mdi:wizard-hat", value_fn=lambda data: data.smart_on, extra_fn=lambda data: {"type": data.smart_type}, @@ -70,8 +70,8 @@ class SensiboDeviceSwitchEntityDescription( PURE_SWITCH_TYPES: tuple[SensiboDeviceSwitchEntityDescription, ...] = ( SensiboDeviceSwitchEntityDescription( key="pure_boost_switch", + translation_key="pure_boost_switch", device_class=SwitchDeviceClass.SWITCH, - name="Pure Boost", value_fn=lambda data: data.pure_boost_enabled, extra_fn=None, command_on="async_turn_on_off_pure_boost", diff --git a/homeassistant/components/sensibo/update.py b/homeassistant/components/sensibo/update.py index 4cfb605874061b..46b9b860ca6e74 100644 --- a/homeassistant/components/sensibo/update.py +++ b/homeassistant/components/sensibo/update.py @@ -41,9 +41,9 @@ class SensiboDeviceUpdateEntityDescription( DEVICE_SENSOR_TYPES: tuple[SensiboDeviceUpdateEntityDescription, ...] = ( SensiboDeviceUpdateEntityDescription( key="fw_ver_available", + translation_key="fw_ver_available", device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.DIAGNOSTIC, - name="Update available", icon="mdi:rocket-launch", value_version=lambda data: data.fw_ver, value_available=lambda data: data.fw_ver_available, diff --git a/tests/components/sensibo/test_binary_sensor.py b/tests/components/sensibo/test_binary_sensor.py index bb190908847c78..99bcfac8c9bb76 100644 --- a/tests/components/sensibo/test_binary_sensor.py +++ b/tests/components/sensibo/test_binary_sensor.py @@ -23,7 +23,7 @@ async def test_binary_sensor( ) -> None: """Test the Sensibo binary sensor.""" - state1 = hass.states.get("binary_sensor.hallway_motion_sensor_alive") + state1 = hass.states.get("binary_sensor.hallway_motion_sensor_connectivity") state2 = hass.states.get("binary_sensor.hallway_motion_sensor_main_sensor") state3 = hass.states.get("binary_sensor.hallway_motion_sensor_motion") state4 = hass.states.get("binary_sensor.hallway_room_occupied") @@ -57,7 +57,7 @@ async def test_binary_sensor( ) await hass.async_block_till_done() - state1 = hass.states.get("binary_sensor.hallway_motion_sensor_alive") + state1 = hass.states.get("binary_sensor.hallway_motion_sensor_connectivity") state3 = hass.states.get("binary_sensor.hallway_motion_sensor_motion") assert state1.state == "off" assert state3.state == "off" From 2c9910d9b601114c6c53b599e7d82a916486e078 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 8 Jul 2023 17:10:51 +0200 Subject: [PATCH 0267/1009] 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 64f7dafc1b7a49..972db00e476496 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 8b3e2f8535f2d6..21f6bd7a549b10 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 281d7af9947988..80011b47915398 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 92693d5fde9a4641701459ffeb1956fcce89271d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 8 Jul 2023 18:36:24 +0200 Subject: [PATCH 0268/1009] Add entity translations to Slack (#96149) --- homeassistant/components/slack/sensor.py | 2 +- homeassistant/components/slack/strings.json | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/slack/sensor.py b/homeassistant/components/slack/sensor.py index b190e6151ed894..4e65fdfc26df38 100644 --- a/homeassistant/components/slack/sensor.py +++ b/homeassistant/components/slack/sensor.py @@ -29,7 +29,7 @@ async def async_setup_entry( hass.data[DOMAIN][entry.entry_id][SLACK_DATA], SensorEntityDescription( key="do_not_disturb_until", - name="Do not disturb until", + translation_key="do_not_disturb_until", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, ), diff --git a/homeassistant/components/slack/strings.json b/homeassistant/components/slack/strings.json index f14129cf156f97..13b48644ffd78f 100644 --- a/homeassistant/components/slack/strings.json +++ b/homeassistant/components/slack/strings.json @@ -25,5 +25,12 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + }, + "entity": { + "sensor": { + "do_not_disturb_until": { + "name": "Do not disturb until" + } + } } } From 88d9a29b55776e4028e5fb917934a100882f16ad Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 8 Jul 2023 19:30:54 +0200 Subject: [PATCH 0269/1009] Update Pillow to 10.0.0 (#96106) --- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/generic/manifest.json | 2 +- homeassistant/components/image_upload/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- homeassistant/components/seven_segments/manifest.json | 2 +- homeassistant/components/sighthound/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 52c89f3f34b675..bc7c7d97430bcc 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/doods", "iot_class": "local_polling", "loggers": ["pydoods"], - "requirements": ["pydoods==1.0.2", "Pillow==9.5.0"] + "requirements": ["pydoods==1.0.2", "Pillow==10.0.0"] } diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index 134ce00ef70846..ea02bfedefb133 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/generic", "iot_class": "local_push", - "requirements": ["ha-av==10.1.0", "Pillow==9.5.0"] + "requirements": ["ha-av==10.1.0", "Pillow==10.0.0"] } diff --git a/homeassistant/components/image_upload/manifest.json b/homeassistant/components/image_upload/manifest.json index 48c57fb5d036fd..4f139785cd3b27 100644 --- a/homeassistant/components/image_upload/manifest.json +++ b/homeassistant/components/image_upload/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/image_upload", "integration_type": "system", "quality_scale": "internal", - "requirements": ["Pillow==9.5.0"] + "requirements": ["Pillow==10.0.0"] } diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 88a2a6c9b0f91f..b38bc93567df35 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -3,5 +3,5 @@ "name": "Camera Proxy", "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["Pillow==9.5.0"] + "requirements": ["Pillow==10.0.0"] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index a19760ad989f89..2176aa0c91e152 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/qrcode", "iot_class": "calculated", "loggers": ["pyzbar"], - "requirements": ["Pillow==9.5.0", "pyzbar==0.1.7"] + "requirements": ["Pillow==10.0.0", "pyzbar==0.1.7"] } diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index e9b2e9e2e9c8ef..ed8638d8419a53 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -4,5 +4,5 @@ "codeowners": ["@fabaff"], "documentation": "https://www.home-assistant.io/integrations/seven_segments", "iot_class": "local_polling", - "requirements": ["Pillow==9.5.0"] + "requirements": ["Pillow==10.0.0"] } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index 2fdf15a4a10b50..33080a9c1a2007 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/sighthound", "iot_class": "cloud_polling", "loggers": ["simplehound"], - "requirements": ["Pillow==9.5.0", "simplehound==0.3"] + "requirements": ["Pillow==10.0.0", "simplehound==0.3"] } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 672bd899962dd4..36d0a67fded4fd 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -10,6 +10,6 @@ "tf-models-official==2.5.0", "pycocotools==2.0.1", "numpy==1.23.2", - "Pillow==9.5.0" + "Pillow==10.0.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d4cdafd7ec1981..b38bd073eaf5f3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ lru-dict==1.2.0 mutagen==1.46.0 orjson==3.9.1 paho-mqtt==1.6.1 -Pillow==9.5.0 +Pillow==10.0.0 pip>=21.3.1,<23.2 psutil-home-assistant==0.0.1 PyJWT==2.7.0 diff --git a/requirements_all.txt b/requirements_all.txt index c724b59ba746aa..e990e89943529e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -45,7 +45,7 @@ Mastodon.py==1.5.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -Pillow==9.5.0 +Pillow==10.0.0 # homeassistant.components.plex PlexAPI==4.13.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0a6df91055e31..819899f96cfbaf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -38,7 +38,7 @@ HATasmota==0.6.5 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -Pillow==9.5.0 +Pillow==10.0.0 # homeassistant.components.plex PlexAPI==4.13.2 From 7f6309c5cbdef9d05ebc02d9ce8e64ddd5aca75d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 8 Jul 2023 19:55:10 +0200 Subject: [PATCH 0270/1009] Add entity translations to SkyBell (#96096) * Add entity translations to SkyBell * Add entity translations to SkyBell --- .../components/skybell/binary_sensor.py | 3 +- homeassistant/components/skybell/camera.py | 10 +++- homeassistant/components/skybell/light.py | 1 + homeassistant/components/skybell/sensor.py | 16 +++--- homeassistant/components/skybell/strings.json | 52 +++++++++++++++++++ homeassistant/components/skybell/switch.py | 6 +-- 6 files changed, 73 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/skybell/binary_sensor.py b/homeassistant/components/skybell/binary_sensor.py index 6b49307d439844..fa55b352f61149 100644 --- a/homeassistant/components/skybell/binary_sensor.py +++ b/homeassistant/components/skybell/binary_sensor.py @@ -19,12 +19,11 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( BinarySensorEntityDescription( key="button", - name="Button", + translation_key="button", device_class=BinarySensorDeviceClass.OCCUPANCY, ), BinarySensorEntityDescription( key="motion", - name="Motion", device_class=BinarySensorDeviceClass.MOTION, ), ) diff --git a/homeassistant/components/skybell/camera.py b/homeassistant/components/skybell/camera.py index b9aba0e82acba6..1e510687a0291f 100644 --- a/homeassistant/components/skybell/camera.py +++ b/homeassistant/components/skybell/camera.py @@ -17,8 +17,14 @@ from .entity import SkybellEntity CAMERA_TYPES: tuple[CameraEntityDescription, ...] = ( - CameraEntityDescription(key="activity", name="Last activity"), - CameraEntityDescription(key="avatar", name="Camera"), + CameraEntityDescription( + key="activity", + translation_key="activity", + ), + CameraEntityDescription( + key="avatar", + translation_key="camera", + ), ) diff --git a/homeassistant/components/skybell/light.py b/homeassistant/components/skybell/light.py index 311122c28e7579..70fe01fdb5e34b 100644 --- a/homeassistant/components/skybell/light.py +++ b/homeassistant/components/skybell/light.py @@ -35,6 +35,7 @@ class SkybellLight(SkybellEntity, LightEntity): _attr_color_mode = ColorMode.RGB _attr_supported_color_modes = {ColorMode.RGB} + _attr_name = None async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" diff --git a/homeassistant/components/skybell/sensor.py b/homeassistant/components/skybell/sensor.py index 4658f0f99c078f..130196a990d638 100644 --- a/homeassistant/components/skybell/sensor.py +++ b/homeassistant/components/skybell/sensor.py @@ -39,27 +39,27 @@ class SkybellSensorEntityDescription( SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( SkybellSensorEntityDescription( key="chime_level", - name="Chime level", + translation_key="chime_level", icon="mdi:bell-ring", value_fn=lambda device: device.outdoor_chime_level, ), SkybellSensorEntityDescription( key="last_button_event", - name="Last button event", + translation_key="last_button_event", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda device: device.latest("button").get(CONST.CREATED_AT), ), SkybellSensorEntityDescription( key="last_motion_event", - name="Last motion event", + translation_key="last_motion_event", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda device: device.latest("motion").get(CONST.CREATED_AT), ), SkybellSensorEntityDescription( key=CONST.ATTR_LAST_CHECK_IN, - name="Last check in", + translation_key="last_check_in", icon="mdi:clock", entity_registry_enabled_default=False, device_class=SensorDeviceClass.TIMESTAMP, @@ -68,7 +68,7 @@ class SkybellSensorEntityDescription( ), SkybellSensorEntityDescription( key="motion_threshold", - name="Motion threshold", + translation_key="motion_threshold", icon="mdi:walk", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -76,14 +76,14 @@ class SkybellSensorEntityDescription( ), SkybellSensorEntityDescription( key="video_profile", - name="Video profile", + translation_key="video_profile", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.video_profile, ), SkybellSensorEntityDescription( key=CONST.ATTR_WIFI_SSID, - name="Wifi SSID", + translation_key="wifi_ssid", icon="mdi:wifi-settings", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -91,7 +91,7 @@ class SkybellSensorEntityDescription( ), SkybellSensorEntityDescription( key=CONST.ATTR_WIFI_STATUS, - name="Wifi status", + translation_key="wifi_status", icon="mdi:wifi-strength-3", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, diff --git a/homeassistant/components/skybell/strings.json b/homeassistant/components/skybell/strings.json index 4289c3ed3c3a66..28a66df2d02c9b 100644 --- a/homeassistant/components/skybell/strings.json +++ b/homeassistant/components/skybell/strings.json @@ -24,5 +24,57 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "entity": { + "binary_sensor": { + "button": { + "name": "Button" + } + }, + "camera": { + "activity": { + "name": "Last activity" + }, + "camera": { + "name": "[%key:component::camera::title%]" + } + }, + "sensor": { + "chime_level": { + "name": "Chime level" + }, + "last_button_event": { + "name": "Last button event" + }, + "last_motion_event": { + "name": "Last motion event" + }, + "last_check_in": { + "name": "Last check in" + }, + "motion_threshold": { + "name": "Motion threshold" + }, + "video_profile": { + "name": "Video profile" + }, + "wifi_ssid": { + "name": "Wi-Fi SSID" + }, + "wifi_status": { + "name": "Wi-Fi status" + } + }, + "switch": { + "do_not_disturb": { + "name": "Do not disturb" + }, + "do_not_ring": { + "name": "Do not ring" + }, + "motion_sensor": { + "name": "Motion sensor" + } + } } } diff --git a/homeassistant/components/skybell/switch.py b/homeassistant/components/skybell/switch.py index b3cb8c53032a47..f67cca41ac9032 100644 --- a/homeassistant/components/skybell/switch.py +++ b/homeassistant/components/skybell/switch.py @@ -14,15 +14,15 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SwitchEntityDescription( key="do_not_disturb", - name="Do not disturb", + translation_key="do_not_disturb", ), SwitchEntityDescription( key="do_not_ring", - name="Do not ring", + translation_key="do_not_ring", ), SwitchEntityDescription( key="motion_sensor", - name="Motion sensor", + translation_key="motion_sensor", ), ) From d37ac5ace99a25bbcc22394d6fb06329e60bb0a8 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 8 Jul 2023 20:03:02 +0200 Subject: [PATCH 0271/1009] 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 799949a462ade8..7ced3487269366 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 fde08d08fbd5ef..397a9cc8db1909 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 2f5ff808a0fb5464c72b3a468941cb5207ddd52c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 8 Jul 2023 20:03:19 +0200 Subject: [PATCH 0272/1009] Add dim to full state service for Sensibo (#96152) Add dim to full state service --- homeassistant/components/sensibo/services.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensibo/services.yaml b/homeassistant/components/sensibo/services.yaml index f9f9365eb8e762..fbd2625961bc92 100644 --- a/homeassistant/components/sensibo/services.yaml +++ b/homeassistant/components/sensibo/services.yaml @@ -146,6 +146,7 @@ full_state: options: - "on" - "off" + - "dim" enable_climate_react: name: Enable Climate React description: Enable and configure Climate React From b2bf36029705bbcbb1670e4a6d6ccfc2b5714f01 Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Sat, 8 Jul 2023 11:27:25 -0700 Subject: [PATCH 0273/1009] Update holidays to 0.28 (#95091) Bump Python Holidays version to 0.28 Set `language` from country's default language for holidays objects. --- homeassistant/components/workday/binary_sensor.py | 15 ++++++++------- homeassistant/components/workday/config_flow.py | 11 +++++++---- homeassistant/components/workday/manifest.json | 9 ++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 51560161faa8fa..0814958ad27d23 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -120,15 +120,16 @@ async def async_setup_entry( sensor_name: str = entry.options[CONF_NAME] workdays: list[str] = entry.options[CONF_WORKDAYS] + cls: HolidayBase = getattr(holidays, country) year: int = (dt_util.now() + timedelta(days=days_offset)).year - obj_holidays: HolidayBase = getattr(holidays, country)(years=year) - if province: - try: - obj_holidays = getattr(holidays, country)(subdiv=province, years=year) - except NotImplementedError: - LOGGER.error("There is no subdivision %s in country %s", province, country) - return + if province and province not in cls.subdivisions: + LOGGER.error("There is no subdivision %s in country %s", province, country) + return + + obj_holidays = cls( + subdiv=province, years=year, language=cls.default_language + ) # type: ignore[operator] # Add custom holidays try: diff --git a/homeassistant/components/workday/config_flow.py b/homeassistant/components/workday/config_flow.py index 7153dac1bcba73..15e04ffca93e3a 100644 --- a/homeassistant/components/workday/config_flow.py +++ b/homeassistant/components/workday/config_flow.py @@ -3,7 +3,8 @@ from typing import Any -from holidays import country_holidays, list_supported_countries +import holidays +from holidays import HolidayBase, list_supported_countries import voluptuous as vol from homeassistant.config_entries import ( @@ -76,10 +77,12 @@ def validate_custom_dates(user_input: dict[str, Any]) -> None: if dt_util.parse_date(add_date) is None: raise AddDatesError("Incorrect date") + cls: HolidayBase = getattr(holidays, user_input[CONF_COUNTRY]) year: int = dt_util.now().year - obj_holidays = country_holidays( - user_input[CONF_COUNTRY], user_input.get(CONF_PROVINCE), year - ) + + obj_holidays = cls( + subdiv=user_input.get(CONF_PROVINCE), years=year, language=cls.default_language + ) # type: ignore[operator] for remove_date in user_input[CONF_REMOVE_HOLIDAYS]: if dt_util.parse_date(remove_date) is None: diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index e018eaa588ee95..698ef17902fdc8 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -5,12 +5,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/workday", "iot_class": "local_polling", - "loggers": [ - "convertdate", - "hijri_converter", - "holidays", - "korean_lunar_calendar" - ], + "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.21.13"] + "requirements": ["holidays==0.28"] } diff --git a/requirements_all.txt b/requirements_all.txt index e990e89943529e..70285de2588617 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -977,7 +977,7 @@ hlk-sw16==0.0.9 hole==0.8.0 # homeassistant.components.workday -holidays==0.21.13 +holidays==0.28 # homeassistant.components.frontend home-assistant-frontend==20230705.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 819899f96cfbaf..f712da5d6edf6e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -760,7 +760,7 @@ hlk-sw16==0.0.9 hole==0.8.0 # homeassistant.components.workday -holidays==0.21.13 +holidays==0.28 # homeassistant.components.frontend home-assistant-frontend==20230705.1 From 4b1d096e6bf99b6f9a7745cd1392f511a1814b75 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Sat, 8 Jul 2023 16:00:22 -0300 Subject: [PATCH 0274/1009] Add `device_class` and `state_class` in config flow for SQL (#95020) * Add device_class and state_class in config flow for SQL * Update when selected NONE_SENTINEL * Add tests * Use SensorDeviceClass and SensorStateClass in tests * Add volatile_organic_compounds_parts in strings selector * Add test_attributes_from_entry_config * Remove test_attributes_from_entry_config and complement test_device_state_class * Add test_attributes_from_entry_config in test_sensor.py --- homeassistant/components/sql/config_flow.py | 50 +++++++++++- homeassistant/components/sql/sensor.py | 6 +- homeassistant/components/sql/strings.json | 79 +++++++++++++++++- tests/components/sql/__init__.py | 2 + tests/components/sql/test_config_flow.py | 90 +++++++++++++++++++++ tests/components/sql/test_sensor.py | 44 ++++++++++ 6 files changed, 264 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sql/config_flow.py b/homeassistant/components/sql/config_flow.py index a6c526a6a7f416..bd0a6d30369e7d 100644 --- a/homeassistant/components/sql/config_flow.py +++ b/homeassistant/components/sql/config_flow.py @@ -12,7 +12,17 @@ from homeassistant import config_entries from homeassistant.components.recorder import CONF_DB_URL, get_instance -from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE +from homeassistant.components.sensor import ( + CONF_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, +) from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector @@ -22,6 +32,8 @@ _LOGGER = logging.getLogger(__name__) +NONE_SENTINEL = "none" + OPTIONS_SCHEMA: vol.Schema = vol.Schema( { vol.Optional( @@ -39,6 +51,34 @@ vol.Optional( CONF_VALUE_TEMPLATE, ): selector.TemplateSelector(), + vol.Optional( + CONF_DEVICE_CLASS, + default=NONE_SENTINEL, + ): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[NONE_SENTINEL] + + sorted( + [ + cls.value + for cls in SensorDeviceClass + if cls != SensorDeviceClass.ENUM + ] + ), + mode=selector.SelectSelectorMode.DROPDOWN, + translation_key="device_class", + ) + ), + vol.Optional( + CONF_STATE_CLASS, + default=NONE_SENTINEL, + ): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[NONE_SENTINEL] + + sorted([cls.value for cls in SensorStateClass]), + mode=selector.SelectSelectorMode.DROPDOWN, + translation_key="state_class", + ) + ), } ) @@ -139,6 +179,10 @@ async def async_step_user( options[CONF_UNIT_OF_MEASUREMENT] = uom if value_template := user_input.get(CONF_VALUE_TEMPLATE): options[CONF_VALUE_TEMPLATE] = value_template + if (device_class := user_input[CONF_DEVICE_CLASS]) != NONE_SENTINEL: + options[CONF_DEVICE_CLASS] = device_class + if (state_class := user_input[CONF_STATE_CLASS]) != NONE_SENTINEL: + options[CONF_STATE_CLASS] = state_class if db_url_for_validation != get_instance(self.hass).db_url: options[CONF_DB_URL] = db_url_for_validation @@ -204,6 +248,10 @@ async def async_step_init( options[CONF_UNIT_OF_MEASUREMENT] = uom if value_template := user_input.get(CONF_VALUE_TEMPLATE): options[CONF_VALUE_TEMPLATE] = value_template + if (device_class := user_input[CONF_DEVICE_CLASS]) != NONE_SENTINEL: + options[CONF_DEVICE_CLASS] = device_class + if (state_class := user_input[CONF_STATE_CLASS]) != NONE_SENTINEL: + options[CONF_STATE_CLASS] = state_class if db_url_for_validation != get_instance(self.hass).db_url: options[CONF_DB_URL] = db_url_for_validation diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 2a8ea80580bba9..96fc4bc943a227 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -101,6 +101,8 @@ async def async_setup_entry( unit: str | None = entry.options.get(CONF_UNIT_OF_MEASUREMENT) template: str | None = entry.options.get(CONF_VALUE_TEMPLATE) column_name: str = entry.options[CONF_COLUMN_NAME] + device_class: SensorDeviceClass | None = entry.options.get(CONF_DEVICE_CLASS, None) + state_class: SensorStateClass | None = entry.options.get(CONF_STATE_CLASS, None) value_template: Template | None = None if template is not None: @@ -122,8 +124,8 @@ async def async_setup_entry( entry.entry_id, db_url, False, - None, - None, + device_class, + state_class, async_add_entities, ) diff --git a/homeassistant/components/sql/strings.json b/homeassistant/components/sql/strings.json index 6888652cb4c66f..74c165e9d20635 100644 --- a/homeassistant/components/sql/strings.json +++ b/homeassistant/components/sql/strings.json @@ -16,7 +16,9 @@ "query": "Select Query", "column": "Column", "unit_of_measurement": "Unit of Measure", - "value_template": "Value Template" + "value_template": "Value Template", + "device_class": "Device Class", + "state_class": "State Class" }, "data_description": { "db_url": "Database URL, leave empty to use HA recorder database", @@ -24,7 +26,9 @@ "query": "Query to run, needs to start with 'SELECT'", "column": "Column for returned query to present as state", "unit_of_measurement": "Unit of Measure (optional)", - "value_template": "Value Template (optional)" + "value_template": "Value Template (optional)", + "device_class": "The type/class of the sensor to set the icon in the frontend", + "state_class": "The state_class of the sensor" } } } @@ -38,7 +42,9 @@ "query": "[%key:component::sql::config::step::user::data::query%]", "column": "[%key:component::sql::config::step::user::data::column%]", "unit_of_measurement": "[%key:component::sql::config::step::user::data::unit_of_measurement%]", - "value_template": "[%key:component::sql::config::step::user::data::value_template%]" + "value_template": "[%key:component::sql::config::step::user::data::value_template%]", + "device_class": "[%key:component::sql::config::step::user::data::device_class%]", + "state_class": "[%key:component::sql::config::step::user::data::state_class%]" }, "data_description": { "db_url": "[%key:component::sql::config::step::user::data_description::db_url%]", @@ -46,7 +52,9 @@ "query": "[%key:component::sql::config::step::user::data_description::query%]", "column": "[%key:component::sql::config::step::user::data_description::column%]", "unit_of_measurement": "[%key:component::sql::config::step::user::data_description::unit_of_measurement%]", - "value_template": "[%key:component::sql::config::step::user::data_description::value_template%]" + "value_template": "[%key:component::sql::config::step::user::data_description::value_template%]", + "device_class": "[%key:component::sql::config::step::user::data_description::device_class%]", + "state_class": "[%key:component::sql::config::step::user::data_description::state_class%]" } } }, @@ -56,6 +64,69 @@ "column_invalid": "[%key:component::sql::config::error::column_invalid%]" } }, + "selector": { + "device_class": { + "options": { + "none": "No device class", + "date": "[%key:component::sensor::entity_component::date::name%]", + "duration": "[%key:component::sensor::entity_component::duration::name%]", + "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", + "aqi": "[%key:component::sensor::entity_component::aqi::name%]", + "atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]", + "battery": "[%key:component::sensor::entity_component::battery::name%]", + "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", + "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", + "current": "[%key:component::sensor::entity_component::current::name%]", + "data_rate": "[%key:component::sensor::entity_component::data_rate::name%]", + "data_size": "[%key:component::sensor::entity_component::data_size::name%]", + "distance": "[%key:component::sensor::entity_component::distance::name%]", + "energy": "[%key:component::sensor::entity_component::energy::name%]", + "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", + "frequency": "[%key:component::sensor::entity_component::frequency::name%]", + "gas": "[%key:component::sensor::entity_component::gas::name%]", + "humidity": "[%key:component::sensor::entity_component::humidity::name%]", + "illuminance": "[%key:component::sensor::entity_component::illuminance::name%]", + "irradiance": "[%key:component::sensor::entity_component::irradiance::name%]", + "moisture": "[%key:component::sensor::entity_component::moisture::name%]", + "monetary": "[%key:component::sensor::entity_component::monetary::name%]", + "nitrogen_dioxide": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]", + "nitrogen_monoxide": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]", + "nitrous_oxide": "[%key:component::sensor::entity_component::nitrous_oxide::name%]", + "ozone": "[%key:component::sensor::entity_component::ozone::name%]", + "pm1": "[%key:component::sensor::entity_component::pm1::name%]", + "pm10": "[%key:component::sensor::entity_component::pm10::name%]", + "pm25": "[%key:component::sensor::entity_component::pm25::name%]", + "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", + "power": "[%key:component::sensor::entity_component::power::name%]", + "precipitation": "[%key:component::sensor::entity_component::precipitation::name%]", + "precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]", + "pressure": "[%key:component::sensor::entity_component::pressure::name%]", + "reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]", + "signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]", + "sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]", + "speed": "[%key:component::sensor::entity_component::speed::name%]", + "sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]", + "temperature": "[%key:component::sensor::entity_component::temperature::name%]", + "timestamp": "[%key:component::sensor::entity_component::timestamp::name%]", + "volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", + "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]", + "voltage": "[%key:component::sensor::entity_component::voltage::name%]", + "volume": "[%key:component::sensor::entity_component::volume::name%]", + "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", + "water": "[%key:component::sensor::entity_component::water::name%]", + "weight": "[%key:component::sensor::entity_component::weight::name%]", + "wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]" + } + }, + "state_class": { + "options": { + "none": "No state class", + "measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]", + "total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]", + "total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]" + } + } + }, "issues": { "entity_id_query_does_full_table_scan": { "title": "SQL query does full table scan", diff --git a/tests/components/sql/__init__.py b/tests/components/sql/__init__.py index 9927a9734cd6e6..a1417cd38dfc71 100644 --- a/tests/components/sql/__init__.py +++ b/tests/components/sql/__init__.py @@ -27,6 +27,8 @@ CONF_QUERY: "SELECT 5 as value", CONF_COLUMN_NAME: "value", CONF_UNIT_OF_MEASUREMENT: "MiB", + CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE, + CONF_STATE_CLASS: SensorStateClass.TOTAL, } ENTRY_CONFIG_WITH_VALUE_TEMPLATE = { diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index 8958454ac62cd7..915394863ead25 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -7,6 +7,8 @@ from homeassistant import config_entries from homeassistant.components.recorder import Recorder +from homeassistant.components.sensor.const import SensorDeviceClass, SensorStateClass +from homeassistant.components.sql.config_flow import NONE_SENTINEL from homeassistant.components.sql.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -50,6 +52,8 @@ async def test_form(recorder_mock: Recorder, hass: HomeAssistant) -> None: "query": "SELECT 5 as value", "column": "value", "unit_of_measurement": "MiB", + "device_class": SensorDeviceClass.DATA_SIZE, + "state_class": SensorStateClass.TOTAL, } assert len(mock_setup_entry.mock_calls) == 1 @@ -151,6 +155,8 @@ async def test_flow_fails_invalid_query( "query": "SELECT 5 as value", "column": "value", "unit_of_measurement": "MiB", + "device_class": SensorDeviceClass.DATA_SIZE, + "state_class": SensorStateClass.TOTAL, } @@ -187,6 +193,8 @@ async def test_flow_fails_invalid_column_name( "query": "SELECT 5 as value", "column": "value", "unit_of_measurement": "MiB", + "device_class": SensorDeviceClass.DATA_SIZE, + "state_class": SensorStateClass.TOTAL, } @@ -201,6 +209,8 @@ async def test_options_flow(recorder_mock: Recorder, hass: HomeAssistant) -> Non "query": "SELECT 5 as value", "column": "value", "unit_of_measurement": "MiB", + "device_class": SensorDeviceClass.DATA_SIZE, + "state_class": SensorStateClass.TOTAL, }, ) entry.add_to_hass(hass) @@ -225,6 +235,8 @@ async def test_options_flow(recorder_mock: Recorder, hass: HomeAssistant) -> Non "column": "size", "unit_of_measurement": "MiB", "value_template": "{{ value }}", + "device_class": SensorDeviceClass.DATA_SIZE, + "state_class": SensorStateClass.TOTAL, }, ) @@ -235,6 +247,8 @@ async def test_options_flow(recorder_mock: Recorder, hass: HomeAssistant) -> Non "column": "size", "unit_of_measurement": "MiB", "value_template": "{{ value }}", + "device_class": SensorDeviceClass.DATA_SIZE, + "state_class": SensorStateClass.TOTAL, } @@ -594,3 +608,79 @@ async def test_full_flow_not_recorder_db( "column": "value", "unit_of_measurement": "MB", } + + +async def test_device_state_class(recorder_mock: Recorder, hass: HomeAssistant) -> None: + """Test we get the form.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + "name": "Get Value", + "query": "SELECT 5 as value", + "column": "value", + "unit_of_measurement": "MiB", + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + + with patch( + "homeassistant.components.sql.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "query": "SELECT 5 as value", + "column": "value", + "unit_of_measurement": "MiB", + "device_class": SensorDeviceClass.DATA_SIZE, + "state_class": SensorStateClass.TOTAL, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["data"] == { + "name": "Get Value", + "query": "SELECT 5 as value", + "column": "value", + "unit_of_measurement": "MiB", + "device_class": SensorDeviceClass.DATA_SIZE, + "state_class": SensorStateClass.TOTAL, + } + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + + with patch( + "homeassistant.components.sql.async_setup_entry", + return_value=True, + ): + result3 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "query": "SELECT 5 as value", + "column": "value", + "unit_of_measurement": "MiB", + "device_class": NONE_SENTINEL, + "state_class": NONE_SENTINEL, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert "device_class" not in result3["data"] + assert "state_class" not in result3["data"] + assert result3["data"] == { + "name": "Get Value", + "query": "SELECT 5 as value", + "column": "value", + "unit_of_measurement": "MiB", + } diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index a6aa18c9294607..0fe0e881c95381 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -457,3 +457,47 @@ async def test_engine_is_disposed_at_stop( await hass.async_stop() assert mock_engine_dispose.call_count == 2 + + +async def test_attributes_from_entry_config( + recorder_mock: Recorder, hass: HomeAssistant +) -> None: + """Test attributes from entry config.""" + + await init_integration( + hass, + config={ + "name": "Get Value - With", + "query": "SELECT 5 as value", + "column": "value", + "unit_of_measurement": "MiB", + "device_class": SensorDeviceClass.DATA_SIZE, + "state_class": SensorStateClass.TOTAL, + }, + entry_id="8693d4782ced4fb1ecca4743f29ab8f1", + ) + + state = hass.states.get("sensor.get_value_with") + assert state.state == "5" + assert state.attributes["value"] == 5 + assert state.attributes["unit_of_measurement"] == "MiB" + assert state.attributes["device_class"] == SensorDeviceClass.DATA_SIZE + assert state.attributes["state_class"] == SensorStateClass.TOTAL + + await init_integration( + hass, + config={ + "name": "Get Value - Without", + "query": "SELECT 5 as value", + "column": "value", + "unit_of_measurement": "MiB", + }, + entry_id="7aec7cd8045fba4778bb0621469e3cd9", + ) + + state = hass.states.get("sensor.get_value_without") + assert state.state == "5" + assert state.attributes["value"] == 5 + assert state.attributes["unit_of_measurement"] == "MiB" + assert "device_class" not in state.attributes + assert "state_class" not in state.attributes From c27a014a0ad071734e6753b5b0e243a226952e0d Mon Sep 17 00:00:00 2001 From: Patrick ZAJDA Date: Sat, 8 Jul 2023 21:13:32 +0200 Subject: [PATCH 0275/1009] Use device name for Nuki door sensor (#95904) Explicitly set Nuki door sensor name to None Signed-off-by: Patrick ZAJDA --- homeassistant/components/nuki/binary_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py index 2b3006eeb3bd76..86c7f8343dfbee 100644 --- a/homeassistant/components/nuki/binary_sensor.py +++ b/homeassistant/components/nuki/binary_sensor.py @@ -36,6 +36,7 @@ class NukiDoorsensorEntity(NukiEntity[NukiDevice], BinarySensorEntity): """Representation of a Nuki Lock Doorsensor.""" _attr_has_entity_name = True + _attr_name = None _attr_device_class = BinarySensorDeviceClass.DOOR @property From 2ebc265184d6ed4b74715d003f64c4d4341fd48a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 8 Jul 2023 21:23:11 +0200 Subject: [PATCH 0276/1009] Bump pysensibo to 1.0.31 (#96154) --- homeassistant/components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sensibo/test_climate.py | 10 +++++----- tests/components/sensibo/test_sensor.py | 8 ++++---- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index f99792f7dc1d55..f90b887d04c11c 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -15,5 +15,5 @@ "iot_class": "cloud_polling", "loggers": ["pysensibo"], "quality_scale": "platinum", - "requirements": ["pysensibo==1.0.28"] + "requirements": ["pysensibo==1.0.31"] } diff --git a/requirements_all.txt b/requirements_all.txt index 70285de2588617..3197a1c60b3559 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1971,7 +1971,7 @@ pysabnzbd==1.1.1 pysaj==0.0.16 # homeassistant.components.sensibo -pysensibo==1.0.28 +pysensibo==1.0.31 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f712da5d6edf6e..5b7464db638fe3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1463,7 +1463,7 @@ pyrympro==0.0.7 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.28 +pysensibo==1.0.31 # homeassistant.components.serial # homeassistant.components.zha diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index b2108d3e6f4c04..4e856d396c1b64 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -86,21 +86,21 @@ async def test_climate( assert state1.state == "heat" assert state1.attributes == { "hvac_modes": [ + "heat_cool", "cool", - "heat", "dry", - "heat_cool", "fan_only", + "heat", "off", ], "min_temp": 10, "max_temp": 20, "target_temp_step": 1, - "fan_modes": ["quiet", "low", "medium"], + "fan_modes": ["low", "medium", "quiet"], "swing_modes": [ - "stopped", - "fixedtop", "fixedmiddletop", + "fixedtop", + "stopped", ], "current_temperature": 21.2, "temperature": 25, diff --git a/tests/components/sensibo/test_sensor.py b/tests/components/sensibo/test_sensor.py index 003c2f27903d48..24dbdef1fe3bde 100644 --- a/tests/components/sensibo/test_sensor.py +++ b/tests/components/sensibo/test_sensor.py @@ -44,12 +44,12 @@ async def test_sensor( "state_class": "measurement", "unit_of_measurement": "°C", "on": True, - "targetTemperature": 21, - "temperatureUnit": "C", + "targettemperature": 21, + "temperatureunit": "c", "mode": "heat", - "fanLevel": "low", + "fanlevel": "low", "swing": "stopped", - "horizontalSwing": "stopped", + "horizontalswing": "stopped", "light": "on", } From 18314b09f681f78d6b23de15f803325e08e380c5 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Sat, 8 Jul 2023 21:23:25 +0200 Subject: [PATCH 0277/1009] 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 91f4940a4e57c9..b38c1d3829b367 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 3197a1c60b3559..4e4e439217d85d 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 5b7464db638fe3..cabbc0768c23eb 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 6758292655a779cc6be203ea2654e52e452801b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Jul 2023 21:42:48 -1000 Subject: [PATCH 0278/1009] Add bthome logbook platform (#96171) --- homeassistant/components/bthome/logbook.py | 43 +++++++++++++++ tests/components/bthome/test_logbook.py | 64 ++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 homeassistant/components/bthome/logbook.py create mode 100644 tests/components/bthome/test_logbook.py diff --git a/homeassistant/components/bthome/logbook.py b/homeassistant/components/bthome/logbook.py new file mode 100644 index 00000000000000..703ad671799f82 --- /dev/null +++ b/homeassistant/components/bthome/logbook.py @@ -0,0 +1,43 @@ +"""Describe bthome logbook events.""" +from __future__ import annotations + +from collections.abc import Callable +from typing import TYPE_CHECKING, cast + +from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers.device_registry import async_get + +from .const import ( + BTHOME_BLE_EVENT, + DOMAIN, + BTHomeBleEvent, +) + + +@callback +def async_describe_events( + hass: HomeAssistant, + async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None], +) -> None: + """Describe logbook events.""" + dr = async_get(hass) + + @callback + def async_describe_bthome_event(event: Event) -> dict[str, str]: + """Describe bthome logbook event.""" + data = event.data + if TYPE_CHECKING: + data = cast(BTHomeBleEvent, data) # type: ignore[assignment] + device = dr.async_get(data["device_id"]) + name = device and device.name or f'BTHome {data["address"]}' + if properties := data["event_properties"]: + message = f"{data['event_class']} {data['event_type']}: {properties}" + else: + message = f"{data['event_class']} {data['event_type']}" + return { + LOGBOOK_ENTRY_NAME: name, + LOGBOOK_ENTRY_MESSAGE: message, + } + + async_describe_event(DOMAIN, BTHOME_BLE_EVENT, async_describe_bthome_event) diff --git a/tests/components/bthome/test_logbook.py b/tests/components/bthome/test_logbook.py new file mode 100644 index 00000000000000..f68197f9fe5cbb --- /dev/null +++ b/tests/components/bthome/test_logbook.py @@ -0,0 +1,64 @@ +"""The tests for bthome logbook.""" +from homeassistant.components.bthome.const import ( + BTHOME_BLE_EVENT, + DOMAIN, + BTHomeBleEvent, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry +from tests.components.logbook.common import MockRow, mock_humanify + + +async def test_humanify_bthome_event(hass: HomeAssistant) -> None: + """Test humanifying bthome button presses.""" + hass.config.components.add("recorder") + assert await async_setup_component(hass, "logbook", {}) + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="A4:C1:38:8D:18:B2", + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + (event1, event2) = mock_humanify( + hass, + [ + MockRow( + BTHOME_BLE_EVENT, + dict( + BTHomeBleEvent( + device_id=None, + address="A4:C1:38:8D:18:B2", + event_class="button", + event_type="long_press", + event_properties={ + "any": "thing", + }, + ) + ), + ), + MockRow( + BTHOME_BLE_EVENT, + dict( + BTHomeBleEvent( + device_id=None, + address="A4:C1:38:8D:18:B2", + event_class="button", + event_type="press", + event_properties=None, + ) + ), + ), + ], + ) + + assert event1["name"] == "BTHome A4:C1:38:8D:18:B2" + assert event1["domain"] == DOMAIN + assert event1["message"] == "button long_press: {'any': 'thing'}" + + assert event2["name"] == "BTHome A4:C1:38:8D:18:B2" + assert event2["domain"] == DOMAIN + assert event2["message"] == "button press" From 479015244d0fab923e5069e8f12b87a2ef61f123 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 9 Jul 2023 12:00:51 +0200 Subject: [PATCH 0279/1009] KNX Cover: Use absolute tilt position if available (#96192) --- homeassistant/components/knx/cover.py | 10 ++++- tests/components/knx/test_cover.py | 59 ++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 29bd9b4f6a9fbb..9e86fc8b36e4e8 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -163,11 +163,17 @@ async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" - await self._device.set_short_up() + if self._device.angle.writable: + await self._device.set_angle(0) + else: + await self._device.set_short_up() async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" - await self._device.set_short_down() + if self._device.angle.writable: + await self._device.set_angle(100) + else: + await self._device.set_short_down() async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the cover tilt.""" diff --git a/tests/components/knx/test_cover.py b/tests/components/knx/test_cover.py index 4ee9bd04eee653..2d2b72e90153d6 100644 --- a/tests/components/knx/test_cover.py +++ b/tests/components/knx/test_cover.py @@ -19,8 +19,6 @@ async def test_cover_basic(hass: HomeAssistant, knx: KNXTestKit) -> None: CoverSchema.CONF_MOVE_SHORT_ADDRESS: "1/0/1", CoverSchema.CONF_POSITION_STATE_ADDRESS: "1/0/2", CoverSchema.CONF_POSITION_ADDRESS: "1/0/3", - CoverSchema.CONF_ANGLE_STATE_ADDRESS: "1/0/4", - CoverSchema.CONF_ANGLE_ADDRESS: "1/0/5", } } ) @@ -28,10 +26,8 @@ async def test_cover_basic(hass: HomeAssistant, knx: KNXTestKit) -> None: # read position state address and angle state address await knx.assert_read("1/0/2") - await knx.assert_read("1/0/4") # StateUpdater initialize state await knx.receive_response("1/0/2", (0x0F,)) - await knx.receive_response("1/0/4", (0x30,)) events.clear() # open cover @@ -82,6 +78,32 @@ async def test_cover_basic(hass: HomeAssistant, knx: KNXTestKit) -> None: assert len(events) == 1 events.pop() + +async def test_cover_tilt_absolute(hass: HomeAssistant, knx: KNXTestKit) -> None: + """Test KNX cover tilt.""" + await knx.setup_integration( + { + CoverSchema.PLATFORM: { + CONF_NAME: "test", + CoverSchema.CONF_MOVE_LONG_ADDRESS: "1/0/0", + CoverSchema.CONF_MOVE_SHORT_ADDRESS: "1/0/1", + CoverSchema.CONF_POSITION_STATE_ADDRESS: "1/0/2", + CoverSchema.CONF_POSITION_ADDRESS: "1/0/3", + CoverSchema.CONF_ANGLE_STATE_ADDRESS: "1/0/4", + CoverSchema.CONF_ANGLE_ADDRESS: "1/0/5", + } + } + ) + events = async_capture_events(hass, "state_changed") + + # read position state address and angle state address + await knx.assert_read("1/0/2") + await knx.assert_read("1/0/4") + # StateUpdater initialize state + await knx.receive_response("1/0/2", (0x0F,)) + await knx.receive_response("1/0/4", (0x30,)) + events.clear() + # set cover tilt position await hass.services.async_call( "cover", @@ -102,7 +124,7 @@ async def test_cover_basic(hass: HomeAssistant, knx: KNXTestKit) -> None: await hass.services.async_call( "cover", "close_cover_tilt", target={"entity_id": "cover.test"}, blocking=True ) - await knx.assert_write("1/0/1", True) + await knx.assert_write("1/0/5", (0xFF,)) assert len(events) == 1 events.pop() @@ -111,4 +133,29 @@ async def test_cover_basic(hass: HomeAssistant, knx: KNXTestKit) -> None: await hass.services.async_call( "cover", "open_cover_tilt", target={"entity_id": "cover.test"}, blocking=True ) - await knx.assert_write("1/0/1", False) + await knx.assert_write("1/0/5", (0x00,)) + + +async def test_cover_tilt_move_short(hass: HomeAssistant, knx: KNXTestKit) -> None: + """Test KNX cover tilt.""" + await knx.setup_integration( + { + CoverSchema.PLATFORM: { + CONF_NAME: "test", + CoverSchema.CONF_MOVE_LONG_ADDRESS: "1/0/0", + CoverSchema.CONF_MOVE_SHORT_ADDRESS: "1/0/1", + } + } + ) + + # close cover tilt + await hass.services.async_call( + "cover", "close_cover_tilt", target={"entity_id": "cover.test"}, blocking=True + ) + await knx.assert_write("1/0/1", 1) + + # open cover tilt + await hass.services.async_call( + "cover", "open_cover_tilt", target={"entity_id": "cover.test"}, blocking=True + ) + await knx.assert_write("1/0/1", 0) From 18dddd63423e5d998bfcdc5e6bb6fb3d301ec12b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 9 Jul 2023 16:10:23 +0200 Subject: [PATCH 0280/1009] Update Ruff to v0.0.277 (#96108) --- .pre-commit-config.yaml | 2 +- homeassistant/components/amcrest/__init__.py | 7 ++- homeassistant/components/auth/indieauth.py | 17 +++--- requirements_test_pre_commit.txt | 2 +- tests/util/test_timeout.py | 62 ++++++++++---------- 5 files changed, 45 insertions(+), 45 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c662c6754f4c79..f85f8583a0425c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.272 + rev: v0.0.277 hooks: - id: ruff args: diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index 8fea717e6bb1e2..ce07741c37fb4a 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -210,9 +210,10 @@ async def async_stream_command( self, *args: Any, **kwargs: Any ) -> AsyncIterator[httpx.Response]: """amcrest.ApiWrapper.command wrapper to catch errors.""" - async with self._async_command_wrapper(): - async with super().async_stream_command(*args, **kwargs) as ret: - yield ret + async with self._async_command_wrapper(), super().async_stream_command( + *args, **kwargs + ) as ret: + yield ret @asynccontextmanager async def _async_command_wrapper(self) -> AsyncIterator[None]: diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index ec8431366ab2e2..e2614af6a3efba 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -92,14 +92,15 @@ async def fetch_redirect_uris(hass: HomeAssistant, url: str) -> list[str]: parser = LinkTagParser("redirect_uri") chunks = 0 try: - async with aiohttp.ClientSession() as session: - async with session.get(url, timeout=5) as resp: - async for data in resp.content.iter_chunked(1024): - parser.feed(data.decode()) - chunks += 1 - - if chunks == 10: - break + async with aiohttp.ClientSession() as session, session.get( + url, timeout=5 + ) as resp: + async for data in resp.content.iter_chunked(1024): + parser.feed(data.decode()) + chunks += 1 + + if chunks == 10: + break except asyncio.TimeoutError: _LOGGER.error("Timeout while looking up redirect_uri %s", url) diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index eff26bcfe82e74..4047daf73cf324 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -2,5 +2,5 @@ black==23.3.0 codespell==2.2.2 -ruff==0.0.272 +ruff==0.0.277 yamllint==1.28.0 diff --git a/tests/util/test_timeout.py b/tests/util/test_timeout.py index e89c6cd3f022f4..f301cd3c634188 100644 --- a/tests/util/test_timeout.py +++ b/tests/util/test_timeout.py @@ -31,9 +31,8 @@ async def test_simple_global_timeout_freeze() -> None: """Test a simple global timeout freeze.""" timeout = TimeoutManager() - async with timeout.async_timeout(0.2): - async with timeout.async_freeze(): - await asyncio.sleep(0.3) + async with timeout.async_timeout(0.2), timeout.async_freeze(): + await asyncio.sleep(0.3) async def test_simple_zone_timeout_freeze_inside_executor_job( @@ -46,9 +45,10 @@ def _some_sync_work(): with timeout.freeze("recorder"): time.sleep(0.3) - async with timeout.async_timeout(1.0): - async with timeout.async_timeout(0.2, zone_name="recorder"): - await hass.async_add_executor_job(_some_sync_work) + async with timeout.async_timeout(1.0), timeout.async_timeout( + 0.2, zone_name="recorder" + ): + await hass.async_add_executor_job(_some_sync_work) async def test_simple_global_timeout_freeze_inside_executor_job( @@ -75,9 +75,10 @@ def _some_sync_work(): with timeout.freeze("recorder"): time.sleep(0.3) - async with timeout.async_timeout(0.1): - async with timeout.async_timeout(0.2, zone_name="recorder"): - await hass.async_add_executor_job(_some_sync_work) + async with timeout.async_timeout(0.1), timeout.async_timeout( + 0.2, zone_name="recorder" + ): + await hass.async_add_executor_job(_some_sync_work) async def test_mix_global_timeout_freeze_and_zone_freeze_different_order( @@ -108,9 +109,10 @@ def _some_sync_work(): with pytest.raises(asyncio.TimeoutError): async with timeout.async_timeout(0.1): - async with timeout.async_timeout(0.2, zone_name="recorder"): - async with timeout.async_timeout(0.2, zone_name="not_recorder"): - await hass.async_add_executor_job(_some_sync_work) + async with timeout.async_timeout( + 0.2, zone_name="recorder" + ), timeout.async_timeout(0.2, zone_name="not_recorder"): + await hass.async_add_executor_job(_some_sync_work) async def test_mix_global_timeout_freeze_and_zone_freeze_inside_executor_job_second_job_outside_zone_context( @@ -136,9 +138,8 @@ async def test_simple_global_timeout_freeze_with_executor_job( """Test a simple global timeout freeze with executor job.""" timeout = TimeoutManager() - async with timeout.async_timeout(0.2): - async with timeout.async_freeze(): - await hass.async_add_executor_job(lambda: time.sleep(0.3)) + async with timeout.async_timeout(0.2), timeout.async_freeze(): + await hass.async_add_executor_job(lambda: time.sleep(0.3)) async def test_simple_global_timeout_freeze_reset() -> None: @@ -185,18 +186,16 @@ async def test_simple_zone_timeout_freeze() -> None: """Test a simple zone timeout freeze.""" timeout = TimeoutManager() - async with timeout.async_timeout(0.2, "test"): - async with timeout.async_freeze("test"): - await asyncio.sleep(0.3) + async with timeout.async_timeout(0.2, "test"), timeout.async_freeze("test"): + await asyncio.sleep(0.3) async def test_simple_zone_timeout_freeze_without_timeout() -> None: """Test a simple zone timeout freeze on a zone that does not have a timeout set.""" timeout = TimeoutManager() - async with timeout.async_timeout(0.1, "test"): - async with timeout.async_freeze("test"): - await asyncio.sleep(0.3) + async with timeout.async_timeout(0.1, "test"), timeout.async_freeze("test"): + await asyncio.sleep(0.3) async def test_simple_zone_timeout_freeze_reset() -> None: @@ -214,29 +213,28 @@ async def test_mix_zone_timeout_freeze_and_global_freeze() -> None: """Test a mix zone timeout freeze and global freeze.""" timeout = TimeoutManager() - async with timeout.async_timeout(0.2, "test"): - async with timeout.async_freeze("test"): - async with timeout.async_freeze(): - await asyncio.sleep(0.3) + async with timeout.async_timeout(0.2, "test"), timeout.async_freeze( + "test" + ), timeout.async_freeze(): + await asyncio.sleep(0.3) async def test_mix_global_and_zone_timeout_freeze_() -> None: """Test a mix zone timeout freeze and global freeze.""" timeout = TimeoutManager() - async with timeout.async_timeout(0.2, "test"): - async with timeout.async_freeze(): - async with timeout.async_freeze("test"): - await asyncio.sleep(0.3) + async with timeout.async_timeout( + 0.2, "test" + ), timeout.async_freeze(), timeout.async_freeze("test"): + await asyncio.sleep(0.3) async def test_mix_zone_timeout_freeze() -> None: """Test a mix zone timeout global freeze.""" timeout = TimeoutManager() - async with timeout.async_timeout(0.2, "test"): - async with timeout.async_freeze(): - await asyncio.sleep(0.3) + async with timeout.async_timeout(0.2, "test"), timeout.async_freeze(): + await asyncio.sleep(0.3) async def test_mix_zone_timeout() -> None: From 8bfe692eea45a230c3daf20a81c5cebe9d43dcdb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Jul 2023 05:36:46 -1000 Subject: [PATCH 0281/1009] Update tplink dhcp discovery (#96191) * Update tplink dhcp discovery - Found a KP device with 54AF97 - Found ES devices also use the same OUIs as EP * from issue 95028 --- homeassistant/components/tplink/manifest.json | 20 ++++++++++++---- homeassistant/generated/dhcp.py | 23 +++++++++++++++---- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index eaa1acc11bf4c8..0a9b0254f91d0c 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -9,21 +9,29 @@ "registered_devices": true }, { - "hostname": "es*", + "hostname": "e[sp]*", "macaddress": "54AF97*" }, { - "hostname": "ep*", + "hostname": "e[sp]*", "macaddress": "E848B8*" }, { - "hostname": "ep*", + "hostname": "e[sp]*", "macaddress": "1C61B4*" }, { - "hostname": "ep*", + "hostname": "e[sp]*", "macaddress": "003192*" }, + { + "hostname": "hs*", + "macaddress": "B4B024*" + }, + { + "hostname": "hs*", + "macaddress": "9C5322*" + }, { "hostname": "hs*", "macaddress": "1C3BF3*" @@ -131,6 +139,10 @@ { "hostname": "k[lp]*", "macaddress": "6C5AB0*" + }, + { + "hostname": "k[lp]*", + "macaddress": "54AF97*" } ], "documentation": "https://www.home-assistant.io/integrations/tplink", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 6c8910cd7f9900..05b53acba5ff84 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -597,24 +597,34 @@ }, { "domain": "tplink", - "hostname": "es*", + "hostname": "e[sp]*", "macaddress": "54AF97*", }, { "domain": "tplink", - "hostname": "ep*", + "hostname": "e[sp]*", "macaddress": "E848B8*", }, { "domain": "tplink", - "hostname": "ep*", + "hostname": "e[sp]*", "macaddress": "1C61B4*", }, { "domain": "tplink", - "hostname": "ep*", + "hostname": "e[sp]*", "macaddress": "003192*", }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "B4B024*", + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "9C5322*", + }, { "domain": "tplink", "hostname": "hs*", @@ -750,6 +760,11 @@ "hostname": "k[lp]*", "macaddress": "6C5AB0*", }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "54AF97*", + }, { "domain": "tuya", "macaddress": "105A17*", From cfe57f7e0cfb8fd98a1e0a74099f6bce732d8bb4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 9 Jul 2023 19:52:45 +0200 Subject: [PATCH 0282/1009] Update pytest-xdist to 3.3.1 (#96110) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index baae4698b1e69b..e6c805a64c1ec1 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -27,7 +27,7 @@ pytest-sugar==0.9.6 pytest-timeout==2.1.0 pytest-unordered==0.5.2 pytest-picked==0.4.6 -pytest-xdist==3.2.1 +pytest-xdist==3.3.1 pytest==7.3.1 requests_mock==1.11.0 respx==0.20.1 From 9ef4b2e5f517152f88e78ad1312702e2014b5785 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 9 Jul 2023 19:55:10 +0200 Subject: [PATCH 0283/1009] Migrate ring to entity name (#96080) Migrate ring to has entity name --- .../components/ring/binary_sensor.py | 4 +- homeassistant/components/ring/camera.py | 8 +--- homeassistant/components/ring/entity.py | 1 + homeassistant/components/ring/light.py | 6 +-- homeassistant/components/ring/sensor.py | 28 +++---------- homeassistant/components/ring/siren.py | 9 ++-- homeassistant/components/ring/strings.json | 42 +++++++++++++++++++ homeassistant/components/ring/switch.py | 7 +--- tests/components/ring/test_light.py | 4 +- tests/components/ring/test_switch.py | 4 +- 10 files changed, 63 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index d2c01bbd4f3695..ab7207f0ac4e09 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -35,13 +35,12 @@ class RingBinarySensorEntityDescription( BINARY_SENSOR_TYPES: tuple[RingBinarySensorEntityDescription, ...] = ( RingBinarySensorEntityDescription( key="ding", - name="Ding", + translation_key="ding", category=["doorbots", "authorized_doorbots"], device_class=BinarySensorDeviceClass.OCCUPANCY, ), RingBinarySensorEntityDescription( key="motion", - name="Motion", category=["doorbots", "authorized_doorbots", "stickup_cams"], device_class=BinarySensorDeviceClass.MOTION, ), @@ -85,7 +84,6 @@ def __init__( super().__init__(config_entry_id, device) self.entity_description = description self._ring = ring - self._attr_name = f"{device.name} {description.name}" self._attr_unique_id = f"{device.id}-{description.key}" self._update_alert() diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index e99fabfab2f2e1..0b3f1509b189f8 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -48,11 +48,12 @@ async def async_setup_entry( class RingCam(RingEntityMixin, Camera): """An implementation of a Ring Door Bell camera.""" + _attr_name = None + def __init__(self, config_entry_id, ffmpeg_manager, device): """Initialize a Ring Door Bell camera.""" super().__init__(config_entry_id, device) - self._name = self._device.name self._ffmpeg_manager = ffmpeg_manager self._last_event = None self._last_video_id = None @@ -90,11 +91,6 @@ def _history_update_callback(self, history_data): self._expires_at = dt_util.utcnow() self.async_write_ha_state() - @property - def name(self): - """Return the name of this camera.""" - return self._name - @property def unique_id(self): """Return a unique ID.""" diff --git a/homeassistant/components/ring/entity.py b/homeassistant/components/ring/entity.py index 16aa86511bed1d..5fc438c23900cd 100644 --- a/homeassistant/components/ring/entity.py +++ b/homeassistant/components/ring/entity.py @@ -10,6 +10,7 @@ class RingEntityMixin(Entity): _attr_attribution = ATTRIBUTION _attr_should_poll = False + _attr_has_entity_name = True def __init__(self, config_entry_id, device): """Initialize a sensor for Ring device.""" diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 143c333f600602..2604e557b79b4d 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -50,6 +50,7 @@ class RingLight(RingEntityMixin, LightEntity): _attr_color_mode = ColorMode.ONOFF _attr_supported_color_modes = {ColorMode.ONOFF} + _attr_translation_key = "light" def __init__(self, config_entry_id, device): """Initialize the light.""" @@ -67,11 +68,6 @@ def _update_callback(self): self._light_on = self._device.lights == ON_STATE self.async_write_ha_state() - @property - def name(self): - """Name of the light.""" - return f"{self._device.name} light" - @property def unique_id(self): """Return a unique ID.""" diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 3d198ce7573c72..fbaeb8a4b5b529 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -13,7 +13,6 @@ from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.icon import icon_for_battery_level from . import DOMAIN from .entity import RingEntityMixin @@ -53,8 +52,6 @@ def __init__( """Initialize a sensor for Ring device.""" super().__init__(config_entry_id, device) self.entity_description = description - self._extra = None - self._attr_name = f"{device.name} {description.name}" self._attr_unique_id = f"{device.id}-{description.key}" @property @@ -67,18 +64,6 @@ def native_value(self): if sensor_type == "battery": return self._device.battery_life - @property - def icon(self): - """Icon to use in the frontend, if any.""" - if ( - self.entity_description.key == "battery" - and self._device.battery_life is not None - ): - return icon_for_battery_level( - battery_level=self._device.battery_life, charging=False - ) - return self.entity_description.icon - class HealthDataRingSensor(RingSensor): """Ring sensor that relies on health data.""" @@ -204,7 +189,6 @@ class RingSensorEntityDescription(SensorEntityDescription, RingRequiredKeysMixin SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( RingSensorEntityDescription( key="battery", - name="Battery", category=["doorbots", "authorized_doorbots", "stickup_cams"], native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, @@ -212,7 +196,7 @@ class RingSensorEntityDescription(SensorEntityDescription, RingRequiredKeysMixin ), RingSensorEntityDescription( key="last_activity", - name="Last Activity", + translation_key="last_activity", category=["doorbots", "authorized_doorbots", "stickup_cams"], icon="mdi:history", device_class=SensorDeviceClass.TIMESTAMP, @@ -220,7 +204,7 @@ class RingSensorEntityDescription(SensorEntityDescription, RingRequiredKeysMixin ), RingSensorEntityDescription( key="last_ding", - name="Last Ding", + translation_key="last_ding", category=["doorbots", "authorized_doorbots"], icon="mdi:history", kind="ding", @@ -229,7 +213,7 @@ class RingSensorEntityDescription(SensorEntityDescription, RingRequiredKeysMixin ), RingSensorEntityDescription( key="last_motion", - name="Last Motion", + translation_key="last_motion", category=["doorbots", "authorized_doorbots", "stickup_cams"], icon="mdi:history", kind="motion", @@ -238,21 +222,21 @@ class RingSensorEntityDescription(SensorEntityDescription, RingRequiredKeysMixin ), RingSensorEntityDescription( key="volume", - name="Volume", + translation_key="volume", category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], icon="mdi:bell-ring", cls=RingSensor, ), RingSensorEntityDescription( key="wifi_signal_category", - name="WiFi Signal Category", + translation_key="wifi_signal_category", category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], icon="mdi:wifi", cls=HealthDataRingSensor, ), RingSensorEntityDescription( key="wifi_signal_strength", - name="WiFi Signal Strength", + translation_key="wifi_signal_strength", category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, icon="mdi:wifi", diff --git a/homeassistant/components/ring/siren.py b/homeassistant/components/ring/siren.py index 626444a9dcf56a..7f1b147471d271 100644 --- a/homeassistant/components/ring/siren.py +++ b/homeassistant/components/ring/siren.py @@ -33,16 +33,15 @@ async def async_setup_entry( class RingChimeSiren(RingEntityMixin, SirenEntity): """Creates a siren to play the test chimes of a Chime device.""" + _attr_available_tones = CHIME_TEST_SOUND_KINDS + _attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TONES + _attr_translation_key = "siren" + def __init__(self, config_entry: ConfigEntry, device) -> None: """Initialize a Ring Chime siren.""" super().__init__(config_entry.entry_id, device) # Entity class attributes - self._attr_name = f"{self._device.name} Siren" self._attr_unique_id = f"{self._device.id}-siren" - self._attr_available_tones = CHIME_TEST_SOUND_KINDS - self._attr_supported_features = ( - SirenEntityFeature.TURN_ON | SirenEntityFeature.TONES - ) def turn_on(self, **kwargs: Any) -> None: """Play the test sound on a Ring Chime device.""" diff --git a/homeassistant/components/ring/strings.json b/homeassistant/components/ring/strings.json index c5b448ad68b2d5..43209a5a6a3c76 100644 --- a/homeassistant/components/ring/strings.json +++ b/homeassistant/components/ring/strings.json @@ -22,5 +22,47 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "binary_sensor": { + "ding": { + "name": "Ding" + } + }, + "light": { + "light": { + "name": "[%key:component::light::title%]" + } + }, + "siren": { + "siren": { + "name": "[%key:component::siren::title%]" + } + }, + "sensor": { + "last_activity": { + "name": "Last activity" + }, + "last_ding": { + "name": "Last ding" + }, + "last_motion": { + "name": "Last motion" + }, + "volume": { + "name": "Volume" + }, + "wifi_signal_category": { + "name": "Wi-Fi signal category" + }, + "wifi_signal_strength": { + "name": "Wi-Fi signal strength" + } + }, + "switch": { + "siren": { + "name": "[%key:component::siren::title%]" + } + } } } diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 9a3c80114e9e4b..43bd303577a13b 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -52,11 +52,6 @@ def __init__(self, config_entry_id, device, device_type): self._device_type = device_type self._unique_id = f"{self._device.id}-{self._device_type}" - @property - def name(self): - """Name of the device.""" - return f"{self._device.name} {self._device_type}" - @property def unique_id(self): """Return a unique ID.""" @@ -66,6 +61,8 @@ def unique_id(self): class SirenSwitch(BaseRingSwitch): """Creates a switch to turn the ring cameras siren on and off.""" + _attr_translation_key = "siren" + def __init__(self, config_entry_id, device): """Initialize the switch for a device with a siren.""" super().__init__(config_entry_id, device, "siren") diff --git a/tests/components/ring/test_light.py b/tests/components/ring/test_light.py index a2eb72c2711cde..7607f9fa5dbaf7 100644 --- a/tests/components/ring/test_light.py +++ b/tests/components/ring/test_light.py @@ -32,7 +32,7 @@ async def test_light_off_reports_correctly( state = hass.states.get("light.front_light") assert state.state == "off" - assert state.attributes.get("friendly_name") == "Front light" + assert state.attributes.get("friendly_name") == "Front Light" async def test_light_on_reports_correctly( @@ -43,7 +43,7 @@ async def test_light_on_reports_correctly( state = hass.states.get("light.internal_light") assert state.state == "on" - assert state.attributes.get("friendly_name") == "Internal light" + assert state.attributes.get("friendly_name") == "Internal Light" async def test_light_can_be_turned_on( diff --git a/tests/components/ring/test_switch.py b/tests/components/ring/test_switch.py index a33b9a0d73200a..468b4f0d0ec00e 100644 --- a/tests/components/ring/test_switch.py +++ b/tests/components/ring/test_switch.py @@ -32,7 +32,7 @@ async def test_siren_off_reports_correctly( state = hass.states.get("switch.front_siren") assert state.state == "off" - assert state.attributes.get("friendly_name") == "Front siren" + assert state.attributes.get("friendly_name") == "Front Siren" async def test_siren_on_reports_correctly( @@ -43,7 +43,7 @@ async def test_siren_on_reports_correctly( state = hass.states.get("switch.internal_siren") assert state.state == "on" - assert state.attributes.get("friendly_name") == "Internal siren" + assert state.attributes.get("friendly_name") == "Internal Siren" assert state.attributes.get("icon") == "mdi:alarm-bell" From ab3b0c90751f9de78cee2cdcbb2cbe743c434e14 Mon Sep 17 00:00:00 2001 From: Luke Date: Sun, 9 Jul 2023 14:17:19 -0400 Subject: [PATCH 0284/1009] Add error sensor to Roborock (#96209) add error sensor --- homeassistant/components/roborock/sensor.py | 11 ++++++- .../components/roborock/strings.json | 32 +++++++++++++++++++ tests/components/roborock/test_sensor.py | 3 +- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 8398995462ff73..818fd338ffbcf6 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -4,7 +4,7 @@ from collections.abc import Callable from dataclasses import dataclass -from roborock.containers import RoborockStateCode +from roborock.containers import RoborockErrorCode, RoborockStateCode from roborock.roborock_typing import DeviceProp from homeassistant.components.sensor import ( @@ -113,6 +113,15 @@ class RoborockSensorDescription( value_fn=lambda data: data.clean_summary.square_meter_clean_area, native_unit_of_measurement=AREA_SQUARE_METERS, ), + RoborockSensorDescription( + key="vacuum_error", + icon="mdi:alert-circle", + translation_key="vacuum_error", + device_class=SensorDeviceClass.ENUM, + value_fn=lambda data: data.status.error_code.name, + entity_category=EntityCategory.DIAGNOSTIC, + options=RoborockErrorCode.keys(), + ), ] diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index e595b7abff4e8a..70ed98a6d5fa2c 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -79,6 +79,38 @@ }, "total_cleaning_area": { "name": "Total cleaning area" + }, + "vacuum_error": { + "name": "Vacuum error", + "state": { + "none": "None", + "lidar_blocked": "Lidar blocked", + "bumper_stuck": "Bumper stuck", + "wheels_suspended": "Wheels suspended", + "cliff_sensor_error": "Cliff sensor error", + "main_brush_jammed": "Main brush jammed", + "side_brush_jammed": "Side brush jammed", + "wheels_jammed": "Wheels jammed", + "robot_trapped": "Robot trapped", + "no_dustbin": "No dustbin", + "low_battery": "Low battery", + "charging_error": "Charging error", + "battery_error": "Battery error", + "wall_sensor_dirty": "Wall sensor dirty", + "robot_tilted": "Robot tilted", + "side_brush_error": "Side brush error", + "fan_error": "Fan error", + "vertical_bumper_pressed": "Vertical bumper pressed", + "dock_locator_error": "Dock locator error", + "return_to_dock_fail": "Return to dock fail", + "nogo_zone_detected": "No-go zone detected", + "vibrarise_jammed": "VibraRise jammed", + "robot_on_carpet": "Robot on carpet", + "filter_blocked": "Filter blocked", + "invisible_wall_detected": "Invisible wall detected", + "cannot_cross_carpet": "Cannot cross carpet", + "internal_error": "Internal error" + } } }, "select": { diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index daa904d482a5df..f9f3d327d29be2 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -14,7 +14,7 @@ async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> None: """Test sensors and check test values are correctly set.""" - assert len(hass.states.async_all("sensor")) == 9 + assert len(hass.states.async_all("sensor")) == 10 assert hass.states.get("sensor.roborock_s7_maxv_main_brush_time_left").state == str( MAIN_BRUSH_REPLACE_TIME - 74382 ) @@ -36,3 +36,4 @@ async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> Non hass.states.get("sensor.roborock_s7_maxv_total_cleaning_area").state == "1159.2" ) assert hass.states.get("sensor.roborock_s7_maxv_cleaning_area").state == "21.0" + assert hass.states.get("sensor.roborock_s7_maxv_vacuum_error").state == "none" From 0735b39fbb6696fbe24be387446837a7bc90d7f2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 9 Jul 2023 20:19:05 +0200 Subject: [PATCH 0285/1009] 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 cd84bec11b2251..5b0bc4d4c63cbc 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 8bbb395bec0e124cc5a21f3080d23f7c6dcc5a70 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 9 Jul 2023 20:20:39 +0200 Subject: [PATCH 0286/1009] Add entity translations to Speedtest.net (#96168) * Add entity translations to Speedtest.net * Fix tests --- .../components/speedtestdotnet/sensor.py | 6 ++-- .../components/speedtestdotnet/strings.json | 13 ++++++++ .../components/speedtestdotnet/test_sensor.py | 32 +++++++++++++------ 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index d44d66bbd4772b..a5ccb78baedabb 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -44,20 +44,20 @@ class SpeedtestSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: tuple[SpeedtestSensorEntityDescription, ...] = ( SpeedtestSensorEntityDescription( key="ping", - name="Ping", + translation_key="ping", native_unit_of_measurement=UnitOfTime.MILLISECONDS, state_class=SensorStateClass.MEASUREMENT, ), SpeedtestSensorEntityDescription( key="download", - name="Download", + translation_key="download", native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, value=lambda value: round(value / 10**6, 2), ), SpeedtestSensorEntityDescription( key="upload", - name="Upload", + translation_key="upload", native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, value=lambda value: round(value / 10**6, 2), diff --git a/homeassistant/components/speedtestdotnet/strings.json b/homeassistant/components/speedtestdotnet/strings.json index 09515dfd4c89bb..740716db78eab7 100644 --- a/homeassistant/components/speedtestdotnet/strings.json +++ b/homeassistant/components/speedtestdotnet/strings.json @@ -17,5 +17,18 @@ } } } + }, + "entity": { + "sensor": { + "ping": { + "name": "Ping" + }, + "download": { + "name": "Download" + }, + "upload": { + "name": "Upload" + } + } } } diff --git a/tests/components/speedtestdotnet/test_sensor.py b/tests/components/speedtestdotnet/test_sensor.py index 68f14c64a69c7e..887f0ba0491225 100644 --- a/tests/components/speedtestdotnet/test_sensor.py +++ b/tests/components/speedtestdotnet/test_sensor.py @@ -3,8 +3,6 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.speedtestdotnet import DOMAIN -from homeassistant.components.speedtestdotnet.const import DEFAULT_NAME -from homeassistant.components.speedtestdotnet.sensor import SENSOR_TYPES from homeassistant.core import HomeAssistant, State from . import MOCK_RESULTS, MOCK_SERVERS, MOCK_STATES @@ -27,10 +25,17 @@ async def test_speedtestdotnet_sensors( assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 - for description in SENSOR_TYPES: - sensor = hass.states.get(f"sensor.{DEFAULT_NAME}_{description.name}") - assert sensor - assert sensor.state == MOCK_STATES[description.key] + sensor = hass.states.get("sensor.speedtest_ping") + assert sensor + assert sensor.state == MOCK_STATES["ping"] + + sensor = hass.states.get("sensor.speedtest_download") + assert sensor + assert sensor.state == MOCK_STATES["download"] + + sensor = hass.states.get("sensor.speedtest_ping") + assert sensor + assert sensor.state == MOCK_STATES["ping"] async def test_restore_last_state(hass: HomeAssistant, mock_api: MagicMock) -> None: @@ -50,7 +55,14 @@ async def test_restore_last_state(hass: HomeAssistant, mock_api: MagicMock) -> N assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 - for description in SENSOR_TYPES: - sensor = hass.states.get(f"sensor.speedtest_{description.name}") - assert sensor - assert sensor.state == MOCK_STATES[description.key] + sensor = hass.states.get("sensor.speedtest_ping") + assert sensor + assert sensor.state == MOCK_STATES["ping"] + + sensor = hass.states.get("sensor.speedtest_download") + assert sensor + assert sensor.state == MOCK_STATES["download"] + + sensor = hass.states.get("sensor.speedtest_ping") + assert sensor + assert sensor.state == MOCK_STATES["ping"] From 89259865fb222df1d6d00eb34349adaf3d46150c Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 9 Jul 2023 21:15:55 +0200 Subject: [PATCH 0287/1009] Restore KNX telegram history (#95800) * Restore KNX telegram history * increase default log size * test removal of telegram history --- homeassistant/components/knx/__init__.py | 25 +++-- homeassistant/components/knx/const.py | 2 +- homeassistant/components/knx/telegrams.py | 33 ++++++- tests/components/knx/test_config_flow.py | 4 +- tests/components/knx/test_init.py | 2 +- tests/components/knx/test_telegrams.py | 114 ++++++++++++++++++++++ 6 files changed, 163 insertions(+), 17 deletions(-) create mode 100644 tests/components/knx/test_telegrams.py diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index e8c237114b51ab..c30098f254b59f 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -74,7 +74,7 @@ ) from .device import KNXInterfaceDevice from .expose import KNXExposeSensor, KNXExposeTime, create_knx_exposure -from .project import KNXProject +from .project import STORAGE_KEY as PROJECT_STORAGE_KEY, KNXProject from .schema import ( BinarySensorSchema, ButtonSchema, @@ -96,7 +96,7 @@ ga_validator, sensor_type_validator, ) -from .telegrams import Telegrams +from .telegrams import STORAGE_KEY as TELEGRAMS_STORAGE_KEY, Telegrams from .websocket import register_panel _LOGGER = logging.getLogger(__name__) @@ -360,16 +360,21 @@ async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Remove a config entry.""" - def remove_keyring_files(file_path: Path) -> None: - """Remove keyring files.""" + def remove_files(storage_dir: Path, knxkeys_filename: str | None) -> None: + """Remove KNX files.""" + if knxkeys_filename is not None: + with contextlib.suppress(FileNotFoundError): + (storage_dir / knxkeys_filename).unlink() with contextlib.suppress(FileNotFoundError): - file_path.unlink() + (storage_dir / PROJECT_STORAGE_KEY).unlink() + with contextlib.suppress(FileNotFoundError): + (storage_dir / TELEGRAMS_STORAGE_KEY).unlink() with contextlib.suppress(FileNotFoundError, OSError): - file_path.parent.rmdir() + (storage_dir / DOMAIN).rmdir() - if (_knxkeys_file := entry.data.get(CONF_KNX_KNXKEY_FILENAME)) is not None: - file_path = Path(hass.config.path(STORAGE_DIR)) / _knxkeys_file - await hass.async_add_executor_job(remove_keyring_files, file_path) + storage_dir = Path(hass.config.path(STORAGE_DIR)) + knxkeys_filename = entry.data.get(CONF_KNX_KNXKEY_FILENAME) + await hass.async_add_executor_job(remove_files, storage_dir, knxkeys_filename) class KNXModule: @@ -420,11 +425,13 @@ def __init__( async def start(self) -> None: """Start XKNX object. Connect to tunneling or Routing device.""" await self.project.load_project() + await self.telegrams.load_history() await self.xknx.start() async def stop(self, event: Event | None = None) -> None: """Stop XKNX object. Disconnect from tunneling or Routing device.""" await self.xknx.stop() + await self.telegrams.save_history() def connection_config(self) -> ConnectionConfig: """Return the connection_config.""" diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index a9f5341fbfdb76..bdc480851c32a6 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -53,7 +53,7 @@ DEFAULT_ROUTING_IA: Final = "0.0.240" CONF_KNX_TELEGRAM_LOG_SIZE: Final = "telegram_log_size" -TELEGRAM_LOG_DEFAULT: Final = 50 +TELEGRAM_LOG_DEFAULT: Final = 200 TELEGRAM_LOG_MAX: Final = 5000 # ~2 MB or ~5 hours of reasonable bus load ## diff --git a/homeassistant/components/knx/telegrams.py b/homeassistant/components/knx/telegrams.py index 09307794066744..87c1a8b605278d 100644 --- a/homeassistant/components/knx/telegrams.py +++ b/homeassistant/components/knx/telegrams.py @@ -3,8 +3,7 @@ from collections import deque from collections.abc import Callable -import datetime as dt -from typing import TypedDict +from typing import Final, TypedDict from xknx import XKNX from xknx.exceptions import XKNXException @@ -12,10 +11,15 @@ from xknx.telegram.apci import GroupValueResponse, GroupValueWrite from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback +from homeassistant.helpers.storage import Store import homeassistant.util.dt as dt_util +from .const import DOMAIN from .project import KNXProject +STORAGE_VERSION: Final = 1 +STORAGE_KEY: Final = f"{DOMAIN}/telegrams_history.json" + class TelegramDict(TypedDict): """Represent a Telegram as a dict.""" @@ -31,7 +35,7 @@ class TelegramDict(TypedDict): source: str source_name: str telegramtype: str - timestamp: dt.datetime + timestamp: str # ISO format unit: str | None value: str | int | float | bool | None @@ -49,6 +53,9 @@ def __init__( """Initialize Telegrams class.""" self.hass = hass self.project = project + self._history_store = Store[list[TelegramDict]]( + hass, STORAGE_VERSION, STORAGE_KEY + ) self._jobs: list[HassJob[[TelegramDict], None]] = [] self._xknx_telegram_cb_handle = ( xknx.telegram_queue.register_telegram_received_cb( @@ -58,6 +65,24 @@ def __init__( ) self.recent_telegrams: deque[TelegramDict] = deque(maxlen=log_size) + async def load_history(self) -> None: + """Load history from store.""" + if (telegrams := await self._history_store.async_load()) is None: + return + if self.recent_telegrams.maxlen == 0: + await self._history_store.async_remove() + return + for telegram in telegrams: + # tuples are stored as lists in JSON + if isinstance(telegram["payload"], list): + telegram["payload"] = tuple(telegram["payload"]) # type: ignore[unreachable] + self.recent_telegrams.extend(telegrams) + + async def save_history(self) -> None: + """Save history to store.""" + if self.recent_telegrams: + await self._history_store.async_save(list(self.recent_telegrams)) + async def _xknx_telegram_cb(self, telegram: Telegram) -> None: """Handle incoming and outgoing telegrams from xknx.""" telegram_dict = self.telegram_to_dict(telegram) @@ -129,7 +154,7 @@ def telegram_to_dict(self, telegram: Telegram) -> TelegramDict: source=f"{telegram.source_address}", source_name=src_name, telegramtype=telegram.payload.__class__.__name__, - timestamp=dt_util.as_local(dt_util.utcnow()), + timestamp=dt_util.now().isoformat(), unit=unit, value=value, ) diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index ca804176ee9be6..5463892a3ef171 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -910,7 +910,7 @@ async def test_form_with_automatic_connection_handling( CONF_KNX_ROUTE_BACK: False, CONF_KNX_TUNNEL_ENDPOINT_IA: None, CONF_KNX_STATE_UPDATER: True, - CONF_KNX_TELEGRAM_LOG_SIZE: 50, + CONF_KNX_TELEGRAM_LOG_SIZE: 200, } knx_setup.assert_called_once() @@ -1210,7 +1210,7 @@ async def test_options_flow_connection_type( CONF_KNX_SECURE_DEVICE_AUTHENTICATION: None, CONF_KNX_SECURE_USER_ID: None, CONF_KNX_SECURE_USER_PASSWORD: None, - CONF_KNX_TELEGRAM_LOG_SIZE: 50, + CONF_KNX_TELEGRAM_LOG_SIZE: 200, } diff --git a/tests/components/knx/test_init.py b/tests/components/knx/test_init.py index 785ff9d8317dc8..a5d3d0f3263fb5 100644 --- a/tests/components/knx/test_init.py +++ b/tests/components/knx/test_init.py @@ -280,7 +280,7 @@ async def test_async_remove_entry( "pathlib.Path.rmdir" ) as rmdir_mock: assert await hass.config_entries.async_remove(config_entry.entry_id) - unlink_mock.assert_called_once() + assert unlink_mock.call_count == 3 rmdir_mock.assert_called_once() await hass.async_block_till_done() diff --git a/tests/components/knx/test_telegrams.py b/tests/components/knx/test_telegrams.py new file mode 100644 index 00000000000000..964b9ea2a11442 --- /dev/null +++ b/tests/components/knx/test_telegrams.py @@ -0,0 +1,114 @@ +"""KNX Telegrams Tests.""" +from copy import copy +from datetime import datetime +from typing import Any + +import pytest + +from homeassistant.components.knx import DOMAIN +from homeassistant.components.knx.const import CONF_KNX_TELEGRAM_LOG_SIZE +from homeassistant.components.knx.telegrams import TelegramDict +from homeassistant.core import HomeAssistant + +from .conftest import KNXTestKit + +MOCK_TIMESTAMP = "2023-07-02T14:51:24.045162-07:00" +MOCK_TELEGRAMS = [ + { + "destination": "1/3/4", + "destination_name": "", + "direction": "Incoming", + "dpt_main": None, + "dpt_sub": None, + "dpt_name": None, + "payload": True, + "source": "1.2.3", + "source_name": "", + "telegramtype": "GroupValueWrite", + "timestamp": MOCK_TIMESTAMP, + "unit": None, + "value": None, + }, + { + "destination": "2/2/2", + "destination_name": "", + "direction": "Outgoing", + "dpt_main": None, + "dpt_sub": None, + "dpt_name": None, + "payload": [1, 2, 3, 4], + "source": "0.0.0", + "source_name": "", + "telegramtype": "GroupValueWrite", + "timestamp": MOCK_TIMESTAMP, + "unit": None, + "value": None, + }, +] + + +def assert_telegram_history(telegrams: list[TelegramDict]) -> bool: + """Assert that the mock telegrams are equal to the given telegrams. Omitting timestamp.""" + assert len(telegrams) == len(MOCK_TELEGRAMS) + for index in range(len(telegrams)): + test_telegram = copy(telegrams[index]) # don't modify the original + comp_telegram = MOCK_TELEGRAMS[index] + assert datetime.fromisoformat(test_telegram["timestamp"]) + if isinstance(test_telegram["payload"], tuple): + # JSON encodes tuples to lists + test_telegram["payload"] = list(test_telegram["payload"]) + assert test_telegram | {"timestamp": MOCK_TIMESTAMP} == comp_telegram + return True + + +async def test_store_telegam_history( + hass: HomeAssistant, + knx: KNXTestKit, + hass_storage: dict[str, Any], +): + """Test storing telegram history.""" + await knx.setup_integration({}) + + await knx.receive_write("1/3/4", True) + await hass.services.async_call( + "knx", "send", {"address": "2/2/2", "payload": [1, 2, 3, 4]}, blocking=True + ) + await knx.assert_write("2/2/2", (1, 2, 3, 4)) + + assert len(hass.data[DOMAIN].telegrams.recent_telegrams) == 2 + with pytest.raises(KeyError): + hass_storage["knx/telegrams_history.json"] + + await hass.config_entries.async_unload(knx.mock_config_entry.entry_id) + saved_telegrams = hass_storage["knx/telegrams_history.json"]["data"] + assert assert_telegram_history(saved_telegrams) + + +async def test_load_telegam_history( + hass: HomeAssistant, + knx: KNXTestKit, + hass_storage: dict[str, Any], +): + """Test telegram history restoration.""" + hass_storage["knx/telegrams_history.json"] = {"version": 1, "data": MOCK_TELEGRAMS} + await knx.setup_integration({}) + loaded_telegrams = hass.data[DOMAIN].telegrams.recent_telegrams + assert assert_telegram_history(loaded_telegrams) + # TelegramDict "payload" is a tuple, this shall be restored when loading from JSON + assert isinstance(loaded_telegrams[1]["payload"], tuple) + + +async def test_remove_telegam_history( + hass: HomeAssistant, + knx: KNXTestKit, + hass_storage: dict[str, Any], +): + """Test telegram history removal when configured to size 0.""" + hass_storage["knx/telegrams_history.json"] = {"version": 1, "data": MOCK_TELEGRAMS} + knx.mock_config_entry.data = knx.mock_config_entry.data | { + CONF_KNX_TELEGRAM_LOG_SIZE: 0 + } + await knx.setup_integration({}) + # Store.async_remove() is mocked by hass_storage - check that data was removed. + assert "knx/telegrams_history.json" not in hass_storage + assert not hass.data[DOMAIN].telegrams.recent_telegrams From 3f907dea805e8a7e84615140cee112a880682801 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 9 Jul 2023 21:43:18 +0200 Subject: [PATCH 0288/1009] Add entity translations to Starlink (#96181) --- .../components/starlink/binary_sensor.py | 19 +++--- homeassistant/components/starlink/button.py | 1 - homeassistant/components/starlink/sensor.py | 14 ++--- .../components/starlink/strings.json | 59 +++++++++++++++++++ homeassistant/components/starlink/switch.py | 2 +- 5 files changed, 76 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/starlink/binary_sensor.py b/homeassistant/components/starlink/binary_sensor.py index 22d1c5042f5728..87614460096e0a 100644 --- a/homeassistant/components/starlink/binary_sensor.py +++ b/homeassistant/components/starlink/binary_sensor.py @@ -60,64 +60,63 @@ def is_on(self) -> bool | None: BINARY_SENSORS = [ StarlinkBinarySensorEntityDescription( key="update", - name="Update available", device_class=BinarySensorDeviceClass.UPDATE, value_fn=lambda data: data.alert["alert_install_pending"], ), StarlinkBinarySensorEntityDescription( key="roaming", - name="Roaming mode", + translation_key="roaming", value_fn=lambda data: data.alert["alert_roaming"], ), StarlinkBinarySensorEntityDescription( key="currently_obstructed", - name="Obstructed", + translation_key="currently_obstructed", device_class=BinarySensorDeviceClass.PROBLEM, value_fn=lambda data: data.status["currently_obstructed"], ), StarlinkBinarySensorEntityDescription( key="heating", - name="Heating", + translation_key="heating", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.alert["alert_is_heating"], ), StarlinkBinarySensorEntityDescription( key="power_save_idle", - name="Idle", + translation_key="power_save_idle", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.alert["alert_is_power_save_idle"], ), StarlinkBinarySensorEntityDescription( key="mast_near_vertical", - name="Mast near vertical", + translation_key="mast_near_vertical", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.alert["alert_mast_not_near_vertical"], ), StarlinkBinarySensorEntityDescription( key="motors_stuck", - name="Motors stuck", + translation_key="motors_stuck", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.alert["alert_motors_stuck"], ), StarlinkBinarySensorEntityDescription( key="slow_ethernet", - name="Ethernet speeds", + translation_key="slow_ethernet", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.alert["alert_slow_ethernet_speeds"], ), StarlinkBinarySensorEntityDescription( key="thermal_throttle", - name="Thermal throttle", + translation_key="thermal_throttle", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.alert["alert_thermal_throttle"], ), StarlinkBinarySensorEntityDescription( key="unexpected_location", - name="Unexpected location", + translation_key="unexpected_location", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.alert["alert_unexpected_location"], diff --git a/homeassistant/components/starlink/button.py b/homeassistant/components/starlink/button.py index 43e276332c809f..2df9d9b033b66b 100644 --- a/homeassistant/components/starlink/button.py +++ b/homeassistant/components/starlink/button.py @@ -58,7 +58,6 @@ async def async_press(self) -> None: BUTTONS = [ StarlinkButtonEntityDescription( key="reboot", - name="Reboot", device_class=ButtonDeviceClass.RESTART, entity_category=EntityCategory.DIAGNOSTIC, press_fn=lambda coordinator: coordinator.async_reboot_starlink(), diff --git a/homeassistant/components/starlink/sensor.py b/homeassistant/components/starlink/sensor.py index a1cc60da79e4c2..efcf92600b8fab 100644 --- a/homeassistant/components/starlink/sensor.py +++ b/homeassistant/components/starlink/sensor.py @@ -68,7 +68,7 @@ def native_value(self) -> StateType | datetime: SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( StarlinkSensorEntityDescription( key="ping", - name="Ping", + translation_key="ping", icon="mdi:speedometer", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTime.MILLISECONDS, @@ -77,7 +77,7 @@ def native_value(self) -> StateType | datetime: ), StarlinkSensorEntityDescription( key="azimuth", - name="Azimuth", + translation_key="azimuth", icon="mdi:compass", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -88,7 +88,7 @@ def native_value(self) -> StateType | datetime: ), StarlinkSensorEntityDescription( key="elevation", - name="Elevation", + translation_key="elevation", icon="mdi:compass", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -99,7 +99,7 @@ def native_value(self) -> StateType | datetime: ), StarlinkSensorEntityDescription( key="uplink_throughput", - name="Uplink throughput", + translation_key="uplink_throughput", icon="mdi:upload", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DATA_RATE, @@ -109,7 +109,7 @@ def native_value(self) -> StateType | datetime: ), StarlinkSensorEntityDescription( key="downlink_throughput", - name="Downlink throughput", + translation_key="downlink_throughput", icon="mdi:download", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DATA_RATE, @@ -119,7 +119,7 @@ def native_value(self) -> StateType | datetime: ), StarlinkSensorEntityDescription( key="last_boot_time", - name="Last boot time", + translation_key="last_boot_time", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, @@ -127,7 +127,7 @@ def native_value(self) -> StateType | datetime: ), StarlinkSensorEntityDescription( key="ping_drop_rate", - name="Ping Drop Rate", + translation_key="ping_drop_rate", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, value_fn=lambda data: data.status["pop_ping_drop_rate"], diff --git a/homeassistant/components/starlink/strings.json b/homeassistant/components/starlink/strings.json index dddbada730d9b8..48f84ea7baf5a3 100644 --- a/homeassistant/components/starlink/strings.json +++ b/homeassistant/components/starlink/strings.json @@ -13,5 +13,64 @@ } } } + }, + "entity": { + "binary_sensor": { + "roaming_mode": { + "name": "Roaming mode" + }, + "currently_obstructed": { + "name": "Obstructed" + }, + "heating": { + "name": "Heating" + }, + "power_save_idle": { + "name": "Idle" + }, + "mast_near_vertical": { + "name": "Mast near vertical" + }, + "motors_stuck": { + "name": "Motors stuck" + }, + "slow_ethernet": { + "name": "Ethernet speeds" + }, + "thermal_throttle": { + "name": "Thermal throttle" + }, + "unexpected_location": { + "name": "Unexpected location" + } + }, + "sensor": { + "ping": { + "name": "Ping" + }, + "azimuth": { + "name": "Azimuth" + }, + "elevation": { + "name": "Elevation" + }, + "uplink_throughput": { + "name": "Uplink throughput" + }, + "downlink_throughput": { + "name": "Downlink throughput" + }, + "last_boot_time": { + "name": "Last boot time" + }, + "ping_drop_rate": { + "name": "Ping drop rate" + } + }, + "switch": { + "stowed": { + "name": "Stowed" + } + } } } diff --git a/homeassistant/components/starlink/switch.py b/homeassistant/components/starlink/switch.py index daa7b45b30529b..31932fe9854f76 100644 --- a/homeassistant/components/starlink/switch.py +++ b/homeassistant/components/starlink/switch.py @@ -69,7 +69,7 @@ async def async_turn_off(self, **kwargs: Any) -> None: SWITCHES = [ StarlinkSwitchEntityDescription( key="stowed", - name="Stowed", + translation_key="stowed", device_class=SwitchDeviceClass.SWITCH, value_fn=lambda data: data.status["state"] == "STOWED", turn_on_fn=lambda coordinator: coordinator.async_stow_starlink(True), From d64ebbdc84c9c49ba52e64a2487cf8d15d42da10 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 9 Jul 2023 21:51:33 +0200 Subject: [PATCH 0289/1009] Fix missing name in wilight service descriptions (#96073) --- .../components/wilight/services.yaml | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wilight/services.yaml b/homeassistant/components/wilight/services.yaml index 07a545bd5d71a6..b6c538bf9fb749 100644 --- a/homeassistant/components/wilight/services.yaml +++ b/homeassistant/components/wilight/services.yaml @@ -1,24 +1,31 @@ set_watering_time: - description: Set watering time + name: Set watering time + description: Sets time for watering target: fields: watering_time: - description: Duration for this irrigation to be turned on + name: Duration + description: Duration for this irrigation to be turned on. example: 30 set_pause_time: - description: Set pause time + name: Set pause time + description: Sets time to pause. target: fields: pause_time: - description: Duration for this irrigation to be paused + name: Duration + description: Duration for this irrigation to be paused. example: 24 set_trigger: - description: Set trigger + name: Set trigger + description: Set the trigger to use. target: fields: trigger_index: + name: Trigger index description: Index of Trigger from 1 to 4 example: "1" trigger: - description: Configuration of trigger + name: Trigger rules + description: Configuration of trigger. example: "'12707001'" From bc28d7f33ee29e3160280815e4c5fc913fe10b2d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Jul 2023 10:06:26 -1000 Subject: [PATCH 0290/1009] Add slots to bluetooth manager (#95881) --- .../bluetooth/advertisement_tracker.py | 2 + homeassistant/components/bluetooth/manager.py | 22 ++++++++++ tests/components/bluetooth/__init__.py | 44 +++++++++++++++---- tests/components/bluetooth/test_usage.py | 18 ++------ 4 files changed, 63 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/bluetooth/advertisement_tracker.py b/homeassistant/components/bluetooth/advertisement_tracker.py index 3936435f84e9e2..b6a70e32865fee 100644 --- a/homeassistant/components/bluetooth/advertisement_tracker.py +++ b/homeassistant/components/bluetooth/advertisement_tracker.py @@ -18,6 +18,8 @@ class AdvertisementTracker: """Tracker to determine the interval that a device is advertising.""" + __slots__ = ("intervals", "sources", "_timings") + def __init__(self) -> None: """Initialize the tracker.""" self.intervals: dict[str, float] = {} diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index d1fcb115180cbb..ce778e0309b6d2 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -102,6 +102,28 @@ def _dispatch_bleak_callback( class BluetoothManager: """Manage Bluetooth.""" + __slots__ = ( + "hass", + "_integration_matcher", + "_cancel_unavailable_tracking", + "_cancel_logging_listener", + "_advertisement_tracker", + "_unavailable_callbacks", + "_connectable_unavailable_callbacks", + "_callback_index", + "_bleak_callbacks", + "_all_history", + "_connectable_history", + "_non_connectable_scanners", + "_connectable_scanners", + "_adapters", + "_sources", + "_bluetooth_adapters", + "storage", + "slot_manager", + "_debug", + ) + def __init__( self, hass: HomeAssistant, diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index 3aedd6f2deb35d..55d995dd63c2e8 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -1,9 +1,11 @@ """Tests for the Bluetooth integration.""" +from contextlib import contextmanager +import itertools import time from typing import Any -from unittest.mock import patch +from unittest.mock import MagicMock from bleak import BleakClient from bleak.backends.scanner import AdvertisementData, BLEDevice @@ -189,20 +191,46 @@ def inject_bluetooth_service_info( inject_advertisement(hass, device, advertisement_data) +@contextmanager def patch_all_discovered_devices(mock_discovered: list[BLEDevice]) -> None: """Mock all the discovered devices from all the scanners.""" - return patch.object( - _get_manager(), - "_async_all_discovered_addresses", - return_value={ble_device.address for ble_device in mock_discovered}, + manager = _get_manager() + original_history = {} + scanners = list( + itertools.chain( + manager._connectable_scanners, manager._non_connectable_scanners + ) ) + for scanner in scanners: + data = scanner.discovered_devices_and_advertisement_data + original_history[scanner] = data.copy() + data.clear() + if scanners: + data = scanners[0].discovered_devices_and_advertisement_data + data.clear() + data.update( + {device.address: (device, MagicMock()) for device in mock_discovered} + ) + yield + for scanner in scanners: + data = scanner.discovered_devices_and_advertisement_data + data.clear() + data.update(original_history[scanner]) +@contextmanager def patch_discovered_devices(mock_discovered: list[BLEDevice]) -> None: """Mock the combined best path to discovered devices from all the scanners.""" - return patch.object( - _get_manager(), "async_discovered_devices", return_value=mock_discovered - ) + manager = _get_manager() + original_all_history = manager._all_history + original_connectable_history = manager._connectable_history + manager._connectable_history = {} + manager._all_history = { + device.address: MagicMock(device=device) for device in mock_discovered + } + yield + manager._all_history = original_all_history + manager._connectable_history = original_connectable_history async def async_setup_with_default_adapter(hass: HomeAssistant) -> MockConfigEntry: diff --git a/tests/components/bluetooth/test_usage.py b/tests/components/bluetooth/test_usage.py index 0edab3ce77bb51..12bdba66d75c9e 100644 --- a/tests/components/bluetooth/test_usage.py +++ b/tests/components/bluetooth/test_usage.py @@ -15,7 +15,7 @@ ) from homeassistant.core import HomeAssistant -from . import _get_manager, generate_ble_device +from . import generate_ble_device MOCK_BLE_DEVICE = generate_ble_device( "00:00:00:00:00:00", @@ -65,12 +65,7 @@ async def test_bleak_client_reports_with_address( """Test we report when we pass an address to BleakClient.""" install_multiple_bleak_catcher() - with patch.object( - _get_manager(), - "async_ble_device_from_address", - return_value=MOCK_BLE_DEVICE, - ): - instance = bleak.BleakClient("00:00:00:00:00:00") + instance = bleak.BleakClient("00:00:00:00:00:00") assert "BleakClient with an address instead of a BLEDevice" in caplog.text @@ -92,14 +87,7 @@ async def test_bleak_retry_connector_client_reports_with_address( """Test we report when we pass an address to BleakClientWithServiceCache.""" install_multiple_bleak_catcher() - with patch.object( - _get_manager(), - "async_ble_device_from_address", - return_value=MOCK_BLE_DEVICE, - ): - instance = bleak_retry_connector.BleakClientWithServiceCache( - "00:00:00:00:00:00" - ) + instance = bleak_retry_connector.BleakClientWithServiceCache("00:00:00:00:00:00") assert "BleakClient with an address instead of a BLEDevice" in caplog.text From c720658c0f069a1b7f7e74f002552b1873fb58ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20L=C3=A9p=C3=A9e?= <205357+alepee@users.noreply.github.com> Date: Sun, 9 Jul 2023 22:49:08 +0200 Subject: [PATCH 0291/1009] Enrich instructions to retreive Roomba password (#95902) To setup Roomba 981 vacuum cleaner, user must press Home AND Spot buttons for about 2 seconds. Pressing only the Home button has no effect (it took me a while before figuring out). Tested against Roomba 981 (may also be the case with all 900 series) --- homeassistant/components/roomba/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json index a644797c1be2e4..be2e5b99159064 100644 --- a/homeassistant/components/roomba/strings.json +++ b/homeassistant/components/roomba/strings.json @@ -18,7 +18,7 @@ }, "link": { "title": "Retrieve Password", - "description": "Make sure that the iRobot app is not running on any device. Press and hold the Home button on {name} until the device generates a sound (about two seconds), then submit within 30 seconds." + "description": "Make sure that the iRobot app is not running on any device. Press and hold the Home button (or both Home and Spot buttons) on {name} until the device generates a sound (about two seconds), then submit within 30 seconds." }, "link_manual": { "title": "Enter Password", From 0546e7601cd42204dcab8791e4948eba25368f66 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 9 Jul 2023 22:50:12 +0200 Subject: [PATCH 0292/1009] Enhance diagnostics for Sensibo (#96150) * Diag Sensibo * Fix snapshot --- .../components/sensibo/diagnostics.py | 6 +- .../sensibo/snapshots/test_diagnostics.ambr | 257 ++++++++++++++++++ tests/components/sensibo/test_diagnostics.py | 22 +- 3 files changed, 276 insertions(+), 9 deletions(-) create mode 100644 tests/components/sensibo/snapshots/test_diagnostics.ambr diff --git a/homeassistant/components/sensibo/diagnostics.py b/homeassistant/components/sensibo/diagnostics.py index 72029acc2f1638..9d998e739f0e1d 100644 --- a/homeassistant/components/sensibo/diagnostics.py +++ b/homeassistant/components/sensibo/diagnostics.py @@ -33,4 +33,8 @@ async def async_get_config_entry_diagnostics( ) -> dict[str, Any]: """Return diagnostics for Sensibo config entry.""" coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - return async_redact_data(coordinator.data.raw, TO_REDACT) + diag_data = {} + diag_data["raw"] = async_redact_data(coordinator.data.raw, TO_REDACT) + for device, device_data in coordinator.data.parsed.items(): + diag_data[device] = async_redact_data(device_data.__dict__, TO_REDACT) + return diag_data diff --git a/tests/components/sensibo/snapshots/test_diagnostics.ambr b/tests/components/sensibo/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000000..a3ec6952c6c673 --- /dev/null +++ b/tests/components/sensibo/snapshots/test_diagnostics.ambr @@ -0,0 +1,257 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'fanLevel': 'high', + 'horizontalSwing': 'stopped', + 'light': 'on', + 'mode': 'heat', + 'on': True, + 'swing': 'stopped', + 'targetTemperature': 25, + 'timestamp': dict({ + 'secondsAgo': -1, + 'time': '2022-04-30T11:23:30.019722Z', + }), + }) +# --- +# name: test_diagnostics.1 + dict({ + 'modes': dict({ + 'auto': dict({ + 'fanLevels': list([ + 'quiet', + 'low', + 'medium', + ]), + 'horizontalSwing': list([ + 'stopped', + 'fixedLeft', + 'fixedCenterLeft', + ]), + 'light': list([ + 'on', + 'off', + ]), + 'swing': list([ + 'stopped', + 'fixedTop', + 'fixedMiddleTop', + ]), + 'temperatures': dict({ + 'C': dict({ + 'isNative': True, + 'values': list([ + 10, + 16, + 17, + 18, + 19, + 20, + ]), + }), + 'F': dict({ + 'isNative': False, + 'values': list([ + 64, + 66, + 68, + ]), + }), + }), + }), + 'cool': dict({ + 'fanLevels': list([ + 'quiet', + 'low', + 'medium', + ]), + 'horizontalSwing': list([ + 'stopped', + 'fixedLeft', + 'fixedCenterLeft', + ]), + 'light': list([ + 'on', + 'off', + ]), + 'swing': list([ + 'stopped', + 'fixedTop', + 'fixedMiddleTop', + ]), + 'temperatures': dict({ + 'C': dict({ + 'isNative': True, + 'values': list([ + 10, + 16, + 17, + 18, + 19, + 20, + ]), + }), + 'F': dict({ + 'isNative': False, + 'values': list([ + 64, + 66, + 68, + ]), + }), + }), + }), + 'dry': dict({ + 'horizontalSwing': list([ + 'stopped', + 'fixedLeft', + 'fixedCenterLeft', + ]), + 'light': list([ + 'on', + 'off', + ]), + 'swing': list([ + 'stopped', + 'fixedTop', + 'fixedMiddleTop', + ]), + 'temperatures': dict({ + 'C': dict({ + 'isNative': True, + 'values': list([ + 10, + 16, + 17, + 18, + 19, + 20, + ]), + }), + 'F': dict({ + 'isNative': False, + 'values': list([ + 64, + 66, + 68, + ]), + }), + }), + }), + 'fan': dict({ + 'fanLevels': list([ + 'quiet', + 'low', + 'medium', + ]), + 'horizontalSwing': list([ + 'stopped', + 'fixedLeft', + 'fixedCenterLeft', + ]), + 'light': list([ + 'on', + 'off', + ]), + 'swing': list([ + 'stopped', + 'fixedTop', + 'fixedMiddleTop', + ]), + 'temperatures': dict({ + }), + }), + 'heat': dict({ + 'fanLevels': list([ + 'quiet', + 'low', + 'medium', + ]), + 'horizontalSwing': list([ + 'stopped', + 'fixedLeft', + 'fixedCenterLeft', + ]), + 'light': list([ + 'on', + 'off', + ]), + 'swing': list([ + 'stopped', + 'fixedTop', + 'fixedMiddleTop', + ]), + 'temperatures': dict({ + 'C': dict({ + 'isNative': True, + 'values': list([ + 10, + 16, + 17, + 18, + 19, + 20, + ]), + }), + 'F': dict({ + 'isNative': False, + 'values': list([ + 63, + 64, + 66, + ]), + }), + }), + }), + }), + }) +# --- +# name: test_diagnostics.2 + dict({ + 'low': 'low', + 'medium': 'medium', + 'quiet': 'quiet', + }) +# --- +# name: test_diagnostics.3 + dict({ + 'fixedmiddletop': 'fixedMiddleTop', + 'fixedtop': 'fixedTop', + 'stopped': 'stopped', + }) +# --- +# name: test_diagnostics.4 + dict({ + 'fixedcenterleft': 'fixedCenterLeft', + 'fixedleft': 'fixedLeft', + 'stopped': 'stopped', + }) +# --- +# name: test_diagnostics.5 + dict({ + 'fanlevel': 'low', + 'horizontalswing': 'stopped', + 'light': 'on', + 'mode': 'heat', + 'on': True, + 'swing': 'stopped', + 'targettemperature': 21, + 'temperatureunit': 'c', + }) +# --- +# name: test_diagnostics.6 + dict({ + 'fanlevel': 'high', + 'horizontalswing': 'stopped', + 'light': 'on', + 'mode': 'cool', + 'on': True, + 'swing': 'stopped', + 'targettemperature': 21, + 'temperatureunit': 'c', + }) +# --- +# name: test_diagnostics.7 + dict({ + }) +# --- diff --git a/tests/components/sensibo/test_diagnostics.py b/tests/components/sensibo/test_diagnostics.py index 2cbd20a7437375..c3e1625d623650 100644 --- a/tests/components/sensibo/test_diagnostics.py +++ b/tests/components/sensibo/test_diagnostics.py @@ -1,6 +1,8 @@ """Test Sensibo diagnostics.""" from __future__ import annotations +from syrupy.assertion import SnapshotAssertion + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -9,17 +11,21 @@ async def test_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator, load_int: ConfigEntry + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + load_int: ConfigEntry, + snapshot: SnapshotAssertion, ) -> None: """Test generating diagnostics for a config entry.""" entry = load_int diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) - assert diag["status"] == "success" - for device in diag["result"]: - assert device["id"] == "**REDACTED**" - assert device["qrId"] == "**REDACTED**" - assert device["macAddress"] == "**REDACTED**" - assert device["location"] == "**REDACTED**" - assert device["productModel"] in ["skyv2", "pure"] + assert diag["ABC999111"]["ac_states"] == snapshot + assert diag["ABC999111"]["full_capabilities"] == snapshot + assert diag["ABC999111"]["fan_modes_translated"] == snapshot + assert diag["ABC999111"]["swing_modes_translated"] == snapshot + assert diag["ABC999111"]["horizontal_swing_modes_translated"] == snapshot + assert diag["ABC999111"]["smart_low_state"] == snapshot + assert diag["ABC999111"]["smart_high_state"] == snapshot + assert diag["ABC999111"]["pure_conf"] == snapshot From 4a785fd2ad1cdfbe57a9a2111827d8788c2c63e0 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 9 Jul 2023 15:53:25 -0500 Subject: [PATCH 0293/1009] Update pyipp to 0.14.2 (#96218) update pyipp to 0.14.2 --- homeassistant/components/ipp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index 59b8b4b070e1e3..7cdf6767362637 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_polling", "loggers": ["deepmerge", "pyipp"], "quality_scale": "platinum", - "requirements": ["pyipp==0.14.0"], + "requirements": ["pyipp==0.14.2"], "zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 4e4e439217d85d..ce569e14c1b919 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1735,7 +1735,7 @@ pyintesishome==1.8.0 pyipma==3.0.6 # homeassistant.components.ipp -pyipp==0.14.0 +pyipp==0.14.2 # homeassistant.components.iqvia pyiqvia==2022.04.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cabbc0768c23eb..480302b7e7e6e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1281,7 +1281,7 @@ pyinsteon==1.4.3 pyipma==3.0.6 # homeassistant.components.ipp -pyipp==0.14.0 +pyipp==0.14.2 # homeassistant.components.iqvia pyiqvia==2022.04.0 From ac594e6bce74ee0b1532c96e8dbe60420fb877f3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 9 Jul 2023 22:58:23 +0200 Subject: [PATCH 0294/1009] Add entity translations to Sonarr (#96159) --- homeassistant/components/sonarr/sensor.py | 12 +++++------ homeassistant/components/sonarr/strings.json | 22 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index 1c4b9afb08d06c..def44d382ce837 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -88,7 +88,7 @@ def get_wanted_attr(wanted: SonarrWantedMissing) -> dict[str, str]: SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = { "commands": SonarrSensorEntityDescription[list[Command]]( key="commands", - name="Commands", + translation_key="commands", icon="mdi:code-braces", native_unit_of_measurement="Commands", entity_registry_enabled_default=False, @@ -97,7 +97,7 @@ def get_wanted_attr(wanted: SonarrWantedMissing) -> dict[str, str]: ), "diskspace": SonarrSensorEntityDescription[list[Diskspace]]( key="diskspace", - name="Disk space", + translation_key="diskspace", icon="mdi:harddisk", native_unit_of_measurement=UnitOfInformation.GIGABYTES, device_class=SensorDeviceClass.DATA_SIZE, @@ -107,7 +107,7 @@ def get_wanted_attr(wanted: SonarrWantedMissing) -> dict[str, str]: ), "queue": SonarrSensorEntityDescription[SonarrQueue]( key="queue", - name="Queue", + translation_key="queue", icon="mdi:download", native_unit_of_measurement="Episodes", entity_registry_enabled_default=False, @@ -116,7 +116,7 @@ def get_wanted_attr(wanted: SonarrWantedMissing) -> dict[str, str]: ), "series": SonarrSensorEntityDescription[list[SonarrSeries]]( key="series", - name="Shows", + translation_key="series", icon="mdi:television", native_unit_of_measurement="Series", entity_registry_enabled_default=False, @@ -130,7 +130,7 @@ def get_wanted_attr(wanted: SonarrWantedMissing) -> dict[str, str]: ), "upcoming": SonarrSensorEntityDescription[list[SonarrCalendar]]( key="upcoming", - name="Upcoming", + translation_key="upcoming", icon="mdi:television", native_unit_of_measurement="Episodes", value_fn=len, @@ -140,7 +140,7 @@ def get_wanted_attr(wanted: SonarrWantedMissing) -> dict[str, str]: ), "wanted": SonarrSensorEntityDescription[SonarrWantedMissing]( key="wanted", - name="Wanted", + translation_key="wanted", icon="mdi:television", native_unit_of_measurement="Episodes", entity_registry_enabled_default=False, diff --git a/homeassistant/components/sonarr/strings.json b/homeassistant/components/sonarr/strings.json index b8537e11442c77..5b17f3283e8d7f 100644 --- a/homeassistant/components/sonarr/strings.json +++ b/homeassistant/components/sonarr/strings.json @@ -33,5 +33,27 @@ } } } + }, + "entity": { + "sensor": { + "commands": { + "name": "Commands" + }, + "diskspace": { + "name": "Disk space" + }, + "queue": { + "name": "Queue" + }, + "series": { + "name": "Shows" + }, + "upcoming": { + "name": "Upcoming" + }, + "wanted": { + "name": "Wanted" + } + } } } From 7390e3a997f1fbdfbd84de8c7b7fa5dba4031ed8 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 9 Jul 2023 17:37:32 -0500 Subject: [PATCH 0295/1009] Refactor IPP tests (#94097) refactor ipp tests --- tests/components/ipp/__init__.py | 112 +------------- tests/components/ipp/conftest.py | 99 ++++++++++++ .../get-printer-attributes-error-0x0503.bin | Bin 75 -> 0 bytes .../get-printer-attributes-success-nodata.bin | Bin 72 -> 0 bytes .../ipp/fixtures/get-printer-attributes.bin | Bin 9143 -> 0 bytes tests/components/ipp/fixtures/printer.json | 36 +++++ tests/components/ipp/test_config_flow.py | 145 ++++++++++-------- tests/components/ipp/test_init.py | 46 ++++-- tests/components/ipp/test_sensor.py | 88 +++++------ 9 files changed, 287 insertions(+), 239 deletions(-) create mode 100644 tests/components/ipp/conftest.py delete mode 100644 tests/components/ipp/fixtures/get-printer-attributes-error-0x0503.bin delete mode 100644 tests/components/ipp/fixtures/get-printer-attributes-success-nodata.bin delete mode 100644 tests/components/ipp/fixtures/get-printer-attributes.bin create mode 100644 tests/components/ipp/fixtures/printer.json diff --git a/tests/components/ipp/__init__.py b/tests/components/ipp/__init__.py index feda655421091e..f66630b2a6963e 100644 --- a/tests/components/ipp/__init__.py +++ b/tests/components/ipp/__init__.py @@ -1,20 +1,8 @@ """Tests for the IPP integration.""" -import aiohttp -from pyipp import IPPConnectionUpgradeRequired, IPPError from homeassistant.components import zeroconf -from homeassistant.components.ipp.const import CONF_BASE_PATH, DOMAIN -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - CONF_SSL, - CONF_UUID, - CONF_VERIFY_SSL, -) -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry, get_fixture_path -from tests.test_util.aiohttp import AiohttpClientMocker +from homeassistant.components.ipp.const import CONF_BASE_PATH +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL ATTR_HOSTNAME = "hostname" ATTR_PROPERTIES = "properties" @@ -59,99 +47,3 @@ port=ZEROCONF_PORT, properties={"rp": ZEROCONF_RP}, ) - - -def load_fixture_binary(filename): - """Load a binary fixture.""" - return get_fixture_path(filename, "ipp").read_bytes() - - -def mock_connection( - aioclient_mock: AiohttpClientMocker, - host: str = HOST, - port: int = PORT, - ssl: bool = False, - base_path: str = BASE_PATH, - conn_error: bool = False, - conn_upgrade_error: bool = False, - ipp_error: bool = False, - no_unique_id: bool = False, - parse_error: bool = False, - version_not_supported: bool = False, -): - """Mock the IPP connection.""" - scheme = "https" if ssl else "http" - ipp_url = f"{scheme}://{host}:{port}" - - if ipp_error: - aioclient_mock.post(f"{ipp_url}{base_path}", exc=IPPError) - return - - if conn_error: - aioclient_mock.post(f"{ipp_url}{base_path}", exc=aiohttp.ClientError) - return - - if conn_upgrade_error: - aioclient_mock.post(f"{ipp_url}{base_path}", exc=IPPConnectionUpgradeRequired) - return - - fixture = "get-printer-attributes.bin" - if no_unique_id: - fixture = "get-printer-attributes-success-nodata.bin" - elif version_not_supported: - fixture = "get-printer-attributes-error-0x0503.bin" - - if parse_error: - content = "BAD" - else: - content = load_fixture_binary(fixture) - - aioclient_mock.post( - f"{ipp_url}{base_path}", - content=content, - headers={"Content-Type": "application/ipp"}, - ) - - -async def init_integration( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - skip_setup: bool = False, - host: str = HOST, - port: int = PORT, - ssl: bool = False, - base_path: str = BASE_PATH, - uuid: str = "cfe92100-67c4-11d4-a45f-f8d027761251", - unique_id: str = "cfe92100-67c4-11d4-a45f-f8d027761251", - conn_error: bool = False, -) -> MockConfigEntry: - """Set up the IPP integration in Home Assistant.""" - entry = MockConfigEntry( - domain=DOMAIN, - unique_id=unique_id, - data={ - CONF_HOST: host, - CONF_PORT: port, - CONF_SSL: ssl, - CONF_VERIFY_SSL: True, - CONF_BASE_PATH: base_path, - CONF_UUID: uuid, - }, - ) - - entry.add_to_hass(hass) - - mock_connection( - aioclient_mock, - host=host, - port=port, - ssl=ssl, - base_path=base_path, - conn_error=conn_error, - ) - - if not skip_setup: - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - return entry diff --git a/tests/components/ipp/conftest.py b/tests/components/ipp/conftest.py new file mode 100644 index 00000000000000..de3f1e0e73c892 --- /dev/null +++ b/tests/components/ipp/conftest.py @@ -0,0 +1,99 @@ +"""Fixtures for IPP integration tests.""" +from collections.abc import Generator +import json +from unittest.mock import AsyncMock, MagicMock, patch + +from pyipp import Printer +import pytest + +from homeassistant.components.ipp.const import CONF_BASE_PATH, DOMAIN +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_UUID, + CONF_VERIFY_SSL, +) +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="IPP Printer", + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.31", + CONF_PORT: 631, + CONF_SSL: False, + CONF_VERIFY_SSL: True, + CONF_BASE_PATH: "/ipp/print", + CONF_UUID: "cfe92100-67c4-11d4-a45f-f8d027761251", + }, + unique_id="cfe92100-67c4-11d4-a45f-f8d027761251", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.ipp.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +async def mock_printer( + request: pytest.FixtureRequest, +) -> Printer: + """Return the mocked printer.""" + fixture: str = "ipp/printer.json" + if hasattr(request, "param") and request.param: + fixture = request.param + + return Printer.from_dict(json.loads(load_fixture(fixture))) + + +@pytest.fixture +def mock_ipp_config_flow( + mock_printer: Printer, +) -> Generator[None, MagicMock, None]: + """Return a mocked IPP client.""" + + with patch( + "homeassistant.components.ipp.config_flow.IPP", autospec=True + ) as ipp_mock: + client = ipp_mock.return_value + client.printer.return_value = mock_printer + yield client + + +@pytest.fixture +def mock_ipp( + request: pytest.FixtureRequest, mock_printer: Printer +) -> Generator[None, MagicMock, None]: + """Return a mocked IPP client.""" + + with patch( + "homeassistant.components.ipp.coordinator.IPP", autospec=True + ) as ipp_mock: + client = ipp_mock.return_value + client.printer.return_value = mock_printer + yield client + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_ipp: MagicMock +) -> MockConfigEntry: + """Set up the IPP integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/ipp/fixtures/get-printer-attributes-error-0x0503.bin b/tests/components/ipp/fixtures/get-printer-attributes-error-0x0503.bin deleted file mode 100644 index c92134b9e3bc72859ffd410f8235e36622c2d57f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75 zcmZQ%WMyVxk7is_i diff --git a/tests/components/ipp/fixtures/get-printer-attributes-success-nodata.bin b/tests/components/ipp/fixtures/get-printer-attributes-success-nodata.bin deleted file mode 100644 index e6061adaccdef6f4705bba31c5166ee7b9cefe5f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72 zcmZQ#00PFkS&Z%sLWw0MMVU#ZC8@=_$r*`7#i=C>tfeJsx)vS`(nxZ7i6x~)i8;DC QiFxUziRq~fOsRRy0Q|TXqyPW_ diff --git a/tests/components/ipp/fixtures/get-printer-attributes.bin b/tests/components/ipp/fixtures/get-printer-attributes.bin deleted file mode 100644 index 24b903efc5d6fda33b8693ecb2238244d439313a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9143 zcmeHNOK%(36~3}0>tQ>xWjVIvq>-!uO&Sh0!-qtXRa?p=Wf?ZbN^+c{McL7CNKQPj z%nW5J1&S<-qKl&41VtA830-tmpu7Hn{DdsJ=%)Xmi+<^nBX^v}vwWYV!o*A$i8aT^thG4(vx{ep2oAW9t%uS{1e=P%%pq$=FKp$`PcJ-^F?)z17hx81;6HFde(Y z;p^-z$1`+0Py@rUB~Smjr~BB#&>peYx5rb(YlxO@=`BMYa4*|x)6|1N_nL)tzON{T zjr9wfn0G7{V+1zrmfn|g{mmx+h?%kL0O$K#^d|s!0O&ZUffUWuS7d>??^w;QaccP3 zTT_vh^k!cv$mvbXqJeH2zSC55&5R=VGuvB9;3lZC++0BbZ}Dw(R8#CCCq`d(^rqW& z0!K2NS?n$^z$<*&r;efN%{;)^xIo+k!tPlox+f`eGnZB}`TllufaQ*iqxZ20Y@@b$HWsA0)VQ;veVdE@t z%)Vpx_=!ilya0{u(%*E3y*Y+1KCL7rW9VJ^g8snAd#``W*zE71GI#hW(#Jj3G=j5% zN|2(=th2kr*m(F54vELFvQ*Q?e=(2!%MyHzuhqIhG0gfa=9!?aTxqPDQ;k-`I z)ASq*r=Yb(r@)?I(~0Hf(B-geeW_(wx=otA1{j2M`~eYPgJee#);n7f+qtcUyi+OS zJ-^2x^q9>K;m7TIh+t&K9DHeY&$-4p5o**KnLIW2u4q8YUp zIF(SBr368IzOx)kLoQm5?Py)kvG@Tj5if>I!j@gn(RAM*0f*CkBU%US#ttOM4Garv zGrF4931sn_!tp|*fR&rL5=Ms!jUvLH<7RB0@1Si2Twra(G^sHi0c>0o6?QGuQeADG zaW5J<#(@i-=vmR2u0oC+J4{%S(0RKO&78@)Torf@4CJ zSP8E@@|m$a`?^=!z~IJQhnE@GMZC)A6NFz4hE;payazv-z^fH5<(;_Z+AlBVJ)EB~ zZ!mKy?|U;7c(=c}ly_p-@r%D+%KPw-6XsPuzm#|L*AsY;Ke&|l-M>xX{qgRlyc75P z>EAEqz09b`>SPudIugi-e;ya&g-GRdHuZ;J@%a%03mc;-Ght(iXp2IGyGd6rbrHZy z9nNG?reEV@Uu^SXVin~sQz zZ^dCD!Z{VmCy{U`Qem$rL*~Tp!f+}K8i_&NL<80}_C_d{L0F$;2Ll_#s%z|lpiM;k z7ZATGh?7ac1=Mc|>Y?0VA~{MFVO;{Eu-itb=b@Y+N)&tSR)mW^sX#qsOCk`C5mQqY zb_cR|k?T>?kepIPiI83A6T{tScUz9uLv9gBZO6mG4WiO}s_UAD#!CYmjuz;Fm)VLZo=A*_!)L4uf*P=!#YJ7W*-E<)rU&53UQhHRV_yh}U?DbKE$aw@ByLMZq z7{tymF?`xicEXT3gjL|an zGP@N~wDeQdG$f-nb?;eiuUKsy9n?#Yo>hv~qeqRWnbdCOQL2)|zs83t0v&Fps&xSN zs`Y9k_583_dRBQ_{IDTy%CfwAq@(WSmmWPmDmD)H>&Ml@Ql)w%MqM2x+Q%g1Lk{#1@L{c=6^;;>vg1U@5|DwU(6oXO;6xm10+S5z=(>8M`J z7qYo*F29|Xvw5^+B~!x6ijNvfs`34tlusS(Zl?}v&*jwNQFc>K9bw8$F7=|2syxf4 z_Ky^4OWI6*prmuDXUB?^MYn)*j-)6OhAP9ChBX$$ZaZv+3u$CK4`ZPlzNBiJ4tXOP z{Kz@++0;}Sx)6?GI>PuEE90O;pryXlCDm^6wAdU!MKwM(%kgk#w3zq1LEsiQHk9p5 zTG=e5mGpL|Sdcdwht;EM{kVL*e^B4pJ*Yo9cz(DWZ^TwD9VY6!8+--pu{x3C7J7<; zrO0fdWE@I~BF@WaGvsX6*iy9mo+}}RCa#q~nbnx9=NSp@UBh(#=u$Vc%En+BRao9C zdKVJGtWeMF!FfQou$!JOina`P!za`=e4#iMd#~xhI(N86+#Jun$u_@mcX;Tuum=F0feXLc1}Ry0VpsSsznhhoFXh75#%0i!GfDm1IL5f z4IVyV*{E{9grU*~eYVt~687q@scJ7*4g2LTnCBmzsdm)4_6#=@9tfiT;tzDwbY4Z> zg#_n~aAY2g_-t++P8S~)ES+F)MeG+hB3UIkPa@7AMveWb(Tp0!YV3av zsDjVFLvH2NpCxt?~QQsp+`gMoMD;XMMoxO*bvKBpE=+aoL zkPwEp5u+fOB?-?~ z9!->8b~`el7KM5g8{$eCd)ZdQmj=3+@IGgr@w*W78EpHj+=q{kpFQ0;K4?H}$NTl# zL7D&$*{X#HK*Kdr366qi7+lgPPa(f}FrXU5lKl3Q;2^jl`eE4Gh6_WepX7h-Rl9|F zkQ{{+MzGga!;1i0Fp@|JFY!vo=A(s>^$#UjBbiZ!J&eQec=;pgvUiN~ba?Z%Hp750sBTz*$aI&lK0!{$7P0*Mv^AIrg>R7pdaYP1b#%gQKZ zRY_-e1*=u<5BPfaKUuNuVOiF5!iyXB)weWoYyifwadj@w=q0 z>#d#4);7i#fWCkz`E7!zeVCN0;a1v_G9~UQs6i1{&)<E0?{$ySw; z3>EkdpP!4l23#30A9V!Ky5*dzC#HUkHU5vTOUY+OnooZ-eB=b zo>v69_`LGsyeg=hV#a|B3xfr&G-rGfz4(}R4SQ%?dAa22^iBp<-jT8+s!$WpqO1IH zp(PqTL&x!;7(0kEgJTEw;RhwkC@I*1hM(=wQ|fcKgfh}A6#2V63rWOu3sghE`;n;1 z+_0Sh0Z0fLQwE6re4Nz7;auPc!-$wIs3-Je1$fa46VGWd#twW$9J(ZywG?@~ux5cd zMj+Cu>Yb6n$NVf242oE*9Ea?V*HLZb6CO%ZzV#q-AV~y%6GDc}*kiUH;e}uE2R{}` zI&l;d9@BjX&u;apb;S+%SKF(`8W4>@mr`oNrS!C1-+Nx(s~&CaHyYoi-i~f7@$!+# zcvQmS$ None: @@ -31,11 +40,10 @@ async def test_show_user_form(hass: HomeAssistant) -> None: async def test_show_zeroconf_form( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_ipp_config_flow: MagicMock, ) -> None: """Test that the zeroconf confirmation form is served.""" - mock_connection(aioclient_mock) - discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -49,10 +57,11 @@ async def test_show_zeroconf_form( async def test_connection_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_ipp_config_flow: MagicMock, ) -> None: """Test we show user form on IPP connection error.""" - mock_connection(aioclient_mock, conn_error=True) + mock_ipp_config_flow.printer.side_effect = IPPConnectionError user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( @@ -67,10 +76,11 @@ async def test_connection_error( async def test_zeroconf_connection_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_ipp_config_flow: MagicMock, ) -> None: """Test we abort zeroconf flow on IPP connection error.""" - mock_connection(aioclient_mock, conn_error=True) + mock_ipp_config_flow.printer.side_effect = IPPConnectionError discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( @@ -84,10 +94,11 @@ async def test_zeroconf_connection_error( async def test_zeroconf_confirm_connection_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_ipp_config_flow: MagicMock, ) -> None: """Test we abort zeroconf flow on IPP connection error.""" - mock_connection(aioclient_mock, conn_error=True) + mock_ipp_config_flow.printer.side_effect = IPPConnectionError discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( @@ -99,10 +110,11 @@ async def test_zeroconf_confirm_connection_error( async def test_user_connection_upgrade_required( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_ipp_config_flow: MagicMock, ) -> None: """Test we show the user form if connection upgrade required by server.""" - mock_connection(aioclient_mock, conn_upgrade_error=True) + mock_ipp_config_flow.printer.side_effect = IPPConnectionUpgradeRequired user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( @@ -117,10 +129,11 @@ async def test_user_connection_upgrade_required( async def test_zeroconf_connection_upgrade_required( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_ipp_config_flow: MagicMock, ) -> None: """Test we abort zeroconf flow on IPP connection error.""" - mock_connection(aioclient_mock, conn_upgrade_error=True) + mock_ipp_config_flow.printer.side_effect = IPPConnectionUpgradeRequired discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( @@ -134,10 +147,11 @@ async def test_zeroconf_connection_upgrade_required( async def test_user_parse_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_ipp_config_flow: MagicMock, ) -> None: """Test we abort user flow on IPP parse error.""" - mock_connection(aioclient_mock, parse_error=True) + mock_ipp_config_flow.printer.side_effect = IPPParseError user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( @@ -151,10 +165,11 @@ async def test_user_parse_error( async def test_zeroconf_parse_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_ipp_config_flow: MagicMock, ) -> None: """Test we abort zeroconf flow on IPP parse error.""" - mock_connection(aioclient_mock, parse_error=True) + mock_ipp_config_flow.printer.side_effect = IPPParseError discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( @@ -168,10 +183,11 @@ async def test_zeroconf_parse_error( async def test_user_ipp_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_ipp_config_flow: MagicMock, ) -> None: """Test we abort the user flow on IPP error.""" - mock_connection(aioclient_mock, ipp_error=True) + mock_ipp_config_flow.printer.side_effect = IPPError user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( @@ -185,10 +201,11 @@ async def test_user_ipp_error( async def test_zeroconf_ipp_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_ipp_config_flow: MagicMock, ) -> None: """Test we abort zeroconf flow on IPP error.""" - mock_connection(aioclient_mock, ipp_error=True) + mock_ipp_config_flow.printer.side_effect = IPPError discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( @@ -202,10 +219,11 @@ async def test_zeroconf_ipp_error( async def test_user_ipp_version_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_ipp_config_flow: MagicMock, ) -> None: """Test we abort user flow on IPP version not supported error.""" - mock_connection(aioclient_mock, version_not_supported=True) + mock_ipp_config_flow.printer.side_effect = IPPVersionNotSupportedError user_input = {**MOCK_USER_INPUT} result = await hass.config_entries.flow.async_init( @@ -219,10 +237,11 @@ async def test_user_ipp_version_error( async def test_zeroconf_ipp_version_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_ipp_config_flow: MagicMock, ) -> None: """Test we abort zeroconf flow on IPP version not supported error.""" - mock_connection(aioclient_mock, version_not_supported=True) + mock_ipp_config_flow.printer.side_effect = IPPVersionNotSupportedError discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( @@ -236,10 +255,12 @@ async def test_zeroconf_ipp_version_error( async def test_user_device_exists_abort( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_ipp_config_flow: MagicMock, ) -> None: """Test we abort user flow if printer already configured.""" - await init_integration(hass, aioclient_mock, skip_setup=True) + mock_config_entry.add_to_hass(hass) user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( @@ -253,10 +274,12 @@ async def test_user_device_exists_abort( async def test_zeroconf_device_exists_abort( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_ipp_config_flow: MagicMock, ) -> None: """Test we abort zeroconf flow if printer already configured.""" - await init_integration(hass, aioclient_mock, skip_setup=True) + mock_config_entry.add_to_hass(hass) discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( @@ -270,10 +293,12 @@ async def test_zeroconf_device_exists_abort( async def test_zeroconf_with_uuid_device_exists_abort( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_ipp_config_flow: MagicMock, ) -> None: """Test we abort zeroconf flow if printer already configured.""" - await init_integration(hass, aioclient_mock, skip_setup=True) + mock_config_entry.add_to_hass(hass) discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) discovery_info.properties = { @@ -292,10 +317,12 @@ async def test_zeroconf_with_uuid_device_exists_abort( async def test_zeroconf_empty_unique_id( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_ipp_config_flow: MagicMock, ) -> None: """Test zeroconf flow if printer lacks (empty) unique identification.""" - mock_connection(aioclient_mock, no_unique_id=True) + printer = mock_ipp_config_flow.printer.return_value + printer.unique_id = None discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) discovery_info.properties = { @@ -312,10 +339,12 @@ async def test_zeroconf_empty_unique_id( async def test_zeroconf_no_unique_id( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_ipp_config_flow: MagicMock, ) -> None: """Test zeroconf flow if printer lacks unique identification.""" - mock_connection(aioclient_mock, no_unique_id=True) + printer = mock_ipp_config_flow.printer.return_value + printer.unique_id = None discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( @@ -328,11 +357,10 @@ async def test_zeroconf_no_unique_id( async def test_full_user_flow_implementation( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_ipp_config_flow: MagicMock, ) -> None: """Test the full manual user flow from start to finish.""" - mock_connection(aioclient_mock) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -341,11 +369,10 @@ async def test_full_user_flow_implementation( assert result["step_id"] == "user" assert result["type"] == FlowResultType.FORM - with patch("homeassistant.components.ipp.async_setup_entry", return_value=True): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"}, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"}, + ) assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.31" @@ -359,11 +386,10 @@ async def test_full_user_flow_implementation( async def test_full_zeroconf_flow_implementation( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_ipp_config_flow: MagicMock, ) -> None: """Test the full manual user flow from start to finish.""" - mock_connection(aioclient_mock) - discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPP_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -374,10 +400,9 @@ async def test_full_zeroconf_flow_implementation( assert result["step_id"] == "zeroconf_confirm" assert result["type"] == FlowResultType.FORM - with patch("homeassistant.components.ipp.async_setup_entry", return_value=True): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "EPSON XP-6000 Series" @@ -393,11 +418,10 @@ async def test_full_zeroconf_flow_implementation( async def test_full_zeroconf_tls_flow_implementation( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_ipp_config_flow: MagicMock, ) -> None: """Test the full manual user flow from start to finish.""" - mock_connection(aioclient_mock, ssl=True) - discovery_info = dataclasses.replace(MOCK_ZEROCONF_IPPS_SERVICE_INFO) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -409,10 +433,9 @@ async def test_full_zeroconf_tls_flow_implementation( assert result["type"] == FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "EPSON XP-6000 Series"} - with patch("homeassistant.components.ipp.async_setup_entry", return_value=True): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "EPSON XP-6000 Series" diff --git a/tests/components/ipp/test_init.py b/tests/components/ipp/test_init.py index 32060b4df86c4a..f502c30068ccc4 100644 --- a/tests/components/ipp/test_init.py +++ b/tests/components/ipp/test_init.py @@ -1,33 +1,45 @@ """Tests for the IPP integration.""" +from unittest.mock import AsyncMock, MagicMock, patch + +from pyipp import IPPConnectionError + from homeassistant.components.ipp.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from . import init_integration - -from tests.test_util.aiohttp import AiohttpClientMocker +from tests.common import MockConfigEntry +@patch( + "homeassistant.components.ipp.coordinator.IPP._request", + side_effect=IPPConnectionError, +) async def test_config_entry_not_ready( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + mock_request: MagicMock, hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: """Test the IPP configuration entry not ready.""" - entry = await init_integration(hass, aioclient_mock, conn_error=True) - assert entry.state is ConfigEntryState.SETUP_RETRY + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_request.call_count == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY -async def test_unload_config_entry( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_ipp: AsyncMock, ) -> None: - """Test the IPP configuration entry unloading.""" - entry = await init_integration(hass, aioclient_mock) + """Test the IPP configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() - assert hass.data[DOMAIN] - assert entry.entry_id in hass.data[DOMAIN] - assert entry.state is ConfigEntryState.LOADED + assert mock_config_entry.entry_id in hass.data[DOMAIN] + assert mock_config_entry.state is ConfigEntryState.LOADED - await hass.config_entries.async_unload(entry.entry_id) + await hass.config_entries.async_unload(mock_config_entry.entry_id) await hass.async_block_till_done() - - assert entry.entry_id not in hass.data[DOMAIN] - assert entry.state is ConfigEntryState.NOT_LOADED + assert mock_config_entry.entry_id not in hass.data[DOMAIN] + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index f8dd94ffc727ea..ebebd18bc722cc 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -1,119 +1,105 @@ """Tests for the IPP sensor platform.""" -from datetime import datetime -from unittest.mock import patch - -from homeassistant.components.ipp.const import DOMAIN -from homeassistant.components.sensor import ( - ATTR_OPTIONS as SENSOR_ATTR_OPTIONS, - DOMAIN as SENSOR_DOMAIN, -) +from unittest.mock import AsyncMock + +import pytest + +from homeassistant.components.sensor import ATTR_OPTIONS from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt as dt_util -from . import init_integration, mock_connection - -from tests.test_util.aiohttp import AiohttpClientMocker +from tests.common import MockConfigEntry +@pytest.mark.freeze_time("2019-11-11 09:10:32+00:00") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, ) -> None: """Test the creation and values of the IPP sensors.""" - mock_connection(aioclient_mock) - - entry = await init_integration(hass, aioclient_mock, skip_setup=True) - registry = er.async_get(hass) - - # Pre-create registry entries for disabled by default sensors - registry.async_get_or_create( - SENSOR_DOMAIN, - DOMAIN, - "cfe92100-67c4-11d4-a45f-f8d027761251_uptime", - suggested_object_id="epson_xp_6000_series_uptime", - disabled_by=None, - ) - - test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=dt_util.UTC) - with patch("homeassistant.components.ipp.sensor.utcnow", return_value=test_time): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.epson_xp_6000_series") + state = hass.states.get("sensor.test_ha_1000_series") assert state assert state.attributes.get(ATTR_ICON) == "mdi:printer" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(SENSOR_ATTR_OPTIONS) == ["idle", "printing", "stopped"] + assert state.attributes.get(ATTR_OPTIONS) == ["idle", "printing", "stopped"] - entry = registry.async_get("sensor.epson_xp_6000_series") + entry = entity_registry.async_get("sensor.test_ha_1000_series") assert entry assert entry.translation_key == "printer" - state = hass.states.get("sensor.epson_xp_6000_series_black_ink") + state = hass.states.get("sensor.test_ha_1000_series_black_ink") assert state assert state.attributes.get(ATTR_ICON) == "mdi:water" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is PERCENTAGE assert state.state == "58" - state = hass.states.get("sensor.epson_xp_6000_series_photo_black_ink") + state = hass.states.get("sensor.test_ha_1000_series_photo_black_ink") assert state assert state.attributes.get(ATTR_ICON) == "mdi:water" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is PERCENTAGE assert state.state == "98" - state = hass.states.get("sensor.epson_xp_6000_series_cyan_ink") + state = hass.states.get("sensor.test_ha_1000_series_cyan_ink") assert state assert state.attributes.get(ATTR_ICON) == "mdi:water" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is PERCENTAGE assert state.state == "91" - state = hass.states.get("sensor.epson_xp_6000_series_yellow_ink") + state = hass.states.get("sensor.test_ha_1000_series_yellow_ink") assert state assert state.attributes.get(ATTR_ICON) == "mdi:water" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is PERCENTAGE assert state.state == "95" - state = hass.states.get("sensor.epson_xp_6000_series_magenta_ink") + state = hass.states.get("sensor.test_ha_1000_series_magenta_ink") assert state assert state.attributes.get(ATTR_ICON) == "mdi:water" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is PERCENTAGE assert state.state == "73" - state = hass.states.get("sensor.epson_xp_6000_series_uptime") + state = hass.states.get("sensor.test_ha_1000_series_uptime") assert state assert state.attributes.get(ATTR_ICON) == "mdi:clock-outline" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.state == "2019-10-26T15:37:00+00:00" + assert state.state == "2019-11-11T09:10:02+00:00" - entry = registry.async_get("sensor.epson_xp_6000_series_uptime") + entry = entity_registry.async_get("sensor.test_ha_1000_series_uptime") assert entry assert entry.unique_id == "cfe92100-67c4-11d4-a45f-f8d027761251_uptime" async def test_disabled_by_default_sensors( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + init_integration: MockConfigEntry, ) -> None: """Test the disabled by default IPP sensors.""" - await init_integration(hass, aioclient_mock) registry = er.async_get(hass) - state = hass.states.get("sensor.epson_xp_6000_series_uptime") + state = hass.states.get("sensor.test_ha_1000_series_uptime") assert state is None - entry = registry.async_get("sensor.epson_xp_6000_series_uptime") + entry = registry.async_get("sensor.test_ha_1000_series_uptime") assert entry assert entry.disabled assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION async def test_missing_entry_unique_id( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_ipp: AsyncMock, ) -> None: """Test the unique_id of IPP sensor when printer is missing identifiers.""" - entry = await init_integration(hass, aioclient_mock, uuid=None, unique_id=None) + mock_config_entry.unique_id = None + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + registry = er.async_get(hass) - entity = registry.async_get("sensor.epson_xp_6000_series") + entity = registry.async_get("sensor.test_ha_1000_series") assert entity - assert entity.unique_id == f"{entry.entry_id}_printer" + assert entity.unique_id == f"{mock_config_entry.entry_id}_printer" From e8397063d3b11196a5e0849570872930003ea7ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Jul 2023 12:56:33 -1000 Subject: [PATCH 0296/1009] Optimize bluetooth base scanners for python3.11+ (#96165) --- .../components/bluetooth/base_scanner.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluetooth/base_scanner.py b/homeassistant/components/bluetooth/base_scanner.py index e8de285138e8ac..455619182abdb7 100644 --- a/homeassistant/components/bluetooth/base_scanner.py +++ b/homeassistant/components/bluetooth/base_scanner.py @@ -303,7 +303,19 @@ def _async_on_advertisement( ) -> None: """Call the registered callback.""" self._last_detection = advertisement_monotonic_time - if prev_discovery := self._discovered_device_advertisement_datas.get(address): + try: + prev_discovery = self._discovered_device_advertisement_datas[address] + except KeyError: + # We expect this is the rare case and since py3.11+ has + # near zero cost try on success, and we can avoid .get() + # which is slower than [] we use the try/except pattern. + device = BLEDevice( + address=address, + name=local_name, + details=self._details | details, + rssi=rssi, # deprecated, will be removed in newer bleak + ) + else: # Merge the new data with the old data # to function the same as BlueZ which # merges the dicts on PropertiesChanged @@ -344,13 +356,6 @@ def _async_on_advertisement( device.details = self._details | details # pylint: disable-next=protected-access device._rssi = rssi # deprecated, will be removed in newer bleak - else: - device = BLEDevice( - address=address, - name=local_name, - details=self._details | details, - rssi=rssi, # deprecated, will be removed in newer bleak - ) advertisement_data = AdvertisementData( local_name=None if local_name == "" else local_name, From 995fb993e6d50a3101af39462d1553f3a8c42473 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Jul 2023 12:57:04 -1000 Subject: [PATCH 0297/1009] Avoid probing ESPHome devices when we do not have the encryption key (#95820) --- .../components/esphome/config_flow.py | 34 +++++++--- tests/components/esphome/test_config_flow.py | 66 +++++++++++++++++++ 2 files changed, 90 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index ecd49718559b2b..9ed7ad7123d394 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -55,6 +55,7 @@ def __init__(self) -> None: self._host: str | None = None self._port: int | None = None self._password: str | None = None + self._noise_required: bool | None = None self._noise_psk: str | None = None self._device_info: DeviceInfo | None = None self._reauth_entry: ConfigEntry | None = None @@ -151,33 +152,45 @@ def _name(self, value: str) -> None: self.context["title_placeholders"] = {"name": self._name} async def _async_try_fetch_device_info(self) -> FlowResult: - error = await self.fetch_device_info() - - if error == ERROR_REQUIRES_ENCRYPTION_KEY: + """Try to fetch device info and return any errors.""" + response: str | None + if self._noise_required: + # If we already know we need encryption, don't try to fetch device info + # without encryption. + response = ERROR_REQUIRES_ENCRYPTION_KEY + else: + # After 2024.08, stop trying to fetch device info without encryption + # so we can avoid probe requests to check for password. At this point + # most devices should announce encryption support and password is + # deprecated and can be discovered by trying to connect only after they + # interact with the flow since it is expected to be a rare case. + response = await self.fetch_device_info() + + if response == 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() + response = 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() + response = await self.fetch_device_info() # If the fetched key is invalid, unset it again. - if error == ERROR_INVALID_ENCRYPTION_KEY: + if response == ERROR_INVALID_ENCRYPTION_KEY: self._noise_psk = None - error = ERROR_REQUIRES_ENCRYPTION_KEY + response = ERROR_REQUIRES_ENCRYPTION_KEY - if error == ERROR_REQUIRES_ENCRYPTION_KEY: + if response == ERROR_REQUIRES_ENCRYPTION_KEY: return await self.async_step_encryption_key() - if error is not None: - return await self._async_step_user_base(error=error) + if response is not None: + return await self._async_step_user_base(error=response) return await self._async_authenticate_or_add() async def _async_authenticate_or_add(self) -> FlowResult: @@ -220,6 +233,7 @@ async def async_step_zeroconf( self._device_name = device_name self._host = discovery_info.host self._port = discovery_info.port + self._noise_required = bool(discovery_info.properties.get("api_encryption")) # Check if already configured await self.async_set_unique_id(mac_address) diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index f5b7795a57bd9d..28d411be9391ad 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -1233,6 +1233,72 @@ async def test_zeroconf_encryption_key_via_dashboard( assert mock_client.noise_psk == VALID_NOISE_PSK +async def test_zeroconf_encryption_key_via_dashboard_with_api_encryption_prop( + hass: HomeAssistant, + mock_client, + mock_zeroconf: None, + mock_dashboard, + mock_setup_entry: None, +) -> None: + """Test encryption key retrieved from dashboard with api_encryption property set.""" + 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", + "api_encryption": "any", + }, + type="mock_type", + ) + flow = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + ) + + assert flow["type"] == FlowResultType.FORM + assert flow["step_id"] == "discovery_confirm" + + mock_dashboard["configured"].append( + { + "name": "test8266", + "configuration": "test8266.yaml", + } + ) + + await dashboard.async_get_dashboard(hass).async_refresh() + + mock_client.device_info.side_effect = [ + DeviceInfo( + uses_password=False, + name="test8266", + mac_address="11:22:33:44:55:AA", + ), + ] + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key: + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], user_input={} + ) + + assert len(mock_get_encryption_key.mock_calls) == 1 + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "test8266" + assert result["data"][CONF_HOST] == "192.168.43.183" + assert result["data"][CONF_PORT] == 6053 + assert result["data"][CONF_NOISE_PSK] == VALID_NOISE_PSK + + assert result["result"] + assert result["result"].unique_id == "11:22:33:44:55:aa" + + assert mock_client.noise_psk == VALID_NOISE_PSK + + async def test_zeroconf_no_encryption_key_via_dashboard( hass: HomeAssistant, mock_client, From 32b3fa1734f1d056dd186a40251c5d1c0e6be5e6 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 9 Jul 2023 16:49:44 -0700 Subject: [PATCH 0298/1009] 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 14a81f2c665186..2af0cb30f1e89f 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 b503e72d3a6e4c..d76ac78f7e9069 100644 --- a/homeassistant/components/rainbird/coordinator.py +++ b/homeassistant/components/rainbird/coordinator.py @@ -9,6 +9,7 @@ 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 @@ def __init__( name: str, controller: AsyncRainbirdController, serial_number: str, + model_info: ModelAndVersion, ) -> None: """Initialize ZoneStateUpdateCoordinator.""" super().__init__( @@ -54,6 +56,7 @@ def __init__( 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 @@ def device_info(self) -> DeviceInfo: 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 22f238ce55360b..21ad5230581393 100644 --- a/tests/components/rainbird/conftest.py +++ b/tests/components/rainbird/conftest.py @@ -35,6 +35,8 @@ # 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 4bf214c50f70c9..2ecdfcc537f072 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 1aefbd8b86e2dfdd115708f29c32e244d36d3253 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Jul 2023 15:18:32 -1000 Subject: [PATCH 0299/1009] Bump zeroconf to 0.71.0 (#96183) --- 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 1c5d25dfb3d2b8..473e08f5c8095b 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.70.0"] + "requirements": ["zeroconf==0.71.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b38bd073eaf5f3..16b25353183347 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.70.0 +zeroconf==0.71.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 ce569e14c1b919..48538b013e07b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2738,7 +2738,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.70.0 +zeroconf==0.71.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 480302b7e7e6e0..8b3e3a261654f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2008,7 +2008,7 @@ youless-api==1.0.1 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.70.0 +zeroconf==0.71.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From 1c54b2e025652e8061b61c00e71dbfcb75a94afc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Jul 2023 15:18:48 -1000 Subject: [PATCH 0300/1009] Reduce system_log overhead (#96177) --- homeassistant/components/system_log/__init__.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 8a5f53d52de284..f025013cc2be0e 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -116,6 +116,19 @@ def _safe_get_message(record: logging.LogRecord) -> str: class LogEntry: """Store HA log entries.""" + __slots__ = ( + "first_occurred", + "timestamp", + "name", + "level", + "message", + "exception", + "root_cause", + "source", + "count", + "key", + ) + def __init__(self, record: logging.LogRecord, source: tuple[str, int]) -> None: """Initialize a log entry.""" self.first_occurred = self.timestamp = record.created @@ -134,7 +147,7 @@ def __init__(self, record: logging.LogRecord, source: tuple[str, int]) -> None: self.root_cause = str(traceback.extract_tb(tb)[-1]) self.source = source self.count = 1 - self.hash = str([self.name, *self.source, self.root_cause]) + self.key = (self.name, source, self.root_cause) def to_dict(self): """Convert object into dict to maintain backward compatibility.""" @@ -160,7 +173,7 @@ def __init__(self, maxlen=50): def add_entry(self, entry: LogEntry) -> None: """Add a new entry.""" - key = entry.hash + key = entry.key if key in self: # Update stored entry From c4a39bbfb12604a014fc50e4550f88fdcc6f0ace Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 9 Jul 2023 19:38:05 -0700 Subject: [PATCH 0301/1009] Remove Legacy Works With Nest (#96111) * Remove Legacy Works With Nest * Simplify nest configuration * Cleanup legacy nest config entries --- .coveragerc | 1 - homeassistant/components/nest/__init__.py | 34 +- homeassistant/components/nest/camera.py | 225 ++++++++- homeassistant/components/nest/camera_sdm.py | 228 --------- homeassistant/components/nest/climate.py | 356 ++++++++++++++- homeassistant/components/nest/climate_sdm.py | 357 --------------- homeassistant/components/nest/config_flow.py | 187 -------- .../components/nest/legacy/__init__.py | 432 ------------------ .../components/nest/legacy/binary_sensor.py | 164 ------- .../components/nest/legacy/camera.py | 147 ------ .../components/nest/legacy/climate.py | 339 -------------- homeassistant/components/nest/legacy/const.py | 6 - .../components/nest/legacy/local_auth.py | 52 --- .../components/nest/legacy/sensor.py | 233 ---------- homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/nest/sensor.py | 100 +++- homeassistant/components/nest/sensor_sdm.py | 104 ----- homeassistant/components/nest/strings.json | 18 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - .../{test_camera_sdm.py => test_camera.py} | 0 .../{test_climate_sdm.py => test_climate.py} | 0 ...config_flow_sdm.py => test_config_flow.py} | 0 .../nest/test_config_flow_legacy.py | 242 ---------- tests/components/nest/test_diagnostics.py | 17 - .../nest/{test_init_sdm.py => test_init.py} | 28 ++ tests/components/nest/test_init_legacy.py | 76 --- tests/components/nest/test_local_auth.py | 51 --- 28 files changed, 704 insertions(+), 2701 deletions(-) delete mode 100644 homeassistant/components/nest/camera_sdm.py delete mode 100644 homeassistant/components/nest/climate_sdm.py delete mode 100644 homeassistant/components/nest/legacy/__init__.py delete mode 100644 homeassistant/components/nest/legacy/binary_sensor.py delete mode 100644 homeassistant/components/nest/legacy/camera.py delete mode 100644 homeassistant/components/nest/legacy/climate.py delete mode 100644 homeassistant/components/nest/legacy/const.py delete mode 100644 homeassistant/components/nest/legacy/local_auth.py delete mode 100644 homeassistant/components/nest/legacy/sensor.py delete mode 100644 homeassistant/components/nest/sensor_sdm.py rename tests/components/nest/{test_camera_sdm.py => test_camera.py} (100%) rename tests/components/nest/{test_climate_sdm.py => test_climate.py} (100%) rename tests/components/nest/{test_config_flow_sdm.py => test_config_flow.py} (100%) delete mode 100644 tests/components/nest/test_config_flow_legacy.py rename tests/components/nest/{test_init_sdm.py => test_init.py} (90%) delete mode 100644 tests/components/nest/test_init_legacy.py delete mode 100644 tests/components/nest/test_local_auth.py diff --git a/.coveragerc b/.coveragerc index 0d44a63633ae43..2e10d1be2570c7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -755,7 +755,6 @@ omit = homeassistant/components/neato/switch.py homeassistant/components/neato/vacuum.py homeassistant/components/nederlandse_spoorwegen/sensor.py - homeassistant/components/nest/legacy/* homeassistant/components/netdata/sensor.py homeassistant/components/netgear/__init__.py homeassistant/components/netgear/button.py diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 092e8ea08d6356..2645139f702bad 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -46,23 +46,22 @@ config_validation as cv, device_registry as dr, entity_registry as er, + issue_registry as ir, ) from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.typing import ConfigType -from . import api, config_flow +from . import api from .const import ( CONF_PROJECT_ID, CONF_SUBSCRIBER_ID, CONF_SUBSCRIBER_ID_IMPORTED, DATA_DEVICE_MANAGER, - DATA_NEST_CONFIG, DATA_SDM, DATA_SUBSCRIBER, DOMAIN, ) from .events import EVENT_NAME_MAP, NEST_EVENT -from .legacy import async_setup_legacy, async_setup_legacy_entry from .media_source import ( async_get_media_event_store, async_get_media_source_devices, @@ -114,15 +113,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.http.register_view(NestEventMediaView(hass)) hass.http.register_view(NestEventMediaThumbnailView(hass)) - if DOMAIN not in config: - return True # ConfigMode.SDM_APPLICATION_CREDENTIALS - - hass.data[DOMAIN][DATA_NEST_CONFIG] = config[DOMAIN] - - config_mode = config_flow.get_config_mode(hass) - if config_mode == config_flow.ConfigMode.LEGACY: - return await async_setup_legacy(hass, config) - + if DOMAIN in config and CONF_PROJECT_ID not in config[DOMAIN]: + ir.async_create_issue( + hass, + DOMAIN, + "legacy_nest_deprecated", + breaks_in_ha_version="2023.8.0", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="legacy_nest_deprecated", + translation_placeholders={ + "documentation_url": "https://www.home-assistant.io/integrations/nest/", + }, + ) + return False return True @@ -167,9 +171,9 @@ async def async_handle_event(self, event_message: EventMessage) -> None: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nest from a config entry with dispatch between old/new flows.""" - config_mode = config_flow.get_config_mode(hass) - if DATA_SDM not in entry.data or config_mode == config_flow.ConfigMode.LEGACY: - return await async_setup_legacy_entry(hass, entry) + if DATA_SDM not in entry.data: + hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) + return False if entry.unique_id != entry.data[CONF_PROJECT_ID]: hass.config_entries.async_update_entry( diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index 7ae3e0db943961..3f8c99d76583e1 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -1,19 +1,228 @@ -"""Support for Nest cameras that dispatches between API versions.""" +"""Support for Google Nest SDM Cameras.""" +from __future__ import annotations +import asyncio +from collections.abc import Callable +import datetime +import functools +import logging +from pathlib import Path + +from google_nest_sdm.camera_traits import ( + CameraImageTrait, + CameraLiveStreamTrait, + RtspStream, + StreamingProtocol, +) +from google_nest_sdm.device import Device +from google_nest_sdm.device_manager import DeviceManager +from google_nest_sdm.exceptions import ApiException + +from homeassistant.components.camera import Camera, CameraEntityFeature, StreamType +from homeassistant.components.stream import CONF_EXTRA_PART_WAIT_TIME 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 homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util.dt import utcnow + +from .const import DATA_DEVICE_MANAGER, DOMAIN +from .device_info import NestDeviceInfo + +_LOGGER = logging.getLogger(__name__) -from .camera_sdm import async_setup_sdm_entry -from .const import DATA_SDM -from .legacy.camera import async_setup_legacy_entry +PLACEHOLDER = Path(__file__).parent / "placeholder.png" + +# Used to schedule an alarm to refresh the stream before expiration +STREAM_EXPIRATION_BUFFER = datetime.timedelta(seconds=30) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the cameras.""" - if DATA_SDM not in entry.data: - await async_setup_legacy_entry(hass, entry, async_add_entities) - return - await async_setup_sdm_entry(hass, entry, async_add_entities) + + device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][ + DATA_DEVICE_MANAGER + ] + entities = [] + for device in device_manager.devices.values(): + if ( + CameraImageTrait.NAME in device.traits + or CameraLiveStreamTrait.NAME in device.traits + ): + entities.append(NestCamera(device)) + async_add_entities(entities) + + +class NestCamera(Camera): + """Devices that support cameras.""" + + _attr_has_entity_name = True + _attr_name = None + + def __init__(self, device: Device) -> None: + """Initialize the camera.""" + super().__init__() + self._device = device + self._device_info = NestDeviceInfo(device) + self._stream: RtspStream | None = None + self._create_stream_url_lock = asyncio.Lock() + self._stream_refresh_unsub: Callable[[], None] | None = None + self._attr_is_streaming = CameraLiveStreamTrait.NAME in self._device.traits + self.stream_options[CONF_EXTRA_PART_WAIT_TIME] = 3 + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + # The API "name" field is a unique device identifier. + return f"{self._device.name}-camera" + + @property + def device_info(self) -> DeviceInfo: + """Return device specific attributes.""" + return self._device_info.device_info + + @property + def brand(self) -> str | None: + """Return the camera brand.""" + return self._device_info.device_brand + + @property + def model(self) -> str | None: + """Return the camera model.""" + return self._device_info.device_model + + @property + def supported_features(self) -> CameraEntityFeature: + """Flag supported features.""" + supported_features = CameraEntityFeature(0) + if CameraLiveStreamTrait.NAME in self._device.traits: + supported_features |= CameraEntityFeature.STREAM + return supported_features + + @property + def frontend_stream_type(self) -> StreamType | None: + """Return the type of stream supported by this camera.""" + if CameraLiveStreamTrait.NAME not in self._device.traits: + return None + trait = self._device.traits[CameraLiveStreamTrait.NAME] + if StreamingProtocol.WEB_RTC in trait.supported_protocols: + return StreamType.WEB_RTC + return super().frontend_stream_type + + @property + def available(self) -> bool: + """Return True if entity is available.""" + # Cameras are marked unavailable on stream errors in #54659 however nest + # streams have a high error rate (#60353). Given nest streams are so flaky, + # marking the stream unavailable has other side effects like not showing + # the camera image which sometimes are still able to work. Until the + # streams are fixed, just leave the streams as available. + return True + + async def stream_source(self) -> str | None: + """Return the source of the stream.""" + if not self.supported_features & CameraEntityFeature.STREAM: + return None + if CameraLiveStreamTrait.NAME not in self._device.traits: + return None + trait = self._device.traits[CameraLiveStreamTrait.NAME] + if StreamingProtocol.RTSP not in trait.supported_protocols: + return None + async with self._create_stream_url_lock: + if not self._stream: + _LOGGER.debug("Fetching stream url") + try: + self._stream = await trait.generate_rtsp_stream() + except ApiException as err: + raise HomeAssistantError(f"Nest API error: {err}") from err + self._schedule_stream_refresh() + assert self._stream + if self._stream.expires_at < utcnow(): + _LOGGER.warning("Stream already expired") + return self._stream.rtsp_stream_url + + def _schedule_stream_refresh(self) -> None: + """Schedules an alarm to refresh the stream url before expiration.""" + assert self._stream + _LOGGER.debug("New stream url expires at %s", self._stream.expires_at) + refresh_time = self._stream.expires_at - STREAM_EXPIRATION_BUFFER + # Schedule an alarm to extend the stream + if self._stream_refresh_unsub is not None: + self._stream_refresh_unsub() + + self._stream_refresh_unsub = async_track_point_in_utc_time( + self.hass, + self._handle_stream_refresh, + refresh_time, + ) + + async def _handle_stream_refresh(self, now: datetime.datetime) -> None: + """Alarm that fires to check if the stream should be refreshed.""" + if not self._stream: + return + _LOGGER.debug("Extending stream url") + try: + self._stream = await self._stream.extend_rtsp_stream() + except ApiException as err: + _LOGGER.debug("Failed to extend stream: %s", err) + # Next attempt to catch a url will get a new one + self._stream = None + if self.stream: + await self.stream.stop() + self.stream = None + return + # Update the stream worker with the latest valid url + if self.stream: + self.stream.update_source(self._stream.rtsp_stream_url) + self._schedule_stream_refresh() + + async def async_will_remove_from_hass(self) -> None: + """Invalidates the RTSP token when unloaded.""" + if self._stream: + _LOGGER.debug("Invalidating stream") + try: + await self._stream.stop_rtsp_stream() + except ApiException as err: + _LOGGER.debug( + "Failed to revoke stream token, will rely on ttl: %s", err + ) + if self._stream_refresh_unsub: + self._stream_refresh_unsub() + + async def async_added_to_hass(self) -> None: + """Run when entity is added to register update signal handler.""" + self.async_on_remove( + self._device.add_update_listener(self.async_write_ha_state) + ) + + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: + """Return bytes of camera image.""" + # Use the thumbnail from RTSP stream, or a placeholder if stream is + # not supported (e.g. WebRTC) + stream = await self.async_create_stream() + if stream: + return await stream.async_get_image(width, height) + return await self.hass.async_add_executor_job(self.placeholder_image) + + @classmethod + @functools.cache + def placeholder_image(cls) -> bytes: + """Return placeholder image to use when no stream is available.""" + return PLACEHOLDER.read_bytes() + + async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None: + """Return the source of the stream.""" + trait: CameraLiveStreamTrait = self._device.traits[CameraLiveStreamTrait.NAME] + if StreamingProtocol.WEB_RTC not in trait.supported_protocols: + return await super().async_handle_web_rtc_offer(offer_sdp) + try: + stream = await trait.generate_web_rtc_stream(offer_sdp) + except ApiException as err: + raise HomeAssistantError(f"Nest API error: {err}") from err + return stream.answer_sdp diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py deleted file mode 100644 index 3eceb448fa4e89..00000000000000 --- a/homeassistant/components/nest/camera_sdm.py +++ /dev/null @@ -1,228 +0,0 @@ -"""Support for Google Nest SDM Cameras.""" -from __future__ import annotations - -import asyncio -from collections.abc import Callable -import datetime -import functools -import logging -from pathlib import Path - -from google_nest_sdm.camera_traits import ( - CameraImageTrait, - CameraLiveStreamTrait, - RtspStream, - StreamingProtocol, -) -from google_nest_sdm.device import Device -from google_nest_sdm.device_manager import DeviceManager -from google_nest_sdm.exceptions import ApiException - -from homeassistant.components.camera import Camera, CameraEntityFeature, StreamType -from homeassistant.components.stream import CONF_EXTRA_PART_WAIT_TIME -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 homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.util.dt import utcnow - -from .const import DATA_DEVICE_MANAGER, DOMAIN -from .device_info import NestDeviceInfo - -_LOGGER = logging.getLogger(__name__) - -PLACEHOLDER = Path(__file__).parent / "placeholder.png" - -# Used to schedule an alarm to refresh the stream before expiration -STREAM_EXPIRATION_BUFFER = datetime.timedelta(seconds=30) - - -async def async_setup_sdm_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up the cameras.""" - - device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][ - DATA_DEVICE_MANAGER - ] - entities = [] - for device in device_manager.devices.values(): - if ( - CameraImageTrait.NAME in device.traits - or CameraLiveStreamTrait.NAME in device.traits - ): - entities.append(NestCamera(device)) - async_add_entities(entities) - - -class NestCamera(Camera): - """Devices that support cameras.""" - - _attr_has_entity_name = True - _attr_name = None - - def __init__(self, device: Device) -> None: - """Initialize the camera.""" - super().__init__() - self._device = device - self._device_info = NestDeviceInfo(device) - self._stream: RtspStream | None = None - self._create_stream_url_lock = asyncio.Lock() - self._stream_refresh_unsub: Callable[[], None] | None = None - self._attr_is_streaming = CameraLiveStreamTrait.NAME in self._device.traits - self.stream_options[CONF_EXTRA_PART_WAIT_TIME] = 3 - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - # The API "name" field is a unique device identifier. - return f"{self._device.name}-camera" - - @property - def device_info(self) -> DeviceInfo: - """Return device specific attributes.""" - return self._device_info.device_info - - @property - def brand(self) -> str | None: - """Return the camera brand.""" - return self._device_info.device_brand - - @property - def model(self) -> str | None: - """Return the camera model.""" - return self._device_info.device_model - - @property - def supported_features(self) -> CameraEntityFeature: - """Flag supported features.""" - supported_features = CameraEntityFeature(0) - if CameraLiveStreamTrait.NAME in self._device.traits: - supported_features |= CameraEntityFeature.STREAM - return supported_features - - @property - def frontend_stream_type(self) -> StreamType | None: - """Return the type of stream supported by this camera.""" - if CameraLiveStreamTrait.NAME not in self._device.traits: - return None - trait = self._device.traits[CameraLiveStreamTrait.NAME] - if StreamingProtocol.WEB_RTC in trait.supported_protocols: - return StreamType.WEB_RTC - return super().frontend_stream_type - - @property - def available(self) -> bool: - """Return True if entity is available.""" - # Cameras are marked unavailable on stream errors in #54659 however nest - # streams have a high error rate (#60353). Given nest streams are so flaky, - # marking the stream unavailable has other side effects like not showing - # the camera image which sometimes are still able to work. Until the - # streams are fixed, just leave the streams as available. - return True - - async def stream_source(self) -> str | None: - """Return the source of the stream.""" - if not self.supported_features & CameraEntityFeature.STREAM: - return None - if CameraLiveStreamTrait.NAME not in self._device.traits: - return None - trait = self._device.traits[CameraLiveStreamTrait.NAME] - if StreamingProtocol.RTSP not in trait.supported_protocols: - return None - async with self._create_stream_url_lock: - if not self._stream: - _LOGGER.debug("Fetching stream url") - try: - self._stream = await trait.generate_rtsp_stream() - except ApiException as err: - raise HomeAssistantError(f"Nest API error: {err}") from err - self._schedule_stream_refresh() - assert self._stream - if self._stream.expires_at < utcnow(): - _LOGGER.warning("Stream already expired") - return self._stream.rtsp_stream_url - - def _schedule_stream_refresh(self) -> None: - """Schedules an alarm to refresh the stream url before expiration.""" - assert self._stream - _LOGGER.debug("New stream url expires at %s", self._stream.expires_at) - refresh_time = self._stream.expires_at - STREAM_EXPIRATION_BUFFER - # Schedule an alarm to extend the stream - if self._stream_refresh_unsub is not None: - self._stream_refresh_unsub() - - self._stream_refresh_unsub = async_track_point_in_utc_time( - self.hass, - self._handle_stream_refresh, - refresh_time, - ) - - async def _handle_stream_refresh(self, now: datetime.datetime) -> None: - """Alarm that fires to check if the stream should be refreshed.""" - if not self._stream: - return - _LOGGER.debug("Extending stream url") - try: - self._stream = await self._stream.extend_rtsp_stream() - except ApiException as err: - _LOGGER.debug("Failed to extend stream: %s", err) - # Next attempt to catch a url will get a new one - self._stream = None - if self.stream: - await self.stream.stop() - self.stream = None - return - # Update the stream worker with the latest valid url - if self.stream: - self.stream.update_source(self._stream.rtsp_stream_url) - self._schedule_stream_refresh() - - async def async_will_remove_from_hass(self) -> None: - """Invalidates the RTSP token when unloaded.""" - if self._stream: - _LOGGER.debug("Invalidating stream") - try: - await self._stream.stop_rtsp_stream() - except ApiException as err: - _LOGGER.debug( - "Failed to revoke stream token, will rely on ttl: %s", err - ) - if self._stream_refresh_unsub: - self._stream_refresh_unsub() - - async def async_added_to_hass(self) -> None: - """Run when entity is added to register update signal handler.""" - self.async_on_remove( - self._device.add_update_listener(self.async_write_ha_state) - ) - - async def async_camera_image( - self, width: int | None = None, height: int | None = None - ) -> bytes | None: - """Return bytes of camera image.""" - # Use the thumbnail from RTSP stream, or a placeholder if stream is - # not supported (e.g. WebRTC) - stream = await self.async_create_stream() - if stream: - return await stream.async_get_image(width, height) - return await self.hass.async_add_executor_job(self.placeholder_image) - - @classmethod - @functools.cache - def placeholder_image(cls) -> bytes: - """Return placeholder image to use when no stream is available.""" - return PLACEHOLDER.read_bytes() - - async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None: - """Return the source of the stream.""" - trait: CameraLiveStreamTrait = self._device.traits[CameraLiveStreamTrait.NAME] - if StreamingProtocol.WEB_RTC not in trait.supported_protocols: - return await super().async_handle_web_rtc_offer(offer_sdp) - try: - stream = await trait.generate_web_rtc_stream(offer_sdp) - except ApiException as err: - raise HomeAssistantError(f"Nest API error: {err}") from err - return stream.answer_sdp diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index 372909d00c227b..307bd201b4dd05 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -1,19 +1,357 @@ -"""Support for Nest climate that dispatches between API versions.""" +"""Support for Google Nest SDM climate devices.""" +from __future__ import annotations +from typing import Any, cast + +from google_nest_sdm.device import Device +from google_nest_sdm.device_manager import DeviceManager +from google_nest_sdm.device_traits import FanTrait, TemperatureTrait +from google_nest_sdm.exceptions import ApiException +from google_nest_sdm.thermostat_traits import ( + ThermostatEcoTrait, + ThermostatHeatCoolTrait, + ThermostatHvacTrait, + ThermostatModeTrait, + ThermostatTemperatureSetpointTrait, +) + +from homeassistant.components.climate import ( + ATTR_HVAC_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + FAN_OFF, + FAN_ON, + PRESET_ECO, + PRESET_NONE, + ClimateEntity, + ClimateEntityFeature, + HVACAction, + HVACMode, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .climate_sdm import async_setup_sdm_entry -from .const import DATA_SDM -from .legacy.climate import async_setup_legacy_entry +from .const import DATA_DEVICE_MANAGER, DOMAIN +from .device_info import NestDeviceInfo + +# Mapping for sdm.devices.traits.ThermostatMode mode field +THERMOSTAT_MODE_MAP: dict[str, HVACMode] = { + "OFF": HVACMode.OFF, + "HEAT": HVACMode.HEAT, + "COOL": HVACMode.COOL, + "HEATCOOL": HVACMode.HEAT_COOL, +} +THERMOSTAT_INV_MODE_MAP = {v: k for k, v in THERMOSTAT_MODE_MAP.items()} + +# Mode for sdm.devices.traits.ThermostatEco +THERMOSTAT_ECO_MODE = "MANUAL_ECO" + +# Mapping for sdm.devices.traits.ThermostatHvac status field +THERMOSTAT_HVAC_STATUS_MAP = { + "OFF": HVACAction.OFF, + "HEATING": HVACAction.HEATING, + "COOLING": HVACAction.COOLING, +} + +THERMOSTAT_RANGE_MODES = [HVACMode.HEAT_COOL, HVACMode.AUTO] + +PRESET_MODE_MAP = { + "MANUAL_ECO": PRESET_ECO, + "OFF": PRESET_NONE, +} +PRESET_INV_MODE_MAP = {v: k for k, v in PRESET_MODE_MAP.items()} + +FAN_MODE_MAP = { + "ON": FAN_ON, + "OFF": FAN_OFF, +} +FAN_INV_MODE_MAP = {v: k for k, v in FAN_MODE_MAP.items()} +FAN_INV_MODES = list(FAN_INV_MODE_MAP) + +MAX_FAN_DURATION = 43200 # 15 hours is the max in the SDM API +MIN_TEMP = 10 +MAX_TEMP = 32 async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the climate platform.""" - if DATA_SDM not in entry.data: - await async_setup_legacy_entry(hass, entry, async_add_entities) - return - await async_setup_sdm_entry(hass, entry, async_add_entities) + """Set up the client entities.""" + + device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][ + DATA_DEVICE_MANAGER + ] + entities = [] + for device in device_manager.devices.values(): + if ThermostatHvacTrait.NAME in device.traits: + entities.append(ThermostatEntity(device)) + async_add_entities(entities) + + +class ThermostatEntity(ClimateEntity): + """A nest thermostat climate entity.""" + + _attr_min_temp = MIN_TEMP + _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.""" + self._device = device + self._device_info = NestDeviceInfo(device) + + @property + def unique_id(self) -> str | None: + """Return a unique ID.""" + # The API "name" field is a unique device identifier. + return self._device.name + + @property + def device_info(self) -> DeviceInfo: + """Return device specific attributes.""" + return self._device_info.device_info + + @property + def available(self) -> bool: + """Return device availability.""" + return self._device_info.available + + async def async_added_to_hass(self) -> None: + """Run when entity is added to register update signal handler.""" + self._attr_supported_features = self._get_supported_features() + self.async_on_remove( + self._device.add_update_listener(self.async_write_ha_state) + ) + + @property + def temperature_unit(self) -> str: + """Return the unit of temperature measurement for the system.""" + return UnitOfTemperature.CELSIUS + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + if TemperatureTrait.NAME not in self._device.traits: + return None + trait: TemperatureTrait = self._device.traits[TemperatureTrait.NAME] + return trait.ambient_temperature_celsius + + @property + def target_temperature(self) -> float | None: + """Return the temperature currently set to be reached.""" + if not (trait := self._target_temperature_trait): + return None + if self.hvac_mode == HVACMode.HEAT: + return trait.heat_celsius + if self.hvac_mode == HVACMode.COOL: + return trait.cool_celsius + return None + + @property + def target_temperature_high(self) -> float | None: + """Return the upper bound target temperature.""" + if self.hvac_mode != HVACMode.HEAT_COOL: + return None + if not (trait := self._target_temperature_trait): + return None + return trait.cool_celsius + + @property + def target_temperature_low(self) -> float | None: + """Return the lower bound target temperature.""" + if self.hvac_mode != HVACMode.HEAT_COOL: + return None + if not (trait := self._target_temperature_trait): + return None + return trait.heat_celsius + + @property + def _target_temperature_trait( + self, + ) -> ThermostatHeatCoolTrait | None: + """Return the correct trait with a target temp depending on mode.""" + if ( + self.preset_mode == PRESET_ECO + and ThermostatEcoTrait.NAME in self._device.traits + ): + return cast( + ThermostatEcoTrait, self._device.traits[ThermostatEcoTrait.NAME] + ) + if ThermostatTemperatureSetpointTrait.NAME in self._device.traits: + return cast( + ThermostatTemperatureSetpointTrait, + self._device.traits[ThermostatTemperatureSetpointTrait.NAME], + ) + return None + + @property + def hvac_mode(self) -> HVACMode: + """Return the current operation (e.g. heat, cool, idle).""" + hvac_mode = HVACMode.OFF + if ThermostatModeTrait.NAME in self._device.traits: + trait = self._device.traits[ThermostatModeTrait.NAME] + if trait.mode in THERMOSTAT_MODE_MAP: + hvac_mode = THERMOSTAT_MODE_MAP[trait.mode] + return hvac_mode + + @property + def hvac_modes(self) -> list[HVACMode]: + """List of available operation modes.""" + supported_modes = [] + for mode in self._get_device_hvac_modes: + if mode in THERMOSTAT_MODE_MAP: + supported_modes.append(THERMOSTAT_MODE_MAP[mode]) + return supported_modes + + @property + def _get_device_hvac_modes(self) -> set[str]: + """Return the set of SDM API hvac modes supported by the device.""" + modes = [] + if ThermostatModeTrait.NAME in self._device.traits: + trait = self._device.traits[ThermostatModeTrait.NAME] + modes.extend(trait.available_modes) + return set(modes) + + @property + def hvac_action(self) -> HVACAction | None: + """Return the current HVAC action (heating, cooling).""" + trait = self._device.traits[ThermostatHvacTrait.NAME] + if trait.status == "OFF" and self.hvac_mode != HVACMode.OFF: + return HVACAction.IDLE + return THERMOSTAT_HVAC_STATUS_MAP.get(trait.status) + + @property + def preset_mode(self) -> str: + """Return the current active preset.""" + if ThermostatEcoTrait.NAME in self._device.traits: + trait = self._device.traits[ThermostatEcoTrait.NAME] + return PRESET_MODE_MAP.get(trait.mode, PRESET_NONE) + return PRESET_NONE + + @property + def preset_modes(self) -> list[str]: + """Return the available presets.""" + modes = [] + if ThermostatEcoTrait.NAME in self._device.traits: + trait = self._device.traits[ThermostatEcoTrait.NAME] + for mode in trait.available_modes: + if mode in PRESET_MODE_MAP: + modes.append(PRESET_MODE_MAP[mode]) + return modes + + @property + def fan_mode(self) -> str: + """Return the current fan mode.""" + if ( + self.supported_features & ClimateEntityFeature.FAN_MODE + and FanTrait.NAME in self._device.traits + ): + trait = self._device.traits[FanTrait.NAME] + return FAN_MODE_MAP.get(trait.timer_mode, FAN_OFF) + return FAN_OFF + + @property + def fan_modes(self) -> list[str]: + """Return the list of available fan modes.""" + if ( + self.supported_features & ClimateEntityFeature.FAN_MODE + and FanTrait.NAME in self._device.traits + ): + return FAN_INV_MODES + return [] + + def _get_supported_features(self) -> ClimateEntityFeature: + """Compute the bitmap of supported features from the current state.""" + features = ClimateEntityFeature(0) + if HVACMode.HEAT_COOL in self.hvac_modes: + features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + if HVACMode.HEAT in self.hvac_modes or HVACMode.COOL in self.hvac_modes: + features |= ClimateEntityFeature.TARGET_TEMPERATURE + if ThermostatEcoTrait.NAME in self._device.traits: + features |= ClimateEntityFeature.PRESET_MODE + if FanTrait.NAME in self._device.traits: + # Fan trait may be present without actually support fan mode + fan_trait = self._device.traits[FanTrait.NAME] + if fan_trait.timer_mode is not None: + features |= ClimateEntityFeature.FAN_MODE + return features + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target hvac mode.""" + if hvac_mode not in self.hvac_modes: + raise ValueError(f"Unsupported hvac_mode '{hvac_mode}'") + api_mode = THERMOSTAT_INV_MODE_MAP[hvac_mode] + trait = self._device.traits[ThermostatModeTrait.NAME] + try: + await trait.set_mode(api_mode) + except ApiException as err: + raise HomeAssistantError( + f"Error setting {self.entity_id} HVAC mode to {hvac_mode}: {err}" + ) from err + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + hvac_mode = self.hvac_mode + if kwargs.get(ATTR_HVAC_MODE) is not None: + hvac_mode = kwargs[ATTR_HVAC_MODE] + await self.async_set_hvac_mode(hvac_mode) + low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) + high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) + temp = kwargs.get(ATTR_TEMPERATURE) + if ThermostatTemperatureSetpointTrait.NAME not in self._device.traits: + raise HomeAssistantError( + f"Error setting {self.entity_id} temperature to {kwargs}: " + "Unable to find setpoint trait." + ) + trait = self._device.traits[ThermostatTemperatureSetpointTrait.NAME] + try: + if self.preset_mode == PRESET_ECO or hvac_mode == HVACMode.HEAT_COOL: + if low_temp and high_temp: + await trait.set_range(low_temp, high_temp) + elif hvac_mode == HVACMode.COOL and temp: + await trait.set_cool(temp) + elif hvac_mode == HVACMode.HEAT and temp: + await trait.set_heat(temp) + except ApiException as err: + raise HomeAssistantError( + f"Error setting {self.entity_id} temperature to {kwargs}: {err}" + ) from err + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new target preset mode.""" + if preset_mode not in self.preset_modes: + raise ValueError(f"Unsupported preset_mode '{preset_mode}'") + if self.preset_mode == preset_mode: # API doesn't like duplicate preset modes + return + trait = self._device.traits[ThermostatEcoTrait.NAME] + try: + await trait.set_mode(PRESET_INV_MODE_MAP[preset_mode]) + except ApiException as err: + raise HomeAssistantError( + f"Error setting {self.entity_id} preset mode to {preset_mode}: {err}" + ) from err + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + if fan_mode not in self.fan_modes: + raise ValueError(f"Unsupported fan_mode '{fan_mode}'") + if fan_mode == FAN_ON and self.hvac_mode == HVACMode.OFF: + raise ValueError( + "Cannot turn on fan, please set an HVAC mode (e.g. heat/cool) first" + ) + trait = self._device.traits[FanTrait.NAME] + duration = None + if fan_mode != FAN_OFF: + duration = MAX_FAN_DURATION + try: + await trait.set_timer(FAN_INV_MODE_MAP[fan_mode], duration=duration) + except ApiException as err: + raise HomeAssistantError( + f"Error setting {self.entity_id} fan mode to {fan_mode}: {err}" + ) from err diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py deleted file mode 100644 index ca975ed055d5f4..00000000000000 --- a/homeassistant/components/nest/climate_sdm.py +++ /dev/null @@ -1,357 +0,0 @@ -"""Support for Google Nest SDM climate devices.""" -from __future__ import annotations - -from typing import Any, cast - -from google_nest_sdm.device import Device -from google_nest_sdm.device_manager import DeviceManager -from google_nest_sdm.device_traits import FanTrait, TemperatureTrait -from google_nest_sdm.exceptions import ApiException -from google_nest_sdm.thermostat_traits import ( - ThermostatEcoTrait, - ThermostatHeatCoolTrait, - ThermostatHvacTrait, - ThermostatModeTrait, - ThermostatTemperatureSetpointTrait, -) - -from homeassistant.components.climate import ( - ATTR_HVAC_MODE, - ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, - FAN_OFF, - FAN_ON, - PRESET_ECO, - PRESET_NONE, - ClimateEntity, - ClimateEntityFeature, - HVACAction, - HVACMode, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature -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 DATA_DEVICE_MANAGER, DOMAIN -from .device_info import NestDeviceInfo - -# Mapping for sdm.devices.traits.ThermostatMode mode field -THERMOSTAT_MODE_MAP: dict[str, HVACMode] = { - "OFF": HVACMode.OFF, - "HEAT": HVACMode.HEAT, - "COOL": HVACMode.COOL, - "HEATCOOL": HVACMode.HEAT_COOL, -} -THERMOSTAT_INV_MODE_MAP = {v: k for k, v in THERMOSTAT_MODE_MAP.items()} - -# Mode for sdm.devices.traits.ThermostatEco -THERMOSTAT_ECO_MODE = "MANUAL_ECO" - -# Mapping for sdm.devices.traits.ThermostatHvac status field -THERMOSTAT_HVAC_STATUS_MAP = { - "OFF": HVACAction.OFF, - "HEATING": HVACAction.HEATING, - "COOLING": HVACAction.COOLING, -} - -THERMOSTAT_RANGE_MODES = [HVACMode.HEAT_COOL, HVACMode.AUTO] - -PRESET_MODE_MAP = { - "MANUAL_ECO": PRESET_ECO, - "OFF": PRESET_NONE, -} -PRESET_INV_MODE_MAP = {v: k for k, v in PRESET_MODE_MAP.items()} - -FAN_MODE_MAP = { - "ON": FAN_ON, - "OFF": FAN_OFF, -} -FAN_INV_MODE_MAP = {v: k for k, v in FAN_MODE_MAP.items()} -FAN_INV_MODES = list(FAN_INV_MODE_MAP) - -MAX_FAN_DURATION = 43200 # 15 hours is the max in the SDM API -MIN_TEMP = 10 -MAX_TEMP = 32 - - -async def async_setup_sdm_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up the client entities.""" - - device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][ - DATA_DEVICE_MANAGER - ] - entities = [] - for device in device_manager.devices.values(): - if ThermostatHvacTrait.NAME in device.traits: - entities.append(ThermostatEntity(device)) - async_add_entities(entities) - - -class ThermostatEntity(ClimateEntity): - """A nest thermostat climate entity.""" - - _attr_min_temp = MIN_TEMP - _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.""" - self._device = device - self._device_info = NestDeviceInfo(device) - - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - # The API "name" field is a unique device identifier. - return self._device.name - - @property - def device_info(self) -> DeviceInfo: - """Return device specific attributes.""" - return self._device_info.device_info - - @property - def available(self) -> bool: - """Return device availability.""" - return self._device_info.available - - async def async_added_to_hass(self) -> None: - """Run when entity is added to register update signal handler.""" - self._attr_supported_features = self._get_supported_features() - self.async_on_remove( - self._device.add_update_listener(self.async_write_ha_state) - ) - - @property - def temperature_unit(self) -> str: - """Return the unit of temperature measurement for the system.""" - return UnitOfTemperature.CELSIUS - - @property - def current_temperature(self) -> float | None: - """Return the current temperature.""" - if TemperatureTrait.NAME not in self._device.traits: - return None - trait: TemperatureTrait = self._device.traits[TemperatureTrait.NAME] - return trait.ambient_temperature_celsius - - @property - def target_temperature(self) -> float | None: - """Return the temperature currently set to be reached.""" - if not (trait := self._target_temperature_trait): - return None - if self.hvac_mode == HVACMode.HEAT: - return trait.heat_celsius - if self.hvac_mode == HVACMode.COOL: - return trait.cool_celsius - return None - - @property - def target_temperature_high(self) -> float | None: - """Return the upper bound target temperature.""" - if self.hvac_mode != HVACMode.HEAT_COOL: - return None - if not (trait := self._target_temperature_trait): - return None - return trait.cool_celsius - - @property - def target_temperature_low(self) -> float | None: - """Return the lower bound target temperature.""" - if self.hvac_mode != HVACMode.HEAT_COOL: - return None - if not (trait := self._target_temperature_trait): - return None - return trait.heat_celsius - - @property - def _target_temperature_trait( - self, - ) -> ThermostatHeatCoolTrait | None: - """Return the correct trait with a target temp depending on mode.""" - if ( - self.preset_mode == PRESET_ECO - and ThermostatEcoTrait.NAME in self._device.traits - ): - return cast( - ThermostatEcoTrait, self._device.traits[ThermostatEcoTrait.NAME] - ) - if ThermostatTemperatureSetpointTrait.NAME in self._device.traits: - return cast( - ThermostatTemperatureSetpointTrait, - self._device.traits[ThermostatTemperatureSetpointTrait.NAME], - ) - return None - - @property - def hvac_mode(self) -> HVACMode: - """Return the current operation (e.g. heat, cool, idle).""" - hvac_mode = HVACMode.OFF - if ThermostatModeTrait.NAME in self._device.traits: - trait = self._device.traits[ThermostatModeTrait.NAME] - if trait.mode in THERMOSTAT_MODE_MAP: - hvac_mode = THERMOSTAT_MODE_MAP[trait.mode] - return hvac_mode - - @property - def hvac_modes(self) -> list[HVACMode]: - """List of available operation modes.""" - supported_modes = [] - for mode in self._get_device_hvac_modes: - if mode in THERMOSTAT_MODE_MAP: - supported_modes.append(THERMOSTAT_MODE_MAP[mode]) - return supported_modes - - @property - def _get_device_hvac_modes(self) -> set[str]: - """Return the set of SDM API hvac modes supported by the device.""" - modes = [] - if ThermostatModeTrait.NAME in self._device.traits: - trait = self._device.traits[ThermostatModeTrait.NAME] - modes.extend(trait.available_modes) - return set(modes) - - @property - def hvac_action(self) -> HVACAction | None: - """Return the current HVAC action (heating, cooling).""" - trait = self._device.traits[ThermostatHvacTrait.NAME] - if trait.status == "OFF" and self.hvac_mode != HVACMode.OFF: - return HVACAction.IDLE - return THERMOSTAT_HVAC_STATUS_MAP.get(trait.status) - - @property - def preset_mode(self) -> str: - """Return the current active preset.""" - if ThermostatEcoTrait.NAME in self._device.traits: - trait = self._device.traits[ThermostatEcoTrait.NAME] - return PRESET_MODE_MAP.get(trait.mode, PRESET_NONE) - return PRESET_NONE - - @property - def preset_modes(self) -> list[str]: - """Return the available presets.""" - modes = [] - if ThermostatEcoTrait.NAME in self._device.traits: - trait = self._device.traits[ThermostatEcoTrait.NAME] - for mode in trait.available_modes: - if mode in PRESET_MODE_MAP: - modes.append(PRESET_MODE_MAP[mode]) - return modes - - @property - def fan_mode(self) -> str: - """Return the current fan mode.""" - if ( - self.supported_features & ClimateEntityFeature.FAN_MODE - and FanTrait.NAME in self._device.traits - ): - trait = self._device.traits[FanTrait.NAME] - return FAN_MODE_MAP.get(trait.timer_mode, FAN_OFF) - return FAN_OFF - - @property - def fan_modes(self) -> list[str]: - """Return the list of available fan modes.""" - if ( - self.supported_features & ClimateEntityFeature.FAN_MODE - and FanTrait.NAME in self._device.traits - ): - return FAN_INV_MODES - return [] - - def _get_supported_features(self) -> ClimateEntityFeature: - """Compute the bitmap of supported features from the current state.""" - features = ClimateEntityFeature(0) - if HVACMode.HEAT_COOL in self.hvac_modes: - features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE - if HVACMode.HEAT in self.hvac_modes or HVACMode.COOL in self.hvac_modes: - features |= ClimateEntityFeature.TARGET_TEMPERATURE - if ThermostatEcoTrait.NAME in self._device.traits: - features |= ClimateEntityFeature.PRESET_MODE - if FanTrait.NAME in self._device.traits: - # Fan trait may be present without actually support fan mode - fan_trait = self._device.traits[FanTrait.NAME] - if fan_trait.timer_mode is not None: - features |= ClimateEntityFeature.FAN_MODE - return features - - async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Set new target hvac mode.""" - if hvac_mode not in self.hvac_modes: - raise ValueError(f"Unsupported hvac_mode '{hvac_mode}'") - api_mode = THERMOSTAT_INV_MODE_MAP[hvac_mode] - trait = self._device.traits[ThermostatModeTrait.NAME] - try: - await trait.set_mode(api_mode) - except ApiException as err: - raise HomeAssistantError( - f"Error setting {self.entity_id} HVAC mode to {hvac_mode}: {err}" - ) from err - - async def async_set_temperature(self, **kwargs: Any) -> None: - """Set new target temperature.""" - hvac_mode = self.hvac_mode - if kwargs.get(ATTR_HVAC_MODE) is not None: - hvac_mode = kwargs[ATTR_HVAC_MODE] - await self.async_set_hvac_mode(hvac_mode) - low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) - high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) - temp = kwargs.get(ATTR_TEMPERATURE) - if ThermostatTemperatureSetpointTrait.NAME not in self._device.traits: - raise HomeAssistantError( - f"Error setting {self.entity_id} temperature to {kwargs}: " - "Unable to find setpoint trait." - ) - trait = self._device.traits[ThermostatTemperatureSetpointTrait.NAME] - try: - if self.preset_mode == PRESET_ECO or hvac_mode == HVACMode.HEAT_COOL: - if low_temp and high_temp: - await trait.set_range(low_temp, high_temp) - elif hvac_mode == HVACMode.COOL and temp: - await trait.set_cool(temp) - elif hvac_mode == HVACMode.HEAT and temp: - await trait.set_heat(temp) - except ApiException as err: - raise HomeAssistantError( - f"Error setting {self.entity_id} temperature to {kwargs}: {err}" - ) from err - - async def async_set_preset_mode(self, preset_mode: str) -> None: - """Set new target preset mode.""" - if preset_mode not in self.preset_modes: - raise ValueError(f"Unsupported preset_mode '{preset_mode}'") - if self.preset_mode == preset_mode: # API doesn't like duplicate preset modes - return - trait = self._device.traits[ThermostatEcoTrait.NAME] - try: - await trait.set_mode(PRESET_INV_MODE_MAP[preset_mode]) - except ApiException as err: - raise HomeAssistantError( - f"Error setting {self.entity_id} preset mode to {preset_mode}: {err}" - ) from err - - async def async_set_fan_mode(self, fan_mode: str) -> None: - """Set new target fan mode.""" - if fan_mode not in self.fan_modes: - raise ValueError(f"Unsupported fan_mode '{fan_mode}'") - if fan_mode == FAN_ON and self.hvac_mode == HVACMode.OFF: - raise ValueError( - "Cannot turn on fan, please set an HVAC mode (e.g. heat/cool) first" - ) - trait = self._device.traits[FanTrait.NAME] - duration = None - if fan_mode != FAN_OFF: - duration = MAX_FAN_DURATION - try: - await trait.set_timer(FAN_INV_MODE_MAP[fan_mode], duration=duration) - except ApiException as err: - raise HomeAssistantError( - f"Error setting {self.entity_id} fan mode to {fan_mode}: {err}" - ) from err diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index d20057f4e28e6e..381cc36449d0e3 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -9,15 +9,10 @@ """ from __future__ import annotations -import asyncio -from collections import OrderedDict from collections.abc import Iterable, Mapping -from enum import Enum import logging -import os from typing import Any -import async_timeout from google_nest_sdm.exceptions import ( ApiException, AuthException, @@ -28,12 +23,9 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util import get_random_string -from homeassistant.util.json import JsonObjectType, load_json_object from . import api from .const import ( @@ -71,69 +63,12 @@ _LOGGER = logging.getLogger(__name__) -class ConfigMode(Enum): - """Integration configuration mode.""" - - SDM = 1 # SDM api with configuration.yaml - LEGACY = 2 # "Works with Nest" API - SDM_APPLICATION_CREDENTIALS = 3 # Config entry only - - -def get_config_mode(hass: HomeAssistant) -> ConfigMode: - """Return the integration configuration mode.""" - if DOMAIN not in hass.data or not ( - config := hass.data[DOMAIN].get(DATA_NEST_CONFIG) - ): - return ConfigMode.SDM_APPLICATION_CREDENTIALS - if CONF_PROJECT_ID in config: - return ConfigMode.SDM - return ConfigMode.LEGACY - - def _generate_subscription_id(cloud_project_id: str) -> str: """Create a new subscription id.""" rnd = get_random_string(SUBSCRIPTION_RAND_LENGTH) return SUBSCRIPTION_FORMAT.format(cloud_project_id=cloud_project_id, rnd=rnd) -@callback -def register_flow_implementation( - hass: HomeAssistant, - domain: str, - name: str, - gen_authorize_url: str, - convert_code: str, -) -> None: - """Register a flow implementation for legacy api. - - domain: Domain of the component responsible for the implementation. - name: Name of the component. - gen_authorize_url: Coroutine function to generate the authorize url. - convert_code: Coroutine function to convert a code to an access token. - """ - if DATA_FLOW_IMPL not in hass.data: - hass.data[DATA_FLOW_IMPL] = OrderedDict() - - hass.data[DATA_FLOW_IMPL][domain] = { - "domain": domain, - "name": name, - "gen_authorize_url": gen_authorize_url, - "convert_code": convert_code, - } - - -class NestAuthError(HomeAssistantError): - """Base class for Nest auth errors.""" - - -class CodeInvalid(NestAuthError): - """Raised when invalid authorization code.""" - - -class UnexpectedStateError(HomeAssistantError): - """Raised when the config flow is invoked in a 'should not happen' case.""" - - def generate_config_title(structures: Iterable[Structure]) -> str | None: """Pick a user friendly config title based on the Google Home name(s).""" names: list[str] = [] @@ -160,11 +95,6 @@ def __init__(self) -> None: # Possible name to use for config entry based on the Google Home name self._structure_config_title: str | None = None - @property - def config_mode(self) -> ConfigMode: - """Return the configuration type for this flow.""" - return get_config_mode(self.hass) - def _async_reauth_entry(self) -> ConfigEntry | None: """Return existing entry for reauth.""" if self.source != SOURCE_REAUTH or not ( @@ -206,7 +136,6 @@ async def async_generate_authorize_url(self) -> str: async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Complete OAuth setup and finish pubsub or finish.""" _LOGGER.debug("Finishing post-oauth configuration") - assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" self._data.update(data) if self.source == SOURCE_REAUTH: _LOGGER.debug("Skipping Pub/Sub configuration") @@ -215,7 +144,6 @@ async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" - assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" self._data.update(entry_data) return await self.async_step_reauth_confirm() @@ -224,7 +152,6 @@ async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm reauth dialog.""" - assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" if user_input is None: return self.async_show_form(step_id="reauth_confirm") return await self.async_step_user() @@ -233,8 +160,6 @@ async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initialized by the user.""" - if self.config_mode == ConfigMode.LEGACY: - return await self.async_step_init(user_input) self._data[DATA_SDM] = {} if self.source == SOURCE_REAUTH: return await super().async_step_user(user_input) @@ -391,7 +316,6 @@ async def async_step_pubsub( async def async_step_finish(self, data: dict[str, Any] | None = None) -> FlowResult: """Create an entry for the SDM flow.""" _LOGGER.debug("Creating/updating configuration entry") - assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" # Update existing config entry when in the reauth flow. if entry := self._async_reauth_entry(): self.hass.config_entries.async_update_entry( @@ -404,114 +328,3 @@ async def async_step_finish(self, data: dict[str, Any] | None = None) -> FlowRes if self._structure_config_title: title = self._structure_config_title return self.async_create_entry(title=title, data=self._data) - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle a flow start.""" - assert ( - self.config_mode == ConfigMode.LEGACY - ), "Step only supported for legacy API" - - flows = self.hass.data.get(DATA_FLOW_IMPL, {}) - - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - if not flows: - return self.async_abort(reason="missing_configuration") - - if len(flows) == 1: - self.flow_impl = list(flows)[0] - return await self.async_step_link() - - if user_input is not None: - self.flow_impl = user_input["flow_impl"] - return await self.async_step_link() - - return self.async_show_form( - step_id="init", - data_schema=vol.Schema({vol.Required("flow_impl"): vol.In(list(flows))}), - ) - - async def async_step_link( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Attempt to link with the Nest account. - - Route the user to a website to authenticate with Nest. Depending on - implementation type we expect a pin or an external component to - deliver the authentication code. - """ - assert ( - self.config_mode == ConfigMode.LEGACY - ), "Step only supported for legacy API" - - flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl] - - errors = {} - - if user_input is not None: - try: - async with async_timeout.timeout(10): - tokens = await flow["convert_code"](user_input["code"]) - return self._entry_from_tokens( - f"Nest (via {flow['name']})", flow, tokens - ) - - except asyncio.TimeoutError: - errors["code"] = "timeout" - except CodeInvalid: - errors["code"] = "invalid_pin" - except NestAuthError: - errors["code"] = "unknown" - except Exception: # pylint: disable=broad-except - errors["code"] = "internal_error" - _LOGGER.exception("Unexpected error resolving code") - - try: - async with async_timeout.timeout(10): - url = await flow["gen_authorize_url"](self.flow_id) - except asyncio.TimeoutError: - return self.async_abort(reason="authorize_url_timeout") - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected error generating auth url") - return self.async_abort(reason="unknown_authorize_url_generation") - - return self.async_show_form( - step_id="link", - description_placeholders={"url": url}, - data_schema=vol.Schema({vol.Required("code"): str}), - errors=errors, - ) - - async def async_step_import(self, info: dict[str, Any]) -> FlowResult: - """Import existing auth from Nest.""" - assert ( - self.config_mode == ConfigMode.LEGACY - ), "Step only supported for legacy API" - - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - config_path = info["nest_conf_path"] - - if not await self.hass.async_add_executor_job(os.path.isfile, config_path): - self.flow_impl = DOMAIN # type: ignore[assignment] - return await self.async_step_link() - - flow = self.hass.data[DATA_FLOW_IMPL][DOMAIN] - tokens = await self.hass.async_add_executor_job(load_json_object, config_path) - - return self._entry_from_tokens( - "Nest (import from configuration.yaml)", flow, tokens - ) - - @callback - def _entry_from_tokens( - self, title: str, flow: dict[str, Any], tokens: JsonObjectType - ) -> FlowResult: - """Create an entry from tokens.""" - return self.async_create_entry( - title=title, data={"tokens": tokens, "impl_domain": flow["domain"]} - ) diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py deleted file mode 100644 index 88d046fb62becc..00000000000000 --- a/homeassistant/components/nest/legacy/__init__.py +++ /dev/null @@ -1,432 +0,0 @@ -"""Support for Nest devices.""" -# mypy: ignore-errors - -from datetime import datetime, timedelta -import logging -import threading - -from nest import Nest -from nest.nest import APIError, AuthorizationError -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_FILENAME, - CONF_STRUCTURE, - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, - Platform, -) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, issue_registry as ir -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.entity import DeviceInfo, Entity - -from . import local_auth -from .const import DATA_NEST, DATA_NEST_CONFIG, DOMAIN, SIGNAL_NEST_UPDATE - -_CONFIGURING = {} -_LOGGER = logging.getLogger(__name__) - -PLATFORMS = [ - Platform.BINARY_SENSOR, - Platform.CAMERA, - Platform.CLIMATE, - Platform.SENSOR, -] - -# Configuration for the legacy nest API -SERVICE_CANCEL_ETA = "cancel_eta" -SERVICE_SET_ETA = "set_eta" - -NEST_CONFIG_FILE = "nest.conf" - -ATTR_ETA = "eta" -ATTR_ETA_WINDOW = "eta_window" -ATTR_STRUCTURE = "structure" -ATTR_TRIP_ID = "trip_id" - -AWAY_MODE_AWAY = "away" -AWAY_MODE_HOME = "home" - -ATTR_AWAY_MODE = "away_mode" -SERVICE_SET_AWAY_MODE = "set_away_mode" - -# Services for the legacy API - -SET_AWAY_MODE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_AWAY_MODE): vol.In([AWAY_MODE_AWAY, AWAY_MODE_HOME]), - vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), - } -) - -SET_ETA_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ETA): cv.time_period, - vol.Optional(ATTR_TRIP_ID): cv.string, - vol.Optional(ATTR_ETA_WINDOW): cv.time_period, - vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), - } -) - -CANCEL_ETA_SCHEMA = vol.Schema( - { - vol.Required(ATTR_TRIP_ID): cv.string, - vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), - } -) - - -def nest_update_event_broker(hass, nest): - """Dispatch SIGNAL_NEST_UPDATE to devices when nest stream API received data. - - Used for the legacy nest API. - - Runs in its own thread. - """ - _LOGGER.debug("Listening for nest.update_event") - - while hass.is_running: - nest.update_event.wait() - - if not hass.is_running: - break - - nest.update_event.clear() - _LOGGER.debug("Dispatching nest data update") - dispatcher_send(hass, SIGNAL_NEST_UPDATE) - - _LOGGER.debug("Stop listening for nest.update_event") - - -async def async_setup_legacy(hass: HomeAssistant, config: dict) -> bool: - """Set up Nest components using the legacy nest API.""" - if DOMAIN not in config: - return True - - ir.async_create_issue( - hass, - DOMAIN, - "legacy_nest_deprecated", - breaks_in_ha_version="2023.8.0", - is_fixable=False, - severity=ir.IssueSeverity.WARNING, - translation_key="legacy_nest_deprecated", - translation_placeholders={ - "documentation_url": "https://www.home-assistant.io/integrations/nest/", - }, - ) - - conf = config[DOMAIN] - - local_auth.initialize(hass, conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET]) - - filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE) - access_token_cache_file = hass.config.path(filename) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={"nest_conf_path": access_token_cache_file}, - ) - ) - - # Store config to be used during entry setup - hass.data[DATA_NEST_CONFIG] = conf - - return True - - -async def async_setup_legacy_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Nest from legacy config entry.""" - - nest = Nest(access_token=entry.data["tokens"]["access_token"]) - - _LOGGER.debug("proceeding with setup") - conf = hass.data.get(DATA_NEST_CONFIG, {}) - hass.data[DATA_NEST] = NestLegacyDevice(hass, conf, nest) - if not await hass.async_add_executor_job(hass.data[DATA_NEST].initialize): - return False - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - def validate_structures(target_structures): - all_structures = [structure.name for structure in nest.structures] - for target in target_structures: - if target not in all_structures: - _LOGGER.info("Invalid structure: %s", target) - - def set_away_mode(service): - """Set the away mode for a Nest structure.""" - if ATTR_STRUCTURE in service.data: - target_structures = service.data[ATTR_STRUCTURE] - validate_structures(target_structures) - else: - target_structures = hass.data[DATA_NEST].local_structure - - for structure in nest.structures: - if structure.name in target_structures: - _LOGGER.info( - "Setting away mode for: %s to: %s", - structure.name, - service.data[ATTR_AWAY_MODE], - ) - structure.away = service.data[ATTR_AWAY_MODE] - - def set_eta(service): - """Set away mode to away and include ETA for a Nest structure.""" - if ATTR_STRUCTURE in service.data: - target_structures = service.data[ATTR_STRUCTURE] - validate_structures(target_structures) - else: - target_structures = hass.data[DATA_NEST].local_structure - - for structure in nest.structures: - if structure.name in target_structures: - if structure.thermostats: - _LOGGER.info( - "Setting away mode for: %s to: %s", - structure.name, - AWAY_MODE_AWAY, - ) - structure.away = AWAY_MODE_AWAY - - now = datetime.utcnow() - trip_id = service.data.get( - ATTR_TRIP_ID, f"trip_{int(now.timestamp())}" - ) - eta_begin = now + service.data[ATTR_ETA] - eta_window = service.data.get(ATTR_ETA_WINDOW, timedelta(minutes=1)) - eta_end = eta_begin + eta_window - _LOGGER.info( - ( - "Setting ETA for trip: %s, " - "ETA window starts at: %s and ends at: %s" - ), - trip_id, - eta_begin, - eta_end, - ) - structure.set_eta(trip_id, eta_begin, eta_end) - else: - _LOGGER.info( - "No thermostats found in structure: %s, unable to set ETA", - structure.name, - ) - - def cancel_eta(service): - """Cancel ETA for a Nest structure.""" - if ATTR_STRUCTURE in service.data: - target_structures = service.data[ATTR_STRUCTURE] - validate_structures(target_structures) - else: - target_structures = hass.data[DATA_NEST].local_structure - - for structure in nest.structures: - if structure.name in target_structures: - if structure.thermostats: - trip_id = service.data[ATTR_TRIP_ID] - _LOGGER.info("Cancelling ETA for trip: %s", trip_id) - structure.cancel_eta(trip_id) - else: - _LOGGER.info( - "No thermostats found in structure: %s, unable to cancel ETA", - structure.name, - ) - - hass.services.async_register( - DOMAIN, SERVICE_SET_AWAY_MODE, set_away_mode, schema=SET_AWAY_MODE_SCHEMA - ) - - hass.services.async_register( - DOMAIN, SERVICE_SET_ETA, set_eta, schema=SET_ETA_SCHEMA - ) - - hass.services.async_register( - DOMAIN, SERVICE_CANCEL_ETA, cancel_eta, schema=CANCEL_ETA_SCHEMA - ) - - @callback - def start_up(event): - """Start Nest update event listener.""" - threading.Thread( - name="Nest update listener", - target=nest_update_event_broker, - args=(hass, nest), - ).start() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_up) - - @callback - def shut_down(event): - """Stop Nest update event listener.""" - nest.update_event.set() - - entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) - ) - - _LOGGER.debug("async_setup_nest is done") - - return True - - -class NestLegacyDevice: - """Structure Nest functions for hass for legacy API.""" - - def __init__(self, hass, conf, nest): - """Init Nest Devices.""" - self.hass = hass - self.nest = nest - self.local_structure = conf.get(CONF_STRUCTURE) - - def initialize(self): - """Initialize Nest.""" - try: - # Do not optimize next statement, it is here for initialize - # persistence Nest API connection. - structure_names = [s.name for s in self.nest.structures] - if self.local_structure is None: - self.local_structure = structure_names - - except (AuthorizationError, APIError, OSError) as err: - _LOGGER.error("Connection error while access Nest web service: %s", err) - return False - return True - - def structures(self): - """Generate a list of structures.""" - try: - for structure in self.nest.structures: - if structure.name not in self.local_structure: - _LOGGER.debug( - "Ignoring structure %s, not in %s", - structure.name, - self.local_structure, - ) - continue - yield structure - - except (AuthorizationError, APIError, OSError) as err: - _LOGGER.error("Connection error while access Nest web service: %s", err) - - def thermostats(self): - """Generate a list of thermostats.""" - return self._devices("thermostats") - - def smoke_co_alarms(self): - """Generate a list of smoke co alarms.""" - return self._devices("smoke_co_alarms") - - def cameras(self): - """Generate a list of cameras.""" - return self._devices("cameras") - - def _devices(self, device_type): - """Generate a list of Nest devices.""" - try: - for structure in self.nest.structures: - if structure.name not in self.local_structure: - _LOGGER.debug( - "Ignoring structure %s, not in %s", - structure.name, - self.local_structure, - ) - continue - - for device in getattr(structure, device_type, []): - try: - # Do not optimize next statement, - # it is here for verify Nest API permission. - device.name_long - except KeyError: - _LOGGER.warning( - ( - "Cannot retrieve device name for [%s]" - ", please check your Nest developer " - "account permission settings" - ), - device.serial, - ) - continue - yield (structure, device) - - except (AuthorizationError, APIError, OSError) as err: - _LOGGER.error("Connection error while access Nest web service: %s", err) - - -class NestSensorDevice(Entity): - """Representation of a Nest sensor.""" - - _attr_should_poll = False - - def __init__(self, structure, device, variable): - """Initialize the sensor.""" - self.structure = structure - self.variable = variable - - if device is not None: - # device specific - self.device = device - self._name = f"{self.device.name_long} {self.variable.replace('_', ' ')}" - else: - # structure only - self.device = structure - self._name = f"{self.structure.name} {self.variable.replace('_', ' ')}" - - self._state = None - self._unit = None - - @property - def name(self): - """Return the name of the nest, if any.""" - return self._name - - @property - def unique_id(self): - """Return unique id based on device serial and variable.""" - return f"{self.device.serial}-{self.variable}" - - @property - def device_info(self) -> DeviceInfo: - """Return information about the device.""" - if not hasattr(self.device, "name_long"): - name = self.structure.name - model = "Structure" - else: - name = self.device.name_long - if self.device.is_thermostat: - model = "Thermostat" - elif self.device.is_camera: - model = "Camera" - elif self.device.is_smoke_co_alarm: - model = "Nest Protect" - else: - model = None - - return DeviceInfo( - identifiers={(DOMAIN, self.device.serial)}, - manufacturer="Nest Labs", - model=model, - name=name, - ) - - def update(self): - """Do not use NestSensorDevice directly.""" - raise NotImplementedError - - async def async_added_to_hass(self): - """Register update signal handler.""" - - async def async_update_state(): - """Update sensor state.""" - await self.async_update_ha_state(True) - - self.async_on_remove( - async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state) - ) diff --git a/homeassistant/components/nest/legacy/binary_sensor.py b/homeassistant/components/nest/legacy/binary_sensor.py deleted file mode 100644 index 5c412b86dbd7e8..00000000000000 --- a/homeassistant/components/nest/legacy/binary_sensor.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Support for Nest Thermostat binary sensors.""" -# mypy: ignore-errors - -from itertools import chain -import logging - -from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, - BinarySensorEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_BINARY_SENSORS, CONF_MONITORED_CONDITIONS -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import NestSensorDevice -from .const import DATA_NEST, DATA_NEST_CONFIG - -_LOGGER = logging.getLogger(__name__) - -BINARY_TYPES = {"online": BinarySensorDeviceClass.CONNECTIVITY} - -CLIMATE_BINARY_TYPES = { - "fan": None, - "is_using_emergency_heat": "heat", - "is_locked": None, - "has_leaf": None, -} - -CAMERA_BINARY_TYPES = { - "motion_detected": BinarySensorDeviceClass.MOTION, - "sound_detected": BinarySensorDeviceClass.SOUND, - "person_detected": BinarySensorDeviceClass.OCCUPANCY, -} - -STRUCTURE_BINARY_TYPES = {"away": None} - -STRUCTURE_BINARY_STATE_MAP = {"away": {"away": True, "home": False}} - -_BINARY_TYPES_DEPRECATED = [ - "hvac_ac_state", - "hvac_aux_heater_state", - "hvac_heater_state", - "hvac_heat_x2_state", - "hvac_heat_x3_state", - "hvac_alt_heat_state", - "hvac_alt_heat_x2_state", - "hvac_emer_heat_state", -] - -_VALID_BINARY_SENSOR_TYPES = { - **BINARY_TYPES, - **CLIMATE_BINARY_TYPES, - **CAMERA_BINARY_TYPES, - **STRUCTURE_BINARY_TYPES, -} - - -async def async_setup_legacy_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up a Nest binary sensor based on a config entry.""" - nest = hass.data[DATA_NEST] - - discovery_info = hass.data.get(DATA_NEST_CONFIG, {}).get(CONF_BINARY_SENSORS, {}) - - # Add all available binary sensors if no Nest binary sensor config is set - if discovery_info == {}: - conditions = _VALID_BINARY_SENSOR_TYPES - else: - conditions = discovery_info.get(CONF_MONITORED_CONDITIONS, {}) - - for variable in conditions: - if variable in _BINARY_TYPES_DEPRECATED: - wstr = ( - f"{variable} is no a longer supported " - "monitored_conditions. See " - "https://www.home-assistant.io/integrations/binary_sensor.nest/ " - "for valid options." - ) - _LOGGER.error(wstr) - - def get_binary_sensors(): - """Get the Nest binary sensors.""" - sensors = [] - for structure in nest.structures(): - sensors += [ - NestBinarySensor(structure, None, variable) - for variable in conditions - if variable in STRUCTURE_BINARY_TYPES - ] - device_chain = chain(nest.thermostats(), nest.smoke_co_alarms(), nest.cameras()) - for structure, device in device_chain: - sensors += [ - NestBinarySensor(structure, device, variable) - for variable in conditions - if variable in BINARY_TYPES - ] - sensors += [ - NestBinarySensor(structure, device, variable) - for variable in conditions - if variable in CLIMATE_BINARY_TYPES and device.is_thermostat - ] - - if device.is_camera: - sensors += [ - NestBinarySensor(structure, device, variable) - for variable in conditions - if variable in CAMERA_BINARY_TYPES - ] - for activity_zone in device.activity_zones: - sensors += [ - NestActivityZoneSensor(structure, device, activity_zone) - ] - - return sensors - - async_add_entities(await hass.async_add_executor_job(get_binary_sensors), True) - - -class NestBinarySensor(NestSensorDevice, BinarySensorEntity): - """Represents a Nest binary sensor.""" - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - - @property - def device_class(self): - """Return the device class of the binary sensor.""" - return _VALID_BINARY_SENSOR_TYPES.get(self.variable) - - def update(self): - """Retrieve latest state.""" - value = getattr(self.device, self.variable) - if self.variable in STRUCTURE_BINARY_TYPES: - self._state = bool(STRUCTURE_BINARY_STATE_MAP[self.variable].get(value)) - else: - self._state = bool(value) - - -class NestActivityZoneSensor(NestBinarySensor): - """Represents a Nest binary sensor for activity in a zone.""" - - def __init__(self, structure, device, zone): - """Initialize the sensor.""" - super().__init__(structure, device, "") - self.zone = zone - self._name = f"{self._name} {self.zone.name} activity" - - @property - def unique_id(self): - """Return unique id based on camera serial and zone id.""" - return f"{self.device.serial}-{self.zone.zone_id}" - - @property - def device_class(self): - """Return the device class of the binary sensor.""" - return BinarySensorDeviceClass.MOTION - - def update(self): - """Retrieve latest state.""" - self._state = self.device.has_ongoing_motion_in_zone(self.zone.zone_id) diff --git a/homeassistant/components/nest/legacy/camera.py b/homeassistant/components/nest/legacy/camera.py deleted file mode 100644 index e74f23aeaf683e..00000000000000 --- a/homeassistant/components/nest/legacy/camera.py +++ /dev/null @@ -1,147 +0,0 @@ -"""Support for Nest Cameras.""" -# mypy: ignore-errors - -from __future__ import annotations - -from datetime import timedelta -import logging - -import requests - -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, CameraEntityFeature -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.dt import utcnow - -from .const import DATA_NEST, DOMAIN - -_LOGGER = logging.getLogger(__name__) - -NEST_BRAND = "Nest" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({}) - - -async def async_setup_legacy_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up a Nest sensor based on a config entry.""" - camera_devices = await hass.async_add_executor_job(hass.data[DATA_NEST].cameras) - cameras = [NestCamera(structure, device) for structure, device in camera_devices] - async_add_entities(cameras, True) - - -class NestCamera(Camera): - """Representation of a Nest Camera.""" - - _attr_should_poll = True # Cameras default to False - _attr_supported_features = CameraEntityFeature.ON_OFF - - def __init__(self, structure, device): - """Initialize a Nest Camera.""" - super().__init__() - self.structure = structure - self.device = device - self._location = None - self._name = None - self._online = None - self._is_streaming = None - self._is_video_history_enabled = False - # Default to non-NestAware subscribed, but will be fixed during update - self._time_between_snapshots = timedelta(seconds=30) - self._last_image = None - self._next_snapshot_at = None - - @property - def name(self): - """Return the name of the nest, if any.""" - return self._name - - @property - def unique_id(self): - """Return the serial number.""" - return self.device.device_id - - @property - def device_info(self) -> DeviceInfo: - """Return information about the device.""" - return DeviceInfo( - identifiers={(DOMAIN, self.device.device_id)}, - manufacturer="Nest Labs", - model="Camera", - name=self.device.name_long, - ) - - @property - def is_recording(self): - """Return true if the device is recording.""" - return self._is_streaming - - @property - def brand(self): - """Return the brand of the camera.""" - return NEST_BRAND - - @property - def is_on(self): - """Return true if on.""" - return self._online and self._is_streaming - - def turn_off(self): - """Turn off camera.""" - _LOGGER.debug("Turn off camera %s", self._name) - # Calling Nest API in is_streaming setter. - # device.is_streaming would not immediately change until the process - # finished in Nest Cam. - self.device.is_streaming = False - - def turn_on(self): - """Turn on camera.""" - if not self._online: - _LOGGER.error("Camera %s is offline", self._name) - return - - _LOGGER.debug("Turn on camera %s", self._name) - # Calling Nest API in is_streaming setter. - # device.is_streaming would not immediately change until the process - # finished in Nest Cam. - self.device.is_streaming = True - - def update(self): - """Cache value from Python-nest.""" - self._location = self.device.where - self._name = self.device.name - self._online = self.device.online - self._is_streaming = self.device.is_streaming - self._is_video_history_enabled = self.device.is_video_history_enabled - - if self._is_video_history_enabled: - # NestAware allowed 10/min - self._time_between_snapshots = timedelta(seconds=6) - else: - # Otherwise, 2/min - self._time_between_snapshots = timedelta(seconds=30) - - def _ready_for_snapshot(self, now): - return self._next_snapshot_at is None or now > self._next_snapshot_at - - def camera_image( - self, width: int | None = None, height: int | None = None - ) -> bytes | None: - """Return a still image response from the camera.""" - now = utcnow() - if self._ready_for_snapshot(now): - url = self.device.snapshot_url - - try: - response = requests.get(url, timeout=10) - except requests.exceptions.RequestException as error: - _LOGGER.error("Error getting camera image: %s", error) - return None - - self._next_snapshot_at = now + self._time_between_snapshots - self._last_image = response.content - - return self._last_image diff --git a/homeassistant/components/nest/legacy/climate.py b/homeassistant/components/nest/legacy/climate.py deleted file mode 100644 index 323633e0ee38f2..00000000000000 --- a/homeassistant/components/nest/legacy/climate.py +++ /dev/null @@ -1,339 +0,0 @@ -"""Legacy Works with Nest climate implementation.""" -# mypy: ignore-errors - -import logging - -from nest.nest import APIError -import voluptuous as vol - -from homeassistant.components.climate import ( - ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, - FAN_AUTO, - FAN_ON, - PLATFORM_SCHEMA, - PRESET_AWAY, - PRESET_ECO, - PRESET_NONE, - ClimateEntity, - ClimateEntityFeature, - HVACAction, - HVACMode, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, CONF_SCAN_INTERVAL, UnitOfTemperature -from homeassistant.core import HomeAssistant -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DATA_NEST, DOMAIN, SIGNAL_NEST_UPDATE - -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_SCAN_INTERVAL): vol.All(vol.Coerce(int), vol.Range(min=1))} -) - -NEST_MODE_HEAT_COOL = "heat-cool" -NEST_MODE_ECO = "eco" -NEST_MODE_HEAT = "heat" -NEST_MODE_COOL = "cool" -NEST_MODE_OFF = "off" - -MODE_HASS_TO_NEST = { - HVACMode.AUTO: NEST_MODE_HEAT_COOL, - HVACMode.HEAT: NEST_MODE_HEAT, - HVACMode.COOL: NEST_MODE_COOL, - HVACMode.OFF: NEST_MODE_OFF, -} - -MODE_NEST_TO_HASS = {v: k for k, v in MODE_HASS_TO_NEST.items()} - -ACTION_NEST_TO_HASS = { - "off": HVACAction.IDLE, - "heating": HVACAction.HEATING, - "cooling": HVACAction.COOLING, -} - -PRESET_AWAY_AND_ECO = "Away and Eco" - -PRESET_MODES = [PRESET_NONE, PRESET_AWAY, PRESET_ECO, PRESET_AWAY_AND_ECO] - - -async def async_setup_legacy_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up the Nest climate device based on a config entry.""" - temp_unit = hass.config.units.temperature_unit - - thermostats = await hass.async_add_executor_job(hass.data[DATA_NEST].thermostats) - - all_devices = [ - NestThermostat(structure, device, temp_unit) - for structure, device in thermostats - ] - - async_add_entities(all_devices, True) - - -class NestThermostat(ClimateEntity): - """Representation of a Nest thermostat.""" - - _attr_should_poll = False - - def __init__(self, structure, device, temp_unit): - """Initialize the thermostat.""" - self._unit = temp_unit - self.structure = structure - self.device = device - self._fan_modes = [FAN_ON, FAN_AUTO] - - # Set the default supported features - self._attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE - ) - - # Not all nest devices support cooling and heating remove unused - self._operation_list = [] - - if self.device.can_heat and self.device.can_cool: - self._operation_list.append(HVACMode.AUTO) - self._attr_supported_features |= ( - ClimateEntityFeature.TARGET_TEMPERATURE_RANGE - ) - - # Add supported nest thermostat features - if self.device.can_heat: - self._operation_list.append(HVACMode.HEAT) - - if self.device.can_cool: - self._operation_list.append(HVACMode.COOL) - - self._operation_list.append(HVACMode.OFF) - - # feature of device - self._has_fan = self.device.has_fan - if self._has_fan: - self._attr_supported_features |= ClimateEntityFeature.FAN_MODE - - # data attributes - self._away = None - self._location = None - self._name = None - self._humidity = None - self._target_temperature = None - self._temperature = None - self._temperature_scale = None - self._mode = None - self._action = None - self._fan = None - self._eco_temperature = None - self._is_locked = None - self._locked_temperature = None - self._min_temperature = None - self._max_temperature = None - - async def async_added_to_hass(self): - """Register update signal handler.""" - - async def async_update_state(): - """Update device state.""" - await self.async_update_ha_state(True) - - self.async_on_remove( - async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state) - ) - - @property - def unique_id(self): - """Return unique ID for this device.""" - return self.device.serial - - @property - def device_info(self) -> DeviceInfo: - """Return information about the device.""" - return DeviceInfo( - identifiers={(DOMAIN, self.device.device_id)}, - manufacturer="Nest Labs", - model="Thermostat", - name=self.device.name_long, - sw_version=self.device.software_version, - ) - - @property - def name(self): - """Return the name of the nest, if any.""" - return self._name - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return self._temperature_scale - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._temperature - - @property - def hvac_mode(self) -> HVACMode: - """Return current operation ie. heat, cool, idle.""" - if self._mode == NEST_MODE_ECO: - if self.device.previous_mode in MODE_NEST_TO_HASS: - return MODE_NEST_TO_HASS[self.device.previous_mode] - - # previous_mode not supported so return the first compatible mode - return self._operation_list[0] - - return MODE_NEST_TO_HASS[self._mode] - - @property - def hvac_action(self) -> HVACAction: - """Return the current hvac action.""" - return ACTION_NEST_TO_HASS[self._action] - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - if self._mode not in (NEST_MODE_HEAT_COOL, NEST_MODE_ECO): - return self._target_temperature - return None - - @property - def target_temperature_low(self): - """Return the lower bound temperature we try to reach.""" - if self._mode == NEST_MODE_ECO: - return self._eco_temperature[0] - if self._mode == NEST_MODE_HEAT_COOL: - return self._target_temperature[0] - return None - - @property - def target_temperature_high(self): - """Return the upper bound temperature we try to reach.""" - if self._mode == NEST_MODE_ECO: - return self._eco_temperature[1] - if self._mode == NEST_MODE_HEAT_COOL: - return self._target_temperature[1] - return None - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - - temp = None - target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) - target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - if self._mode == NEST_MODE_HEAT_COOL: - if target_temp_low is not None and target_temp_high is not None: - temp = (target_temp_low, target_temp_high) - _LOGGER.debug("Nest set_temperature-output-value=%s", temp) - else: - temp = kwargs.get(ATTR_TEMPERATURE) - _LOGGER.debug("Nest set_temperature-output-value=%s", temp) - try: - if temp is not None: - self.device.target = temp - except APIError as api_error: - _LOGGER.error("An error occurred while setting temperature: %s", api_error) - # restore target temperature - self.schedule_update_ha_state(True) - - def set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Set operation mode.""" - self.device.mode = MODE_HASS_TO_NEST[hvac_mode] - - @property - def hvac_modes(self) -> list[HVACMode]: - """List of available operation modes.""" - return self._operation_list - - @property - def preset_mode(self): - """Return current preset mode.""" - if self._away and self._mode == NEST_MODE_ECO: - return PRESET_AWAY_AND_ECO - - if self._away: - return PRESET_AWAY - - if self._mode == NEST_MODE_ECO: - return PRESET_ECO - - return PRESET_NONE - - @property - def preset_modes(self): - """Return preset modes.""" - return PRESET_MODES - - def set_preset_mode(self, preset_mode): - """Set preset mode.""" - if preset_mode == self.preset_mode: - return - - need_away = preset_mode in (PRESET_AWAY, PRESET_AWAY_AND_ECO) - need_eco = preset_mode in (PRESET_ECO, PRESET_AWAY_AND_ECO) - is_away = self._away - is_eco = self._mode == NEST_MODE_ECO - - if is_away != need_away: - self.structure.away = need_away - - if is_eco != need_eco: - if need_eco: - self.device.mode = NEST_MODE_ECO - else: - self.device.mode = self.device.previous_mode - - @property - def fan_mode(self): - """Return whether the fan is on.""" - if self._has_fan: - # Return whether the fan is on - return FAN_ON if self._fan else FAN_AUTO - # No Fan available so disable slider - return None - - @property - def fan_modes(self): - """List of available fan modes.""" - if self._has_fan: - return self._fan_modes - return None - - def set_fan_mode(self, fan_mode): - """Turn fan on/off.""" - if self._has_fan: - self.device.fan = fan_mode.lower() - - @property - def min_temp(self): - """Identify min_temp in Nest API or defaults if not available.""" - return self._min_temperature - - @property - def max_temp(self): - """Identify max_temp in Nest API or defaults if not available.""" - return self._max_temperature - - def update(self): - """Cache value from Python-nest.""" - self._location = self.device.where - self._name = self.device.name - self._humidity = self.device.humidity - self._temperature = self.device.temperature - self._mode = self.device.mode - self._action = self.device.hvac_state - self._target_temperature = self.device.target - self._fan = self.device.fan - self._away = self.structure.away == "away" - self._eco_temperature = self.device.eco_temperature - self._locked_temperature = self.device.locked_temperature - self._min_temperature = self.device.min_temperature - self._max_temperature = self.device.max_temperature - self._is_locked = self.device.is_locked - if self.device.temperature_scale == "C": - self._temperature_scale = UnitOfTemperature.CELSIUS - else: - self._temperature_scale = UnitOfTemperature.FAHRENHEIT diff --git a/homeassistant/components/nest/legacy/const.py b/homeassistant/components/nest/legacy/const.py deleted file mode 100644 index 664606b9edc541..00000000000000 --- a/homeassistant/components/nest/legacy/const.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Constants used by the legacy Nest component.""" - -DOMAIN = "nest" -DATA_NEST = "nest" -DATA_NEST_CONFIG = "nest_config" -SIGNAL_NEST_UPDATE = "nest_update" diff --git a/homeassistant/components/nest/legacy/local_auth.py b/homeassistant/components/nest/legacy/local_auth.py deleted file mode 100644 index a091469cd81e24..00000000000000 --- a/homeassistant/components/nest/legacy/local_auth.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Local Nest authentication for the legacy api.""" -# mypy: ignore-errors - -import asyncio -from functools import partial -from http import HTTPStatus - -from nest.nest import AUTHORIZE_URL, AuthorizationError, NestAuth - -from homeassistant.core import callback - -from ..config_flow import CodeInvalid, NestAuthError, register_flow_implementation -from .const import DOMAIN - - -@callback -def initialize(hass, client_id, client_secret): - """Initialize a local auth provider.""" - register_flow_implementation( - hass, - DOMAIN, - "configuration.yaml", - partial(generate_auth_url, client_id), - partial(resolve_auth_code, hass, client_id, client_secret), - ) - - -async def generate_auth_url(client_id, flow_id): - """Generate an authorize url.""" - return AUTHORIZE_URL.format(client_id, flow_id) - - -async def resolve_auth_code(hass, client_id, client_secret, code): - """Resolve an authorization code.""" - - result = asyncio.Future() - auth = NestAuth( - client_id=client_id, - client_secret=client_secret, - auth_callback=result.set_result, - ) - auth.pin = code - - try: - await hass.async_add_executor_job(auth.login) - return await result - except AuthorizationError as err: - if err.response.status_code == HTTPStatus.UNAUTHORIZED: - raise CodeInvalid() from err - raise NestAuthError( - f"Unknown error: {err} ({err.response.status_code})" - ) from err diff --git a/homeassistant/components/nest/legacy/sensor.py b/homeassistant/components/nest/legacy/sensor.py deleted file mode 100644 index 3c397f3d1f444e..00000000000000 --- a/homeassistant/components/nest/legacy/sensor.py +++ /dev/null @@ -1,233 +0,0 @@ -"""Support for Nest Thermostat sensors for the legacy API.""" -# mypy: ignore-errors - -import logging - -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorStateClass, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, - CONF_SENSORS, - PERCENTAGE, - STATE_OFF, - UnitOfTemperature, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import NestSensorDevice -from .const import DATA_NEST, DATA_NEST_CONFIG - -SENSOR_TYPES = ["humidity", "operation_mode", "hvac_state"] - -TEMP_SENSOR_TYPES = ["temperature", "target"] - -PROTECT_SENSOR_TYPES = [ - "co_status", - "smoke_status", - "battery_health", - # color_status: "gray", "green", "yellow", "red" - "color_status", -] - -STRUCTURE_SENSOR_TYPES = ["eta"] - -STATE_HEAT = "heat" -STATE_COOL = "cool" - -# security_state is structure level sensor, but only meaningful when -# Nest Cam exist -STRUCTURE_CAMERA_SENSOR_TYPES = ["security_state"] - -_VALID_SENSOR_TYPES = ( - SENSOR_TYPES - + TEMP_SENSOR_TYPES - + PROTECT_SENSOR_TYPES - + STRUCTURE_SENSOR_TYPES - + STRUCTURE_CAMERA_SENSOR_TYPES -) - -SENSOR_UNITS = {"humidity": PERCENTAGE} - -SENSOR_DEVICE_CLASSES = {"humidity": SensorDeviceClass.HUMIDITY} - -SENSOR_STATE_CLASSES = {"humidity": SensorStateClass.MEASUREMENT} - -VARIABLE_NAME_MAPPING = {"eta": "eta_begin", "operation_mode": "mode"} - -VALUE_MAPPING = { - "hvac_state": {"heating": STATE_HEAT, "cooling": STATE_COOL, "off": STATE_OFF} -} - -SENSOR_TYPES_DEPRECATED = ["last_ip", "local_ip", "last_connection", "battery_level"] - -DEPRECATED_WEATHER_VARS = [ - "weather_humidity", - "weather_temperature", - "weather_condition", - "wind_speed", - "wind_direction", -] - -_SENSOR_TYPES_DEPRECATED = SENSOR_TYPES_DEPRECATED + DEPRECATED_WEATHER_VARS - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_legacy_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up a Nest sensor based on a config entry.""" - nest = hass.data[DATA_NEST] - - discovery_info = hass.data.get(DATA_NEST_CONFIG, {}).get(CONF_SENSORS, {}) - - # Add all available sensors if no Nest sensor config is set - if discovery_info == {}: - conditions = _VALID_SENSOR_TYPES - else: - conditions = discovery_info.get(CONF_MONITORED_CONDITIONS, {}) - - for variable in conditions: - if variable in _SENSOR_TYPES_DEPRECATED: - if variable in DEPRECATED_WEATHER_VARS: - wstr = ( - f"Nest no longer provides weather data like {variable}. See " - "https://www.home-assistant.io/integrations/#weather " - "for a list of other weather integrations to use." - ) - else: - wstr = ( - f"{variable} is no a longer supported " - "monitored_conditions. See " - "https://www.home-assistant.io/integrations/" - "binary_sensor.nest/ for valid options." - ) - _LOGGER.error(wstr) - - def get_sensors(): - """Get the Nest sensors.""" - all_sensors = [] - for structure in nest.structures(): - all_sensors += [ - NestBasicSensor(structure, None, variable) - for variable in conditions - if variable in STRUCTURE_SENSOR_TYPES - ] - - for structure, device in nest.thermostats(): - all_sensors += [ - NestBasicSensor(structure, device, variable) - for variable in conditions - if variable in SENSOR_TYPES - ] - all_sensors += [ - NestTempSensor(structure, device, variable) - for variable in conditions - if variable in TEMP_SENSOR_TYPES - ] - - for structure, device in nest.smoke_co_alarms(): - all_sensors += [ - NestBasicSensor(structure, device, variable) - for variable in conditions - if variable in PROTECT_SENSOR_TYPES - ] - - structures_has_camera = {} - for structure, _ in nest.cameras(): - structures_has_camera[structure] = True - for structure in structures_has_camera: - all_sensors += [ - NestBasicSensor(structure, None, variable) - for variable in conditions - if variable in STRUCTURE_CAMERA_SENSOR_TYPES - ] - - return all_sensors - - async_add_entities(await hass.async_add_executor_job(get_sensors), True) - - -class NestBasicSensor(NestSensorDevice, SensorEntity): - """Representation a basic Nest sensor.""" - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def device_class(self): - """Return the device class of the sensor.""" - return SENSOR_DEVICE_CLASSES.get(self.variable) - - @property - def state_class(self): - """Return the state class of the sensor.""" - return SENSOR_STATE_CLASSES.get(self.variable) - - def update(self): - """Retrieve latest state.""" - self._unit = SENSOR_UNITS.get(self.variable) - - if self.variable in VARIABLE_NAME_MAPPING: - self._state = getattr(self.device, VARIABLE_NAME_MAPPING[self.variable]) - elif self.variable in VALUE_MAPPING: - state = getattr(self.device, self.variable) - self._state = VALUE_MAPPING[self.variable].get(state, state) - elif self.variable in PROTECT_SENSOR_TYPES and self.variable != "color_status": - # keep backward compatibility - state = getattr(self.device, self.variable) - self._state = state.capitalize() if state is not None else None - else: - self._state = getattr(self.device, self.variable) - - -class NestTempSensor(NestSensorDevice, SensorEntity): - """Representation of a Nest Temperature sensor.""" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit - - @property - def device_class(self): - """Return the device class of the sensor.""" - return SensorDeviceClass.TEMPERATURE - - @property - def state_class(self): - """Return the state class of the sensor.""" - return SensorStateClass.MEASUREMENT - - def update(self): - """Retrieve latest state.""" - if self.device.temperature_scale == "C": - self._unit = UnitOfTemperature.CELSIUS - else: - self._unit = UnitOfTemperature.FAHRENHEIT - - if (temp := getattr(self.device, self.variable)) is None: - self._state = None - - if isinstance(temp, tuple): - low, high = temp - self._state = f"{int(low)}-{int(high)}" - else: - self._state = round(temp, 1) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index dbb30ceb52adea..54bc44a09b363e 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -20,5 +20,5 @@ "iot_class": "cloud_push", "loggers": ["google_nest_sdm", "nest"], "quality_scale": "platinum", - "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.2.5"] + "requirements": ["google-nest-sdm==2.2.5"] } diff --git a/homeassistant/components/nest/sensor.py b/homeassistant/components/nest/sensor.py index a9073aec80d5e0..aa170710eb6448 100644 --- a/homeassistant/components/nest/sensor.py +++ b/homeassistant/components/nest/sensor.py @@ -1,20 +1,104 @@ -"""Support for Nest sensors that dispatches between API versions.""" +"""Support for Google Nest SDM sensors.""" +from __future__ import annotations +import logging + +from google_nest_sdm.device import Device +from google_nest_sdm.device_manager import DeviceManager +from google_nest_sdm.device_traits import HumidityTrait, TemperatureTrait + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_SDM -from .legacy.sensor import async_setup_legacy_entry -from .sensor_sdm import async_setup_sdm_entry +from .const import DATA_DEVICE_MANAGER, DOMAIN +from .device_info import NestDeviceInfo + +_LOGGER = logging.getLogger(__name__) + + +DEVICE_TYPE_MAP = { + "sdm.devices.types.CAMERA": "Camera", + "sdm.devices.types.DISPLAY": "Display", + "sdm.devices.types.DOORBELL": "Doorbell", + "sdm.devices.types.THERMOSTAT": "Thermostat", +} async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the sensors.""" - if DATA_SDM not in entry.data: - await async_setup_legacy_entry(hass, entry, async_add_entities) - return - await async_setup_sdm_entry(hass, entry, async_add_entities) + device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][ + DATA_DEVICE_MANAGER + ] + entities: list[SensorEntity] = [] + for device in device_manager.devices.values(): + if TemperatureTrait.NAME in device.traits: + entities.append(TemperatureSensor(device)) + if HumidityTrait.NAME in device.traits: + entities.append(HumiditySensor(device)) + async_add_entities(entities) + + +class SensorBase(SensorEntity): + """Representation of a dynamically updated Sensor.""" + + _attr_should_poll = False + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_has_entity_name = True + + def __init__(self, device: Device) -> None: + """Initialize the sensor.""" + self._device = device + self._device_info = NestDeviceInfo(device) + self._attr_unique_id = f"{device.name}-{self.device_class}" + self._attr_device_info = self._device_info.device_info + + @property + def available(self) -> bool: + """Return the device availability.""" + return self._device_info.available + + async def async_added_to_hass(self) -> None: + """Run when entity is added to register update signal handler.""" + self.async_on_remove( + self._device.add_update_listener(self.async_write_ha_state) + ) + + +class TemperatureSensor(SensorBase): + """Representation of a Temperature Sensor.""" + + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + + @property + def native_value(self) -> float: + """Return the state of the sensor.""" + trait: TemperatureTrait = self._device.traits[TemperatureTrait.NAME] + # Round for display purposes because the API returns 5 decimal places. + # This can be removed if the SDM API issue is fixed, or a frontend + # display fix is added for all integrations. + return float(round(trait.ambient_temperature_celsius, 1)) + + +class HumiditySensor(SensorBase): + """Representation of a Humidity Sensor.""" + + _attr_device_class = SensorDeviceClass.HUMIDITY + _attr_native_unit_of_measurement = PERCENTAGE + + @property + def native_value(self) -> int: + """Return the state of the sensor.""" + trait: HumidityTrait = self._device.traits[HumidityTrait.NAME] + # Cast without loss of precision because the API always returns an integer. + return int(trait.ambient_humidity_percent) diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py deleted file mode 100644 index a74d0f3a54b9af..00000000000000 --- a/homeassistant/components/nest/sensor_sdm.py +++ /dev/null @@ -1,104 +0,0 @@ -"""Support for Google Nest SDM sensors.""" -from __future__ import annotations - -import logging - -from google_nest_sdm.device import Device -from google_nest_sdm.device_manager import DeviceManager -from google_nest_sdm.device_traits import HumidityTrait, TemperatureTrait - -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorStateClass, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, UnitOfTemperature -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DATA_DEVICE_MANAGER, DOMAIN -from .device_info import NestDeviceInfo - -_LOGGER = logging.getLogger(__name__) - - -DEVICE_TYPE_MAP = { - "sdm.devices.types.CAMERA": "Camera", - "sdm.devices.types.DISPLAY": "Display", - "sdm.devices.types.DOORBELL": "Doorbell", - "sdm.devices.types.THERMOSTAT": "Thermostat", -} - - -async def async_setup_sdm_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up the sensors.""" - - device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][ - DATA_DEVICE_MANAGER - ] - entities: list[SensorEntity] = [] - for device in device_manager.devices.values(): - if TemperatureTrait.NAME in device.traits: - entities.append(TemperatureSensor(device)) - if HumidityTrait.NAME in device.traits: - entities.append(HumiditySensor(device)) - async_add_entities(entities) - - -class SensorBase(SensorEntity): - """Representation of a dynamically updated Sensor.""" - - _attr_should_poll = False - _attr_state_class = SensorStateClass.MEASUREMENT - _attr_has_entity_name = True - - def __init__(self, device: Device) -> None: - """Initialize the sensor.""" - self._device = device - self._device_info = NestDeviceInfo(device) - self._attr_unique_id = f"{device.name}-{self.device_class}" - self._attr_device_info = self._device_info.device_info - - @property - def available(self) -> bool: - """Return the device availability.""" - return self._device_info.available - - async def async_added_to_hass(self) -> None: - """Run when entity is added to register update signal handler.""" - self.async_on_remove( - self._device.add_update_listener(self.async_write_ha_state) - ) - - -class TemperatureSensor(SensorBase): - """Representation of a Temperature Sensor.""" - - _attr_device_class = SensorDeviceClass.TEMPERATURE - _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS - - @property - def native_value(self) -> float: - """Return the state of the sensor.""" - trait: TemperatureTrait = self._device.traits[TemperatureTrait.NAME] - # Round for display purposes because the API returns 5 decimal places. - # This can be removed if the SDM API issue is fixed, or a frontend - # display fix is added for all integrations. - return float(round(trait.ambient_temperature_celsius, 1)) - - -class HumiditySensor(SensorBase): - """Representation of a Humidity Sensor.""" - - _attr_device_class = SensorDeviceClass.HUMIDITY - _attr_native_unit_of_measurement = PERCENTAGE - - @property - def native_value(self) -> int: - """Return the state of the sensor.""" - trait: HumidityTrait = self._device.traits[HumidityTrait.NAME] - # Cast without loss of precision because the API always returns an integer. - return int(trait.ambient_humidity_percent) diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index a452d015a2b946..b9069db8e481d8 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -35,27 +35,9 @@ "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "The Nest integration needs to re-authenticate your account" - }, - "init": { - "title": "Authentication Provider", - "description": "[%key:common::config_flow::title::oauth2_pick_implementation%]", - "data": { - "flow_impl": "Provider" - } - }, - "link": { - "title": "Link Nest Account", - "description": "To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided PIN code below.", - "data": { - "code": "[%key:common::config_flow::data::pin%]" - } } }, "error": { - "timeout": "Timeout validating code", - "invalid_pin": "Invalid PIN", - "unknown": "[%key:common::config_flow::error::unknown%]", - "internal_error": "Internal error validating code", "bad_project_id": "Please enter a valid Cloud Project ID (check Cloud Console)", "wrong_project_id": "Please enter a valid Cloud Project ID (was same as Device Access Project ID)", "subscriber_error": "Unknown subscriber error, see logs" diff --git a/requirements_all.txt b/requirements_all.txt index 48538b013e07b6..7a2b1497ab7a1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2119,9 +2119,6 @@ python-mpd2==3.0.5 # homeassistant.components.mystrom python-mystrom==2.2.0 -# homeassistant.components.nest -python-nest==4.2.0 - # homeassistant.components.swiss_public_transport python-opendata-transport==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b3e3a261654f9..c482150e9ce549 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1554,9 +1554,6 @@ python-miio==0.5.12 # homeassistant.components.mystrom python-mystrom==2.2.0 -# homeassistant.components.nest -python-nest==4.2.0 - # homeassistant.components.otbr # homeassistant.components.thread python-otbr-api==2.2.0 diff --git a/tests/components/nest/test_camera_sdm.py b/tests/components/nest/test_camera.py similarity index 100% rename from tests/components/nest/test_camera_sdm.py rename to tests/components/nest/test_camera.py diff --git a/tests/components/nest/test_climate_sdm.py b/tests/components/nest/test_climate.py similarity index 100% rename from tests/components/nest/test_climate_sdm.py rename to tests/components/nest/test_climate.py diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow.py similarity index 100% rename from tests/components/nest/test_config_flow_sdm.py rename to tests/components/nest/test_config_flow.py diff --git a/tests/components/nest/test_config_flow_legacy.py b/tests/components/nest/test_config_flow_legacy.py deleted file mode 100644 index 897961d9f9823d..00000000000000 --- a/tests/components/nest/test_config_flow_legacy.py +++ /dev/null @@ -1,242 +0,0 @@ -"""Tests for the Nest config flow.""" -import asyncio -from unittest.mock import patch - -from homeassistant import config_entries, data_entry_flow -from homeassistant.components.nest import DOMAIN, config_flow -from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component - -from .common import TEST_CONFIG_LEGACY - -from tests.common import MockConfigEntry - -CONFIG = TEST_CONFIG_LEGACY.config - - -async def test_abort_if_single_instance_allowed(hass: HomeAssistant) -> None: - """Test we abort if Nest is already setup.""" - existing_entry = MockConfigEntry(domain=DOMAIN, data={}) - existing_entry.add_to_hass(hass) - - assert await async_setup_component(hass, DOMAIN, CONFIG) - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "single_instance_allowed" - - -async def test_full_flow_implementation(hass: HomeAssistant) -> None: - """Test registering an implementation and finishing flow works.""" - assert await async_setup_component(hass, DOMAIN, CONFIG) - await hass.async_block_till_done() - # Register an additional implementation to select from during the flow - config_flow.register_flow_implementation( - hass, "test-other", "Test Other", None, None - ) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"flow_impl": "nest"}, - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "link" - assert ( - result["description_placeholders"] - .get("url") - .startswith("https://home.nest.com/login/oauth2?client_id=some-client-id") - ) - - def mock_login(auth): - assert auth.pin == "123ABC" - auth.auth_callback({"access_token": "yoo"}) - - with patch( - "homeassistant.components.nest.legacy.local_auth.NestAuth.login", new=mock_login - ), patch( - "homeassistant.components.nest.async_setup_legacy_entry", return_value=True - ) as mock_setup: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"code": "123ABC"} - ) - await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"]["tokens"] == {"access_token": "yoo"} - assert result["data"]["impl_domain"] == "nest" - assert result["title"] == "Nest (via configuration.yaml)" - - -async def test_not_pick_implementation_if_only_one(hass: HomeAssistant) -> None: - """Test we pick the default implementation when registered.""" - assert await async_setup_component(hass, DOMAIN, CONFIG) - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "link" - - -async def test_abort_if_timeout_generating_auth_url(hass: HomeAssistant) -> None: - """Test we abort if generating authorize url fails.""" - with patch( - "homeassistant.components.nest.legacy.local_auth.generate_auth_url", - side_effect=asyncio.TimeoutError, - ): - assert await async_setup_component(hass, DOMAIN, CONFIG) - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "authorize_url_timeout" - - -async def test_abort_if_exception_generating_auth_url(hass: HomeAssistant) -> None: - """Test we abort if generating authorize url blows up.""" - with patch( - "homeassistant.components.nest.legacy.local_auth.generate_auth_url", - side_effect=ValueError, - ): - assert await async_setup_component(hass, DOMAIN, CONFIG) - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "unknown_authorize_url_generation" - - -async def test_verify_code_timeout(hass: HomeAssistant) -> None: - """Test verify code timing out.""" - assert await async_setup_component(hass, DOMAIN, CONFIG) - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "link" - - with patch( - "homeassistant.components.nest.legacy.local_auth.NestAuth.login", - side_effect=asyncio.TimeoutError, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"code": "123ABC"} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "link" - assert result["errors"] == {"code": "timeout"} - - -async def test_verify_code_invalid(hass: HomeAssistant) -> None: - """Test verify code invalid.""" - assert await async_setup_component(hass, DOMAIN, CONFIG) - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "link" - - with patch( - "homeassistant.components.nest.legacy.local_auth.NestAuth.login", - side_effect=config_flow.CodeInvalid, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"code": "123ABC"} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "link" - assert result["errors"] == {"code": "invalid_pin"} - - -async def test_verify_code_unknown_error(hass: HomeAssistant) -> None: - """Test verify code unknown error.""" - assert await async_setup_component(hass, DOMAIN, CONFIG) - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "link" - - with patch( - "homeassistant.components.nest.legacy.local_auth.NestAuth.login", - side_effect=config_flow.NestAuthError, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"code": "123ABC"} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "link" - assert result["errors"] == {"code": "unknown"} - - -async def test_verify_code_exception(hass: HomeAssistant) -> None: - """Test verify code blows up.""" - assert await async_setup_component(hass, DOMAIN, CONFIG) - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "link" - - with patch( - "homeassistant.components.nest.legacy.local_auth.NestAuth.login", - side_effect=ValueError, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"code": "123ABC"} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "link" - assert result["errors"] == {"code": "internal_error"} - - -async def test_step_import(hass: HomeAssistant) -> None: - """Test that we trigger import when configuring with client.""" - with patch("os.path.isfile", return_value=False): - assert await async_setup_component(hass, DOMAIN, CONFIG) - await hass.async_block_till_done() - - flow = hass.config_entries.flow.async_progress()[0] - result = await hass.config_entries.flow.async_configure(flow["flow_id"]) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "link" - - -async def test_step_import_with_token_cache(hass: HomeAssistant) -> None: - """Test that we import existing token cache.""" - with patch("os.path.isfile", return_value=True), patch( - "homeassistant.components.nest.config_flow.load_json_object", - return_value={"access_token": "yo"}, - ), patch( - "homeassistant.components.nest.async_setup_legacy_entry", return_value=True - ) as mock_setup: - assert await async_setup_component(hass, DOMAIN, CONFIG) - await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 - - entry = hass.config_entries.async_entries(DOMAIN)[0] - - assert entry.data == {"impl_domain": "nest", "tokens": {"access_token": "yo"}} diff --git a/tests/components/nest/test_diagnostics.py b/tests/components/nest/test_diagnostics.py index 530e3695d119f3..408e4e0d96391d 100644 --- a/tests/components/nest/test_diagnostics.py +++ b/tests/components/nest/test_diagnostics.py @@ -9,8 +9,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from .common import TEST_CONFIG_LEGACY - from tests.components.diagnostics import ( get_diagnostics_for_config_entry, get_diagnostics_for_device, @@ -146,21 +144,6 @@ async def test_setup_susbcriber_failure( assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == {} -@pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_LEGACY]) -async def test_legacy_config_entry_diagnostics( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - config_entry, - setup_base_platform, -) -> None: - """Test config entry diagnostics for legacy integration doesn't fail.""" - - with patch("homeassistant.components.nest.legacy.Nest"): - await setup_base_platform() - - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == {} - - async def test_camera_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, diff --git a/tests/components/nest/test_init_sdm.py b/tests/components/nest/test_init.py similarity index 90% rename from tests/components/nest/test_init_sdm.py rename to tests/components/nest/test_init.py index db560e44e832ab..ecfe412bdbf7c0 100644 --- a/tests/components/nest/test_init_sdm.py +++ b/tests/components/nest/test_init.py @@ -22,15 +22,20 @@ from homeassistant.components.nest import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from .common import ( PROJECT_ID, SUBSCRIBER_ID, + TEST_CONFIG_ENTRY_LEGACY, + TEST_CONFIG_LEGACY, TEST_CONFIGFLOW_APP_CREDS, FakeSubscriber, YieldFixture, ) +from tests.common import MockConfigEntry + PLATFORM = "sensor" @@ -276,3 +281,26 @@ async def test_migrate_unique_id( assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == PROJECT_ID + + +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_LEGACY]) +async def test_legacy_works_with_nest_yaml( + hass: HomeAssistant, + config: dict[str, Any], + config_entry: MockConfigEntry, +) -> None: + """Test integration won't start with legacy works with nest yaml config.""" + config_entry.add_to_hass(hass) + assert not await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_ENTRY_LEGACY]) +async def test_legacy_works_with_nest_cleanup( + hass: HomeAssistant, setup_platform +) -> None: + """Test legacy works with nest config entries are silently removed once yaml is removed.""" + await setup_platform() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 0 diff --git a/tests/components/nest/test_init_legacy.py b/tests/components/nest/test_init_legacy.py deleted file mode 100644 index f27382d0345cca..00000000000000 --- a/tests/components/nest/test_init_legacy.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Test basic initialization for the Legacy Nest API using mocks for the Nest python library.""" -from unittest.mock import MagicMock, PropertyMock, patch - -import pytest - -from homeassistant.core import HomeAssistant - -from .common import TEST_CONFIG_ENTRY_LEGACY, TEST_CONFIG_LEGACY - -DOMAIN = "nest" - - -@pytest.fixture -def nest_test_config(): - """Fixture to specify the overall test fixture configuration.""" - return TEST_CONFIG_LEGACY - - -def make_thermostat(): - """Make a mock thermostat with dummy values.""" - device = MagicMock() - type(device).device_id = PropertyMock(return_value="a.b.c.d.e.f.g") - type(device).name = PropertyMock(return_value="My Thermostat") - type(device).name_long = PropertyMock(return_value="My Thermostat") - type(device).serial = PropertyMock(return_value="serial-number") - type(device).mode = "off" - type(device).hvac_state = "off" - type(device).target = PropertyMock(return_value=31.0) - type(device).temperature = PropertyMock(return_value=30.1) - type(device).min_temperature = PropertyMock(return_value=10.0) - type(device).max_temperature = PropertyMock(return_value=50.0) - type(device).humidity = PropertyMock(return_value=40.4) - type(device).software_version = PropertyMock(return_value="a.b.c") - return device - - -@pytest.mark.parametrize( - "nest_test_config", [TEST_CONFIG_LEGACY, TEST_CONFIG_ENTRY_LEGACY] -) -async def test_thermostat(hass: HomeAssistant, setup_base_platform) -> None: - """Test simple initialization for thermostat entities.""" - - thermostat = make_thermostat() - - structure = MagicMock() - type(structure).name = PropertyMock(return_value="My Room") - type(structure).thermostats = PropertyMock(return_value=[thermostat]) - type(structure).eta = PropertyMock(return_value="away") - - nest = MagicMock() - type(nest).structures = PropertyMock(return_value=[structure]) - - with patch("homeassistant.components.nest.legacy.Nest", return_value=nest), patch( - "homeassistant.components.nest.legacy.sensor._VALID_SENSOR_TYPES", - ["humidity", "temperature"], - ), patch( - "homeassistant.components.nest.legacy.binary_sensor._VALID_BINARY_SENSOR_TYPES", - {"fan": None}, - ): - await setup_base_platform() - - climate = hass.states.get("climate.my_thermostat") - assert climate is not None - assert climate.state == "off" - - temperature = hass.states.get("sensor.my_thermostat_temperature") - assert temperature is not None - assert temperature.state == "-1.1" - - humidity = hass.states.get("sensor.my_thermostat_humidity") - assert humidity is not None - assert humidity.state == "40.4" - - fan = hass.states.get("binary_sensor.my_thermostat_fan") - assert fan is not None - assert fan.state == "on" diff --git a/tests/components/nest/test_local_auth.py b/tests/components/nest/test_local_auth.py deleted file mode 100644 index 6ba704e6c3ec3b..00000000000000 --- a/tests/components/nest/test_local_auth.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Test Nest local auth.""" -from urllib.parse import parse_qsl - -import pytest -import requests_mock -from requests_mock import create_response - -from homeassistant.components.nest import config_flow, const -from homeassistant.components.nest.legacy import local_auth - - -@pytest.fixture -def registered_flow(hass): - """Mock a registered flow.""" - local_auth.initialize(hass, "TEST-CLIENT-ID", "TEST-CLIENT-SECRET") - return hass.data[config_flow.DATA_FLOW_IMPL][const.DOMAIN] - - -async def test_generate_auth_url(registered_flow) -> None: - """Test generating an auth url. - - Mainly testing that it doesn't blow up. - """ - url = await registered_flow["gen_authorize_url"]("TEST-FLOW-ID") - assert url is not None - - -async def test_convert_code( - requests_mock: requests_mock.Mocker, registered_flow -) -> None: - """Test converting a code.""" - from nest.nest import ACCESS_TOKEN_URL - - def token_matcher(request): - """Match a fetch token request.""" - if request.url != ACCESS_TOKEN_URL: - return None - - assert dict(parse_qsl(request.text)) == { - "client_id": "TEST-CLIENT-ID", - "client_secret": "TEST-CLIENT-SECRET", - "code": "TEST-CODE", - "grant_type": "authorization_code", - } - - return create_response(request, json={"access_token": "TEST-ACCESS-TOKEN"}) - - requests_mock.add_matcher(token_matcher) - - tokens = await registered_flow["convert_code"]("TEST-CODE") - assert tokens == {"access_token": "TEST-ACCESS-TOKEN"} From e5ccd85e7e26c167d0b73669a88bc3a7614dd456 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 10 Jul 2023 08:13:47 +0200 Subject: [PATCH 0302/1009] Fix missing name in Siren service descriptions (#96072) --- homeassistant/components/siren/services.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/siren/services.yaml b/homeassistant/components/siren/services.yaml index 18bf782eaf2c99..209dece71ab8d4 100644 --- a/homeassistant/components/siren/services.yaml +++ b/homeassistant/components/siren/services.yaml @@ -1,6 +1,7 @@ # Describes the format for available siren services turn_on: + name: Turn on description: Turn siren on. target: entity: @@ -29,12 +30,14 @@ turn_on: text: turn_off: + name: Turn off description: Turn siren off. target: entity: domain: siren toggle: + name: Toggle description: Toggles a siren. target: entity: From 303e5492136b5d323bd9654a76f4bbf2c21aca35 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 10 Jul 2023 10:13:48 +0200 Subject: [PATCH 0303/1009] Update yamllint to 1.32.0 (#96109) Co-authored-by: Paulus Schoutsen --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f85f8583a0425c..6f9a24d6db0653 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - --branch=master - --branch=rc - repo: https://github.com/adrienverge/yamllint.git - rev: v1.28.0 + rev: v1.32.0 hooks: - id: yamllint - repo: https://github.com/pre-commit/mirrors-prettier diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 4047daf73cf324..f1dde9ca022fc3 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -3,4 +3,4 @@ black==23.3.0 codespell==2.2.2 ruff==0.0.277 -yamllint==1.28.0 +yamllint==1.32.0 From fa6d659f2bf8b3e1e0a99d11db2e2e17c841c272 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Jul 2023 23:11:08 -1000 Subject: [PATCH 0304/1009] Bump aioesphomeapi to 15.1.4 (#96227) --- 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 1acf0f1154e472..63bd2ffc0814f4 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.1.3", + "aioesphomeapi==15.1.4", "bluetooth-data-tools==1.3.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 7a2b1497ab7a1e..415685b2f5ae2c 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.1.3 +aioesphomeapi==15.1.4 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c482150e9ce549..6dc2fdacfe26f1 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.1.3 +aioesphomeapi==15.1.4 # homeassistant.components.flo aioflo==2021.11.0 From 882529c0a0da2bb9f76dcfbf03eb9569107289de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Jul 2023 23:13:27 -1000 Subject: [PATCH 0305/1009] Simplify FastUrlDispatcher resolve (#96234) --- homeassistant/components/http/__init__.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index f559b09a1ff992..68602e34d3e291 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -600,7 +600,7 @@ async def resolve(self, request: web.Request) -> UrlMappingMatchInfo: resource_index = self._resource_index # Walk the url parts looking for candidates - for i in range(len(url_parts), 1, -1): + for i in range(len(url_parts), 0, -1): url_part = "/" + "/".join(url_parts[1:i]) if (resource_candidates := resource_index.get(url_part)) is not None: for candidate in resource_candidates: @@ -608,11 +608,6 @@ async def resolve(self, request: web.Request) -> UrlMappingMatchInfo: match_dict := (await candidate.resolve(request))[0] ) is not None: return match_dict - # Next try the index view if we don't have a match - if (index_view_candidates := resource_index.get("/")) is not None: - for candidate in index_view_candidates: - if (match_dict := (await candidate.resolve(request))[0]) is not None: - return match_dict # Finally, fallback to the linear search return await super().resolve(request) From bc2319bbe60262a9a89664d5a4371a07fceae5b6 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 10 Jul 2023 02:22:15 -0700 Subject: [PATCH 0306/1009] Update Nest Legacy removal strings (#96229) --- homeassistant/components/nest/__init__.py | 2 +- homeassistant/components/nest/strings.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 2645139f702bad..5f2a0b0bffd6c1 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -121,7 +121,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: breaks_in_ha_version="2023.8.0", is_fixable=False, severity=ir.IssueSeverity.WARNING, - translation_key="legacy_nest_deprecated", + translation_key="legacy_nest_removed", translation_placeholders={ "documentation_url": "https://www.home-assistant.io/integrations/nest/", }, diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index b9069db8e481d8..86650bbbe9aa51 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -64,9 +64,9 @@ } }, "issues": { - "legacy_nest_deprecated": { - "title": "Legacy Works With Nest is being removed", - "description": "Legacy Works With Nest is being removed from Home Assistant.\n\nYou must take action to use the SDM API. Remove all `nest` configuration from `configuration.yaml` and restart Home Assistant, then see the Nest [integration instructions]({documentation_url}) for set up instructions and supported devices." + "legacy_nest_removed": { + "title": "Legacy Works With Nest has been removed", + "description": "Legacy Works With Nest has been removed from Home Assistant, and the API shuts down as of September 2023.\n\nYou must take action to use the SDM API. Remove all `nest` configuration from `configuration.yaml` and restart Home Assistant, then see the Nest [integration instructions]({documentation_url}) for set up instructions and supported devices." } } } From e7b00da662f220e7f96cb0d4df7c0c36d4a5f5d5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 10 Jul 2023 12:23:42 +0200 Subject: [PATCH 0307/1009] Clean up unused device class translations from binary sensor (#96241) --- .../components/binary_sensor/strings.json | 14 -------------- script/hassfest/translations.py | 8 -------- 2 files changed, 22 deletions(-) diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index ee70420fec04a2..abe7efee3ed6cb 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -300,19 +300,5 @@ "on": "[%key:common::state::open%]" } } - }, - "device_class": { - "co": "carbon monoxide", - "cold": "cold", - "gas": "gas", - "heat": "heat", - "moisture": "moisture", - "motion": "motion", - "occupancy": "occupancy", - "power": "power", - "problem": "problem", - "smoke": "smoke", - "sound": "sound", - "vibration": "vibration" } } diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 5f233b4dec8c1e..56609e57fd978f 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -431,14 +431,6 @@ def validate_translation_file( # noqa: C901 strings_schema = gen_auth_schema(config, integration) elif integration.domain == "onboarding": strings_schema = ONBOARDING_SCHEMA - elif integration.domain == "binary_sensor": - strings_schema = gen_strings_schema(config, integration).extend( - { - vol.Optional("device_class"): cv.schema_with_slug_keys( - translation_value_validator, slug_validator=vol.Any("_", cv.slug) - ) - } - ) elif integration.domain == "homeassistant_hardware": strings_schema = gen_ha_hardware_schema(config, integration) else: From 7ca9f6757a847d086dbbaff30183c7da70bd6f40 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 10 Jul 2023 12:42:55 +0200 Subject: [PATCH 0308/1009] Use fixed token for CodeCov uploads to deal with recent failures (#96133) --- .github/workflows/ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 331a1bc151a5da..368a3eb6e98c1b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1019,6 +1019,7 @@ jobs: with: | fail_ci_if_error: true flags: full-suite + token: ${{ env.CODECOV_TOKEN }} attempt_limit: 5 attempt_delay: 30000 - name: Upload coverage to Codecov (partial coverage) @@ -1028,5 +1029,6 @@ jobs: action: codecov/codecov-action@v3.1.3 with: | fail_ci_if_error: true + token: ${{ env.CODECOV_TOKEN }} attempt_limit: 5 attempt_delay: 30000 From af03a284a50ff51925c25382fa71d583b0351c64 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 10 Jul 2023 12:50:56 +0200 Subject: [PATCH 0309/1009] Add entity translations to tailscale (#96237) --- .../components/tailscale/binary_sensor.py | 14 ++++---- homeassistant/components/tailscale/sensor.py | 6 ++-- .../components/tailscale/strings.json | 36 +++++++++++++++++++ 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/tailscale/binary_sensor.py b/homeassistant/components/tailscale/binary_sensor.py index 9570c4a462830c..ecc561f03554b7 100644 --- a/homeassistant/components/tailscale/binary_sensor.py +++ b/homeassistant/components/tailscale/binary_sensor.py @@ -37,49 +37,49 @@ class TailscaleBinarySensorEntityDescription( BINARY_SENSORS: tuple[TailscaleBinarySensorEntityDescription, ...] = ( TailscaleBinarySensorEntityDescription( key="update_available", - name="Client", + translation_key="client", device_class=BinarySensorDeviceClass.UPDATE, entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda device: device.update_available, ), TailscaleBinarySensorEntityDescription( key="client_supports_hair_pinning", - name="Supports hairpinning", + translation_key="client_supports_hair_pinning", icon="mdi:wan", entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda device: device.client_connectivity.client_supports.hair_pinning, ), TailscaleBinarySensorEntityDescription( key="client_supports_ipv6", - name="Supports IPv6", + translation_key="client_supports_ipv6", icon="mdi:wan", entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda device: device.client_connectivity.client_supports.ipv6, ), TailscaleBinarySensorEntityDescription( key="client_supports_pcp", - name="Supports PCP", + translation_key="client_supports_pcp", icon="mdi:wan", entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda device: device.client_connectivity.client_supports.pcp, ), TailscaleBinarySensorEntityDescription( key="client_supports_pmp", - name="Supports NAT-PMP", + translation_key="client_supports_pmp", icon="mdi:wan", entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda device: device.client_connectivity.client_supports.pmp, ), TailscaleBinarySensorEntityDescription( key="client_supports_udp", - name="Supports UDP", + translation_key="client_supports_udp", icon="mdi:wan", entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda device: device.client_connectivity.client_supports.udp, ), TailscaleBinarySensorEntityDescription( key="client_supports_upnp", - name="Supports UPnP", + translation_key="client_supports_upnp", icon="mdi:wan", entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda device: device.client_connectivity.client_supports.upnp, diff --git a/homeassistant/components/tailscale/sensor.py b/homeassistant/components/tailscale/sensor.py index 71fc7d848eaacf..75dca4ed840780 100644 --- a/homeassistant/components/tailscale/sensor.py +++ b/homeassistant/components/tailscale/sensor.py @@ -38,21 +38,21 @@ class TailscaleSensorEntityDescription( SENSORS: tuple[TailscaleSensorEntityDescription, ...] = ( TailscaleSensorEntityDescription( key="expires", - name="Expires", + translation_key="expires", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.expires, ), TailscaleSensorEntityDescription( key="ip", - name="IP address", + translation_key="ip", icon="mdi:ip-network", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.addresses[0] if device.addresses else None, ), TailscaleSensorEntityDescription( key="last_seen", - name="Last seen", + translation_key="last_seen", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda device: device.last_seen, ), diff --git a/homeassistant/components/tailscale/strings.json b/homeassistant/components/tailscale/strings.json index c03b5a3f841e33..b110e53ee64e8a 100644 --- a/homeassistant/components/tailscale/strings.json +++ b/homeassistant/components/tailscale/strings.json @@ -23,5 +23,41 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "entity": { + "binary_sensor": { + "client": { + "name": "Client" + }, + "client_supports_hair_pinning": { + "name": "Supports hairpinning" + }, + "client_supports_ipv6": { + "name": "Supports IPv6" + }, + "client_supports_pcp": { + "name": "Supports PCP" + }, + "client_supports_pmp": { + "name": "Supports NAT-PMP" + }, + "client_supports_udp": { + "name": "Supports UDP" + }, + "client_supports_upnp": { + "name": "Supports UPnP" + } + }, + "sensor": { + "expires": { + "name": "Expires" + }, + "ip": { + "name": "IP address" + }, + "last_seen": { + "name": "Last seen" + } + } } } From 7eb087a9d713c33db1c46dbf5b784124d0970a59 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 10 Jul 2023 12:56:51 +0200 Subject: [PATCH 0310/1009] Use common string references for device_automation translations (#95897) --- .../components/binary_sensor/strings.json | 8 ++++---- homeassistant/components/fan/strings.json | 16 ++++++++-------- homeassistant/components/humidifier/strings.json | 16 ++++++++-------- homeassistant/components/kodi/strings.json | 4 ++-- homeassistant/components/light/strings.json | 16 ++++++++-------- .../components/media_player/strings.json | 10 +++++----- homeassistant/components/netatmo/strings.json | 4 ++-- homeassistant/components/remote/strings.json | 16 ++++++++-------- homeassistant/components/switch/strings.json | 16 ++++++++-------- .../components/water_heater/strings.json | 4 ++-- homeassistant/strings.json | 16 ++++++++++++++++ 11 files changed, 71 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index abe7efee3ed6cb..185482d62e3e43 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -52,8 +52,8 @@ "is_no_vibration": "{entity_name} is not detecting vibration", "is_open": "{entity_name} is open", "is_not_open": "{entity_name} is closed", - "is_on": "{entity_name} is on", - "is_off": "{entity_name} is off" + "is_on": "[%key:common::device_automation::condition_type::is_on%]", + "is_off": "[%key:common::device_automation::condition_type::is_off%]" }, "trigger_type": { "bat_low": "{entity_name} battery low", @@ -106,8 +106,8 @@ "no_vibration": "{entity_name} stopped detecting vibration", "opened": "{entity_name} opened", "not_opened": "{entity_name} closed", - "turned_on": "{entity_name} turned on", - "turned_off": "{entity_name} turned off" + "turned_on": "[%key:common::device_automation::trigger_type::turned_on%]", + "turned_off": "[%key:common::device_automation::trigger_type::turned_off%]" } }, "entity_component": { diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index b16d6da6df5651..0f3b88fd7f204d 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -2,18 +2,18 @@ "title": "Fan", "device_automation": { "condition_type": { - "is_on": "{entity_name} is on", - "is_off": "{entity_name} is off" + "is_on": "[%key:common::device_automation::condition_type::is_on%]", + "is_off": "[%key:common::device_automation::condition_type::is_off%]" }, "trigger_type": { - "changed_states": "{entity_name} turned on or off", - "turned_on": "{entity_name} turned on", - "turned_off": "{entity_name} turned off" + "changed_states": "[%key:common::device_automation::trigger_type::changed_states%]", + "turned_on": "[%key:common::device_automation::trigger_type::turned_on%]", + "turned_off": "[%key:common::device_automation::trigger_type::turned_off%]" }, "action_type": { - "toggle": "Toggle {entity_name}", - "turn_on": "Turn on {entity_name}", - "turn_off": "Turn off {entity_name}" + "toggle": "[%key:common::device_automation::action_type::toggle%]", + "turn_on": "[%key:common::device_automation::action_type::turn_on%]", + "turn_off": "[%key:common::device_automation::action_type::turn_off%]" } }, "entity_component": { diff --git a/homeassistant/components/humidifier/strings.json b/homeassistant/components/humidifier/strings.json index f06bf7ccd598a7..7512b2abec7bcf 100644 --- a/homeassistant/components/humidifier/strings.json +++ b/homeassistant/components/humidifier/strings.json @@ -3,21 +3,21 @@ "device_automation": { "trigger_type": { "target_humidity_changed": "{entity_name} target humidity changed", - "changed_states": "{entity_name} turned on or off", - "turned_on": "{entity_name} turned on", - "turned_off": "{entity_name} turned off" + "changed_states": "[%key:common::device_automation::trigger_type::changed_states%]", + "turned_on": "[%key:common::device_automation::trigger_type::turned_on%]", + "turned_off": "[%key:common::device_automation::trigger_type::turned_off%]" }, "condition_type": { "is_mode": "{entity_name} is set to a specific mode", - "is_on": "{entity_name} is on", - "is_off": "{entity_name} is off" + "is_on": "[%key:common::device_automation::condition_type::is_on%]", + "is_off": "[%key:common::device_automation::condition_type::is_off%]" }, "action_type": { "set_humidity": "Set humidity for {entity_name}", "set_mode": "Change mode on {entity_name}", - "toggle": "Toggle {entity_name}", - "turn_on": "Turn on {entity_name}", - "turn_off": "Turn off {entity_name}" + "toggle": "[%key:common::device_automation::action_type::toggle%]", + "turn_on": "[%key:common::device_automation::action_type::turn_on%]", + "turn_off": "[%key:common::device_automation::action_type::turn_off%]" } }, "entity_component": { diff --git a/homeassistant/components/kodi/strings.json b/homeassistant/components/kodi/strings.json index 6315fffb193bc5..8097eb6336b75d 100644 --- a/homeassistant/components/kodi/strings.json +++ b/homeassistant/components/kodi/strings.json @@ -43,8 +43,8 @@ }, "device_automation": { "trigger_type": { - "turn_on": "{entity_name} was requested to turn on", - "turn_off": "{entity_name} was requested to turn off" + "turn_on": "[%key:common::device_automation::action_type::turn_on%]", + "turn_off": "[%key:common::device_automation::action_type::turn_off%]" } } } diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index 935e38d33d9647..6219ade3e58384 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -4,19 +4,19 @@ "action_type": { "brightness_decrease": "Decrease {entity_name} brightness", "brightness_increase": "Increase {entity_name} brightness", - "toggle": "Toggle {entity_name}", - "turn_on": "Turn on {entity_name}", - "turn_off": "Turn off {entity_name}", + "toggle": "[%key:common::device_automation::action_type::toggle%]", + "turn_on": "[%key:common::device_automation::action_type::turn_on%]", + "turn_off": "[%key:common::device_automation::action_type::turn_off%]", "flash": "Flash {entity_name}" }, "condition_type": { - "is_on": "{entity_name} is on", - "is_off": "{entity_name} is off" + "is_on": "[%key:common::device_automation::condition_type::is_on%]", + "is_off": "[%key:common::device_automation::condition_type::is_off%]" }, "trigger_type": { - "changed_states": "{entity_name} turned on or off", - "turned_on": "{entity_name} turned on", - "turned_off": "{entity_name} turned off" + "changed_states": "[%key:common::device_automation::trigger_type::changed_states%]", + "turned_on": "[%key:common::device_automation::trigger_type::turned_on%]", + "turned_off": "[%key:common::device_automation::trigger_type::turned_off%]" } }, "entity_component": { diff --git a/homeassistant/components/media_player/strings.json b/homeassistant/components/media_player/strings.json index 2c63a543119669..67c92d7ce072f3 100644 --- a/homeassistant/components/media_player/strings.json +++ b/homeassistant/components/media_player/strings.json @@ -3,20 +3,20 @@ "device_automation": { "condition_type": { "is_buffering": "{entity_name} is buffering", - "is_on": "{entity_name} is on", - "is_off": "{entity_name} is off", + "is_on": "[%key:common::device_automation::condition_type::is_on%]", + "is_off": "[%key:common::device_automation::condition_type::is_off%]", "is_idle": "{entity_name} is idle", "is_paused": "{entity_name} is paused", "is_playing": "{entity_name} is playing" }, "trigger_type": { "buffering": "{entity_name} starts buffering", - "turned_on": "{entity_name} turned on", - "turned_off": "{entity_name} turned off", + "turned_on": "[%key:common::device_automation::trigger_type::turned_on%]", + "turned_off": "[%key:common::device_automation::trigger_type::turned_off%]", "idle": "{entity_name} becomes idle", "paused": "{entity_name} is paused", "playing": "{entity_name} starts playing", - "changed_states": "{entity_name} changed states" + "changed_states": "[%key:common::device_automation::trigger_type::changed_states%]" } }, "entity_component": { diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index 5fdf580c6aa647..617d813007c5de 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -52,8 +52,8 @@ "hg": "Frost guard" }, "trigger_type": { - "turned_off": "{entity_name} turned off", - "turned_on": "{entity_name} turned on", + "turned_off": "[%key:common::device_automation::trigger_type::turned_off%]", + "turned_on": "[%key:common::device_automation::trigger_type::turned_on%]", "human": "{entity_name} detected a human", "movement": "{entity_name} detected movement", "person": "{entity_name} detected a person", diff --git a/homeassistant/components/remote/strings.json b/homeassistant/components/remote/strings.json index f0d2787b658659..bf8a669af50428 100644 --- a/homeassistant/components/remote/strings.json +++ b/homeassistant/components/remote/strings.json @@ -2,18 +2,18 @@ "title": "Remote", "device_automation": { "action_type": { - "toggle": "Toggle {entity_name}", - "turn_on": "Turn on {entity_name}", - "turn_off": "Turn off {entity_name}" + "toggle": "[%key:common::device_automation::action_type::toggle%]", + "turn_on": "[%key:common::device_automation::action_type::turn_on%]", + "turn_off": "[%key:common::device_automation::action_type::turn_off%]" }, "condition_type": { - "is_on": "{entity_name} is on", - "is_off": "{entity_name} is off" + "is_on": "[%key:common::device_automation::condition_type::is_on%]", + "is_off": "[%key:common::device_automation::condition_type::is_off%]" }, "trigger_type": { - "changed_states": "{entity_name} turned on or off", - "turned_on": "{entity_name} turned on", - "turned_off": "{entity_name} turned off" + "changed_states": "[%key:common::device_automation::trigger_type::changed_states%]", + "turned_on": "[%key:common::device_automation::trigger_type::turned_on%]", + "turned_off": "[%key:common::device_automation::trigger_type::turned_off%]" } }, "entity_component": { diff --git a/homeassistant/components/switch/strings.json b/homeassistant/components/switch/strings.json index a7934ba420927a..70cd45f4d2194f 100644 --- a/homeassistant/components/switch/strings.json +++ b/homeassistant/components/switch/strings.json @@ -2,18 +2,18 @@ "title": "Switch", "device_automation": { "action_type": { - "toggle": "Toggle {entity_name}", - "turn_on": "Turn on {entity_name}", - "turn_off": "Turn off {entity_name}" + "toggle": "[%key:common::device_automation::action_type::toggle%]", + "turn_on": "[%key:common::device_automation::action_type::turn_on%]", + "turn_off": "[%key:common::device_automation::action_type::turn_off%]" }, "condition_type": { - "is_on": "{entity_name} is on", - "is_off": "{entity_name} is off" + "is_on": "[%key:common::device_automation::condition_type::is_on%]", + "is_off": "[%key:common::device_automation::condition_type::is_off%]" }, "trigger_type": { - "changed_states": "{entity_name} turned on or off", - "turned_on": "{entity_name} turned on", - "turned_off": "{entity_name} turned off" + "changed_states": "[%key:common::device_automation::trigger_type::changed_states%]", + "turned_on": "[%key:common::device_automation::trigger_type::turned_on%]", + "turned_off": "[%key:common::device_automation::trigger_type::turned_off%]" } }, "entity_component": { diff --git a/homeassistant/components/water_heater/strings.json b/homeassistant/components/water_heater/strings.json index 6344b5a847a202..b0a625d0016a15 100644 --- a/homeassistant/components/water_heater/strings.json +++ b/homeassistant/components/water_heater/strings.json @@ -1,8 +1,8 @@ { "device_automation": { "action_type": { - "turn_on": "Turn on {entity_name}", - "turn_off": "Turn off {entity_name}" + "turn_on": "[%key:common::device_automation::action_type::turn_on%]", + "turn_off": "[%key:common::device_automation::action_type::turn_off%]" } }, "entity_component": { diff --git a/homeassistant/strings.json b/homeassistant/strings.json index c4cf0593aaeb54..51a5636092af3e 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -4,6 +4,22 @@ "model": "Model", "ui_managed": "Managed via UI" }, + "device_automation": { + "condition_type": { + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off" + }, + "trigger_type": { + "changed_states": "{entity_name} turned on or off", + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off" + }, + "action_type": { + "toggle": "Toggle {entity_name}", + "turn_on": "Turn on {entity_name}", + "turn_off": "Turn off {entity_name}" + } + }, "state": { "off": "Off", "on": "On", From 87f284c7e9093522820dc773d25ed0e7eb47b76a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 10 Jul 2023 12:58:53 +0200 Subject: [PATCH 0311/1009] Add MEDIA_ANNOUNCE to MediaPlayerEntityFeature (#95906) --- homeassistant/components/forked_daapd/const.py | 1 + homeassistant/components/group/media_player.py | 8 ++++++++ homeassistant/components/media_player/const.py | 1 + homeassistant/components/media_player/services.yaml | 3 +++ homeassistant/components/sonos/media_player.py | 1 + tests/components/group/test_media_player.py | 4 +++- 6 files changed, 17 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/forked_daapd/const.py b/homeassistant/components/forked_daapd/const.py index 5668f941c6e712..686a9dbbde969a 100644 --- a/homeassistant/components/forked_daapd/const.py +++ b/homeassistant/components/forked_daapd/const.py @@ -82,6 +82,7 @@ | MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.BROWSE_MEDIA + | MediaPlayerEntityFeature.MEDIA_ANNOUNCE | MediaPlayerEntityFeature.MEDIA_ENQUEUE ) SUPPORTED_FEATURES_ZONE = ( diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index fa43ac76ea636c..b271e57cb8a0a8 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -50,6 +50,7 @@ from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +KEY_ANNOUNCE = "announce" KEY_CLEAR_PLAYLIST = "clear_playlist" KEY_ENQUEUE = "enqueue" KEY_ON_OFF = "on_off" @@ -116,6 +117,7 @@ def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> Non self._entities = entities self._features: dict[str, set[str]] = { + KEY_ANNOUNCE: set(), KEY_CLEAR_PLAYLIST: set(), KEY_ENQUEUE: set(), KEY_ON_OFF: set(), @@ -194,6 +196,10 @@ def async_update_supported_features( self._features[KEY_VOLUME].add(entity_id) else: self._features[KEY_VOLUME].discard(entity_id) + if new_features & MediaPlayerEntityFeature.MEDIA_ANNOUNCE: + self._features[KEY_ANNOUNCE].add(entity_id) + else: + self._features[KEY_ANNOUNCE].discard(entity_id) if new_features & MediaPlayerEntityFeature.MEDIA_ENQUEUE: self._features[KEY_ENQUEUE].add(entity_id) else: @@ -440,6 +446,8 @@ def async_update_state(self) -> None: | MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_STEP ) + if self._features[KEY_ANNOUNCE]: + supported_features |= MediaPlayerEntityFeature.MEDIA_ANNOUNCE if self._features[KEY_ENQUEUE]: supported_features |= MediaPlayerEntityFeature.MEDIA_ENQUEUE diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index f96d2a012c837a..9ad7b983c7face 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -199,6 +199,7 @@ class MediaPlayerEntityFeature(IntFlag): BROWSE_MEDIA = 131072 REPEAT_SET = 262144 GROUPING = 524288 + MEDIA_ANNOUNCE = 1048576 MEDIA_ENQUEUE = 2097152 diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 536d229dbda8a7..21807262742248 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -172,6 +172,9 @@ play_media: announce: name: Announce description: If the media should be played as an announcement. + filter: + supported_features: + - media_player.MediaPlayerEntityFeature.MEDIA_ANNOUNCE required: false example: "true" selector: diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index c519d2371009bc..08f2b08f4df22e 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -195,6 +195,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): MediaPlayerEntityFeature.BROWSE_MEDIA | MediaPlayerEntityFeature.CLEAR_PLAYLIST | MediaPlayerEntityFeature.GROUPING + | MediaPlayerEntityFeature.MEDIA_ANNOUNCE | MediaPlayerEntityFeature.MEDIA_ENQUEUE | MediaPlayerEntityFeature.NEXT_TRACK | MediaPlayerEntityFeature.PAUSE diff --git a/tests/components/group/test_media_player.py b/tests/components/group/test_media_player.py index 3524c0f1e88d38..2a1a2a05e4e4d9 100644 --- a/tests/components/group/test_media_player.py +++ b/tests/components/group/test_media_player.py @@ -192,7 +192,9 @@ async def test_supported_features(hass: HomeAssistant) -> None: | MediaPlayerEntityFeature.STOP ) play_media = ( - MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.MEDIA_ENQUEUE + MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.MEDIA_ANNOUNCE + | MediaPlayerEntityFeature.MEDIA_ENQUEUE ) volume = ( MediaPlayerEntityFeature.VOLUME_MUTE From 7dc03ef301f1f98609757e5a0bfef226a8918e0b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Jul 2023 01:02:34 -1000 Subject: [PATCH 0312/1009] Use the ESPHome object_id to suggest the entity id (#95852) --- homeassistant/components/esphome/entity.py | 9 +- .../esphome/test_alarm_control_panel.py | 22 +-- .../components/esphome/test_binary_sensor.py | 10 +- tests/components/esphome/test_button.py | 8 +- tests/components/esphome/test_camera.py | 36 ++--- tests/components/esphome/test_climate.py | 28 ++-- tests/components/esphome/test_cover.py | 24 ++-- tests/components/esphome/test_entity.py | 41 +++++- tests/components/esphome/test_fan.py | 44 +++--- tests/components/esphome/test_light.py | 132 +++++++++--------- tests/components/esphome/test_lock.py | 14 +- tests/components/esphome/test_media_player.py | 22 +-- tests/components/esphome/test_number.py | 6 +- tests/components/esphome/test_select.py | 4 +- tests/components/esphome/test_sensor.py | 22 +-- tests/components/esphome/test_switch.py | 6 +- 16 files changed, 232 insertions(+), 196 deletions(-) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 15c136f17c3df3..2cfbc537dbb150 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -23,6 +23,7 @@ EntityCategory, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr from homeassistant.helpers.dispatcher import ( @@ -60,6 +61,7 @@ async def platform_async_setup_entry( entry_data: RuntimeEntryData = DomainData.get(hass).get_entry_data(entry) entry_data.info[info_type] = {} entry_data.state.setdefault(state_type, {}) + platform = entity_platform.async_get_current_platform() @callback def async_list_entities(infos: list[EntityInfo]) -> None: @@ -71,7 +73,7 @@ def async_list_entities(infos: list[EntityInfo]) -> None: for info in infos: if not current_infos.pop(info.key, None): # Create new entity - entity = entity_type(entry_data, info, state_type) + entity = entity_type(entry_data, platform.domain, info, state_type) add_entities.append(entity) new_infos[info.key] = info @@ -145,10 +147,12 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): def __init__( self, entry_data: RuntimeEntryData, + domain: str, entity_info: EntityInfo, state_type: type[_StateT], ) -> None: """Initialize.""" + self._entry_data = entry_data self._on_entry_data_changed() self._key = entity_info.key @@ -157,6 +161,9 @@ def __init__( assert entry_data.device_info is not None device_info = entry_data.device_info self._device_info = device_info + if object_id := entity_info.object_id: + # Use the object_id to suggest the entity_id + self.entity_id = f"{domain}.{device_info.name}_{object_id}" self._attr_has_entity_name = bool(device_info.friendly_name) self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)} diff --git a/tests/components/esphome/test_alarm_control_panel.py b/tests/components/esphome/test_alarm_control_panel.py index 90d7bde521582e..5a99f403394f56 100644 --- a/tests/components/esphome/test_alarm_control_panel.py +++ b/tests/components/esphome/test_alarm_control_panel.py @@ -61,7 +61,7 @@ async def test_generic_alarm_control_panel_requires_code( user_service=user_service, states=states, ) - state = hass.states.get("alarm_control_panel.test_my_alarm_control_panel") + state = hass.states.get("alarm_control_panel.test_myalarm_control_panel") assert state is not None assert state.state == STATE_ALARM_ARMED_AWAY @@ -69,7 +69,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_ARM_AWAY, { - ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -83,7 +83,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS, { - ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -97,7 +97,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_ARM_HOME, { - ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -111,7 +111,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_ARM_NIGHT, { - ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -125,7 +125,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_ARM_VACATION, { - ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -139,7 +139,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_TRIGGER, { - ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -153,7 +153,7 @@ async def test_generic_alarm_control_panel_requires_code( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_DISARM, { - ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel", + ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel", ATTR_CODE: 1234, }, blocking=True, @@ -196,14 +196,14 @@ async def test_generic_alarm_control_panel_no_code( user_service=user_service, states=states, ) - state = hass.states.get("alarm_control_panel.test_my_alarm_control_panel") + state = hass.states.get("alarm_control_panel.test_myalarm_control_panel") assert state is not None assert state.state == STATE_ALARM_ARMED_AWAY await hass.services.async_call( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_DISARM, - {ATTR_ENTITY_ID: "alarm_control_panel.test_my_alarm_control_panel"}, + {ATTR_ENTITY_ID: "alarm_control_panel.test_myalarm_control_panel"}, blocking=True, ) mock_client.alarm_control_panel_command.assert_has_calls( @@ -242,6 +242,6 @@ async def test_generic_alarm_control_panel_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("alarm_control_panel.test_my_alarm_control_panel") + state = hass.states.get("alarm_control_panel.test_myalarm_control_panel") assert state is not None assert state.state == STATE_UNKNOWN diff --git a/tests/components/esphome/test_binary_sensor.py b/tests/components/esphome/test_binary_sensor.py index 231bd51c0a33f5..209ea34432807c 100644 --- a/tests/components/esphome/test_binary_sensor.py +++ b/tests/components/esphome/test_binary_sensor.py @@ -73,7 +73,7 @@ async def test_binary_sensor_generic_entity( user_service=user_service, states=states, ) - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == hass_state @@ -104,7 +104,7 @@ async def test_status_binary_sensor( user_service=user_service, states=states, ) - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_ON @@ -134,7 +134,7 @@ async def test_binary_sensor_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_UNKNOWN @@ -164,12 +164,12 @@ async def test_binary_sensor_has_state_false( user_service=user_service, states=states, ) - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_UNKNOWN mock_device.set_state(BinarySensorState(key=1, state=True, missing_state=False)) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_ON diff --git a/tests/components/esphome/test_button.py b/tests/components/esphome/test_button.py index c0e7db14998df6..f33026800e7432 100644 --- a/tests/components/esphome/test_button.py +++ b/tests/components/esphome/test_button.py @@ -33,22 +33,22 @@ async def test_button_generic_entity( user_service=user_service, states=states, ) - state = hass.states.get("button.test_my_button") + state = hass.states.get("button.test_mybutton") assert state is not None assert state.state == STATE_UNKNOWN await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.test_my_button"}, + {ATTR_ENTITY_ID: "button.test_mybutton"}, blocking=True, ) mock_client.button_command.assert_has_calls([call(1)]) - state = hass.states.get("button.test_my_button") + state = hass.states.get("button.test_mybutton") assert state is not None assert state.state != STATE_UNKNOWN await mock_device.mock_disconnect(False) - state = hass.states.get("button.test_my_button") + state = hass.states.get("button.test_mybutton") assert state is not None assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/esphome/test_camera.py b/tests/components/esphome/test_camera.py index 94ff4c6e7a889c..f9a25d6b5f2c54 100644 --- a/tests/components/esphome/test_camera.py +++ b/tests/components/esphome/test_camera.py @@ -54,7 +54,7 @@ async def test_camera_single_image( user_service=user_service, states=states, ) - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_IDLE @@ -64,9 +64,9 @@ async def _mock_camera_image(): mock_client.request_single_image = _mock_camera_image client = await hass_client() - resp = await client.get("/api/camera_proxy/camera.test_my_camera") + resp = await client.get("/api/camera_proxy/camera.test_mycamera") await hass.async_block_till_done() - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_IDLE @@ -102,15 +102,15 @@ async def test_camera_single_image_unavailable_before_requested( user_service=user_service, states=states, ) - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_IDLE await mock_device.mock_disconnect(False) client = await hass_client() - resp = await client.get("/api/camera_proxy/camera.test_my_camera") + resp = await client.get("/api/camera_proxy/camera.test_mycamera") await hass.async_block_till_done() - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_UNAVAILABLE @@ -143,7 +143,7 @@ async def test_camera_single_image_unavailable_during_request( user_service=user_service, states=states, ) - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_IDLE @@ -153,9 +153,9 @@ async def _mock_camera_image(): mock_client.request_single_image = _mock_camera_image client = await hass_client() - resp = await client.get("/api/camera_proxy/camera.test_my_camera") + resp = await client.get("/api/camera_proxy/camera.test_mycamera") await hass.async_block_till_done() - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_UNAVAILABLE @@ -188,7 +188,7 @@ async def test_camera_stream( user_service=user_service, states=states, ) - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_IDLE remaining_responses = 3 @@ -204,9 +204,9 @@ async def _mock_camera_image(): mock_client.request_single_image = _mock_camera_image client = await hass_client() - resp = await client.get("/api/camera_proxy_stream/camera.test_my_camera") + resp = await client.get("/api/camera_proxy_stream/camera.test_mycamera") await hass.async_block_till_done() - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_IDLE @@ -248,16 +248,16 @@ async def test_camera_stream_unavailable( user_service=user_service, states=states, ) - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_IDLE await mock_device.mock_disconnect(False) client = await hass_client() - await client.get("/api/camera_proxy_stream/camera.test_my_camera") + await client.get("/api/camera_proxy_stream/camera.test_mycamera") await hass.async_block_till_done() - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_UNAVAILABLE @@ -288,7 +288,7 @@ async def test_camera_stream_with_disconnection( user_service=user_service, states=states, ) - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_IDLE remaining_responses = 3 @@ -306,8 +306,8 @@ async def _mock_camera_image(): mock_client.request_single_image = _mock_camera_image client = await hass_client() - await client.get("/api/camera_proxy_stream/camera.test_my_camera") + await client.get("/api/camera_proxy_stream/camera.test_mycamera") await hass.async_block_till_done() - state = hass.states.get("camera.test_my_camera") + state = hass.states.get("camera.test_mycamera") assert state is not None assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/esphome/test_climate.py b/tests/components/esphome/test_climate.py index 59072dc2e5999c..7e00fd22a1c7cb 100644 --- a/tests/components/esphome/test_climate.py +++ b/tests/components/esphome/test_climate.py @@ -71,14 +71,14 @@ async def test_climate_entity( user_service=user_service, states=states, ) - state = hass.states.get("climate.test_my_climate") + state = hass.states.get("climate.test_myclimate") assert state is not None assert state.state == HVACMode.COOL await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_TEMPERATURE: 25}, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_TEMPERATURE: 25}, blocking=True, ) mock_client.climate_command.assert_has_calls([call(key=1, target_temperature=25.0)]) @@ -123,14 +123,14 @@ async def test_climate_entity_with_step_and_two_point( user_service=user_service, states=states, ) - state = hass.states.get("climate.test_my_climate") + state = hass.states.get("climate.test_myclimate") assert state is not None assert state.state == HVACMode.COOL await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_TEMPERATURE: 25}, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_TEMPERATURE: 25}, blocking=True, ) mock_client.climate_command.assert_has_calls([call(key=1, target_temperature=25.0)]) @@ -140,7 +140,7 @@ async def test_climate_entity_with_step_and_two_point( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { - ATTR_ENTITY_ID: "climate.test_my_climate", + ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_HVAC_MODE: HVACMode.AUTO, ATTR_TARGET_TEMP_LOW: 20, ATTR_TARGET_TEMP_HIGH: 30, @@ -202,14 +202,14 @@ async def test_climate_entity_with_step_and_target_temp( user_service=user_service, states=states, ) - state = hass.states.get("climate.test_my_climate") + state = hass.states.get("climate.test_myclimate") assert state is not None assert state.state == HVACMode.COOL await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_TEMPERATURE: 25}, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_TEMPERATURE: 25}, blocking=True, ) mock_client.climate_command.assert_has_calls([call(key=1, target_temperature=25.0)]) @@ -219,7 +219,7 @@ async def test_climate_entity_with_step_and_target_temp( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { - ATTR_ENTITY_ID: "climate.test_my_climate", + ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_HVAC_MODE: HVACMode.AUTO, ATTR_TARGET_TEMP_LOW: 20, ATTR_TARGET_TEMP_HIGH: 30, @@ -242,7 +242,7 @@ async def test_climate_entity_with_step_and_target_temp( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, { - ATTR_ENTITY_ID: "climate.test_my_climate", + ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_HVAC_MODE: HVACMode.HEAT, }, blocking=True, @@ -260,7 +260,7 @@ async def test_climate_entity_with_step_and_target_temp( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_PRESET_MODE: "away"}, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_PRESET_MODE: "away"}, blocking=True, ) mock_client.climate_command.assert_has_calls( @@ -276,7 +276,7 @@ async def test_climate_entity_with_step_and_target_temp( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_PRESET_MODE: "preset1"}, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_PRESET_MODE: "preset1"}, blocking=True, ) mock_client.climate_command.assert_has_calls([call(key=1, custom_preset="preset1")]) @@ -285,7 +285,7 @@ async def test_climate_entity_with_step_and_target_temp( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_FAN_MODE: FAN_HIGH}, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_FAN_MODE: FAN_HIGH}, blocking=True, ) mock_client.climate_command.assert_has_calls( @@ -296,7 +296,7 @@ async def test_climate_entity_with_step_and_target_temp( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_FAN_MODE: "fan2"}, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_FAN_MODE: "fan2"}, blocking=True, ) mock_client.climate_command.assert_has_calls([call(key=1, custom_fan_mode="fan2")]) @@ -305,7 +305,7 @@ async def test_climate_entity_with_step_and_target_temp( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_SWING_MODE, - {ATTR_ENTITY_ID: "climate.test_my_climate", ATTR_SWING_MODE: SWING_BOTH}, + {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_SWING_MODE: SWING_BOTH}, blocking=True, ) mock_client.climate_command.assert_has_calls( diff --git a/tests/components/esphome/test_cover.py b/tests/components/esphome/test_cover.py index 59eadb3cfd9c41..b190d287198f62 100644 --- a/tests/components/esphome/test_cover.py +++ b/tests/components/esphome/test_cover.py @@ -72,7 +72,7 @@ async def test_cover_entity( user_service=user_service, states=states, ) - state = hass.states.get("cover.test_my_cover") + state = hass.states.get("cover.test_mycover") assert state is not None assert state.state == STATE_OPENING assert state.attributes[ATTR_CURRENT_POSITION] == 50 @@ -81,7 +81,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.test_my_cover"}, + {ATTR_ENTITY_ID: "cover.test_mycover"}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, position=0.0)]) @@ -90,7 +90,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.test_my_cover"}, + {ATTR_ENTITY_ID: "cover.test_mycover"}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, position=1.0)]) @@ -99,7 +99,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: "cover.test_my_cover", ATTR_POSITION: 50}, + {ATTR_ENTITY_ID: "cover.test_mycover", ATTR_POSITION: 50}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, position=0.5)]) @@ -108,7 +108,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: "cover.test_my_cover"}, + {ATTR_ENTITY_ID: "cover.test_mycover"}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, stop=True)]) @@ -117,7 +117,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER_TILT, - {ATTR_ENTITY_ID: "cover.test_my_cover"}, + {ATTR_ENTITY_ID: "cover.test_mycover"}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, tilt=1.0)]) @@ -126,7 +126,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER_TILT, - {ATTR_ENTITY_ID: "cover.test_my_cover"}, + {ATTR_ENTITY_ID: "cover.test_mycover"}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, tilt=0.0)]) @@ -135,7 +135,7 @@ async def test_cover_entity( await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_TILT_POSITION, - {ATTR_ENTITY_ID: "cover.test_my_cover", ATTR_TILT_POSITION: 50}, + {ATTR_ENTITY_ID: "cover.test_mycover", ATTR_TILT_POSITION: 50}, blocking=True, ) mock_client.cover_command.assert_has_calls([call(key=1, tilt=0.5)]) @@ -145,7 +145,7 @@ async def test_cover_entity( CoverState(key=1, position=0.0, current_operation=CoverOperation.IDLE) ) await hass.async_block_till_done() - state = hass.states.get("cover.test_my_cover") + state = hass.states.get("cover.test_mycover") assert state is not None assert state.state == STATE_CLOSED @@ -153,7 +153,7 @@ async def test_cover_entity( CoverState(key=1, position=0.5, current_operation=CoverOperation.IS_CLOSING) ) await hass.async_block_till_done() - state = hass.states.get("cover.test_my_cover") + state = hass.states.get("cover.test_mycover") assert state is not None assert state.state == STATE_CLOSING @@ -161,7 +161,7 @@ async def test_cover_entity( CoverState(key=1, position=1.0, current_operation=CoverOperation.IDLE) ) await hass.async_block_till_done() - state = hass.states.get("cover.test_my_cover") + state = hass.states.get("cover.test_mycover") assert state is not None assert state.state == STATE_OPEN @@ -201,7 +201,7 @@ async def test_cover_entity_without_position( user_service=user_service, states=states, ) - state = hass.states.get("cover.test_my_cover") + state = hass.states.get("cover.test_mycover") assert state is not None assert state.state == STATE_OPENING assert ATTR_CURRENT_TILT_POSITION not in state.attributes diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index 39bfec852e7959..1a7d62f886b318 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -55,10 +55,10 @@ async def test_entities_removed( entry = mock_device.entry entry_id = entry.entry_id storage_key = f"esphome.{entry_id}" - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_ON - state = hass.states.get("binary_sensor.test_my_binary_sensor_to_be_removed") + state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") assert state is not None assert state.state == STATE_ON @@ -67,10 +67,10 @@ async def test_entities_removed( assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 2 - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.attributes[ATTR_RESTORED] is True - state = hass.states.get("binary_sensor.test_my_binary_sensor_to_be_removed") + state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") assert state is not None assert state.attributes[ATTR_RESTORED] is True @@ -93,11 +93,40 @@ async def test_entities_removed( entry=entry, ) assert mock_device.entry.entry_id == entry_id - state = hass.states.get("binary_sensor.test_my_binary_sensor") + state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_ON - state = hass.states.get("binary_sensor.test_my_binary_sensor_to_be_removed") + state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed") assert state is None await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 1 + + +async def test_entity_info_object_ids( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test how object ids affect entity id.""" + entity_info = [ + BinarySensorInfo( + object_id="object_id_is_used", + key=1, + name="my binary_sensor", + unique_id="my_binary_sensor", + ) + ] + states = [] + user_service = [] + await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("binary_sensor.test_object_id_is_used") + assert state is not None diff --git a/tests/components/esphome/test_fan.py b/tests/components/esphome/test_fan.py index 4f8f3918a1be47..99f4bbc86a9fe2 100644 --- a/tests/components/esphome/test_fan.py +++ b/tests/components/esphome/test_fan.py @@ -61,14 +61,14 @@ async def test_fan_entity_with_all_features_old_api( user_service=user_service, states=states, ) - state = hass.states.get("fan.test_my_fan") + state = hass.states.get("fan.test_myfan") assert state is not None assert state.state == STATE_ON await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 20}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 20}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -79,7 +79,7 @@ async def test_fan_entity_with_all_features_old_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 50}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 50}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -90,7 +90,7 @@ async def test_fan_entity_with_all_features_old_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_DECREASE_SPEED, - {ATTR_ENTITY_ID: "fan.test_my_fan"}, + {ATTR_ENTITY_ID: "fan.test_myfan"}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -101,7 +101,7 @@ async def test_fan_entity_with_all_features_old_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_INCREASE_SPEED, - {ATTR_ENTITY_ID: "fan.test_my_fan"}, + {ATTR_ENTITY_ID: "fan.test_myfan"}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -112,7 +112,7 @@ async def test_fan_entity_with_all_features_old_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "fan.test_my_fan"}, + {ATTR_ENTITY_ID: "fan.test_myfan"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, state=False)]) @@ -121,7 +121,7 @@ async def test_fan_entity_with_all_features_old_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 100}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 100}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -163,14 +163,14 @@ async def test_fan_entity_with_all_features_new_api( user_service=user_service, states=states, ) - state = hass.states.get("fan.test_my_fan") + state = hass.states.get("fan.test_myfan") assert state is not None assert state.state == STATE_ON await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 20}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 20}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, speed_level=1, state=True)]) @@ -179,7 +179,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 50}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 50}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, speed_level=2, state=True)]) @@ -188,7 +188,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_DECREASE_SPEED, - {ATTR_ENTITY_ID: "fan.test_my_fan"}, + {ATTR_ENTITY_ID: "fan.test_myfan"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, speed_level=2, state=True)]) @@ -197,7 +197,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_INCREASE_SPEED, - {ATTR_ENTITY_ID: "fan.test_my_fan"}, + {ATTR_ENTITY_ID: "fan.test_myfan"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, speed_level=4, state=True)]) @@ -206,7 +206,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "fan.test_my_fan"}, + {ATTR_ENTITY_ID: "fan.test_myfan"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, state=False)]) @@ -215,7 +215,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 100}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 100}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, speed_level=4, state=True)]) @@ -224,7 +224,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PERCENTAGE, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_PERCENTAGE: 0}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_PERCENTAGE: 0}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, state=False)]) @@ -233,7 +233,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_OSCILLATE, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_OSCILLATING: True}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_OSCILLATING: True}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, oscillating=True)]) @@ -242,7 +242,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_OSCILLATE, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_OSCILLATING: False}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_OSCILLATING: False}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, oscillating=False)]) @@ -251,7 +251,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_DIRECTION, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_DIRECTION: "forward"}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_DIRECTION: "forward"}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -262,7 +262,7 @@ async def test_fan_entity_with_all_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_DIRECTION, - {ATTR_ENTITY_ID: "fan.test_my_fan", ATTR_DIRECTION: "reverse"}, + {ATTR_ENTITY_ID: "fan.test_myfan", ATTR_DIRECTION: "reverse"}, blocking=True, ) mock_client.fan_command.assert_has_calls( @@ -295,14 +295,14 @@ async def test_fan_entity_with_no_features_new_api( user_service=user_service, states=states, ) - state = hass.states.get("fan.test_my_fan") + state = hass.states.get("fan.test_myfan") assert state is not None assert state.state == STATE_ON await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.test_my_fan"}, + {ATTR_ENTITY_ID: "fan.test_myfan"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, state=True)]) @@ -311,7 +311,7 @@ async def test_fan_entity_with_no_features_new_api( await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "fan.test_my_fan"}, + {ATTR_ENTITY_ID: "fan.test_myfan"}, blocking=True, ) mock_client.fan_command.assert_has_calls([call(key=1, state=False)]) diff --git a/tests/components/esphome/test_light.py b/tests/components/esphome/test_light.py index a0998898e757bf..99058ad3ed4d6c 100644 --- a/tests/components/esphome/test_light.py +++ b/tests/components/esphome/test_light.py @@ -65,14 +65,14 @@ async def test_light_on_off( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -105,14 +105,14 @@ async def test_light_brightness( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -123,7 +123,7 @@ async def test_light_brightness( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -141,7 +141,7 @@ async def test_light_brightness( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_TRANSITION: 2}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_TRANSITION: 2}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -152,7 +152,7 @@ async def test_light_brightness( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_FLASH: FLASH_LONG}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_FLASH: FLASH_LONG}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -163,7 +163,7 @@ async def test_light_brightness( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_TRANSITION: 2}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_TRANSITION: 2}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -181,7 +181,7 @@ async def test_light_brightness( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_FLASH: FLASH_SHORT}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_FLASH: FLASH_SHORT}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -223,14 +223,14 @@ async def test_light_brightness_on_off( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -248,7 +248,7 @@ async def test_light_brightness_on_off( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -293,14 +293,14 @@ async def test_light_legacy_white_converted_to_brightness( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -343,14 +343,14 @@ async def test_light_brightness_on_off_with_unknown_color_mode( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -369,7 +369,7 @@ async def test_light_brightness_on_off_with_unknown_color_mode( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -414,14 +414,14 @@ async def test_light_on_and_brightness( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -464,14 +464,14 @@ async def test_rgb_color_temp_light( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -489,7 +489,7 @@ async def test_rgb_color_temp_light( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -508,7 +508,7 @@ async def test_rgb_color_temp_light( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_COLOR_TEMP_KELVIN: 2500}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_COLOR_TEMP_KELVIN: 2500}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -552,14 +552,14 @@ async def test_light_rgb( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -578,7 +578,7 @@ async def test_light_rgb( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -599,7 +599,7 @@ async def test_light_rgb( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_my_light", + ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127, ATTR_HS_COLOR: (100, 100), }, @@ -624,7 +624,7 @@ async def test_light_rgb( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGB_COLOR: (255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGB_COLOR: (255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -684,7 +684,7 @@ async def test_light_rgbw( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.RGBW] @@ -693,7 +693,7 @@ async def test_light_rgbw( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -713,7 +713,7 @@ async def test_light_rgbw( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -735,7 +735,7 @@ async def test_light_rgbw( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_my_light", + ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127, ATTR_HS_COLOR: (100, 100), }, @@ -762,7 +762,7 @@ async def test_light_rgbw( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGB_COLOR: (255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGB_COLOR: (255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -785,7 +785,7 @@ async def test_light_rgbw( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGBW_COLOR: (255, 255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGBW_COLOR: (255, 255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -855,7 +855,7 @@ async def test_light_rgbww_with_cold_warm_white_support( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.RGBWW] @@ -865,7 +865,7 @@ async def test_light_rgbww_with_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -887,7 +887,7 @@ async def test_light_rgbww_with_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -911,7 +911,7 @@ async def test_light_rgbww_with_cold_warm_white_support( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_my_light", + ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127, ATTR_HS_COLOR: (100, 100), }, @@ -941,7 +941,7 @@ async def test_light_rgbww_with_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGB_COLOR: (255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGB_COLOR: (255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -967,7 +967,7 @@ async def test_light_rgbww_with_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGBW_COLOR: (255, 255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGBW_COLOR: (255, 255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -994,7 +994,7 @@ async def test_light_rgbww_with_cold_warm_white_support( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_my_light", + ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGBWW_COLOR: (255, 255, 255, 255, 255), }, blocking=True, @@ -1022,7 +1022,7 @@ async def test_light_rgbww_with_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_COLOR_TEMP_KELVIN: 2500}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_COLOR_TEMP_KELVIN: 2500}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1092,7 +1092,7 @@ async def test_light_rgbww_without_cold_warm_white_support( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.RGBWW] @@ -1102,7 +1102,7 @@ async def test_light_rgbww_without_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1123,7 +1123,7 @@ async def test_light_rgbww_without_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_BRIGHTNESS: 127}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1146,7 +1146,7 @@ async def test_light_rgbww_without_cold_warm_white_support( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_my_light", + ATTR_ENTITY_ID: "light.test_mylight", ATTR_BRIGHTNESS: 127, ATTR_HS_COLOR: (100, 100), }, @@ -1175,7 +1175,7 @@ async def test_light_rgbww_without_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGB_COLOR: (255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGB_COLOR: (255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1200,7 +1200,7 @@ async def test_light_rgbww_without_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGBW_COLOR: (255, 255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGBW_COLOR: (255, 255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1226,7 +1226,7 @@ async def test_light_rgbww_without_cold_warm_white_support( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.test_my_light", + ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGBWW_COLOR: (255, 255, 255, 255, 255), }, blocking=True, @@ -1253,7 +1253,7 @@ async def test_light_rgbww_without_cold_warm_white_support( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_COLOR_TEMP_KELVIN: 2500}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_COLOR_TEMP_KELVIN: 2500}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1312,7 +1312,7 @@ async def test_light_color_temp( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON attributes = state.attributes @@ -1325,7 +1325,7 @@ async def test_light_color_temp( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1344,7 +1344,7 @@ async def test_light_color_temp( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls([call(key=1, state=False)]) @@ -1387,7 +1387,7 @@ async def test_light_color_temp_no_mireds_set( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON attributes = state.attributes @@ -1400,7 +1400,7 @@ async def test_light_color_temp_no_mireds_set( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1419,7 +1419,7 @@ async def test_light_color_temp_no_mireds_set( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_COLOR_TEMP_KELVIN: 6000}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_COLOR_TEMP_KELVIN: 6000}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1439,7 +1439,7 @@ async def test_light_color_temp_no_mireds_set( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls([call(key=1, state=False)]) @@ -1489,7 +1489,7 @@ async def test_light_color_temp_legacy( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON attributes = state.attributes @@ -1504,7 +1504,7 @@ async def test_light_color_temp_legacy( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1523,7 +1523,7 @@ async def test_light_color_temp_legacy( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls([call(key=1, state=False)]) @@ -1575,7 +1575,7 @@ async def test_light_rgb_legacy( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON attributes = state.attributes @@ -1585,7 +1585,7 @@ async def test_light_rgb_legacy( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1601,7 +1601,7 @@ async def test_light_rgb_legacy( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_my_light"}, + {ATTR_ENTITY_ID: "light.test_mylight"}, blocking=True, ) mock_client.light_command.assert_has_calls([call(key=1, state=False)]) @@ -1610,7 +1610,7 @@ async def test_light_rgb_legacy( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_RGB_COLOR: (255, 255, 255)}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_RGB_COLOR: (255, 255, 255)}, blocking=True, ) mock_client.light_command.assert_has_calls( @@ -1653,7 +1653,7 @@ async def test_light_effects( user_service=user_service, states=states, ) - state = hass.states.get("light.test_my_light") + state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_EFFECT_LIST] == ["effect1", "effect2"] @@ -1661,7 +1661,7 @@ async def test_light_effects( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_my_light", ATTR_EFFECT: "effect1"}, + {ATTR_ENTITY_ID: "light.test_mylight", ATTR_EFFECT: "effect1"}, blocking=True, ) mock_client.light_command.assert_has_calls( diff --git a/tests/components/esphome/test_lock.py b/tests/components/esphome/test_lock.py index 6e6461d34b1449..83312c8593470d 100644 --- a/tests/components/esphome/test_lock.py +++ b/tests/components/esphome/test_lock.py @@ -40,14 +40,14 @@ async def test_lock_entity_no_open( user_service=user_service, states=states, ) - state = hass.states.get("lock.test_my_lock") + state = hass.states.get("lock.test_mylock") assert state is not None assert state.state == STATE_UNLOCKING await hass.services.async_call( LOCK_DOMAIN, SERVICE_LOCK, - {ATTR_ENTITY_ID: "lock.test_my_lock"}, + {ATTR_ENTITY_ID: "lock.test_mylock"}, blocking=True, ) mock_client.lock_command.assert_has_calls([call(1, LockCommand.LOCK)]) @@ -74,7 +74,7 @@ async def test_lock_entity_start_locked( user_service=user_service, states=states, ) - state = hass.states.get("lock.test_my_lock") + state = hass.states.get("lock.test_mylock") assert state is not None assert state.state == STATE_LOCKED @@ -101,14 +101,14 @@ async def test_lock_entity_supports_open( user_service=user_service, states=states, ) - state = hass.states.get("lock.test_my_lock") + state = hass.states.get("lock.test_mylock") assert state is not None assert state.state == STATE_LOCKING await hass.services.async_call( LOCK_DOMAIN, SERVICE_LOCK, - {ATTR_ENTITY_ID: "lock.test_my_lock"}, + {ATTR_ENTITY_ID: "lock.test_mylock"}, blocking=True, ) mock_client.lock_command.assert_has_calls([call(1, LockCommand.LOCK)]) @@ -117,7 +117,7 @@ async def test_lock_entity_supports_open( await hass.services.async_call( LOCK_DOMAIN, SERVICE_UNLOCK, - {ATTR_ENTITY_ID: "lock.test_my_lock"}, + {ATTR_ENTITY_ID: "lock.test_mylock"}, blocking=True, ) mock_client.lock_command.assert_has_calls([call(1, LockCommand.UNLOCK, None)]) @@ -126,7 +126,7 @@ async def test_lock_entity_supports_open( await hass.services.async_call( LOCK_DOMAIN, SERVICE_OPEN, - {ATTR_ENTITY_ID: "lock.test_my_lock"}, + {ATTR_ENTITY_ID: "lock.test_mylock"}, blocking=True, ) mock_client.lock_command.assert_has_calls([call(1, LockCommand.OPEN)]) diff --git a/tests/components/esphome/test_media_player.py b/tests/components/esphome/test_media_player.py index bcef78e93454fc..ca97d9abeba366 100644 --- a/tests/components/esphome/test_media_player.py +++ b/tests/components/esphome/test_media_player.py @@ -63,7 +63,7 @@ async def test_media_player_entity( user_service=user_service, states=states, ) - state = hass.states.get("media_player.test_my_media_player") + state = hass.states.get("media_player.test_mymedia_player") assert state is not None assert state.state == "paused" @@ -71,7 +71,7 @@ async def test_media_player_entity( MEDIA_PLAYER_DOMAIN, SERVICE_VOLUME_MUTE, { - ATTR_ENTITY_ID: "media_player.test_my_media_player", + ATTR_ENTITY_ID: "media_player.test_mymedia_player", ATTR_MEDIA_VOLUME_MUTED: True, }, blocking=True, @@ -85,7 +85,7 @@ async def test_media_player_entity( MEDIA_PLAYER_DOMAIN, SERVICE_VOLUME_MUTE, { - ATTR_ENTITY_ID: "media_player.test_my_media_player", + ATTR_ENTITY_ID: "media_player.test_mymedia_player", ATTR_MEDIA_VOLUME_MUTED: True, }, blocking=True, @@ -99,7 +99,7 @@ async def test_media_player_entity( MEDIA_PLAYER_DOMAIN, SERVICE_VOLUME_SET, { - ATTR_ENTITY_ID: "media_player.test_my_media_player", + ATTR_ENTITY_ID: "media_player.test_mymedia_player", ATTR_MEDIA_VOLUME_LEVEL: 0.5, }, blocking=True, @@ -111,7 +111,7 @@ async def test_media_player_entity( MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_PAUSE, { - ATTR_ENTITY_ID: "media_player.test_my_media_player", + ATTR_ENTITY_ID: "media_player.test_mymedia_player", }, blocking=True, ) @@ -124,7 +124,7 @@ async def test_media_player_entity( MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_PLAY, { - ATTR_ENTITY_ID: "media_player.test_my_media_player", + ATTR_ENTITY_ID: "media_player.test_mymedia_player", }, blocking=True, ) @@ -137,7 +137,7 @@ async def test_media_player_entity( MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_STOP, { - ATTR_ENTITY_ID: "media_player.test_my_media_player", + ATTR_ENTITY_ID: "media_player.test_mymedia_player", }, blocking=True, ) @@ -206,7 +206,7 @@ async def test_media_player_entity_with_source( user_service=user_service, states=states, ) - state = hass.states.get("media_player.test_my_media_player") + state = hass.states.get("media_player.test_mymedia_player") assert state is not None assert state.state == "playing" @@ -215,7 +215,7 @@ async def test_media_player_entity_with_source( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { - ATTR_ENTITY_ID: "media_player.test_my_media_player", + ATTR_ENTITY_ID: "media_player.test_mymedia_player", ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, ATTR_MEDIA_CONTENT_ID: "media-source://local/xz", }, @@ -228,7 +228,7 @@ async def test_media_player_entity_with_source( { "id": 1, "type": "media_player/browse_media", - "entity_id": "media_player.test_my_media_player", + "entity_id": "media_player.test_mymedia_player", } ) response = await client.receive_json() @@ -238,7 +238,7 @@ async def test_media_player_entity_with_source( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { - ATTR_ENTITY_ID: "media_player.test_my_media_player", + ATTR_ENTITY_ID: "media_player.test_mymedia_player", ATTR_MEDIA_CONTENT_TYPE: MediaType.URL, ATTR_MEDIA_CONTENT_ID: "media-source://tts?message=hello", }, diff --git a/tests/components/esphome/test_number.py b/tests/components/esphome/test_number.py index 8157c5f5c3d824..cf3ee4876a832e 100644 --- a/tests/components/esphome/test_number.py +++ b/tests/components/esphome/test_number.py @@ -45,14 +45,14 @@ async def test_generic_number_entity( user_service=user_service, states=states, ) - state = hass.states.get("number.test_my_number") + state = hass.states.get("number.test_mynumber") assert state is not None assert state.state == "50" await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: "number.test_my_number", ATTR_VALUE: 50}, + {ATTR_ENTITY_ID: "number.test_mynumber", ATTR_VALUE: 50}, blocking=True, ) mock_client.number_command.assert_has_calls([call(1, 50)]) @@ -86,6 +86,6 @@ async def test_generic_number_nan( user_service=user_service, states=states, ) - state = hass.states.get("number.test_my_number") + state = hass.states.get("number.test_mynumber") assert state is not None assert state.state == STATE_UNKNOWN diff --git a/tests/components/esphome/test_select.py b/tests/components/esphome/test_select.py index 8d17276c304211..528483d42908b8 100644 --- a/tests/components/esphome/test_select.py +++ b/tests/components/esphome/test_select.py @@ -60,14 +60,14 @@ async def test_select_generic_entity( user_service=user_service, states=states, ) - state = hass.states.get("select.test_my_select") + state = hass.states.get("select.test_myselect") assert state is not None assert state.state == "a" await hass.services.async_call( SELECT_DOMAIN, SERVICE_SELECT_OPTION, - {ATTR_ENTITY_ID: "select.test_my_select", ATTR_OPTION: "b"}, + {ATTR_ENTITY_ID: "select.test_myselect", ATTR_OPTION: "b"}, blocking=True, ) mock_client.select_command.assert_has_calls([call(1, "b")]) diff --git a/tests/components/esphome/test_sensor.py b/tests/components/esphome/test_sensor.py index 8f4eb0f95139c8..9a1863c3c9072e 100644 --- a/tests/components/esphome/test_sensor.py +++ b/tests/components/esphome/test_sensor.py @@ -41,7 +41,7 @@ async def test_generic_numeric_sensor( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "50" @@ -70,12 +70,12 @@ async def test_generic_numeric_sensor_with_entity_category_and_icon( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "50" assert state.attributes[ATTR_ICON] == "mdi:leaf" entity_reg = er.async_get(hass) - entry = entity_reg.async_get("sensor.test_my_sensor") + entry = entity_reg.async_get("sensor.test_mysensor") assert entry is not None assert entry.unique_id == "my_sensor" assert entry.entity_category is EntityCategory.CONFIG @@ -106,12 +106,12 @@ async def test_generic_numeric_sensor_state_class_measurement( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "50" assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT entity_reg = er.async_get(hass) - entry = entity_reg.async_get("sensor.test_my_sensor") + entry = entity_reg.async_get("sensor.test_mysensor") assert entry is not None assert entry.unique_id == "my_sensor" assert entry.entity_category is None @@ -140,7 +140,7 @@ async def test_generic_numeric_sensor_device_class_timestamp( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "2023-06-22T18:43:52+00:00" @@ -169,7 +169,7 @@ async def test_generic_numeric_sensor_legacy_last_reset_convert( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "50" assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL_INCREASING @@ -195,7 +195,7 @@ async def test_generic_numeric_sensor_no_state( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == STATE_UNKNOWN @@ -220,7 +220,7 @@ async def test_generic_numeric_sensor_nan_state( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == STATE_UNKNOWN @@ -245,7 +245,7 @@ async def test_generic_numeric_sensor_missing_state( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == STATE_UNKNOWN @@ -272,6 +272,6 @@ async def test_generic_text_sensor( user_service=user_service, states=states, ) - state = hass.states.get("sensor.test_my_sensor") + state = hass.states.get("sensor.test_mysensor") assert state is not None assert state.state == "i am a teapot" diff --git a/tests/components/esphome/test_switch.py b/tests/components/esphome/test_switch.py index 39e01a7d07c852..cd60eb70edd59b 100644 --- a/tests/components/esphome/test_switch.py +++ b/tests/components/esphome/test_switch.py @@ -34,14 +34,14 @@ async def test_switch_generic_entity( user_service=user_service, states=states, ) - state = hass.states.get("switch.test_my_switch") + state = hass.states.get("switch.test_myswitch") assert state is not None assert state.state == STATE_ON await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.test_my_switch"}, + {ATTR_ENTITY_ID: "switch.test_myswitch"}, blocking=True, ) mock_client.switch_command.assert_has_calls([call(1, True)]) @@ -49,7 +49,7 @@ async def test_switch_generic_entity( await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.test_my_switch"}, + {ATTR_ENTITY_ID: "switch.test_myswitch"}, blocking=True, ) mock_client.switch_command.assert_has_calls([call(1, False)]) From 96c71b214f1e499a8bf7148a71e953025bb829f3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 10 Jul 2023 13:05:47 +0200 Subject: [PATCH 0313/1009] Check supported features in calls to vacuum services (#95833) * Check supported features in vacuum services * Update tests * Add comment --- homeassistant/components/vacuum/__init__.py | 78 +++++++++++++++---- .../components/vacuum/reproduce_state.py | 2 +- tests/components/demo/test_vacuum.py | 77 ++++++++---------- tests/components/mqtt/test_legacy_vacuum.py | 28 ++++--- tests/components/mqtt/test_state_vacuum.py | 11 ++- tests/components/template/test_vacuum.py | 21 +++-- 6 files changed, 136 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index cf82836cbecba3..0dc4d19ba36331 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -77,19 +77,19 @@ class VacuumEntityFeature(IntFlag): """Supported features of the vacuum entity.""" - TURN_ON = 1 - TURN_OFF = 2 + TURN_ON = 1 # Deprecated, not supported by StateVacuumEntity + TURN_OFF = 2 # Deprecated, not supported by StateVacuumEntity PAUSE = 4 STOP = 8 RETURN_HOME = 16 FAN_SPEED = 32 BATTERY = 64 - STATUS = 128 + STATUS = 128 # Deprecated, not supported by StateVacuumEntity SEND_COMMAND = 256 LOCATE = 512 CLEAN_SPOT = 1024 MAP = 2048 - STATE = 4096 + STATE = 4096 # Must be set by vacuum platforms derived from StateVacuumEntity START = 8192 @@ -127,24 +127,73 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await component.async_setup(config) - component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") - component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") - component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") component.async_register_entity_service( - SERVICE_START_PAUSE, {}, "async_start_pause" + SERVICE_TURN_ON, + {}, + "async_turn_on", + [VacuumEntityFeature.TURN_ON], ) - component.async_register_entity_service(SERVICE_START, {}, "async_start") - component.async_register_entity_service(SERVICE_PAUSE, {}, "async_pause") component.async_register_entity_service( - SERVICE_RETURN_TO_BASE, {}, "async_return_to_base" + SERVICE_TURN_OFF, + {}, + "async_turn_off", + [VacuumEntityFeature.TURN_OFF], + ) + component.async_register_entity_service( + SERVICE_TOGGLE, + {}, + "async_toggle", + [VacuumEntityFeature.TURN_OFF | VacuumEntityFeature.TURN_ON], + ) + # start_pause is a legacy service, only supported by VacuumEntity, and only needs + # VacuumEntityFeature.PAUSE + component.async_register_entity_service( + SERVICE_START_PAUSE, + {}, + "async_start_pause", + [VacuumEntityFeature.PAUSE], + ) + component.async_register_entity_service( + SERVICE_START, + {}, + "async_start", + [VacuumEntityFeature.START], + ) + component.async_register_entity_service( + SERVICE_PAUSE, + {}, + "async_pause", + [VacuumEntityFeature.PAUSE], + ) + component.async_register_entity_service( + SERVICE_RETURN_TO_BASE, + {}, + "async_return_to_base", + [VacuumEntityFeature.RETURN_HOME], + ) + component.async_register_entity_service( + SERVICE_CLEAN_SPOT, + {}, + "async_clean_spot", + [VacuumEntityFeature.CLEAN_SPOT], + ) + component.async_register_entity_service( + SERVICE_LOCATE, + {}, + "async_locate", + [VacuumEntityFeature.LOCATE], + ) + component.async_register_entity_service( + SERVICE_STOP, + {}, + "async_stop", + [VacuumEntityFeature.STOP], ) - component.async_register_entity_service(SERVICE_CLEAN_SPOT, {}, "async_clean_spot") - component.async_register_entity_service(SERVICE_LOCATE, {}, "async_locate") - component.async_register_entity_service(SERVICE_STOP, {}, "async_stop") component.async_register_entity_service( SERVICE_SET_FAN_SPEED, {vol.Required(ATTR_FAN_SPEED): cv.string}, "async_set_fan_speed", + [VacuumEntityFeature.FAN_SPEED], ) component.async_register_entity_service( SERVICE_SEND_COMMAND, @@ -153,6 +202,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: vol.Optional(ATTR_PARAMS): vol.Any(dict, cv.ensure_list), }, "async_send_command", + [VacuumEntityFeature.SEND_COMMAND], ) return True diff --git a/homeassistant/components/vacuum/reproduce_state.py b/homeassistant/components/vacuum/reproduce_state.py index fbcc97445c8134..4d0d6b4b12cf48 100644 --- a/homeassistant/components/vacuum/reproduce_state.py +++ b/homeassistant/components/vacuum/reproduce_state.py @@ -37,8 +37,8 @@ STATE_CLEANING, STATE_DOCKED, STATE_IDLE, - STATE_RETURNING, STATE_PAUSED, + STATE_RETURNING, } diff --git a/tests/components/demo/test_vacuum.py b/tests/components/demo/test_vacuum.py index bc003c6e27ba38..4f977436055299 100644 --- a/tests/components/demo/test_vacuum.py +++ b/tests/components/demo/test_vacuum.py @@ -37,6 +37,7 @@ STATE_ON, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -201,42 +202,39 @@ async def test_unsupported_methods(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert vacuum.is_on(hass, ENTITY_VACUUM_NONE) - await common.async_turn_off(hass, ENTITY_VACUUM_NONE) - assert vacuum.is_on(hass, ENTITY_VACUUM_NONE) + with pytest.raises(HomeAssistantError): + await common.async_turn_off(hass, ENTITY_VACUUM_NONE) - await common.async_stop(hass, ENTITY_VACUUM_NONE) - assert vacuum.is_on(hass, ENTITY_VACUUM_NONE) + with pytest.raises(HomeAssistantError): + await common.async_stop(hass, ENTITY_VACUUM_NONE) hass.states.async_set(ENTITY_VACUUM_NONE, STATE_OFF) await hass.async_block_till_done() assert not vacuum.is_on(hass, ENTITY_VACUUM_NONE) - await common.async_turn_on(hass, ENTITY_VACUUM_NONE) - assert not vacuum.is_on(hass, ENTITY_VACUUM_NONE) + with pytest.raises(HomeAssistantError): + await common.async_turn_on(hass, ENTITY_VACUUM_NONE) - await common.async_toggle(hass, ENTITY_VACUUM_NONE) - assert not vacuum.is_on(hass, ENTITY_VACUUM_NONE) + with pytest.raises(HomeAssistantError): + await common.async_toggle(hass, ENTITY_VACUUM_NONE) # Non supported methods: - await common.async_start_pause(hass, ENTITY_VACUUM_NONE) - assert not vacuum.is_on(hass, ENTITY_VACUUM_NONE) + with pytest.raises(HomeAssistantError): + await common.async_start_pause(hass, ENTITY_VACUUM_NONE) - await common.async_locate(hass, ENTITY_VACUUM_NONE) - state = hass.states.get(ENTITY_VACUUM_NONE) - assert state.attributes.get(ATTR_STATUS) is None + with pytest.raises(HomeAssistantError): + await common.async_locate(hass, ENTITY_VACUUM_NONE) - await common.async_return_to_base(hass, ENTITY_VACUUM_NONE) - state = hass.states.get(ENTITY_VACUUM_NONE) - assert state.attributes.get(ATTR_STATUS) is None + with pytest.raises(HomeAssistantError): + await common.async_return_to_base(hass, ENTITY_VACUUM_NONE) - await common.async_set_fan_speed(hass, FAN_SPEEDS[-1], entity_id=ENTITY_VACUUM_NONE) - state = hass.states.get(ENTITY_VACUUM_NONE) - assert state.attributes.get(ATTR_FAN_SPEED) != FAN_SPEEDS[-1] + with pytest.raises(HomeAssistantError): + await common.async_set_fan_speed( + hass, FAN_SPEEDS[-1], entity_id=ENTITY_VACUUM_NONE + ) - await common.async_clean_spot(hass, entity_id=ENTITY_VACUUM_BASIC) - state = hass.states.get(ENTITY_VACUUM_BASIC) - assert "spot" not in state.attributes.get(ATTR_STATUS) - assert state.state == STATE_OFF + with pytest.raises(HomeAssistantError): + await common.async_clean_spot(hass, entity_id=ENTITY_VACUUM_BASIC) # VacuumEntity should not support start and pause methods. hass.states.async_set(ENTITY_VACUUM_COMPLETE, STATE_ON) @@ -250,21 +248,18 @@ async def test_unsupported_methods(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert not vacuum.is_on(hass, ENTITY_VACUUM_COMPLETE) - await common.async_start(hass, ENTITY_VACUUM_COMPLETE) - assert not vacuum.is_on(hass, ENTITY_VACUUM_COMPLETE) + with pytest.raises(HomeAssistantError): + await common.async_start(hass, ENTITY_VACUUM_COMPLETE) # StateVacuumEntity does not support on/off - await common.async_turn_on(hass, entity_id=ENTITY_VACUUM_STATE) - state = hass.states.get(ENTITY_VACUUM_STATE) - assert state.state != STATE_CLEANING + with pytest.raises(HomeAssistantError): + await common.async_turn_on(hass, entity_id=ENTITY_VACUUM_STATE) - await common.async_turn_off(hass, entity_id=ENTITY_VACUUM_STATE) - state = hass.states.get(ENTITY_VACUUM_STATE) - assert state.state != STATE_RETURNING + with pytest.raises(HomeAssistantError): + await common.async_turn_off(hass, entity_id=ENTITY_VACUUM_STATE) - await common.async_toggle(hass, entity_id=ENTITY_VACUUM_STATE) - state = hass.states.get(ENTITY_VACUUM_STATE) - assert state.state != STATE_CLEANING + with pytest.raises(HomeAssistantError): + await common.async_toggle(hass, entity_id=ENTITY_VACUUM_STATE) async def test_services(hass: HomeAssistant) -> None: @@ -302,22 +297,15 @@ async def test_services(hass: HomeAssistant) -> None: async def test_set_fan_speed(hass: HomeAssistant) -> None: """Test vacuum service to set the fan speed.""" - group_vacuums = ",".join( - [ENTITY_VACUUM_BASIC, ENTITY_VACUUM_COMPLETE, ENTITY_VACUUM_STATE] - ) - old_state_basic = hass.states.get(ENTITY_VACUUM_BASIC) + group_vacuums = ",".join([ENTITY_VACUUM_COMPLETE, ENTITY_VACUUM_STATE]) old_state_complete = hass.states.get(ENTITY_VACUUM_COMPLETE) old_state_state = hass.states.get(ENTITY_VACUUM_STATE) await common.async_set_fan_speed(hass, FAN_SPEEDS[0], entity_id=group_vacuums) - new_state_basic = hass.states.get(ENTITY_VACUUM_BASIC) new_state_complete = hass.states.get(ENTITY_VACUUM_COMPLETE) new_state_state = hass.states.get(ENTITY_VACUUM_STATE) - assert old_state_basic == new_state_basic - assert ATTR_FAN_SPEED not in new_state_basic.attributes - assert old_state_complete != new_state_complete assert old_state_complete.attributes[ATTR_FAN_SPEED] == FAN_SPEEDS[1] assert new_state_complete.attributes[ATTR_FAN_SPEED] == FAN_SPEEDS[0] @@ -329,18 +317,15 @@ async def test_set_fan_speed(hass: HomeAssistant) -> None: async def test_send_command(hass: HomeAssistant) -> None: """Test vacuum service to send a command.""" - group_vacuums = ",".join([ENTITY_VACUUM_BASIC, ENTITY_VACUUM_COMPLETE]) - old_state_basic = hass.states.get(ENTITY_VACUUM_BASIC) + group_vacuums = ",".join([ENTITY_VACUUM_COMPLETE]) old_state_complete = hass.states.get(ENTITY_VACUUM_COMPLETE) await common.async_send_command( hass, "test_command", params={"p1": 3}, entity_id=group_vacuums ) - new_state_basic = hass.states.get(ENTITY_VACUUM_BASIC) new_state_complete = hass.states.get(ENTITY_VACUUM_COMPLETE) - assert old_state_basic == new_state_basic assert old_state_complete != new_state_complete assert new_state_complete.state == STATE_ON assert ( diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 0297f4216c4d63..9a71c747e65d73 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -32,6 +32,7 @@ ) from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType from .test_common import ( @@ -245,39 +246,48 @@ async def test_commands_without_supported_features( """Test commands which are not supported by the vacuum.""" mqtt_mock = await mqtt_mock_entry() - await common.async_turn_on(hass, "vacuum.mqtttest") + with pytest.raises(HomeAssistantError): + await common.async_turn_on(hass, "vacuum.mqtttest") mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await common.async_turn_off(hass, "vacuum.mqtttest") + with pytest.raises(HomeAssistantError): + await common.async_turn_off(hass, "vacuum.mqtttest") mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await common.async_stop(hass, "vacuum.mqtttest") + with pytest.raises(HomeAssistantError): + await common.async_stop(hass, "vacuum.mqtttest") mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await common.async_clean_spot(hass, "vacuum.mqtttest") + with pytest.raises(HomeAssistantError): + await common.async_clean_spot(hass, "vacuum.mqtttest") mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await common.async_locate(hass, "vacuum.mqtttest") + with pytest.raises(HomeAssistantError): + await common.async_locate(hass, "vacuum.mqtttest") mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await common.async_start_pause(hass, "vacuum.mqtttest") + with pytest.raises(HomeAssistantError): + await common.async_start_pause(hass, "vacuum.mqtttest") mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await common.async_return_to_base(hass, "vacuum.mqtttest") + with pytest.raises(HomeAssistantError): + await common.async_return_to_base(hass, "vacuum.mqtttest") mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await common.async_set_fan_speed(hass, "high", "vacuum.mqtttest") + with pytest.raises(HomeAssistantError): + await common.async_set_fan_speed(hass, "high", "vacuum.mqtttest") mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await common.async_send_command(hass, "44 FE 93", entity_id="vacuum.mqtttest") + with pytest.raises(HomeAssistantError): + await common.async_send_command(hass, "44 FE 93", entity_id="vacuum.mqtttest") mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index dd15399f67088b..38baf591094d34 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -29,6 +29,7 @@ ) from homeassistant.const import CONF_NAME, ENTITY_MATCH_ALL, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .test_common import ( help_custom_config, @@ -242,13 +243,15 @@ async def test_commands_without_supported_features( mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await common.async_set_fan_speed(hass, "medium", "vacuum.mqtttest") + with pytest.raises(HomeAssistantError): + await common.async_set_fan_speed(hass, "medium", "vacuum.mqtttest") mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await common.async_send_command( - hass, "44 FE 93", {"key": "value"}, entity_id="vacuum.mqtttest" - ) + with pytest.raises(HomeAssistantError): + await common.async_send_command( + hass, "44 FE 93", {"key": "value"}, entity_id="vacuum.mqtttest" + ) mqtt_mock.async_publish.assert_not_called() diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index 3a911a68416109..e68507284502ef 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -12,6 +12,7 @@ ) from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_component import async_update_entity from tests.common import assert_setup_component @@ -317,31 +318,37 @@ async def test_unique_id(hass: HomeAssistant, start_ha) -> None: async def test_unused_services(hass: HomeAssistant) -> None: - """Test calling unused services should not crash.""" + """Test calling unused services raises.""" await _register_basic_vacuum(hass) # Pause vacuum - await common.async_pause(hass, _TEST_VACUUM) + with pytest.raises(HomeAssistantError): + await common.async_pause(hass, _TEST_VACUUM) await hass.async_block_till_done() # Stop vacuum - await common.async_stop(hass, _TEST_VACUUM) + with pytest.raises(HomeAssistantError): + await common.async_stop(hass, _TEST_VACUUM) await hass.async_block_till_done() # Return vacuum to base - await common.async_return_to_base(hass, _TEST_VACUUM) + with pytest.raises(HomeAssistantError): + await common.async_return_to_base(hass, _TEST_VACUUM) await hass.async_block_till_done() # Spot cleaning - await common.async_clean_spot(hass, _TEST_VACUUM) + with pytest.raises(HomeAssistantError): + await common.async_clean_spot(hass, _TEST_VACUUM) await hass.async_block_till_done() # Locate vacuum - await common.async_locate(hass, _TEST_VACUUM) + with pytest.raises(HomeAssistantError): + await common.async_locate(hass, _TEST_VACUUM) await hass.async_block_till_done() # Set fan's speed - await common.async_set_fan_speed(hass, "medium", _TEST_VACUUM) + with pytest.raises(HomeAssistantError): + await common.async_set_fan_speed(hass, "medium", _TEST_VACUUM) await hass.async_block_till_done() _verify(hass, STATE_UNKNOWN, None) From 08a5f6347487be6238bdf16b51daba041daa1052 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 10 Jul 2023 13:06:52 +0200 Subject: [PATCH 0314/1009] Add deprecated_yaml issue to the homeassistant integration (#95980) * Add deprecated_yaml issue to the homeassistant integration * Update test * Update homeassistant/components/homeassistant/strings.json Co-authored-by: G Johansson * Include DOMAIN in issue_id * Update test --------- Co-authored-by: G Johansson --- homeassistant/components/brottsplatskartan/sensor.py | 11 ++++++++--- .../components/brottsplatskartan/strings.json | 6 ------ .../components/dwd_weather_warnings/sensor.py | 11 ++++++++--- .../components/dwd_weather_warnings/strings.json | 6 ------ homeassistant/components/dynalite/config_flow.py | 10 ++++++++-- homeassistant/components/dynalite/strings.json | 6 ------ .../components/geo_json_events/geo_location.py | 11 ++++++++--- homeassistant/components/geo_json_events/strings.json | 6 ------ homeassistant/components/homeassistant/strings.json | 4 ++++ homeassistant/components/lastfm/sensor.py | 11 ++++++++--- homeassistant/components/lastfm/strings.json | 6 ------ homeassistant/components/mystrom/light.py | 11 ++++++++--- homeassistant/components/mystrom/strings.json | 6 ------ homeassistant/components/mystrom/switch.py | 11 ++++++++--- homeassistant/components/qbittorrent/sensor.py | 11 ++++++++--- homeassistant/components/qbittorrent/strings.json | 6 ------ homeassistant/components/qnap/sensor.py | 11 ++++++++--- homeassistant/components/qnap/strings.json | 6 ------ homeassistant/components/snapcast/media_player.py | 11 ++++++++--- homeassistant/components/snapcast/strings.json | 6 ------ homeassistant/components/workday/binary_sensor.py | 11 ++++++++--- homeassistant/components/workday/strings.json | 6 ------ homeassistant/components/zodiac/__init__.py | 11 ++++++++--- homeassistant/components/zodiac/strings.json | 6 ------ tests/components/dynalite/test_config_flow.py | 7 +++++-- 25 files changed, 105 insertions(+), 103 deletions(-) diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index 5512bcd1176ff8..add558ff48b336 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -13,7 +13,7 @@ ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -45,12 +45,17 @@ async def async_setup_platform( async_create_issue( hass, - DOMAIN, - "deprecated_yaml", + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", breaks_in_ha_version="2023.11.0", is_fixable=False, + issue_domain=DOMAIN, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Brottsplatskartan", + }, ) hass.async_create_task( diff --git a/homeassistant/components/brottsplatskartan/strings.json b/homeassistant/components/brottsplatskartan/strings.json index 8d9677a0af478f..f10120f7884553 100644 --- a/homeassistant/components/brottsplatskartan/strings.json +++ b/homeassistant/components/brottsplatskartan/strings.json @@ -16,12 +16,6 @@ } } }, - "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/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index f44d736b426cf7..62bb4af7930ef9 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -22,7 +22,7 @@ ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -89,12 +89,17 @@ async def async_setup_platform( # Show issue as long as the YAML configuration exists. async_create_issue( hass, - DOMAIN, - "deprecated_yaml", + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", breaks_in_ha_version="2023.12.0", is_fixable=False, + issue_domain=DOMAIN, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Deutscher Wetterdienst (DWD) Weather Warnings", + }, ) hass.async_create_task( diff --git a/homeassistant/components/dwd_weather_warnings/strings.json b/homeassistant/components/dwd_weather_warnings/strings.json index c5c954a9f8e5a9..60e53f90dbdb39 100644 --- a/homeassistant/components/dwd_weather_warnings/strings.json +++ b/homeassistant/components/dwd_weather_warnings/strings.json @@ -15,11 +15,5 @@ "already_configured": "Warncell ID / name is already configured.", "invalid_identifier": "[%key:component::dwd_weather_warnings::config::error::invalid_identifier%]" } - }, - "issues": { - "deprecated_yaml": { - "title": "The Deutscher Wetterdienst (DWD) Weather Warnings YAML configuration is being removed", - "description": "Configuring Deutscher Wetterdienst (DWD) Weather Warnings using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Deutscher Wetterdienst (DWD) Weather Warnings YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } } } diff --git a/homeassistant/components/dynalite/config_flow.py b/homeassistant/components/dynalite/config_flow.py index 946d4ac653d79b..8438307c698c4d 100644 --- a/homeassistant/components/dynalite/config_flow.py +++ b/homeassistant/components/dynalite/config_flow.py @@ -7,6 +7,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -31,12 +32,17 @@ async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult: # Raise an issue that this is deprecated and has been imported async_create_issue( self.hass, - DOMAIN, - "deprecated_yaml", + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", is_fixable=False, is_persistent=False, + issue_domain=DOMAIN, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Dynalite", + }, ) host = import_info[CONF_HOST] diff --git a/homeassistant/components/dynalite/strings.json b/homeassistant/components/dynalite/strings.json index 1d78108f909fff..8ad7deacd92ffa 100644 --- a/homeassistant/components/dynalite/strings.json +++ b/homeassistant/components/dynalite/strings.json @@ -14,11 +14,5 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } - }, - "issues": { - "deprecated_yaml": { - "title": "The Dynalite YAML configuration is being removed", - "description": "Configuring Dynalite using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Dynalite YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } } } diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py index b922d98f25e1e2..c0192a0037d228 100644 --- a/homeassistant/components/geo_json_events/geo_location.py +++ b/homeassistant/components/geo_json_events/geo_location.py @@ -17,7 +17,7 @@ CONF_URL, UnitOfLength, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -81,12 +81,17 @@ async def async_setup_platform( """Set up the GeoJSON Events platform.""" async_create_issue( hass, - DOMAIN, - "deprecated_yaml", + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", breaks_in_ha_version="2023.12.0", is_fixable=False, + issue_domain=DOMAIN, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "GeoJSON feed", + }, ) hass.async_create_task( hass.config_entries.flow.async_init( diff --git a/homeassistant/components/geo_json_events/strings.json b/homeassistant/components/geo_json_events/strings.json index e50369d6e74918..1a2409b1cd2323 100644 --- a/homeassistant/components/geo_json_events/strings.json +++ b/homeassistant/components/geo_json_events/strings.json @@ -12,11 +12,5 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } - }, - "issues": { - "deprecated_yaml": { - "title": "The GeoJSON feed YAML configuration is being removed", - "description": "Configuring a GeoJSON feed using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the GeoJSON feed YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } } } diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index edb26c3622ef1e..89da615cf31111 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -4,6 +4,10 @@ "title": "The country has not been configured", "description": "No country has been configured, please update the configuration by clicking on the \"learn more\" button below." }, + "deprecated_yaml": { + "title": "The {integration_title} YAML configuration is being removed", + "description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + }, "historic_currency": { "title": "The configured currency is no longer in use", "description": "The currency {currency} is no longer in use, please reconfigure the currency configuration." diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index b4776b19c50db9..c51868394dead9 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_API_KEY -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -51,12 +51,17 @@ async def async_setup_platform( async_create_issue( hass, - DOMAIN, - "deprecated_yaml", + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", breaks_in_ha_version="2023.12.0", is_fixable=False, + issue_domain=DOMAIN, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "LastFM", + }, ) hass.async_create_task( diff --git a/homeassistant/components/lastfm/strings.json b/homeassistant/components/lastfm/strings.json index f9156bed658c46..fe9a4b6453fefd 100644 --- a/homeassistant/components/lastfm/strings.json +++ b/homeassistant/components/lastfm/strings.json @@ -35,11 +35,5 @@ "invalid_account": "Invalid username", "unknown": "[%key:common::config_flow::error::unknown%]" } - }, - "issues": { - "deprecated_yaml": { - "title": "The LastFM YAML configuration is being removed", - "description": "Configuring LastFM using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the LastFM YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } } } diff --git a/homeassistant/components/mystrom/light.py b/homeassistant/components/mystrom/light.py index 14badde17d246c..8c4998fa45e2c6 100644 --- a/homeassistant/components/mystrom/light.py +++ b/homeassistant/components/mystrom/light.py @@ -18,7 +18,7 @@ ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -61,12 +61,17 @@ async def async_setup_platform( """Set up the myStrom light integration.""" async_create_issue( hass, - DOMAIN, - "deprecated_yaml", + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", breaks_in_ha_version="2023.12.0", is_fixable=False, + issue_domain=DOMAIN, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "myStrom", + }, ) hass.async_create_task( hass.config_entries.flow.async_init( diff --git a/homeassistant/components/mystrom/strings.json b/homeassistant/components/mystrom/strings.json index 259501e14864e5..a485a58f5a6621 100644 --- a/homeassistant/components/mystrom/strings.json +++ b/homeassistant/components/mystrom/strings.json @@ -14,11 +14,5 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } - }, - "issues": { - "deprecated_yaml": { - "title": "The myStrom YAML configuration is being removed", - "description": "Configuring myStrom using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the myStrom YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } } } diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index 8e89bb5f1518b7..7180862758cb23 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -10,7 +10,7 @@ from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -48,12 +48,17 @@ async def async_setup_platform( """Set up the myStrom switch/plug integration.""" async_create_issue( hass, - DOMAIN, - "deprecated_yaml", + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", breaks_in_ha_version="2023.12.0", is_fixable=False, + issue_domain=DOMAIN, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "myStrom", + }, ) hass.async_create_task( hass.config_entries.flow.async_init( diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 15a634cf7a9872..0d5dc160a1194d 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -23,7 +23,7 @@ STATE_IDLE, UnitOfDataRate, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers import issue_registry as ir import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -84,12 +84,17 @@ async def async_setup_platform( ) ir.async_create_issue( hass, - DOMAIN, - "deprecated_yaml", + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", breaks_in_ha_version="2023.11.0", is_fixable=False, + issue_domain=DOMAIN, severity=ir.IssueSeverity.WARNING, translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "qBittorrent", + }, ) diff --git a/homeassistant/components/qbittorrent/strings.json b/homeassistant/components/qbittorrent/strings.json index 24d1885a9177ac..66c9430911e746 100644 --- a/homeassistant/components/qbittorrent/strings.json +++ b/homeassistant/components/qbittorrent/strings.json @@ -17,11 +17,5 @@ "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/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index 6d214b63e2e184..c26b72f92956aa 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -28,7 +28,7 @@ UnitOfInformation, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo @@ -235,12 +235,17 @@ async def async_setup_platform( async_create_issue( hass, - DOMAIN, - "deprecated_yaml", + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", breaks_in_ha_version="2023.12.0", is_fixable=False, + issue_domain=DOMAIN, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "QNAP", + }, ) hass.async_create_task( diff --git a/homeassistant/components/qnap/strings.json b/homeassistant/components/qnap/strings.json index 36946b81c0ca18..26ca5dedd34422 100644 --- a/homeassistant/components/qnap/strings.json +++ b/homeassistant/components/qnap/strings.json @@ -19,11 +19,5 @@ "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." - } } } diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index 096e3829bc7ba8..9dadae2e3e2a55 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -14,7 +14,7 @@ ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, 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 @@ -102,12 +102,17 @@ async def async_setup_platform( """Set up the Snapcast platform.""" async_create_issue( hass, - DOMAIN, - "deprecated_yaml", + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", breaks_in_ha_version="2023.11.0", is_fixable=False, + issue_domain=DOMAIN, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Snapcast", + }, ) config[CONF_PORT] = config.get(CONF_PORT, CONTROL_PORT) diff --git a/homeassistant/components/snapcast/strings.json b/homeassistant/components/snapcast/strings.json index 0087b70d8204e0..766bca634955d6 100644 --- a/homeassistant/components/snapcast/strings.json +++ b/homeassistant/components/snapcast/strings.json @@ -17,11 +17,5 @@ "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/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 0814958ad27d23..c80608ab1c2e9a 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -14,7 +14,7 @@ ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -90,12 +90,17 @@ async def async_setup_platform( """Set up the Workday sensor.""" async_create_issue( hass, - DOMAIN, - "deprecated_yaml", + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", breaks_in_ha_version="2023.11.0", is_fixable=False, + issue_domain=DOMAIN, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Workday", + }, ) hass.async_create_task( diff --git a/homeassistant/components/workday/strings.json b/homeassistant/components/workday/strings.json index fcebc7638c6ef3..6ea8348812d1ef 100644 --- a/homeassistant/components/workday/strings.json +++ b/homeassistant/components/workday/strings.json @@ -65,12 +65,6 @@ "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/homeassistant/components/zodiac/__init__.py b/homeassistant/components/zodiac/__init__.py index 892bcac5bf9177..81d5b5bdc219bf 100644 --- a/homeassistant/components/zodiac/__init__.py +++ b/homeassistant/components/zodiac/__init__.py @@ -3,7 +3,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType @@ -20,12 +20,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_create_issue( hass, - DOMAIN, - "deprecated_yaml", + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", breaks_in_ha_version="2024.1.0", is_fixable=False, + issue_domain=DOMAIN, severity=IssueSeverity.WARNING, translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Zodiac", + }, ) hass.async_create_task( diff --git a/homeassistant/components/zodiac/strings.json b/homeassistant/components/zodiac/strings.json index 8cf0e22237e24f..f8ae42d30a84fd 100644 --- a/homeassistant/components/zodiac/strings.json +++ b/homeassistant/components/zodiac/strings.json @@ -28,11 +28,5 @@ } } } - }, - "issues": { - "deprecated_yaml": { - "title": "The Zodiac YAML configuration is being removed", - "description": "Configuring Zodiac using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Zodiac YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } } } diff --git a/tests/components/dynalite/test_config_flow.py b/tests/components/dynalite/test_config_flow.py index d0bd335decc585..f337c7c3e7402f 100644 --- a/tests/components/dynalite/test_config_flow.py +++ b/tests/components/dynalite/test_config_flow.py @@ -6,7 +6,7 @@ from homeassistant import config_entries from homeassistant.components import dynalite from homeassistant.const import CONF_PORT -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers.issue_registry import ( IssueSeverity, async_get as async_get_issue_registry, @@ -52,8 +52,11 @@ async def test_flow( assert result["result"].state == exp_result if exp_reason: assert result["reason"] == exp_reason - issue = registry.async_get_issue(dynalite.DOMAIN, "deprecated_yaml") + issue = registry.async_get_issue( + HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{dynalite.DOMAIN}" + ) assert issue is not None + assert issue.issue_domain == dynalite.DOMAIN assert issue.severity == IssueSeverity.WARNING From b7980ec13562a7f5324617608d70aab0c47762d1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 10 Jul 2023 13:56:43 +0200 Subject: [PATCH 0315/1009] Add entity translations to trafikverket ferry (#96249) --- .../components/trafikverket_ferry/sensor.py | 12 +++++----- .../trafikverket_ferry/strings.json | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/trafikverket_ferry/sensor.py b/homeassistant/components/trafikverket_ferry/sensor.py index b02c673f698fdc..366c193f8fe4fc 100644 --- a/homeassistant/components/trafikverket_ferry/sensor.py +++ b/homeassistant/components/trafikverket_ferry/sensor.py @@ -51,7 +51,7 @@ class TrafikverketSensorEntityDescription( SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="departure_time", - name="Departure time", + translation_key="departure_time", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_time"]), @@ -59,21 +59,21 @@ class TrafikverketSensorEntityDescription( ), TrafikverketSensorEntityDescription( key="departure_from", - name="Departure from", + translation_key="departure_from", icon="mdi:ferry", value_fn=lambda data: cast(str, data["departure_from"]), info_fn=lambda data: cast(list[str], data["departure_information"]), ), TrafikverketSensorEntityDescription( key="departure_to", - name="Departure to", + translation_key="departure_to", icon="mdi:ferry", value_fn=lambda data: cast(str, data["departure_to"]), info_fn=lambda data: cast(list[str], data["departure_information"]), ), TrafikverketSensorEntityDescription( key="departure_modified", - name="Departure modified", + translation_key="departure_modified", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_modified"]), @@ -82,7 +82,7 @@ class TrafikverketSensorEntityDescription( ), TrafikverketSensorEntityDescription( key="departure_time_next", - name="Departure time next", + translation_key="departure_time_next", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_time_next"]), @@ -91,7 +91,7 @@ class TrafikverketSensorEntityDescription( ), TrafikverketSensorEntityDescription( key="departure_time_next_next", - name="Departure time next after", + translation_key="departure_time_next_next", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_time_next_next"]), diff --git a/homeassistant/components/trafikverket_ferry/strings.json b/homeassistant/components/trafikverket_ferry/strings.json index 86ce87c92e4ec3..3d84e4480b406e 100644 --- a/homeassistant/components/trafikverket_ferry/strings.json +++ b/homeassistant/components/trafikverket_ferry/strings.json @@ -39,5 +39,27 @@ "sun": "Sunday" } } + }, + "entity": { + "sensor": { + "departure_time": { + "name": "Departure time" + }, + "departure_from": { + "name": "Departure from" + }, + "departure_to": { + "name": "Departure to" + }, + "departure_modified": { + "name": "Departure modified" + }, + "departure_time_next": { + "name": "Departure time next" + }, + "departure_time_next_next": { + "name": "Departure time next after" + } + } } } From 81dd3a4a93678220d9941fb73b7c69ae56203a53 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 10 Jul 2023 14:00:27 +0200 Subject: [PATCH 0316/1009] Use explicit device name in trafikverket train (#96250) --- homeassistant/components/trafikverket_train/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index 4ea6ff48dc1cb0..c0643858f42108 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -98,6 +98,7 @@ class TrainSensor(SensorEntity): _attr_icon = ICON _attr_device_class = SensorDeviceClass.TIMESTAMP _attr_has_entity_name = True + _attr_name = None def __init__( self, From df229e655bf20bdd569631f5a8cb56cc1e7d7069 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 10 Jul 2023 14:17:37 +0200 Subject: [PATCH 0317/1009] Correct flags for issue registry issue raised by ezviz (#95846) * Correct flags for issue registry issue raised by ezviz * Fix translation strings --- homeassistant/components/ezviz/camera.py | 3 ++- homeassistant/components/ezviz/strings.json | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 60a332446cef36..6150a657c1ad7f 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -313,7 +313,8 @@ def perform_set_alarm_detection_sensibility( DOMAIN, "service_depreciation_detection_sensibility", breaks_in_ha_version="2023.12.0", - is_fixable=False, + is_fixable=True, + is_persistent=True, severity=ir.IssueSeverity.WARNING, translation_key="service_depreciation_detection_sensibility", ) diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index 5711aff2a4a036..92ff8c6fa05ed7 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -62,7 +62,14 @@ "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.12; Please adjust the automation or script that uses the service and select submit below to mark this issue as resolved." + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::ezviz::issues::service_depreciation_detection_sensibility::title%]", + "description": "The Ezviz Detection sensitivity service is deprecated and will be removed in Home Assistant 2023.12.\nTo set the sensitivity, you can instead use the `number.set_value` service targetting the Detection sensitivity entity.\n\nPlease remove the use of this service from your automations and scripts and select **submit** to close this issue." + } + } + } } } } From 39208a3749914f9f5cc8a1febcb4c7c149e9079d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 10 Jul 2023 15:03:40 +0200 Subject: [PATCH 0318/1009] Remove unsupported vacuum service handlers (#95787) * Prevent implementing unsupported vacuum service handlers * Remove unsupported service handlers * Update test --- homeassistant/components/vacuum/__init__.py | 15 --------------- tests/components/demo/test_vacuum.py | 4 ++-- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 0dc4d19ba36331..2399e5d9b3b450 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -429,12 +429,6 @@ async def async_start_pause(self, **kwargs: Any) -> None: """ await self.hass.async_add_executor_job(partial(self.start_pause, **kwargs)) - async def async_pause(self) -> None: - """Not supported.""" - - async def async_start(self) -> None: - """Not supported.""" - @dataclass class StateVacuumEntityDescription(EntityDescription): @@ -482,12 +476,3 @@ async def async_pause(self) -> None: This method must be run in the event loop. """ await self.hass.async_add_executor_job(self.pause) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Not supported.""" - - async def async_turn_off(self, **kwargs: Any) -> None: - """Not supported.""" - - async def async_toggle(self, **kwargs: Any) -> None: - """Not supported.""" diff --git a/tests/components/demo/test_vacuum.py b/tests/components/demo/test_vacuum.py index 4f977436055299..711d0217f2d34a 100644 --- a/tests/components/demo/test_vacuum.py +++ b/tests/components/demo/test_vacuum.py @@ -241,8 +241,8 @@ async def test_unsupported_methods(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert vacuum.is_on(hass, ENTITY_VACUUM_COMPLETE) - await common.async_pause(hass, ENTITY_VACUUM_COMPLETE) - assert vacuum.is_on(hass, ENTITY_VACUUM_COMPLETE) + with pytest.raises(AttributeError): + await common.async_pause(hass, ENTITY_VACUUM_COMPLETE) hass.states.async_set(ENTITY_VACUUM_COMPLETE, STATE_OFF) await hass.async_block_till_done() From 3dcf65bf314f9b065e9d4391398fe9f2b250351b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 10 Jul 2023 15:14:08 +0200 Subject: [PATCH 0319/1009] Add filters to vacuum/services.yaml (#95865) --- homeassistant/components/vacuum/services.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml index 26c8d745b27ea9..c517f1aeaaf913 100644 --- a/homeassistant/components/vacuum/services.yaml +++ b/homeassistant/components/vacuum/services.yaml @@ -6,6 +6,8 @@ turn_on: target: entity: domain: vacuum + supported_features: + - vacuum.VacuumEntityFeature.TURN_ON turn_off: name: Turn off @@ -13,6 +15,8 @@ turn_off: target: entity: domain: vacuum + supported_features: + - vacuum.VacuumEntityFeature.TURN_OFF stop: name: Stop @@ -20,6 +24,8 @@ stop: target: entity: domain: vacuum + supported_features: + - vacuum.VacuumEntityFeature.STOP locate: name: Locate @@ -27,6 +33,8 @@ locate: target: entity: domain: vacuum + supported_features: + - vacuum.VacuumEntityFeature.LOCATE start_pause: name: Start/Pause @@ -34,6 +42,8 @@ start_pause: target: entity: domain: vacuum + supported_features: + - vacuum.VacuumEntityFeature.PAUSE start: name: Start @@ -41,6 +51,8 @@ start: target: entity: domain: vacuum + supported_features: + - vacuum.VacuumEntityFeature.START pause: name: Pause @@ -48,6 +60,8 @@ pause: target: entity: domain: vacuum + supported_features: + - vacuum.VacuumEntityFeature.PAUSE return_to_base: name: Return to base @@ -55,6 +69,8 @@ return_to_base: target: entity: domain: vacuum + supported_features: + - vacuum.VacuumEntityFeature.RETURN_HOME clean_spot: name: Clean spot From 3cc66c8318956cef059595b98a9590022bf4a5cc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 10 Jul 2023 15:14:41 +0200 Subject: [PATCH 0320/1009] Add filters to remote/services.yaml (#95863) --- homeassistant/components/remote/services.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index bdeef15971ec1b..a2b648d9eb31a0 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -11,6 +11,9 @@ turn_on: name: Activity description: Activity ID or Activity Name to start. example: "BedroomTV" + filter: + supported_features: + - remote.RemoteEntityFeature.ACTIVITY selector: text: From 039a3bb6e9d8c8575b7b5e76c6830e8d4d205525 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Jul 2023 03:17:35 -1000 Subject: [PATCH 0321/1009] Only load the device entry when it changes in the base entity (#95801) --- homeassistant/helpers/entity.py | 26 +++++++++++++----------- homeassistant/helpers/entity_platform.py | 2 ++ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index e87eb15b9545ec..55dc69540fdfb4 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -277,6 +277,9 @@ class Entity(ABC): # Entry in the entity registry registry_entry: er.RegistryEntry | None = None + # The device entry for this entity + device_entry: dr.DeviceEntry | None = None + # Hold list for functions to call on remove. _on_remove: list[CALLBACK_TYPE] | None = None @@ -763,13 +766,7 @@ def _friendly_name_internal(self) -> str | None: if name is UNDEFINED: name = None - if not self.has_entity_name or not self.registry_entry: - return name - - device_registry = dr.async_get(self.hass) - if not (device_id := self.registry_entry.device_id) or not ( - device_entry := device_registry.async_get(device_id) - ): + if not self.has_entity_name or not (device_entry := self.device_entry): return name device_name = device_entry.name_by_user or device_entry.name @@ -1116,22 +1113,26 @@ async def _async_registry_updated(self, event: Event) -> None: ent_reg = er.async_get(self.hass) old = self.registry_entry - self.registry_entry = ent_reg.async_get(data["entity_id"]) - assert self.registry_entry is not None + registry_entry = ent_reg.async_get(data["entity_id"]) + assert registry_entry is not None + self.registry_entry = registry_entry + + if device_id := registry_entry.device_id: + self.device_entry = dr.async_get(self.hass).async_get(device_id) - if self.registry_entry.disabled: + if registry_entry.disabled: await self.async_remove() return assert old is not None - if self.registry_entry.entity_id == old.entity_id: + if registry_entry.entity_id == old.entity_id: self.async_registry_entry_updated() self.async_write_ha_state() return await self.async_remove(force_remove=True) - self.entity_id = self.registry_entry.entity_id + self.entity_id = registry_entry.entity_id await self.platform.async_add_entities([self]) @callback @@ -1153,6 +1154,7 @@ def _async_device_registry_updated(self, event: Event) -> None: if "name" not in data["changes"] and "name_by_user" not in data["changes"]: return + self.device_entry = dr.async_get(self.hass).async_get(data["device_id"]) self.async_write_ha_state() @callback diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 55d167ae253c6e..da3c76c73f8650 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -738,6 +738,8 @@ async def _async_add_entity( # noqa: C901 ) entity.registry_entry = entry + if device: + entity.device_entry = device entity.entity_id = entry.entity_id # We won't generate an entity ID if the platform has already set one From 907c667859b4ce1fe22d75cbfbddf53d2c33faa3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 10 Jul 2023 15:40:59 +0200 Subject: [PATCH 0322/1009] Remove unreferenced issues (#96262) --- homeassistant/components/moon/strings.json | 6 ------ homeassistant/components/season/strings.json | 6 ------ homeassistant/components/uptime/strings.json | 6 ------ 3 files changed, 18 deletions(-) diff --git a/homeassistant/components/moon/strings.json b/homeassistant/components/moon/strings.json index 818460bc13d29e..1210fb6403e130 100644 --- a/homeassistant/components/moon/strings.json +++ b/homeassistant/components/moon/strings.json @@ -10,12 +10,6 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, - "issues": { - "removed_yaml": { - "title": "The Moon YAML configuration has been removed", - "description": "Configuring Moon using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } - }, "entity": { "sensor": { "phase": { diff --git a/homeassistant/components/season/strings.json b/homeassistant/components/season/strings.json index d53d6a0890f1da..162daddd41223d 100644 --- a/homeassistant/components/season/strings.json +++ b/homeassistant/components/season/strings.json @@ -12,12 +12,6 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } }, - "issues": { - "removed_yaml": { - "title": "The Season YAML configuration has been removed", - "description": "Configuring Season using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } - }, "entity": { "sensor": { "season": { diff --git a/homeassistant/components/uptime/strings.json b/homeassistant/components/uptime/strings.json index 3d374015acbb15..9ceb91de9bad01 100644 --- a/homeassistant/components/uptime/strings.json +++ b/homeassistant/components/uptime/strings.json @@ -9,11 +9,5 @@ "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } - }, - "issues": { - "removed_yaml": { - "title": "The Uptime YAML configuration has been removed", - "description": "Configuring Uptime using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } } } From 5f6ddedd677eee8f853917b49acd6dc6a3d3f6e3 Mon Sep 17 00:00:00 2001 From: disforw Date: Mon, 10 Jul 2023 09:46:56 -0400 Subject: [PATCH 0323/1009] Change explicit rounding to suggested_display_precision (#95773) --- homeassistant/components/qnap/sensor.py | 67 +++++++++++++------------ 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index c26b72f92956aa..febd4b61ebb77b 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -95,26 +95,31 @@ native_unit_of_measurement=PERCENTAGE, icon="mdi:chip", state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, ), ) _MEMORY_MON_COND: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="memory_free", name="Memory Available", - native_unit_of_measurement=UnitOfInformation.GIBIBYTES, + native_unit_of_measurement=UnitOfInformation.MEBIBYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:memory", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES, ), SensorEntityDescription( key="memory_used", name="Memory Used", - native_unit_of_measurement=UnitOfInformation.GIBIBYTES, + native_unit_of_measurement=UnitOfInformation.MEBIBYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:memory", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES, ), SensorEntityDescription( key="memory_percent_used", @@ -122,6 +127,7 @@ native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, ), ) _NETWORK_MON_COND: tuple[SensorEntityDescription, ...] = ( @@ -133,20 +139,24 @@ SensorEntityDescription( key="network_tx", name="Network Up", - native_unit_of_measurement=UnitOfDataRate.MEBIBYTES_PER_SECOND, + native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, icon="mdi:upload", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, ), SensorEntityDescription( key="network_rx", name="Network Down", - native_unit_of_measurement=UnitOfDataRate.MEBIBYTES_PER_SECOND, + native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, icon="mdi:download", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, ), ) _DRIVE_MON_COND: tuple[SensorEntityDescription, ...] = ( @@ -170,20 +180,24 @@ SensorEntityDescription( key="volume_size_used", name="Used Space", - native_unit_of_measurement=UnitOfInformation.GIBIBYTES, + native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:chart-pie", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES, ), SensorEntityDescription( key="volume_size_free", name="Free Space", - native_unit_of_measurement=UnitOfInformation.GIBIBYTES, + native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:chart-pie", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES, ), SensorEntityDescription( key="volume_percentage_used", @@ -191,6 +205,7 @@ native_unit_of_measurement=PERCENTAGE, icon="mdi:chart-pie", state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, ), ) @@ -316,16 +331,6 @@ async def async_setup_entry( async_add_entities(sensors) -def round_nicely(number): - """Round a number based on its size (so it looks nice).""" - if number < 10: - return round(number, 2) - if number < 100: - return round(number, 1) - - return round(number) - - class QNAPSensor(CoordinatorEntity[QnapCoordinator], SensorEntity): """Base class for a QNAP sensor.""" @@ -378,25 +383,25 @@ class QNAPMemorySensor(QNAPSensor): @property def native_value(self): """Return the state of the sensor.""" - free = float(self.coordinator.data["system_stats"]["memory"]["free"]) / 1024 + free = float(self.coordinator.data["system_stats"]["memory"]["free"]) if self.entity_description.key == "memory_free": - return round_nicely(free) + return free - total = float(self.coordinator.data["system_stats"]["memory"]["total"]) / 1024 + total = float(self.coordinator.data["system_stats"]["memory"]["total"]) used = total - free if self.entity_description.key == "memory_used": - return round_nicely(used) + return used if self.entity_description.key == "memory_percent_used": - return round(used / total * 100) + return used / total * 100 @property def extra_state_attributes(self): """Return the state attributes.""" if self.coordinator.data: data = self.coordinator.data["system_stats"]["memory"] - size = round_nicely(float(data["total"]) / 1024) + size = round(float(data["total"]) / 1024, 2) return {ATTR_MEMORY_SIZE: f"{size} {UnitOfInformation.GIBIBYTES}"} @@ -412,10 +417,10 @@ def native_value(self): data = self.coordinator.data["bandwidth"][self.monitor_device] if self.entity_description.key == "network_tx": - return round_nicely(data["tx"] / 1024 / 1024) + return data["tx"] if self.entity_description.key == "network_rx": - return round_nicely(data["rx"] / 1024 / 1024) + return data["rx"] @property def extra_state_attributes(self): @@ -427,8 +432,6 @@ def extra_state_attributes(self): ATTR_MASK: data["mask"], ATTR_MAC: data["mac"], ATTR_MAX_SPEED: data["max_speed"], - ATTR_PACKETS_TX: data["tx_packets"], - ATTR_PACKETS_RX: data["rx_packets"], ATTR_PACKETS_ERR: data["err_packets"], } @@ -507,18 +510,18 @@ def native_value(self): """Return the state of the sensor.""" data = self.coordinator.data["volumes"][self.monitor_device] - free_gb = int(data["free_size"]) / 1024 / 1024 / 1024 + free_gb = int(data["free_size"]) if self.entity_description.key == "volume_size_free": - return round_nicely(free_gb) + return free_gb - total_gb = int(data["total_size"]) / 1024 / 1024 / 1024 + total_gb = int(data["total_size"]) used_gb = total_gb - free_gb if self.entity_description.key == "volume_size_used": - return round_nicely(used_gb) + return used_gb if self.entity_description.key == "volume_percentage_used": - return round(used_gb / total_gb * 100) + return used_gb / total_gb * 100 @property def extra_state_attributes(self): @@ -528,5 +531,5 @@ def extra_state_attributes(self): total_gb = int(data["total_size"]) / 1024 / 1024 / 1024 return { - ATTR_VOLUME_SIZE: f"{round_nicely(total_gb)} {UnitOfInformation.GIBIBYTES}" + ATTR_VOLUME_SIZE: f"{round(total_gb, 1)} {UnitOfInformation.GIBIBYTES}" } From a3681774d67aaa3a5eec787217df5dfd67fe66c6 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 10 Jul 2023 15:49:08 +0200 Subject: [PATCH 0324/1009] Use snapshots in devolo Home Network sensor tests (#95104) Use snapshots Co-authored-by: G Johansson --- .../snapshots/test_sensor.ambr | 133 ++++++++++++++++ .../devolo_home_network/test_sensor.py | 148 +++++------------- 2 files changed, 173 insertions(+), 108 deletions(-) create mode 100644 tests/components/devolo_home_network/snapshots/test_sensor.ambr diff --git a/tests/components/devolo_home_network/snapshots/test_sensor.ambr b/tests/components/devolo_home_network/snapshots/test_sensor.ambr new file mode 100644 index 00000000000000..241313965c4d9f --- /dev/null +++ b/tests/components/devolo_home_network/snapshots/test_sensor.ambr @@ -0,0 +1,133 @@ +# serializer version: 1 +# name: test_sensor[connected_plc_devices-async_get_network_overview-interval2] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Connected PLC devices', + 'icon': 'mdi:lan', + }), + 'context': , + 'entity_id': 'sensor.mock_title_connected_plc_devices', + 'last_changed': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_sensor[connected_plc_devices-async_get_network_overview-interval2].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_title_connected_plc_devices', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:lan', + 'original_name': 'Connected PLC devices', + 'platform': 'devolo_home_network', + 'supported_features': 0, + 'translation_key': 'connected_plc_devices', + 'unique_id': '1234567890_connected_plc_devices', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[connected_wifi_clients-async_get_wifi_connected_station-interval0] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Connected Wifi clients', + 'icon': 'mdi:wifi', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.mock_title_connected_wifi_clients', + 'last_changed': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_sensor[connected_wifi_clients-async_get_wifi_connected_station-interval0].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_title_connected_wifi_clients', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:wifi', + 'original_name': 'Connected Wifi clients', + 'platform': 'devolo_home_network', + 'supported_features': 0, + 'translation_key': 'connected_wifi_clients', + 'unique_id': '1234567890_connected_wifi_clients', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[neighboring_wifi_networks-async_get_wifi_neighbor_access_points-interval1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Neighboring Wifi networks', + 'icon': 'mdi:wifi-marker', + }), + 'context': , + 'entity_id': 'sensor.mock_title_neighboring_wifi_networks', + 'last_changed': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_sensor[neighboring_wifi_networks-async_get_wifi_neighbor_access_points-interval1].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_title_neighboring_wifi_networks', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:wifi-marker', + 'original_name': 'Neighboring Wifi networks', + 'platform': 'devolo_home_network', + 'supported_features': 0, + 'translation_key': 'neighboring_wifi_networks', + 'unique_id': '1234567890_neighboring_wifi_networks', + 'unit_of_measurement': None, + }) +# --- diff --git a/tests/components/devolo_home_network/test_sensor.py b/tests/components/devolo_home_network/test_sensor.py index 0511544224ad1b..dc7842e5fbd38e 100644 --- a/tests/components/devolo_home_network/test_sensor.py +++ b/tests/components/devolo_home_network/test_sensor.py @@ -1,15 +1,17 @@ """Tests for the devolo Home Network sensors.""" +from datetime import timedelta from unittest.mock import AsyncMock from devolo_plc_api.exceptions.device import DeviceUnavailable import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components.devolo_home_network.const import ( LONG_UPDATE_INTERVAL, SHORT_UPDATE_INTERVAL, ) -from homeassistant.components.sensor import DOMAIN, SensorStateClass -from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE, EntityCategory +from homeassistant.components.sensor import DOMAIN +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util @@ -35,120 +37,50 @@ async def test_sensor_setup(hass: HomeAssistant) -> None: await hass.config_entries.async_unload(entry.entry_id) -async def test_update_connected_wifi_clients( - hass: HomeAssistant, mock_device: MockDevice -) -> None: - """Test state change of a connected_wifi_clients sensor device.""" - entry = configure_integration(hass) - device_name = entry.title.replace(" ", "_").lower() - state_key = f"{DOMAIN}.{device_name}_connected_wifi_clients" - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state is not None - assert state.state == "1" - assert ( - state.attributes[ATTR_FRIENDLY_NAME] == f"{entry.title} Connected Wifi clients" - ) - assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT - - # Emulate device failure - mock_device.device.async_get_wifi_connected_station = AsyncMock( - side_effect=DeviceUnavailable - ) - async_fire_time_changed(hass, dt_util.utcnow() + SHORT_UPDATE_INTERVAL) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_UNAVAILABLE - - # Emulate state change - mock_device.reset() - async_fire_time_changed(hass, dt_util.utcnow() + SHORT_UPDATE_INTERVAL) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state is not None - assert state.state == "1" - - await hass.config_entries.async_unload(entry.entry_id) - - -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_update_neighboring_wifi_networks( - hass: HomeAssistant, mock_device: MockDevice, entity_registry: er.EntityRegistry -) -> None: - """Test state change of a neighboring_wifi_networks sensor device.""" - entry = configure_integration(hass) - device_name = entry.title.replace(" ", "_").lower() - state_key = f"{DOMAIN}.{device_name}_neighboring_wifi_networks" - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state is not None - assert state.state == "1" - assert ( - state.attributes[ATTR_FRIENDLY_NAME] - == f"{entry.title} Neighboring Wifi networks" - ) - assert ( - entity_registry.async_get(state_key).entity_category - is EntityCategory.DIAGNOSTIC - ) - - # Emulate device failure - mock_device.device.async_get_wifi_neighbor_access_points = AsyncMock( - side_effect=DeviceUnavailable - ) - async_fire_time_changed(hass, dt_util.utcnow() + LONG_UPDATE_INTERVAL) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_UNAVAILABLE - - # Emulate state change - mock_device.reset() - async_fire_time_changed(hass, dt_util.utcnow() + LONG_UPDATE_INTERVAL) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state is not None - assert state.state == "1" - - await hass.config_entries.async_unload(entry.entry_id) - - +@pytest.mark.parametrize( + ("name", "get_method", "interval"), + [ + [ + "connected_wifi_clients", + "async_get_wifi_connected_station", + SHORT_UPDATE_INTERVAL, + ], + [ + "neighboring_wifi_networks", + "async_get_wifi_neighbor_access_points", + LONG_UPDATE_INTERVAL, + ], + [ + "connected_plc_devices", + "async_get_network_overview", + LONG_UPDATE_INTERVAL, + ], + ], +) @pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_update_connected_plc_devices( - hass: HomeAssistant, mock_device: MockDevice, entity_registry: er.EntityRegistry +async def test_sensor( + hass: HomeAssistant, + mock_device: MockDevice, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + name: str, + get_method: str, + interval: timedelta, ) -> None: - """Test state change of a connected_plc_devices sensor device.""" + """Test state change of a sensor device.""" entry = configure_integration(hass) device_name = entry.title.replace(" ", "_").lower() - state_key = f"{DOMAIN}.{device_name}_connected_plc_devices" + state_key = f"{DOMAIN}.{device_name}_{name}" await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get(state_key) - assert state is not None - assert state.state == "1" - assert ( - state.attributes[ATTR_FRIENDLY_NAME] == f"{entry.title} Connected PLC devices" - ) - assert ( - entity_registry.async_get(state_key).entity_category - is EntityCategory.DIAGNOSTIC - ) + assert hass.states.get(state_key) == snapshot + assert entity_registry.async_get(state_key) == snapshot # Emulate device failure - mock_device.plcnet.async_get_network_overview = AsyncMock( - side_effect=DeviceUnavailable - ) - async_fire_time_changed(hass, dt_util.utcnow() + LONG_UPDATE_INTERVAL) + setattr(mock_device.device, get_method, AsyncMock(side_effect=DeviceUnavailable)) + setattr(mock_device.plcnet, get_method, AsyncMock(side_effect=DeviceUnavailable)) + async_fire_time_changed(hass, dt_util.utcnow() + interval) await hass.async_block_till_done() state = hass.states.get(state_key) @@ -157,7 +89,7 @@ async def test_update_connected_plc_devices( # Emulate state change mock_device.reset() - async_fire_time_changed(hass, dt_util.utcnow() + LONG_UPDATE_INTERVAL) + async_fire_time_changed(hass, dt_util.utcnow() + interval) await hass.async_block_till_done() state = hass.states.get(state_key) From af22a90b3a344a5566cd17347ae39b1d639c1320 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 10 Jul 2023 15:49:25 +0200 Subject: [PATCH 0325/1009] Make Zodiac integration title translatable (#95816) --- homeassistant/components/zodiac/strings.json | 1 + homeassistant/generated/integrations.json | 4 ++-- script/hassfest/translations.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zodiac/strings.json b/homeassistant/components/zodiac/strings.json index f8ae42d30a84fd..b4eacef743548e 100644 --- a/homeassistant/components/zodiac/strings.json +++ b/homeassistant/components/zodiac/strings.json @@ -1,4 +1,5 @@ { + "title": "Zodiac", "config": { "step": { "user": { diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index c7f842748c20af..d1f68271c55169 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6541,7 +6541,6 @@ "iot_class": "local_polling" }, "zodiac": { - "name": "Zodiac", "integration_type": "hub", "config_flow": true, "iot_class": "calculated" @@ -6695,6 +6694,7 @@ "uptime", "utility_meter", "waze_travel_time", - "workday" + "workday", + "zodiac" ] } diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 56609e57fd978f..e53b311b43e44b 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -40,6 +40,7 @@ "nmap_tracker", "rpi_power", "waze_travel_time", + "zodiac", } REMOVED_TITLE_MSG = ( From eee85666941439254567129d3ac57b74672a9ac4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Jul 2023 09:56:06 -0400 Subject: [PATCH 0326/1009] Differentiate between device info types (#95641) * Differentiate between device info types * Update allowed fields * Update homeassistant/helpers/entity_platform.py Co-authored-by: Martin Hjelmare * Split up message in 2 lines * Use dict for device info types * Extract device info function and test error checking * Simplify parsing device info * move checks around * Simplify more * Move error checking around * Fix order * fallback config entry title to domain * Remove fallback for name to config entry domain * Ensure mocked configuration URLs are strings * one more test case * Apply suggestions from code review Co-authored-by: Erik Montnemery --------- Co-authored-by: Martin Hjelmare Co-authored-by: Erik Montnemery --- homeassistant/helpers/entity_platform.py | 165 +++++++++++++---------- tests/components/fritzbox/conftest.py | 1 + tests/components/hyperion/__init__.py | 1 + tests/components/purpleair/conftest.py | 1 + tests/helpers/test_entity_platform.py | 130 +++++++++--------- 5 files changed, 165 insertions(+), 133 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index da3c76c73f8650..f97e509f486943 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -30,7 +30,6 @@ from homeassistant.exceptions import ( HomeAssistantError, PlatformNotReady, - RequiredParameterMissing, ) from homeassistant.generated import languages from homeassistant.setup import async_start_setup @@ -43,14 +42,13 @@ service, translation, ) -from .device_registry import DeviceRegistry from .entity_registry import EntityRegistry, RegistryEntryDisabler, RegistryEntryHider from .event import async_call_later, async_track_time_interval from .issue_registry import IssueSeverity, async_create_issue from .typing import UNDEFINED, ConfigType, DiscoveryInfoType if TYPE_CHECKING: - from .entity import Entity + from .entity import DeviceInfo, Entity SLOW_SETUP_WARNING = 10 @@ -62,6 +60,37 @@ DATA_ENTITY_PLATFORM = "entity_platform" PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds +DEVICE_INFO_TYPES = { + # Device info is categorized by finding the first device info type which has all + # the keys of the device info. The link device info type must be kept first + # to make it preferred over primary. + "link": { + "connections", + "identifiers", + }, + "primary": { + "configuration_url", + "connections", + "entry_type", + "hw_version", + "identifiers", + "manufacturer", + "model", + "name", + "suggested_area", + "sw_version", + "via_device", + }, + "secondary": { + "connections", + "default_manufacturer", + "default_model", + "default_name", + # Used by Fritz + "via_device", + }, +} + _LOGGER = getLogger(__name__) @@ -497,12 +526,9 @@ async def async_add_entities( hass = self.hass - device_registry = dev_reg.async_get(hass) entity_registry = ent_reg.async_get(hass) tasks = [ - self._async_add_entity( - entity, update_before_add, entity_registry, device_registry - ) + self._async_add_entity(entity, update_before_add, entity_registry) for entity in new_entities ] @@ -564,7 +590,6 @@ async def _async_add_entity( # noqa: C901 entity: Entity, update_before_add: bool, entity_registry: EntityRegistry, - device_registry: DeviceRegistry, ) -> None: """Add an entity to the platform.""" if entity is None: @@ -620,68 +645,10 @@ async def _async_add_entity( # noqa: C901 entity.add_to_platform_abort() return - device_info = entity.device_info - device_id = None - device = None - - if self.config_entry and device_info is not None: - processed_dev_info: dict[str, str | None] = {} - for key in ( - "connections", - "default_manufacturer", - "default_model", - "default_name", - "entry_type", - "identifiers", - "manufacturer", - "model", - "name", - "suggested_area", - "sw_version", - "hw_version", - "via_device", - ): - if key in device_info: - processed_dev_info[key] = device_info[ - key # type: ignore[literal-required] - ] - - if ( - # device info that is purely meant for linking doesn't need default name - any( - key not in {"identifiers", "connections"} - for key in (processed_dev_info) - ) - and "default_name" not in processed_dev_info - and not processed_dev_info.get("name") - ): - processed_dev_info["name"] = self.config_entry.title - - if "configuration_url" in device_info: - if device_info["configuration_url"] is None: - processed_dev_info["configuration_url"] = None - else: - configuration_url = str(device_info["configuration_url"]) - if urlparse(configuration_url).scheme in [ - "http", - "https", - "homeassistant", - ]: - processed_dev_info["configuration_url"] = configuration_url - else: - _LOGGER.warning( - "Ignoring invalid device configuration_url '%s'", - configuration_url, - ) - - try: - device = device_registry.async_get_or_create( - config_entry_id=self.config_entry.entry_id, - **processed_dev_info, # type: ignore[arg-type] - ) - device_id = device.id - except RequiredParameterMissing: - pass + if self.config_entry and (device_info := entity.device_info): + device = self._async_process_device_info(device_info) + else: + device = None # An entity may suggest the entity_id by setting entity_id itself suggested_entity_id: str | None = entity.entity_id @@ -716,7 +683,7 @@ async def _async_add_entity( # noqa: C901 entity.unique_id, capabilities=entity.capability_attributes, config_entry=self.config_entry, - device_id=device_id, + device_id=device.id if device else None, disabled_by=disabled_by, entity_category=entity.entity_category, get_initial_options=entity.get_initial_entity_options, @@ -806,6 +773,62 @@ def remove_entity_cb() -> None: await entity.add_to_platform_finish() + @callback + def _async_process_device_info( + self, device_info: DeviceInfo + ) -> dev_reg.DeviceEntry | None: + """Process a device info.""" + keys = set(device_info) + + # If no keys or not enough info to match up, abort + if len(keys & {"connections", "identifiers"}) == 0: + self.logger.error( + "Ignoring device info without identifiers or connections: %s", + device_info, + ) + return None + + device_info_type: str | None = None + + # Find the first device info type which has all keys in the device info + for possible_type, allowed_keys in DEVICE_INFO_TYPES.items(): + if keys <= allowed_keys: + device_info_type = possible_type + break + + if device_info_type is None: + self.logger.error( + "Device info for %s needs to either describe a device, " + "link to existing device or provide extra information.", + device_info, + ) + return None + + if (config_url := device_info.get("configuration_url")) is not None: + if type(config_url) is not str or urlparse(config_url).scheme not in [ + "http", + "https", + "homeassistant", + ]: + self.logger.error( + "Ignoring device info with invalid configuration_url '%s'", + config_url, + ) + return None + + assert self.config_entry is not None + + if device_info_type == "primary" and not device_info.get("name"): + device_info = { + **device_info, # type: ignore[misc] + "name": self.config_entry.title, + } + + return dev_reg.async_get(self.hass).async_get_or_create( + config_entry_id=self.config_entry.entry_id, + **device_info, + ) + async def async_reset(self) -> None: """Remove all entities and reset data. diff --git a/tests/components/fritzbox/conftest.py b/tests/components/fritzbox/conftest.py index 50fca4581b35f7..1fbaf48de4bfc6 100644 --- a/tests/components/fritzbox/conftest.py +++ b/tests/components/fritzbox/conftest.py @@ -10,4 +10,5 @@ def fritz_fixture() -> Mock: with patch("homeassistant.components.fritzbox.Fritzhome") as fritz, patch( "homeassistant.components.fritzbox.config_flow.Fritzhome" ): + fritz.return_value.get_prefixed_host.return_value = "http://1.2.3.4" yield fritz diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py index 382a168bc4434e..3714e58479b81c 100644 --- a/tests/components/hyperion/__init__.py +++ b/tests/components/hyperion/__init__.py @@ -114,6 +114,7 @@ def create_mock_client() -> Mock: mock_client.instances = [ {"friendly_name": "Test instance 1", "instance": 0, "running": True} ] + mock_client.remote_url = f"http://{TEST_HOST}:{TEST_PORT_UI}" return mock_client diff --git a/tests/components/purpleair/conftest.py b/tests/components/purpleair/conftest.py index ef48a5988a37fc..4883f79b349e07 100644 --- a/tests/components/purpleair/conftest.py +++ b/tests/components/purpleair/conftest.py @@ -19,6 +19,7 @@ def api_fixture(get_sensors_response): """Define a fixture to return a mocked aiopurple API object.""" return Mock( async_check_api_key=AsyncMock(), + get_map_url=Mock(return_value="http://example.com"), sensors=Mock( async_get_nearby_sensors=AsyncMock( return_value=[ diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 711c333c5ff4e9..9673e1dc73a7fb 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1169,57 +1169,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): assert device2.model == "test-model" -async def test_device_info_invalid_url( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """Test device info is forwarded correctly.""" - registry = dr.async_get(hass) - registry.async_get_or_create( - config_entry_id="123", - connections=set(), - identifiers={("hue", "via-id")}, - manufacturer="manufacturer", - model="via", - ) - - async def async_setup_entry(hass, config_entry, async_add_entities): - """Mock setup entry method.""" - async_add_entities( - [ - # Valid device info, but invalid url - MockEntity( - unique_id="qwer", - device_info={ - "identifiers": {("hue", "1234")}, - "configuration_url": "foo://192.168.0.100/config", - }, - ), - ] - ) - return True - - platform = MockPlatform(async_setup_entry=async_setup_entry) - config_entry = MockConfigEntry(entry_id="super-mock-id") - entity_platform = MockEntityPlatform( - hass, platform_name=config_entry.domain, platform=platform - ) - - assert await entity_platform.async_setup_entry(config_entry) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids()) == 1 - - device = registry.async_get_device({("hue", "1234")}) - assert device is not None - assert device.identifiers == {("hue", "1234")} - assert device.configuration_url is None - - assert ( - "Ignoring invalid device configuration_url 'foo://192.168.0.100/config'" - in caplog.text - ) - - async def test_device_info_homeassistant_url( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: @@ -1838,28 +1787,35 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @pytest.mark.parametrize( - ("entity_device_name", "entity_device_default_name", "expected_device_name"), + ( + "config_entry_title", + "entity_device_name", + "entity_device_default_name", + "expected_device_name", + ), [ - (None, None, "Mock Config Entry Title"), - ("", None, "Mock Config Entry Title"), - (None, "Hello", "Hello"), - ("Mock Device Name", None, "Mock Device Name"), + ("Mock Config Entry Title", None, None, "Mock Config Entry Title"), + ("Mock Config Entry Title", "", None, "Mock Config Entry Title"), + ("Mock Config Entry Title", None, "Hello", "Hello"), + ("Mock Config Entry Title", "Mock Device Name", None, "Mock Device Name"), ], ) async def test_device_name_defaulting_config_entry( hass: HomeAssistant, + config_entry_title: str, entity_device_name: str, entity_device_default_name: str, expected_device_name: str, ) -> None: """Test setting the device name based on input info.""" device_info = { - "identifiers": {("hue", "1234")}, - "name": entity_device_name, + "connections": {(dr.CONNECTION_NETWORK_MAC, "1234")}, } if entity_device_default_name: device_info["default_name"] = entity_device_default_name + else: + device_info["name"] = entity_device_name class DeviceNameEntity(Entity): _attr_unique_id = "qwer" @@ -1871,9 +1827,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): return True platform = MockPlatform(async_setup_entry=async_setup_entry) - config_entry = MockConfigEntry( - title="Mock Config Entry Title", entry_id="super-mock-id" - ) + config_entry = MockConfigEntry(title=config_entry_title, entry_id="super-mock-id") entity_platform = MockEntityPlatform( hass, platform_name=config_entry.domain, platform=platform ) @@ -1882,6 +1836,58 @@ async def async_setup_entry(hass, config_entry, async_add_entities): await hass.async_block_till_done() dev_reg = dr.async_get(hass) - device = dev_reg.async_get_device({("hue", "1234")}) + device = dev_reg.async_get_device(set(), {(dr.CONNECTION_NETWORK_MAC, "1234")}) assert device is not None assert device.name == expected_device_name + + +@pytest.mark.parametrize( + ("device_info"), + [ + # No identifiers + {}, + {"name": "bla"}, + {"default_name": "bla"}, + # Match multiple types + { + "identifiers": {("hue", "1234")}, + "name": "bla", + "default_name": "yo", + }, + # Invalid configuration URL + { + "identifiers": {("hue", "1234")}, + "configuration_url": "foo://192.168.0.100/config", + }, + ], +) +async def test_device_type_error_checking( + hass: HomeAssistant, + device_info: dict, +) -> None: + """Test catching invalid device info.""" + + class DeviceNameEntity(Entity): + _attr_unique_id = "qwer" + _attr_device_info = device_info + + async def async_setup_entry(hass, config_entry, async_add_entities): + """Mock setup entry method.""" + async_add_entities([DeviceNameEntity()]) + return True + + platform = MockPlatform(async_setup_entry=async_setup_entry) + config_entry = MockConfigEntry( + title="Mock Config Entry Title", entry_id="super-mock-id" + ) + entity_platform = MockEntityPlatform( + hass, platform_name=config_entry.domain, platform=platform + ) + + assert await entity_platform.async_setup_entry(config_entry) + + dev_reg = dr.async_get(hass) + assert len(dev_reg.devices) == 0 + # Entity should still be registered + ent_reg = er.async_get(hass) + assert ent_reg.async_get("test_domain.test_qwer") is not None From cf999d9ba4b00337eb25db10879da18b8469b157 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 10 Jul 2023 17:19:26 +0200 Subject: [PATCH 0327/1009] Bump fritzconection to 1.12.2 (#96265) --- homeassistant/components/fritz/manifest.json | 2 +- homeassistant/components/fritzbox_callmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index 54419d5ae3fddf..8d52115d49b3dd 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/fritz", "iot_class": "local_polling", "loggers": ["fritzconnection"], - "requirements": ["fritzconnection[qr]==1.12.0", "xmltodict==0.13.0"], + "requirements": ["fritzconnection[qr]==1.12.2", "xmltodict==0.13.0"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index d445c12e4dab4f..c3c305ab07ece8 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["fritzconnection"], - "requirements": ["fritzconnection[qr]==1.12.0"] + "requirements": ["fritzconnection[qr]==1.12.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 415685b2f5ae2c..567b8a96d5c340 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -817,7 +817,7 @@ freesms==0.2.0 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -fritzconnection[qr]==1.12.0 +fritzconnection[qr]==1.12.2 # homeassistant.components.google_translate gTTS==2.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6dc2fdacfe26f1..0e6a806518e4f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -636,7 +636,7 @@ freebox-api==1.1.0 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -fritzconnection[qr]==1.12.0 +fritzconnection[qr]==1.12.2 # homeassistant.components.google_translate gTTS==2.2.4 From b8369a58315632d293caeae068392873cfe97ada Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 10 Jul 2023 19:42:13 +0200 Subject: [PATCH 0328/1009] Add entity translations to trafikverket weatherstation (#96251) --- .../trafikverket_weatherstation/sensor.py | 20 ++++------ .../trafikverket_weatherstation/strings.json | 38 ++++++++++++++----- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 8523ded1fffc3f..f34eae3cf1fcb8 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -87,32 +87,33 @@ class TrafikverketSensorEntityDescription( SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="air_temp", + translation_key="air_temperature", api_key="air_temp", - name="Air temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TrafikverketSensorEntityDescription( key="road_temp", + translation_key="road_temperature", api_key="road_temp", - name="Road temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TrafikverketSensorEntityDescription( key="precipitation", + translation_key="precipitation", api_key="precipitationtype_translated", name="Precipitation type", icon="mdi:weather-snowy-rainy", entity_registry_enabled_default=False, - translation_key="precipitation", options=PRECIPITATION_TYPE, device_class=SensorDeviceClass.ENUM, ), TrafikverketSensorEntityDescription( key="wind_direction", + translation_key="wind_direction", api_key="winddirection", name="Wind direction", native_unit_of_measurement=DEGREE, @@ -121,25 +122,24 @@ class TrafikverketSensorEntityDescription( ), TrafikverketSensorEntityDescription( key="wind_direction_text", + translation_key="wind_direction_text", api_key="winddirectiontext_translated", name="Wind direction text", icon="mdi:flag-triangle", - translation_key="wind_direction_text", options=WIND_DIRECTIONS, device_class=SensorDeviceClass.ENUM, ), TrafikverketSensorEntityDescription( key="wind_speed", api_key="windforce", - name="Wind speed", native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, device_class=SensorDeviceClass.WIND_SPEED, state_class=SensorStateClass.MEASUREMENT, ), TrafikverketSensorEntityDescription( key="wind_speed_max", + translation_key="wind_speed_max", api_key="windforcemax", - name="Wind speed max", native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, device_class=SensorDeviceClass.WIND_SPEED, icon="mdi:weather-windy-variant", @@ -149,9 +149,7 @@ class TrafikverketSensorEntityDescription( TrafikverketSensorEntityDescription( key="humidity", api_key="humidity", - name="Humidity", native_unit_of_measurement=PERCENTAGE, - icon="mdi:water-percent", device_class=SensorDeviceClass.HUMIDITY, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, @@ -159,25 +157,23 @@ class TrafikverketSensorEntityDescription( TrafikverketSensorEntityDescription( key="precipitation_amount", api_key="precipitation_amount", - name="Precipitation amount", native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, state_class=SensorStateClass.MEASUREMENT, ), TrafikverketSensorEntityDescription( key="precipitation_amountname", + translation_key="precipitation_amountname", api_key="precipitation_amountname_translated", - name="Precipitation name", icon="mdi:weather-pouring", entity_registry_enabled_default=False, - translation_key="precipitation_amountname", options=PRECIPITATION_AMOUNTNAME, device_class=SensorDeviceClass.ENUM, ), TrafikverketSensorEntityDescription( key="measure_time", + translation_key="measure_time", api_key="measure_time", - name="Measure Time", icon="mdi:clock", entity_registry_enabled_default=False, device_class=SensorDeviceClass.TIMESTAMP, diff --git a/homeassistant/components/trafikverket_weatherstation/strings.json b/homeassistant/components/trafikverket_weatherstation/strings.json index 3680fae6d8c626..9ff1b077f33b92 100644 --- a/homeassistant/components/trafikverket_weatherstation/strings.json +++ b/homeassistant/components/trafikverket_weatherstation/strings.json @@ -20,7 +20,29 @@ }, "entity": { "sensor": { + "air_temperature": { + "name": "Air temperature" + }, + "road_temperature": { + "name": "Road temperature" + }, + "precipitation": { + "name": "Precipitation type", + "state": { + "drizzle": "Drizzle", + "hail": "Hail", + "none": "None", + "rain": "Rain", + "snow": "Snow", + "rain_snow_mixed": "Rain and snow mixed", + "freezing_rain": "Freezing rain" + } + }, + "wind_direction": { + "name": "Wind direction" + }, "wind_direction_text": { + "name": "Wind direction text", "state": { "east": "East", "north_east": "North east", @@ -36,7 +58,11 @@ "west": "West" } }, + "wind_speed_max": { + "name": "Wind speed max" + }, "precipitation_amountname": { + "name": "Precipitation name", "state": { "error": "Error", "mild_rain": "Mild rain", @@ -53,16 +79,8 @@ "unknown": "Unknown" } }, - "precipitation": { - "state": { - "drizzle": "Drizzle", - "hail": "Hail", - "none": "None", - "rain": "Rain", - "snow": "Snow", - "rain_snow_mixed": "Rain and snow mixed", - "freezing_rain": "Freezing rain" - } + "measure_time": { + "name": "Measure time" } } } From 7f666849c2bde7ac7a040a9708290b2956785531 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 10 Jul 2023 20:20:36 +0200 Subject: [PATCH 0329/1009] Add filters to siren/services.yaml (#95864) --- homeassistant/components/siren/services.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/siren/services.yaml b/homeassistant/components/siren/services.yaml index 209dece71ab8d4..154ffff78a3a12 100644 --- a/homeassistant/components/siren/services.yaml +++ b/homeassistant/components/siren/services.yaml @@ -6,16 +6,24 @@ turn_on: target: entity: domain: siren + supported_features: + - siren.SirenEntityFeature.TURN_ON fields: tone: description: The tone to emit when turning the siren on. When `available_tones` property is a map, either the key or the value can be used. Must be supported by the integration. example: fire + filter: + supported_features: + - siren.SirenEntityFeature.TONES required: false selector: text: volume_level: description: The volume level of the noise to emit when turning the siren on. Must be supported by the integration. example: 0.5 + filter: + supported_features: + - siren.SirenEntityFeature.VOLUME_SET required: false selector: number: @@ -25,6 +33,9 @@ turn_on: duration: description: The duration in seconds of the noise to emit when turning the siren on. Must be supported by the integration. example: 15 + filter: + supported_features: + - siren.SirenEntityFeature.DURATION required: false selector: text: @@ -35,6 +46,8 @@ turn_off: target: entity: domain: siren + supported_features: + - siren.SirenEntityFeature.TURN_OFF toggle: name: Toggle @@ -42,3 +55,6 @@ toggle: target: entity: domain: siren + supported_features: + - - siren.SirenEntityFeature.TURN_OFF + - siren.SirenEntityFeature.TURN_ON From 22357701f0ccc630b1e74fcd62bcb5cb49263409 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 10 Jul 2023 20:21:28 +0200 Subject: [PATCH 0330/1009] Add filters to media_player/services.yaml (#95862) --- .../components/media_player/services.yaml | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 21807262742248..97605886036a27 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -6,6 +6,8 @@ turn_on: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.TURN_ON turn_off: name: Turn off @@ -13,6 +15,8 @@ turn_off: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.TURN_OFF toggle: name: Toggle @@ -20,6 +24,9 @@ toggle: target: entity: domain: media_player + supported_features: + - - media_player.MediaPlayerEntityFeature.TURN_OFF + - media_player.MediaPlayerEntityFeature.TURN_ON volume_up: name: Turn up volume @@ -27,6 +34,9 @@ volume_up: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.VOLUME_SET + - media_player.MediaPlayerEntityFeature.VOLUME_STEP volume_down: name: Turn down volume @@ -34,6 +44,9 @@ volume_down: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.VOLUME_SET + - media_player.MediaPlayerEntityFeature.VOLUME_STEP volume_mute: name: Mute volume @@ -41,6 +54,8 @@ volume_mute: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.VOLUME_MUTE fields: is_volume_muted: name: Muted @@ -55,6 +70,8 @@ volume_set: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.VOLUME_SET fields: volume_level: name: Level @@ -72,6 +89,9 @@ media_play_pause: target: entity: domain: media_player + supported_features: + - - media_player.MediaPlayerEntityFeature.PAUSE + - media_player.MediaPlayerEntityFeature.PLAY media_play: name: Play @@ -79,6 +99,8 @@ media_play: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.PLAY media_pause: name: Pause @@ -86,6 +108,8 @@ media_pause: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.PAUSE media_stop: name: Stop @@ -93,6 +117,8 @@ media_stop: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.STOP media_next_track: name: Next @@ -100,6 +126,8 @@ media_next_track: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.NEXT_TRACK media_previous_track: name: Previous @@ -107,6 +135,8 @@ media_previous_track: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.PREVIOUS_TRACK media_seek: name: Seek @@ -114,6 +144,8 @@ media_seek: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.SEEK fields: seek_position: name: Position @@ -132,6 +164,8 @@ play_media: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.PLAY_MEDIA fields: media_content_id: name: Content ID @@ -186,6 +220,8 @@ select_source: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.SELECT_SOURCE fields: source: name: Source @@ -201,6 +237,8 @@ select_sound_mode: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.SELECT_SOUND_MODE fields: sound_mode: name: Sound mode @@ -215,6 +253,8 @@ clear_playlist: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.CLEAR_PLAYLIST shuffle_set: name: Shuffle @@ -222,6 +262,8 @@ shuffle_set: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.SHUFFLE_SET fields: shuffle: name: Shuffle @@ -236,6 +278,8 @@ repeat_set: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.REPEAT_SET fields: repeat: name: Repeat mode @@ -259,6 +303,8 @@ join: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.GROUPING fields: group_members: name: Group members @@ -280,3 +326,5 @@ unjoin: target: entity: domain: media_player + supported_features: + - media_player.MediaPlayerEntityFeature.GROUPING From d973e43b9013f4c4959c00e264a467fec2514f2e Mon Sep 17 00:00:00 2001 From: David Knowles Date: Tue, 11 Jul 2023 00:26:02 -0400 Subject: [PATCH 0331/1009] Move Hydrawise to a supported library (#96023) --- homeassistant/components/hydrawise/__init__.py | 4 ++-- homeassistant/components/hydrawise/binary_sensor.py | 4 ++-- homeassistant/components/hydrawise/coordinator.py | 4 ++-- homeassistant/components/hydrawise/manifest.json | 4 ++-- homeassistant/components/hydrawise/sensor.py | 4 ++-- homeassistant/components/hydrawise/switch.py | 4 ++-- requirements_all.txt | 6 +++--- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index e09cabb74fc7fc..6d9f2747847e65 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -1,7 +1,7 @@ """Support for Hydrawise cloud.""" -from hydrawiser.core import Hydrawiser +from pydrawise.legacy import LegacyHydrawise from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol @@ -34,7 +34,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: scan_interval = conf.get(CONF_SCAN_INTERVAL) try: - hydrawise = await hass.async_add_executor_job(Hydrawiser, access_token) + hydrawise = await hass.async_add_executor_job(LegacyHydrawise, access_token) except (ConnectTimeout, HTTPError) as ex: LOGGER.error("Unable to connect to Hydrawise cloud service: %s", str(ex)) _show_failure_notification(hass, str(ex)) diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index 2986bbb170eafd..bc9b8722c58ccd 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Hydrawise sprinkler binary sensors.""" from __future__ import annotations -from hydrawiser.core import Hydrawiser +from pydrawise.legacy import LegacyHydrawise import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -55,7 +55,7 @@ def setup_platform( ) -> None: """Set up a sensor for a Hydrawise device.""" coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN] - hydrawise: Hydrawiser = coordinator.api + hydrawise: LegacyHydrawise = coordinator.api monitored_conditions = config[CONF_MONITORED_CONDITIONS] entities = [] diff --git a/homeassistant/components/hydrawise/coordinator.py b/homeassistant/components/hydrawise/coordinator.py index ea2e2dd2c4c017..007b15d2403dda 100644 --- a/homeassistant/components/hydrawise/coordinator.py +++ b/homeassistant/components/hydrawise/coordinator.py @@ -4,7 +4,7 @@ from datetime import timedelta -from hydrawiser.core import Hydrawiser +from pydrawise.legacy import LegacyHydrawise from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -16,7 +16,7 @@ class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[None]): """The Hydrawise Data Update Coordinator.""" def __init__( - self, hass: HomeAssistant, api: Hydrawiser, scan_interval: timedelta + self, hass: HomeAssistant, api: LegacyHydrawise, scan_interval: timedelta ) -> None: """Initialize HydrawiseDataUpdateCoordinator.""" super().__init__(hass, LOGGER, name=DOMAIN, update_interval=scan_interval) diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index fc88c08b27ae02..48c9cdcf042d6c 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -4,6 +4,6 @@ "codeowners": ["@dknowles2", "@ptcryan"], "documentation": "https://www.home-assistant.io/integrations/hydrawise", "iot_class": "cloud_polling", - "loggers": ["hydrawiser"], - "requirements": ["Hydrawiser==0.2"] + "loggers": ["pydrawise"], + "requirements": ["pydrawise==2023.7.0"] } diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index d1334143375d53..9214b9daeaca09 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -1,7 +1,7 @@ """Support for Hydrawise sprinkler sensors.""" from __future__ import annotations -from hydrawiser.core import Hydrawiser +from pydrawise.legacy import LegacyHydrawise import voluptuous as vol from homeassistant.components.sensor import ( @@ -57,7 +57,7 @@ def setup_platform( ) -> None: """Set up a sensor for a Hydrawise device.""" coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN] - hydrawise: Hydrawiser = coordinator.api + hydrawise: LegacyHydrawise = coordinator.api monitored_conditions = config[CONF_MONITORED_CONDITIONS] entities = [ diff --git a/homeassistant/components/hydrawise/switch.py b/homeassistant/components/hydrawise/switch.py index 00089bb8774f75..dbd2c08b28ea91 100644 --- a/homeassistant/components/hydrawise/switch.py +++ b/homeassistant/components/hydrawise/switch.py @@ -3,7 +3,7 @@ from typing import Any -from hydrawiser.core import Hydrawiser +from pydrawise.legacy import LegacyHydrawise import voluptuous as vol from homeassistant.components.switch import ( @@ -63,7 +63,7 @@ def setup_platform( ) -> None: """Set up a sensor for a Hydrawise device.""" coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN] - hydrawise: Hydrawiser = coordinator.api + hydrawise: LegacyHydrawise = coordinator.api monitored_conditions: list[str] = config[CONF_MONITORED_CONDITIONS] default_watering_timer: int = config[CONF_WATERING_TIME] diff --git a/requirements_all.txt b/requirements_all.txt index 567b8a96d5c340..44d3f4834f8183 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -31,9 +31,6 @@ HAP-python==4.7.0 # homeassistant.components.tasmota HATasmota==0.6.5 -# homeassistant.components.hydrawise -Hydrawiser==0.2 - # homeassistant.components.mastodon Mastodon.py==1.5.1 @@ -1638,6 +1635,9 @@ pydiscovergy==1.2.1 # homeassistant.components.doods pydoods==1.0.2 +# homeassistant.components.hydrawise +pydrawise==2023.7.0 + # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 From aec0694823f514e34a1275ebb67f40ced7985848 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 11 Jul 2023 08:09:57 +0200 Subject: [PATCH 0332/1009] Move tractive attribute to entity class (#96247) Clean up tractive entities --- homeassistant/components/tractive/binary_sensor.py | 2 -- homeassistant/components/tractive/device_tracker.py | 1 - homeassistant/components/tractive/entity.py | 2 ++ homeassistant/components/tractive/sensor.py | 2 -- homeassistant/components/tractive/switch.py | 1 - 5 files changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tractive/binary_sensor.py b/homeassistant/components/tractive/binary_sensor.py index 4b376941344047..cb4abc9b385a0d 100644 --- a/homeassistant/components/tractive/binary_sensor.py +++ b/homeassistant/components/tractive/binary_sensor.py @@ -30,8 +30,6 @@ class TractiveBinarySensor(TractiveEntity, BinarySensorEntity): """Tractive sensor.""" - _attr_has_entity_name = True - def __init__( self, user_id: str, item: Trackables, description: BinarySensorEntityDescription ) -> None: diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index 038461494d6f01..e9739819734fe2 100644 --- a/homeassistant/components/tractive/device_tracker.py +++ b/homeassistant/components/tractive/device_tracker.py @@ -36,7 +36,6 @@ async def async_setup_entry( class TractiveDeviceTracker(TractiveEntity, TrackerEntity): """Tractive device tracker.""" - _attr_has_entity_name = True _attr_icon = "mdi:paw" _attr_translation_key = "tracker" diff --git a/homeassistant/components/tractive/entity.py b/homeassistant/components/tractive/entity.py index def321d928f192..712f8eda75a7db 100644 --- a/homeassistant/components/tractive/entity.py +++ b/homeassistant/components/tractive/entity.py @@ -11,6 +11,8 @@ class TractiveEntity(Entity): """Tractive entity class.""" + _attr_has_entity_name = True + def __init__( self, user_id: str, trackable: dict[str, Any], tracker_details: dict[str, Any] ) -> None: diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index 9c0f8f307edc07..24439b489c8a73 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -52,8 +52,6 @@ class TractiveSensorEntityDescription( class TractiveSensor(TractiveEntity, SensorEntity): """Tractive sensor.""" - _attr_has_entity_name = True - def __init__( self, user_id: str, diff --git a/homeassistant/components/tractive/switch.py b/homeassistant/components/tractive/switch.py index 7ae480d4f98a17..6d8274df2538e9 100644 --- a/homeassistant/components/tractive/switch.py +++ b/homeassistant/components/tractive/switch.py @@ -88,7 +88,6 @@ async def async_setup_entry( class TractiveSwitch(TractiveEntity, SwitchEntity): """Tractive switch.""" - _attr_has_entity_name = True entity_description: TractiveSwitchEntityDescription def __init__( From 6aa2ede6c7337e627cf1694a42f9a0f0b4746a00 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Jul 2023 08:45:45 +0200 Subject: [PATCH 0333/1009] Correct issues raised when calling deprecated vacuum services (#96295) --- homeassistant/components/roborock/strings.json | 9 ++++++++- homeassistant/components/tuya/strings.json | 18 ++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 70ed98a6d5fa2c..63ebd31b34c305 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -175,7 +175,14 @@ "issues": { "service_deprecation_start_pause": { "title": "Roborock vacuum support for vacuum.start_pause is being removed", - "description": "Roborock vacuum support for the vacuum.start_pause service is deprecated and will be removed in Home Assistant 2024.2; Please adjust any automation or script that uses the service to instead call vacuum.pause or vacuum.start and select submit below to mark this issue as resolved." + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::roborock::issues::service_deprecation_start_pause::title%]", + "description": "Roborock vacuum support for the vacuum.start_pause service is deprecated and will be removed in Home Assistant 2024.2; Please adjust any automation or script that uses the service to instead call vacuum.pause or vacuum.start and select submit below to mark this issue as resolved." + } + } + } } } } diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 0cab59de2912c8..f4443e89f76029 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -216,11 +216,25 @@ "issues": { "service_deprecation_turn_off": { "title": "Tuya vacuum support for vacuum.turn_off is being removed", - "description": "Tuya vacuum support for the vacuum.turn_off service is deprecated and will be removed in Home Assistant 2024.2; Please adjust any automation or script that uses the service to instead call vacuum.stop and select submit below to mark this issue as resolved." + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::tuya::issues::service_deprecation_turn_off::title%]", + "description": "Tuya vacuum support for the vacuum.turn_off service is deprecated and will be removed in Home Assistant 2024.2; Please adjust any automation or script that uses the service to instead call vacuum.stop and select submit below to mark this issue as resolved." + } + } + } }, "service_deprecation_turn_on": { "title": "Tuya vacuum support for vacuum.turn_on is being removed", - "description": "Tuya vacuum support for the vacuum.turn_on service is deprecated and will be removed in Home Assistant 2024.2; Please adjust any automation or script that uses the service to instead call vacuum.start and select submit below to mark this issue as resolved." + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::tuya::issues::service_deprecation_turn_on::title%]", + "description": "Tuya vacuum support for the vacuum.turn_on service is deprecated and will be removed in Home Assistant 2024.2; Please adjust any automation or script that uses the service to instead call vacuum.start and select submit below to mark this issue as resolved." + } + } + } } } } From 30578d623655018805c5e97bf52e938a3843d115 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 11 Jul 2023 09:54:28 +0200 Subject: [PATCH 0334/1009] Deprecate mqtt vacuum with legacy schema (#95836) * Deprecate mqtt vacuum with legacy schema * Consistent comments * Correct comment * Remove persistence option * Adjust string, mention restart * Update deprecation comment --- homeassistant/components/mqtt/strings.json | 10 ++++ .../components/mqtt/vacuum/__init__.py | 50 +++++++++++++++++++ .../components/mqtt/vacuum/schema_legacy.py | 7 ++- tests/components/mqtt/test_legacy_vacuum.py | 29 +++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index c1eff29e3bef09..3423b2cd470102 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -1,4 +1,14 @@ { + "issues": { + "deprecation_mqtt_legacy_vacuum_yaml": { + "title": "MQTT vacuum entities with legacy schema found in your configuration.yaml", + "description": "MQTT vacuum entities that use the legacy schema are deprecated, please adjust your configuration.yaml and restart Home Assistant to fix this issue." + }, + "deprecation_mqtt_legacy_vacuum_discovery": { + "title": "MQTT vacuum entities with legacy schema added through MQTT discovery", + "description": "MQTT vacuum entities that use the legacy schema are deprecated, please adjust your devices to use the correct schema and restart Home Assistant to fix this issue." + } + }, "config": { "step": { "broker": { diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 068bc183ec411c..3a2586bdfd7ce3 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -1,7 +1,12 @@ """Support for MQTT vacuums.""" + +# The legacy schema for MQTT vacuum was deprecated with HA Core 2023.8.0 +# and will be removed with HA Core 2024.2.0 + from __future__ import annotations import functools +import logging import voluptuous as vol @@ -9,8 +14,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant 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 DOMAIN from ..mixins import async_setup_entry_helper from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE from .schema_legacy import ( @@ -24,9 +31,44 @@ async_setup_entity_state, ) +_LOGGER = logging.getLogger(__name__) + +MQTT_VACUUM_DOCS_URL = "https://www.home-assistant.io/integrations/vacuum.mqtt/" + + +# The legacy schema for MQTT vacuum was deprecated with HA Core 2023.8.0 +# and will be removed with HA Core 2024.2.0 +def warn_for_deprecation_legacy_schema( + hass: HomeAssistant, config: ConfigType, discovery_data: DiscoveryInfoType | None +) -> None: + """Warn for deprecation of legacy schema.""" + if config[CONF_SCHEMA] == STATE: + return + + key_suffix = "yaml" if discovery_data is None else "discovery" + translation_key = f"deprecation_mqtt_legacy_vacuum_{key_suffix}" + async_create_issue( + hass, + DOMAIN, + translation_key, + breaks_in_ha_version="2024.2.0", + is_fixable=False, + translation_key=translation_key, + learn_more_url=MQTT_VACUUM_DOCS_URL, + severity=IssueSeverity.WARNING, + ) + _LOGGER.warning( + "Deprecated `legacy` schema detected for MQTT vacuum, expected `state` schema, config found: %s", + config, + ) + def validate_mqtt_vacuum_discovery(config_value: ConfigType) -> ConfigType: """Validate MQTT vacuum schema.""" + + # The legacy schema for MQTT vacuum was deprecated with HA Core 2023.8.0 + # and will be removed with HA Core 2024.2.0 + schemas = {LEGACY: DISCOVERY_SCHEMA_LEGACY, STATE: DISCOVERY_SCHEMA_STATE} config: ConfigType = schemas[config_value[CONF_SCHEMA]](config_value) return config @@ -34,6 +76,10 @@ def validate_mqtt_vacuum_discovery(config_value: ConfigType) -> ConfigType: def validate_mqtt_vacuum_modern(config_value: ConfigType) -> ConfigType: """Validate MQTT vacuum modern schema.""" + + # The legacy schema for MQTT vacuum was deprecated with HA Core 2023.8.0 + # and will be removed with HA Core 2024.2.0 + schemas = { LEGACY: PLATFORM_SCHEMA_LEGACY_MODERN, STATE: PLATFORM_SCHEMA_STATE_MODERN, @@ -71,6 +117,10 @@ async def _async_setup_entity( discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT vacuum.""" + + # The legacy schema for MQTT vacuum was deprecated with HA Core 2023.8.0 + # and will be removed with HA Core 2024.2.0 + warn_for_deprecation_legacy_schema(hass, config, discovery_data) setup_entity = { LEGACY: async_setup_entity_legacy, STATE: async_setup_entity_state, diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 6cab62cdb5d0bb..18cda0b137d91a 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -1,4 +1,9 @@ -"""Support for Legacy MQTT vacuum.""" +"""Support for Legacy MQTT vacuum. + +The legacy schema for MQTT vacuum was deprecated with HA Core 2023.8.0 +and is will be removed with HA Core 2024.2.0 +""" + from __future__ import annotations from collections.abc import Callable diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 9a71c747e65d73..8034d42ecbe94c 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -1,4 +1,8 @@ """The tests for the Legacy Mqtt vacuum platform.""" + +# The legacy schema for MQTT vacuum was deprecated with HA Core 2023.8.0 +# and will be removed with HA Core 2024.2.0 + from copy import deepcopy import json from typing import Any @@ -124,6 +128,31 @@ def vacuum_platform_only(): yield +@pytest.mark.parametrize( + ("hass_config", "deprecated"), + [ + ({mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test", "schema": "legacy"}}}, True), + ({mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test"}}}, True), + ({mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test", "schema": "state"}}}, False), + ], +) +async def test_deprecation( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, + deprecated: bool, +) -> None: + """Test that the depration warning for the legacy schema works.""" + assert await mqtt_mock_entry() + entity = hass.states.get("vacuum.test") + assert entity is not None + + if deprecated: + assert "Deprecated `legacy` schema detected for MQTT vacuum" in caplog.text + else: + assert "Deprecated `legacy` schema detected for MQTT vacuum" not in caplog.text + + @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_default_supported_features( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator From beff19f93cefc3a21dc9d16cc401814fd6f0140b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 11 Jul 2023 10:12:31 +0200 Subject: [PATCH 0335/1009] Improve mqtt tag schema logging and avoid tests that use xfail (#95711) Improve schema logging and tests --- homeassistant/components/mqtt/tag.py | 7 ++++- tests/components/mqtt/test_discovery.py | 2 -- tests/components/mqtt/test_init.py | 1 - tests/components/mqtt/test_tag.py | 38 ++++++++++++------------- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 02883b5cd8530e..848950169d8dde 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -20,6 +20,7 @@ from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, MqttDiscoveryDeviceUpdate, + async_handle_schema_error, async_setup_entry_helper, send_discovery_done, update_device, @@ -119,7 +120,11 @@ def __init__( async def async_update(self, discovery_data: MQTTDiscoveryPayload) -> None: """Handle MQTT tag discovery updates.""" # Update tag scanner - config: DiscoveryInfoType = PLATFORM_SCHEMA(discovery_data) + try: + config: DiscoveryInfoType = PLATFORM_SCHEMA(discovery_data) + except vol.Invalid as err: + async_handle_schema_error(discovery_data, err) + return self._config = config self._value_template = MqttValueTemplate( config.get(CONF_VALUE_TEMPLATE), diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index f35af9fb0378ea..14074ce113546a 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -7,7 +7,6 @@ from unittest.mock import AsyncMock, call, patch import pytest -from voluptuous import MultipleInvalid from homeassistant import config_entries from homeassistant.components import mqtt @@ -1643,7 +1642,6 @@ async def test_unique_id_collission_has_priority( assert entity_registry.async_get("sensor.sbfspot_12345_2") is None -@pytest.mark.xfail(raises=MultipleInvalid) @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) async def test_update_with_bad_config_not_breaks_discovery( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index eee1d00613732e..08aa53aec7a1b5 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -2096,7 +2096,6 @@ async def test_setup_manual_mqtt_with_platform_key( @pytest.mark.parametrize("hass_config", [{mqtt.DOMAIN: {"light": {"name": "test"}}}]) -@pytest.mark.xfail(reason="Invalid config for [mqtt]: required key not provided") @patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_setup_manual_mqtt_with_invalid_config( hass: HomeAssistant, diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index f8c7b55f7ce01f..c18e24d1a701b5 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -1,10 +1,10 @@ """The tests for MQTT tag scanner.""" +from collections.abc import Generator import copy import json -from unittest.mock import ANY, patch +from unittest.mock import ANY, AsyncMock, patch import pytest -from voluptuous import MultipleInvalid from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.mqtt.const import DOMAIN as MQTT_DOMAIN @@ -47,14 +47,14 @@ @pytest.fixture(autouse=True) -def binary_sensor_only(): +def binary_sensor_only() -> Generator[None, None, None]: """Only setup the binary_sensor platform to speed up test.""" with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]): yield @pytest.fixture -def tag_mock(): +def tag_mock() -> Generator[AsyncMock, None, None]: """Fixture to mock tag.""" with patch("homeassistant.components.tag.async_scan_tag") as mock_tag: yield mock_tag @@ -65,7 +65,7 @@ async def test_discover_bad_tag( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, - tag_mock, + tag_mock: AsyncMock, ) -> None: """Test bad discovery message.""" await mqtt_mock_entry() @@ -92,7 +92,7 @@ async def test_if_fires_on_mqtt_message_with_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, - tag_mock, + tag_mock: AsyncMock, ) -> None: """Test tag scanning, with device.""" await mqtt_mock_entry() @@ -110,9 +110,8 @@ async def test_if_fires_on_mqtt_message_with_device( async def test_if_fires_on_mqtt_message_without_device( hass: HomeAssistant, - device_registry: dr.DeviceRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, - tag_mock, + tag_mock: AsyncMock, ) -> None: """Test tag scanning, without device.""" await mqtt_mock_entry() @@ -131,7 +130,7 @@ async def test_if_fires_on_mqtt_message_with_template( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, - tag_mock, + tag_mock: AsyncMock, ) -> None: """Test tag scanning, with device.""" await mqtt_mock_entry() @@ -150,7 +149,7 @@ async def test_if_fires_on_mqtt_message_with_template( async def test_strip_tag_id( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - tag_mock, + tag_mock: AsyncMock, ) -> None: """Test strip whitespace from tag_id.""" await mqtt_mock_entry() @@ -169,7 +168,7 @@ async def test_if_fires_on_mqtt_message_after_update_with_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, - tag_mock, + tag_mock: AsyncMock, ) -> None: """Test tag scanning after update.""" await mqtt_mock_entry() @@ -218,7 +217,7 @@ async def test_if_fires_on_mqtt_message_after_update_with_device( async def test_if_fires_on_mqtt_message_after_update_without_device( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - tag_mock, + tag_mock: AsyncMock, ) -> None: """Test tag scanning after update.""" await mqtt_mock_entry() @@ -265,7 +264,7 @@ async def test_if_fires_on_mqtt_message_after_update_with_template( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, - tag_mock, + tag_mock: AsyncMock, ) -> None: """Test tag scanning after update.""" await mqtt_mock_entry() @@ -333,7 +332,7 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_with_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, - tag_mock, + tag_mock: AsyncMock, ) -> None: """Test tag scanning after removal.""" await mqtt_mock_entry() @@ -369,7 +368,7 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_with_device( async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_without_device( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - tag_mock, + tag_mock: AsyncMock, ) -> None: """Test tag scanning not firing after removal.""" await mqtt_mock_entry() @@ -406,7 +405,7 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( hass_ws_client: WebSocketGenerator, device_registry: dr.DeviceRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, - tag_mock, + tag_mock: AsyncMock, ) -> None: """Test tag scanning after removal.""" assert await async_setup_component(hass, "config", {}) @@ -843,11 +842,11 @@ async def test_cleanup_device_with_entity2( assert device_entry is None -@pytest.mark.xfail(raises=MultipleInvalid) async def test_update_with_bad_config_not_breaks_discovery( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - tag_mock, + caplog: pytest.LogCaptureFixture, + tag_mock: AsyncMock, ) -> None: """Test a bad update does not break discovery.""" await mqtt_mock_entry() @@ -875,6 +874,7 @@ async def test_update_with_bad_config_not_breaks_discovery( # Update with bad identifier async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", data2) await hass.async_block_till_done() + assert "extra keys not allowed @ data['device']['bad_key']" in caplog.text # Topic update async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", data3) @@ -891,7 +891,7 @@ async def test_unload_entry( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mqtt_mock: MqttMockHAClient, - tag_mock, + tag_mock: AsyncMock, ) -> None: """Test unloading the MQTT entry.""" From f3e55e96f442c6d5d8cbfd258e49b0c1a8535257 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 11 Jul 2023 10:16:00 +0200 Subject: [PATCH 0336/1009] Improve test coverage mqtt vacuum (#96288) --- tests/components/mqtt/test_legacy_vacuum.py | 35 ++++++++++++++++++ tests/components/mqtt/test_state_vacuum.py | 40 ++++++++++++++++++++- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 8034d42ecbe94c..6b1a74f256dcca 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -321,6 +321,41 @@ async def test_commands_without_supported_features( mqtt_mock.async_publish.reset_mock() +@pytest.mark.parametrize( + "hass_config", + [ + { + "mqtt": { + "vacuum": { + "name": "test", + "schema": "legacy", + mqttvacuum.CONF_SUPPORTED_FEATURES: services_to_strings( + ALL_SERVICES, SERVICE_TO_STRING + ), + } + } + } + ], +) +async def test_command_without_command_topic( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test commands which are not supported by the vacuum.""" + mqtt_mock = await mqtt_mock_entry() + + await common.async_turn_on(hass, "vacuum.test") + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + await common.async_set_fan_speed(hass, "low", "vacuum.test") + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + await common.async_send_command(hass, "some command", "vacuum.test") + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + @pytest.mark.parametrize( "hass_config", [ diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 38baf591094d34..b22fb96aa13706 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -11,7 +11,10 @@ from homeassistant.components.mqtt.vacuum import CONF_SCHEMA, schema_state as mqttvacuum from homeassistant.components.mqtt.vacuum.const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from homeassistant.components.mqtt.vacuum.schema import services_to_strings -from homeassistant.components.mqtt.vacuum.schema_state import SERVICE_TO_STRING +from homeassistant.components.mqtt.vacuum.schema_state import ( + ALL_SERVICES, + SERVICE_TO_STRING, +) from homeassistant.components.vacuum import ( ATTR_BATTERY_ICON, ATTR_BATTERY_LEVEL, @@ -255,6 +258,41 @@ async def test_commands_without_supported_features( mqtt_mock.async_publish.assert_not_called() +@pytest.mark.parametrize( + "hass_config", + [ + { + "mqtt": { + "vacuum": { + "name": "test", + "schema": "state", + mqttvacuum.CONF_SUPPORTED_FEATURES: services_to_strings( + ALL_SERVICES, SERVICE_TO_STRING + ), + } + } + } + ], +) +async def test_command_without_command_topic( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test commands which are not supported by the vacuum.""" + mqtt_mock = await mqtt_mock_entry() + + await common.async_start(hass, "vacuum.test") + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + await common.async_set_fan_speed(hass, "low", "vacuum.test") + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + await common.async_send_command(hass, "some command", "vacuum.test") + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + @pytest.mark.parametrize("hass_config", [CONFIG_ALL_SERVICES]) async def test_status( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator From f46188c85a3fecdacf6cf5de09c3127ae4bfe9f1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Jul 2023 11:34:16 +0200 Subject: [PATCH 0337/1009] Improve the docstring of some config schema generators (#96296) --- homeassistant/helpers/config_validation.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index e8f1e58615cfe3..90aa499af4bab8 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1127,7 +1127,11 @@ def validator(config: dict) -> dict: def config_entry_only_config_schema(domain: str) -> Callable[[dict], dict]: - """Return a config schema which logs if attempted to setup from YAML.""" + """Return a config schema which logs if attempted to setup from YAML. + + Use this when an integration's __init__.py defines setup or async_setup + but setup from yaml is not supported. + """ return _no_yaml_config_schema( domain, @@ -1138,7 +1142,11 @@ def config_entry_only_config_schema(domain: str) -> Callable[[dict], dict]: def platform_only_config_schema(domain: str) -> Callable[[dict], dict]: - """Return a config schema which logs if attempted to setup from YAML.""" + """Return a config schema which logs if attempted to setup from YAML. + + Use this when an integration's __init__.py defines setup or async_setup + but setup from the integration key is not supported. + """ return _no_yaml_config_schema( domain, From d4089bbdbe9c2de67e083aa25498f931b9579a69 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Jul 2023 01:29:05 -1000 Subject: [PATCH 0338/1009] 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 d0a88bf8249afc..2a9e2225e9f3be 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 44d3f4834f8183..e5013b24ce67a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -249,7 +249,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==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 0e6a806518e4f8..13586bf8f262c7 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 d9f27400b7981ce13259de5007159e635238a87d Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 11 Jul 2023 14:10:32 +0200 Subject: [PATCH 0339/1009] Reolink add reboot button (#96311) --- homeassistant/components/reolink/button.py | 70 ++++++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/reolink/button.py b/homeassistant/components/reolink/button.py index 3aa5faa527b096..7a6e2486c71c9d 100644 --- a/homeassistant/components/reolink/button.py +++ b/homeassistant/components/reolink/button.py @@ -7,7 +7,11 @@ from reolink_aio.api import GuardEnum, Host, PtzEnum -from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant @@ -15,12 +19,12 @@ from . import ReolinkData from .const import DOMAIN -from .entity import ReolinkChannelCoordinatorEntity +from .entity import ReolinkChannelCoordinatorEntity, ReolinkHostCoordinatorEntity @dataclass class ReolinkButtonEntityDescriptionMixin: - """Mixin values for Reolink button entities.""" + """Mixin values for Reolink button entities for a camera channel.""" method: Callable[[Host, int], Any] @@ -29,11 +33,27 @@ class ReolinkButtonEntityDescriptionMixin: class ReolinkButtonEntityDescription( ButtonEntityDescription, ReolinkButtonEntityDescriptionMixin ): - """A class that describes button entities.""" + """A class that describes button entities for a camera channel.""" supported: Callable[[Host, int], bool] = lambda api, ch: True +@dataclass +class ReolinkHostButtonEntityDescriptionMixin: + """Mixin values for Reolink button entities for the host.""" + + method: Callable[[Host], Any] + + +@dataclass +class ReolinkHostButtonEntityDescription( + ButtonEntityDescription, ReolinkHostButtonEntityDescriptionMixin +): + """A class that describes button entities for the host.""" + + supported: Callable[[Host], bool] = lambda api: True + + BUTTON_ENTITIES = ( ReolinkButtonEntityDescription( key="ptz_stop", @@ -95,6 +115,17 @@ class ReolinkButtonEntityDescription( ), ) +HOST_BUTTON_ENTITIES = ( + ReolinkHostButtonEntityDescription( + key="reboot", + device_class=ButtonDeviceClass.RESTART, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + supported=lambda api: api.supported(None, "reboot"), + method=lambda api: api.reboot(), + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -104,12 +135,20 @@ async def async_setup_entry( """Set up a Reolink button entities.""" reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( + entities: list[ReolinkButtonEntity | ReolinkHostButtonEntity] = [ ReolinkButtonEntity(reolink_data, channel, entity_description) for entity_description in BUTTON_ENTITIES for channel in reolink_data.host.api.channels if entity_description.supported(reolink_data.host.api, channel) + ] + entities.extend( + [ + ReolinkHostButtonEntity(reolink_data, entity_description) + for entity_description in HOST_BUTTON_ENTITIES + if entity_description.supported(reolink_data.host.api) + ] ) + async_add_entities(entities) class ReolinkButtonEntity(ReolinkChannelCoordinatorEntity, ButtonEntity): @@ -134,3 +173,24 @@ def __init__( async def async_press(self) -> None: """Execute the button action.""" await self.entity_description.method(self._host.api, self._channel) + + +class ReolinkHostButtonEntity(ReolinkHostCoordinatorEntity, ButtonEntity): + """Base button entity class for Reolink IP cameras.""" + + entity_description: ReolinkHostButtonEntityDescription + + def __init__( + self, + reolink_data: ReolinkData, + entity_description: ReolinkHostButtonEntityDescription, + ) -> None: + """Initialize Reolink button entity.""" + super().__init__(reolink_data) + self.entity_description = entity_description + + self._attr_unique_id = f"{self._host.unique_id}_{entity_description.key}" + + async def async_press(self) -> None: + """Execute the button action.""" + await self.entity_description.method(self._host.api) From f12f8bca0355e4b02797aa9e4f477d8ad5fe1c44 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 11 Jul 2023 15:27:31 +0200 Subject: [PATCH 0340/1009] Avoid CI fail in command_line tests (#96324) * Avoid CI fail in command_line tests * Speedup tests manual update --- .../command_line/test_binary_sensor.py | 19 +++++++++---------- tests/components/command_line/test_cover.py | 19 +++++++++---------- tests/components/command_line/test_sensor.py | 17 ++++++++--------- tests/components/command_line/test_switch.py | 17 ++++++++--------- 4 files changed, 34 insertions(+), 38 deletions(-) diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index 9e97f053e07778..910288d6920cae 100644 --- a/tests/components/command_line/test_binary_sensor.py +++ b/tests/components/command_line/test_binary_sensor.py @@ -240,16 +240,18 @@ async def _async_update(self) -> None: ) await hass.async_block_till_done() - assert len(called) == 1 + assert called assert ( "Updating Command Line Binary Sensor Test took longer than the scheduled update interval" not in caplog.text ) + called.clear() + caplog.clear() async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1)) await hass.async_block_till_done() - assert len(called) == 2 + assert called assert ( "Updating Command Line Binary Sensor Test took longer than the scheduled update interval" in caplog.text @@ -266,13 +268,11 @@ async def test_updating_manually( called = [] class MockCommandBinarySensor(CommandBinarySensor): - """Mock entity that updates slow.""" + """Mock entity that updates.""" async def _async_update(self) -> None: - """Update slow.""" + """Update.""" called.append(1) - # Add waiting time - await asyncio.sleep(1) with patch( "homeassistant.components.command_line.binary_sensor.CommandBinarySensor", @@ -297,7 +297,8 @@ async def _async_update(self) -> None: ) await hass.async_block_till_done() - assert len(called) == 1 + assert called + called.clear await hass.services.async_call( HA_DOMAIN, @@ -306,6 +307,4 @@ async def _async_update(self) -> None: blocking=True, ) await hass.async_block_till_done() - assert len(called) == 2 - - await asyncio.sleep(0.2) + assert called diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index ac0a33fc7a979c..d4114f9bbbd787 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -326,16 +326,18 @@ async def _async_update(self) -> None: ) await hass.async_block_till_done() - assert len(called) == 0 + assert not called assert ( "Updating Command Line Cover Test took longer than the scheduled update interval" not in caplog.text ) + called.clear() + caplog.clear() async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1)) await hass.async_block_till_done() - assert len(called) == 1 + assert called assert ( "Updating Command Line Cover Test took longer than the scheduled update interval" in caplog.text @@ -352,13 +354,11 @@ async def test_updating_manually( called = [] class MockCommandCover(CommandCover): - """Mock entity that updates slow.""" + """Mock entity that updates.""" async def _async_update(self) -> None: - """Update slow.""" + """Update.""" called.append(1) - # Add waiting time - await asyncio.sleep(1) with patch( "homeassistant.components.command_line.cover.CommandCover", @@ -384,7 +384,8 @@ async def _async_update(self) -> None: async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(called) == 1 + assert called + called.clear() await hass.services.async_call( HA_DOMAIN, @@ -393,6 +394,4 @@ async def _async_update(self) -> None: blocking=True, ) await hass.async_block_till_done() - assert len(called) == 2 - - await asyncio.sleep(0.2) + assert called diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index b837f5808622c6..af7bf3222a14bd 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -575,16 +575,18 @@ async def _async_update(self) -> None: ) await hass.async_block_till_done() - assert len(called) == 1 + assert called assert ( "Updating Command Line Sensor Test took longer than the scheduled update interval" not in caplog.text ) + called.clear() + caplog.clear() async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1)) await hass.async_block_till_done() - assert len(called) == 2 + assert called assert ( "Updating Command Line Sensor Test took longer than the scheduled update interval" in caplog.text @@ -601,13 +603,11 @@ async def test_updating_manually( called = [] class MockCommandSensor(CommandSensor): - """Mock entity that updates slow.""" + """Mock entity that updates.""" async def _async_update(self) -> None: """Update slow.""" called.append(1) - # Add waiting time - await asyncio.sleep(1) with patch( "homeassistant.components.command_line.sensor.CommandSensor", @@ -630,7 +630,8 @@ async def _async_update(self) -> None: ) await hass.async_block_till_done() - assert len(called) == 1 + assert called + called.clear() await hass.services.async_call( HA_DOMAIN, @@ -639,6 +640,4 @@ async def _async_update(self) -> None: blocking=True, ) await hass.async_block_till_done() - assert len(called) == 2 - - await asyncio.sleep(0.2) + assert called diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index e5331fbe7dda69..12a037f0dd115e 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -684,16 +684,18 @@ async def _async_update(self) -> None: ) await hass.async_block_till_done() - assert len(called) == 0 + assert not called assert ( "Updating Command Line Switch Test took longer than the scheduled update interval" not in caplog.text ) + called.clear() + caplog.clear() async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1)) await hass.async_block_till_done() - assert len(called) == 1 + assert called assert ( "Updating Command Line Switch Test took longer than the scheduled update interval" in caplog.text @@ -710,13 +712,11 @@ async def test_updating_manually( called = [] class MockCommandSwitch(CommandSwitch): - """Mock entity that updates slow.""" + """Mock entity that updates.""" async def _async_update(self) -> None: """Update slow.""" called.append(1) - # Add waiting time - await asyncio.sleep(1) with patch( "homeassistant.components.command_line.switch.CommandSwitch", @@ -743,7 +743,8 @@ async def _async_update(self) -> None: async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10)) await hass.async_block_till_done() - assert len(called) == 1 + assert called + called.clear() await hass.services.async_call( HA_DOMAIN, @@ -752,6 +753,4 @@ async def _async_update(self) -> None: blocking=True, ) await hass.async_block_till_done() - assert len(called) == 2 - - await asyncio.sleep(0.2) + assert called From f054de0ad5d60bcec27de7771dc19c18e40a314b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 15:52:12 +0200 Subject: [PATCH 0341/1009] Add support for service translations (#95984) --- homeassistant/components/light/services.yaml | 84 +----- homeassistant/components/light/strings.json | 302 +++++++++++++++++++ homeassistant/helpers/service.py | 85 ++++-- homeassistant/helpers/translation.py | 2 +- script/hassfest/services.py | 72 ++++- script/hassfest/translations.py | 14 + tests/helpers/test_service.py | 45 ++- 7 files changed, 484 insertions(+), 120 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index d1221dd121071e..1ba204e5eda900 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -1,17 +1,11 @@ # Describes the format for available light services turn_on: - name: Turn on - description: > - Turn on one or more lights and adjust properties of the light, even when - they are turned on already. target: entity: domain: light fields: transition: - name: Transition - description: Duration it takes to get to next state. filter: supported_features: - light.LightEntityFeature.TRANSITION @@ -21,8 +15,6 @@ turn_on: max: 300 unit_of_measurement: seconds rgb_color: - name: Color - description: The color for the light (based on RGB - red, green, blue). filter: attribute: supported_color_modes: @@ -34,8 +26,6 @@ turn_on: selector: color_rgb: rgbw_color: - name: RGBW-color - description: A list containing four integers between 0 and 255 representing the RGBW (red, green, blue, white) color for the light. filter: attribute: supported_color_modes: @@ -49,8 +39,6 @@ turn_on: selector: object: rgbww_color: - name: RGBWW-color - description: A list containing five integers between 0 and 255 representing the RGBWW (red, green, blue, cold white, warm white) color for the light. filter: attribute: supported_color_modes: @@ -64,8 +52,6 @@ turn_on: selector: object: color_name: - name: Color name - description: A human readable color name. filter: attribute: supported_color_modes: @@ -77,6 +63,7 @@ turn_on: advanced: true selector: select: + translation_key: color_name options: - "homeassistant" - "aliceblue" @@ -228,8 +215,6 @@ turn_on: - "yellow" - "yellowgreen" hs_color: - name: Hue/Sat color - description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. filter: attribute: supported_color_modes: @@ -243,8 +228,6 @@ turn_on: selector: object: xy_color: - name: XY-color - description: Color for the light in XY-format. filter: attribute: supported_color_modes: @@ -258,8 +241,6 @@ turn_on: selector: object: color_temp: - name: Color temperature - description: Color temperature for the light in mireds. filter: attribute: supported_color_modes: @@ -274,8 +255,6 @@ turn_on: min_mireds: 153 max_mireds: 500 kelvin: - name: Color temperature (Kelvin) - description: Color temperature for the light in Kelvin. filter: attribute: supported_color_modes: @@ -293,10 +272,6 @@ turn_on: step: 100 unit_of_measurement: K brightness: - name: Brightness value - description: Number indicating brightness, where 0 turns the light - off, 1 is the minimum brightness and 255 is the maximum brightness - supported by the light. filter: attribute: supported_color_modes: @@ -313,10 +288,6 @@ turn_on: min: 0 max: 255 brightness_pct: - name: Brightness - description: Number indicating percentage of full brightness, where 0 - turns the light off, 1 is the minimum brightness and 100 is the maximum - brightness supported by the light. filter: attribute: supported_color_modes: @@ -333,8 +304,6 @@ turn_on: max: 100 unit_of_measurement: "%" brightness_step: - name: Brightness step value - description: Change brightness by an amount. filter: attribute: supported_color_modes: @@ -351,8 +320,6 @@ turn_on: min: -225 max: 255 brightness_step_pct: - name: Brightness step - description: Change brightness by a percentage. filter: attribute: supported_color_modes: @@ -369,8 +336,6 @@ turn_on: max: 100 unit_of_measurement: "%" white: - name: White - description: Set the light to white mode. filter: attribute: supported_color_modes: @@ -381,15 +346,11 @@ turn_on: value: true label: Enabled profile: - name: Profile - description: Name of a light profile to use. advanced: true example: relax selector: text: flash: - name: Flash - description: If the light should flash. filter: supported_features: - light.LightEntityFeature.FLASH @@ -402,8 +363,6 @@ turn_on: - label: "Short" value: "short" effect: - name: Effect - description: Light effect. filter: supported_features: - light.LightEntityFeature.EFFECT @@ -411,15 +370,11 @@ turn_on: text: turn_off: - name: Turn off - description: Turns off one or more lights. target: entity: domain: light fields: transition: - name: Transition - description: Duration it takes to get to next state. filter: supported_features: - light.LightEntityFeature.TRANSITION @@ -429,8 +384,6 @@ turn_off: max: 300 unit_of_measurement: seconds flash: - name: Flash - description: If the light should flash. filter: supported_features: - light.LightEntityFeature.FLASH @@ -444,17 +397,11 @@ turn_off: value: "short" toggle: - name: Toggle - description: > - Toggles one or more lights, from on to off, or, off to on, based on their - current state. target: entity: domain: light fields: transition: - name: Transition - description: Duration it takes to get to next state. filter: supported_features: - light.LightEntityFeature.TRANSITION @@ -464,8 +411,6 @@ toggle: max: 300 unit_of_measurement: seconds rgb_color: - name: RGB-color - description: Color for the light in RGB-format. filter: attribute: supported_color_modes: @@ -479,8 +424,6 @@ toggle: selector: object: color_name: - name: Color name - description: A human readable color name. filter: attribute: supported_color_modes: @@ -492,6 +435,7 @@ toggle: advanced: true selector: select: + translation_key: color_name options: - "homeassistant" - "aliceblue" @@ -643,8 +587,6 @@ toggle: - "yellow" - "yellowgreen" hs_color: - name: Hue/Sat color - description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. filter: attribute: supported_color_modes: @@ -658,8 +600,6 @@ toggle: selector: object: xy_color: - name: XY-color - description: Color for the light in XY-format. filter: attribute: supported_color_modes: @@ -673,8 +613,6 @@ toggle: selector: object: color_temp: - name: Color temperature (mireds) - description: Color temperature for the light in mireds. filter: attribute: supported_color_modes: @@ -688,8 +626,6 @@ toggle: selector: color_temp: kelvin: - name: Color temperature (Kelvin) - description: Color temperature for the light in Kelvin. filter: attribute: supported_color_modes: @@ -707,10 +643,6 @@ toggle: step: 100 unit_of_measurement: K brightness: - name: Brightness value - description: Number indicating brightness, where 0 turns the light - off, 1 is the minimum brightness and 255 is the maximum brightness - supported by the light. filter: attribute: supported_color_modes: @@ -727,10 +659,6 @@ toggle: min: 0 max: 255 brightness_pct: - name: Brightness - description: Number indicating percentage of full brightness, where 0 - turns the light off, 1 is the minimum brightness and 100 is the maximum - brightness supported by the light. filter: attribute: supported_color_modes: @@ -747,8 +675,6 @@ toggle: max: 100 unit_of_measurement: "%" white: - name: White - description: Set the light to white mode. filter: attribute: supported_color_modes: @@ -759,15 +685,11 @@ toggle: value: true label: Enabled profile: - name: Profile - description: Name of a light profile to use. advanced: true example: relax selector: text: flash: - name: Flash - description: If the light should flash. filter: supported_features: - light.LightEntityFeature.FLASH @@ -780,8 +702,6 @@ toggle: - label: "Short" value: "short" effect: - name: Effect - description: Light effect. filter: supported_features: - light.LightEntityFeature.EFFECT diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index 6219ade3e58384..a4a46d2ca94140 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -86,5 +86,307 @@ } } } + }, + "selector": { + "color_name": { + "options": { + "homeassistant": "Home Assistant", + "aliceblue": "Alice blue", + "antiquewhite": "Antique white", + "aqua": "Aqua", + "aquamarine": "Aquamarine", + "azure": "Azure", + "beige": "Beige", + "bisque": "Bisque", + "blanchedalmond": "Blanched almond", + "blue": "Blue", + "blueviolet": "Blue violet", + "brown": "Brown", + "burlywood": "Burlywood", + "cadetblue": "Cadet blue", + "chartreuse": "Chartreuse", + "chocolate": "Chocolate", + "coral": "Coral", + "cornflowerblue": "Cornflower blue", + "cornsilk": "Cornsilk", + "crimson": "Crimson", + "cyan": "Cyan", + "darkblue": "Dark blue", + "darkcyan": "Dark cyan", + "darkgoldenrod": "Dark goldenrod", + "darkgray": "Dark gray", + "darkgreen": "Dark green", + "darkgrey": "Dark grey", + "darkkhaki": "Dark khaki", + "darkmagenta": "Dark magenta", + "darkolivegreen": "Dark olive green", + "darkorange": "Dark orange", + "darkorchid": "Dark orchid", + "darkred": "Dark red", + "darksalmon": "Dark salmon", + "darkseagreen": "Dark sea green", + "darkslateblue": "Dark slate blue", + "darkslategray": "Dark slate gray", + "darkslategrey": "Dark slate grey", + "darkturquoise": "Dark turquoise", + "darkviolet": "Dark violet", + "deeppink": "Deep pink", + "deepskyblue": "Deep sky blue", + "dimgray": "Dim gray", + "dimgrey": "Dim grey", + "dodgerblue": "Dodger blue", + "firebrick": "Fire brick", + "floralwhite": "Floral white", + "forestgreen": "Forest green", + "fuchsia": "Fuchsia", + "gainsboro": "Gainsboro", + "ghostwhite": "Ghost white", + "gold": "Gold", + "goldenrod": "Goldenrod", + "gray": "Gray", + "green": "Green", + "greenyellow": "Green yellow", + "grey": "Grey", + "honeydew": "Honeydew", + "hotpink": "Hot pink", + "indianred": "Indian red", + "indigo": "Indigo", + "ivory": "Ivory", + "khaki": "Khaki", + "lavender": "Lavender", + "lavenderblush": "Lavender blush", + "lawngreen": "Lawn green", + "lemonchiffon": "Lemon chiffon", + "lightblue": "Light blue", + "lightcoral": "Light coral", + "lightcyan": "Light cyan", + "lightgoldenrodyellow": "Light goldenrod yellow", + "lightgray": "Light gray", + "lightgreen": "Light green", + "lightgrey": "Light grey", + "lightpink": "Light pink", + "lightsalmon": "Light salmon", + "lightseagreen": "Light sea green", + "lightskyblue": "Light sky blue", + "lightslategray": "Light slate gray", + "lightslategrey": "Light slate grey", + "lightsteelblue": "Light steel blue", + "lightyellow": "Light yellow", + "lime": "Lime", + "limegreen": "Lime green", + "linen": "Linen", + "magenta": "Magenta", + "maroon": "Maroon", + "mediumaquamarine": "Medium aquamarine", + "mediumblue": "Medium blue", + "mediumorchid": "Medium orchid", + "mediumpurple": "Medium purple", + "mediumseagreen": "Medium sea green", + "mediumslateblue": "Medium slate blue", + "mediumspringgreen": "Medium spring green", + "mediumturquoise": "Medium turquoise", + "mediumvioletred": "Medium violet red", + "midnightblue": "Midnight blue", + "mintcream": "Mint cream", + "mistyrose": "Misty rose", + "moccasin": "Moccasin", + "navajowhite": "Navajo white", + "navy": "Navy", + "navyblue": "Navy blue", + "oldlace": "Old lace", + "olive": "Olive", + "olivedrab": "Olive drab", + "orange": "Orange", + "orangered": "Orange red", + "orchid": "Orchid", + "palegoldenrod": "Pale goldenrod", + "palegreen": "Pale green", + "paleturquoise": "Pale turquoise", + "palevioletred": "Pale violet red", + "papayawhip": "Papaya whip", + "peachpuff": "Peach puff", + "peru": "Peru", + "pink": "Pink", + "plum": "Plum", + "powderblue": "Powder blue", + "purple": "Purple", + "red": "Red", + "rosybrown": "Rosy brown", + "royalblue": "Royal blue", + "saddlebrown": "Saddle brown", + "salmon": "Salmon", + "sandybrown": "Sandy brown", + "seagreen": "Sea green", + "seashell": "Seashell", + "sienna": "Sienna", + "silver": "Silver", + "skyblue": "Sky blue", + "slateblue": "Slate blue", + "slategray": "Slate gray", + "slategrey": "Slate grey", + "snow": "Snow", + "springgreen": "Spring green", + "steelblue": "Steel blue", + "tan": "Tan", + "teal": "Teal", + "thistle": "Thistle", + "tomato": "Tomato", + "turquoise": "Turquoise", + "violet": "Violet", + "wheat": "Wheat", + "white": "White", + "whitesmoke": "White smoke", + "yellow": "Yellow", + "yellowgreen": "Yellow green" + } + } + }, + "services": { + "turn_on": { + "name": "Turn on", + "description": "Turn on one or more lights and adjust properties of the light, even when they are turned on already.", + "fields": { + "transition": { + "name": "Transition", + "description": "Duration it takes to get to next state." + }, + "rgb_color": { + "name": "Color", + "description": "The color in RGB format. A list of three integers between 0 and 255 representing the values of red, green, and blue." + }, + "rgbw_color": { + "name": "RGBW-color", + "description": "The color in RGBW format. A list of four integers between 0 and 255 representing the values of red, green, blue, and white." + }, + "rgbww_color": { + "name": "RGBWW-color", + "description": "The color in RGBWW format. A list of five integers between 0 and 255 representing the values of red, green, blue, cold white, and warm white." + }, + "color_name": { + "name": "Color name", + "description": "A human readable color name." + }, + "hs_color": { + "name": "Hue/Sat color", + "description": "Color in hue/sat format. A list of two integers. Hue is 0-360 and Sat is 0-100." + }, + "xy_color": { + "name": "XY-color", + "description": "Color in XY-format. A list of two decimal numbers between 0 and 1." + }, + "color_temp": { + "name": "Color temperature", + "description": "Color temperature in mireds." + }, + "kelvin": { + "name": "Color temperature", + "description": "Color temperature in Kelvin." + }, + "brightness": { + "name": "Brightness value", + "description": "Number indicating brightness, where 0 turns the light off, 1 is the minimum brightness, and 255 is the maximum brightness." + }, + "brightness_pct": { + "name": "Brightness", + "description": "Number indicating the percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness, and 100 is the maximum brightness." + }, + "brightness_step": { + "name": "Brightness step value", + "description": "Change brightness by an amount." + }, + "brightness_step_pct": { + "name": "Brightness step", + "description": "Change brightness by a percentage." + }, + "white": { + "name": "White", + "description": "Set the light to white mode." + }, + "profile": { + "name": "Profile", + "description": "Name of a light profile to use." + }, + "flash": { + "name": "Flash", + "description": "If the light should flash." + }, + "effect": { + "name": "Effect", + "description": "Light effect." + } + } + }, + "turn_off": { + "name": "Turn off", + "description": "Turn off one or more lights.", + "fields": { + "transition": { + "name": "[%key:component::light::services::turn_on::fields::transition::name%]", + "description": "[%key:component::light::services::turn_on::fields::transition::description%]" + }, + "flash": { + "name": "[%key:component::light::services::turn_on::fields::flash::name%]", + "description": "[%key:component::light::services::turn_on::fields::flash::description%]" + } + } + }, + "toggle": { + "name": "Toggle", + "description": "Toggles one or more lights, from on to off, or, off to on, based on their current state.", + "fields": { + "transition": { + "name": "[%key:component::light::services::turn_on::fields::transition::name%]", + "description": "[%key:component::light::services::turn_on::fields::transition::description%]" + }, + "rgb_color": { + "name": "[%key:component::light::services::turn_on::fields::rgb_color::name%]", + "description": "[%key:component::light::services::turn_on::fields::rgb_color::description%]" + }, + "color_name": { + "name": "[%key:component::light::services::turn_on::fields::color_name::name%]", + "description": "[%key:component::light::services::turn_on::fields::color_name::description%]" + }, + "hs_color": { + "name": "[%key:component::light::services::turn_on::fields::hs_color::name%]", + "description": "[%key:component::light::services::turn_on::fields::hs_color::description%]" + }, + "xy_color": { + "name": "[%key:component::light::services::turn_on::fields::xy_color::name%]", + "description": "[%key:component::light::services::turn_on::fields::xy_color::description%]" + }, + "color_temp": { + "name": "[%key:component::light::services::turn_on::fields::color_temp::name%]", + "description": "[%key:component::light::services::turn_on::fields::color_temp::description%]" + }, + "kelvin": { + "name": "[%key:component::light::services::turn_on::fields::kelvin::name%]", + "description": "[%key:component::light::services::turn_on::fields::kelvin::description%]" + }, + "brightness": { + "name": "[%key:component::light::services::turn_on::fields::brightness::name%]", + "description": "[%key:component::light::services::turn_on::fields::brightness::description%]" + }, + "brightness_pct": { + "name": "[%key:component::light::services::turn_on::fields::brightness_pct::name%]", + "description": "[%key:component::light::services::turn_on::fields::brightness_pct::description%]" + }, + "white": { + "name": "[%key:component::light::services::turn_on::fields::white::name%]", + "description": "[%key:component::light::services::turn_on::fields::white::description%]" + }, + "profile": { + "name": "[%key:component::light::services::turn_on::fields::profile::name%]", + "description": "[%key:component::light::services::turn_on::fields::profile::description%]" + }, + "flash": { + "name": "[%key:component::light::services::turn_on::fields::flash::name%]", + "description": "[%key:component::light::services::turn_on::fields::flash::description%]" + }, + "effect": { + "name": "[%key:component::light::services::turn_on::fields::effect::name%]", + "description": "[%key:component::light::services::turn_on::fields::effect::description%]" + } + } + } } } diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 40bb96506309e8..1a418a68fd1c9d 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -50,6 +50,7 @@ device_registry, entity_registry, template, + translation, ) from .selector import TargetSelector from .typing import ConfigType, TemplateVarsType @@ -607,6 +608,11 @@ async def async_get_all_descriptions( ) loaded = dict(zip(missing, contents)) + # Load translations for all service domains + translations = await translation.async_get_translations( + hass, "en", "services", list(services) + ) + # Build response descriptions: dict[str, dict[str, Any]] = {} for domain, services_map in services.items(): @@ -616,37 +622,62 @@ async def async_get_all_descriptions( for service_name in services_map: cache_key = (domain, service_name) description = descriptions_cache.get(cache_key) + if description is not None: + domain_descriptions[service_name] = description + continue + # Cache missing descriptions - if description is None: - 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_name, {} - ) + 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_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", ""), - "fields": yaml_description.get("fields", {}), + # Don't warn for missing services, because it triggers false + # positives for things like scripts, that register as a service + # + # When name & description are in the translations use those; + # otherwise fallback to backwards compatible behavior from + # the time when we didn't have translations for descriptions yet. + # This mimics the behavior of the frontend. + description = { + "name": translations.get( + f"component.{domain}.services.{service_name}.name", + yaml_description.get("name", ""), + ), + "description": translations.get( + f"component.{domain}.services.{service_name}.description", + yaml_description.get("description", ""), + ), + "fields": dict(yaml_description.get("fields", {})), + } + + # Translate fields names & descriptions as well + for field_name, field_schema in description["fields"].items(): + if name := translations.get( + f"component.{domain}.services.{service_name}.fields.{field_name}.name" + ): + field_schema["name"] = name + if desc := translations.get( + f"component.{domain}.services.{service_name}.fields.{field_name}.description" + ): + field_schema["description"] = desc + + if "target" in yaml_description: + description["target"] = yaml_description["target"] + + if ( + response := hass.services.supports_response(domain, service_name) + ) != SupportsResponse.NONE: + description["response"] = { + "optional": response == SupportsResponse.OPTIONAL, } - if "target" in yaml_description: - description["target"] = yaml_description["target"] - - if ( - response := hass.services.supports_response(domain, service_name) - ) != SupportsResponse.NONE: - description["response"] = { - "optional": response == SupportsResponse.OPTIONAL, - } - - descriptions_cache[cache_key] = description + descriptions_cache[cache_key] = description domain_descriptions[service_name] = description diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 96ce9b618c2d9a..79ac3a0c5b73b2 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -302,7 +302,7 @@ async def async_get_translations( components = set(integrations) elif config_flow: components = (await async_get_config_flows(hass)) - hass.config.components - elif category in ("state", "entity_component"): + elif category in ("state", "entity_component", "services"): components = set(hass.config.components) else: # Only 'state' supports merging, so remove platforms from selection diff --git a/script/hassfest/services.py b/script/hassfest/services.py index a0c629567fac9f..e0e771ee11d606 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -1,6 +1,8 @@ """Validate dependencies.""" from __future__ import annotations +import contextlib +import json import pathlib import re from typing import Any @@ -25,7 +27,7 @@ def exists(value: Any) -> Any: FIELD_SCHEMA = vol.Schema( { - vol.Required("description"): str, + vol.Optional("description"): str, vol.Optional("name"): str, vol.Optional("example"): exists, vol.Optional("default"): exists, @@ -46,7 +48,7 @@ def exists(value: Any) -> Any: SERVICE_SCHEMA = vol.Schema( { - vol.Required("description"): str, + vol.Optional("description"): str, vol.Optional("name"): str, vol.Optional("target"): vol.Any(selector.TargetSelector.CONFIG_SCHEMA, None), vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}), @@ -70,7 +72,7 @@ def grep_dir(path: pathlib.Path, glob_pattern: str, search_pattern: str) -> bool return False -def validate_services(integration: Integration) -> None: +def validate_services(config: Config, integration: Integration) -> None: """Validate services.""" try: data = load_yaml(str(integration.path / "services.yaml")) @@ -92,15 +94,75 @@ def validate_services(integration: Integration) -> None: return try: - SERVICES_SCHEMA(data) + services = SERVICES_SCHEMA(data) except vol.Invalid as err: integration.add_error( "services", f"Invalid services.yaml: {humanize_error(data, err)}" ) + # Try loading translation strings + if integration.core: + strings_file = integration.path / "strings.json" + else: + # For custom integrations, use the en.json file + strings_file = integration.path / "translations/en.json" + + strings = {} + if strings_file.is_file(): + with contextlib.suppress(ValueError): + strings = json.loads(strings_file.read_text()) + + # For each service in the integration, check if the description if set, + # if not, check if it's in the strings file. If not, add an error. + for service_name, service_schema in services.items(): + if "name" not in service_schema: + try: + strings["services"][service_name]["name"] + except KeyError: + integration.add_error( + "services", + f"Service {service_name} has no name and is not in the translations file", + ) + + if "description" not in service_schema: + try: + strings["services"][service_name]["description"] + except KeyError: + integration.add_error( + "services", + f"Service {service_name} has no description and is not in the translations file", + ) + + # The same check is done for the description in each of the fields of the + # service schema. + for field_name, field_schema in service_schema.get("fields", {}).items(): + if "description" not in field_schema: + try: + strings["services"][service_name]["fields"][field_name][ + "description" + ] + except KeyError: + integration.add_error( + "services", + f"Service {service_name} has a field {field_name} with no description and is not in the translations file", + ) + + if "selector" in field_schema: + with contextlib.suppress(KeyError): + translation_key = field_schema["selector"]["select"][ + "translation_key" + ] + try: + strings["selector"][translation_key] + except KeyError: + integration.add_error( + "services", + f"Service {service_name} has a field {field_name} with a selector with a translation key {translation_key} that is not in the translations file", + ) + def validate(integrations: dict[str, Integration], config: Config) -> None: """Handle dependencies for integrations.""" # check services.yaml is cool for integration in integrations.values(): - validate_services(integration) + validate_services(config, integration) diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index e53b311b43e44b..597b8e1ae1f62e 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -326,6 +326,20 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: ), slug_validator=cv.slug, ), + vol.Optional("services"): cv.schema_with_slug_keys( + { + vol.Required("name"): translation_value_validator, + vol.Required("description"): translation_value_validator, + vol.Optional("fields"): cv.schema_with_slug_keys( + { + vol.Required("name"): str, + vol.Required("description"): translation_value_validator, + }, + slug_validator=translation_key_validator, + ), + }, + slug_validator=translation_key_validator, + ), } ) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index bc7a93f0f19d75..a99f303f6c99f7 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -1,6 +1,8 @@ """Test service helpers.""" from collections import OrderedDict +from collections.abc import Iterable from copy import deepcopy +from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest @@ -556,13 +558,47 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: logger = hass.components.logger logger_config = {logger.DOMAIN: {}} - await async_setup_component(hass, logger.DOMAIN, logger_config) - descriptions = await service.async_get_all_descriptions(hass) + + async def async_get_translations( + hass: HomeAssistant, + language: str, + category: str, + integrations: Iterable[str] | None = None, + config_flow: bool | None = None, + ) -> dict[str, Any]: + """Return all backend translations.""" + translation_key_prefix = f"component.{logger.DOMAIN}.services.set_default_level" + return { + f"{translation_key_prefix}.name": "Translated name", + f"{translation_key_prefix}.description": "Translated description", + f"{translation_key_prefix}.fields.level.name": "Field name", + f"{translation_key_prefix}.fields.level.description": "Field description", + } + + with patch( + "homeassistant.helpers.service.translation.async_get_translations", + side_effect=async_get_translations, + ): + await async_setup_component(hass, logger.DOMAIN, logger_config) + descriptions = await service.async_get_all_descriptions(hass) assert len(descriptions) == 2 - assert "description" in descriptions[logger.DOMAIN]["set_level"] - assert "fields" in descriptions[logger.DOMAIN]["set_level"] + assert descriptions[logger.DOMAIN]["set_default_level"]["name"] == "Translated name" + assert ( + descriptions[logger.DOMAIN]["set_default_level"]["description"] + == "Translated description" + ) + assert ( + descriptions[logger.DOMAIN]["set_default_level"]["fields"]["level"]["name"] + == "Field name" + ) + assert ( + descriptions[logger.DOMAIN]["set_default_level"]["fields"]["level"][ + "description" + ] + == "Field description" + ) hass.services.async_register(logger.DOMAIN, "new_service", lambda x: None, None) service.async_set_service_schema( @@ -602,7 +638,6 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: "another_service_with_response", {"description": "response service"}, ) - 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] From 107f589a2e18b41062c376ffb75cbf7200a2a36f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Jul 2023 16:38:18 +0200 Subject: [PATCH 0342/1009] Remove some duplicated translations (#96300) --- homeassistant/components/automation/strings.json | 2 +- homeassistant/components/counter/strings.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/strings.json b/homeassistant/components/automation/strings.json index 4e433119a2ae98..6f925fe090db89 100644 --- a/homeassistant/components/automation/strings.json +++ b/homeassistant/components/automation/strings.json @@ -38,7 +38,7 @@ "fix_flow": { "step": { "confirm": { - "title": "{name} uses an unknown service", + "title": "[%key:component::automation::issues::service_not_found::title%]", "description": "The automation \"{name}\" (`{entity_id}`) has an action that calls an unknown service: `{service}`.\n\nThis error prevents the automation from running correctly. Maybe this service is no longer available, or perhaps a typo caused it.\n\nTo fix this error, [edit the automation]({edit}) and remove the action that calls this service.\n\nClick on SUBMIT below to confirm you have fixed this automation." } } diff --git a/homeassistant/components/counter/strings.json b/homeassistant/components/counter/strings.json index 0959259465939b..6dcfe14a03a241 100644 --- a/homeassistant/components/counter/strings.json +++ b/homeassistant/components/counter/strings.json @@ -32,7 +32,7 @@ "fix_flow": { "step": { "confirm": { - "title": "The counter configure service is being removed", + "title": "[%key:component::counter::issues::deprecated_configure_service::title%]", "description": "The counter service `counter.configure` is being removed and use of it has been detected. If you want to change the current value of a counter, use the new `counter.set_value` service instead.\n\nPlease remove the use of this service from your automations and scripts and select **submit** to close this issue." } } From f2f9b20880a436e6dcc1e4022ae16cb3d70f47dc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 16:48:07 +0200 Subject: [PATCH 0343/1009] Fix hassfest services check (#96337) --- script/hassfest/services.py | 1 + 1 file changed, 1 insertion(+) diff --git a/script/hassfest/services.py b/script/hassfest/services.py index e0e771ee11d606..2f8e20939db4bb 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -99,6 +99,7 @@ def validate_services(config: Config, integration: Integration) -> None: integration.add_error( "services", f"Invalid services.yaml: {humanize_error(data, err)}" ) + return # Try loading translation strings if integration.core: From 916e7dd35925f04237a793cf9e8eccfdf1300852 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Jul 2023 17:28:54 +0200 Subject: [PATCH 0344/1009] Fix a couple of typos (#96298) --- homeassistant/components/device_automation/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_automation/helpers.py b/homeassistant/components/device_automation/helpers.py index 038ded07e8aef0..83c599bc65d591 100644 --- a/homeassistant/components/device_automation/helpers.py +++ b/homeassistant/components/device_automation/helpers.py @@ -75,7 +75,7 @@ async def async_validate_device_automation_config( # config entry is loaded registry = dr.async_get(hass) if not (device := registry.async_get(validated_config[CONF_DEVICE_ID])): - # The device referenced by the device trigger does not exist + # The device referenced by the device automation does not exist raise InvalidDeviceAutomationConfig( f"Unknown device '{validated_config[CONF_DEVICE_ID]}'" ) @@ -91,7 +91,7 @@ async def async_validate_device_automation_config( break if not device_config_entry: - # The config entry referenced by the device trigger does not exist + # The config entry referenced by the device automation does not exist raise InvalidDeviceAutomationConfig( f"Device '{validated_config[CONF_DEVICE_ID]}' has no config entry from " f"domain '{validated_config[CONF_DOMAIN]}'" From 38aa62a9903651137625692328b8fed56131a9c5 Mon Sep 17 00:00:00 2001 From: Luke Date: Tue, 11 Jul 2023 11:32:06 -0400 Subject: [PATCH 0345/1009] 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 baab687e64a3cd..0cf6db4ae816db 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 e5013b24ce67a1..35aa527997c1d4 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 13586bf8f262c7..4e3791da4a4ded 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 65bacdddd85455aec7ed6e283e9c45e877ca2a99 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Jul 2023 17:33:49 +0200 Subject: [PATCH 0346/1009] Remove removed_yaml from the spotify integeration (#96261) * Add removed_yaml issue to the homeassistant integration * Remove issue translation from spotify * Remove unrelated change * Remove async_setup from spotify --- homeassistant/components/spotify/__init__.py | 20 ------------------- homeassistant/components/spotify/strings.json | 6 ------ 2 files changed, 26 deletions(-) diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index cb6484c5e3e0cd..ca9f63bbd1c302 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -13,13 +13,10 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, async_get_config_entry_implementation, ) -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .browse_media import async_browse_media @@ -30,7 +27,6 @@ spotify_uri_from_media_browser_url, ) -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.MEDIA_PLAYER] @@ -53,22 +49,6 @@ class HomeAssistantSpotifyData: session: OAuth2Session -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Spotify integration.""" - if DOMAIN in config: - async_create_issue( - hass, - DOMAIN, - "removed_yaml", - breaks_in_ha_version="2022.8.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="removed_yaml", - ) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Spotify from a config entry.""" implementation = await async_get_config_entry_implementation(hass, entry) diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index 4405bd213102e7..caec5b8a288f58 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -21,11 +21,5 @@ "info": { "api_endpoint_reachable": "Spotify API endpoint reachable" } - }, - "issues": { - "removed_yaml": { - "title": "The Spotify YAML configuration has been removed", - "description": "Configuring Spotify using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } } } From 5a87186916f86be84da2db2e49dccffffd18ad93 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 11 Jul 2023 18:01:05 +0200 Subject: [PATCH 0347/1009] Improve integration startup in AVM Fritz!Tools (#96269) --- homeassistant/components/fritz/common.py | 87 +++++++++++------------- tests/components/fritz/conftest.py | 11 ++- tests/components/fritz/const.py | 73 ++++++++++++++------ 3 files changed, 102 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 26b336208fef98..81fdcde236a408 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -129,13 +129,34 @@ class Interface(TypedDict): type: str -class HostInfo(TypedDict): - """FRITZ!Box host info class.""" - - mac: str - name: str - ip: str - status: bool +HostAttributes = TypedDict( + "HostAttributes", + { + "Index": int, + "IPAddress": str, + "MACAddress": str, + "Active": bool, + "HostName": str, + "InterfaceType": str, + "X_AVM-DE_Port": int, + "X_AVM-DE_Speed": int, + "X_AVM-DE_UpdateAvailable": bool, + "X_AVM-DE_UpdateSuccessful": str, + "X_AVM-DE_InfoURL": str | None, + "X_AVM-DE_MACAddressList": str | None, + "X_AVM-DE_Model": str | None, + "X_AVM-DE_URL": str | None, + "X_AVM-DE_Guest": bool, + "X_AVM-DE_RequestClient": str, + "X_AVM-DE_VPN": bool, + "X_AVM-DE_WANAccess": str, + "X_AVM-DE_Disallow": bool, + "X_AVM-DE_IsMeshable": str, + "X_AVM-DE_Priority": str, + "X_AVM-DE_FriendlyName": str, + "X_AVM-DE_FriendlyNameIsWriteable": str, + }, +) class UpdateCoordinatorDataType(TypedDict): @@ -353,11 +374,11 @@ def signal_device_update(self) -> str: """Event specific per FRITZ!Box entry to signal updates in devices.""" return f"{DOMAIN}-device-update-{self._unique_id}" - async def _async_update_hosts_info(self) -> list[HostInfo]: + async def _async_update_hosts_info(self) -> list[HostAttributes]: """Retrieve latest hosts information from the FRITZ!Box.""" try: return await self.hass.async_add_executor_job( - self.fritz_hosts.get_hosts_info + self.fritz_hosts.get_hosts_attributes ) except Exception as ex: # pylint: disable=[broad-except] if not self.hass.is_stopping: @@ -392,29 +413,6 @@ async def async_update_call_deflections( return {int(item["DeflectionId"]): item for item in items} return {} - async def _async_get_wan_access(self, ip_address: str) -> bool | None: - """Get WAN access rule for given IP address.""" - try: - wan_access = await self.hass.async_add_executor_job( - partial( - self.connection.call_action, - "X_AVM-DE_HostFilter:1", - "GetWANAccessByIP", - NewIPv4Address=ip_address, - ) - ) - return not wan_access.get("NewDisallow") - except FRITZ_EXCEPTIONS as ex: - _LOGGER.debug( - ( - "could not get WAN access rule for client device with IP '%s'," - " error: %s" - ), - ip_address, - ex, - ) - return None - def manage_device_info( self, dev_info: Device, dev_mac: str, consider_home: bool ) -> bool: @@ -462,17 +460,17 @@ async def async_scan_devices(self, now: datetime | None = None) -> None: new_device = False hosts = {} for host in await self._async_update_hosts_info(): - if not host.get("mac"): + if not host.get("MACAddress"): continue - hosts[host["mac"]] = Device( - name=host["name"], - connected=host["status"], + hosts[host["MACAddress"]] = Device( + name=host["HostName"], + connected=host["Active"], connected_to="", connection_type="", - ip_address=host["ip"], + ip_address=host["IPAddress"], ssid=None, - wan_access=None, + wan_access="granted" in host["X_AVM-DE_WANAccess"], ) if not self.fritz_status.device_has_mesh_support or ( @@ -484,8 +482,6 @@ async def async_scan_devices(self, now: datetime | None = None) -> None: ) self.mesh_role = MeshRoles.NONE for mac, info in hosts.items(): - if info.ip_address: - info.wan_access = await self._async_get_wan_access(info.ip_address) if self.manage_device_info(info, mac, consider_home): new_device = True await self.async_send_signal_device_update(new_device) @@ -535,11 +531,6 @@ async def async_scan_devices(self, now: datetime | None = None) -> None: dev_info: Device = hosts[dev_mac] - if dev_info.ip_address: - dev_info.wan_access = await self._async_get_wan_access( - dev_info.ip_address - ) - for link in interf["node_links"]: intf = mesh_intf.get(link["node_interface_1_uid"]) if intf is not None: @@ -583,7 +574,7 @@ async def async_trigger_cleanup( ) -> None: """Trigger device trackers cleanup.""" device_hosts_list = await self.hass.async_add_executor_job( - self.fritz_hosts.get_hosts_info + self.fritz_hosts.get_hosts_attributes ) entity_reg: er.EntityRegistry = er.async_get(self.hass) @@ -600,8 +591,8 @@ async def async_trigger_cleanup( device_hosts_macs = set() device_hosts_names = set() for device in device_hosts_list: - device_hosts_macs.add(device["mac"]) - device_hosts_names.add(device["name"]) + device_hosts_macs.add(device["MACAddress"]) + device_hosts_names.add(device["HostName"]) for entry in ha_entity_reg_list: if entry.original_name is None: diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index 66f4cf2b879c81..acb135d01bb9ba 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -6,7 +6,12 @@ from fritzconnection.lib.fritzhosts import FritzHosts import pytest -from .const import MOCK_FB_SERVICES, MOCK_MESH_DATA, MOCK_MODELNAME +from .const import ( + MOCK_FB_SERVICES, + MOCK_HOST_ATTRIBUTES_DATA, + MOCK_MESH_DATA, + MOCK_MODELNAME, +) LOGGER = logging.getLogger(__name__) @@ -75,6 +80,10 @@ def get_mesh_topology(self, raw=False): """Retrurn mocked mesh data.""" return MOCK_MESH_DATA + def get_hosts_attributes(self): + """Retrurn mocked host attributes data.""" + return MOCK_HOST_ATTRIBUTES_DATA + @pytest.fixture(name="fc_data") def fc_data_mock(): diff --git a/tests/components/fritz/const.py b/tests/components/fritz/const.py index 7a89aab1af1df0..c19327fbf5e303 100644 --- a/tests/components/fritz/const.py +++ b/tests/components/fritz/const.py @@ -52,27 +52,8 @@ }, }, "Hosts1": { - "GetGenericHostEntry": [ - { - "NewIPAddress": MOCK_IPS["fritz.box"], - "NewAddressSource": "Static", - "NewLeaseTimeRemaining": 0, - "NewMACAddress": MOCK_MESH_MASTER_MAC, - "NewInterfaceType": "", - "NewActive": True, - "NewHostName": "fritz.box", - }, - { - "NewIPAddress": MOCK_IPS["printer"], - "NewAddressSource": "DHCP", - "NewLeaseTimeRemaining": 0, - "NewMACAddress": "AA:BB:CC:00:11:22", - "NewInterfaceType": "Ethernet", - "NewActive": True, - "NewHostName": "printer", - }, - ], "X_AVM-DE_GetMeshListPath": {}, + "X_AVM-DE_GetHostListPath": {}, }, "LANEthernetInterfaceConfig1": { "GetStatistics": { @@ -783,6 +764,58 @@ ], } +MOCK_HOST_ATTRIBUTES_DATA = [ + { + "Index": 1, + "IPAddress": MOCK_IPS["printer"], + "MACAddress": "AA:BB:CC:00:11:22", + "Active": True, + "HostName": "printer", + "InterfaceType": "Ethernet", + "X_AVM-DE_Port": 1, + "X_AVM-DE_Speed": 1000, + "X_AVM-DE_UpdateAvailable": False, + "X_AVM-DE_UpdateSuccessful": "unknown", + "X_AVM-DE_InfoURL": None, + "X_AVM-DE_MACAddressList": None, + "X_AVM-DE_Model": None, + "X_AVM-DE_URL": f"http://{MOCK_IPS['printer']}", + "X_AVM-DE_Guest": False, + "X_AVM-DE_RequestClient": "0", + "X_AVM-DE_VPN": False, + "X_AVM-DE_WANAccess": "granted", + "X_AVM-DE_Disallow": False, + "X_AVM-DE_IsMeshable": "0", + "X_AVM-DE_Priority": "0", + "X_AVM-DE_FriendlyName": "printer", + "X_AVM-DE_FriendlyNameIsWriteable": "1", + }, + { + "Index": 2, + "IPAddress": MOCK_IPS["fritz.box"], + "MACAddress": MOCK_MESH_MASTER_MAC, + "Active": True, + "HostName": "fritz.box", + "InterfaceType": None, + "X_AVM-DE_Port": 0, + "X_AVM-DE_Speed": 0, + "X_AVM-DE_UpdateAvailable": False, + "X_AVM-DE_UpdateSuccessful": "unknown", + "X_AVM-DE_InfoURL": None, + "X_AVM-DE_MACAddressList": f"{MOCK_MESH_MASTER_MAC},{MOCK_MESH_MASTER_WIFI1_MAC}", + "X_AVM-DE_Model": None, + "X_AVM-DE_URL": f"http://{MOCK_IPS['fritz.box']}", + "X_AVM-DE_Guest": False, + "X_AVM-DE_RequestClient": "0", + "X_AVM-DE_VPN": False, + "X_AVM-DE_WANAccess": "granted", + "X_AVM-DE_Disallow": False, + "X_AVM-DE_IsMeshable": "1", + "X_AVM-DE_Priority": "0", + "X_AVM-DE_FriendlyName": "fritz.box", + "X_AVM-DE_FriendlyNameIsWriteable": "0", + }, +] MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] MOCK_DEVICE_INFO = { From c61c5a0443a3fe59b3c23b584b2cf057f7ec86d4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Jul 2023 18:20:00 +0200 Subject: [PATCH 0348/1009] Schedule `VacuumEntity` for removal in Home Assistant Core 2024.2 (#96236) --- homeassistant/components/vacuum/__init__.py | 45 +++++++++- homeassistant/components/vacuum/strings.json | 6 ++ tests/components/vacuum/test_init.py | 93 ++++++++++++++++++++ 3 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 tests/components/vacuum/test_init.py diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 2399e5d9b3b450..8285e1d76d1e66 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -1,6 +1,7 @@ """Support for vacuum cleaner robots (botvacs).""" from __future__ import annotations +import asyncio from collections.abc import Mapping from dataclasses import dataclass from datetime import timedelta @@ -22,8 +23,8 @@ STATE_ON, STATE_PAUSED, ) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -36,6 +37,7 @@ ToggleEntityDescription, ) from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass @@ -367,6 +369,45 @@ class VacuumEntityDescription(ToggleEntityDescription): class VacuumEntity(_BaseVacuum, ToggleEntity): """Representation of a vacuum cleaner robot.""" + @callback + def add_to_platform_start( + self, + hass: HomeAssistant, + platform: EntityPlatform, + parallel_updates: asyncio.Semaphore | None, + ) -> None: + """Start adding an entity to a platform.""" + super().add_to_platform_start(hass, platform, parallel_updates) + # Don't report core integrations known to still use the deprecated base class; + # we don't worry about demo and mqtt has it's own deprecation warnings. + if self.platform.platform_name in ("demo", "mqtt"): + return + ir.async_create_issue( + hass, + DOMAIN, + f"deprecated_vacuum_base_class_{self.platform.platform_name}", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + is_persistent=False, + issue_domain=self.platform.platform_name, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_vacuum_base_class", + translation_placeholders={ + "platform": self.platform.platform_name, + }, + ) + _LOGGER.warning( + ( + "%s::%s is extending the deprecated base class VacuumEntity instead of " + "StateVacuumEntity, this is not valid and will be unsupported " + "from Home Assistant 2024.2. Please report it to the author of the '%s'" + " custom integration" + ), + self.platform.platform_name, + self.__class__.__name__, + self.platform.platform_name, + ) + entity_description: VacuumEntityDescription _attr_status: str | None = None diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index a27a60bba4f30b..93ef1e8584caf5 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -28,5 +28,11 @@ "returning": "Returning to dock" } } + }, + "issues": { + "deprecated_vacuum_base_class": { + "title": "The {platform} custom integration is using deprecated vacuum feature", + "description": "The custom integration `{platform}` is extending the deprecated base class `VacuumEntity` instead of `StateVacuumEntity`.\n\nPlease report it to the author of the `{platform}` custom integration.\n\nOnce an updated version of `{platform}` is available, install it and restart Home Assistant to fix this issue." + } } } diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py new file mode 100644 index 00000000000000..eaa39bceaec3e6 --- /dev/null +++ b/tests/components/vacuum/test_init.py @@ -0,0 +1,93 @@ +"""The tests for the Vacuum entity integration.""" +from __future__ import annotations + +from collections.abc import Generator + +import pytest + +from homeassistant.components.vacuum import DOMAIN as VACUUM_DOMAIN, VacuumEntity +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from tests.common import ( + MockConfigEntry, + MockModule, + MockPlatform, + mock_config_flow, + mock_integration, + mock_platform, +) + +TEST_DOMAIN = "test" + + +class MockFlow(ConfigFlow): + """Test flow.""" + + +@pytest.fixture(autouse=True) +def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: + """Mock config flow.""" + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + + with mock_config_flow(TEST_DOMAIN, MockFlow): + yield + + +async def test_deprecated_base_class( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test warnings when adding VacuumEntity to the state machine.""" + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setup(config_entry, VACUUM_DOMAIN) + return True + + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + mock_integration( + hass, + MockModule( + TEST_DOMAIN, + async_setup_entry=async_setup_entry_init, + ), + ) + + entity1 = VacuumEntity() + entity1.entity_id = "vacuum.test1" + + async def async_setup_entry_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test stt platform via config entry.""" + async_add_entities([entity1]) + + mock_platform( + hass, + f"{TEST_DOMAIN}.{VACUUM_DOMAIN}", + MockPlatform(async_setup_entry=async_setup_entry_platform), + ) + + config_entry = MockConfigEntry(domain=TEST_DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity1.entity_id) + + assert ( + "test::VacuumEntity is extending the deprecated base class VacuumEntity" + in caplog.text + ) + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + VACUUM_DOMAIN, f"deprecated_vacuum_base_class_{TEST_DOMAIN}" + ) + assert issue.issue_domain == TEST_DOMAIN From a226b90943c736a9521a2cc142878c5a0aa7c03b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 11 Jul 2023 10:21:05 -0600 Subject: [PATCH 0349/1009] 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 5b9b443b65ee4d..72a29182169a10 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 35aa527997c1d4..9b66ea6dbe55d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -327,7 +327,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 4e3791da4a4ded..6eca0fd720d8c4 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 49b6c8ed6eab526feea6d86882eb4699f94402cb Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 11 Jul 2023 18:24:40 +0200 Subject: [PATCH 0350/1009] Fix diagnostics Sensibo (#96336) --- .../sensibo/snapshots/test_diagnostics.ambr | 27 +++++-------------- tests/components/sensibo/test_diagnostics.py | 1 - 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/tests/components/sensibo/snapshots/test_diagnostics.ambr b/tests/components/sensibo/snapshots/test_diagnostics.ambr index a3ec6952c6c673..b1cda16fb4dae5 100644 --- a/tests/components/sensibo/snapshots/test_diagnostics.ambr +++ b/tests/components/sensibo/snapshots/test_diagnostics.ambr @@ -1,20 +1,5 @@ # serializer version: 1 # name: test_diagnostics - dict({ - 'fanLevel': 'high', - 'horizontalSwing': 'stopped', - 'light': 'on', - 'mode': 'heat', - 'on': True, - 'swing': 'stopped', - 'targetTemperature': 25, - 'timestamp': dict({ - 'secondsAgo': -1, - 'time': '2022-04-30T11:23:30.019722Z', - }), - }) -# --- -# name: test_diagnostics.1 dict({ 'modes': dict({ 'auto': dict({ @@ -206,28 +191,28 @@ }), }) # --- -# name: test_diagnostics.2 +# name: test_diagnostics.1 dict({ 'low': 'low', 'medium': 'medium', 'quiet': 'quiet', }) # --- -# name: test_diagnostics.3 +# name: test_diagnostics.2 dict({ 'fixedmiddletop': 'fixedMiddleTop', 'fixedtop': 'fixedTop', 'stopped': 'stopped', }) # --- -# name: test_diagnostics.4 +# name: test_diagnostics.3 dict({ 'fixedcenterleft': 'fixedCenterLeft', 'fixedleft': 'fixedLeft', 'stopped': 'stopped', }) # --- -# name: test_diagnostics.5 +# name: test_diagnostics.4 dict({ 'fanlevel': 'low', 'horizontalswing': 'stopped', @@ -239,7 +224,7 @@ 'temperatureunit': 'c', }) # --- -# name: test_diagnostics.6 +# name: test_diagnostics.5 dict({ 'fanlevel': 'high', 'horizontalswing': 'stopped', @@ -251,7 +236,7 @@ 'temperatureunit': 'c', }) # --- -# name: test_diagnostics.7 +# name: test_diagnostics.6 dict({ }) # --- diff --git a/tests/components/sensibo/test_diagnostics.py b/tests/components/sensibo/test_diagnostics.py index c3e1625d623650..bc35b7fdd57852 100644 --- a/tests/components/sensibo/test_diagnostics.py +++ b/tests/components/sensibo/test_diagnostics.py @@ -21,7 +21,6 @@ async def test_diagnostics( diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) - assert diag["ABC999111"]["ac_states"] == snapshot assert diag["ABC999111"]["full_capabilities"] == snapshot assert diag["ABC999111"]["fan_modes_translated"] == snapshot assert diag["ABC999111"]["swing_modes_translated"] == snapshot From 50442c56884889d42cb13c4f07220804e2c23e4f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 11 Jul 2023 18:31:32 +0200 Subject: [PATCH 0351/1009] Speedup tests command_line integration (#96349) --- .../command_line/test_binary_sensor.py | 29 +++++++++------- tests/components/command_line/test_cover.py | 32 ++++++++++++----- tests/components/command_line/test_sensor.py | 29 +++++++++------- tests/components/command_line/test_switch.py | 34 +++++++++++++------ 4 files changed, 81 insertions(+), 43 deletions(-) diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index 910288d6920cae..50971219f48833 100644 --- a/tests/components/command_line/test_binary_sensor.py +++ b/tests/components/command_line/test_binary_sensor.py @@ -206,16 +206,19 @@ async def test_updating_to_often( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test handling updating when command already running.""" + + wait_till_event = asyncio.Event() + wait_till_event.set() called = [] class MockCommandBinarySensor(CommandBinarySensor): - """Mock entity that updates slow.""" + """Mock entity that updates.""" async def _async_update(self) -> None: - """Update slow.""" + """Update the entity.""" called.append(1) - # Add waiting time - await asyncio.sleep(1) + # Wait till event is set + await wait_till_event.wait() with patch( "homeassistant.components.command_line.binary_sensor.CommandBinarySensor", @@ -232,7 +235,7 @@ async def _async_update(self) -> None: "command": "echo 1", "payload_on": "1", "payload_off": "0", - "scan_interval": 0.1, + "scan_interval": 10, } } ] @@ -241,24 +244,26 @@ async def _async_update(self) -> None: await hass.async_block_till_done() assert called + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=15)) + wait_till_event.set() + asyncio.wait(0) assert ( "Updating Command Line Binary Sensor Test took longer than the scheduled update interval" not in caplog.text ) - called.clear() - caplog.clear() - async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1)) - await hass.async_block_till_done() + # Simulate update takes too long + wait_till_event.clear() + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10)) + await asyncio.sleep(0) + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10)) + wait_till_event.set() - assert called assert ( "Updating Command Line Binary Sensor Test took longer than the scheduled update interval" in caplog.text ) - await asyncio.sleep(0.2) - async def test_updating_manually( hass: HomeAssistant, caplog: pytest.LogCaptureFixture diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index d4114f9bbbd787..64fa2a60719453 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -293,16 +293,19 @@ async def test_updating_to_often( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test handling updating when command already running.""" + called = [] + wait_till_event = asyncio.Event() + wait_till_event.set() class MockCommandCover(CommandCover): - """Mock entity that updates slow.""" + """Mock entity that updates.""" async def _async_update(self) -> None: - """Update slow.""" + """Update the entity.""" called.append(1) # Add waiting time - await asyncio.sleep(1) + await wait_till_event.wait() with patch( "homeassistant.components.command_line.cover.CommandCover", @@ -318,7 +321,7 @@ async def _async_update(self) -> None: "command_state": "echo 1", "value_template": "{{ value }}", "name": "Test", - "scan_interval": 0.1, + "scan_interval": 10, } } ] @@ -331,20 +334,31 @@ async def _async_update(self) -> None: "Updating Command Line Cover Test took longer than the scheduled update interval" not in caplog.text ) + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=11)) + await hass.async_block_till_done() + assert called called.clear() - caplog.clear() - async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1)) - await hass.async_block_till_done() + assert ( + "Updating Command Line Cover Test took longer than the scheduled update interval" + not in caplog.text + ) + # Simulate update takes too long + wait_till_event.clear() + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10)) + await asyncio.sleep(0) + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10)) + wait_till_event.set() + + # Finish processing update + await hass.async_block_till_done() assert called assert ( "Updating Command Line Cover Test took longer than the scheduled update interval" in caplog.text ) - await asyncio.sleep(0.2) - async def test_updating_manually( hass: HomeAssistant, caplog: pytest.LogCaptureFixture diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index af7bf3222a14bd..a0f8f2cdf84935 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -543,16 +543,18 @@ async def test_updating_to_often( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test handling updating when command already running.""" + wait_till_event = asyncio.Event() + wait_till_event.set() called = [] class MockCommandSensor(CommandSensor): - """Mock entity that updates slow.""" + """Mock entity that updates.""" async def _async_update(self) -> None: - """Update slow.""" + """Update entity.""" called.append(1) - # Add waiting time - await asyncio.sleep(1) + # Wait till event is set + await wait_till_event.wait() with patch( "homeassistant.components.command_line.sensor.CommandSensor", @@ -567,7 +569,7 @@ async def _async_update(self) -> None: "sensor": { "name": "Test", "command": "echo 1", - "scan_interval": 0.1, + "scan_interval": 10, } } ] @@ -576,24 +578,27 @@ async def _async_update(self) -> None: await hass.async_block_till_done() assert called + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=15)) + wait_till_event.set() + asyncio.wait(0) + assert ( "Updating Command Line Sensor Test took longer than the scheduled update interval" not in caplog.text ) - called.clear() - caplog.clear() - async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1)) - await hass.async_block_till_done() + # Simulate update takes too long + wait_till_event.clear() + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10)) + await asyncio.sleep(0) + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10)) + wait_till_event.set() - assert called assert ( "Updating Command Line Sensor Test took longer than the scheduled update interval" in caplog.text ) - await asyncio.sleep(0.2) - async def test_updating_manually( hass: HomeAssistant, caplog: pytest.LogCaptureFixture diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index 12a037f0dd115e..09e8c47d708032 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -650,16 +650,19 @@ async def test_updating_to_often( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test handling updating when command already running.""" + called = [] + wait_till_event = asyncio.Event() + wait_till_event.set() class MockCommandSwitch(CommandSwitch): - """Mock entity that updates slow.""" + """Mock entity that updates.""" async def _async_update(self) -> None: - """Update slow.""" + """Update entity.""" called.append(1) - # Add waiting time - await asyncio.sleep(1) + # Wait till event is set + await wait_till_event.wait() with patch( "homeassistant.components.command_line.switch.CommandSwitch", @@ -676,7 +679,7 @@ async def _async_update(self) -> None: "command_on": "echo 2", "command_off": "echo 3", "name": "Test", - "scan_interval": 0.1, + "scan_interval": 10, } } ] @@ -689,20 +692,31 @@ async def _async_update(self) -> None: "Updating Command Line Switch Test took longer than the scheduled update interval" not in caplog.text ) + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=11)) + await hass.async_block_till_done() + assert called called.clear() - caplog.clear() - async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=1)) - await hass.async_block_till_done() + assert ( + "Updating Command Line Switch Test took longer than the scheduled update interval" + not in caplog.text + ) + + # Simulate update takes too long + wait_till_event.clear() + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10)) + await asyncio.sleep(0) + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10)) + wait_till_event.set() + # Finish processing update + await hass.async_block_till_done() assert called assert ( "Updating Command Line Switch Test took longer than the scheduled update interval" in caplog.text ) - await asyncio.sleep(0.2) - async def test_updating_manually( hass: HomeAssistant, caplog: pytest.LogCaptureFixture From f25d5a157a9d38e605da290ac45d88bdf1275a8d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 19:33:07 +0200 Subject: [PATCH 0352/1009] Fix service schema to allow for services without any fields/properties (#96346) --- homeassistant/helpers/service.py | 4 ++-- script/hassfest/services.py | 21 ++++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 1a418a68fd1c9d..946340ea69cae7 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -633,8 +633,8 @@ async def async_get_all_descriptions( # service.async_set_service_schema for the dynamic # service - yaml_description = domain_yaml.get( # type: ignore[union-attr] - service_name, {} + yaml_description = ( + domain_yaml.get(service_name) or {} # type: ignore[union-attr] ) # Don't warn for missing services, because it triggers false diff --git a/script/hassfest/services.py b/script/hassfest/services.py index 2f8e20939db4bb..b3f59ab66a3813 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -46,13 +46,18 @@ def exists(value: Any) -> Any: } ) -SERVICE_SCHEMA = vol.Schema( - { - vol.Optional("description"): str, - vol.Optional("name"): str, - vol.Optional("target"): vol.Any(selector.TargetSelector.CONFIG_SCHEMA, None), - vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}), - } +SERVICE_SCHEMA = vol.Any( + vol.Schema( + { + vol.Optional("description"): str, + vol.Optional("name"): str, + vol.Optional("target"): vol.Any( + selector.TargetSelector.CONFIG_SCHEMA, None + ), + vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}), + } + ), + None, ) SERVICES_SCHEMA = vol.Schema({cv.slug: SERVICE_SCHEMA}) @@ -116,6 +121,8 @@ def validate_services(config: Config, integration: Integration) -> None: # For each service in the integration, check if the description if set, # if not, check if it's in the strings file. If not, add an error. for service_name, service_schema in services.items(): + if service_schema is None: + continue if "name" not in service_schema: try: strings["services"][service_name]["name"] From 2f6826dbe36cab4da93c613006abe2f4f70452e7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 11 Jul 2023 19:40:15 +0200 Subject: [PATCH 0353/1009] Use DeviceInfo object s-x (#96281) * Use DeviceInfo object o-x * Use DeviceInfo object --- .../components/sfr_box/binary_sensor.py | 5 ++++- homeassistant/components/sfr_box/button.py | 5 ++++- homeassistant/components/sfr_box/sensor.py | 5 ++++- homeassistant/components/shelly/climate.py | 4 +++- .../components/traccar/device_tracker.py | 8 ++++++-- homeassistant/components/venstar/__init__.py | 17 +++++++++-------- homeassistant/components/vulcan/calendar.py | 18 +++++++++--------- homeassistant/components/xiaomi_miio/sensor.py | 5 ++++- 8 files changed, 43 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/sfr_box/binary_sensor.py b/homeassistant/components/sfr_box/binary_sensor.py index e4d41fb0cb853e..9e8201bc1b58c8 100644 --- a/homeassistant/components/sfr_box/binary_sensor.py +++ b/homeassistant/components/sfr_box/binary_sensor.py @@ -15,6 +15,7 @@ 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.helpers.update_coordinator import CoordinatorEntity @@ -112,7 +113,9 @@ def __init__( self._attr_unique_id = ( f"{system_info.mac_addr}_{coordinator.name}_{description.key}" ) - self._attr_device_info = {"identifiers": {(DOMAIN, system_info.mac_addr)}} + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, system_info.mac_addr)}, + ) @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/sfr_box/button.py b/homeassistant/components/sfr_box/button.py index f6741da1398413..ab987944acc116 100644 --- a/homeassistant/components/sfr_box/button.py +++ b/homeassistant/components/sfr_box/button.py @@ -19,6 +19,7 @@ from homeassistant.const import EntityCategory 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 @@ -100,7 +101,9 @@ def __init__( self.entity_description = description self._box = box self._attr_unique_id = f"{system_info.mac_addr}_{description.key}" - self._attr_device_info = {"identifiers": {(DOMAIN, system_info.mac_addr)}} + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, system_info.mac_addr)}, + ) @with_error_wrapping async def async_press(self) -> None: diff --git a/homeassistant/components/sfr_box/sensor.py b/homeassistant/components/sfr_box/sensor.py index 19512f4382157d..fa754bbe62f1dd 100644 --- a/homeassistant/components/sfr_box/sensor.py +++ b/homeassistant/components/sfr_box/sensor.py @@ -20,6 +20,7 @@ UnitOfTemperature, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -252,7 +253,9 @@ def __init__( self._attr_unique_id = ( f"{system_info.mac_addr}_{coordinator.name}_{description.key}" ) - self._attr_device_info = {"identifiers": {(DOMAIN, system_info.mac_addr)}} + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, system_info.mac_addr)}, + ) @property def native_value(self) -> StateType: diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 6cd4c19c638d90..4cc5cacbde39a0 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -254,7 +254,9 @@ def preset_modes(self) -> list[str]: @property def device_info(self) -> DeviceInfo: """Device info.""" - return {"connections": {(CONNECTION_NETWORK_MAC, self.coordinator.mac)}} + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self.coordinator.mac)}, + ) def _check_is_off(self) -> bool: """Return if valve is off or on.""" diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 9ed7922fa19f66..a22b8a993f1b9c 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -39,6 +39,7 @@ from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity @@ -411,9 +412,12 @@ def unique_id(self): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - return {"name": self._name, "identifiers": {(DOMAIN, self._unique_id)}} + return DeviceInfo( + name=self._name, + identifiers={(DOMAIN, self._unique_id)}, + ) @property def source_type(self) -> SourceType: diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index 48760a8bfc0811..4b2d29558324c4 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -18,6 +18,7 @@ ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import update_coordinator +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import _LOGGER, DOMAIN, VENSTAR_SLEEP, VENSTAR_TIMEOUT @@ -143,12 +144,12 @@ def _handle_coordinator_update(self) -> None: self.async_write_ha_state() @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device information for this entity.""" - return { - "identifiers": {(DOMAIN, self._config.entry_id)}, - "name": self._client.name, - "manufacturer": "Venstar", - "model": f"{self._client.model}-{self._client.get_type()}", - "sw_version": self._client.get_api_ver(), - } + return DeviceInfo( + identifiers={(DOMAIN, self._config.entry_id)}, + name=self._client.name, + manufacturer="Venstar", + model=f"{self._client.model}-{self._client.get_type()}", + sw_version=self._client.get_api_ver(), + ) diff --git a/homeassistant/components/vulcan/calendar.py b/homeassistant/components/vulcan/calendar.py index d9182bb9905679..debf1f4ea0dfeb 100644 --- a/homeassistant/components/vulcan/calendar.py +++ b/homeassistant/components/vulcan/calendar.py @@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import generate_entity_id +from homeassistant.helpers.entity import DeviceInfo, generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN @@ -64,19 +64,19 @@ def __init__(self, client, data, entity_id) -> None: self._unique_id = f"vulcan_calendar_{self.student_info['id']}" self._attr_name = f"Vulcan calendar - {self.student_info['full_name']}" self._attr_unique_id = f"vulcan_calendar_{self.student_info['id']}" - self._attr_device_info = { - "identifiers": {(DOMAIN, f"calendar_{self.student_info['id']}")}, - "entry_type": DeviceEntryType.SERVICE, - "name": f"{self.student_info['full_name']}: Calendar", - "model": ( + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"calendar_{self.student_info['id']}")}, + entry_type=DeviceEntryType.SERVICE, + name=f"{self.student_info['full_name']}: Calendar", + model=( f"{self.student_info['full_name']} -" f" {self.student_info['class']} {self.student_info['school']}" ), - "manufacturer": "Uonet +", - "configuration_url": ( + manufacturer="Uonet +", + configuration_url=( f"https://uonetplus.vulcan.net.pl/{self.student_info['symbol']}" ), - } + ) @property def event(self) -> CalendarEvent | None: diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 249774519d0832..b28f06eb97d6e5 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -42,6 +42,7 @@ UnitOfVolume, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util @@ -997,7 +998,9 @@ def __init__(self, gateway_device, gateway_name, gateway_device_id, description) """Initialize the entity.""" self._attr_name = f"{gateway_name} {description.name}" self._attr_unique_id = f"{gateway_device_id}-{description.key}" - self._attr_device_info = {"identifiers": {(DOMAIN, gateway_device_id)}} + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, gateway_device_id)}, + ) self._gateway = gateway_device self.entity_description = description self._available = False From a04aaf10a59049ce13f27342f1e84a2c551856ab Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 11 Jul 2023 19:41:55 +0200 Subject: [PATCH 0354/1009] Use DeviceInfo object d-o (#96280) --- homeassistant/components/demo/button.py | 9 +++++---- homeassistant/components/demo/climate.py | 9 +++++---- homeassistant/components/elmax/common.py | 17 +++++++++-------- homeassistant/components/kmtronic/switch.py | 13 +++++++------ homeassistant/components/lcn/__init__.py | 14 +++++++------- .../components/lutron_caseta/__init__.py | 16 ++++++++-------- homeassistant/components/overkiz/entity.py | 6 +++--- homeassistant/components/overkiz/sensor.py | 6 +++--- 8 files changed, 47 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/demo/button.py b/homeassistant/components/demo/button.py index f7a653e17797a2..02f3f584003a26 100644 --- a/homeassistant/components/demo/button.py +++ b/homeassistant/components/demo/button.py @@ -5,6 +5,7 @@ from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN @@ -43,10 +44,10 @@ def __init__( """Initialize the Demo button entity.""" self._attr_unique_id = unique_id self._attr_icon = icon - self._attr_device_info = { - "identifiers": {(DOMAIN, unique_id)}, - "name": device_name, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=device_name, + ) async def async_press(self) -> None: """Send out a persistent notification.""" diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index 340a4b306cba49..407860526ae797 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN @@ -152,10 +153,10 @@ def __init__( self._swing_modes = ["auto", "1", "2", "3", "off"] self._target_temperature_high = target_temp_high self._target_temperature_low = target_temp_low - self._attr_device_info = { - "identifiers": {(DOMAIN, unique_id)}, - "name": device_name, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=device_name, + ) @property def unique_id(self) -> str: diff --git a/homeassistant/components/elmax/common.py b/homeassistant/components/elmax/common.py index 5334da23125149..b0f51740b04ef1 100644 --- a/homeassistant/components/elmax/common.py +++ b/homeassistant/components/elmax/common.py @@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -168,17 +169,17 @@ def name(self) -> str | None: return self._device.name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" - return { - "identifiers": {(DOMAIN, self._panel.hash)}, - "name": self._panel.get_name_by_user( + return DeviceInfo( + identifiers={(DOMAIN, self._panel.hash)}, + name=self._panel.get_name_by_user( self.coordinator.http_client.get_authenticated_username() ), - "manufacturer": "Elmax", - "model": self._panel_version, - "sw_version": self._panel_version, - } + manufacturer="Elmax", + model=self._panel_version, + sw_version=self._panel_version, + ) @property def available(self) -> bool: diff --git a/homeassistant/components/kmtronic/switch.py b/homeassistant/components/kmtronic/switch.py index 860e5bf832eab2..ed54315de908f8 100644 --- a/homeassistant/components/kmtronic/switch.py +++ b/homeassistant/components/kmtronic/switch.py @@ -5,6 +5,7 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -38,12 +39,12 @@ def __init__(self, hub, coordinator, relay, reverse, config_entry_id): self._reverse = reverse hostname = urllib.parse.urlsplit(hub.host).hostname - self._attr_device_info = { - "identifiers": {(DOMAIN, config_entry_id)}, - "name": f"Controller {hostname}", - "manufacturer": MANUFACTURER, - "configuration_url": hub.host, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, config_entry_id)}, + name=f"Controller {hostname}", + manufacturer=MANUFACTURER, + configuration_url=hub.host, + ) self._attr_name = f"Relay{relay.id}" self._attr_unique_id = f"{config_entry_id}_relay{relay.id}" diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 2e1185fd69215d..72b66bc5cf1929 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -276,16 +276,16 @@ def device_info(self) -> DeviceInfo | None: f" ({get_device_model(self.config[CONF_DOMAIN], self.config[CONF_DOMAIN_DATA])})" ) - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": f"{address}.{self.config[CONF_RESOURCE]}", - "model": model, - "manufacturer": "Issendorff", - "via_device": ( + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + name=f"{address}.{self.config[CONF_RESOURCE]}", + model=model, + manufacturer="Issendorff", + via_device=( DOMAIN, generate_unique_id(self.entry_id, self.config[CONF_ADDRESS]), ), - } + ) async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 64abf6e54c4e28..6d20f29905d243 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -219,14 +219,14 @@ def _async_register_bridge_device( """Register the bridge device in the device registry.""" device_registry = dr.async_get(hass) - device_args: DeviceInfo = { - "name": bridge_device["name"], - "manufacturer": MANUFACTURER, - "identifiers": {(DOMAIN, bridge_device["serial"])}, - "model": f"{bridge_device['model']} ({bridge_device['type']})", - "via_device": (DOMAIN, bridge_device["serial"]), - "configuration_url": "https://device-login.lutron.com", - } + device_args = DeviceInfo( + name=bridge_device["name"], + manufacturer=MANUFACTURER, + identifiers={(DOMAIN, bridge_device["serial"])}, + model=f"{bridge_device['model']} ({bridge_device['type']})", + via_device=(DOMAIN, bridge_device["serial"]), + configuration_url="https://device-login.lutron.com", + ) area = _area_name_from_id(bridge.areas, bridge_device["area"]) if area != UNASSIGNED_AREA: diff --git a/homeassistant/components/overkiz/entity.py b/homeassistant/components/overkiz/entity.py index 16ea12a5d9669d..fa531410e33d33 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -60,9 +60,9 @@ def generate_device_info(self) -> DeviceInfo: if self.is_sub_device: # Only return the url of the base device, to inherit device name # and model from parent device. - return { - "identifiers": {(DOMAIN, self.executor.base_device_url)}, - } + return DeviceInfo( + identifiers={(DOMAIN, self.executor.base_device_url)}, + ) manufacturer = ( self.executor.select_attribute(OverkizAttribute.CORE_MANUFACTURER) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index 9aca0850b053eb..c841e3b0e3644a 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -527,6 +527,6 @@ def device_info(self) -> DeviceInfo: # By default this sensor will be listed at a virtual HomekitStack device, # but it makes more sense to show this at the gateway device # in the entity registry. - return { - "identifiers": {(DOMAIN, self.executor.get_gateway_id())}, - } + return DeviceInfo( + identifiers={(DOMAIN, self.executor.get_gateway_id())}, + ) From a0e20c6c6be79ee1f7388507f0c3a2328b004edf Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 11 Jul 2023 19:42:59 +0200 Subject: [PATCH 0355/1009] 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 69b3d5db6f7ed1..00f0e0f518bcfa 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 6303bc58131926..2ae3442278e609 100644 --- a/homeassistant/components/reolink/select.py +++ b/homeassistant/components/reolink/select.py @@ -49,7 +49,7 @@ class ReolinkSelectEntityDescription( 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 7dc89ddbaf3f55..53f2e57b97bac5 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -62,7 +62,9 @@ "state": { "off": "[%key:common::state::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 9b66ea6dbe55d8..3aaab1edf2e9a6 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 6eca0fd720d8c4..bddb261e3eb98d 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 85ed347ff30587c851ec3000ee83489757ef10da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Jul 2023 08:08:01 -1000 Subject: [PATCH 0356/1009] Bump aioesphomeapi to 15.1.6 (#96297) * Bump aioesphomeapi to 15.1.5 changelog: https://github.com/esphome/aioesphomeapi/compare/v15.1.4...v15.1.5 - reduce traffic - improve error reporting * 6 --- 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 63bd2ffc0814f4..764b12cedc2438 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.1.4", + "aioesphomeapi==15.1.6", "bluetooth-data-tools==1.3.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 3aaab1edf2e9a6..17d45dcb24a9e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==15.1.4 +aioesphomeapi==15.1.6 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bddb261e3eb98d..746b5cf643e665 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.1.4 +aioesphomeapi==15.1.6 # homeassistant.components.flo aioflo==2021.11.0 From 7e686db4be795b7f250ebb713040e8442e194a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 11 Jul 2023 20:09:20 +0200 Subject: [PATCH 0357/1009] Tibber upgrade lib, improve reconnect issues (#96276) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tibber upgrade lib, improve recoonect issues Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 1b6c5e3045ac62..c668430914fae7 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["tibber"], "quality_scale": "silver", - "requirements": ["pyTibber==0.27.2"] + "requirements": ["pyTibber==0.28.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 17d45dcb24a9e4..e95d879d72f3d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1533,7 +1533,7 @@ pyRFXtrx==0.30.1 pySDCP==1 # homeassistant.components.tibber -pyTibber==0.27.2 +pyTibber==0.28.0 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 746b5cf643e665..e954b5a447ced6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1148,7 +1148,7 @@ pyElectra==1.2.0 pyRFXtrx==0.30.1 # homeassistant.components.tibber -pyTibber==0.27.2 +pyTibber==0.28.0 # homeassistant.components.dlink pyW215==0.7.0 From b6e83be6f9b1d6f5ebbc71f2e7b33e16893288d8 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 0358/1009] 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 293822987c30ae..7694a85b8ede4f 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 e95d879d72f3d8..f6d8a03cbe873f 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 e954b5a447ced6..cd72a6df01b4a6 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 7346f1e5bcbe27..2df6c2be5db52e 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 72f080bf8b55948f39d55c152d25c9b3556ee034 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 11 Jul 2023 20:10:14 +0200 Subject: [PATCH 0359/1009] 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 df191afb859fc5..0c85705a2a6495 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 b106ca79837bbdeb8ee7d414fde0773637ae28fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Jul 2023 08:11:51 -1000 Subject: [PATCH 0360/1009] 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 9ed7ad7123d394..5011439c7780e9 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -35,7 +35,7 @@ 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" @@ -406,7 +406,9 @@ async def _retrieve_encryption_key_from_dashboard(self) -> bool: """ 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 35e9cf74555968..c9d74f0b30c62a 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -143,7 +143,14 @@ async def on_hass_stop(_: Event) -> None: @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 28d411be9391ad..fc37e1e51ee511 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -1374,3 +1374,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 7d2559e6a5ec687e3201207e719a8905a01d65ee Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 11 Jul 2023 20:12:16 +0200 Subject: [PATCH 0361/1009] 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 5d0ea67f31df76..75a2644791e03d 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 c7daf0ec1e1d58..1487c6a7b42df5 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 e74555f8db909e..9740e427e9c823 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 2257e7454a564fd80c265c3c9e35fdc5c8044eb0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Jul 2023 20:15:16 +0200 Subject: [PATCH 0362/1009] Remove unreferenced issues (#96264) * Remove unreferenced issues * Remove outdated tests --- homeassistant/components/guardian/__init__.py | 40 ------------- .../components/guardian/strings.json | 13 ---- .../components/litterrobot/strings.json | 6 -- homeassistant/components/openuv/strings.json | 10 ---- .../components/unifiprotect/strings.json | 4 -- tests/components/unifiprotect/test_repairs.py | 60 +------------------ 6 files changed, 1 insertion(+), 132 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index f587ef2e54ca38..ec8bd818d38874 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -25,7 +25,6 @@ from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo, EntityDescription -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -107,45 +106,6 @@ def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) raise ValueError(f"No config entry for device ID: {device_id}") -@callback -def async_log_deprecated_service_call( - hass: HomeAssistant, - call: ServiceCall, - alternate_service: str, - alternate_target: str, - breaks_in_ha_version: str, -) -> None: - """Log a warning about a deprecated service call.""" - deprecated_service = f"{call.domain}.{call.service}" - - async_create_issue( - hass, - DOMAIN, - f"deprecated_service_{deprecated_service}", - breaks_in_ha_version=breaks_in_ha_version, - is_fixable=True, - is_persistent=True, - severity=IssueSeverity.WARNING, - translation_key="deprecated_service", - translation_placeholders={ - "alternate_service": alternate_service, - "alternate_target": alternate_target, - "deprecated_service": deprecated_service, - }, - ) - - LOGGER.warning( - ( - 'The "%s" service is deprecated and will be removed in %s; use the "%s" ' - 'service and pass it a target entity ID of "%s"' - ), - deprecated_service, - breaks_in_ha_version, - alternate_service, - alternate_target, - ) - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elexa Guardian from a config entry.""" client = Client(entry.data[CONF_IP_ADDRESS], port=entry.data[CONF_PORT]) diff --git a/homeassistant/components/guardian/strings.json b/homeassistant/components/guardian/strings.json index dc3e6f4c17df50..ec2ad8d77cca71 100644 --- a/homeassistant/components/guardian/strings.json +++ b/homeassistant/components/guardian/strings.json @@ -18,19 +18,6 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } }, - "issues": { - "deprecated_service": { - "title": "The {deprecated_service} service will be removed", - "fix_flow": { - "step": { - "confirm": { - "title": "The {deprecated_service} service will be removed", - "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`." - } - } - } - } - }, "entity": { "binary_sensor": { "leak": { diff --git a/homeassistant/components/litterrobot/strings.json b/homeassistant/components/litterrobot/strings.json index 00a8a6122db998..5a6a0bf6998f38 100644 --- a/homeassistant/components/litterrobot/strings.json +++ b/homeassistant/components/litterrobot/strings.json @@ -25,12 +25,6 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, - "issues": { - "migrated_attributes": { - "title": "Litter-Robot attributes are now their own sensors", - "description": "The vacuum entity attributes are now available as diagnostic sensors.\n\nPlease adjust any automations or scripts you may have that use these attributes." - } - }, "entity": { "binary_sensor": { "sleeping": { diff --git a/homeassistant/components/openuv/strings.json b/homeassistant/components/openuv/strings.json index 4aa29d11fcf26c..2534622975c5fc 100644 --- a/homeassistant/components/openuv/strings.json +++ b/homeassistant/components/openuv/strings.json @@ -37,16 +37,6 @@ } } }, - "issues": { - "deprecated_service_multiple_alternate_targets": { - "title": "The {deprecated_service} service is being removed", - "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with one of these entity IDs as the target: `{alternate_targets}`." - }, - "deprecated_service_single_alternate_target": { - "title": "The {deprecated_service} service is being removed", - "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with `{alternate_targets}` as the target." - } - }, "entity": { "binary_sensor": { "protection_window": { diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index fc50e8141a1660..b7be12233df742 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -75,10 +75,6 @@ "ea_setup_failed": { "title": "Setup error using Early Access version", "description": "You are using v{version} of UniFi Protect which is an Early Access version. An unrecoverable error occurred while trying to load the integration. Please [downgrade to a stable version](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) of UniFi Protect to continue using the integration.\n\nError: {error}" - }, - "deprecate_smart_sensor": { - "title": "Smart Detection Sensor Deprecated", - "description": "The unified \"Detected Object\" sensor for smart detections is now deprecated. It has been replaced with individual smart detection binary sensors for each smart detection type.\n\nBelow are the detected automations or scripts that use one or more of the deprecated entities:\n{items}\nThe above list may be incomplete and it does not include any template usages inside of dashboards. Please update any templates, automations or scripts accordingly." } }, "entity": { diff --git a/tests/components/unifiprotect/test_repairs.py b/tests/components/unifiprotect/test_repairs.py index b9fa9bc57b8213..f68ebd9c8c65f8 100644 --- a/tests/components/unifiprotect/test_repairs.py +++ b/tests/components/unifiprotect/test_repairs.py @@ -5,7 +5,7 @@ from http import HTTPStatus from unittest.mock import Mock -from pyunifiprotect.data import Camera, Version +from pyunifiprotect.data import Version from homeassistant.components.repairs.issue_handler import ( async_process_repairs_platforms, @@ -15,9 +15,7 @@ RepairsFlowResourceView, ) from homeassistant.components.unifiprotect.const import DOMAIN -from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er from .utils import MockUFPFixture, init_entry @@ -127,59 +125,3 @@ async def test_ea_warning_fix( data = await resp.json() assert data["type"] == "create_entry" - - -async def test_deprecate_smart_default( - hass: HomeAssistant, - ufp: MockUFPFixture, - hass_ws_client: WebSocketGenerator, - doorbell: Camera, -) -> None: - """Test Deprecate Sensor repair does not exist by default (new installs).""" - - await init_entry(hass, ufp, [doorbell]) - - await async_process_repairs_platforms(hass) - ws_client = await hass_ws_client(hass) - - await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) - msg = await ws_client.receive_json() - - assert msg["success"] - issue = None - for i in msg["result"]["issues"]: - if i["issue_id"] == "deprecate_smart_sensor": - issue = i - assert issue is None - - -async def test_deprecate_smart_no_automations( - hass: HomeAssistant, - ufp: MockUFPFixture, - hass_ws_client: WebSocketGenerator, - doorbell: Camera, -) -> None: - """Test Deprecate Sensor repair exists for existing installs.""" - - registry = er.async_get(hass) - registry.async_get_or_create( - Platform.SENSOR, - DOMAIN, - f"{doorbell.mac}_detected_object", - config_entry=ufp.entry, - ) - - await init_entry(hass, ufp, [doorbell]) - - await async_process_repairs_platforms(hass) - ws_client = await hass_ws_client(hass) - - await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) - msg = await ws_client.receive_json() - - assert msg["success"] - issue = None - for i in msg["result"]["issues"]: - if i["issue_id"] == "deprecate_smart_sensor": - issue = i - assert issue is None From a7edf0a6081edca28c4f027ac592ab5fb4ef2970 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 11 Jul 2023 20:16:24 +0200 Subject: [PATCH 0363/1009] Add entity translations to Ukraine Alarm (#96260) * Add entity translations to Ukraine Alarm * Add entity translations to Ukraine Alarm --- .../components/ukraine_alarm/binary_sensor.py | 14 ++++++------ .../components/ukraine_alarm/strings.json | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ukraine_alarm/binary_sensor.py b/homeassistant/components/ukraine_alarm/binary_sensor.py index 3cfe79ef5fb23c..eb83fe490e7029 100644 --- a/homeassistant/components/ukraine_alarm/binary_sensor.py +++ b/homeassistant/components/ukraine_alarm/binary_sensor.py @@ -30,36 +30,36 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( BinarySensorEntityDescription( key=ALERT_TYPE_UNKNOWN, - name="Unknown", + translation_key="unknown", device_class=BinarySensorDeviceClass.SAFETY, ), BinarySensorEntityDescription( key=ALERT_TYPE_AIR, - name="Air", + translation_key="air", device_class=BinarySensorDeviceClass.SAFETY, icon="mdi:cloud", ), BinarySensorEntityDescription( key=ALERT_TYPE_URBAN_FIGHTS, - name="Urban Fights", + translation_key="urban_fights", device_class=BinarySensorDeviceClass.SAFETY, icon="mdi:pistol", ), BinarySensorEntityDescription( key=ALERT_TYPE_ARTILLERY, - name="Artillery", + translation_key="artillery", device_class=BinarySensorDeviceClass.SAFETY, icon="mdi:tank", ), BinarySensorEntityDescription( key=ALERT_TYPE_CHEMICAL, - name="Chemical", + translation_key="chemical", device_class=BinarySensorDeviceClass.SAFETY, icon="mdi:chemical-weapon", ), BinarySensorEntityDescription( key=ALERT_TYPE_NUCLEAR, - name="Nuclear", + translation_key="nuclear", device_class=BinarySensorDeviceClass.SAFETY, icon="mdi:nuke", ), @@ -92,6 +92,7 @@ class UkraineAlarmSensor( """Class for a Ukraine Alarm binary sensor.""" _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True def __init__( self, @@ -105,7 +106,6 @@ def __init__( self.entity_description = description - self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{unique_id}-{description.key}".lower() self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, diff --git a/homeassistant/components/ukraine_alarm/strings.json b/homeassistant/components/ukraine_alarm/strings.json index 6831d66adb3657..73a2657065ebf9 100644 --- a/homeassistant/components/ukraine_alarm/strings.json +++ b/homeassistant/components/ukraine_alarm/strings.json @@ -28,5 +28,27 @@ "description": "If you want to monitor not only state and district, choose its specific community" } } + }, + "entity": { + "binary_sensor": { + "unknown": { + "name": "Unknown" + }, + "air": { + "name": "Air" + }, + "urban_fights": { + "name": "Urban fights" + }, + "artillery": { + "name": "Artillery" + }, + "chemical": { + "name": "Chemical" + }, + "nuclear": { + "name": "Nuclear" + } + } } } From e9f76ed3d313a6cb905a96b981d8018b85e29bba Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 20:16:43 +0200 Subject: [PATCH 0364/1009] Update orjson to 3.9.2 (#96257) --- 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 16b25353183347..b82a7315648f40 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ janus==1.0.0 Jinja2==3.1.2 lru-dict==1.2.0 mutagen==1.46.0 -orjson==3.9.1 +orjson==3.9.2 paho-mqtt==1.6.1 Pillow==10.0.0 pip>=21.3.1,<23.2 diff --git a/pyproject.toml b/pyproject.toml index 8256ce2d06060d..f746797277308a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ dependencies = [ "cryptography==41.0.1", # pyOpenSSL 23.2.0 is required to work with cryptography 41+ "pyOpenSSL==23.2.0", - "orjson==3.9.1", + "orjson==3.9.2", "pip>=21.3.1,<23.2", "python-slugify==4.0.1", "PyYAML==6.0", diff --git a/requirements.txt b/requirements.txt index 31e5812dadf759..210bd8a0bfc348 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ lru-dict==1.2.0 PyJWT==2.7.0 cryptography==41.0.1 pyOpenSSL==23.2.0 -orjson==3.9.1 +orjson==3.9.2 pip>=21.3.1,<23.2 python-slugify==4.0.1 PyYAML==6.0 From fe6402ef7357348aac5d8108a3180e682ce0a204 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 11 Jul 2023 20:19:04 +0200 Subject: [PATCH 0365/1009] Use device class naming for sfr box (#96092) --- homeassistant/components/sfr_box/button.py | 1 - homeassistant/components/sfr_box/sensor.py | 2 -- homeassistant/components/sfr_box/strings.json | 11 ----------- tests/components/sfr_box/snapshots/test_button.ambr | 2 +- tests/components/sfr_box/snapshots/test_sensor.ambr | 4 ++-- 5 files changed, 3 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/sfr_box/button.py b/homeassistant/components/sfr_box/button.py index ab987944acc116..13a1563034f274 100644 --- a/homeassistant/components/sfr_box/button.py +++ b/homeassistant/components/sfr_box/button.py @@ -67,7 +67,6 @@ class SFRBoxButtonEntityDescription(ButtonEntityDescription, SFRBoxButtonMixin): device_class=ButtonDeviceClass.RESTART, entity_category=EntityCategory.CONFIG, key="system_reboot", - translation_key="reboot", ), ) diff --git a/homeassistant/components/sfr_box/sensor.py b/homeassistant/components/sfr_box/sensor.py index fa754bbe62f1dd..c01d298daffdd4 100644 --- a/homeassistant/components/sfr_box/sensor.py +++ b/homeassistant/components/sfr_box/sensor.py @@ -180,7 +180,6 @@ class SFRBoxSensorEntityDescription(SensorEntityDescription, SFRBoxSensorMixin[_ entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, - translation_key="voltage", value_fn=lambda x: x.alimvoltage, ), SFRBoxSensorEntityDescription[SystemInfo]( @@ -189,7 +188,6 @@ class SFRBoxSensorEntityDescription(SensorEntityDescription, SFRBoxSensorMixin[_ entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfTemperature.CELSIUS, - translation_key="temperature", value_fn=lambda x: x.temperature / 1000, ), ) diff --git a/homeassistant/components/sfr_box/strings.json b/homeassistant/components/sfr_box/strings.json index cf74e9eb656ceb..3fc9691cc12f47 100644 --- a/homeassistant/components/sfr_box/strings.json +++ b/homeassistant/components/sfr_box/strings.json @@ -42,11 +42,6 @@ "name": "WAN status" } }, - "button": { - "reboot": { - "name": "[%key:component::button::entity_component::restart::name%]" - } - }, "sensor": { "dsl_attenuation_down": { "name": "DSL attenuation down" @@ -110,12 +105,6 @@ "unknown": "Unknown" } }, - "temperature": { - "name": "[%key:component::sensor::entity_component::temperature::name%]" - }, - "voltage": { - "name": "[%key:component::sensor::entity_component::voltage::name%]" - }, "wan_mode": { "name": "WAN mode", "state": { diff --git a/tests/components/sfr_box/snapshots/test_button.ambr b/tests/components/sfr_box/snapshots/test_button.ambr index dc6ccc1f25d1ec..f362cfc146fd3a 100644 --- a/tests/components/sfr_box/snapshots/test_button.ambr +++ b/tests/components/sfr_box/snapshots/test_button.ambr @@ -54,7 +54,7 @@ 'original_name': 'Restart', 'platform': 'sfr_box', 'supported_features': 0, - 'translation_key': 'reboot', + 'translation_key': None, 'unique_id': 'e4:5d:51:00:11:22_system_reboot', 'unit_of_measurement': None, }), diff --git a/tests/components/sfr_box/snapshots/test_sensor.ambr b/tests/components/sfr_box/snapshots/test_sensor.ambr index 2390ba625eb3ff..171a5803ada9c1 100644 --- a/tests/components/sfr_box/snapshots/test_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_sensor.ambr @@ -89,7 +89,7 @@ 'original_name': 'Voltage', 'platform': 'sfr_box', 'supported_features': 0, - 'translation_key': 'voltage', + 'translation_key': None, 'unique_id': 'e4:5d:51:00:11:22_system_alimvoltage', 'unit_of_measurement': , }), @@ -117,7 +117,7 @@ 'original_name': 'Temperature', 'platform': 'sfr_box', 'supported_features': 0, - 'translation_key': 'temperature', + 'translation_key': None, 'unique_id': 'e4:5d:51:00:11:22_system_temperature', 'unit_of_measurement': , }), From dfad1a920f3c12857a3610ccf382178b95feba8c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 11 Jul 2023 20:19:51 +0200 Subject: [PATCH 0366/1009] Add entity translations to solarlog (#96157) --- homeassistant/components/solarlog/sensor.py | 44 ++++++------ .../components/solarlog/strings.json | 70 +++++++++++++++++++ 2 files changed, 92 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index 906d9aee629a85..a69d2a4c3829bf 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -36,13 +36,13 @@ class SolarLogSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( SolarLogSensorEntityDescription( key="time", - name="last update", + translation_key="last_update", device_class=SensorDeviceClass.TIMESTAMP, value=as_local, ), SolarLogSensorEntityDescription( key="power_ac", - name="power AC", + translation_key="power_ac", icon="mdi:solar-power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, @@ -50,7 +50,7 @@ class SolarLogSensorEntityDescription(SensorEntityDescription): ), SolarLogSensorEntityDescription( key="power_dc", - name="power DC", + translation_key="power_dc", icon="mdi:solar-power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, @@ -58,21 +58,21 @@ class SolarLogSensorEntityDescription(SensorEntityDescription): ), SolarLogSensorEntityDescription( key="voltage_ac", - name="voltage AC", + translation_key="voltage_ac", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, ), SolarLogSensorEntityDescription( key="voltage_dc", - name="voltage DC", + translation_key="voltage_dc", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, ), SolarLogSensorEntityDescription( key="yield_day", - name="yield day", + translation_key="yield_day", icon="mdi:solar-power", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, @@ -80,7 +80,7 @@ class SolarLogSensorEntityDescription(SensorEntityDescription): ), SolarLogSensorEntityDescription( key="yield_yesterday", - name="yield yesterday", + translation_key="yield_yesterday", icon="mdi:solar-power", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, @@ -88,7 +88,7 @@ class SolarLogSensorEntityDescription(SensorEntityDescription): ), SolarLogSensorEntityDescription( key="yield_month", - name="yield month", + translation_key="yield_month", icon="mdi:solar-power", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, @@ -96,7 +96,7 @@ class SolarLogSensorEntityDescription(SensorEntityDescription): ), SolarLogSensorEntityDescription( key="yield_year", - name="yield year", + translation_key="yield_year", icon="mdi:solar-power", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, @@ -104,7 +104,7 @@ class SolarLogSensorEntityDescription(SensorEntityDescription): ), SolarLogSensorEntityDescription( key="yield_total", - name="yield total", + translation_key="yield_total", icon="mdi:solar-power", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, @@ -113,42 +113,42 @@ class SolarLogSensorEntityDescription(SensorEntityDescription): ), SolarLogSensorEntityDescription( key="consumption_ac", - name="consumption AC", + translation_key="consumption_ac", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SolarLogSensorEntityDescription( key="consumption_day", - name="consumption day", + translation_key="consumption_day", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, value=lambda value: round(value / 1000, 3), ), SolarLogSensorEntityDescription( key="consumption_yesterday", - name="consumption yesterday", + translation_key="consumption_yesterday", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, value=lambda value: round(value / 1000, 3), ), SolarLogSensorEntityDescription( key="consumption_month", - name="consumption month", + translation_key="consumption_month", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, value=lambda value: round(value / 1000, 3), ), SolarLogSensorEntityDescription( key="consumption_year", - name="consumption year", + translation_key="consumption_year", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, value=lambda value: round(value / 1000, 3), ), SolarLogSensorEntityDescription( key="consumption_total", - name="consumption total", + translation_key="consumption_total", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, @@ -156,14 +156,14 @@ class SolarLogSensorEntityDescription(SensorEntityDescription): ), SolarLogSensorEntityDescription( key="total_power", - name="installed peak power", + translation_key="total_power", icon="mdi:solar-power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, ), SolarLogSensorEntityDescription( key="alternator_loss", - name="alternator loss", + translation_key="alternator_loss", icon="mdi:solar-power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, @@ -171,7 +171,7 @@ class SolarLogSensorEntityDescription(SensorEntityDescription): ), SolarLogSensorEntityDescription( key="capacity", - name="capacity", + translation_key="capacity", icon="mdi:solar-power", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.POWER_FACTOR, @@ -180,7 +180,7 @@ class SolarLogSensorEntityDescription(SensorEntityDescription): ), SolarLogSensorEntityDescription( key="efficiency", - name="efficiency", + translation_key="efficiency", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, @@ -188,7 +188,7 @@ class SolarLogSensorEntityDescription(SensorEntityDescription): ), SolarLogSensorEntityDescription( key="power_available", - name="power available", + translation_key="power_available", icon="mdi:solar-power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, @@ -196,7 +196,7 @@ class SolarLogSensorEntityDescription(SensorEntityDescription): ), SolarLogSensorEntityDescription( key="usage", - name="usage", + translation_key="usage", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/solarlog/strings.json b/homeassistant/components/solarlog/strings.json index 068132dea41b9f..62e923a766dbdf 100644 --- a/homeassistant/components/solarlog/strings.json +++ b/homeassistant/components/solarlog/strings.json @@ -16,5 +16,75 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "sensor": { + "last_update": { + "name": "Last update" + }, + "power_ac": { + "name": "Power AC" + }, + "power_dc": { + "name": "Power DC" + }, + "voltage_ac": { + "name": "Voltage AC" + }, + "voltage_dc": { + "name": "Voltage DC" + }, + "yield_day": { + "name": "Yield day" + }, + "yield_yesterday": { + "name": "Yield yesterday" + }, + "yield_month": { + "name": "Yield month" + }, + "yield_year": { + "name": "Yield year" + }, + "yield_total": { + "name": "Yield total" + }, + "consumption_ac": { + "name": "Consumption AC" + }, + "consumption_day": { + "name": "Consumption day" + }, + "consumption_yesterday": { + "name": "Consumption yesterday" + }, + "consumption_month": { + "name": "Consumption month" + }, + "consumption_year": { + "name": "Consumption year" + }, + "consumption_total": { + "name": "Consumption total" + }, + "total_power": { + "name": "Installed peak power" + }, + "alternator_loss": { + "name": "Alternator loss" + }, + "capacity": { + "name": "Capacity" + }, + "efficiency": { + "name": "Efficiency" + }, + "power_available": { + "name": "Power available" + }, + "usage": { + "name": "Usage" + } + } } } From efcaad1179cf4b642707378d93a3f3951400564b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 11 Jul 2023 20:22:12 +0200 Subject: [PATCH 0367/1009] Fix handling MQTT light brightness from zero rgb (#96286) * Fix handling MQTT light brightness from zero rgb * Fix log message --- homeassistant/components/mqtt/light/schema_basic.py | 7 +++++++ tests/components/mqtt/test_light.py | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 7f2c2cf5e06abe..fe09667ca4aaa1 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -482,6 +482,13 @@ def _rgbx_received( if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is None: rgb = convert_color(*color) brightness = max(rgb) + if brightness == 0: + _LOGGER.debug( + "Ignoring %s message with zero rgb brightness from '%s'", + color_mode, + msg.topic, + ) + return None self._attr_brightness = brightness # Normalize the color to 100% brightness color = tuple( diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index ee4f170e8e675d..59d5090b7118da 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -666,6 +666,12 @@ async def test_brightness_from_rgb_controlling_scale( assert state.attributes.get("brightness") == 128 assert state.attributes.get("rgb_color") == (255, 128, 64) + # Test zero rgb is ignored + async_fire_mqtt_message(hass, "test_scale_rgb/rgb/status", "0,0,0") + state = hass.states.get("light.test") + assert state.attributes.get("brightness") == 128 + assert state.attributes.get("rgb_color") == (255, 128, 64) + mqtt_mock.async_publish.reset_mock() await common.async_turn_on(hass, "light.test", brightness=191) await hass.async_block_till_done() From fe44827e3c6d7bb2620673833d64d541a2e4b58d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 11 Jul 2023 20:24:33 +0200 Subject: [PATCH 0368/1009] Add entity translations to Rainforest eagle (#96031) * Add entity translations to Rainforest eagle * Add entity translations to Rainforest Eagle --- .../components/rainforest_eagle/sensor.py | 11 ++++++----- .../components/rainforest_eagle/strings.json | 16 ++++++++++++++++ tests/components/rainforest_eagle/test_sensor.py | 8 ++++---- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/rainforest_eagle/sensor.py b/homeassistant/components/rainforest_eagle/sensor.py index 0680aa7455d1b0..a7fd27a051fb34 100644 --- a/homeassistant/components/rainforest_eagle/sensor.py +++ b/homeassistant/components/rainforest_eagle/sensor.py @@ -21,22 +21,21 @@ SENSORS = ( SensorEntityDescription( key="zigbee:InstantaneousDemand", - # We can drop the "Eagle-200" part of the name in HA 2021.12 - name="Eagle-200 Meter Power Demand", + translation_key="power_demand", native_unit_of_measurement=UnitOfPower.KILO_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="zigbee:CurrentSummationDelivered", - name="Eagle-200 Total Meter Energy Delivered", + translation_key="total_energy_delivered", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="zigbee:CurrentSummationReceived", - name="Eagle-200 Total Meter Energy Received", + translation_key="total_energy_received", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -57,7 +56,7 @@ async def async_setup_entry( coordinator, SensorEntityDescription( key="zigbee:Price", - name="Meter Price", + translation_key="meter_price", native_unit_of_measurement=f"{coordinator.data['zigbee:PriceCurrency']}/{UnitOfEnergy.KILO_WATT_HOUR}", state_class=SensorStateClass.MEASUREMENT, ), @@ -70,6 +69,8 @@ async def async_setup_entry( class EagleSensor(CoordinatorEntity[EagleDataCoordinator], SensorEntity): """Implementation of the Rainforest Eagle sensor.""" + _attr_has_entity_name = True + def __init__(self, coordinator, entity_description): """Initialize the sensor.""" super().__init__(coordinator) diff --git a/homeassistant/components/rainforest_eagle/strings.json b/homeassistant/components/rainforest_eagle/strings.json index b32f38302f486e..58c7f6bd795a37 100644 --- a/homeassistant/components/rainforest_eagle/strings.json +++ b/homeassistant/components/rainforest_eagle/strings.json @@ -17,5 +17,21 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "sensor": { + "power_demand": { + "name": "Meter power demand" + }, + "total_energy_delivered": { + "name": "Total meter energy delivered" + }, + "total_energy_received": { + "name": "Total meter energy received" + }, + "meter_price": { + "name": "Meter price" + } + } } } diff --git a/tests/components/rainforest_eagle/test_sensor.py b/tests/components/rainforest_eagle/test_sensor.py index 96b9e0a85dc94f..5e76a81932aff2 100644 --- a/tests/components/rainforest_eagle/test_sensor.py +++ b/tests/components/rainforest_eagle/test_sensor.py @@ -32,7 +32,7 @@ async def test_sensors_200(hass: HomeAssistant, setup_rainforest_200) -> None: assert len(hass.states.async_all()) == 4 - price = hass.states.get("sensor.meter_price") + price = hass.states.get("sensor.eagle_200_meter_price") assert price is not None assert price.state == "0.053990" assert price.attributes["unit_of_measurement"] == "USD/kWh" @@ -42,17 +42,17 @@ async def test_sensors_100(hass: HomeAssistant, setup_rainforest_100) -> None: """Test the sensors.""" assert len(hass.states.async_all()) == 3 - demand = hass.states.get("sensor.eagle_200_meter_power_demand") + demand = hass.states.get("sensor.eagle_100_meter_power_demand") assert demand is not None assert demand.state == "1.152000" assert demand.attributes["unit_of_measurement"] == "kW" - delivered = hass.states.get("sensor.eagle_200_total_meter_energy_delivered") + delivered = hass.states.get("sensor.eagle_100_total_meter_energy_delivered") assert delivered is not None assert delivered.state == "45251.285000" assert delivered.attributes["unit_of_measurement"] == "kWh" - received = hass.states.get("sensor.eagle_200_total_meter_energy_received") + received = hass.states.get("sensor.eagle_100_total_meter_energy_received") assert received is not None assert received.state == "232.232000" assert received.attributes["unit_of_measurement"] == "kWh" From 8b379254c33e8f0b8fbb76753488160f89035be5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 11 Jul 2023 20:27:31 +0200 Subject: [PATCH 0369/1009] Migrate Roomba to has entity name (#96085) --- .../components/roomba/binary_sensor.py | 6 +--- .../components/roomba/irobot_base.py | 8 ++---- homeassistant/components/roomba/sensor.py | 28 ++----------------- homeassistant/components/roomba/strings.json | 7 +++++ 4 files changed, 13 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/roomba/binary_sensor.py b/homeassistant/components/roomba/binary_sensor.py index 0acd655363f505..f480839388ca46 100644 --- a/homeassistant/components/roomba/binary_sensor.py +++ b/homeassistant/components/roomba/binary_sensor.py @@ -28,11 +28,7 @@ class RoombaBinStatus(IRobotEntity, BinarySensorEntity): """Class to hold Roomba Sensor basic info.""" ICON = "mdi:delete-variant" - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} Bin Full" + _attr_translation_key = "bin_full" @property def unique_id(self): diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py index 317209886bd2b6..5dbd1e986f3f55 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -61,6 +61,7 @@ class IRobotEntity(Entity): """Base class for iRobot Entities.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__(self, roomba, blid): """Initialize the iRobot handler.""" @@ -135,6 +136,8 @@ def on_message(self, json_data): class IRobotVacuum(IRobotEntity, StateVacuumEntity): """Base class for iRobot robots.""" + _attr_name = None + def __init__(self, roomba, blid): """Initialize the iRobot handler.""" super().__init__(roomba, blid) @@ -160,11 +163,6 @@ def available(self) -> bool: """Return True if entity is available.""" return True # Always available, otherwise setup will fail - @property - def name(self): - """Return the name of the device.""" - return self._name - @property def extra_state_attributes(self): """Return the state attributes of the device.""" diff --git a/homeassistant/components/roomba/sensor.py b/homeassistant/components/roomba/sensor.py index c0092922783ed5..dd74a023ff101d 100644 --- a/homeassistant/components/roomba/sensor.py +++ b/homeassistant/components/roomba/sensor.py @@ -1,11 +1,9 @@ """Sensor for checking the battery level of Roomba.""" from homeassistant.components.sensor import SensorDeviceClass, SensorEntity -from homeassistant.components.vacuum import STATE_DOCKED from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.icon import icon_for_battery_level from .const import BLID, DOMAIN, ROOMBA_SESSION from .irobot_base import IRobotEntity @@ -28,36 +26,14 @@ class RoombaBattery(IRobotEntity, SensorEntity): """Class to hold Roomba Sensor basic info.""" _attr_entity_category = EntityCategory.DIAGNOSTIC - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} Battery Level" + _attr_device_class = SensorDeviceClass.BATTERY + _attr_native_unit_of_measurement = PERCENTAGE @property def unique_id(self): """Return the ID of this sensor.""" return f"battery_{self._blid}" - @property - def device_class(self): - """Return the device class of the sensor.""" - return SensorDeviceClass.BATTERY - - @property - def native_unit_of_measurement(self): - """Return the unit_of_measurement of the device.""" - return PERCENTAGE - - @property - def icon(self): - """Return the icon for the battery.""" - charging = bool(self._robot_state == STATE_DOCKED) - - return icon_for_battery_level( - battery_level=self._battery_level, charging=charging - ) - @property def native_value(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json index be2e5b99159064..206e8c5bae0741 100644 --- a/homeassistant/components/roomba/strings.json +++ b/homeassistant/components/roomba/strings.json @@ -47,5 +47,12 @@ } } } + }, + "entity": { + "binary_sensor": { + "bin_full": { + "name": "Bin full" + } + } } } From 38823bae71103216bc692e103054c7a6c8ae2ab9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 20:29:09 +0200 Subject: [PATCH 0370/1009] Update colorlog to 6.7.0 (#96131) --- homeassistant/scripts/check_config.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 92f5b442d9ed45..5384b86cb98fd3 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -26,7 +26,7 @@ # mypy: allow-untyped-calls, allow-untyped-defs -REQUIREMENTS = ("colorlog==6.6.0",) +REQUIREMENTS = ("colorlog==6.7.0",) _LOGGER = logging.getLogger(__name__) # pylint: disable=protected-access diff --git a/requirements_all.txt b/requirements_all.txt index f6d8a03cbe873f..27884a263f06ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -592,7 +592,7 @@ clx-sdk-xms==1.0.0 coinbase==2.1.0 # homeassistant.scripts.check_config -colorlog==6.6.0 +colorlog==6.7.0 # homeassistant.components.color_extractor colorthief==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd72a6df01b4a6..5210e0e335b340 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -481,7 +481,7 @@ caldav==1.2.0 coinbase==2.1.0 # homeassistant.scripts.check_config -colorlog==6.6.0 +colorlog==6.7.0 # homeassistant.components.color_extractor colorthief==0.2.1 From 05c194f36d8d0ac8ce312bfdaa9538b60ec59647 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 20:29:55 +0200 Subject: [PATCH 0371/1009] Upgrade pylint-per-file-ignore to v1.2.1 (#96134) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index e6c805a64c1ec1..11920917a5913f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -15,7 +15,7 @@ mypy==1.4.1 pre-commit==3.1.0 pydantic==1.10.11 pylint==2.17.4 -pylint-per-file-ignores==1.1.0 +pylint-per-file-ignores==1.2.1 pipdeptree==2.9.4 pytest-asyncio==0.20.3 pytest-aiohttp==1.0.4 From ad091479ea1052db022ae10c04d65f8801b78ab3 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 11 Jul 2023 20:32:33 +0200 Subject: [PATCH 0372/1009] Cleanup unneeded MQTT vacuum feature check (#96312) --- homeassistant/components/mqtt/vacuum/schema_legacy.py | 4 ++-- homeassistant/components/mqtt/vacuum/schema_state.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 18cda0b137d91a..7c73e579112488 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -419,9 +419,9 @@ def battery_icon(self) -> str: ) async def _async_publish_command(self, feature: VacuumEntityFeature) -> None: - """Check for a missing feature or command topic.""" + """Publish a command.""" - if self._command_topic is None or self.supported_features & feature == 0: + if self._command_topic is None: return await self.async_publish( diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index fef185687db971..ee06131af0251d 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -259,8 +259,8 @@ async def _subscribe_topics(self) -> None: await subscription.async_subscribe_topics(self.hass, self._sub_state) async def _async_publish_command(self, feature: VacuumEntityFeature) -> None: - """Check for a missing feature or command topic.""" - if self._command_topic is None or self.supported_features & feature == 0: + """Publish a command.""" + if self._command_topic is None: return await self.async_publish( From 77ebf8a8e5cdfc939884c92c1d5d43a31b87ca92 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 11 Jul 2023 20:34:11 +0200 Subject: [PATCH 0373/1009] Add entity translations to Juicenet (#95487) --- homeassistant/components/juicenet/entity.py | 2 ++ homeassistant/components/juicenet/number.py | 4 +--- homeassistant/components/juicenet/sensor.py | 9 ++------- .../components/juicenet/strings.json | 20 +++++++++++++++++++ homeassistant/components/juicenet/switch.py | 7 ++----- 5 files changed, 27 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/juicenet/entity.py b/homeassistant/components/juicenet/entity.py index 0f3811bef6f477..2f25a934e7f32e 100644 --- a/homeassistant/components/juicenet/entity.py +++ b/homeassistant/components/juicenet/entity.py @@ -14,6 +14,8 @@ class JuiceNetDevice(CoordinatorEntity): """Represent a base JuiceNet device.""" + _attr_has_entity_name = True + def __init__( self, device: Charger, key: str, coordinator: DataUpdateCoordinator ) -> None: diff --git a/homeassistant/components/juicenet/number.py b/homeassistant/components/juicenet/number.py index 45be1dd900495c..e78f6189baf90a 100644 --- a/homeassistant/components/juicenet/number.py +++ b/homeassistant/components/juicenet/number.py @@ -37,7 +37,7 @@ class JuiceNetNumberEntityDescription( NUMBER_TYPES: tuple[JuiceNetNumberEntityDescription, ...] = ( JuiceNetNumberEntityDescription( - name="Amperage Limit", + translation_key="amperage_limit", key="current_charging_amperage_limit", native_min_value=6, native_max_value_key="max_charging_amperage", @@ -80,8 +80,6 @@ def __init__( super().__init__(device, description.key, coordinator) self.entity_description = description - self._attr_name = f"{self.device.name} {description.name}" - @property def native_value(self) -> float | None: """Return the value of the entity.""" diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py index fdc40211d775d8..5f71e066b9c24b 100644 --- a/homeassistant/components/juicenet/sensor.py +++ b/homeassistant/components/juicenet/sensor.py @@ -29,40 +29,36 @@ ), SensorEntityDescription( key="temperature", - name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="voltage", - name="Voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, ), SensorEntityDescription( key="amps", - name="Amps", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="watts", - name="Watts", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="charge_time", - name="Charge time", + translation_key="charge_time", native_unit_of_measurement=UnitOfTime.SECONDS, icon="mdi:timer-outline", ), SensorEntityDescription( key="energy_added", - name="Energy added", + translation_key="energy_added", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -97,7 +93,6 @@ def __init__( """Initialise the sensor.""" super().__init__(device, description.key, coordinator) self.entity_description = description - self._attr_name = f"{self.device.name} {description.name}" @property def icon(self): diff --git a/homeassistant/components/juicenet/strings.json b/homeassistant/components/juicenet/strings.json index bc4a66e72d4361..0e3732c66d2bfa 100644 --- a/homeassistant/components/juicenet/strings.json +++ b/homeassistant/components/juicenet/strings.json @@ -17,5 +17,25 @@ "title": "Connect to JuiceNet" } } + }, + "entity": { + "number": { + "amperage_limit": { + "name": "Amperage limit" + } + }, + "sensor": { + "charge_time": { + "name": "Charge time" + }, + "energy_added": { + "name": "Energy added" + } + }, + "switch": { + "charge_now": { + "name": "Charge now" + } + } } } diff --git a/homeassistant/components/juicenet/switch.py b/homeassistant/components/juicenet/switch.py index 576c66c0841f6a..7c373eeeb245b7 100644 --- a/homeassistant/components/juicenet/switch.py +++ b/homeassistant/components/juicenet/switch.py @@ -29,15 +29,12 @@ async def async_setup_entry( class JuiceNetChargeNowSwitch(JuiceNetDevice, SwitchEntity): """Implementation of a JuiceNet switch.""" + _attr_translation_key = "charge_now" + def __init__(self, device, coordinator): """Initialise the switch.""" super().__init__(device, "charge_now", coordinator) - @property - def name(self): - """Return the name of the device.""" - return f"{self.device.name} Charge Now" - @property def is_on(self): """Return true if switch is on.""" From c431fc2297ebb1d5fd49c245e8f7a9028d4248b4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 20:56:21 +0200 Subject: [PATCH 0374/1009] Migrate reload only helper services to support translations (#96344) --- homeassistant/components/bayesian/services.yaml | 2 -- homeassistant/components/bayesian/strings.json | 6 ++++++ homeassistant/components/filter/services.yaml | 2 -- homeassistant/components/filter/strings.json | 8 ++++++++ homeassistant/components/generic/services.yaml | 2 -- homeassistant/components/generic/strings.json | 6 ++++++ homeassistant/components/generic_thermostat/services.yaml | 2 -- homeassistant/components/generic_thermostat/strings.json | 8 ++++++++ homeassistant/components/history_stats/services.yaml | 2 -- homeassistant/components/history_stats/strings.json | 8 ++++++++ homeassistant/components/min_max/services.yaml | 2 -- homeassistant/components/min_max/strings.json | 6 ++++++ homeassistant/components/person/services.yaml | 2 -- homeassistant/components/person/strings.json | 6 ++++++ homeassistant/components/schedule/services.yaml | 2 -- homeassistant/components/schedule/strings.json | 6 ++++++ homeassistant/components/statistics/services.yaml | 2 -- homeassistant/components/statistics/strings.json | 8 ++++++++ homeassistant/components/trend/services.yaml | 2 -- homeassistant/components/trend/strings.json | 8 ++++++++ homeassistant/components/universal/services.yaml | 2 -- homeassistant/components/universal/strings.json | 8 ++++++++ homeassistant/components/zone/services.yaml | 2 -- homeassistant/components/zone/strings.json | 8 ++++++++ 24 files changed, 86 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/filter/strings.json create mode 100644 homeassistant/components/generic_thermostat/strings.json create mode 100644 homeassistant/components/history_stats/strings.json create mode 100644 homeassistant/components/statistics/strings.json create mode 100644 homeassistant/components/trend/strings.json create mode 100644 homeassistant/components/universal/strings.json create mode 100644 homeassistant/components/zone/strings.json diff --git a/homeassistant/components/bayesian/services.yaml b/homeassistant/components/bayesian/services.yaml index c1dc891805a3b7..c983a105c93977 100644 --- a/homeassistant/components/bayesian/services.yaml +++ b/homeassistant/components/bayesian/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload all bayesian entities diff --git a/homeassistant/components/bayesian/strings.json b/homeassistant/components/bayesian/strings.json index 338795624cd806..f7c12523b2c8c5 100644 --- a/homeassistant/components/bayesian/strings.json +++ b/homeassistant/components/bayesian/strings.json @@ -8,5 +8,11 @@ "description": "In the Bayesian integration `prob_given_false` is now a required configuration variable as there was no mathematical rationale for the previous default value. Please add this to your `configuration.yml` for `bayesian/{entity}`. These observations will be ignored until you do.", "title": "Manual YAML addition required for Bayesian" } + }, + "services": { + "reload": { + "name": "Reload", + "description": "Reloads bayesian sensors from the YAML-configuration." + } } } diff --git a/homeassistant/components/filter/services.yaml b/homeassistant/components/filter/services.yaml index 431c73616ceff7..c983a105c93977 100644 --- a/homeassistant/components/filter/services.yaml +++ b/homeassistant/components/filter/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload all filter entities diff --git a/homeassistant/components/filter/strings.json b/homeassistant/components/filter/strings.json new file mode 100644 index 00000000000000..078e5b35980def --- /dev/null +++ b/homeassistant/components/filter/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "reload": { + "name": "Reload", + "description": "Reloads filters from the YAML-configuration." + } + } +} diff --git a/homeassistant/components/generic/services.yaml b/homeassistant/components/generic/services.yaml index a05a9e3415daeb..c983a105c93977 100644 --- a/homeassistant/components/generic/services.yaml +++ b/homeassistant/components/generic/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload all generic entities. diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index 0ce8af4f3a6849..d23bb605c7b44e 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -83,5 +83,11 @@ "stream_io_error": "[%key:component::generic::config::error::stream_io_error%]", "stream_not_permitted": "[%key:component::generic::config::error::stream_not_permitted%]" } + }, + "services": { + "reload": { + "name": "Reload", + "description": "Reloads generic cameras from the YAML-configuration." + } } } diff --git a/homeassistant/components/generic_thermostat/services.yaml b/homeassistant/components/generic_thermostat/services.yaml index ef6745bd36f1f6..c983a105c93977 100644 --- a/homeassistant/components/generic_thermostat/services.yaml +++ b/homeassistant/components/generic_thermostat/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload all generic_thermostat entities. diff --git a/homeassistant/components/generic_thermostat/strings.json b/homeassistant/components/generic_thermostat/strings.json new file mode 100644 index 00000000000000..f1525b2516da05 --- /dev/null +++ b/homeassistant/components/generic_thermostat/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "reload": { + "name": "Reload", + "description": "Reloads generic thermostats from the YAML-configuration." + } + } +} diff --git a/homeassistant/components/history_stats/services.yaml b/homeassistant/components/history_stats/services.yaml index f254295ea20252..c983a105c93977 100644 --- a/homeassistant/components/history_stats/services.yaml +++ b/homeassistant/components/history_stats/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload all history_stats entities. diff --git a/homeassistant/components/history_stats/strings.json b/homeassistant/components/history_stats/strings.json new file mode 100644 index 00000000000000..cb4601f2a096b6 --- /dev/null +++ b/homeassistant/components/history_stats/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "reload": { + "name": "Reload", + "description": "Reloads history stats sensors from the YAML-configuration." + } + } +} diff --git a/homeassistant/components/min_max/services.yaml b/homeassistant/components/min_max/services.yaml index cca67d921444c1..c983a105c93977 100644 --- a/homeassistant/components/min_max/services.yaml +++ b/homeassistant/components/min_max/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload all min_max entities. diff --git a/homeassistant/components/min_max/strings.json b/homeassistant/components/min_max/strings.json index c76a6faf2f5981..464d01b90b4c91 100644 --- a/homeassistant/components/min_max/strings.json +++ b/homeassistant/components/min_max/strings.json @@ -43,5 +43,11 @@ "sum": "Sum" } } + }, + "services": { + "reload": { + "name": "Reload", + "description": "Reloads min/max sensors from the YAML-configuration." + } } } diff --git a/homeassistant/components/person/services.yaml b/homeassistant/components/person/services.yaml index 265c6049563dfb..c983a105c93977 100644 --- a/homeassistant/components/person/services.yaml +++ b/homeassistant/components/person/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload the person configuration. diff --git a/homeassistant/components/person/strings.json b/homeassistant/components/person/strings.json index 8a8915541d8305..10a982535f2f02 100644 --- a/homeassistant/components/person/strings.json +++ b/homeassistant/components/person/strings.json @@ -25,5 +25,11 @@ } } } + }, + "services": { + "reload": { + "name": "Reload", + "description": "Reloads persons from the YAML-configuration." + } } } diff --git a/homeassistant/components/schedule/services.yaml b/homeassistant/components/schedule/services.yaml index b34dd5e83da469..c983a105c93977 100644 --- a/homeassistant/components/schedule/services.yaml +++ b/homeassistant/components/schedule/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload the schedule configuration diff --git a/homeassistant/components/schedule/strings.json b/homeassistant/components/schedule/strings.json index 4c22e5ecead342..aea07cc3ff26c6 100644 --- a/homeassistant/components/schedule/strings.json +++ b/homeassistant/components/schedule/strings.json @@ -20,5 +20,11 @@ } } } + }, + "services": { + "reload": { + "name": "Reload", + "description": "Reloads schedules from the YAML-configuration." + } } } diff --git a/homeassistant/components/statistics/services.yaml b/homeassistant/components/statistics/services.yaml index 8c2c8f8464aa83..c983a105c93977 100644 --- a/homeassistant/components/statistics/services.yaml +++ b/homeassistant/components/statistics/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload all statistics entities. diff --git a/homeassistant/components/statistics/strings.json b/homeassistant/components/statistics/strings.json new file mode 100644 index 00000000000000..6b2a04a85df497 --- /dev/null +++ b/homeassistant/components/statistics/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "reload": { + "name": "Reload", + "description": "Reloads statistics sensors from the YAML-configuration." + } + } +} diff --git a/homeassistant/components/trend/services.yaml b/homeassistant/components/trend/services.yaml index 1d29e08dccf795..c983a105c93977 100644 --- a/homeassistant/components/trend/services.yaml +++ b/homeassistant/components/trend/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload all trend entities. diff --git a/homeassistant/components/trend/strings.json b/homeassistant/components/trend/strings.json new file mode 100644 index 00000000000000..1715f019f272d6 --- /dev/null +++ b/homeassistant/components/trend/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "reload": { + "name": "Reload", + "description": "Reloads trend sensors from the YAML-configuration." + } + } +} diff --git a/homeassistant/components/universal/services.yaml b/homeassistant/components/universal/services.yaml index e0af28bf3a608c..c983a105c93977 100644 --- a/homeassistant/components/universal/services.yaml +++ b/homeassistant/components/universal/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload all universal entities diff --git a/homeassistant/components/universal/strings.json b/homeassistant/components/universal/strings.json new file mode 100644 index 00000000000000..b440d76ebc23ab --- /dev/null +++ b/homeassistant/components/universal/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "reload": { + "name": "Reload", + "description": "Reloads universal media players from the YAML-configuration." + } + } +} diff --git a/homeassistant/components/zone/services.yaml b/homeassistant/components/zone/services.yaml index 2ce77132a532c6..c983a105c93977 100644 --- a/homeassistant/components/zone/services.yaml +++ b/homeassistant/components/zone/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload the YAML-based zone configuration. diff --git a/homeassistant/components/zone/strings.json b/homeassistant/components/zone/strings.json new file mode 100644 index 00000000000000..b2f3b5efffa92e --- /dev/null +++ b/homeassistant/components/zone/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "reload": { + "name": "Reload", + "description": "Reloads zones from the YAML-configuration." + } + } +} From bc9b9048f01d6c5845ddd97286ff9e801af91b69 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 11 Jul 2023 21:36:44 +0200 Subject: [PATCH 0375/1009] Add Reolink sensor platform (#96323) * Add Reolink sensor platform * fix styling * Add state class * Add Event connection sensor * Update homeassistant/components/reolink/sensor.py Co-authored-by: Joost Lekkerkerker * Use translation keys * fix json * fix json 2 * fix json 3 * Apply suggestions from code review Co-authored-by: G Johansson --------- Co-authored-by: Joost Lekkerkerker Co-authored-by: G Johansson --- .coveragerc | 1 + homeassistant/components/reolink/__init__.py | 1 + homeassistant/components/reolink/host.py | 9 ++ homeassistant/components/reolink/sensor.py | 121 ++++++++++++++++++ homeassistant/components/reolink/strings.json | 13 ++ 5 files changed, 145 insertions(+) create mode 100644 homeassistant/components/reolink/sensor.py diff --git a/.coveragerc b/.coveragerc index 2e10d1be2570c7..912c472de3e19b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -991,6 +991,7 @@ omit = homeassistant/components/reolink/light.py homeassistant/components/reolink/number.py homeassistant/components/reolink/select.py + homeassistant/components/reolink/sensor.py homeassistant/components/reolink/siren.py homeassistant/components/reolink/switch.py homeassistant/components/reolink/update.py diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 923df261d843fc..2de87659919d18 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -30,6 +30,7 @@ Platform.LIGHT, Platform.NUMBER, Platform.SELECT, + Platform.SENSOR, Platform.SIREN, Platform.SWITCH, Platform.UPDATE, diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 81fbda63fef00c..dac02b913152b3 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -432,6 +432,15 @@ def unregister_webhook(self): webhook.async_unregister(self._hass, self.webhook_id) self.webhook_id = None + @property + def event_connection(self) -> str: + """Return the event connection type.""" + if self._webhook_reachable: + return "onvif_push" + if self._long_poll_received: + return "onvif_long_poll" + return "fast_poll" + async def _async_long_polling(self, *_) -> None: """Use ONVIF long polling to immediately receive events.""" # This task will be cancelled once _async_stop_long_polling is called diff --git a/homeassistant/components/reolink/sensor.py b/homeassistant/components/reolink/sensor.py new file mode 100644 index 00000000000000..42758dc99290ca --- /dev/null +++ b/homeassistant/components/reolink/sensor.py @@ -0,0 +1,121 @@ +"""Component providing support for Reolink sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import date, datetime +from decimal import Decimal + +from reolink_aio.api import Host + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from . import ReolinkData +from .const import DOMAIN +from .entity import ReolinkHostCoordinatorEntity + + +@dataclass +class ReolinkHostSensorEntityDescriptionMixin: + """Mixin values for Reolink host sensor entities.""" + + value: Callable[[Host], bool] + + +@dataclass +class ReolinkHostSensorEntityDescription( + SensorEntityDescription, ReolinkHostSensorEntityDescriptionMixin +): + """A class that describes host sensor entities.""" + + supported: Callable[[Host], bool] = lambda host: True + + +HOST_SENSORS = ( + ReolinkHostSensorEntityDescription( + key="wifi_signal", + translation_key="wifi_signal", + icon="mdi:wifi", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value=lambda api: api.wifi_signal, + supported=lambda api: api.supported(None, "wifi") and api.wifi_connection, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a Reolink IP Camera.""" + reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id] + + entities: list[ReolinkHostSensorEntity | EventConnectionSensorEntity] = [ + ReolinkHostSensorEntity(reolink_data, entity_description) + for entity_description in HOST_SENSORS + if entity_description.supported(reolink_data.host.api) + ] + entities.append(EventConnectionSensorEntity(reolink_data)) + async_add_entities(entities) + + +class ReolinkHostSensorEntity(ReolinkHostCoordinatorEntity, SensorEntity): + """Base sensor class for Reolink host sensors.""" + + entity_description: ReolinkHostSensorEntityDescription + + def __init__( + self, + reolink_data: ReolinkData, + entity_description: ReolinkHostSensorEntityDescription, + ) -> None: + """Initialize Reolink binary sensor.""" + super().__init__(reolink_data) + self.entity_description = entity_description + + self._attr_unique_id = f"{self._host.unique_id}_{entity_description.key}" + + @property + def native_value(self) -> StateType | date | datetime | Decimal: + """Return the value reported by the sensor.""" + return self.entity_description.value(self._host.api) + + +class EventConnectionSensorEntity(ReolinkHostCoordinatorEntity, SensorEntity): + """Reolink Event connection sensor.""" + + def __init__( + self, + reolink_data: ReolinkData, + ) -> None: + """Initialize Reolink binary sensor.""" + super().__init__(reolink_data) + self.entity_description = SensorEntityDescription( + key="event_connection", + translation_key="event_connection", + icon="mdi:swap-horizontal", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + options=["onvif_push", "onvif_long_poll", "fast_poll"], + ) + + self._attr_unique_id = f"{self._host.unique_id}_{self.entity_description.key}" + + @property + def native_value(self) -> str: + """Return the value reported by the sensor.""" + return self._host.event_connection diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 53f2e57b97bac5..c0c2094eeb9d10 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -93,6 +93,19 @@ "alwaysonatnight": "Auto & always on at night" } } + }, + "sensor": { + "event_connection": { + "name": "Event connection", + "state": { + "onvif_push": "ONVIF push", + "onvif_long_poll": "ONVIF long poll", + "fast_poll": "Fast poll" + } + }, + "wifi_signal": { + "name": "Wi-Fi signal" + } } } } From 91273481a8d7b1cff86c72d9f428fb85e1e162a0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 21:52:25 +0200 Subject: [PATCH 0376/1009] Migrate number services to support translations (#96343) --- homeassistant/components/number/services.yaml | 4 ---- homeassistant/components/number/strings.json | 12 ++++++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/number/services.yaml b/homeassistant/components/number/services.yaml index 2014c4c5221da9..dcbb955d739873 100644 --- a/homeassistant/components/number/services.yaml +++ b/homeassistant/components/number/services.yaml @@ -1,15 +1,11 @@ # Describes the format for available Number entity services set_value: - name: Set - description: Set the value of a Number entity. target: entity: domain: number fields: value: - name: Value - description: The target value the entity should be set to. example: 42 selector: text: diff --git a/homeassistant/components/number/strings.json b/homeassistant/components/number/strings.json index 46db471305c315..e954a55b280edd 100644 --- a/homeassistant/components/number/strings.json +++ b/homeassistant/components/number/strings.json @@ -154,5 +154,17 @@ "wind_speed": { "name": "[%key:component::sensor::entity_component::wind_speed::name%]" } + }, + "services": { + "set_value": { + "name": "Set", + "description": "Sets the value of a number.", + "fields": { + "value": { + "name": "Value", + "description": "The target value to set." + } + } + } } } From 76e3272432ad58493d94ef27c3fc4a6a91670824 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 21:59:08 +0200 Subject: [PATCH 0377/1009] Migrate camera services to support translations (#96313) * Migrate camera services to support translations * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> * Update homeassistant/components/camera/strings.json Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/camera/services.yaml | 28 --------- homeassistant/components/camera/strings.json | 63 ++++++++++++++++++- 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml index 024bb927508900..55ac9f2bfeba03 100644 --- a/homeassistant/components/camera/services.yaml +++ b/homeassistant/components/camera/services.yaml @@ -1,65 +1,47 @@ # Describes the format for available camera services turn_off: - name: Turn off - description: Turn off camera. target: entity: domain: camera turn_on: - name: Turn on - description: Turn on camera. target: entity: domain: camera enable_motion_detection: - name: Enable motion detection - description: Enable the motion detection in a camera. target: entity: domain: camera disable_motion_detection: - name: Disable motion detection - description: Disable the motion detection in a camera. target: entity: domain: camera snapshot: - name: Take snapshot - description: Take a snapshot from a camera. target: entity: domain: camera fields: filename: - name: Filename - description: Template of a Filename. Variable is entity_id. required: true example: "/tmp/snapshot_{{ entity_id.name }}.jpg" selector: text: play_stream: - name: Play stream - description: Play camera stream on supported media player. target: entity: domain: camera fields: media_player: - name: Media Player - description: Name(s) of media player to stream to. required: true selector: entity: domain: media_player format: - name: Format - description: Stream format supported by media player. default: "hls" selector: select: @@ -67,22 +49,16 @@ play_stream: - "hls" record: - name: Record - description: Record live camera feed. target: entity: domain: camera fields: filename: - name: Filename - description: Template of a Filename. Variable is entity_id. Must be mp4. required: true example: "/tmp/snapshot_{{ entity_id.name }}.mp4" selector: text: duration: - name: Duration - description: Target recording length. default: 30 selector: number: @@ -90,10 +66,6 @@ record: max: 3600 unit_of_measurement: seconds lookback: - name: Lookback - description: - Target lookback period to include in addition to duration. Only - available if there is currently an active HLS stream. default: 0 selector: number: diff --git a/homeassistant/components/camera/strings.json b/homeassistant/components/camera/strings.json index 0722ec1c5e6dd2..ac061194d5cc49 100644 --- a/homeassistant/components/camera/strings.json +++ b/homeassistant/components/camera/strings.json @@ -34,5 +34,66 @@ } } } - } + }, + "services": { + "turn_off": { + "name": "Turn off", + "description": "Turns off the camera." + }, + "turn_on": { + "name": "Turn on", + "description": "Turns on the camera." + }, + "enable_motion_detection": { + "name": "Enable motion detection", + "description": "Enables the motion detection." + }, + "disable_motion_detection": { + "name": "Disable motion detection", + "description": "Disables the motion detection." + }, + "snapshot": { + "name": "Take snapshot", + "description": "Takes a snapshot from a camera.", + "fields": { + "filename": { + "name": "Filename", + "description": "Template of a filename. Variable available is `entity_id`." + } + } + }, + "play_stream": { + "name": "Play stream", + "description": "Plays the camera stream on a supported media player.", + "fields": { + "media_player": { + "name": "Media player", + "description": "Media players to stream to." + }, + "format": { + "name": "Format", + "description": "Stream format supported by the media player." + } + } + }, + "record": { + "name": "Record", + "description": "Creates a recording of a live camera feed.", + "fields": { + "filename": { + "name": "[%key:component::camera::services::snapshot::fields::filename::name%]", + "description": "Template of a filename. Variable available is `entity_id`. Must be mp4." + }, + "duration": { + "name": "Duration", + "description": "Planned duration of the recording. The actual duration may vary." + }, + "lookback": { + "name": "Lookback", + "description": "Planned lookback period to include in the recording (in addition to the duration). Only available if there is currently an active HLS stream. The actual length of the lookback period may vary." + } + } + } + }, + "selector": {} } From aea2fc68e78826fef534d73c017c2dc0e25621b7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 22:00:00 +0200 Subject: [PATCH 0378/1009] Migrate backup services to support translations (#96308) * Migrate backup services to support translations * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/backup/services.yaml | 2 -- homeassistant/components/backup/strings.json | 8 ++++++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/backup/strings.json diff --git a/homeassistant/components/backup/services.yaml b/homeassistant/components/backup/services.yaml index d001c57ef5caf3..900aa39dd6e404 100644 --- a/homeassistant/components/backup/services.yaml +++ b/homeassistant/components/backup/services.yaml @@ -1,3 +1 @@ create: - name: Create backup - description: Create a new backup. diff --git a/homeassistant/components/backup/strings.json b/homeassistant/components/backup/strings.json new file mode 100644 index 00000000000000..6ad3416b1b937d --- /dev/null +++ b/homeassistant/components/backup/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "create": { + "name": "Create backup", + "description": "Creates a new backup." + } + } +} From 0ff015c3adc3830ebca4b9d16a6d4ee440dc0467 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 23:04:27 +0200 Subject: [PATCH 0379/1009] Migrate integration services (A) to support translations (#96362) --- homeassistant/components/abode/services.yaml | 14 -- homeassistant/components/abode/strings.json | 36 +++++ .../components/adguard/services.yaml | 22 --- homeassistant/components/adguard/strings.json | 56 ++++++++ homeassistant/components/ads/services.yaml | 8 -- homeassistant/components/ads/strings.json | 22 +++ .../components/advantage_air/services.yaml | 4 - .../components/advantage_air/strings.json | 12 ++ .../components/aftership/services.yaml | 14 -- .../components/aftership/strings.json | 36 +++++ .../components/agent_dvr/services.yaml | 10 -- .../components/agent_dvr/strings.json | 22 +++ .../components/alarmdecoder/services.yaml | 8 -- .../components/alarmdecoder/strings.json | 26 +++- .../components/ambiclimate/services.yaml | 19 --- .../components/ambiclimate/strings.json | 40 ++++++ .../components/amcrest/services.yaml | 51 ------- homeassistant/components/amcrest/strings.json | 130 ++++++++++++++++++ .../components/androidtv/services.yaml | 18 --- .../components/androidtv/strings.json | 44 ++++++ 20 files changed, 423 insertions(+), 169 deletions(-) create mode 100644 homeassistant/components/ads/strings.json create mode 100644 homeassistant/components/aftership/strings.json create mode 100644 homeassistant/components/amcrest/strings.json diff --git a/homeassistant/components/abode/services.yaml b/homeassistant/components/abode/services.yaml index 843cc123c69b6f..f9d4e73a4e5c06 100644 --- a/homeassistant/components/abode/services.yaml +++ b/homeassistant/components/abode/services.yaml @@ -1,10 +1,6 @@ capture_image: - name: Capture image - description: Request a new image capture from a camera device. fields: entity_id: - name: Entity - description: Entity id of the camera to request an image. required: true selector: entity: @@ -12,31 +8,21 @@ capture_image: domain: camera change_setting: - name: Change setting - description: Change an Abode system setting. fields: setting: - name: Setting - description: Setting to change. required: true example: beeper_mute selector: text: value: - name: Value - description: Value of the setting. required: true example: "1" selector: text: trigger_automation: - name: Trigger automation - description: Trigger an Abode automation. fields: entity_id: - name: Entity - description: Entity id of the automation to trigger. required: true selector: entity: diff --git a/homeassistant/components/abode/strings.json b/homeassistant/components/abode/strings.json index b974007707e398..c0c32d487941b2 100644 --- a/homeassistant/components/abode/strings.json +++ b/homeassistant/components/abode/strings.json @@ -31,5 +31,41 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "services": { + "capture_image": { + "name": "Capture image", + "description": "Request a new image capture from a camera device.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Entity id of the camera to request an image." + } + } + }, + "change_setting": { + "name": "Change setting", + "description": "Change an Abode system setting.", + "fields": { + "setting": { + "name": "Setting", + "description": "Setting to change." + }, + "value": { + "name": "Value", + "description": "Value of the setting." + } + } + }, + "trigger_automation": { + "name": "Trigger automation", + "description": "Trigger an Abode automation.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Entity id of the automation to trigger." + } + } + } } } diff --git a/homeassistant/components/adguard/services.yaml b/homeassistant/components/adguard/services.yaml index 5e4c2a157de443..f38dc4ed86693f 100644 --- a/homeassistant/components/adguard/services.yaml +++ b/homeassistant/components/adguard/services.yaml @@ -1,65 +1,43 @@ add_url: - name: Add url - description: Add a new filter subscription to AdGuard Home. fields: name: - name: Name - description: The name of the filter subscription. required: true example: Example selector: text: url: - name: Url - description: The filter URL to subscribe to, containing the filter rules. required: true example: https://www.example.com/filter/1.txt selector: text: remove_url: - name: Remove url - description: Removes a filter subscription from AdGuard Home. fields: url: - name: Url - description: The filter subscription URL to remove. required: true example: https://www.example.com/filter/1.txt selector: text: enable_url: - name: Enable url - description: Enables a filter subscription in AdGuard Home. fields: url: - name: Url - description: The filter subscription URL to enable. required: true example: https://www.example.com/filter/1.txt selector: text: disable_url: - name: Disable url - description: Disables a filter subscription in AdGuard Home. fields: url: - name: Url - description: The filter subscription URL to disable. required: true example: https://www.example.com/filter/1.txt selector: text: refresh: - name: Refresh - description: Refresh all filter subscriptions in AdGuard Home. fields: force: - name: Force - description: Force update (bypasses AdGuard Home throttling). "true" to force, or "false" to omit for a regular refresh. default: false selector: boolean: diff --git a/homeassistant/components/adguard/strings.json b/homeassistant/components/adguard/strings.json index bde73e82b37b37..95ce968a67f778 100644 --- a/homeassistant/components/adguard/strings.json +++ b/homeassistant/components/adguard/strings.json @@ -72,5 +72,61 @@ "name": "Query log" } } + }, + "services": { + "add_url": { + "name": "Add URL", + "description": "Add a new filter subscription to AdGuard Home.", + "fields": { + "name": { + "name": "Name", + "description": "The name of the filter subscription." + }, + "url": { + "name": "URL", + "description": "The filter URL to subscribe to, containing the filter rules." + } + } + }, + "remove_url": { + "name": "Remove URL", + "description": "Removes a filter subscription from AdGuard Home.", + "fields": { + "url": { + "name": "URL", + "description": "The filter subscription URL to remove." + } + } + }, + "enable_url": { + "name": "Enable URL", + "description": "Enables a filter subscription in AdGuard Home.", + "fields": { + "url": { + "name": "URL", + "description": "The filter subscription URL to enable." + } + } + }, + "disable_url": { + "name": "Disable URL", + "description": "Disables a filter subscription in AdGuard Home.", + "fields": { + "url": { + "name": "URL", + "description": "The filter subscription URL to disable." + } + } + }, + "refresh": { + "name": "Refresh", + "description": "Refresh all filter subscriptions in AdGuard Home.", + "fields": { + "force": { + "name": "Force", + "description": "Force update (bypasses AdGuard Home throttling). \"true\" to force, or \"false\" to omit for a regular refresh." + } + } + } } } diff --git a/homeassistant/components/ads/services.yaml b/homeassistant/components/ads/services.yaml index 53c514bb587067..e2d5c60ada2be1 100644 --- a/homeassistant/components/ads/services.yaml +++ b/homeassistant/components/ads/services.yaml @@ -1,19 +1,13 @@ # Describes the format for available ADS services write_data_by_name: - name: Write data by name - description: Write a value to the connected ADS device. fields: adsvar: - name: ADS variable - description: The name of the variable to write to. required: true example: ".global_var" selector: text: adstype: - name: ADS type - description: The data type of the variable to write to. required: true selector: select: @@ -25,8 +19,6 @@ write_data_by_name: - "udint" - "uint" value: - name: Value - description: The value to write to the variable. required: true selector: number: diff --git a/homeassistant/components/ads/strings.json b/homeassistant/components/ads/strings.json new file mode 100644 index 00000000000000..fd34973a21d7e2 --- /dev/null +++ b/homeassistant/components/ads/strings.json @@ -0,0 +1,22 @@ +{ + "services": { + "write_data_by_name": { + "name": "Write data by name", + "description": "Write a value to the connected ADS device.", + "fields": { + "adsvar": { + "name": "ADS variable", + "description": "The name of the variable to write to." + }, + "adstype": { + "name": "ADS type", + "description": "The data type of the variable to write to." + }, + "value": { + "name": "Value", + "description": "The value to write to the variable." + } + } + } + } +} diff --git a/homeassistant/components/advantage_air/services.yaml b/homeassistant/components/advantage_air/services.yaml index 6bd3bf815d6412..cb93ef568fc847 100644 --- a/homeassistant/components/advantage_air/services.yaml +++ b/homeassistant/components/advantage_air/services.yaml @@ -1,14 +1,10 @@ set_time_to: - name: Set Time To - description: Control timers to turn the system on or off after a set number of minutes target: entity: integration: advantage_air domain: sensor fields: minutes: - name: Minutes - description: Minutes until action required: true selector: number: diff --git a/homeassistant/components/advantage_air/strings.json b/homeassistant/components/advantage_air/strings.json index 76ecb174f6d2ae..39681201766a2f 100644 --- a/homeassistant/components/advantage_air/strings.json +++ b/homeassistant/components/advantage_air/strings.json @@ -16,5 +16,17 @@ "title": "Connect" } } + }, + "services": { + "set_time_to": { + "name": "Set time to", + "description": "Controls timers to turn the system on or off after a set number of minutes.", + "fields": { + "minutes": { + "name": "Minutes", + "description": "Minutes until action." + } + } + } } } diff --git a/homeassistant/components/aftership/services.yaml b/homeassistant/components/aftership/services.yaml index 62e339dbda89ad..2950d5162dd030 100644 --- a/homeassistant/components/aftership/services.yaml +++ b/homeassistant/components/aftership/services.yaml @@ -1,43 +1,29 @@ # Describes the format for available aftership services add_tracking: - name: Add tracking - description: Add new tracking number to Aftership. fields: tracking_number: - name: Tracking number - description: Tracking number for the new tracking required: true example: "123456789" selector: text: title: - name: Title - description: A custom title for the new tracking example: "Laptop" selector: text: slug: - name: Slug - description: Slug (carrier) of the new tracking example: "USPS" selector: text: remove_tracking: - name: Remove tracking - description: Remove a tracking number from Aftership. fields: tracking_number: - name: Tracking number - description: Tracking number of the tracking to remove required: true example: "123456789" selector: text: slug: - name: Slug - description: Slug (carrier) of the tracking to remove example: "USPS" selector: text: diff --git a/homeassistant/components/aftership/strings.json b/homeassistant/components/aftership/strings.json new file mode 100644 index 00000000000000..602138e82f58aa --- /dev/null +++ b/homeassistant/components/aftership/strings.json @@ -0,0 +1,36 @@ +{ + "services": { + "add_tracking": { + "name": "Add tracking", + "description": "Adds a new tracking number to Aftership.", + "fields": { + "tracking_number": { + "name": "Tracking number", + "description": "Tracking number for the new tracking." + }, + "title": { + "name": "Title", + "description": "A custom title for the new tracking." + }, + "slug": { + "name": "Slug", + "description": "Slug (carrier) of the new tracking." + } + } + }, + "remove_tracking": { + "name": "Remove tracking", + "description": "Removes a tracking number from Aftership.", + "fields": { + "tracking_number": { + "name": "Tracking number", + "description": "Tracking number of the tracking to remove." + }, + "slug": { + "name": "Slug", + "description": "Slug (carrier) of the tracking to remove." + } + } + } + } +} diff --git a/homeassistant/components/agent_dvr/services.yaml b/homeassistant/components/agent_dvr/services.yaml index 206b32cb526e9a..6256cfaac1ed36 100644 --- a/homeassistant/components/agent_dvr/services.yaml +++ b/homeassistant/components/agent_dvr/services.yaml @@ -1,38 +1,28 @@ start_recording: - name: Start recording - description: Enable continuous recording. target: entity: integration: agent_dvr domain: camera stop_recording: - name: Stop recording - description: Disable continuous recording. target: entity: integration: agent_dvr domain: camera enable_alerts: - name: Enable alerts - description: Enable alerts target: entity: integration: agent_dvr domain: camera disable_alerts: - name: Disable alerts - description: Disable alerts target: entity: integration: agent_dvr domain: camera snapshot: - name: Snapshot - description: Take a photo target: entity: integration: agent_dvr diff --git a/homeassistant/components/agent_dvr/strings.json b/homeassistant/components/agent_dvr/strings.json index 127fbb69b332e7..77167b8294bd65 100644 --- a/homeassistant/components/agent_dvr/strings.json +++ b/homeassistant/components/agent_dvr/strings.json @@ -16,5 +16,27 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } + }, + "services": { + "start_recording": { + "name": "Start recording", + "description": "Enables continuous recording." + }, + "stop_recording": { + "name": "Stop recording", + "description": "Disables continuous recording." + }, + "enable_alerts": { + "name": "Enable alerts", + "description": "Enables alerts." + }, + "disable_alerts": { + "name": "Disable alerts", + "description": "Disables alerts." + }, + "snapshot": { + "name": "Snapshot", + "description": "Takes a photo." + } } } diff --git a/homeassistant/components/alarmdecoder/services.yaml b/homeassistant/components/alarmdecoder/services.yaml index 9d50eae07e68bd..91a6000e683bae 100644 --- a/homeassistant/components/alarmdecoder/services.yaml +++ b/homeassistant/components/alarmdecoder/services.yaml @@ -1,30 +1,22 @@ alarm_keypress: - name: Key press - description: Send custom keypresses to the alarm. target: entity: integration: alarmdecoder domain: alarm_control_panel fields: keypress: - name: Key press - description: "String to send to the alarm panel." required: true example: "*71" selector: text: alarm_toggle_chime: - name: Toggle Chime - description: Send the alarm the toggle chime command. target: entity: integration: alarmdecoder domain: alarm_control_panel fields: code: - name: Code - description: A code to toggle the alarm control panel chime with. required: true example: 1234 selector: diff --git a/homeassistant/components/alarmdecoder/strings.json b/homeassistant/components/alarmdecoder/strings.json index 33b3374904802a..585db4b1fa32e7 100644 --- a/homeassistant/components/alarmdecoder/strings.json +++ b/homeassistant/components/alarmdecoder/strings.json @@ -20,7 +20,9 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, - "create_entry": { "default": "Successfully connected to AlarmDecoder." }, + "create_entry": { + "default": "Successfully connected to AlarmDecoder." + }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } @@ -68,5 +70,27 @@ "loop_rfid": "RF Loop cannot be used without RF Serial.", "loop_range": "RF Loop must be an integer between 1 and 4." } + }, + "services": { + "alarm_keypress": { + "name": "Key press", + "description": "Sends custom keypresses to the alarm.", + "fields": { + "keypress": { + "name": "Key press", + "description": "String to send to the alarm panel." + } + } + }, + "alarm_toggle_chime": { + "name": "Toggle chime", + "description": "Sends the alarm the toggle chime command.", + "fields": { + "code": { + "name": "Code", + "description": "Code to toggle the alarm control panel chime with." + } + } + } } } diff --git a/homeassistant/components/ambiclimate/services.yaml b/homeassistant/components/ambiclimate/services.yaml index e5532ae82f9796..bf72d18b259fa6 100644 --- a/homeassistant/components/ambiclimate/services.yaml +++ b/homeassistant/components/ambiclimate/services.yaml @@ -1,53 +1,34 @@ # Describes the format for available services for ambiclimate set_comfort_mode: - name: Set comfort mode - description: > - Enable comfort mode on your AC. fields: name: - description: > - String with device name. required: true example: Bedroom selector: text: send_comfort_feedback: - name: Send comfort feedback - description: > - Send feedback for comfort mode. fields: name: - description: > - String with device name. required: true example: Bedroom selector: text: value: - description: > - Send any of the following comfort values: too_hot, too_warm, bit_warm, comfortable, bit_cold, too_cold, freezing required: true example: bit_warm selector: text: set_temperature_mode: - name: Set temperature mode - description: > - Enable temperature mode on your AC. fields: name: - description: > - String with device name. required: true example: Bedroom selector: text: value: - description: > - Target value in celsius required: true example: 22 selector: diff --git a/homeassistant/components/ambiclimate/strings.json b/homeassistant/components/ambiclimate/strings.json index c51c25a2f61cbe..2b55f7bebb60ad 100644 --- a/homeassistant/components/ambiclimate/strings.json +++ b/homeassistant/components/ambiclimate/strings.json @@ -18,5 +18,45 @@ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "access_token": "Unknown error generating an access token." } + }, + "services": { + "set_comfort_mode": { + "name": "Set comfort mode", + "description": "Enables comfort mode on your AC.", + "fields": { + "name": { + "name": "Device name", + "description": "String with device name." + } + } + }, + "send_comfort_feedback": { + "name": "Send comfort feedback", + "description": "Sends feedback for comfort mode.", + "fields": { + "name": { + "name": "[%key:component::ambiclimate::services::set_comfort_mode::fields::name::name%]", + "description": "[%key:component::ambiclimate::services::set_comfort_mode::fields::name::description%]" + }, + "value": { + "name": "Comfort value", + "description": "Send any of the following comfort values: too_hot, too_warm, bit_warm, comfortable, bit_cold, too_cold, freezing\n." + } + } + }, + "set_temperature_mode": { + "name": "Set temperature mode", + "description": "Enables temperature mode on your AC.", + "fields": { + "name": { + "name": "[%key:component::ambiclimate::services::set_comfort_mode::fields::name::name%]", + "description": "[%key:component::ambiclimate::services::set_comfort_mode::fields::name::description%]" + }, + "value": { + "name": "Temperature", + "description": "Target value in celsius." + } + } + } } } diff --git a/homeassistant/components/amcrest/services.yaml b/homeassistant/components/amcrest/services.yaml index b79a333101b2b7..cdcaf0e2c04c0a 100644 --- a/homeassistant/components/amcrest/services.yaml +++ b/homeassistant/components/amcrest/services.yaml @@ -1,82 +1,53 @@ enable_recording: - name: Enable recording - description: Enable continuous recording to camera storage. fields: entity_id: - name: Entity - description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" selector: text: disable_recording: - name: Disable recording - description: Disable continuous recording to camera storage. fields: entity_id: - name: Entity - description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" selector: text: enable_audio: - name: Enable audio - description: Enable audio stream. fields: entity_id: - name: Entity - description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" selector: text: disable_audio: - name: Disable audio - description: Disable audio stream. fields: entity_id: - name: Entity - description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" selector: text: enable_motion_recording: - name: Enable motion recording - description: Enable recording a clip to camera storage when motion is detected. fields: entity_id: - name: Entity - description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" selector: text: disable_motion_recording: - name: Disable motion recording - description: Disable recording a clip to camera storage when motion is detected. fields: entity_id: - name: Entity - description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" selector: text: goto_preset: - name: Go to preset - description: Move camera to PTZ preset. fields: entity_id: - description: "Name(s) of the cameras, or 'all' for all cameras." selector: entity: integration: amcrest domain: camera preset: - name: Preset - description: Preset number. required: true selector: number: @@ -84,18 +55,12 @@ goto_preset: max: 1000 set_color_bw: - name: Set color - description: Set camera color mode. fields: entity_id: - name: Entity - description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" selector: text: color_bw: - name: Color - description: Color mode. selector: select: options: @@ -104,40 +69,26 @@ set_color_bw: - "color" start_tour: - name: Start tour - description: Start camera's PTZ tour function. fields: entity_id: - name: Entity - description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" selector: text: stop_tour: - name: Stop tour - description: Stop camera's PTZ tour function. fields: entity_id: - name: Entity - description: "Name(s) of the cameras, or 'all' for all cameras." example: "camera.house_front" selector: text: ptz_control: - name: PTZ control - description: Move (Pan/Tilt) and/or Zoom a PTZ camera. fields: entity_id: - name: Entity - description: "Name of the camera, or 'all' for all cameras." example: "camera.house_front" selector: text: movement: - name: Movement - description: "Direction to move the camera." required: true selector: select: @@ -153,8 +104,6 @@ ptz_control: - "zoom_in" - "zoom_out" travel_time: - name: Travel time - description: "Travel time in fractional seconds: from 0 to 1." default: .2 selector: number: diff --git a/homeassistant/components/amcrest/strings.json b/homeassistant/components/amcrest/strings.json new file mode 100644 index 00000000000000..816511bf05e96a --- /dev/null +++ b/homeassistant/components/amcrest/strings.json @@ -0,0 +1,130 @@ +{ + "services": { + "enable_recording": { + "name": "Enable recording", + "description": "Enables continuous recording to camera storage.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name(s) of the cameras, or 'all' for all cameras." + } + } + }, + "disable_recording": { + "name": "Disable recording", + "description": "Disables continuous recording to camera storage.", + "fields": { + "entity_id": { + "name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]", + "description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]" + } + } + }, + "enable_audio": { + "name": "Enable audio", + "description": "Enables audio stream.", + "fields": { + "entity_id": { + "name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]", + "description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]" + } + } + }, + "disable_audio": { + "name": "Disable audio", + "description": "Disables audio stream.", + "fields": { + "entity_id": { + "name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]", + "description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]" + } + } + }, + "enable_motion_recording": { + "name": "Enables motion recording", + "description": "Enables recording a clip to camera storage when motion is detected.", + "fields": { + "entity_id": { + "name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]", + "description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]" + } + } + }, + "disable_motion_recording": { + "name": "Disables motion recording", + "description": "Disable recording a clip to camera storage when motion is detected.", + "fields": { + "entity_id": { + "name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]", + "description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]" + } + } + }, + "goto_preset": { + "name": "Go to preset", + "description": "Moves camera to PTZ preset.", + "fields": { + "entity_id": { + "name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]", + "description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]" + }, + "preset": { + "name": "Preset", + "description": "Preset number." + } + } + }, + "set_color_bw": { + "name": "Set color", + "description": "Sets camera color mode.", + "fields": { + "entity_id": { + "name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]", + "description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]" + }, + "color_bw": { + "name": "Color", + "description": "Color mode." + } + } + }, + "start_tour": { + "name": "Start tour", + "description": "Starts camera's PTZ tour function.", + "fields": { + "entity_id": { + "name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]", + "description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]" + } + } + }, + "stop_tour": { + "name": "Stop tour", + "description": "Stops camera's PTZ tour function.", + "fields": { + "entity_id": { + "name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]", + "description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]" + } + } + }, + "ptz_control": { + "name": "PTZ control", + "description": "Moves (pan/tilt) and/or zoom a PTZ camera.", + "fields": { + "entity_id": { + "name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]", + "description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]" + }, + "movement": { + "name": "Movement", + "description": "Direction to move the camera." + }, + "travel_time": { + "name": "Travel time", + "description": "Travel time in fractional seconds: from 0 to 1." + } + } + } + } +} diff --git a/homeassistant/components/androidtv/services.yaml b/homeassistant/components/androidtv/services.yaml index 4482f50f3e2672..41f7dbfea8f000 100644 --- a/homeassistant/components/androidtv/services.yaml +++ b/homeassistant/components/androidtv/services.yaml @@ -1,67 +1,49 @@ # Describes the format for available Android and Fire TV services adb_command: - name: ADB command - description: Send an ADB command to an Android / Fire TV device. target: entity: integration: androidtv domain: media_player fields: command: - name: Command - description: Either a key command or an ADB shell command. required: true example: "HOME" selector: text: download: - name: Download - description: Download a file from your Android / Fire TV device to your Home Assistant instance. target: entity: integration: androidtv domain: media_player fields: device_path: - name: Device path - description: The filepath on the Android / Fire TV device. required: true example: "/storage/emulated/0/Download/example.txt" selector: text: local_path: - name: Local path - description: The filepath on your Home Assistant instance. required: true example: "/config/www/example.txt" selector: text: upload: - name: Upload - description: Upload a file from your Home Assistant instance to an Android / Fire TV device. target: entity: integration: androidtv domain: media_player fields: device_path: - name: Device path - description: The filepath on the Android / Fire TV device. required: true example: "/storage/emulated/0/Download/example.txt" selector: text: local_path: - name: Local path - description: The filepath on your Home Assistant instance. required: true example: "/config/www/example.txt" selector: text: learn_sendevent: - name: Learn sendevent - description: Translate a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of calling this service. target: entity: integration: androidtv diff --git a/homeassistant/components/androidtv/strings.json b/homeassistant/components/androidtv/strings.json index e7d06a9f6248b2..9eb3d14a2253b1 100644 --- a/homeassistant/components/androidtv/strings.json +++ b/homeassistant/components/androidtv/strings.json @@ -59,5 +59,49 @@ "error": { "invalid_det_rules": "Invalid state detection rules" } + }, + "services": { + "adb_command": { + "name": "ADB command", + "description": "Sends an ADB command to an Android / Fire TV device.", + "fields": { + "command": { + "name": "Command", + "description": "Either a key command or an ADB shell command." + } + } + }, + "download": { + "name": "Download", + "description": "Downloads a file from your Android / Fire TV device to your Home Assistant instance.", + "fields": { + "device_path": { + "name": "Device path", + "description": "The filepath on the Android / Fire TV device." + }, + "local_path": { + "name": "Local path", + "description": "The filepath on your Home Assistant instance." + } + } + }, + "upload": { + "name": "Upload", + "description": "Uploads a file from your Home Assistant instance to an Android / Fire TV device.", + "fields": { + "device_path": { + "name": "Device path", + "description": "The filepath on the Android / Fire TV device." + }, + "local_path": { + "name": "Local path", + "description": "The filepath on your Home Assistant instance." + } + } + }, + "learn_sendevent": { + "name": "Learn sendevent", + "description": "Translates a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of calling this service." + } } } From c252758ac2dc825972b65f0951d28a3e06a44d92 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 23:06:32 +0200 Subject: [PATCH 0380/1009] Migrate integration services (B-D) to support translations (#96363) --- homeassistant/components/alert/services.yaml | 6 -- homeassistant/components/alert/strings.json | 14 ++++ .../components/blackbird/services.yaml | 6 -- .../components/blackbird/strings.json | 18 +++++ homeassistant/components/blink/services.yaml | 21 ------ homeassistant/components/blink/strings.json | 52 +++++++++++++- .../components/bluesound/services.yaml | 18 ----- .../components/bluesound/strings.json | 48 +++++++++++++ .../bluetooth_tracker/services.yaml | 2 - .../components/bluetooth_tracker/strings.json | 8 +++ homeassistant/components/bond/services.yaml | 30 -------- homeassistant/components/bond/strings.json | 70 +++++++++++++++++++ .../components/browser/services.yaml | 4 -- homeassistant/components/browser/strings.json | 14 ++++ homeassistant/components/cast/services.yaml | 8 --- homeassistant/components/cast/strings.json | 22 +++++- .../components/channels/services.yaml | 8 --- .../components/channels/strings.json | 22 ++++++ .../components/cloudflare/services.yaml | 2 - .../components/cloudflare/strings.json | 6 ++ .../components/color_extractor/services.yaml | 12 ---- .../components/color_extractor/strings.json | 18 +++++ .../components/command_line/services.yaml | 2 - .../components/command_line/strings.json | 6 ++ .../components/counter/services.yaml | 10 --- homeassistant/components/counter/strings.json | 24 +++++++ .../components/debugpy/services.yaml | 2 - homeassistant/components/debugpy/strings.json | 8 +++ homeassistant/components/deconz/services.yaml | 32 --------- homeassistant/components/deconz/strings.json | 44 ++++++++++++ .../components/denonavr/services.yaml | 9 --- .../components/denonavr/strings.json | 26 +++++++ .../components/dominos/services.yaml | 4 -- homeassistant/components/dominos/strings.json | 14 ++++ .../components/downloader/services.yaml | 10 --- .../components/downloader/strings.json | 26 +++++++ .../components/duckdns/services.yaml | 4 -- homeassistant/components/duckdns/strings.json | 14 ++++ .../components/dynalite/services.yaml | 13 ---- .../components/dynalite/strings.json | 38 ++++++++++ 40 files changed, 490 insertions(+), 205 deletions(-) create mode 100644 homeassistant/components/blackbird/strings.json create mode 100644 homeassistant/components/bluesound/strings.json create mode 100644 homeassistant/components/bluetooth_tracker/strings.json create mode 100644 homeassistant/components/browser/strings.json create mode 100644 homeassistant/components/channels/strings.json create mode 100644 homeassistant/components/color_extractor/strings.json create mode 100644 homeassistant/components/debugpy/strings.json create mode 100644 homeassistant/components/dominos/strings.json create mode 100644 homeassistant/components/downloader/strings.json create mode 100644 homeassistant/components/duckdns/strings.json diff --git a/homeassistant/components/alert/services.yaml b/homeassistant/components/alert/services.yaml index 3242a9cedb4f02..e1d842f5bc4026 100644 --- a/homeassistant/components/alert/services.yaml +++ b/homeassistant/components/alert/services.yaml @@ -1,20 +1,14 @@ toggle: - name: Toggle - description: Toggle alert's notifications. target: entity: domain: alert turn_off: - name: Turn off - description: Silence alert's notifications. target: entity: domain: alert turn_on: - name: Turn on - description: Reset alert's notifications. target: entity: domain: alert diff --git a/homeassistant/components/alert/strings.json b/homeassistant/components/alert/strings.json index 4d948b2f4d11b1..16192d5d59561d 100644 --- a/homeassistant/components/alert/strings.json +++ b/homeassistant/components/alert/strings.json @@ -9,5 +9,19 @@ "on": "[%key:common::state::active%]" } } + }, + "services": { + "toggle": { + "name": "Toggle", + "description": "Toggles alert's notifications." + }, + "turn_off": { + "name": "Turn off", + "description": "Silences alert's notifications." + }, + "turn_on": { + "name": "Turn on", + "description": "Resets alert's notifications." + } } } diff --git a/homeassistant/components/blackbird/services.yaml b/homeassistant/components/blackbird/services.yaml index 7b3096c25e4f61..00425c93eb6001 100644 --- a/homeassistant/components/blackbird/services.yaml +++ b/homeassistant/components/blackbird/services.yaml @@ -1,10 +1,6 @@ set_all_zones: - name: Set all zones - description: Set all Blackbird zones to a single source. fields: entity_id: - name: Entity - description: Name of any blackbird zone. required: true example: "media_player.zone_1" selector: @@ -12,8 +8,6 @@ set_all_zones: integration: blackbird domain: media_player source: - name: Source - description: Name of source to switch to. required: true example: "Source 1" selector: diff --git a/homeassistant/components/blackbird/strings.json b/homeassistant/components/blackbird/strings.json new file mode 100644 index 00000000000000..93c0e6ef23dbc4 --- /dev/null +++ b/homeassistant/components/blackbird/strings.json @@ -0,0 +1,18 @@ +{ + "services": { + "set_all_zones": { + "name": "Set all zones", + "description": "Sets all Blackbird zones to a single source.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name of any blackbird zone." + }, + "source": { + "name": "Source", + "description": "Name of source to switch to." + } + } + } + } +} diff --git a/homeassistant/components/blink/services.yaml b/homeassistant/components/blink/services.yaml index 3d51ba2f7bbe56..95f4d33f91f732 100644 --- a/homeassistant/components/blink/services.yaml +++ b/homeassistant/components/blink/services.yaml @@ -1,62 +1,41 @@ # Describes the format for available Blink services blink_update: - name: Update - description: Force a refresh. - trigger_camera: - name: Trigger camera - description: Request camera to take new image. target: entity: integration: blink domain: camera save_video: - name: Save video - description: Save last recorded video clip to local file. fields: name: - name: Name - description: Name of camera to grab video from. required: true example: "Living Room" selector: text: filename: - name: File name - description: Filename to writable path (directory may need to be included in allowlist_external_dirs in config) required: true example: "/tmp/video.mp4" selector: text: save_recent_clips: - name: Save recent clips - description: 'Save all recent video clips to local directory with file pattern "%Y%m%d_%H%M%S_{name}.mp4"' fields: name: - name: Name - description: Name of camera to grab recent clips from. required: true example: "Living Room" selector: text: file_path: - name: Output directory - description: Directory name of writable path (directory may need to be included in allowlist_external_dirs in config) required: true example: "/tmp" selector: text: send_pin: - name: Send pin - description: Send a new PIN to blink for 2FA. fields: pin: - name: Pin - description: PIN received from blink. Leave empty if you only received a verification email. example: "abc123" selector: text: diff --git a/homeassistant/components/blink/strings.json b/homeassistant/components/blink/strings.json index 61c9a21af37043..6c07d1fea5513f 100644 --- a/homeassistant/components/blink/strings.json +++ b/homeassistant/components/blink/strings.json @@ -10,7 +10,9 @@ }, "2fa": { "title": "Two-factor authentication", - "data": { "2fa": "Two-factor code" }, + "data": { + "2fa": "Two-factor code" + }, "description": "Enter the PIN sent via email or SMS" } }, @@ -46,5 +48,53 @@ "name": "Camera armed" } } + }, + "services": { + "blink_update": { + "name": "Update", + "description": "Forces a refresh." + }, + "trigger_camera": { + "name": "Trigger camera", + "description": "Requests camera to take new image." + }, + "save_video": { + "name": "Save video", + "description": "Saves last recorded video clip to local file.", + "fields": { + "name": { + "name": "Name", + "description": "Name of camera to grab video from." + }, + "filename": { + "name": "File name", + "description": "Filename to writable path (directory may need to be included in allowlist_external_dirs in config)." + } + } + }, + "save_recent_clips": { + "name": "Save recent clips", + "description": "Saves all recent video clips to local directory with file pattern \"%Y%m%d_%H%M%S_{name}.mp4\".", + "fields": { + "name": { + "name": "Name", + "description": "Name of camera to grab recent clips from." + }, + "file_path": { + "name": "Output directory", + "description": "Directory name of writable path (directory may need to be included in allowlist_external_dirs in config)." + } + } + }, + "send_pin": { + "name": "Send pin", + "description": "Sends a new PIN to blink for 2FA.", + "fields": { + "pin": { + "name": "Pin", + "description": "PIN received from blink. Leave empty if you only received a verification email." + } + } + } } } diff --git a/homeassistant/components/bluesound/services.yaml b/homeassistant/components/bluesound/services.yaml index 7c04cc00f39851..7ab69a82124211 100644 --- a/homeassistant/components/bluesound/services.yaml +++ b/homeassistant/components/bluesound/services.yaml @@ -1,54 +1,36 @@ join: - name: Join - description: Group player together. fields: master: - name: Master - description: Entity ID of the player that should become the master of the group. required: true selector: entity: integration: bluesound domain: media_player entity_id: - name: Entity - description: Name of entity that will coordinate the grouping. Platform dependent. selector: entity: integration: bluesound domain: media_player unjoin: - name: Unjoin - description: Unjoin the player from a group. fields: entity_id: - name: Entity - description: Name of entity that will be unjoined from their group. Platform dependent. selector: entity: integration: bluesound domain: media_player set_sleep_timer: - name: Set sleep timer - description: "Set a Bluesound timer. It will increase timer in steps: 15, 30, 45, 60, 90, 0" fields: entity_id: - name: Entity - description: Name(s) of entities that will have a timer set. selector: entity: integration: bluesound domain: media_player clear_sleep_timer: - name: Clear sleep timer - description: Clear a Bluesound timer. fields: entity_id: - name: Entity - description: Name(s) of entities that will have the timer cleared. selector: entity: integration: bluesound diff --git a/homeassistant/components/bluesound/strings.json b/homeassistant/components/bluesound/strings.json new file mode 100644 index 00000000000000..f41c34a7449ac2 --- /dev/null +++ b/homeassistant/components/bluesound/strings.json @@ -0,0 +1,48 @@ +{ + "services": { + "join": { + "name": "Join", + "description": "Group player together.", + "fields": { + "master": { + "name": "Master", + "description": "Entity ID of the player that should become the master of the group." + }, + "entity_id": { + "name": "Entity", + "description": "Name of entity that will coordinate the grouping. Platform dependent." + } + } + }, + "unjoin": { + "name": "Unjoin", + "description": "Unjoin the player from a group.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name of entity that will be unjoined from their group. Platform dependent." + } + } + }, + "set_sleep_timer": { + "name": "Set sleep timer", + "description": "Set a Bluesound timer. It will increase timer in steps: 15, 30, 45, 60, 90, 0.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name(s) of entities that will have a timer set." + } + } + }, + "clear_sleep_timer": { + "name": "Clear sleep timer", + "description": "Clear a Bluesound timer.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name(s) of entities that will have the timer cleared." + } + } + } + } +} diff --git a/homeassistant/components/bluetooth_tracker/services.yaml b/homeassistant/components/bluetooth_tracker/services.yaml index 3150403dbf1057..91b8669505b380 100644 --- a/homeassistant/components/bluetooth_tracker/services.yaml +++ b/homeassistant/components/bluetooth_tracker/services.yaml @@ -1,3 +1 @@ update: - name: Update - description: Trigger manual tracker update diff --git a/homeassistant/components/bluetooth_tracker/strings.json b/homeassistant/components/bluetooth_tracker/strings.json new file mode 100644 index 00000000000000..bf22845d054ab5 --- /dev/null +++ b/homeassistant/components/bluetooth_tracker/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "update": { + "name": "Update", + "description": "Triggers manual tracker update." + } + } +} diff --git a/homeassistant/components/bond/services.yaml b/homeassistant/components/bond/services.yaml index 6be18eaa1ef5e9..bda0bc5835f23c 100644 --- a/homeassistant/components/bond/services.yaml +++ b/homeassistant/components/bond/services.yaml @@ -1,13 +1,9 @@ # Describes the format for available bond services set_fan_speed_tracked_state: - name: Set fan speed tracked state - description: Sets the tracked fan speed for a bond fan fields: entity_id: - description: Name(s) of entities to set the tracked fan speed. example: "fan.living_room_fan" - name: Entity required: true selector: entity: @@ -15,8 +11,6 @@ set_fan_speed_tracked_state: domain: fan speed: required: true - name: Fan Speed - description: Fan Speed as %. example: 50 selector: number: @@ -26,13 +20,9 @@ set_fan_speed_tracked_state: mode: slider set_switch_power_tracked_state: - name: Set switch power tracked state - description: Sets the tracked power state of a bond switch fields: entity_id: - description: Name(s) of entities to set the tracked power state of. example: "switch.whatever" - name: Entity required: true selector: entity: @@ -40,20 +30,14 @@ set_switch_power_tracked_state: domain: switch power_state: required: true - name: Power state - description: Power state example: true selector: boolean: set_light_power_tracked_state: - name: Set light power tracked state - description: Sets the tracked power state of a bond light fields: entity_id: - description: Name(s) of entities to set the tracked power state of. example: "light.living_room_lights" - name: Entity required: true selector: entity: @@ -61,20 +45,14 @@ set_light_power_tracked_state: domain: light power_state: required: true - name: Power state - description: Power state example: true selector: boolean: set_light_brightness_tracked_state: - name: Set light brightness tracked state - description: Sets the tracked brightness state of a bond light fields: entity_id: - description: Name(s) of entities to set the tracked brightness state of. example: "light.living_room_lights" - name: Entity required: true selector: entity: @@ -82,8 +60,6 @@ set_light_brightness_tracked_state: domain: light brightness: required: true - name: Brightness - description: Brightness example: 50 selector: number: @@ -93,24 +69,18 @@ set_light_brightness_tracked_state: mode: slider start_increasing_brightness: - name: Start increasing brightness - description: "Start increasing the brightness of the light. (deprecated)" target: entity: integration: bond domain: light start_decreasing_brightness: - name: Start decreasing brightness - description: "Start decreasing the brightness of the light. (deprecated)" target: entity: integration: bond domain: light stop: - name: Stop - description: "Stop any in-progress action and empty the queue. (deprecated)" target: entity: integration: bond diff --git a/homeassistant/components/bond/strings.json b/homeassistant/components/bond/strings.json index e923ded939e301..9cbd895683c563 100644 --- a/homeassistant/components/bond/strings.json +++ b/homeassistant/components/bond/strings.json @@ -24,5 +24,75 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "services": { + "set_fan_speed_tracked_state": { + "name": "Set fan speed tracked state", + "description": "Sets the tracked fan speed for a bond fan.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name(s) of entities to set the tracked fan speed." + }, + "speed": { + "name": "Fan Speed", + "description": "Fan Speed as %." + } + } + }, + "set_switch_power_tracked_state": { + "name": "Set switch power tracked state", + "description": "Sets the tracked power state of a bond switch.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name(s) of entities to set the tracked power state of." + }, + "power_state": { + "name": "Power state", + "description": "Power state." + } + } + }, + "set_light_power_tracked_state": { + "name": "Set light power tracked state", + "description": "Sets the tracked power state of a bond light.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name(s) of entities to set the tracked power state of." + }, + "power_state": { + "name": "Power state", + "description": "Power state." + } + } + }, + "set_light_brightness_tracked_state": { + "name": "Set light brightness tracked state", + "description": "Sets the tracked brightness state of a bond light.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name(s) of entities to set the tracked brightness state of." + }, + "brightness": { + "name": "Brightness", + "description": "Brightness." + } + } + }, + "start_increasing_brightness": { + "name": "Start increasing brightness", + "description": "Start increasing the brightness of the light. (deprecated)." + }, + "start_decreasing_brightness": { + "name": "Start decreasing brightness", + "description": "Start decreasing the brightness of the light. (deprecated)." + }, + "stop": { + "name": "Stop", + "description": "Stop any in-progress action and empty the queue. (deprecated)." + } } } diff --git a/homeassistant/components/browser/services.yaml b/homeassistant/components/browser/services.yaml index dd3ddd095cc45c..c2192911eeabda 100644 --- a/homeassistant/components/browser/services.yaml +++ b/homeassistant/components/browser/services.yaml @@ -1,10 +1,6 @@ browse_url: - name: Browse - description: Open a URL in the default browser on the host machine of Home Assistant. fields: url: - name: URL - description: The URL to open. required: true example: "https://www.home-assistant.io" selector: diff --git a/homeassistant/components/browser/strings.json b/homeassistant/components/browser/strings.json new file mode 100644 index 00000000000000..fafd5fb96b006f --- /dev/null +++ b/homeassistant/components/browser/strings.json @@ -0,0 +1,14 @@ +{ + "services": { + "browse_url": { + "name": "Browse", + "description": "Opens a URL in the default browser on the host machine of Home Assistant.", + "fields": { + "url": { + "name": "URL", + "description": "The URL to open." + } + } + } + } +} diff --git a/homeassistant/components/cast/services.yaml b/homeassistant/components/cast/services.yaml index f0fbcf4a8d757b..e2e23ad40a2dd6 100644 --- a/homeassistant/components/cast/services.yaml +++ b/homeassistant/components/cast/services.yaml @@ -1,25 +1,17 @@ show_lovelace_view: - name: Show lovelace view - description: Show a Lovelace view on a Chromecast. fields: entity_id: - name: Entity - description: Media Player entity to show the Lovelace view on. required: true selector: entity: integration: cast domain: media_player dashboard_path: - name: Dashboard path - description: The URL path of the Lovelace dashboard to show. required: true example: lovelace-cast selector: text: view_path: - name: View Path - description: The path of the Lovelace view to show. example: downstairs selector: text: diff --git a/homeassistant/components/cast/strings.json b/homeassistant/components/cast/strings.json index 719465e98ca182..4de0f85851f307 100644 --- a/homeassistant/components/cast/strings.json +++ b/homeassistant/components/cast/strings.json @@ -30,7 +30,7 @@ }, "advanced_options": { "title": "Advanced Google Cast configuration", - "description": "Allowed UUIDs - A comma-separated list of UUIDs of Cast devices to add to Home Assistant. Use only if you don’t want to add all available cast devices.\nIgnore CEC - A comma-separated list of Chromecasts that should ignore CEC data for determining the active input. This will be passed to pychromecast.IGNORE_CEC.", + "description": "Allowed UUIDs - A comma-separated list of UUIDs of Cast devices to add to Home Assistant. Use only if you don\u2019t want to add all available cast devices.\nIgnore CEC - A comma-separated list of Chromecasts that should ignore CEC data for determining the active input. This will be passed to pychromecast.IGNORE_CEC.", "data": { "ignore_cec": "Ignore CEC", "uuid": "Allowed UUIDs" @@ -40,5 +40,25 @@ "error": { "invalid_known_hosts": "Known hosts must be a comma separated list of hosts." } + }, + "services": { + "show_lovelace_view": { + "name": "Show dashboard view", + "description": "Shows a dashboard view on a Chromecast device.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Media player entity to show the dashboard view on." + }, + "dashboard_path": { + "name": "Dashboard path", + "description": "The URL path of the dashboard to show." + }, + "view_path": { + "name": "View path", + "description": "The path of the dashboard view to show." + } + } + } } } diff --git a/homeassistant/components/channels/services.yaml b/homeassistant/components/channels/services.yaml index 5aa2f1ebda73d2..73ac6675ccf8d6 100644 --- a/homeassistant/components/channels/services.yaml +++ b/homeassistant/components/channels/services.yaml @@ -1,30 +1,22 @@ seek_forward: - name: Seek forward - description: Seek forward by a set number of seconds. target: entity: integration: channels domain: media_player seek_backward: - name: Seek backward - description: Seek backward by a set number of seconds. target: entity: integration: channels domain: media_player seek_by: - name: Seek by - description: Seek by an inputted number of seconds. target: entity: integration: channels domain: media_player fields: seconds: - name: Seconds - description: Number of seconds to seek by. Negative numbers seek backwards. required: true selector: number: diff --git a/homeassistant/components/channels/strings.json b/homeassistant/components/channels/strings.json new file mode 100644 index 00000000000000..0eceed8a8e0b6d --- /dev/null +++ b/homeassistant/components/channels/strings.json @@ -0,0 +1,22 @@ +{ + "services": { + "seek_forward": { + "name": "Seek forward", + "description": "Seeks forward by a set number of seconds." + }, + "seek_backward": { + "name": "Seek backward", + "description": "Seeks backward by a set number of seconds." + }, + "seek_by": { + "name": "Seek by", + "description": "Seeks by an inputted number of seconds.", + "fields": { + "seconds": { + "name": "Seconds", + "description": "Number of seconds to seek by. Negative numbers seek backwards." + } + } + } + } +} diff --git a/homeassistant/components/cloudflare/services.yaml b/homeassistant/components/cloudflare/services.yaml index f9465e788d89e1..e800a3a3eeebb7 100644 --- a/homeassistant/components/cloudflare/services.yaml +++ b/homeassistant/components/cloudflare/services.yaml @@ -1,3 +1 @@ update_records: - name: Update records - description: Manually trigger update to Cloudflare records diff --git a/homeassistant/components/cloudflare/strings.json b/homeassistant/components/cloudflare/strings.json index 89bc67feeed0f7..080be414b5caad 100644 --- a/homeassistant/components/cloudflare/strings.json +++ b/homeassistant/components/cloudflare/strings.json @@ -38,5 +38,11 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "unknown": "[%key:common::config_flow::error::unknown%]" } + }, + "services": { + "update_records": { + "name": "Update records", + "description": "Manually trigger update to Cloudflare records." + } } } diff --git a/homeassistant/components/color_extractor/services.yaml b/homeassistant/components/color_extractor/services.yaml index be278a59059427..2fd0b0db8152bd 100644 --- a/homeassistant/components/color_extractor/services.yaml +++ b/homeassistant/components/color_extractor/services.yaml @@ -1,25 +1,13 @@ turn_on: - name: Turn on - description: - Set the light RGB to the predominant color found in the image provided by - URL or file path. target: entity: domain: light fields: color_extract_url: - name: URL - description: - The URL of the image we want to extract RGB values from. Must be allowed - in allowlist_external_urls. example: https://www.example.com/images/logo.png selector: text: color_extract_path: - name: Path - description: - The full system path to the image we want to extract RGB values from. - Must be allowed in allowlist_external_dirs. example: /opt/images/logo.png selector: text: diff --git a/homeassistant/components/color_extractor/strings.json b/homeassistant/components/color_extractor/strings.json new file mode 100644 index 00000000000000..df720586631552 --- /dev/null +++ b/homeassistant/components/color_extractor/strings.json @@ -0,0 +1,18 @@ +{ + "services": { + "turn_on": { + "name": "Turn on", + "description": "Sets the light RGB to the predominant color found in the image provided by URL or file path.", + "fields": { + "color_extract_url": { + "name": "URL", + "description": "The URL of the image we want to extract RGB values from. Must be allowed in allowlist_external_urls." + }, + "color_extract_path": { + "name": "Path", + "description": "The full system path to the image we want to extract RGB values from. Must be allowed in allowlist_external_dirs." + } + } + } + } +} diff --git a/homeassistant/components/command_line/services.yaml b/homeassistant/components/command_line/services.yaml index f4cec426860635..c983a105c93977 100644 --- a/homeassistant/components/command_line/services.yaml +++ b/homeassistant/components/command_line/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload all command_line entities diff --git a/homeassistant/components/command_line/strings.json b/homeassistant/components/command_line/strings.json index dab4a77a6ec37d..e249ad877d56e1 100644 --- a/homeassistant/components/command_line/strings.json +++ b/homeassistant/components/command_line/strings.json @@ -4,5 +4,11 @@ "title": "Command Line YAML configuration has moved", "description": "Configuring Command Line `{platform}` using YAML has moved.\n\nConsult the documentation to move your YAML configuration to integration key and restart Home Assistant to fix this issue." } + }, + "services": { + "reload": { + "name": "Reload", + "description": "Reloads command line configuration from the YAML-configuration." + } } } diff --git a/homeassistant/components/counter/services.yaml b/homeassistant/components/counter/services.yaml index 835d39c9d2e038..643fc2230831bb 100644 --- a/homeassistant/components/counter/services.yaml +++ b/homeassistant/components/counter/services.yaml @@ -1,37 +1,27 @@ # Describes the format for available counter services decrement: - name: Decrement - description: Decrement a counter. target: entity: domain: counter increment: - name: Increment - description: Increment a counter. target: entity: domain: counter reset: - name: Reset - description: Reset a counter. target: entity: domain: counter set_value: - name: Set - description: Set the counter value target: entity: domain: counter fields: value: - name: Value required: true - description: The new counter value the entity should be set to. selector: number: min: 0 diff --git a/homeassistant/components/counter/strings.json b/homeassistant/components/counter/strings.json index 6dcfe14a03a241..0446b2447872e0 100644 --- a/homeassistant/components/counter/strings.json +++ b/homeassistant/components/counter/strings.json @@ -38,5 +38,29 @@ } } } + }, + "services": { + "decrement": { + "name": "Decrement", + "description": "Decrements a counter." + }, + "increment": { + "name": "Increment", + "description": "Increments a counter." + }, + "reset": { + "name": "Reset", + "description": "Resets a counter." + }, + "set_value": { + "name": "Set", + "description": "Sets the counter value.", + "fields": { + "value": { + "name": "Value", + "description": "The new counter value the entity should be set to." + } + } + } } } diff --git a/homeassistant/components/debugpy/services.yaml b/homeassistant/components/debugpy/services.yaml index c864684226f053..453b3af46bdd0a 100644 --- a/homeassistant/components/debugpy/services.yaml +++ b/homeassistant/components/debugpy/services.yaml @@ -1,4 +1,2 @@ # Describes the format for available Remote Python Debugger services start: - name: Start - description: Start the Remote Python Debugger diff --git a/homeassistant/components/debugpy/strings.json b/homeassistant/components/debugpy/strings.json new file mode 100644 index 00000000000000..b03a57a51dcf20 --- /dev/null +++ b/homeassistant/components/debugpy/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "start": { + "name": "Start", + "description": "Starts the Remote Python Debugger." + } + } +} diff --git a/homeassistant/components/deconz/services.yaml b/homeassistant/components/deconz/services.yaml index 9084728a216f63..d08312852b385c 100644 --- a/homeassistant/components/deconz/services.yaml +++ b/homeassistant/components/deconz/services.yaml @@ -1,65 +1,33 @@ configure: - name: Configure - description: >- - Configure attributes of either a device endpoint in deCONZ - or the deCONZ service itself. fields: entity: - name: Entity - description: Represents a specific device endpoint in deCONZ. selector: entity: integration: deconz field: - name: Path - description: >- - String representing a full path to deCONZ endpoint (when - entity is not specified) or a subpath of the device path for the - entity (when entity is specified). example: '"/lights/1/state" or "/state"' selector: text: data: - name: Configuration payload - description: JSON object with what data you want to alter. required: true example: '{"on": true}' selector: object: bridgeid: - name: Bridge identifier - description: >- - Unique string for each deCONZ hardware. - It can be found as part of the integration name. - Useful if you run multiple deCONZ integrations. example: "00212EFFFF012345" selector: text: device_refresh: - name: Device refresh - description: Refresh available devices from deCONZ. fields: bridgeid: - name: Bridge identifier - description: >- - Unique string for each deCONZ hardware. - It can be found as part of the integration name. - Useful if you run multiple deCONZ integrations. example: "00212EFFFF012345" selector: text: remove_orphaned_entries: - name: Remove orphaned entries - description: Clean up device and entity registry entries orphaned by deCONZ. fields: bridgeid: - name: Bridge identifier - description: >- - Unique string for each deCONZ hardware. - It can be found as part of the integration name. - Useful if you run multiple deCONZ integrations. example: "00212EFFFF012345" selector: text: diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 45a19b0466da7f..448a221b2ca58e 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -105,5 +105,49 @@ "side_5": "Side 5", "side_6": "Side 6" } + }, + "services": { + "configure": { + "name": "Configure", + "description": "Configures attributes of either a device endpoint in deCONZ or the deCONZ service itself.", + "fields": { + "entity": { + "name": "Entity", + "description": "Represents a specific device endpoint in deCONZ." + }, + "field": { + "name": "Path", + "description": "String representing a full path to deCONZ endpoint (when entity is not specified) or a subpath of the device path for the entity (when entity is specified)." + }, + "data": { + "name": "Configuration payload", + "description": "JSON object with what data you want to alter." + }, + "bridgeid": { + "name": "Bridge identifier", + "description": "Unique string for each deCONZ hardware. It can be found as part of the integration name. Useful if you run multiple deCONZ integrations." + } + } + }, + "device_refresh": { + "name": "Device refresh", + "description": "Refreshes available devices from deCONZ.", + "fields": { + "bridgeid": { + "name": "Bridge identifier", + "description": "Unique string for each deCONZ hardware. It can be found as part of the integration name. Useful if you run multiple deCONZ integrations." + } + } + }, + "remove_orphaned_entries": { + "name": "Remove orphaned entries", + "description": "Cleans up device and entity registry entries orphaned by deCONZ.", + "fields": { + "bridgeid": { + "name": "Bridge identifier", + "description": "Unique string for each deCONZ hardware. It can be found as part of the integration name. Useful if you run multiple deCONZ integrations." + } + } + } } } diff --git a/homeassistant/components/denonavr/services.yaml b/homeassistant/components/denonavr/services.yaml index ee35732e3117e6..9c53ff9994a981 100644 --- a/homeassistant/components/denonavr/services.yaml +++ b/homeassistant/components/denonavr/services.yaml @@ -1,36 +1,27 @@ # Describes the format for available denonavr services get_command: - name: Get command - description: "Send a generic HTTP get command." target: entity: integration: denonavr domain: media_player fields: command: - name: Command - description: Endpoint of the command, including associated parameters. example: "/goform/formiPhoneAppDirect.xml?RCKSK0410370" required: true selector: text: set_dynamic_eq: - name: Set dynamic equalizer - description: "Enable or disable DynamicEQ." target: entity: integration: denonavr domain: media_player fields: dynamic_eq: - description: "True/false for enable/disable." default: true selector: boolean: update_audyssey: - name: Update audyssey - description: "Update Audyssey settings." target: entity: integration: denonavr diff --git a/homeassistant/components/denonavr/strings.json b/homeassistant/components/denonavr/strings.json index 1c85efc9ff4d29..a4e07e33a6a815 100644 --- a/homeassistant/components/denonavr/strings.json +++ b/homeassistant/components/denonavr/strings.json @@ -46,5 +46,31 @@ } } } + }, + "services": { + "get_command": { + "name": "Get command", + "description": "Send sa generic HTTP get command.", + "fields": { + "command": { + "name": "Command", + "description": "Endpoint of the command, including associated parameters." + } + } + }, + "set_dynamic_eq": { + "name": "Set dynamic equalizer", + "description": "Enables or disables DynamicEQ.", + "fields": { + "dynamic_eq": { + "name": "Dynamic equalizer", + "description": "True/false for enable/disable." + } + } + }, + "update_audyssey": { + "name": "Update Audyssey", + "description": "Updates Audyssey settings." + } } } diff --git a/homeassistant/components/dominos/services.yaml b/homeassistant/components/dominos/services.yaml index 6a354bc3a63a34..f2261072ddd182 100644 --- a/homeassistant/components/dominos/services.yaml +++ b/homeassistant/components/dominos/services.yaml @@ -1,10 +1,6 @@ order: - name: Order - description: Places a set of orders with Dominos Pizza. fields: order_entity_id: - name: Order Entity - description: The ID (as specified in the configuration) of an order to place. If provided as an array, all of the identified orders will be placed. example: dominos.medium_pan selector: text: diff --git a/homeassistant/components/dominos/strings.json b/homeassistant/components/dominos/strings.json new file mode 100644 index 00000000000000..0ceabd7abe8c48 --- /dev/null +++ b/homeassistant/components/dominos/strings.json @@ -0,0 +1,14 @@ +{ + "services": { + "order": { + "name": "Order", + "description": "Places a set of orders with Dominos Pizza.", + "fields": { + "order_entity_id": { + "name": "Order entity", + "description": "The ID (as specified in the configuration) of an order to place. If provided as an array, all of the identified orders will be placed." + } + } + } + } +} diff --git a/homeassistant/components/downloader/services.yaml b/homeassistant/components/downloader/services.yaml index cecb3804227a77..54d06db56273f2 100644 --- a/homeassistant/components/downloader/services.yaml +++ b/homeassistant/components/downloader/services.yaml @@ -1,29 +1,19 @@ download_file: - name: Download file - description: Download a file to the download location. fields: url: - name: URL - description: The URL of the file to download. required: true example: "http://example.org/myfile" selector: text: subdir: - name: Subdirectory - description: Download into subdirectory. example: "download_dir" selector: text: filename: - name: Filename - description: Determine the filename. example: "my_file_name" selector: text: overwrite: - name: Overwrite - description: Whether to overwrite the file or not. default: false selector: boolean: diff --git a/homeassistant/components/downloader/strings.json b/homeassistant/components/downloader/strings.json new file mode 100644 index 00000000000000..49a7388add2e05 --- /dev/null +++ b/homeassistant/components/downloader/strings.json @@ -0,0 +1,26 @@ +{ + "services": { + "download_file": { + "name": "Download file", + "description": "Downloads a file to the download location.", + "fields": { + "url": { + "name": "URL", + "description": "The URL of the file to download." + }, + "subdir": { + "name": "Subdirectory", + "description": "Download into subdirectory." + }, + "filename": { + "name": "Filename", + "description": "Determine the filename." + }, + "overwrite": { + "name": "Overwrite", + "description": "Whether to overwrite the file or not." + } + } + } + } +} diff --git a/homeassistant/components/duckdns/services.yaml b/homeassistant/components/duckdns/services.yaml index 6c8b5af8199986..485afa44a03796 100644 --- a/homeassistant/components/duckdns/services.yaml +++ b/homeassistant/components/duckdns/services.yaml @@ -1,10 +1,6 @@ set_txt: - name: Set TXT - description: Set the TXT record of your DuckDNS subdomain. fields: txt: - name: TXT - description: Payload for the TXT record. required: true example: "This domain name is reserved for use in documentation" selector: diff --git a/homeassistant/components/duckdns/strings.json b/homeassistant/components/duckdns/strings.json new file mode 100644 index 00000000000000..d560b760e47c5e --- /dev/null +++ b/homeassistant/components/duckdns/strings.json @@ -0,0 +1,14 @@ +{ + "services": { + "set_txt": { + "name": "Set TXT", + "description": "Sets the TXT record of your DuckDNS subdomain.", + "fields": { + "txt": { + "name": "TXT", + "description": "Payload for the TXT record." + } + } + } + } +} diff --git a/homeassistant/components/dynalite/services.yaml b/homeassistant/components/dynalite/services.yaml index d34335ca1d3447..97c5d9c24865e4 100644 --- a/homeassistant/components/dynalite/services.yaml +++ b/homeassistant/components/dynalite/services.yaml @@ -1,21 +1,16 @@ request_area_preset: - name: Request area preset - description: "Requests Dynalite to report the preset for an area." fields: host: - description: "Host gateway IP to send to or all configured gateways if not specified." example: "192.168.0.101" selector: text: area: - description: "Area to request the preset reported" required: true selector: number: min: 1 max: 9999 channel: - description: "Channel to request the preset to be reported from." default: 1 selector: number: @@ -23,26 +18,18 @@ request_area_preset: max: 9999 request_channel_level: - name: Request channel level - description: "Requests Dynalite to report the level of a specific channel." fields: host: - name: Host - description: "Host gateway IP to send to or all configured gateways if not specified." example: "192.168.0.101" selector: text: area: - name: Area - description: "Area for the requested channel" required: true selector: number: min: 1 max: 9999 channel: - name: Channel - description: "Channel to request the level for." required: true selector: number: diff --git a/homeassistant/components/dynalite/strings.json b/homeassistant/components/dynalite/strings.json index 8ad7deacd92ffa..512e00237d9852 100644 --- a/homeassistant/components/dynalite/strings.json +++ b/homeassistant/components/dynalite/strings.json @@ -14,5 +14,43 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } + }, + "services": { + "request_area_preset": { + "name": "Request area preset", + "description": "Requests Dynalite to report the preset for an area.", + "fields": { + "host": { + "name": "Host", + "description": "Host gateway IP to send to or all configured gateways if not specified." + }, + "area": { + "name": "Area", + "description": "Area to request the preset reported." + }, + "channel": { + "name": "Channel", + "description": "Channel to request the preset to be reported from." + } + } + }, + "request_channel_level": { + "name": "Request channel level", + "description": "Requests Dynalite to report the level of a specific channel.", + "fields": { + "host": { + "name": "Host", + "description": "Host gateway IP to send to or all configured gateways if not specified." + }, + "area": { + "name": "Area", + "description": "Area for the requested channel." + }, + "channel": { + "name": "Channel", + "description": "Channel to request the level for." + } + } + } } } From 5d5c58338fc8b65f383daffe41e5beff146ce118 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Jul 2023 11:12:24 -1000 Subject: [PATCH 0381/1009] Fix ESPHome deep sleep devices staying unavailable after unexpected disconnect (#96353) --- homeassistant/components/esphome/manager.py | 6 +++ tests/components/esphome/conftest.py | 18 ++++++- tests/components/esphome/test_entity.py | 56 ++++++++++++++++++++- 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index b87d3ac38992a5..026d0315238b26 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -378,6 +378,12 @@ async def on_connect(self) -> None: assert cli.api_version is not None entry_data.api_version = cli.api_version entry_data.available = True + # Reset expected disconnect flag on successful reconnect + # as it will be flipped to False on unexpected disconnect. + # + # We use this to determine if a deep sleep device should + # be marked as unavailable or not. + entry_data.expected_disconnect = True if entry_data.device_info.name: assert reconnect_logic is not None, "Reconnect logic must be set" reconnect_logic.name = entry_data.device_info.name diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 1dcdc559de7b61..f4b3bfa3ec7c83 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -154,6 +154,7 @@ def __init__(self, entry: MockConfigEntry) -> None: self.entry = entry self.state_callback: Callable[[EntityState], None] self.on_disconnect: Callable[[bool], None] + self.on_connect: Callable[[bool], None] def set_state_callback(self, state_callback: Callable[[EntityState], None]) -> None: """Set the state callback.""" @@ -171,6 +172,14 @@ async def mock_disconnect(self, expected_disconnect: bool) -> None: """Mock disconnecting.""" await self.on_disconnect(expected_disconnect) + def set_on_connect(self, on_connect: Callable[[], None]) -> None: + """Set the connect callback.""" + self.on_connect = on_connect + + async def mock_connect(self) -> None: + """Mock connecting.""" + await self.on_connect() + async def _mock_generic_device_entry( hass: HomeAssistant, @@ -226,6 +235,7 @@ def __init__(self, *args, **kwargs): """Init the mock.""" super().__init__(*args, **kwargs) mock_device.set_on_disconnect(kwargs["on_disconnect"]) + mock_device.set_on_connect(kwargs["on_connect"]) self._try_connect = self.mock_try_connect async def mock_try_connect(self): @@ -313,9 +323,15 @@ async def _mock_device( user_service: list[UserService], states: list[EntityState], entry: MockConfigEntry | None = None, + device_info: dict[str, Any] | None = None, ) -> MockESPHomeDevice: return await _mock_generic_device_entry( - hass, mock_client, {}, (entity_info, user_service), states, entry + hass, + mock_client, + device_info or {}, + (entity_info, user_service), + states, + entry, ) return _mock_device diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index 1a7d62f886b318..e268d065e21c0e 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -11,7 +11,7 @@ UserService, ) -from homeassistant.const import ATTR_RESTORED, STATE_ON +from homeassistant.const import ATTR_RESTORED, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from .conftest import MockESPHomeDevice @@ -130,3 +130,57 @@ async def test_entity_info_object_ids( ) state = hass.states.get("binary_sensor.test_object_id_is_used") assert state is not None + + +async def test_deep_sleep_device( + hass: HomeAssistant, + mock_client: APIClient, + hass_storage: dict[str, Any], + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test a deep sleep device.""" + entity_info = [ + BinarySensorInfo( + object_id="mybinary_sensor", + key=1, + name="my binary_sensor", + unique_id="my_binary_sensor", + ), + ] + states = [ + BinarySensorState(key=1, state=True, missing_state=False), + BinarySensorState(key=2, state=True, missing_state=False), + ] + user_service = [] + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + device_info={"has_deep_sleep": True}, + ) + state = hass.states.get("binary_sensor.test_mybinary_sensor") + assert state is not None + assert state.state == STATE_ON + + await mock_device.mock_disconnect(False) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.test_mybinary_sensor") + assert state is not None + assert state.state == STATE_UNAVAILABLE + + await mock_device.mock_connect() + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test_mybinary_sensor") + assert state is not None + assert state.state == STATE_ON + + await mock_device.mock_disconnect(True) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.test_mybinary_sensor") + assert state is not None + assert state.state == STATE_ON From 2330af82a5a298363d48ce0926bcdac7d0dfd913 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 23:17:09 +0200 Subject: [PATCH 0382/1009] Migrate climate services to support translations (#96314) * Migrate climate services to support translations * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- .../components/climate/services.yaml | 84 ++-------- homeassistant/components/climate/strings.json | 153 ++++++++++++++++-- 2 files changed, 157 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 33e114c87f5d40..405bb735b66a2b 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -1,8 +1,6 @@ # Describes the format for available climate services set_aux_heat: - name: Turn on/off auxiliary heater - description: Turn auxiliary heater on/off for climate device. target: entity: domain: climate @@ -10,15 +8,11 @@ set_aux_heat: - climate.ClimateEntityFeature.AUX_HEAT fields: aux_heat: - name: Auxiliary heating - description: New value of auxiliary heater. required: true selector: boolean: set_preset_mode: - name: Set preset mode - description: Set preset mode for climate device. target: entity: domain: climate @@ -26,16 +20,12 @@ set_preset_mode: - climate.ClimateEntityFeature.PRESET_MODE fields: preset_mode: - name: Preset mode - description: New value of preset mode. required: true example: "away" selector: text: set_temperature: - name: Set temperature - description: Set target temperature of climate device. target: entity: domain: climate @@ -44,8 +34,6 @@ set_temperature: - climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE fields: temperature: - name: Temperature - description: New target temperature for HVAC. filter: supported_features: - climate.ClimateEntityFeature.TARGET_TEMPERATURE @@ -56,8 +44,6 @@ set_temperature: step: 0.1 mode: box target_temp_high: - name: Target temperature high - description: New target high temperature for HVAC. filter: supported_features: - climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE @@ -69,8 +55,6 @@ set_temperature: step: 0.1 mode: box target_temp_low: - name: Target temperature low - description: New target low temperature for HVAC. filter: supported_features: - climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE @@ -82,29 +66,18 @@ set_temperature: step: 0.1 mode: box hvac_mode: - name: HVAC mode - description: HVAC operation mode to set temperature to. selector: select: options: - - label: "Off" - value: "off" - - label: "Auto" - value: "auto" - - label: "Cool" - value: "cool" - - label: "Dry" - value: "dry" - - label: "Fan Only" - value: "fan_only" - - label: "Heat/Cool" - value: "heat_cool" - - label: "Heat" - value: "heat" - + - "off" + - "auto" + - "cool" + - "dry" + - "fan_only" + - "heat_cool" + - "heat" + translation_key: hvac_mode set_humidity: - name: Set target humidity - description: Set target humidity of climate device. target: entity: domain: climate @@ -112,8 +85,6 @@ set_humidity: - climate.ClimateEntityFeature.TARGET_HUMIDITY fields: humidity: - name: Humidity - description: New target humidity for climate device. required: true selector: number: @@ -122,8 +93,6 @@ set_humidity: unit_of_measurement: "%" set_fan_mode: - name: Set fan mode - description: Set fan operation for climate device. target: entity: domain: climate @@ -131,44 +100,29 @@ set_fan_mode: - climate.ClimateEntityFeature.FAN_MODE fields: fan_mode: - name: Fan mode - description: New value of fan mode. required: true example: "low" selector: text: set_hvac_mode: - name: Set HVAC mode - description: Set HVAC operation mode for climate device. target: entity: domain: climate fields: hvac_mode: - name: HVAC mode - description: New value of operation mode. selector: select: options: - - label: "Off" - value: "off" - - label: "Auto" - value: "auto" - - label: "Cool" - value: "cool" - - label: "Dry" - value: "dry" - - label: "Fan Only" - value: "fan_only" - - label: "Heat/Cool" - value: "heat_cool" - - label: "Heat" - value: "heat" - + - "off" + - "auto" + - "cool" + - "dry" + - "fan_only" + - "heat_cool" + - "heat" + translation_key: hvac_mode set_swing_mode: - name: Set swing mode - description: Set swing operation for climate device. target: entity: domain: climate @@ -176,23 +130,17 @@ set_swing_mode: - climate.ClimateEntityFeature.SWING_MODE fields: swing_mode: - name: Swing mode - description: New value of swing mode. required: true example: "horizontal" selector: text: turn_on: - name: Turn on - description: Turn climate device on. target: entity: domain: climate turn_off: - name: Turn off - description: Turn climate device off. target: entity: domain: climate diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index 8034799a6d0c5f..bfe0f490cda653 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -28,9 +28,15 @@ "fan_only": "Fan only" }, "state_attributes": { - "aux_heat": { "name": "Aux heat" }, - "current_humidity": { "name": "Current humidity" }, - "current_temperature": { "name": "Current temperature" }, + "aux_heat": { + "name": "Aux heat" + }, + "current_humidity": { + "name": "Current humidity" + }, + "current_temperature": { + "name": "Current temperature" + }, "fan_mode": { "name": "Fan mode", "state": { @@ -49,7 +55,9 @@ "fan_modes": { "name": "Fan modes" }, - "humidity": { "name": "Target humidity" }, + "humidity": { + "name": "Target humidity" + }, "hvac_action": { "name": "Current action", "state": { @@ -65,10 +73,18 @@ "hvac_modes": { "name": "HVAC modes" }, - "max_humidity": { "name": "Max target humidity" }, - "max_temp": { "name": "Max target temperature" }, - "min_humidity": { "name": "Min target humidity" }, - "min_temp": { "name": "Min target temperature" }, + "max_humidity": { + "name": "Max target humidity" + }, + "max_temp": { + "name": "Max target temperature" + }, + "min_humidity": { + "name": "Min target humidity" + }, + "min_temp": { + "name": "Min target temperature" + }, "preset_mode": { "name": "Preset", "state": { @@ -98,10 +114,123 @@ "swing_modes": { "name": "Swing modes" }, - "target_temp_high": { "name": "Upper target temperature" }, - "target_temp_low": { "name": "Lower target temperature" }, - "target_temp_step": { "name": "Target temperature step" }, - "temperature": { "name": "Target temperature" } + "target_temp_high": { + "name": "Upper target temperature" + }, + "target_temp_low": { + "name": "Lower target temperature" + }, + "target_temp_step": { + "name": "Target temperature step" + }, + "temperature": { + "name": "Target temperature" + } + } + } + }, + "services": { + "set_aux_heat": { + "name": "Turn on/off auxiliary heater", + "description": "Turns auxiliary heater on/off.", + "fields": { + "aux_heat": { + "name": "Auxiliary heating", + "description": "New value of auxiliary heater." + } + } + }, + "set_preset_mode": { + "name": "Set preset mode", + "description": "Sets preset mode.", + "fields": { + "preset_mode": { + "name": "Preset mode", + "description": "Preset mode." + } + } + }, + "set_temperature": { + "name": "Set target temperature", + "description": "Sets target temperature.", + "fields": { + "temperature": { + "name": "Temperature", + "description": "Target temperature." + }, + "target_temp_high": { + "name": "Target temperature high", + "description": "High target temperature." + }, + "target_temp_low": { + "name": "Target temperature low", + "description": "Low target temperature." + }, + "hvac_mode": { + "name": "HVAC mode", + "description": "HVAC operation mode." + } + } + }, + "set_humidity": { + "name": "Set target humidity", + "description": "Sets target humidity.", + "fields": { + "humidity": { + "name": "Humidity", + "description": "Target humidity." + } + } + }, + "set_fan_mode": { + "name": "Set fan mode", + "description": "Sets fan operation mode.", + "fields": { + "fan_mode": { + "name": "Fan mode", + "description": "Fan operation mode." + } + } + }, + "set_hvac_mode": { + "name": "Set HVAC mode", + "description": "Sets HVAC operation mode.", + "fields": { + "hvac_mode": { + "name": "HVAC mode", + "description": "HVAC operation mode." + } + } + }, + "set_swing_mode": { + "name": "Set swing mode", + "description": "Sets swing operation mode.", + "fields": { + "swing_mode": { + "name": "Swing mode", + "description": "Swing operation mode." + } + } + }, + "turn_on": { + "name": "Turn on", + "description": "Turns climate device on." + }, + "turn_off": { + "name": "Turn off", + "description": "Turns climate device off." + } + }, + "selector": { + "hvac_mode": { + "options": { + "off": "Off", + "auto": "Auto", + "cool": "Cool", + "dry": "Dry", + "fan_only": "Fan only", + "heat_cool": "Heat/cool", + "heat": "Heat" } } } From bde7d734b5ec673a20f8a369842865f2f83592fa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 23:17:54 +0200 Subject: [PATCH 0383/1009] Migrate automation services to support translations (#96306) * Migrate automation services to support translations * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- .../components/automation/services.yaml | 14 -------- .../components/automation/strings.json | 34 +++++++++++++++++++ 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/automation/services.yaml b/homeassistant/components/automation/services.yaml index 62d0988d7702ee..6b3afdca33508f 100644 --- a/homeassistant/components/automation/services.yaml +++ b/homeassistant/components/automation/services.yaml @@ -1,46 +1,32 @@ # Describes the format for available automation services turn_on: - name: Turn on - description: Enable an automation. target: entity: domain: automation turn_off: - name: Turn off - description: Disable an automation. target: entity: domain: automation fields: stop_actions: - name: Stop actions - description: Stop currently running actions. default: true selector: boolean: toggle: - name: Toggle - description: Toggle (enable / disable) an automation. target: entity: domain: automation trigger: - name: Trigger - description: Trigger the actions of an automation. target: entity: domain: automation fields: skip_condition: - name: Skip conditions - description: Whether or not the conditions will be skipped. default: true selector: boolean: reload: - name: Reload - description: Reload the automation configuration. diff --git a/homeassistant/components/automation/strings.json b/homeassistant/components/automation/strings.json index 6f925fe090db89..cfeafa856d2e14 100644 --- a/homeassistant/components/automation/strings.json +++ b/homeassistant/components/automation/strings.json @@ -44,5 +44,39 @@ } } } + }, + "services": { + "turn_on": { + "name": "Turn on", + "description": "Enables an automation." + }, + "turn_off": { + "name": "Turn off", + "description": "Disables an automation.", + "fields": { + "stop_actions": { + "name": "Stop actions", + "description": "Stops currently running actions." + } + } + }, + "toggle": { + "name": "Toggle", + "description": "Toggles (enable / disable) an automation." + }, + "trigger": { + "name": "Trigger", + "description": "Triggers the actions of an automation.", + "fields": { + "skip_condition": { + "name": "Skip conditions", + "description": "Defines whether or not the conditions will be skipped." + } + } + }, + "reload": { + "name": "Reload", + "description": "Reloads the automation configuration." + } } } From 746832086013957d22370c230b632f50825ef66e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 23:19:29 +0200 Subject: [PATCH 0384/1009] Migrate device_tracker services to support translations (#96320) * Migrate device_tracker services to support translations * Tweaks * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- .../components/device_tracker/services.yaml | 16 --------- .../components/device_tracker/strings.json | 36 +++++++++++++++++++ 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index 22d89b42253c68..08ccbcf0b5a921 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -1,50 +1,34 @@ # Describes the format for available device tracker services see: - name: See - description: Control tracked device. fields: mac: - name: MAC address - description: MAC address of device example: "FF:FF:FF:FF:FF:FF" selector: text: dev_id: - name: Device ID - description: Id of device (find id in known_devices.yaml). example: "phonedave" selector: text: host_name: - name: Host name - description: Hostname of device example: "Dave" selector: text: location_name: - name: Location name - description: Name of location where device is located (not_home is away). example: "home" selector: text: gps: - name: GPS coordinates - description: GPS coordinates where device is located, specified by latitude and longitude. example: "[51.509802, -0.086692]" selector: object: gps_accuracy: - name: GPS accuracy - description: Accuracy of GPS coordinates. selector: number: min: 1 max: 100 unit_of_measurement: "%" battery: - name: Battery level - description: Battery level of device. selector: number: min: 0 diff --git a/homeassistant/components/device_tracker/strings.json b/homeassistant/components/device_tracker/strings.json index c15b9723c972ec..44c43219b82a2d 100644 --- a/homeassistant/components/device_tracker/strings.json +++ b/homeassistant/components/device_tracker/strings.json @@ -41,5 +41,41 @@ } } } + }, + "services": { + "see": { + "name": "See", + "description": "Records a seen tracked device.", + "fields": { + "mac": { + "name": "MAC address", + "description": "MAC address of the device." + }, + "dev_id": { + "name": "Device ID", + "description": "ID of the device (find the ID in `known_devices.yaml`)." + }, + "host_name": { + "name": "Hostname", + "description": "Hostname of the device." + }, + "location_name": { + "name": "Location", + "description": "Name of the location where the device is located. The options are: `home`, `not_home`, or the name of the zone." + }, + "gps": { + "name": "GPS coordinates", + "description": "GPS coordinates where the device is located, specified by latitude and longitude (for example: [51.513845, -0.100539])." + }, + "gps_accuracy": { + "name": "GPS accuracy", + "description": "Accuracy of the GPS coordinates." + }, + "battery": { + "name": "Battery level", + "description": "Battery level of the device." + } + } + } } } From b1e4bae3f0c63fcfbea8e532beb29b0c66ad3fd4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 23:19:50 +0200 Subject: [PATCH 0385/1009] Migrate image_processing services to support translations (#96328) * Migrate image_processing services to support translations * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/image_processing/services.yaml | 2 -- homeassistant/components/image_processing/strings.json | 6 ++++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/image_processing/services.yaml b/homeassistant/components/image_processing/services.yaml index 620bd3518061f1..6309bafcfb936d 100644 --- a/homeassistant/components/image_processing/services.yaml +++ b/homeassistant/components/image_processing/services.yaml @@ -1,8 +1,6 @@ # Describes the format for available image processing services scan: - name: Scan - description: Process an image immediately target: entity: domain: image_processing diff --git a/homeassistant/components/image_processing/strings.json b/homeassistant/components/image_processing/strings.json index 861a2acc1f1166..2e630cfb4ded11 100644 --- a/homeassistant/components/image_processing/strings.json +++ b/homeassistant/components/image_processing/strings.json @@ -12,5 +12,11 @@ } } } + }, + "services": { + "scan": { + "name": "Scan", + "description": "Processes an image immediately." + } } } From 7d6148a295ea8ba2589f60b683b34af79e9bd799 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 23:20:07 +0200 Subject: [PATCH 0386/1009] Migrate button services to support translations (#96309) --- homeassistant/components/button/services.yaml | 2 -- homeassistant/components/button/strings.json | 6 ++++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/button/services.yaml b/homeassistant/components/button/services.yaml index 245368f9d5b33a..2f4d2c6fafe648 100644 --- a/homeassistant/components/button/services.yaml +++ b/homeassistant/components/button/services.yaml @@ -1,6 +1,4 @@ press: - name: Press - description: Press the button entity. target: entity: domain: button diff --git a/homeassistant/components/button/strings.json b/homeassistant/components/button/strings.json index a92a5a0f38a7ad..39456cdf42757a 100644 --- a/homeassistant/components/button/strings.json +++ b/homeassistant/components/button/strings.json @@ -21,5 +21,11 @@ "update": { "name": "Update" } + }, + "services": { + "press": { + "name": "Press", + "description": "Press the button entity." + } } } From f3b0c56c8c9a5f7104ff9613ce50a04b35702dc8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 23:20:40 +0200 Subject: [PATCH 0387/1009] Migrate calendar services to support translations (#96310) * Migrate camera services to support translations * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- .../components/calendar/services.yaml | 26 --------- .../components/calendar/strings.json | 58 +++++++++++++++++++ 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/calendar/services.yaml b/homeassistant/components/calendar/services.yaml index 1f4d6aa3152a65..712d6ad88232e7 100644 --- a/homeassistant/components/calendar/services.yaml +++ b/homeassistant/components/calendar/services.yaml @@ -1,6 +1,4 @@ create_event: - name: Create event - description: Add a new calendar event. target: entity: domain: calendar @@ -8,73 +6,49 @@ create_event: - calendar.CalendarEntityFeature.CREATE_EVENT fields: summary: - name: Summary - description: Defines the short summary or subject for the event required: true example: "Department Party" selector: text: description: - name: Description - description: A more complete description of the event than that provided by the summary. example: "Meeting to provide technical review for 'Phoenix' design." selector: text: start_date_time: - name: Start time - description: The date and time the event should start. example: "2022-03-22 20:00:00" selector: datetime: end_date_time: - name: End time - description: The date and time the event should end. example: "2022-03-22 22:00:00" selector: datetime: start_date: - name: Start date - description: The date the all-day event should start. example: "2022-03-22" selector: date: end_date: - name: End date - description: The date the all-day event should end (exclusive). example: "2022-03-23" selector: date: in: - name: In - description: Days or weeks that you want to create the event in. example: '{"days": 2} or {"weeks": 2}' location: - name: Location - description: The location of the event. example: "Conference Room - F123, Bldg. 002" selector: text: list_events: - name: List event - description: List events on a calendar within a time range. target: entity: domain: calendar fields: start_date_time: - name: Start time - description: Return active events after this time (exclusive). When not set, defaults to now. example: "2022-03-22 20:00:00" selector: datetime: end_date_time: - name: End time - description: Return active events before this time (exclusive). Cannot be used with 'duration'. example: "2022-03-22 22:00:00" selector: datetime: duration: - name: Duration - description: Return active events from start_date_time until the specified duration. selector: duration: diff --git a/homeassistant/components/calendar/strings.json b/homeassistant/components/calendar/strings.json index 898953c18acdcc..81334c12379667 100644 --- a/homeassistant/components/calendar/strings.json +++ b/homeassistant/components/calendar/strings.json @@ -32,5 +32,63 @@ } } } + }, + "services": { + "create_event": { + "name": "Create event", + "description": "Adds a new calendar event.", + "fields": { + "summary": { + "name": "Summary", + "description": "Defines the short summary or subject for the event." + }, + "description": { + "name": "Description", + "description": "A more complete description of the event than the one provided by the summary." + }, + "start_date_time": { + "name": "Start time", + "description": "The date and time the event should start." + }, + "end_date_time": { + "name": "End time", + "description": "The date and time the event should end." + }, + "start_date": { + "name": "Start date", + "description": "The date the all-day event should start." + }, + "end_date": { + "name": "End date", + "description": "The date the all-day event should end (exclusive)." + }, + "in": { + "name": "In", + "description": "Days or weeks that you want to create the event in." + }, + "location": { + "name": "Location", + "description": "The location of the event." + } + } + }, + "list_events": { + "name": "List event", + "description": "Lists events on a calendar within a time range.", + "fields": { + "start_date_time": { + "name": "Start time", + "description": "Returns active events after this time (exclusive). When not set, defaults to now." + }, + "end_date_time": { + "name": "End time", + "description": "Returns active events before this time (exclusive). Cannot be used with 'duration'." + }, + "duration": { + "name": "Duration", + "description": "Returns active events from start_date_time until the specified duration." + } + } + } } } From e4af29342860e31f203391a4750c426f0ae43f58 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 23:21:00 +0200 Subject: [PATCH 0388/1009] Migrate cloud services to support translations (#96319) * Migrate cloud services to support translations * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/cloud/services.yaml | 5 ----- homeassistant/components/cloud/strings.json | 10 ++++++++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cloud/services.yaml b/homeassistant/components/cloud/services.yaml index 1b676ea6be96e9..b54d35d4221cd8 100644 --- a/homeassistant/components/cloud/services.yaml +++ b/homeassistant/components/cloud/services.yaml @@ -1,9 +1,4 @@ # Describes the format for available cloud services remote_connect: - name: Remote connect - description: Make instance UI available outside over NabuCasa cloud - remote_disconnect: - name: Remote disconnect - description: Disconnect UI from NabuCasa cloud diff --git a/homeassistant/components/cloud/strings.json b/homeassistant/components/cloud/strings.json index a3cf7fe04576c2..aba2e770bc91f7 100644 --- a/homeassistant/components/cloud/strings.json +++ b/homeassistant/components/cloud/strings.json @@ -30,5 +30,15 @@ } } } + }, + "services": { + "remote_connect": { + "name": "Remote connect", + "description": "Makes the instance UI accessible from outside of the local network by using Home Assistant Cloud." + }, + "remote_disconnect": { + "name": "Remote disconnect", + "description": "Disconnects the Home Assistant UI from the Home Assistant Cloud. You will no longer be able to access your Home Assistant instance from outside your local network." + } } } From ea3be7a7891ea5dcb072b4b6247c3e27d882820a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Jul 2023 23:57:29 +0200 Subject: [PATCH 0389/1009] Migrate integration services (E-F) to support translations (#96367) --- homeassistant/components/demo/services.yaml | 2 - homeassistant/components/demo/strings.json | 6 + homeassistant/components/ebusd/services.yaml | 4 - homeassistant/components/ebusd/strings.json | 14 ++ homeassistant/components/ecobee/services.yaml | 62 -------- homeassistant/components/ecobee/strings.json | 124 +++++++++++++++ .../components/eight_sleep/services.yaml | 6 - .../components/eight_sleep/strings.json | 16 ++ homeassistant/components/elgato/services.yaml | 4 - homeassistant/components/elgato/strings.json | 6 + homeassistant/components/elkm1/services.yaml | 60 -------- homeassistant/components/elkm1/strings.json | 144 ++++++++++++++++++ .../environment_canada/services.yaml | 4 - .../environment_canada/strings.json | 12 ++ .../components/envisalink/services.yaml | 16 -- .../components/envisalink/strings.json | 32 ++++ homeassistant/components/epson/services.yaml | 4 - homeassistant/components/epson/strings.json | 12 ++ .../components/evohome/services.yaml | 38 ----- homeassistant/components/evohome/strings.json | 58 +++++++ homeassistant/components/ezviz/services.yaml | 22 --- homeassistant/components/ezviz/strings.json | 54 +++++++ .../components/facebox/services.yaml | 8 - homeassistant/components/facebox/strings.json | 22 +++ .../components/fastdotcom/services.yaml | 2 - .../components/fastdotcom/strings.json | 8 + homeassistant/components/ffmpeg/services.yaml | 12 -- homeassistant/components/ffmpeg/strings.json | 34 +++++ homeassistant/components/flo/services.yaml | 12 -- homeassistant/components/flo/strings.json | 28 ++++ .../components/flux_led/services.yaml | 18 --- .../components/flux_led/strings.json | 68 +++++++++ homeassistant/components/foscam/services.yaml | 7 - homeassistant/components/foscam/strings.json | 26 ++++ .../components/freebox/services.yaml | 2 - homeassistant/components/freebox/strings.json | 6 + homeassistant/components/fritz/services.yaml | 20 --- homeassistant/components/fritz/strings.json | 130 +++++++++++++--- .../components/fully_kiosk/services.yaml | 14 -- .../components/fully_kiosk/strings.json | 36 +++++ 40 files changed, 816 insertions(+), 337 deletions(-) create mode 100644 homeassistant/components/ebusd/strings.json create mode 100644 homeassistant/components/envisalink/strings.json create mode 100644 homeassistant/components/evohome/strings.json create mode 100644 homeassistant/components/facebox/strings.json create mode 100644 homeassistant/components/fastdotcom/strings.json create mode 100644 homeassistant/components/ffmpeg/strings.json diff --git a/homeassistant/components/demo/services.yaml b/homeassistant/components/demo/services.yaml index a09b4498035bf6..300ea37f8053cd 100644 --- a/homeassistant/components/demo/services.yaml +++ b/homeassistant/components/demo/services.yaml @@ -1,3 +1 @@ randomize_device_tracker_data: - name: Randomize device tracker data - description: Demonstrates using a device tracker to see where devices are located diff --git a/homeassistant/components/demo/strings.json b/homeassistant/components/demo/strings.json index 3794b27cc0ea01..2dfb3465d68655 100644 --- a/homeassistant/components/demo/strings.json +++ b/homeassistant/components/demo/strings.json @@ -75,5 +75,11 @@ } } } + }, + "services": { + "randomize_device_tracker_data": { + "name": "Randomize device tracker data", + "description": "Demonstrates using a device tracker to see where devices are located." + } } } diff --git a/homeassistant/components/ebusd/services.yaml b/homeassistant/components/ebusd/services.yaml index dc356bec22618e..6615e947f287c9 100644 --- a/homeassistant/components/ebusd/services.yaml +++ b/homeassistant/components/ebusd/services.yaml @@ -1,10 +1,6 @@ write: - name: Write - description: Call ebusd write command. fields: call: - name: Call - description: Property name and value to set required: true example: '{"name": "Hc1MaxFlowTempDesired", "value": 21}' selector: diff --git a/homeassistant/components/ebusd/strings.json b/homeassistant/components/ebusd/strings.json new file mode 100644 index 00000000000000..4097be023933f0 --- /dev/null +++ b/homeassistant/components/ebusd/strings.json @@ -0,0 +1,14 @@ +{ + "services": { + "write": { + "name": "Write", + "description": "Calls the ebusd write command.", + "fields": { + "call": { + "name": "Call", + "description": "Property name and value to set." + } + } + } + } +} diff --git a/homeassistant/components/ecobee/services.yaml b/homeassistant/components/ecobee/services.yaml index aba579891195bf..a184f4227253cc 100644 --- a/homeassistant/components/ecobee/services.yaml +++ b/homeassistant/components/ecobee/services.yaml @@ -1,28 +1,17 @@ create_vacation: - name: Create vacation - description: >- - Create a vacation on the selected thermostat. Note: start/end date and time must all be specified - together for these parameters to have an effect. If start/end date and time are not specified, the - vacation will start immediately and last 14 days (unless deleted earlier). fields: entity_id: - name: Entity - description: ecobee thermostat on which to create the vacation. required: true selector: entity: integration: ecobee domain: climate vacation_name: - name: Vacation name - description: Name of the vacation to create; must be unique on the thermostat. required: true example: "Skiing" selector: text: cool_temp: - name: Cool temperature - description: Cooling temperature during the vacation. required: true selector: number: @@ -31,8 +20,6 @@ create_vacation: step: 0.5 unit_of_measurement: "°" heat_temp: - name: Heat temperature - description: Heating temperature during the vacation. required: true selector: number: @@ -41,36 +28,22 @@ create_vacation: step: 0.5 unit_of_measurement: "°" start_date: - name: Start date - description: >- - Date the vacation starts in the YYYY-MM-DD format (optional, immediately if not provided along with - start_time, end_date, and end_time). example: "2019-03-15" selector: text: start_time: - name: start time - description: Time the vacation starts, in the local time of the thermostat, in the 24-hour format "HH:MM:SS" example: "20:00:00" selector: time: end_date: - name: End date - description: >- - Date the vacation ends in the YYYY-MM-DD format (optional, 14 days from now if not provided along with - start_date, start_time, and end_time). example: "2019-03-20" selector: text: end_time: - name: End time - description: Time the vacation ends, in the local time of the thermostat, in the 24-hour format "HH:MM:SS" example: "20:00:00" selector: time: fan_mode: - name: Fan mode - description: Fan mode of the thermostat during the vacation. default: "auto" selector: select: @@ -78,8 +51,6 @@ create_vacation: - "on" - "auto" fan_min_on_time: - name: Fan minimum on time - description: Minimum number of minutes to run the fan each hour (0 to 60) during the vacation. default: 0 selector: number: @@ -88,13 +59,8 @@ create_vacation: unit_of_measurement: minutes delete_vacation: - name: Delete vacation - description: >- - Delete a vacation on the selected thermostat. fields: entity_id: - name: Entity - description: ecobee thermostat on which to delete the vacation. required: true example: "climate.kitchen" selector: @@ -102,45 +68,31 @@ delete_vacation: integration: ecobee domain: climate vacation_name: - name: Vacation name - description: Name of the vacation to delete. required: true example: "Skiing" selector: text: resume_program: - name: Resume program - description: Resume the programmed schedule. fields: entity_id: - name: Entity - description: Name(s) of entities to change. selector: entity: integration: ecobee domain: climate resume_all: - name: Resume all - description: Resume all events and return to the scheduled program. default: false selector: boolean: set_fan_min_on_time: - name: Set fan minimum on time - description: Set the minimum fan on time. fields: entity_id: - name: Entity - description: Name(s) of entities to change. selector: entity: integration: ecobee domain: climate fan_min_on_time: - name: Fan minimum on time - description: New value of fan min on time. required: true selector: number: @@ -149,50 +101,36 @@ set_fan_min_on_time: unit_of_measurement: minutes set_dst_mode: - name: Set Daylight savings time mode - description: Enable/disable automatic daylight savings time. target: entity: integration: ecobee domain: climate fields: dst_enabled: - name: Daylight savings time enabled - description: Enable automatic daylight savings time. required: true selector: boolean: set_mic_mode: - name: Set mic mode - description: Enable/disable Alexa mic (only for Ecobee 4). target: entity: integration: ecobee domain: climate fields: mic_enabled: - name: Mic enabled - description: Enable Alexa mic. required: true selector: boolean: set_occupancy_modes: - name: Set occupancy modes - description: Enable/disable Smart Home/Away and Follow Me modes. target: entity: integration: ecobee domain: climate fields: auto_away: - name: Auto away - description: Enable Smart Home/Away mode. selector: boolean: follow_me: - name: Follow me - description: Enable Follow Me mode. selector: boolean: diff --git a/homeassistant/components/ecobee/strings.json b/homeassistant/components/ecobee/strings.json index 647ea55e3115fd..05ae600d4b733a 100644 --- a/homeassistant/components/ecobee/strings.json +++ b/homeassistant/components/ecobee/strings.json @@ -28,5 +28,129 @@ "name": "Ventilator min time away" } } + }, + "services": { + "create_vacation": { + "name": "Create vacation", + "description": "Creates a vacation on the selected thermostat. Note: start/end date and time must all be specified together for these parameters to have an effect. If start/end date and time are not specified, the vacation will start immediately and last 14 days (unless deleted earlier).", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Ecobee thermostat on which to create the vacation." + }, + "vacation_name": { + "name": "Vacation name", + "description": "Name of the vacation to create; must be unique on the thermostat." + }, + "cool_temp": { + "name": "Cool temperature", + "description": "Cooling temperature during the vacation." + }, + "heat_temp": { + "name": "Heat temperature", + "description": "Heating temperature during the vacation." + }, + "start_date": { + "name": "Start date", + "description": "Date the vacation starts in the YYYY-MM-DD format (optional, immediately if not provided along with start_time, end_date, and end_time)." + }, + "start_time": { + "name": "Start time", + "description": "Time the vacation starts, in the local time of the thermostat, in the 24-hour format \"HH:MM:SS\"." + }, + "end_date": { + "name": "End date", + "description": "Date the vacation ends in the YYYY-MM-DD format (optional, 14 days from now if not provided along with start_date, start_time, and end_time)." + }, + "end_time": { + "name": "End time", + "description": "Time the vacation ends, in the local time of the thermostat, in the 24-hour format \"HH:MM:SS\"." + }, + "fan_mode": { + "name": "Fan mode", + "description": "Fan mode of the thermostat during the vacation." + }, + "fan_min_on_time": { + "name": "Fan minimum on time", + "description": "Minimum number of minutes to run the fan each hour (0 to 60) during the vacation." + } + } + }, + "delete_vacation": { + "name": "Delete vacation", + "description": "Deletes a vacation on the selected thermostat.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Ecobee thermostat on which to delete the vacation." + }, + "vacation_name": { + "name": "Vacation name", + "description": "Name of the vacation to delete." + } + } + }, + "resume_program": { + "name": "Resume program", + "description": "Resumes the programmed schedule.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name(s) of entities to change." + }, + "resume_all": { + "name": "Resume all", + "description": "Resume all events and return to the scheduled program." + } + } + }, + "set_fan_min_on_time": { + "name": "Set fan minimum on time", + "description": "Sets the minimum fan on time.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name(s) of entities to change." + }, + "fan_min_on_time": { + "name": "Fan minimum on time", + "description": "New value of fan min on time." + } + } + }, + "set_dst_mode": { + "name": "Set Daylight savings time mode", + "description": "Enables/disables automatic daylight savings time.", + "fields": { + "dst_enabled": { + "name": "Daylight savings time enabled", + "description": "Enable automatic daylight savings time." + } + } + }, + "set_mic_mode": { + "name": "Set mic mode", + "description": "Enables/disables Alexa mic (only for Ecobee 4).", + "fields": { + "mic_enabled": { + "name": "Mic enabled", + "description": "Enable Alexa mic." + } + } + }, + "set_occupancy_modes": { + "name": "Set occupancy modes", + "description": "Enables/disables Smart Home/Away and Follow Me modes.", + "fields": { + "auto_away": { + "name": "Auto away", + "description": "Enable Smart Home/Away mode." + }, + "follow_me": { + "name": "Follow me", + "description": "Enable Follow Me mode." + } + } + } } } diff --git a/homeassistant/components/eight_sleep/services.yaml b/homeassistant/components/eight_sleep/services.yaml index 39b960a6f7c1f1..b191187bb0ac6e 100644 --- a/homeassistant/components/eight_sleep/services.yaml +++ b/homeassistant/components/eight_sleep/services.yaml @@ -1,14 +1,10 @@ heat_set: - name: Heat set - description: Set heating/cooling level for eight sleep. target: entity: integration: eight_sleep domain: sensor fields: duration: - name: Duration - description: Duration to heat/cool at the target level in seconds. required: true selector: number: @@ -16,8 +12,6 @@ heat_set: max: 28800 unit_of_measurement: seconds target: - name: Target - description: Target cooling/heating level from -100 to 100. required: true selector: number: diff --git a/homeassistant/components/eight_sleep/strings.json b/homeassistant/components/eight_sleep/strings.json index 21accc53a06a17..bd2b4f11b9d966 100644 --- a/homeassistant/components/eight_sleep/strings.json +++ b/homeassistant/components/eight_sleep/strings.json @@ -15,5 +15,21 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "Cannot connect to Eight Sleep cloud: {error}" } + }, + "services": { + "heat_set": { + "name": "Heat set", + "description": "Sets heating/cooling level for eight sleep.", + "fields": { + "duration": { + "name": "Duration", + "description": "Duration to heat/cool at the target level in seconds." + }, + "target": { + "name": "Target", + "description": "Target cooling/heating level from -100 to 100." + } + } + } } } diff --git a/homeassistant/components/elgato/services.yaml b/homeassistant/components/elgato/services.yaml index 05d341a704130e..2037633ff71f6f 100644 --- a/homeassistant/components/elgato/services.yaml +++ b/homeassistant/components/elgato/services.yaml @@ -1,8 +1,4 @@ identify: - name: Identify - description: >- - Identify an Elgato Light. Blinks the light, which can be useful - for, e.g., a visual notification. target: entity: integration: elgato diff --git a/homeassistant/components/elgato/strings.json b/homeassistant/components/elgato/strings.json index 8a2f20f209f8bf..e6b16215793c42 100644 --- a/homeassistant/components/elgato/strings.json +++ b/homeassistant/components/elgato/strings.json @@ -45,5 +45,11 @@ "name": "Energy saving" } } + }, + "services": { + "identify": { + "name": "Identify", + "description": "Identifies an Elgato Light. Blinks the light, which can be useful for, e.g., a visual notification." + } } } diff --git a/homeassistant/components/elkm1/services.yaml b/homeassistant/components/elkm1/services.yaml index 1f130416363102..1f3bb8ffebb030 100644 --- a/homeassistant/components/elkm1/services.yaml +++ b/homeassistant/components/elkm1/services.yaml @@ -1,197 +1,143 @@ alarm_bypass: - name: Alarm bypass - description: Bypass all zones for the area. target: entity: integration: elkm1 domain: alarm_control_panel fields: code: - name: Code - description: An code to authorize the bypass of the alarm control panel. required: true example: 4242 selector: text: alarm_clear_bypass: - name: Alarm clear bypass - description: Remove bypass on all zones for the area. target: entity: integration: elkm1 domain: alarm_control_panel fields: code: - name: Code - description: An code to authorize the bypass clear of the alarm control panel. required: true example: 4242 selector: text: alarm_arm_home_instant: - name: Alarm are home instant - description: Arm the ElkM1 in home instant mode. target: entity: integration: elkm1 domain: alarm_control_panel fields: code: - name: Code - description: An code to arm the alarm control panel. required: true example: 1234 selector: text: alarm_arm_night_instant: - name: Alarm arm night instant - description: Arm the ElkM1 in night instant mode. target: entity: integration: elkm1 domain: alarm_control_panel fields: code: - name: Code - description: An code to arm the alarm control panel. required: true example: 1234 selector: text: alarm_arm_vacation: - name: Alarm arm vacation - description: Arm the ElkM1 in vacation mode. target: entity: integration: elkm1 domain: alarm_control_panel fields: code: - name: Code - description: An code to arm the alarm control panel. required: true example: 1234 selector: text: alarm_display_message: - name: Alarm display message - description: Display a message on all of the ElkM1 keypads for an area. target: entity: integration: elkm1 domain: alarm_control_panel fields: clear: - name: Clear - description: 0=clear message, 1=clear message with * key, 2=Display until timeout default: 2 selector: number: min: 0 max: 2 beep: - name: Beep - description: 0=no beep, 1=beep default: 0 selector: boolean: timeout: - name: Timeout - description: Time to display message, 0=forever, max 65535 default: 0 selector: number: min: 0 max: 65535 line1: - name: Line 1 - description: Up to 16 characters of text (truncated if too long). example: The answer to life. default: "" selector: text: line2: - name: Line 2 - description: Up to 16 characters of text (truncated if too long). example: the universe, and everything. default: "" selector: text: set_time: - name: Set time - description: Set the time for the panel. fields: prefix: - name: Prefix - description: Prefix for the panel. example: gatehouse selector: text: speak_phrase: - name: Speak phrase - description: Speak a phrase. See list of phrases in ElkM1 ASCII Protocol documentation. fields: number: - name: Phrase number - description: Phrase number to speak. required: true example: 42 selector: text: prefix: - name: Prefix - description: Prefix to identify panel when multiple panels configured. example: gatehouse default: "" selector: text: speak_word: - name: Speak word - description: Speak a word. See list of words in ElkM1 ASCII Protocol documentation. fields: number: - name: Word number - description: Word number to speak. required: true selector: number: min: 1 max: 473 prefix: - name: Prefix - description: Prefix to identify panel when multiple panels configured. example: gatehouse default: "" selector: text: sensor_counter_refresh: - name: Sensor counter refresh - description: Refresh the value of a counter from the panel. target: entity: integration: elkm1 domain: sensor sensor_counter_set: - name: Sensor counter set - description: Set the value of a counter on the panel. target: entity: integration: elkm1 domain: sensor fields: value: - name: Value - description: Value to set the counter to. required: true selector: number: @@ -199,24 +145,18 @@ sensor_counter_set: max: 65536 sensor_zone_bypass: - name: Sensor zone bypass - description: Bypass zone. target: entity: integration: elkm1 domain: sensor fields: code: - name: Code - description: An code to authorize the bypass of the zone. required: true example: 4242 selector: text: sensor_zone_trigger: - name: Sensor zone trigger - description: Trigger zone. target: entity: integration: elkm1 diff --git a/homeassistant/components/elkm1/strings.json b/homeassistant/components/elkm1/strings.json index d1871c7536c649..5ef15827eb9ea8 100644 --- a/homeassistant/components/elkm1/strings.json +++ b/homeassistant/components/elkm1/strings.json @@ -45,5 +45,149 @@ "already_configured": "An ElkM1 with this prefix is already configured", "address_already_configured": "An ElkM1 with this address is already configured" } + }, + "services": { + "alarm_bypass": { + "name": "Alarm bypass", + "description": "Bypasses all zones for the area.", + "fields": { + "code": { + "name": "Code", + "description": "An code to authorize the bypass of the alarm control panel." + } + } + }, + "alarm_clear_bypass": { + "name": "Alarm clear bypass", + "description": "Removes bypass on all zones for the area.", + "fields": { + "code": { + "name": "Code", + "description": "An code to authorize the bypass clear of the alarm control panel." + } + } + }, + "alarm_arm_home_instant": { + "name": "Alarm are home instant", + "description": "Arms the ElkM1 in home instant mode.", + "fields": { + "code": { + "name": "Code", + "description": "An code to arm the alarm control panel." + } + } + }, + "alarm_arm_night_instant": { + "name": "Alarm arm night instant", + "description": "Arms the ElkM1 in night instant mode.", + "fields": { + "code": { + "name": "Code", + "description": "An code to arm the alarm control panel." + } + } + }, + "alarm_arm_vacation": { + "name": "Alarm arm vacation", + "description": "Arm the ElkM1 in vacation mode.", + "fields": { + "code": { + "name": "Code", + "description": "An code to arm the alarm control panel." + } + } + }, + "alarm_display_message": { + "name": "Alarm display message", + "description": "Displays a message on all of the ElkM1 keypads for an area.", + "fields": { + "clear": { + "name": "Clear", + "description": "0=clear message, 1=clear message with * key, 2=Display until timeout." + }, + "beep": { + "name": "Beep", + "description": "0=no beep, 1=beep." + }, + "timeout": { + "name": "Timeout", + "description": "Time to display message, 0=forever, max 65535." + }, + "line1": { + "name": "Line 1", + "description": "Up to 16 characters of text (truncated if too long)." + }, + "line2": { + "name": "Line 2", + "description": "Up to 16 characters of text (truncated if too long)." + } + } + }, + "set_time": { + "name": "Set time", + "description": "Sets the time for the panel.", + "fields": { + "prefix": { + "name": "Prefix", + "description": "Prefix for the panel." + } + } + }, + "speak_phrase": { + "name": "Speak phrase", + "description": "Speaks a phrase. See list of phrases in ElkM1 ASCII Protocol documentation.", + "fields": { + "number": { + "name": "Phrase number", + "description": "Phrase number to speak." + }, + "prefix": { + "name": "Prefix", + "description": "Prefix to identify panel when multiple panels configured." + } + } + }, + "speak_word": { + "name": "Speak word", + "description": "Speaks a word. See list of words in ElkM1 ASCII Protocol documentation.", + "fields": { + "number": { + "name": "Word number", + "description": "Word number to speak." + }, + "prefix": { + "name": "Prefix", + "description": "Prefix to identify panel when multiple panels configured." + } + } + }, + "sensor_counter_refresh": { + "name": "Sensor counter refresh", + "description": "Refreshes the value of a counter from the panel." + }, + "sensor_counter_set": { + "name": "Sensor counter set", + "description": "Sets the value of a counter on the panel.", + "fields": { + "value": { + "name": "Value", + "description": "Value to set the counter to." + } + } + }, + "sensor_zone_bypass": { + "name": "Sensor zone bypass", + "description": "Bypasses zone.", + "fields": { + "code": { + "name": "Code", + "description": "An code to authorize the bypass of the zone." + } + } + }, + "sensor_zone_trigger": { + "name": "Sensor zone trigger", + "description": "Triggers zone." + } } } diff --git a/homeassistant/components/environment_canada/services.yaml b/homeassistant/components/environment_canada/services.yaml index 09f95f16a44de3..4293b313f5ce62 100644 --- a/homeassistant/components/environment_canada/services.yaml +++ b/homeassistant/components/environment_canada/services.yaml @@ -1,14 +1,10 @@ set_radar_type: - name: Set radar type - description: Set the type of radar image to retrieve. target: entity: integration: environment_canada domain: camera fields: radar_type: - name: Radar type - description: The type of radar image to display. required: true example: Snow selector: diff --git a/homeassistant/components/environment_canada/strings.json b/homeassistant/components/environment_canada/strings.json index d30124ddf5af69..eb9ec24dad0f98 100644 --- a/homeassistant/components/environment_canada/strings.json +++ b/homeassistant/components/environment_canada/strings.json @@ -117,5 +117,17 @@ "name": "Forecast" } } + }, + "services": { + "set_radar_type": { + "name": "Set radar type", + "description": "Sets the type of radar image to retrieve.", + "fields": { + "radar_type": { + "name": "Radar type", + "description": "The type of radar image to display." + } + } + } } } diff --git a/homeassistant/components/envisalink/services.yaml b/homeassistant/components/envisalink/services.yaml index b15a3b94e014ee..6751a3ecc560f7 100644 --- a/homeassistant/components/envisalink/services.yaml +++ b/homeassistant/components/envisalink/services.yaml @@ -1,43 +1,27 @@ # Describes the format for available Envisalink services. alarm_keypress: - name: Alarm keypress - description: Send custom keypresses to the alarm. fields: entity_id: - name: Entity - description: Name of the alarm control panel to trigger. required: true selector: entity: integration: envisalink domain: alarm_control_panel keypress: - name: Keypress - description: "String to send to the alarm panel (1-6 characters)." required: true example: "*71" selector: text: invoke_custom_function: - name: Invoke custom function - description: > - Allows users with DSC panels to trigger a PGM output (1-4). - Note that you need to specify the alarm panel's "code" parameter for this to work. fields: partition: - name: Partition - description: > - The alarm panel partition to trigger the PGM output on. - Typically this is just "1". required: true example: "1" selector: text: pgm: - name: PGM - description: The PGM number to trigger on the alarm panel. required: true selector: number: diff --git a/homeassistant/components/envisalink/strings.json b/homeassistant/components/envisalink/strings.json new file mode 100644 index 00000000000000..a539c890169141 --- /dev/null +++ b/homeassistant/components/envisalink/strings.json @@ -0,0 +1,32 @@ +{ + "services": { + "alarm_keypress": { + "name": "Alarm keypress", + "description": "Sends custom keypresses to the alarm.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name of the alarm control panel to trigger." + }, + "keypress": { + "name": "Keypress", + "description": "String to send to the alarm panel (1-6 characters)." + } + } + }, + "invoke_custom_function": { + "name": "Invoke custom function", + "description": "Allows users with DSC panels to trigger a PGM output (1-4). Note that you need to specify the alarm panel's \"code\" parameter for this to work.\n.", + "fields": { + "partition": { + "name": "Partition", + "description": "The alarm panel partition to trigger the PGM output on. Typically this is just \"1\".\n." + }, + "pgm": { + "name": "PGM", + "description": "The PGM number to trigger on the alarm panel." + } + } + } + } +} diff --git a/homeassistant/components/epson/services.yaml b/homeassistant/components/epson/services.yaml index 37add1bc20202e..94038aab408deb 100644 --- a/homeassistant/components/epson/services.yaml +++ b/homeassistant/components/epson/services.yaml @@ -1,14 +1,10 @@ select_cmode: - name: Select color mode - description: Select Color mode of Epson projector target: entity: integration: epson domain: media_player fields: cmode: - name: Color mode - description: Name of Cmode required: true example: "cinema" selector: diff --git a/homeassistant/components/epson/strings.json b/homeassistant/components/epson/strings.json index 9716153958b8b5..4e3780322e9301 100644 --- a/homeassistant/components/epson/strings.json +++ b/homeassistant/components/epson/strings.json @@ -15,5 +15,17 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "powered_off": "Is projector turned on? You need to turn on projector for initial configuration." } + }, + "services": { + "select_cmode": { + "name": "Select color mode", + "description": "Selects color mode of Epson projector.", + "fields": { + "cmode": { + "name": "Color mode", + "description": "Name of Cmode." + } + } + } } } diff --git a/homeassistant/components/evohome/services.yaml b/homeassistant/components/evohome/services.yaml index 52428dd5e1ea7b..a16395ad6c0278 100644 --- a/homeassistant/components/evohome/services.yaml +++ b/homeassistant/components/evohome/services.yaml @@ -2,14 +2,8 @@ # Describes the format for available services set_system_mode: - name: Set system mode - description: >- - Set the system mode, either indefinitely, or for a specified period of time, after - which it will revert to Auto. Not all systems support all modes. fields: mode: - name: Mode - description: "Mode to set thermostat." example: Away selector: select: @@ -21,41 +15,19 @@ set_system_mode: - "DayOff" - "HeatingOff" period: - name: Period - description: >- - A period of time in days; used only with Away, DayOff, or Custom. The system - will revert to Auto at midnight (up to 99 days, today is day 1). example: '{"days": 28}' selector: object: duration: - name: Duration - description: The duration in hours; used only with AutoWithEco (up to 24 hours). example: '{"hours": 18}' selector: object: reset_system: - name: Reset system - description: >- - Set the system to Auto mode and reset all the zones to follow their schedules. - Not all Evohome systems support this feature (i.e. AutoWithReset mode). - refresh_system: - name: Refresh system - description: >- - Pull the latest data from the vendor's servers now, rather than waiting for the - next scheduled update. - set_zone_override: - name: Set zone override - description: >- - Override a zone's setpoint, either indefinitely, or for a specified period of - time, after which it will revert to following its schedule. fields: entity_id: - name: Entity - description: The entity_id of the Evohome zone. required: true example: climate.bathroom selector: @@ -63,8 +35,6 @@ set_zone_override: integration: evohome domain: climate setpoint: - name: Setpoint - description: The temperature to be used instead of the scheduled setpoint. required: true selector: number: @@ -72,21 +42,13 @@ set_zone_override: max: 35.0 step: 0.1 duration: - name: Duration - description: >- - The zone will revert to its schedule after this time. If 0 the change is until - the next scheduled setpoint. example: '{"minutes": 135}' selector: object: clear_zone_override: - name: Clear zone override - description: Set a zone to follow its schedule. fields: entity_id: - name: Entity - description: The entity_id of the zone. required: true selector: entity: diff --git a/homeassistant/components/evohome/strings.json b/homeassistant/components/evohome/strings.json new file mode 100644 index 00000000000000..d8214c3aa8bf31 --- /dev/null +++ b/homeassistant/components/evohome/strings.json @@ -0,0 +1,58 @@ +{ + "services": { + "set_system_mode": { + "name": "Set system mode", + "description": "Sets the system mode, either indefinitely, or for a specified period of time, after which it will revert to Auto. Not all systems support all modes.", + "fields": { + "mode": { + "name": "Mode", + "description": "Mode to set thermostat." + }, + "period": { + "name": "Period", + "description": "A period of time in days; used only with Away, DayOff, or Custom. The system will revert to Auto at midnight (up to 99 days, today is day 1)." + }, + "duration": { + "name": "Duration", + "description": "The duration in hours; used only with AutoWithEco (up to 24 hours)." + } + } + }, + "reset_system": { + "name": "Reset system", + "description": "Sets the system to Auto mode and reset all the zones to follow their schedules. Not all Evohome systems support this feature (i.e. AutoWithReset mode)." + }, + "refresh_system": { + "name": "Refresh system", + "description": "Pulls the latest data from the vendor's servers now, rather than waiting for the next scheduled update." + }, + "set_zone_override": { + "name": "Set zone override", + "description": "Overrides a zone's setpoint, either indefinitely, or for a specified period of time, after which it will revert to following its schedule.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "The entity_id of the Evohome zone." + }, + "setpoint": { + "name": "Setpoint", + "description": "The temperature to be used instead of the scheduled setpoint." + }, + "duration": { + "name": "Duration", + "description": "The zone will revert to its schedule after this time. If 0 the change is until the next scheduled setpoint." + } + } + }, + "clear_zone_override": { + "name": "Clear zone override", + "description": "Sets a zone to follow its schedule.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "The entity_id of the zone." + } + } + } + } +} diff --git a/homeassistant/components/ezviz/services.yaml b/homeassistant/components/ezviz/services.yaml index 9733d7418a34e4..7d1cda2fa637a1 100644 --- a/homeassistant/components/ezviz/services.yaml +++ b/homeassistant/components/ezviz/services.yaml @@ -1,14 +1,10 @@ alarm_sound: - name: Set warning sound level. - description: Set movement warning sound level. target: entity: integration: ezviz domain: camera fields: level: - name: Sound level - description: Sound level (2 is disabled, 1 intensive, 0 soft). required: true example: 0 default: 0 @@ -19,16 +15,12 @@ alarm_sound: step: 1 mode: box ptz: - name: PTZ - description: Moves the camera to the direction, with defined speed target: entity: integration: ezviz domain: camera fields: direction: - name: Direction - description: Direction to move camera (up, down, left, right). required: true example: "up" default: "up" @@ -40,8 +32,6 @@ ptz: - "left" - "right" speed: - name: Speed - description: Speed of movement (from 1 to 9). required: true example: 5 default: 5 @@ -52,17 +42,12 @@ ptz: step: 1 mode: box set_alarm_detection_sensibility: - name: Detection sensitivity - description: Sets the detection sensibility level. target: entity: integration: ezviz domain: camera fields: level: - name: Sensitivity Level - description: "Sensibility level (1-6) for type 0 (Normal camera) - or (1-100) for type 3 (PIR sensor camera)." required: true example: 3 default: 3 @@ -73,8 +58,6 @@ set_alarm_detection_sensibility: step: 1 mode: box type_value: - name: Detection type - description: "Type of detection. Options : 0 - Camera or 3 - PIR Sensor Camera" required: true example: "0" default: "0" @@ -84,15 +67,12 @@ set_alarm_detection_sensibility: - "0" - "3" sound_alarm: - name: Sound Alarm - description: Sounds the alarm on your camera. target: entity: integration: ezviz domain: camera fields: enable: - description: Enter 1 or 2 (1=disable, 2=enable). required: true example: 1 default: 1 @@ -103,8 +83,6 @@ sound_alarm: step: 1 mode: box wake_device: - name: Wake Camera - description: This can be used to wake the camera/device from hibernation. target: entity: integration: ezviz diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index 92ff8c6fa05ed7..5355fcc377ca3a 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -71,5 +71,59 @@ } } } + }, + "services": { + "alarm_sound": { + "name": "Set warning sound level.", + "description": "Setx movement warning sound level.", + "fields": { + "level": { + "name": "Sound level", + "description": "Sound level (2 is disabled, 1 intensive, 0 soft)." + } + } + }, + "ptz": { + "name": "PTZ", + "description": "Moves the camera to the direction, with defined speed.", + "fields": { + "direction": { + "name": "Direction", + "description": "Direction to move camera (up, down, left, right)." + }, + "speed": { + "name": "Speed", + "description": "Speed of movement (from 1 to 9)." + } + } + }, + "set_alarm_detection_sensibility": { + "name": "Detection sensitivity", + "description": "Sets the detection sensibility level.", + "fields": { + "level": { + "name": "Sensitivity level", + "description": "Sensibility level (1-6) for type 0 (Normal camera) or (1-100) for type 3 (PIR sensor camera)." + }, + "type_value": { + "name": "Detection type", + "description": "Type of detection. Options : 0 - Camera or 3 - PIR Sensor Camera." + } + } + }, + "sound_alarm": { + "name": "Sound alarm", + "description": "Sounds the alarm on your camera.", + "fields": { + "enable": { + "name": "Alarm sound", + "description": "Enter 1 or 2 (1=disable, 2=enable)." + } + } + }, + "wake_device": { + "name": "Wake camera", + "description": "This can be used to wake the camera/device from hibernation." + } } } diff --git a/homeassistant/components/facebox/services.yaml b/homeassistant/components/facebox/services.yaml index 3f968cf385a8e2..0438338f55e1ef 100644 --- a/homeassistant/components/facebox/services.yaml +++ b/homeassistant/components/facebox/services.yaml @@ -1,24 +1,16 @@ teach_face: - name: Teach face - description: Teach facebox a face using a file. fields: entity_id: - name: Entity - description: The facebox entity to teach. selector: entity: integration: facebox domain: image_processing name: - name: Name - description: The name of the face to teach. required: true example: "my_name" selector: text: file_path: - name: File path - description: The path to the image file. required: true example: "/images/my_image.jpg" selector: diff --git a/homeassistant/components/facebox/strings.json b/homeassistant/components/facebox/strings.json new file mode 100644 index 00000000000000..776644c7cfa2f9 --- /dev/null +++ b/homeassistant/components/facebox/strings.json @@ -0,0 +1,22 @@ +{ + "services": { + "teach_face": { + "name": "Teach face", + "description": "Teaches facebox a face using a file.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "The facebox entity to teach." + }, + "name": { + "name": "Name", + "description": "The name of the face to teach." + }, + "file_path": { + "name": "File path", + "description": "The path to the image file." + } + } + } + } +} diff --git a/homeassistant/components/fastdotcom/services.yaml b/homeassistant/components/fastdotcom/services.yaml index 75963557a03a24..002b28b4e4d685 100644 --- a/homeassistant/components/fastdotcom/services.yaml +++ b/homeassistant/components/fastdotcom/services.yaml @@ -1,3 +1 @@ speedtest: - name: Speed test - description: Immediately execute a speed test with Fast.com diff --git a/homeassistant/components/fastdotcom/strings.json b/homeassistant/components/fastdotcom/strings.json new file mode 100644 index 00000000000000..b1e03681c960cc --- /dev/null +++ b/homeassistant/components/fastdotcom/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "speedtest": { + "name": "Speed test", + "description": "Immediately executs a speed test with Fast.com." + } + } +} diff --git a/homeassistant/components/ffmpeg/services.yaml b/homeassistant/components/ffmpeg/services.yaml index 1fdde46e55c46a..35c11ee678f28c 100644 --- a/homeassistant/components/ffmpeg/services.yaml +++ b/homeassistant/components/ffmpeg/services.yaml @@ -1,32 +1,20 @@ restart: - name: Restart - description: Send a restart command to a ffmpeg based sensor. fields: entity_id: - name: Entity - description: Name of entity that will restart. Platform dependent. selector: entity: integration: ffmpeg domain: binary_sensor start: - name: Start - description: Send a start command to a ffmpeg based sensor. fields: entity_id: - name: Entity - description: Name of entity that will start. Platform dependent. selector: entity: integration: ffmpeg domain: binary_sensor stop: - name: Stop - description: Send a stop command to a ffmpeg based sensor. fields: entity_id: - name: Entity - description: Name of entity that will stop. Platform dependent. selector: entity: integration: ffmpeg diff --git a/homeassistant/components/ffmpeg/strings.json b/homeassistant/components/ffmpeg/strings.json new file mode 100644 index 00000000000000..9aaff2d1e9343b --- /dev/null +++ b/homeassistant/components/ffmpeg/strings.json @@ -0,0 +1,34 @@ +{ + "services": { + "restart": { + "name": "Restart", + "description": "Sends a restart command to a ffmpeg based sensor.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name of entity that will restart. Platform dependent." + } + } + }, + "start": { + "name": "Start", + "description": "Sends a start command to a ffmpeg based sensor.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name of entity that will start. Platform dependent." + } + } + }, + "stop": { + "name": "Stop", + "description": "Sends a stop command to a ffmpeg based sensor.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name of entity that will stop. Platform dependent." + } + } + } + } +} diff --git a/homeassistant/components/flo/services.yaml b/homeassistant/components/flo/services.yaml index a074ebafe9996b..ce4abacb64cbac 100644 --- a/homeassistant/components/flo/services.yaml +++ b/homeassistant/components/flo/services.yaml @@ -1,16 +1,12 @@ # Describes the format for available Flo services set_sleep_mode: - name: Set sleep mode - description: Set the location into sleep mode. target: entity: integration: flo domain: switch fields: sleep_minutes: - name: Sleep minutes - description: The time to sleep in minutes. default: true selector: select: @@ -19,8 +15,6 @@ set_sleep_mode: - "1440" - "4320" revert_to_mode: - name: Revert to mode - description: The mode to revert to after sleep_minutes has elapsed. default: true selector: select: @@ -28,22 +22,16 @@ set_sleep_mode: - "away" - "home" set_away_mode: - name: Set away mode - description: Set the location into away mode. target: entity: integration: flo domain: switch set_home_mode: - name: Set home mode - description: Set the location into home mode. target: entity: integration: flo domain: switch run_health_test: - name: Run health test - description: Have the Flo device run a health test. target: entity: integration: flo diff --git a/homeassistant/components/flo/strings.json b/homeassistant/components/flo/strings.json index fadfc304fce415..627f562be7e14c 100644 --- a/homeassistant/components/flo/strings.json +++ b/homeassistant/components/flo/strings.json @@ -49,5 +49,33 @@ "name": "Shutoff valve" } } + }, + "services": { + "set_sleep_mode": { + "name": "Set sleep mode", + "description": "Sets the location into sleep mode.", + "fields": { + "sleep_minutes": { + "name": "Sleep minutes", + "description": "The time to sleep in minutes." + }, + "revert_to_mode": { + "name": "Revert to mode", + "description": "The mode to revert to after sleep_minutes has elapsed." + } + } + }, + "set_away_mode": { + "name": "Set away mode", + "description": "Sets the location into away mode." + }, + "set_home_mode": { + "name": "Set home mode", + "description": "Sets the location into home mode." + }, + "run_health_test": { + "name": "Run health test", + "description": "Have the Flo device run a health test." + } } } diff --git a/homeassistant/components/flux_led/services.yaml b/homeassistant/components/flux_led/services.yaml index 5d880370818bb2..73f479825da4c0 100644 --- a/homeassistant/components/flux_led/services.yaml +++ b/homeassistant/components/flux_led/services.yaml @@ -1,13 +1,10 @@ set_custom_effect: - name: Set custom effect - description: Set a custom light effect. target: entity: integration: flux_led domain: light fields: colors: - description: List of colors for the custom effect (RGB). (Max 16 Colors) example: | - [255,0,0] - [0,255,0] @@ -16,7 +13,6 @@ set_custom_effect: selector: object: speed_pct: - description: Effect speed for the custom effect (0-100). example: 80 default: 50 required: false @@ -27,7 +23,6 @@ set_custom_effect: max: 100 unit_of_measurement: "%" transition: - description: Effect transition. example: "jump" default: "gradual" required: false @@ -38,15 +33,12 @@ set_custom_effect: - "jump" - "strobe" set_zones: - name: Set zones - description: Set strip zones for Addressable v3 controllers (0xA3). target: entity: integration: flux_led domain: light fields: colors: - description: List of colors for each zone (RGB). The length of each zone is the number of pixels per segment divided by the number of colors. (Max 2048 Colors) example: | - [255,0,0] - [0,255,0] @@ -56,7 +48,6 @@ set_zones: selector: object: speed_pct: - description: Effect speed for the custom effect (0-100) example: 80 default: 50 required: false @@ -67,7 +58,6 @@ set_zones: max: 100 unit_of_measurement: "%" effect: - description: Effect example: "running_water" default: "static" required: false @@ -80,15 +70,12 @@ set_zones: - "jump" - "breathing" set_music_mode: - name: Set music mode - description: Configure music mode on Controller RGB with MIC (0x08), Addressable v2 (0xA2), and Addressable v3 (0xA3) devices that have a built-in microphone. target: entity: integration: flux_led domain: light fields: sensitivity: - description: Microphone sensitivity (0-100) example: 80 default: 100 required: false @@ -99,7 +86,6 @@ set_music_mode: max: 100 unit_of_measurement: "%" brightness: - description: Light brightness (0-100) example: 80 default: 100 required: false @@ -110,13 +96,11 @@ set_music_mode: max: 100 unit_of_measurement: "%" light_screen: - description: Light screen mode for 2 dimensional pixels (Addressable models only) default: false required: false selector: boolean: effect: - description: Effect (1-16 on Addressable models, 0-3 on RGB with MIC models) example: 1 default: 1 required: false @@ -126,13 +110,11 @@ set_music_mode: step: 1 max: 16 foreground_color: - description: The foreground RGB color example: "[255, 100, 100]" required: false selector: object: background_color: - description: The background RGB color (Addressable models only) example: "[255, 100, 100]" required: false selector: diff --git a/homeassistant/components/flux_led/strings.json b/homeassistant/components/flux_led/strings.json index 51edd207e95073..7617d56d512194 100644 --- a/homeassistant/components/flux_led/strings.json +++ b/homeassistant/components/flux_led/strings.json @@ -89,5 +89,73 @@ "name": "Music" } } + }, + "services": { + "set_custom_effect": { + "name": "Set custom effect", + "description": "Sets a custom light effect.", + "fields": { + "colors": { + "name": "Colors", + "description": "List of colors for the custom effect (RGB). (Max 16 Colors)." + }, + "speed_pct": { + "name": "Speed", + "description": "Effect speed for the custom effect (0-100)." + }, + "transition": { + "name": "Transition", + "description": "Effect transition." + } + } + }, + "set_zones": { + "name": "Set zones", + "description": "Sets strip zones for Addressable v3 controllers (0xA3).", + "fields": { + "colors": { + "name": "Colors", + "description": "List of colors for each zone (RGB). The length of each zone is the number of pixels per segment divided by the number of colors. (Max 2048 Colors)." + }, + "speed_pct": { + "name": "Speed", + "description": "Effect speed for the custom effect (0-100)." + }, + "effect": { + "name": "Effect", + "description": "Effect." + } + } + }, + "set_music_mode": { + "name": "Set music mode", + "description": "Configures music mode on Controller RGB with MIC (0x08), Addressable v2 (0xA2), and Addressable v3 (0xA3) devices that have a built-in microphone.", + "fields": { + "sensitivity": { + "name": "Sensitivity", + "description": "Microphone sensitivity (0-100)." + }, + "brightness": { + "name": "Brightness", + "description": "Light brightness (0-100)." + }, + "light_screen": { + "name": "Light screen", + "description": "Light screen mode for 2 dimensional pixels (Addressable models only)." + }, + "effect": { + "name": "Effect", + "description": "Effect (1-16 on Addressable models, 0-3 on RGB with MIC models)." + }, + "foreground_color": { + "name": "Foreground color", + "description": "The foreground RGB color." + }, + "background_color": { + "name": "Background color", + "description": "The background RGB color (Addressable models only)." + } + } + } } } diff --git a/homeassistant/components/foscam/services.yaml b/homeassistant/components/foscam/services.yaml index a7e5394802bbdc..ad46ec130d0386 100644 --- a/homeassistant/components/foscam/services.yaml +++ b/homeassistant/components/foscam/services.yaml @@ -1,13 +1,10 @@ ptz: - name: PTZ - description: Pan/Tilt service for Foscam camera. target: entity: integration: foscam domain: camera fields: movement: - description: "Direction of the movement." required: true selector: select: @@ -21,7 +18,6 @@ ptz: - "top_right" - "up" travel_time: - description: "Travel time in seconds." default: 0.125 selector: number: @@ -31,15 +27,12 @@ ptz: unit_of_measurement: seconds ptz_preset: - name: PTZ preset - description: PTZ Preset service for Foscam camera. target: entity: integration: foscam domain: camera fields: preset_name: - description: "The name of the preset to move to. Presets can be created from within the official Foscam apps." required: true example: "TopMost" selector: diff --git a/homeassistant/components/foscam/strings.json b/homeassistant/components/foscam/strings.json index 14aa88b79526e6..35964ee4546ce9 100644 --- a/homeassistant/components/foscam/strings.json +++ b/homeassistant/components/foscam/strings.json @@ -21,5 +21,31 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "services": { + "ptz": { + "name": "PTZ", + "description": "Pan/Tilt service for Foscam camera.", + "fields": { + "movement": { + "name": "Movement", + "description": "Direction of the movement." + }, + "travel_time": { + "name": "Travel time", + "description": "Travel time in seconds." + } + } + }, + "ptz_preset": { + "name": "PTZ preset", + "description": "PTZ Preset service for Foscam camera.", + "fields": { + "preset_name": { + "name": "Preset name", + "description": "The name of the preset to move to. Presets can be created from within the official Foscam apps." + } + } + } } } diff --git a/homeassistant/components/freebox/services.yaml b/homeassistant/components/freebox/services.yaml index 7b2a4059434aeb..8ba6f278bfa8e8 100644 --- a/homeassistant/components/freebox/services.yaml +++ b/homeassistant/components/freebox/services.yaml @@ -1,5 +1,3 @@ # Freebox service entries description. reboot: - name: Reboot - description: Reboots the Freebox. diff --git a/homeassistant/components/freebox/strings.json b/homeassistant/components/freebox/strings.json index 53a5fd59de3b0a..5c4143b4562ac8 100644 --- a/homeassistant/components/freebox/strings.json +++ b/homeassistant/components/freebox/strings.json @@ -20,5 +20,11 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "services": { + "reboot": { + "name": "Reboot", + "description": "Reboots the Freebox." + } } } diff --git a/homeassistant/components/fritz/services.yaml b/homeassistant/components/fritz/services.yaml index 95527257ea9c48..b9828280aa2ac9 100644 --- a/homeassistant/components/fritz/services.yaml +++ b/homeassistant/components/fritz/services.yaml @@ -1,10 +1,6 @@ reconnect: - name: Reconnect - description: Reconnects your FRITZ!Box internet connection fields: device_id: - name: Fritz!Box Device - description: Select the Fritz!Box to reconnect required: true selector: device: @@ -12,12 +8,8 @@ reconnect: entity: device_class: connectivity reboot: - name: Reboot - description: Reboots your FRITZ!Box fields: device_id: - name: Fritz!Box Device - description: Select the Fritz!Box to reboot required: true selector: device: @@ -26,12 +18,8 @@ reboot: device_class: connectivity cleanup: - name: Remove stale device tracker entities - description: Remove FRITZ!Box stale device_tracker entities fields: device_id: - name: Fritz!Box Device - description: Select the Fritz!Box to check required: true selector: device: @@ -39,12 +27,8 @@ cleanup: entity: device_class: connectivity set_guest_wifi_password: - name: Set guest wifi password - description: Set a new password for the guest wifi. The password must be between 8 and 63 characters long. If no additional parameter is set, the password will be auto-generated with a length of 12 characters. fields: device_id: - name: Fritz!Box Device - description: Select the Fritz!Box to check required: true selector: device: @@ -52,14 +36,10 @@ set_guest_wifi_password: entity: device_class: connectivity password: - name: Password - description: New password for the guest wifi required: false selector: text: length: - name: Password length - description: Length of the new password. The password will be auto-generated, if no password is set. required: false selector: number: diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index fcaa56424f1b65..dd845fc2a1b54e 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -55,33 +55,123 @@ }, "entity": { "binary_sensor": { - "is_connected": { "name": "Connection" }, - "is_linked": { "name": "Link" } + "is_connected": { + "name": "Connection" + }, + "is_linked": { + "name": "Link" + } }, "button": { - "cleanup": { "name": "Cleanup" }, - "firmware_update": { "name": "Firmware update" }, - "reconnect": { "name": "Reconnect" } + "cleanup": { + "name": "Cleanup" + }, + "firmware_update": { + "name": "Firmware update" + }, + "reconnect": { + "name": "Reconnect" + } }, "sensor": { - "connection_uptime": { "name": "Connection uptime" }, - "device_uptime": { "name": "Last restart" }, - "external_ip": { "name": "External IP" }, - "external_ipv6": { "name": "External IPv6" }, - "gb_received": { "name": "GB received" }, - "gb_sent": { "name": "GB sent" }, - "kb_s_received": { "name": "Download throughput" }, - "kb_s_sent": { "name": "Upload throughput" }, + "connection_uptime": { + "name": "Connection uptime" + }, + "device_uptime": { + "name": "Last restart" + }, + "external_ip": { + "name": "External IP" + }, + "external_ipv6": { + "name": "External IPv6" + }, + "gb_received": { + "name": "GB received" + }, + "gb_sent": { + "name": "GB sent" + }, + "kb_s_received": { + "name": "Download throughput" + }, + "kb_s_sent": { + "name": "Upload throughput" + }, "link_attenuation_received": { "name": "Link download power attenuation" }, - "link_attenuation_sent": { "name": "Link upload power attenuation" }, - "link_kb_s_received": { "name": "Link download throughput" }, - "link_kb_s_sent": { "name": "Link upload throughput" }, - "link_noise_margin_received": { "name": "Link download noise margin" }, - "link_noise_margin_sent": { "name": "Link upload noise margin" }, - "max_kb_s_received": { "name": "Max connection download throughput" }, - "max_kb_s_sent": { "name": "Max connection upload throughput" } + "link_attenuation_sent": { + "name": "Link upload power attenuation" + }, + "link_kb_s_received": { + "name": "Link download throughput" + }, + "link_kb_s_sent": { + "name": "Link upload throughput" + }, + "link_noise_margin_received": { + "name": "Link download noise margin" + }, + "link_noise_margin_sent": { + "name": "Link upload noise margin" + }, + "max_kb_s_received": { + "name": "Max connection download throughput" + }, + "max_kb_s_sent": { + "name": "Max connection upload throughput" + } + } + }, + "services": { + "reconnect": { + "name": "Reconnect", + "description": "Reconnects your FRITZ!Box internet connection.", + "fields": { + "device_id": { + "name": "Fritz!Box Device", + "description": "Select the Fritz!Box to reconnect." + } + } + }, + "reboot": { + "name": "Reboot", + "description": "Reboots your FRITZ!Box.", + "fields": { + "device_id": { + "name": "Fritz!Box Device", + "description": "Select the Fritz!Box to reboot." + } + } + }, + "cleanup": { + "name": "Remove stale device tracker entities", + "description": "Remove FRITZ!Box stale device_tracker entities.", + "fields": { + "device_id": { + "name": "Fritz!Box Device", + "description": "Select the Fritz!Box to check." + } + } + }, + "set_guest_wifi_password": { + "name": "Set guest Wi-Fi password", + "description": "Sets a new password for the guest Wi-Fi. The password must be between 8 and 63 characters long. If no additional parameter is set, the password will be auto-generated with a length of 12 characters.", + "fields": { + "device_id": { + "name": "Fritz!Box Device", + "description": "Select the Fritz!Box to check." + }, + "password": { + "name": "Password", + "description": "New password for the guest Wi-Fi." + }, + "length": { + "name": "Password length", + "description": "Length of the new password. The password will be auto-generated, if no password is set." + } + } } } } diff --git a/homeassistant/components/fully_kiosk/services.yaml b/homeassistant/components/fully_kiosk/services.yaml index 1f75e4a0347368..7784996da9beb5 100644 --- a/homeassistant/components/fully_kiosk/services.yaml +++ b/homeassistant/components/fully_kiosk/services.yaml @@ -1,50 +1,36 @@ load_url: - name: Load URL - description: Load a URL on Fully Kiosk Browser target: device: integration: fully_kiosk fields: url: - name: URL - description: URL to load. example: "https://home-assistant.io" required: true selector: text: set_config: - name: Set Configuration - description: Set a configuration parameter on Fully Kiosk Browser. target: device: integration: fully_kiosk fields: key: - name: Key - description: Configuration parameter to set. example: "motionSensitivity" required: true selector: text: value: - name: Value - description: Value for the configuration parameter. example: "90" required: true selector: text: start_application: - name: Start Application - description: Start an application on the device running Fully Kiosk Browser. target: device: integration: fully_kiosk fields: application: - name: Application - description: Package name of the application to start. example: "de.ozerov.fully" required: true selector: diff --git a/homeassistant/components/fully_kiosk/strings.json b/homeassistant/components/fully_kiosk/strings.json index c10b6162859a08..2ecac4a57422ea 100644 --- a/homeassistant/components/fully_kiosk/strings.json +++ b/homeassistant/components/fully_kiosk/strings.json @@ -105,5 +105,41 @@ "name": "Screen" } } + }, + "services": { + "load_url": { + "name": "Load URL", + "description": "Loads a URL on Fully Kiosk Browser.", + "fields": { + "url": { + "name": "URL", + "description": "URL to load." + } + } + }, + "set_config": { + "name": "Set Configuration", + "description": "Sets a configuration parameter on Fully Kiosk Browser.", + "fields": { + "key": { + "name": "Key", + "description": "Configuration parameter to set." + }, + "value": { + "name": "Value", + "description": "Value for the configuration parameter." + } + } + }, + "start_application": { + "name": "Start Application", + "description": "Starts an application on the device running Fully Kiosk Browser.", + "fields": { + "application": { + "name": "Application", + "description": "Package name of the application to start." + } + } + } } } From 0329378f2f4db151cd39704053a83e88f6fcb452 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 00:24:16 +0200 Subject: [PATCH 0390/1009] Migrate integration services (L-M) to support translations (#96374) --- homeassistant/components/lcn/services.yaml | 114 -------- homeassistant/components/lcn/strings.json | 256 ++++++++++++++++++ homeassistant/components/lifx/services.yaml | 80 ------ homeassistant/components/lifx/strings.json | 176 ++++++++++++ .../components/litterrobot/services.yaml | 6 - .../components/litterrobot/strings.json | 16 ++ .../components/local_file/services.yaml | 6 - .../components/local_file/strings.json | 18 ++ .../components/logi_circle/services.yaml | 22 -- .../components/logi_circle/strings.json | 56 +++- homeassistant/components/lyric/services.yaml | 4 - homeassistant/components/lyric/strings.json | 12 + homeassistant/components/matrix/services.yaml | 8 - homeassistant/components/matrix/strings.json | 22 ++ homeassistant/components/mazda/services.yaml | 10 - homeassistant/components/mazda/strings.json | 24 ++ .../components/media_extractor/services.yaml | 6 - .../components/media_extractor/strings.json | 18 ++ .../components/melcloud/services.yaml | 12 - .../components/melcloud/strings.json | 22 ++ .../components/microsoft_face/services.yaml | 32 --- .../components/microsoft_face/strings.json | 80 ++++++ homeassistant/components/mill/services.yaml | 10 - homeassistant/components/mill/strings.json | 24 ++ homeassistant/components/minio/services.yaml | 22 -- homeassistant/components/minio/strings.json | 54 ++++ homeassistant/components/modbus/services.yaml | 30 -- homeassistant/components/modbus/strings.json | 72 +++++ .../components/modern_forms/services.yaml | 12 - .../components/modern_forms/strings.json | 30 ++ .../components/monoprice/services.yaml | 4 - .../components/monoprice/strings.json | 10 + .../components/motion_blinds/services.yaml | 8 - .../components/motion_blinds/strings.json | 20 ++ .../components/motioneye/services.yaml | 16 -- .../components/motioneye/strings.json | 38 +++ 36 files changed, 947 insertions(+), 403 deletions(-) create mode 100644 homeassistant/components/local_file/strings.json create mode 100644 homeassistant/components/matrix/strings.json create mode 100644 homeassistant/components/media_extractor/strings.json create mode 100644 homeassistant/components/microsoft_face/strings.json create mode 100644 homeassistant/components/minio/strings.json create mode 100644 homeassistant/components/modbus/strings.json diff --git a/homeassistant/components/lcn/services.yaml b/homeassistant/components/lcn/services.yaml index 52aa886387230e..d62a1e72d45209 100644 --- a/homeassistant/components/lcn/services.yaml +++ b/homeassistant/components/lcn/services.yaml @@ -1,19 +1,13 @@ # Describes the format for available LCN services output_abs: - name: Output absolute brightness - description: Set absolute brightness of output port in percent. fields: address: - name: Address - description: Module address required: true example: "myhome.s0.m7" selector: text: output: - name: Output - description: Output port required: true selector: select: @@ -23,8 +17,6 @@ output_abs: - "output3" - "output4" brightness: - name: Brightness - description: Absolute brightness. required: true selector: number: @@ -32,8 +24,6 @@ output_abs: max: 100 unit_of_measurement: "%" transition: - name: Transition - description: Transition time. default: 0 selector: number: @@ -43,19 +33,13 @@ output_abs: unit_of_measurement: seconds output_rel: - name: Output relative brightness - description: Set relative brightness of output port in percent. fields: address: - name: Address - description: Module address required: true example: "myhome.s0.m7" selector: text: output: - name: Output - description: Output port required: true selector: select: @@ -65,8 +49,6 @@ output_rel: - "output3" - "output4" brightness: - name: Brightness - description: Relative brightness. required: true selector: number: @@ -75,19 +57,13 @@ output_rel: unit_of_measurement: "%" output_toggle: - name: Toggle output - description: Toggle output port. fields: address: - name: Address - description: Module address required: true example: "myhome.s0.m7" selector: text: output: - name: Output - description: Output port required: true selector: select: @@ -97,8 +73,6 @@ output_toggle: - "output3" - "output4" transition: - name: Transition - description: Transition time. default: 0 selector: number: @@ -108,38 +82,26 @@ output_toggle: unit_of_measurement: seconds relays: - name: Relays - description: Set the relays status. fields: address: - name: Address - description: Module address required: true example: "myhome.s0.m7" selector: text: state: - name: State - description: Relays states as string (1=on, 2=off, t=toggle, -=no change) required: true example: "t---001-" selector: text: led: - name: LED - description: Set the led state. fields: address: - name: Address - description: Module address required: true example: "myhome.s0.m7" selector: text: led: - name: LED - description: Led required: true selector: select: @@ -157,8 +119,6 @@ led: - "led11" - "led12" state: - name: State - description: Led state required: true selector: select: @@ -169,19 +129,13 @@ led: - "on" var_abs: - name: Set absolute variable - description: Set absolute value of a variable or setpoint. fields: address: - name: Address - description: Module address required: true example: "myhome.s0.m7" selector: text: variable: - name: Variable - description: Variable or setpoint name required: true default: native selector: @@ -208,16 +162,12 @@ var_abs: - "var11" - "var12" value: - name: Value - description: Value to set. default: 0 selector: number: min: 0 max: 100000 unit_of_measurement: - name: Unit of measurement - description: Unit of value. selector: select: options: @@ -246,19 +196,13 @@ var_abs: - "volt" var_reset: - name: Reset variable - description: Reset value of variable or setpoint. fields: address: - name: Address - description: Module address required: true example: "myhome.s0.m7" selector: text: variable: - name: Variable - description: Variable or setpoint name. required: true selector: select: @@ -285,19 +229,13 @@ var_reset: - "var12" var_rel: - name: Shift variable - description: Shift value of a variable, setpoint or threshold. fields: address: - name: Address - description: Module address required: true example: "myhome.s0.m7" selector: text: variable: - name: Variable - description: Variable or setpoint name required: true selector: select: @@ -340,16 +278,12 @@ var_rel: - "var11" - "var12" value: - name: Value - description: Shift value default: 0 selector: number: min: 0 max: 100000 unit_of_measurement: - name: Unit of measurement - description: Unit of value default: native selector: select: @@ -378,8 +312,6 @@ var_rel: - "v" - "volt" value_reference: - name: Reference value - description: Reference value for setpoint and threshold default: current selector: select: @@ -388,19 +320,13 @@ var_rel: - "prog" lock_regulator: - name: Lock regulator - description: Lock a regulator setpoint. fields: address: - name: Address - description: Module address required: true example: "myhome.s0.m7" selector: text: setpoint: - name: Setpoint - description: Setpoint name required: true selector: select: @@ -423,33 +349,23 @@ lock_regulator: - "thrs4_3" - "thrs4_4" state: - name: State - description: New setpoint state default: false selector: boolean: send_keys: - name: Send keys - description: Send keys (which executes bound commands). fields: address: - name: Address - description: Module address required: true example: "myhome.s0.m7" selector: text: keys: - name: Keys - description: Keys to send required: true example: "a1a5d8" selector: text: state: - name: State - description: "Key state upon sending (must be hit for deferred)" default: hit selector: select: @@ -459,16 +375,12 @@ send_keys: - "break" - "dontsend" time: - name: Time - description: Send delay. default: 0 selector: number: min: 0 max: 60 time_unit: - name: Time unit - description: Time unit of send delay. default: s selector: select: @@ -489,41 +401,29 @@ send_keys: - "seconds" lock_keys: - name: Lock keys - description: Lock keys. fields: address: - name: Address - description: Module address required: true example: "myhome.s0.m7" selector: text: table: - name: Table - description: "Table with keys to lock (must be A for interval)." example: "a" default: a selector: text: state: - name: State - description: Key lock states as string (1=on, 2=off, T=toggle, -=nochange) required: true example: "1---t0--" selector: text: time: - name: Time - description: Lock interval. default: 0 selector: number: min: 0 max: 60 time_unit: - name: Time unit - description: Time unit of lock interval. default: s selector: select: @@ -544,46 +444,32 @@ lock_keys: - "seconds" dyn_text: - name: Dynamic text - description: Send dynamic text to LCN-GTxD displays. fields: address: - name: Address - description: Module address required: true example: "myhome.s0.m7" selector: text: row: - name: Row - description: Text row. required: true selector: number: min: 1 max: 4 text: - name: Text - description: Text to send (up to 60 characters encoded as UTF-8) required: true example: "text up to 60 characters" selector: text: pck: - name: PCK - description: Send arbitrary PCK command. fields: address: - name: Address - description: Module address required: true example: "myhome.s0.m7" selector: text: pck: - name: PCK - description: PCK command (without address header) required: true example: "PIN4" selector: diff --git a/homeassistant/components/lcn/strings.json b/homeassistant/components/lcn/strings.json index bee6c0f0e298b5..267100eaad631d 100644 --- a/homeassistant/components/lcn/strings.json +++ b/homeassistant/components/lcn/strings.json @@ -7,5 +7,261 @@ "codelock": "Code lock code received", "send_keys": "Send keys received" } + }, + "services": { + "output_abs": { + "name": "Output absolute brightness", + "description": "Sets absolute brightness of output port in percent.", + "fields": { + "address": { + "name": "Address", + "description": "Module address." + }, + "output": { + "name": "Output", + "description": "Output port." + }, + "brightness": { + "name": "Brightness", + "description": "Absolute brightness." + }, + "transition": { + "name": "Transition", + "description": "Transition time." + } + } + }, + "output_rel": { + "name": "Output relative brightness", + "description": "Sets relative brightness of output port in percent.", + "fields": { + "address": { + "name": "Address", + "description": "Module address." + }, + "output": { + "name": "Output", + "description": "Output port." + }, + "brightness": { + "name": "Brightness", + "description": "Relative brightness." + } + } + }, + "output_toggle": { + "name": "Toggle output", + "description": "Toggles output port.", + "fields": { + "address": { + "name": "Address", + "description": "Module address." + }, + "output": { + "name": "Output", + "description": "Output port." + }, + "transition": { + "name": "Transition", + "description": "Transition time." + } + } + }, + "relays": { + "name": "Relays", + "description": "Sets the relays status.", + "fields": { + "address": { + "name": "Address", + "description": "Module address." + }, + "state": { + "name": "State", + "description": "Relays states as string (1=on, 2=off, t=toggle, -=no change)." + } + } + }, + "led": { + "name": "LED", + "description": "Sets the led state.", + "fields": { + "address": { + "name": "Address", + "description": "Module address." + }, + "led": { + "name": "LED", + "description": "Led." + }, + "state": { + "name": "State", + "description": "Led state." + } + } + }, + "var_abs": { + "name": "Set absolute variable", + "description": "Sets absolute value of a variable or setpoint.", + "fields": { + "address": { + "name": "Address", + "description": "Module address." + }, + "variable": { + "name": "Variable", + "description": "Variable or setpoint name." + }, + "value": { + "name": "Value", + "description": "Value to set." + }, + "unit_of_measurement": { + "name": "Unit of measurement", + "description": "Unit of value." + } + } + }, + "var_reset": { + "name": "Reset variable", + "description": "Resets value of variable or setpoint.", + "fields": { + "address": { + "name": "Address", + "description": "Module address." + }, + "variable": { + "name": "Variable", + "description": "Variable or setpoint name." + } + } + }, + "var_rel": { + "name": "Shift variable", + "description": "Shift value of a variable, setpoint or threshold.", + "fields": { + "address": { + "name": "Address", + "description": "Module address." + }, + "variable": { + "name": "Variable", + "description": "Variable or setpoint name." + }, + "value": { + "name": "Value", + "description": "Shift value." + }, + "unit_of_measurement": { + "name": "Unit of measurement", + "description": "Unit of value." + }, + "value_reference": { + "name": "Reference value", + "description": "Reference value for setpoint and threshold." + } + } + }, + "lock_regulator": { + "name": "Lock regulator", + "description": "Locks a regulator setpoint.", + "fields": { + "address": { + "name": "Address", + "description": "Module address." + }, + "setpoint": { + "name": "Setpoint", + "description": "Setpoint name." + }, + "state": { + "name": "State", + "description": "New setpoint state." + } + } + }, + "send_keys": { + "name": "Send keys", + "description": "Sends keys (which executes bound commands).", + "fields": { + "address": { + "name": "Address", + "description": "Module address." + }, + "keys": { + "name": "Keys", + "description": "Keys to send." + }, + "state": { + "name": "State", + "description": "Key state upon sending (must be hit for deferred)." + }, + "time": { + "name": "Time", + "description": "Send delay." + }, + "time_unit": { + "name": "Time unit", + "description": "Time unit of send delay." + } + } + }, + "lock_keys": { + "name": "Lock keys", + "description": "Locks keys.", + "fields": { + "address": { + "name": "Address", + "description": "Module address." + }, + "table": { + "name": "Table", + "description": "Table with keys to lock (must be A for interval)." + }, + "state": { + "name": "State", + "description": "Key lock states as string (1=on, 2=off, T=toggle, -=nochange)." + }, + "time": { + "name": "Time", + "description": "Lock interval." + }, + "time_unit": { + "name": "Time unit", + "description": "Time unit of lock interval." + } + } + }, + "dyn_text": { + "name": "Dynamic text", + "description": "Sends dynamic text to LCN-GTxD displays.", + "fields": { + "address": { + "name": "Address", + "description": "Module address." + }, + "row": { + "name": "Row", + "description": "Text row." + }, + "text": { + "name": "Text", + "description": "Text to send (up to 60 characters encoded as UTF-8)." + } + } + }, + "pck": { + "name": "PCK", + "description": "Sends arbitrary PCK command.", + "fields": { + "address": { + "name": "Address", + "description": "Module address." + }, + "pck": { + "name": "PCK", + "description": "PCK command (without address header)." + } + } + } } } diff --git a/homeassistant/components/lifx/services.yaml b/homeassistant/components/lifx/services.yaml index 6613bb6a329e53..83d314396661ee 100644 --- a/homeassistant/components/lifx/services.yaml +++ b/homeassistant/components/lifx/services.yaml @@ -1,21 +1,15 @@ set_hev_cycle_state: - name: Set HEV cycle state - description: Control the HEV LEDs on a LIFX Clean bulb. target: entity: integration: lifx domain: light fields: power: - name: enable - description: Start or stop a Clean cycle. required: true example: true selector: boolean: duration: - name: Duration - description: How long the HEV LEDs will remain on. Uses the configured default duration if not specified. required: false default: 7200 example: 3600 @@ -25,51 +19,37 @@ set_hev_cycle_state: max: 86400 unit_of_measurement: seconds set_state: - name: Set State - description: Set a color/brightness and possibly turn the light on/off. target: entity: integration: lifx domain: light fields: infrared: - name: infrared - description: Automatic infrared level when light brightness is low. selector: number: min: 0 max: 255 zones: - name: Zones - description: List of zone numbers to affect (8 per LIFX Z, starts at 0). example: "[0,5]" selector: object: transition: - name: Transition - description: Duration it takes to get to the final state. selector: number: min: 0 max: 3600 unit_of_measurement: seconds power: - name: Power - description: Turn the light on or off. Leave out to keep the power as it is. selector: boolean: effect_pulse: - name: Pulse effect - description: Run a flash effect by changing to a color and back. target: entity: integration: lifx domain: light fields: mode: - name: Mode - description: "Decides how colors are changed." selector: select: options: @@ -79,35 +59,25 @@ effect_pulse: - "strobe" - "solid" brightness: - name: Brightness value - description: Number indicating brightness of the temporary color, where 1 is the minimum brightness and 255 is the maximum brightness supported by the light. selector: number: min: 1 max: 255 brightness_pct: - name: Brightness - description: Percentage indicating the brightness of the temporary color, where 1 is the minimum brightness and 100 is the maximum brightness supported by the light. selector: number: min: 1 max: 100 unit_of_measurement: "%" color_name: - name: Color name - description: A human readable color name. example: "red" selector: text: rgb_color: - name: RGB color - description: The temporary color in RGB-format. example: "[255, 100, 100]" selector: object: period: - name: Period - description: Duration of the effect. default: 1.0 selector: number: @@ -116,46 +86,34 @@ effect_pulse: step: 0.05 unit_of_measurement: seconds cycles: - name: Cycles - description: Number of times the effect should run. default: 1 selector: number: min: 1 max: 10000 power_on: - name: Power on - description: Powered off lights are temporarily turned on during the effect. default: true selector: boolean: effect_colorloop: - name: Color loop effect - description: Run an effect with looping colors. target: entity: integration: lifx domain: light fields: brightness: - name: Brightness value - description: Number indicating brightness of the color loop, where 1 is the minimum brightness and 255 is the maximum brightness supported by the light. selector: number: min: 0 max: 255 brightness_pct: - name: Brightness - description: Percentage indicating the brightness of the color loop, where 1 is the minimum brightness and 100 is the maximum brightness supported by the light. selector: number: min: 0 max: 100 unit_of_measurement: "%" saturation_min: - name: Minimum saturation - description: Percentage indicating the minimum saturation of the colors in the loop. default: 80 selector: number: @@ -163,8 +121,6 @@ effect_colorloop: max: 100 unit_of_measurement: "%" saturation_max: - name: Maximum saturation - description: Percentage indicating the maximum saturation of the colors in the loop. default: 100 selector: number: @@ -172,8 +128,6 @@ effect_colorloop: max: 100 unit_of_measurement: "%" period: - name: Period - description: Duration between color changes. default: 60 selector: number: @@ -182,8 +136,6 @@ effect_colorloop: step: 0.05 unit_of_measurement: seconds change: - name: Change - description: Hue movement per period, in degrees on a color wheel. default: 20 selector: number: @@ -191,8 +143,6 @@ effect_colorloop: max: 360 unit_of_measurement: "°" spread: - name: Spread - description: Maximum hue difference between participating lights, in degrees on a color wheel. default: 30 selector: number: @@ -200,22 +150,16 @@ effect_colorloop: max: 360 unit_of_measurement: "°" power_on: - name: Power on - description: Powered off lights are temporarily turned on during the effect. default: true selector: boolean: effect_move: - name: Move effect - description: Start the firmware-based Move effect on a LIFX Z, Lightstrip or Beam. target: entity: integration: lifx domain: light fields: speed: - name: Speed - description: How long in seconds for the effect to move across the length of the light. default: 3.0 example: 3.0 selector: @@ -225,8 +169,6 @@ effect_move: step: 0.1 unit_of_measurement: seconds direction: - name: Direction - description: Direction the effect will move across the device. default: right example: right selector: @@ -236,8 +178,6 @@ effect_move: - right - left theme: - name: Theme - description: (Optional) set one of the predefined themes onto the device before starting the effect. example: exciting default: exciting selector: @@ -269,22 +209,16 @@ effect_move: - "tranquil" - "warming" power_on: - name: Power on - description: Powered off lights will be turned on before starting the effect. default: true selector: boolean: effect_flame: - name: Flame effect - description: Start the firmware-based Flame effect on LIFX Tiles or Candle. target: entity: integration: lifx domain: light fields: speed: - name: Speed - description: How fast the flames will move. default: 3 selector: number: @@ -293,22 +227,16 @@ effect_flame: step: 1 unit_of_measurement: seconds power_on: - name: Power on - description: Powered off lights will be turned on before starting the effect. default: true selector: boolean: effect_morph: - name: Morph effect - description: Start the firmware-based Morph effect on LIFX Tiles on Candle. target: entity: integration: lifx domain: light fields: speed: - name: Speed - description: How fast the colors will move. default: 3 selector: number: @@ -317,15 +245,11 @@ effect_morph: step: 1 unit_of_measurement: seconds palette: - name: Palette - description: List of at least 2 and at most 16 colors as hue (0-360), saturation (0-100), brightness (0-100) and kelvin (1500-900) values to use for this effect. Overrides the theme attribute. example: - "[[0, 100, 100, 3500], [60, 100, 100, 3500]]" selector: object: theme: - name: Theme - description: Predefined color theme to use for the effect. Overridden by the palette attribute. selector: select: options: @@ -354,14 +278,10 @@ effect_morph: - "tranquil" - "warming" power_on: - name: Power on - description: Powered off lights will be turned on before starting the effect. default: true selector: boolean: effect_stop: - name: Stop effect - description: Stop a running effect. target: entity: integration: lifx diff --git a/homeassistant/components/lifx/strings.json b/homeassistant/components/lifx/strings.json index 69055d6bbc67cb..cff9b572cc6285 100644 --- a/homeassistant/components/lifx/strings.json +++ b/homeassistant/components/lifx/strings.json @@ -45,5 +45,181 @@ "name": "RSSI" } } + }, + "services": { + "set_hev_cycle_state": { + "name": "Set HEV cycle state", + "description": "Controls the HEV LEDs on a LIFX Clean bulb.", + "fields": { + "power": { + "name": "Enable", + "description": "Start or stop a Clean cycle." + }, + "duration": { + "name": "Duration", + "description": "How long the HEV LEDs will remain on. Uses the configured default duration if not specified." + } + } + }, + "set_state": { + "name": "Set State", + "description": "Sets a color/brightness and possibly turn the light on/off.", + "fields": { + "infrared": { + "name": "Infrared", + "description": "Automatic infrared level when light brightness is low." + }, + "zones": { + "name": "Zones", + "description": "List of zone numbers to affect (8 per LIFX Z, starts at 0)." + }, + "transition": { + "name": "Transition", + "description": "Duration it takes to get to the final state." + }, + "power": { + "name": "Power", + "description": "Turn the light on or off. Leave out to keep the power as it is." + } + } + }, + "effect_pulse": { + "name": "Pulse effect", + "description": "Runs a flash effect by changing to a color and back.", + "fields": { + "mode": { + "name": "Mode", + "description": "Decides how colors are changed." + }, + "brightness": { + "name": "Brightness value", + "description": "Number indicating brightness of the temporary color, where 1 is the minimum brightness and 255 is the maximum brightness supported by the light." + }, + "brightness_pct": { + "name": "Brightness", + "description": "Percentage indicating the brightness of the temporary color, where 1 is the minimum brightness and 100 is the maximum brightness supported by the light." + }, + "color_name": { + "name": "Color name", + "description": "A human readable color name." + }, + "rgb_color": { + "name": "RGB color", + "description": "The temporary color in RGB-format." + }, + "period": { + "name": "Period", + "description": "Duration of the effect." + }, + "cycles": { + "name": "Cycles", + "description": "Number of times the effect should run." + }, + "power_on": { + "name": "Power on", + "description": "Powered off lights are temporarily turned on during the effect." + } + } + }, + "effect_colorloop": { + "name": "Color loop effect", + "description": "Runs an effect with looping colors.", + "fields": { + "brightness": { + "name": "Brightness value", + "description": "Number indicating brightness of the color loop, where 1 is the minimum brightness and 255 is the maximum brightness supported by the light." + }, + "brightness_pct": { + "name": "Brightness", + "description": "Percentage indicating the brightness of the color loop, where 1 is the minimum brightness and 100 is the maximum brightness supported by the light." + }, + "saturation_min": { + "name": "Minimum saturation", + "description": "Percentage indicating the minimum saturation of the colors in the loop." + }, + "saturation_max": { + "name": "Maximum saturation", + "description": "Percentage indicating the maximum saturation of the colors in the loop." + }, + "period": { + "name": "Period", + "description": "Duration between color changes." + }, + "change": { + "name": "Change", + "description": "Hue movement per period, in degrees on a color wheel." + }, + "spread": { + "name": "Spread", + "description": "Maximum hue difference between participating lights, in degrees on a color wheel." + }, + "power_on": { + "name": "Power on", + "description": "Powered off lights are temporarily turned on during the effect." + } + } + }, + "effect_move": { + "name": "Move effect", + "description": "Starts the firmware-based Move effect on a LIFX Z, Lightstrip or Beam.", + "fields": { + "speed": { + "name": "Speed", + "description": "How long in seconds for the effect to move across the length of the light." + }, + "direction": { + "name": "Direction", + "description": "Direction the effect will move across the device." + }, + "theme": { + "name": "Theme", + "description": "(Optional) set one of the predefined themes onto the device before starting the effect." + }, + "power_on": { + "name": "Power on", + "description": "Powered off lights will be turned on before starting the effect." + } + } + }, + "effect_flame": { + "name": "Flame effect", + "description": "Starts the firmware-based Flame effect on LIFX Tiles or Candle.", + "fields": { + "speed": { + "name": "Speed", + "description": "How fast the flames will move." + }, + "power_on": { + "name": "Power on", + "description": "Powered off lights will be turned on before starting the effect." + } + } + }, + "effect_morph": { + "name": "Morph effect", + "description": "Starts the firmware-based Morph effect on LIFX Tiles on Candle.", + "fields": { + "speed": { + "name": "Speed", + "description": "How fast the colors will move." + }, + "palette": { + "name": "Palette", + "description": "List of at least 2 and at most 16 colors as hue (0-360), saturation (0-100), brightness (0-100) and kelvin (1500-900) values to use for this effect. Overrides the theme attribute." + }, + "theme": { + "name": "Theme", + "description": "Predefined color theme to use for the effect. Overridden by the palette attribute." + }, + "power_on": { + "name": "Power on", + "description": "Powered off lights will be turned on before starting the effect." + } + } + }, + "effect_stop": { + "name": "Stop effect", + "description": "Stops a running effect." + } } } diff --git a/homeassistant/components/litterrobot/services.yaml b/homeassistant/components/litterrobot/services.yaml index 164445e375f375..48d17dfdcf72c5 100644 --- a/homeassistant/components/litterrobot/services.yaml +++ b/homeassistant/components/litterrobot/services.yaml @@ -1,21 +1,15 @@ # Describes the format for available Litter-Robot services set_sleep_mode: - name: Set sleep mode - description: Set the sleep mode and start time. target: entity: integration: litterrobot fields: enabled: - name: Enabled - description: Whether sleep mode should be enabled. required: true selector: boolean: start_time: - name: Start time - description: The start time at which the Litter-Robot will enter sleep mode and prevent an automatic clean cycle for 8 hours. required: false example: '"22:30:00"' selector: diff --git a/homeassistant/components/litterrobot/strings.json b/homeassistant/components/litterrobot/strings.json index 5a6a0bf6998f38..e5cd35703f3edb 100644 --- a/homeassistant/components/litterrobot/strings.json +++ b/homeassistant/components/litterrobot/strings.json @@ -137,5 +137,21 @@ "name": "Firmware" } } + }, + "services": { + "set_sleep_mode": { + "name": "Set sleep mode", + "description": "Sets the sleep mode and start time.", + "fields": { + "enabled": { + "name": "Enabled", + "description": "Whether sleep mode should be enabled." + }, + "start_time": { + "name": "Start time", + "description": "The start time at which the Litter-Robot will enter sleep mode and prevent an automatic clean cycle for 8 hours." + } + } + } } } diff --git a/homeassistant/components/local_file/services.yaml b/homeassistant/components/local_file/services.yaml index f4382decb0ff4f..5fc0b11f4c2d0d 100644 --- a/homeassistant/components/local_file/services.yaml +++ b/homeassistant/components/local_file/services.yaml @@ -1,17 +1,11 @@ update_file_path: - name: Update file path - description: Use this service to change the file displayed by the camera. fields: entity_id: - name: Entity - description: Name of the entity_id of the camera to update. required: true selector: entity: domain: camera file_path: - name: file path - description: The full path to the new image file to be displayed. required: true example: "/config/www/images/image.jpg" selector: diff --git a/homeassistant/components/local_file/strings.json b/homeassistant/components/local_file/strings.json new file mode 100644 index 00000000000000..3f977fc941ea52 --- /dev/null +++ b/homeassistant/components/local_file/strings.json @@ -0,0 +1,18 @@ +{ + "services": { + "update_file_path": { + "name": "Updates file path", + "description": "Use this service to change the file displayed by the camera.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name of the entity_id of the camera to update." + }, + "file_path": { + "name": "File path", + "description": "The full path to the new image file to be displayed." + } + } + } + } +} diff --git a/homeassistant/components/logi_circle/services.yaml b/homeassistant/components/logi_circle/services.yaml index 10df6c564b481d..cb855a953a6901 100644 --- a/homeassistant/components/logi_circle/services.yaml +++ b/homeassistant/components/logi_circle/services.yaml @@ -1,19 +1,13 @@ # Describes the format for available Logi Circle services set_config: - name: Set config - description: Set a configuration property. fields: entity_id: - name: Entity - description: Name(s) of entities to apply the operation mode to. selector: entity: integration: logi_circle domain: camera mode: - name: Mode - description: "Operation mode. Allowed values: LED, RECORDING_MODE." required: true selector: select: @@ -21,52 +15,36 @@ set_config: - "LED" - "RECORDING_MODE" value: - name: Value - description: "Operation value." required: true selector: boolean: livestream_snapshot: - name: Livestream snapshot - description: Take a snapshot from the camera's livestream. Will wake the camera from sleep if required. fields: entity_id: - name: Entity - description: Name(s) of entities to create snapshots from. selector: entity: integration: logi_circle domain: camera filename: - name: File name - description: Template of a Filename. Variable is entity_id. required: true example: "/tmp/snapshot_{{ entity_id }}.jpg" selector: text: livestream_record: - name: Livestream record - description: Take a video recording from the camera's livestream. fields: entity_id: - name: Entity - description: Name(s) of entities to create recordings from. selector: entity: integration: logi_circle domain: camera filename: - name: File name - description: Template of a Filename. Variable is entity_id. required: true example: "/tmp/snapshot_{{ entity_id }}.mp4" selector: text: duration: - name: Duration - description: Recording duration. required: true selector: number: diff --git a/homeassistant/components/logi_circle/strings.json b/homeassistant/components/logi_circle/strings.json index a73ade9311ce1c..9a06fb45ad24ad 100644 --- a/homeassistant/components/logi_circle/strings.json +++ b/homeassistant/components/logi_circle/strings.json @@ -4,7 +4,9 @@ "user": { "title": "Authentication Provider", "description": "Pick via which authentication provider you want to authenticate with Logi Circle.", - "data": { "flow_impl": "Provider" } + "data": { + "flow_impl": "Provider" + } }, "auth": { "title": "Authenticate with Logi Circle", @@ -22,5 +24,57 @@ "external_setup": "Logi Circle successfully configured from another flow.", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]" } + }, + "services": { + "set_config": { + "name": "Set config", + "description": "Sets a configuration property.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name(s) of entities to apply the operation mode to." + }, + "mode": { + "name": "Mode", + "description": "Operation mode. Allowed values: LED, RECORDING_MODE." + }, + "value": { + "name": "Value", + "description": "Operation value." + } + } + }, + "livestream_snapshot": { + "name": "Livestream snapshot", + "description": "Takes a snapshot from the camera's livestream. Will wake the camera from sleep if required.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name(s) of entities to create snapshots from." + }, + "filename": { + "name": "File name", + "description": "Template of a Filename. Variable is entity_id." + } + } + }, + "livestream_record": { + "name": "Livestream record", + "description": "Takes a video recording from the camera's livestream.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name(s) of entities to create recordings from." + }, + "filename": { + "name": "File name", + "description": "Template of a Filename. Variable is entity_id." + }, + "duration": { + "name": "Duration", + "description": "Recording duration." + } + } + } } } diff --git a/homeassistant/components/lyric/services.yaml b/homeassistant/components/lyric/services.yaml index 69c802d90aaa2c..c3c4bc640bf583 100644 --- a/homeassistant/components/lyric/services.yaml +++ b/homeassistant/components/lyric/services.yaml @@ -1,6 +1,4 @@ set_hold_time: - name: Set Hold Time - description: "Sets the time to hold until" target: device: integration: lyric @@ -9,8 +7,6 @@ set_hold_time: domain: climate fields: time_period: - name: Time Period - description: Time to hold until default: "01:00:00" example: "01:00:00" required: true diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json index 3c9cd6043dfa25..2271d4201f65dc 100644 --- a/homeassistant/components/lyric/strings.json +++ b/homeassistant/components/lyric/strings.json @@ -17,5 +17,17 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "services": { + "set_hold_time": { + "name": "Set Hold Time", + "description": "Sets the time to hold until.", + "fields": { + "time_period": { + "name": "Time Period", + "description": "Time to hold until." + } + } + } } } diff --git a/homeassistant/components/matrix/services.yaml b/homeassistant/components/matrix/services.yaml index 9b5171d1483e5d..f2ce72397d4f83 100644 --- a/homeassistant/components/matrix/services.yaml +++ b/homeassistant/components/matrix/services.yaml @@ -1,24 +1,16 @@ send_message: - name: Send message - description: Send message to target room(s) fields: message: - name: Message - description: The message to be sent. required: true example: This is a message I am sending to matrix selector: text: target: - name: Target - description: A list of room(s) to send the message to. required: true example: "#hasstest:matrix.org" selector: text: data: - name: Data - description: Extended information of notification. Supports list of images. Supports message format. Optional. example: "{'images': ['/tmp/test.jpg'], 'format': 'text'}" selector: object: diff --git a/homeassistant/components/matrix/strings.json b/homeassistant/components/matrix/strings.json new file mode 100644 index 00000000000000..03d4c5728a5397 --- /dev/null +++ b/homeassistant/components/matrix/strings.json @@ -0,0 +1,22 @@ +{ + "services": { + "send_message": { + "name": "Send message", + "description": "Sends message to target room(s).", + "fields": { + "message": { + "name": "Message", + "description": "The message to be sent." + }, + "target": { + "name": "Target", + "description": "A list of room(s) to send the message to." + }, + "data": { + "name": "Data", + "description": "Extended information of notification. Supports list of images. Supports message format. Optional." + } + } + } + } +} diff --git a/homeassistant/components/mazda/services.yaml b/homeassistant/components/mazda/services.yaml index 1abf8bd5dea2e6..b401c01f3a3465 100644 --- a/homeassistant/components/mazda/services.yaml +++ b/homeassistant/components/mazda/services.yaml @@ -1,17 +1,11 @@ send_poi: - name: Send POI - description: Send a GPS location to the vehicle's navigation system as a POI (Point of Interest). Requires a navigation SD card installed in the vehicle. fields: device_id: - name: Vehicle - description: The vehicle to send the GPS location to required: true selector: device: integration: mazda latitude: - name: Latitude - description: The latitude of the location to send example: 12.34567 required: true selector: @@ -21,8 +15,6 @@ send_poi: unit_of_measurement: ° mode: box longitude: - name: Longitude - description: The longitude of the location to send example: -34.56789 required: true selector: @@ -32,8 +24,6 @@ send_poi: unit_of_measurement: ° mode: box poi_name: - name: POI name - description: A friendly name for the location example: Work required: true selector: diff --git a/homeassistant/components/mazda/strings.json b/homeassistant/components/mazda/strings.json index d2cc1bcfec97d9..9c881e6324fc4c 100644 --- a/homeassistant/components/mazda/strings.json +++ b/homeassistant/components/mazda/strings.json @@ -20,5 +20,29 @@ "description": "Please enter the email address and password you use to log into the MyMazda mobile app." } } + }, + "services": { + "send_poi": { + "name": "Send POI", + "description": "Sends a GPS location to the vehicle's navigation system as a POI (Point of Interest). Requires a navigation SD card installed in the vehicle.", + "fields": { + "device_id": { + "name": "Vehicle", + "description": "The vehicle to send the GPS location to." + }, + "latitude": { + "name": "Latitude", + "description": "The latitude of the location to send." + }, + "longitude": { + "name": "Longitude", + "description": "The longitude of the location to send." + }, + "poi_name": { + "name": "POI name", + "description": "A friendly name for the location." + } + } + } } } diff --git a/homeassistant/components/media_extractor/services.yaml b/homeassistant/components/media_extractor/services.yaml index 0b7295bd7bfcf9..8af2d12d0e9015 100644 --- a/homeassistant/components/media_extractor/services.yaml +++ b/homeassistant/components/media_extractor/services.yaml @@ -1,20 +1,14 @@ play_media: - name: Play media - description: Downloads file from given URL. target: entity: domain: media_player fields: media_content_id: - name: Media content ID - description: The ID of the content to play. Platform dependent. required: true example: "https://soundcloud.com/bruttoband/brutto-11" selector: text: media_content_type: - name: Media content type - description: The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST MUSIC. required: true selector: select: diff --git a/homeassistant/components/media_extractor/strings.json b/homeassistant/components/media_extractor/strings.json new file mode 100644 index 00000000000000..0cdffd5d508f74 --- /dev/null +++ b/homeassistant/components/media_extractor/strings.json @@ -0,0 +1,18 @@ +{ + "services": { + "play_media": { + "name": "Play media", + "description": "Downloads file from given URL.", + "fields": { + "media_content_id": { + "name": "Media content ID", + "description": "The ID of the content to play. Platform dependent." + }, + "media_content_type": { + "name": "Media content type", + "description": "The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST MUSIC." + } + } + } + } +} diff --git a/homeassistant/components/melcloud/services.yaml b/homeassistant/components/melcloud/services.yaml index f470076ee7fd65..f13cd646388a15 100644 --- a/homeassistant/components/melcloud/services.yaml +++ b/homeassistant/components/melcloud/services.yaml @@ -1,34 +1,22 @@ set_vane_horizontal: - name: Set vane horizontal - description: Sets horizontal vane position. target: entity: integration: melcloud domain: climate fields: position: - name: Position - description: > - Horizontal vane position. Possible options can be found in the - vane_horizontal_positions state attribute. required: true example: "auto" selector: text: set_vane_vertical: - name: Set vane vertical - description: Sets vertical vane position. target: entity: integration: melcloud domain: climate fields: position: - name: Position - description: > - Vertical vane position. Possible options can be found in the - vane_vertical_positions state attribute. required: true example: "auto" selector: diff --git a/homeassistant/components/melcloud/strings.json b/homeassistant/components/melcloud/strings.json index a1bce80d7ad425..bef65e288805b3 100644 --- a/homeassistant/components/melcloud/strings.json +++ b/homeassistant/components/melcloud/strings.json @@ -18,5 +18,27 @@ "abort": { "already_configured": "MELCloud integration already configured for this email. Access token has been refreshed." } + }, + "services": { + "set_vane_horizontal": { + "name": "Set vane horizontal", + "description": "Sets horizontal vane position.", + "fields": { + "position": { + "name": "Position", + "description": "Horizontal vane position. Possible options can be found in the vane_horizontal_positions state attribute.\n." + } + } + }, + "set_vane_vertical": { + "name": "Set vane vertical", + "description": "Sets vertical vane position.", + "fields": { + "position": { + "name": "Position", + "description": "Vertical vane position. Possible options can be found in the vane_vertical_positions state attribute.\n." + } + } + } } } diff --git a/homeassistant/components/microsoft_face/services.yaml b/homeassistant/components/microsoft_face/services.yaml index e27e29dfc6fad3..13078495b43bd6 100644 --- a/homeassistant/components/microsoft_face/services.yaml +++ b/homeassistant/components/microsoft_face/services.yaml @@ -1,93 +1,61 @@ create_group: - name: Create group - description: Create a new person group. fields: name: - name: Name - description: Name of the group. required: true example: family selector: text: create_person: - name: Create person - description: Create a new person in the group. fields: group: - name: Group - description: Name of the group required: true example: family selector: text: name: - name: Name - description: Name of the person required: true example: Hans selector: text: delete_group: - name: Delete group - description: Delete a new person group. fields: name: - name: Name - description: Name of the group. required: true example: family selector: text: delete_person: - name: Delete person - description: Delete a person in the group. fields: group: - name: Group - description: Name of the group. required: true example: family selector: text: name: - name: Name - description: Name of the person. required: true example: Hans selector: text: face_person: - name: Face person - description: Add a new picture to a person. fields: camera_entity: - name: Camera entity - description: Camera to take a picture. required: true example: camera.door selector: text: group: - name: Group - description: Name of the group. required: true example: family selector: text: person: - name: Person - description: Name of the person. required: true example: Hans selector: text: train_group: - name: Train group - description: Train a person group. fields: group: - name: Group - description: Name of the group required: true example: family selector: diff --git a/homeassistant/components/microsoft_face/strings.json b/homeassistant/components/microsoft_face/strings.json new file mode 100644 index 00000000000000..b1008336992f05 --- /dev/null +++ b/homeassistant/components/microsoft_face/strings.json @@ -0,0 +1,80 @@ +{ + "services": { + "create_group": { + "name": "Create group", + "description": "Creates a new person group.", + "fields": { + "name": { + "name": "Name", + "description": "Name of the group." + } + } + }, + "create_person": { + "name": "Create person", + "description": "Creates a new person in the group.", + "fields": { + "group": { + "name": "Group", + "description": "Name of the group." + }, + "name": { + "name": "Name", + "description": "Name of the person." + } + } + }, + "delete_group": { + "name": "Delete group", + "description": "Deletes a new person group.", + "fields": { + "name": { + "name": "Name", + "description": "Name of the group." + } + } + }, + "delete_person": { + "name": "Delete person", + "description": "Deletes a person in the group.", + "fields": { + "group": { + "name": "Group", + "description": "Name of the group." + }, + "name": { + "name": "Name", + "description": "Name of the person." + } + } + }, + "face_person": { + "name": "Face person", + "description": "Adds a new picture to a person.", + "fields": { + "camera_entity": { + "name": "Camera entity", + "description": "Camera to take a picture." + }, + "group": { + "name": "Group", + "description": "Name of the group." + }, + "person": { + "name": "Person", + "description": "Name of the person." + } + } + }, + "train_group": { + "name": "Train group", + "description": "Trains a person group.", + "fields": { + "group": { + "name": "Group", + "description": "Name of the group." + } + } + } + } +} diff --git a/homeassistant/components/mill/services.yaml b/homeassistant/components/mill/services.yaml index ad5cff4a5ff1b0..14e2196eb8350b 100644 --- a/homeassistant/components/mill/services.yaml +++ b/homeassistant/components/mill/services.yaml @@ -1,33 +1,23 @@ set_room_temperature: - name: Set room temperature - description: Set Mill room temperatures. fields: room_name: - name: Room name - description: Name of room to change. required: true example: "kitchen" selector: text: away_temp: - name: Away temperature - description: Away temp. selector: number: min: 0 max: 100 unit_of_measurement: "°" comfort_temp: - name: Comfort temperature - description: Comfort temp. selector: number: min: 0 max: 100 unit_of_measurement: "°" sleep_temp: - name: Sleep temperature - description: Sleep temp. selector: number: min: 0 diff --git a/homeassistant/components/mill/strings.json b/homeassistant/components/mill/strings.json index 5f4cec1336ec64..caeea189c0e13a 100644 --- a/homeassistant/components/mill/strings.json +++ b/homeassistant/components/mill/strings.json @@ -26,5 +26,29 @@ "description": "Local IP address of the device." } } + }, + "services": { + "set_room_temperature": { + "name": "Set room temperature", + "description": "Sets Mill room temperatures.", + "fields": { + "room_name": { + "name": "Room name", + "description": "Name of room to change." + }, + "away_temp": { + "name": "Away temperature", + "description": "Away temp." + }, + "comfort_temp": { + "name": "Comfort temperature", + "description": "Comfort temp." + }, + "sleep_temp": { + "name": "Sleep temperature", + "description": "Sleep temp." + } + } + } } } diff --git a/homeassistant/components/minio/services.yaml b/homeassistant/components/minio/services.yaml index 39e430ab165ba1..b40797bc1651c4 100644 --- a/homeassistant/components/minio/services.yaml +++ b/homeassistant/components/minio/services.yaml @@ -1,69 +1,47 @@ get: - name: Get - description: Download file from Minio. fields: bucket: - name: Bucket - description: Bucket to use. required: true example: camera-files selector: text: key: - name: Kay - description: Object key of the file. required: true example: front_camera/2018/01/02/snapshot_12512514.jpg selector: text: file_path: - name: File path - description: File path on local filesystem. required: true example: /data/camera_files/snapshot.jpg selector: text: put: - name: Put - description: Upload file to Minio. fields: bucket: - name: Bucket - description: Bucket to use. required: true example: camera-files selector: text: key: - name: Key - description: Object key of the file. required: true example: front_camera/2018/01/02/snapshot_12512514.jpg selector: text: file_path: - name: File path - description: File path on local filesystem. required: true example: /data/camera_files/snapshot.jpg selector: text: remove: - name: Remove - description: Delete file from Minio. fields: bucket: - name: Bucket - description: Bucket to use. required: true example: camera-files selector: text: key: - name: Key - description: Object key of the file. required: true example: front_camera/2018/01/02/snapshot_12512514.jpg selector: diff --git a/homeassistant/components/minio/strings.json b/homeassistant/components/minio/strings.json new file mode 100644 index 00000000000000..21902ad1825b8a --- /dev/null +++ b/homeassistant/components/minio/strings.json @@ -0,0 +1,54 @@ +{ + "services": { + "get": { + "name": "Get", + "description": "Downloads file from Minio.", + "fields": { + "bucket": { + "name": "Bucket", + "description": "Bucket to use." + }, + "key": { + "name": "Kay", + "description": "Object key of the file." + }, + "file_path": { + "name": "File path", + "description": "File path on local filesystem." + } + } + }, + "put": { + "name": "Put", + "description": "Uploads file to Minio.", + "fields": { + "bucket": { + "name": "Bucket", + "description": "Bucket to use." + }, + "key": { + "name": "Key", + "description": "Object key of the file." + }, + "file_path": { + "name": "File path", + "description": "File path on local filesystem." + } + } + }, + "remove": { + "name": "Remove", + "description": "Deletes file from Minio.", + "fields": { + "bucket": { + "name": "Bucket", + "description": "Bucket to use." + }, + "key": { + "name": "Key", + "description": "Object key of the file." + } + } + } + } +} diff --git a/homeassistant/components/modbus/services.yaml b/homeassistant/components/modbus/services.yaml index 07acf0a72df54e..8dafa911ada1d1 100644 --- a/homeassistant/components/modbus/services.yaml +++ b/homeassistant/components/modbus/services.yaml @@ -1,92 +1,62 @@ reload: - name: Reload - description: Reload all modbus entities. write_coil: - name: Write coil - description: Write to a modbus coil. fields: address: - name: Address - description: Address of the register to write to. required: true selector: number: min: 0 max: 65535 state: - name: State - description: State to write. required: true example: "0 or [1,0]" selector: object: slave: - name: Slave - description: Address of the modbus unit/slave. required: false selector: number: min: 1 max: 255 hub: - name: Hub - description: Modbus hub name. example: "hub1" default: "modbus_hub" selector: text: write_register: - name: Write register - description: Write to a modbus holding register. fields: address: - name: Address - description: Address of the holding register to write to. required: true selector: number: min: 0 max: 65535 slave: - name: Slave - description: Address of the modbus unit/slave. required: false selector: number: min: 1 max: 255 value: - name: Value - description: Value (single value or array) to write. required: true example: "0 or [4,0]" selector: object: hub: - name: Hub - description: Modbus hub name. example: "hub1" default: "modbus_hub" selector: text: stop: - name: Stop - description: Stop modbus hub. fields: hub: - name: Hub - description: Modbus hub name. example: "hub1" default: "modbus_hub" selector: text: restart: - name: Restart - description: Restart modbus hub (if running stop then start). fields: hub: - name: Hub - description: Modbus hub name. example: "hub1" default: "modbus_hub" selector: diff --git a/homeassistant/components/modbus/strings.json b/homeassistant/components/modbus/strings.json new file mode 100644 index 00000000000000..ad07a4d7565966 --- /dev/null +++ b/homeassistant/components/modbus/strings.json @@ -0,0 +1,72 @@ +{ + "services": { + "reload": { + "name": "Reload", + "description": "Reloads all modbus entities." + }, + "write_coil": { + "name": "Write coil", + "description": "Writes to a modbus coil.", + "fields": { + "address": { + "name": "Address", + "description": "Address of the register to write to." + }, + "state": { + "name": "State", + "description": "State to write." + }, + "slave": { + "name": "Slave", + "description": "Address of the modbus unit/slave." + }, + "hub": { + "name": "Hub", + "description": "Modbus hub name." + } + } + }, + "write_register": { + "name": "Write register", + "description": "Writes to a modbus holding register.", + "fields": { + "address": { + "name": "Address", + "description": "Address of the holding register to write to." + }, + "slave": { + "name": "Slave", + "description": "Address of the modbus unit/slave." + }, + "value": { + "name": "Value", + "description": "Value (single value or array) to write." + }, + "hub": { + "name": "Hub", + "description": "Modbus hub name." + } + } + }, + "stop": { + "name": "Stop", + "description": "Stops modbus hub.", + "fields": { + "hub": { + "name": "Hub", + "description": "Modbus hub name." + } + } + }, + "restart": { + "name": "Restart", + "description": "Restarts modbus hub (if running stop then start).", + "fields": { + "hub": { + "name": "Hub", + "description": "Modbus hub name." + } + } + } + } +} diff --git a/homeassistant/components/modern_forms/services.yaml b/homeassistant/components/modern_forms/services.yaml index ce3c29f39b527b..07150f530be477 100644 --- a/homeassistant/components/modern_forms/services.yaml +++ b/homeassistant/components/modern_forms/services.yaml @@ -1,14 +1,10 @@ set_light_sleep_timer: - name: Set light sleep timer - description: Set a sleep timer on a Modern Forms light. target: entity: integration: modern_forms domain: light fields: sleep_time: - name: Sleep Time - description: Number of minutes to set the timer. required: true example: "900" selector: @@ -17,23 +13,17 @@ set_light_sleep_timer: max: 1440 unit_of_measurement: minutes clear_light_sleep_timer: - name: Clear light sleep timer - description: Clear the sleep timer on a Modern Forms light. target: entity: integration: modern_forms domain: light set_fan_sleep_timer: - name: Set fan sleep timer - description: Set a sleep timer on a Modern Forms fan. target: entity: integration: modern_forms domain: fan fields: sleep_time: - name: Sleep Time - description: Number of minutes to set the timer. required: true example: "900" selector: @@ -42,8 +32,6 @@ set_fan_sleep_timer: max: 1440 unit_of_measurement: minutes clear_fan_sleep_timer: - name: Clear fan sleep timer - description: Clear the sleep timer on a Modern Forms fan. target: entity: integration: modern_forms diff --git a/homeassistant/components/modern_forms/strings.json b/homeassistant/components/modern_forms/strings.json index fc30709960b80d..397d7267bc092e 100644 --- a/homeassistant/components/modern_forms/strings.json +++ b/homeassistant/components/modern_forms/strings.json @@ -20,5 +20,35 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } + }, + "services": { + "set_light_sleep_timer": { + "name": "Set light sleep timer", + "description": "Sets a sleep timer on a Modern Forms light.", + "fields": { + "sleep_time": { + "name": "Sleep time", + "description": "Number of minutes to set the timer." + } + } + }, + "clear_light_sleep_timer": { + "name": "Clear light sleep timer", + "description": "Clears the sleep timer on a Modern Forms light." + }, + "set_fan_sleep_timer": { + "name": "Set fan sleep timer", + "description": "Sets a sleep timer on a Modern Forms fan.", + "fields": { + "sleep_time": { + "name": "Sleep time", + "description": "Number of minutes to set the timer." + } + } + }, + "clear_fan_sleep_timer": { + "name": "Clear fan sleep timer", + "description": "Clears the sleep timer on a Modern Forms fan." + } } } diff --git a/homeassistant/components/monoprice/services.yaml b/homeassistant/components/monoprice/services.yaml index 93275fd2a1dc5d..7f3039509ba496 100644 --- a/homeassistant/components/monoprice/services.yaml +++ b/homeassistant/components/monoprice/services.yaml @@ -1,14 +1,10 @@ snapshot: - name: Snapshot - description: Take a snapshot of the media player zone. target: entity: integration: monoprice domain: media_player restore: - name: Restore - description: Restore a snapshot of the media player zone. target: entity: integration: monoprice diff --git a/homeassistant/components/monoprice/strings.json b/homeassistant/components/monoprice/strings.json index 008c182f41b063..4ecf4cfee45caa 100644 --- a/homeassistant/components/monoprice/strings.json +++ b/homeassistant/components/monoprice/strings.json @@ -36,5 +36,15 @@ } } } + }, + "services": { + "snapshot": { + "name": "Snapshot", + "description": "Takes a snapshot of the media player zone." + }, + "restore": { + "name": "Restore", + "description": "Restores a snapshot of the media player zone." + } } } diff --git a/homeassistant/components/motion_blinds/services.yaml b/homeassistant/components/motion_blinds/services.yaml index d37d6bb5be83c4..7b18979ed0ed8a 100644 --- a/homeassistant/components/motion_blinds/services.yaml +++ b/homeassistant/components/motion_blinds/services.yaml @@ -1,16 +1,12 @@ # Describes the format for available motion blinds services set_absolute_position: - name: Set absolute position - description: "Set the absolute position of the cover." target: entity: integration: motion_blinds domain: cover fields: absolute_position: - name: Absolute position - description: Absolute position to move to. required: true selector: number: @@ -18,16 +14,12 @@ set_absolute_position: max: 100 unit_of_measurement: "%" tilt_position: - name: Tilt position - description: Tilt position to move to. selector: number: min: 0 max: 100 unit_of_measurement: "%" width: - name: Width - description: Specify the width that is covered, only for TDBU Combined entities. selector: number: min: 1 diff --git a/homeassistant/components/motion_blinds/strings.json b/homeassistant/components/motion_blinds/strings.json index 47c0867187e894..0e0a32bfb2420e 100644 --- a/homeassistant/components/motion_blinds/strings.json +++ b/homeassistant/components/motion_blinds/strings.json @@ -40,5 +40,25 @@ } } } + }, + "services": { + "set_absolute_position": { + "name": "Set absolute position", + "description": "Sets the absolute position of the cover.", + "fields": { + "absolute_position": { + "name": "Absolute position", + "description": "Absolute position to move to." + }, + "tilt_position": { + "name": "Tilt position", + "description": "Tilt position to move to." + }, + "width": { + "name": "Width", + "description": "Specify the width that is covered, only for TDBU Combined entities." + } + } + } } } diff --git a/homeassistant/components/motioneye/services.yaml b/homeassistant/components/motioneye/services.yaml index 2970124c000652..c5a11db8a6f782 100644 --- a/homeassistant/components/motioneye/services.yaml +++ b/homeassistant/components/motioneye/services.yaml @@ -1,6 +1,4 @@ set_text_overlay: - name: Set Text Overlay - description: Sets the text overlay for a camera. target: device: integration: motioneye @@ -8,8 +6,6 @@ set_text_overlay: integration: motioneye fields: left_text: - name: Left Text Overlay - description: Text to display on the left required: false advanced: false example: "timestamp" @@ -22,8 +18,6 @@ set_text_overlay: - "timestamp" - "custom-text" custom_left_text: - name: Left Custom Text - description: Custom text to display on the left required: false advanced: false example: "Hello on the left!" @@ -32,8 +26,6 @@ set_text_overlay: text: multiline: true right_text: - name: Right Text Overlay - description: Text to display on the right required: false advanced: false example: "timestamp" @@ -46,8 +38,6 @@ set_text_overlay: - "timestamp" - "custom-text" custom_right_text: - name: Right Custom Text - description: Custom text to display on the right required: false advanced: false example: "Hello on the right!" @@ -57,8 +47,6 @@ set_text_overlay: multiline: true action: - name: Action - description: Trigger a motionEye action target: device: integration: motioneye @@ -66,8 +54,6 @@ action: integration: motioneye fields: action: - name: Action - description: Action to trigger required: true advanced: false example: "snapshot" @@ -101,8 +87,6 @@ action: - "preset9" snapshot: - name: Snapshot - description: Trigger a motionEye still snapshot target: device: integration: motioneye diff --git a/homeassistant/components/motioneye/strings.json b/homeassistant/components/motioneye/strings.json index f92fa11cd77170..fdf73cd8cf8ee7 100644 --- a/homeassistant/components/motioneye/strings.json +++ b/homeassistant/components/motioneye/strings.json @@ -36,5 +36,43 @@ } } } + }, + "services": { + "set_text_overlay": { + "name": "Set text overlay", + "description": "Sets the text overlay for a camera.", + "fields": { + "left_text": { + "name": "Left text overlay", + "description": "Text to display on the left." + }, + "custom_left_text": { + "name": "Left custom text", + "description": "Custom text to display on the left." + }, + "right_text": { + "name": "Right text overlay", + "description": "Text to display on the right." + }, + "custom_right_text": { + "name": "Right custom text", + "description": "Custom text to display on the right." + } + } + }, + "action": { + "name": "Action", + "description": "Triggers a motionEye action.", + "fields": { + "action": { + "name": "Action", + "description": "Action to trigger." + } + } + }, + "snapshot": { + "name": "Snapshot", + "description": "Triggers a motionEye still snapshot." + } } } From 90d839724c5fd27855215ffdb2fac502370a9f1c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 00:33:37 +0200 Subject: [PATCH 0391/1009] Migrate integration services (N-P) to support translations (#96376) --- homeassistant/components/neato/services.yaml | 10 -- homeassistant/components/neato/strings.json | 24 +++ .../components/ness_alarm/services.yaml | 10 -- .../components/ness_alarm/strings.json | 28 +++ homeassistant/components/nest/services.yaml | 22 --- homeassistant/components/nest/strings.json | 52 ++++++ .../components/netatmo/services.yaml | 25 --- homeassistant/components/netatmo/strings.json | 50 ++++++ .../components/netgear_lte/services.yaml | 21 --- .../components/netgear_lte/strings.json | 57 ++++++ homeassistant/components/nexia/services.yaml | 14 -- homeassistant/components/nexia/strings.json | 36 ++++ .../components/nissan_leaf/services.yaml | 13 -- .../components/nissan_leaf/strings.json | 24 +++ homeassistant/components/nuki/services.yaml | 8 - homeassistant/components/nuki/strings.json | 22 +++ homeassistant/components/nx584/services.yaml | 8 - homeassistant/components/nx584/strings.json | 24 +++ homeassistant/components/nzbget/services.yaml | 10 -- homeassistant/components/nzbget/strings.json | 20 +++ homeassistant/components/ombi/services.yaml | 14 -- homeassistant/components/ombi/strings.json | 38 ++++ .../components/omnilogic/services.yaml | 4 - .../components/omnilogic/strings.json | 12 ++ homeassistant/components/onvif/services.yaml | 18 -- homeassistant/components/onvif/strings.json | 40 +++++ .../components/openhome/services.yaml | 4 - .../components/openhome/strings.json | 14 ++ .../components/opentherm_gw/services.yaml | 105 ----------- .../components/opentherm_gw/strings.json | 164 ++++++++++++++++++ .../components/pi_hole/services.yaml | 4 - homeassistant/components/pi_hole/strings.json | 64 +++++-- homeassistant/components/picnic/services.yaml | 13 -- homeassistant/components/picnic/strings.json | 24 +++ .../components/pilight/services.yaml | 4 - homeassistant/components/pilight/strings.json | 14 ++ homeassistant/components/ping/services.yaml | 2 - homeassistant/components/ping/strings.json | 8 + homeassistant/components/plex/services.yaml | 8 - homeassistant/components/plex/strings.json | 20 +++ .../components/profiler/services.yaml | 32 ---- .../components/profiler/strings.json | 76 ++++++++ .../components/prosegur/services.yaml | 2 - .../components/prosegur/strings.json | 6 + homeassistant/components/ps4/services.yaml | 6 - homeassistant/components/ps4/strings.json | 16 ++ .../components/python_script/services.yaml | 2 - .../components/python_script/strings.json | 8 + 48 files changed, 828 insertions(+), 372 deletions(-) create mode 100644 homeassistant/components/ness_alarm/strings.json create mode 100644 homeassistant/components/netgear_lte/strings.json create mode 100644 homeassistant/components/nissan_leaf/strings.json create mode 100644 homeassistant/components/nx584/strings.json create mode 100644 homeassistant/components/ombi/strings.json create mode 100644 homeassistant/components/openhome/strings.json create mode 100644 homeassistant/components/pilight/strings.json create mode 100644 homeassistant/components/ping/strings.json create mode 100644 homeassistant/components/python_script/strings.json diff --git a/homeassistant/components/neato/services.yaml b/homeassistant/components/neato/services.yaml index cbfff7808eef0e..5ec782d7bf3d66 100644 --- a/homeassistant/components/neato/services.yaml +++ b/homeassistant/components/neato/services.yaml @@ -1,14 +1,10 @@ custom_cleaning: - name: Zone Cleaning service - description: Zone Cleaning service call specific to Neato Botvacs. target: entity: integration: neato domain: vacuum fields: mode: - name: Set cleaning mode - description: "Set the cleaning mode: 1 for eco and 2 for turbo. Defaults to turbo if not set." default: 2 selector: number: @@ -16,8 +12,6 @@ custom_cleaning: max: 2 mode: box navigation: - name: Set navigation mode - description: "Set the navigation mode: 1 for normal, 2 for extra care, 3 for deep. Defaults to normal if not set." default: 1 selector: number: @@ -25,8 +19,6 @@ custom_cleaning: max: 3 mode: box category: - name: Use cleaning map - description: "Whether to use a persistent map or not for cleaning (i.e. No go lines): 2 for no map, 4 for map. Default to using map if not set (and fallback to no map if no map is found)." default: 4 selector: number: @@ -35,8 +27,6 @@ custom_cleaning: step: 2 mode: box zone: - name: Name of the zone to clean (Only Botvac D7) - description: Only supported on the Botvac D7. Name of the zone to clean. Defaults to no zone i.e. complete house cleanup. example: "Kitchen" selector: text: diff --git a/homeassistant/components/neato/strings.json b/homeassistant/components/neato/strings.json index 20848ccff08a3c..6136ac94e99e50 100644 --- a/homeassistant/components/neato/strings.json +++ b/homeassistant/components/neato/strings.json @@ -18,5 +18,29 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "services": { + "custom_cleaning": { + "name": "Zone cleaning service", + "description": "Zone cleaning service call specific to Neato Botvacs.", + "fields": { + "mode": { + "name": "Set cleaning mode", + "description": "Set the cleaning mode: 1 for eco and 2 for turbo. Defaults to turbo if not set." + }, + "navigation": { + "name": "Set navigation mode", + "description": "Set the navigation mode: 1 for normal, 2 for extra care, 3 for deep. Defaults to normal if not set." + }, + "category": { + "name": "Use cleaning map", + "description": "Whether to use a persistent map or not for cleaning (i.e. No go lines): 2 for no map, 4 for map. Default to using map if not set (and fallback to no map if no map is found)." + }, + "zone": { + "name": "Name of the zone to clean (Only Botvac D7)", + "description": "Only supported on the Botvac D7. Name of the zone to clean. Defaults to no zone i.e. complete house cleanup." + } + } + } } } diff --git a/homeassistant/components/ness_alarm/services.yaml b/homeassistant/components/ness_alarm/services.yaml index ad320285d5b468..b02d5e368052a6 100644 --- a/homeassistant/components/ness_alarm/services.yaml +++ b/homeassistant/components/ness_alarm/services.yaml @@ -1,31 +1,21 @@ # Describes the format for available ness alarm services aux: - name: Aux - description: Trigger an aux output. fields: output_id: - name: Output ID - description: The aux output you wish to change. required: true selector: number: min: 1 max: 4 state: - name: State - description: The On/Off State. If P14xE 8E is enabled then a value of true will pulse output x for the time specified in P14(x+4)E. default: true selector: boolean: panic: - name: Panic - description: Trigger a panic fields: code: - name: Code - description: The user code to use to trigger the panic. required: true example: 1234 selector: diff --git a/homeassistant/components/ness_alarm/strings.json b/homeassistant/components/ness_alarm/strings.json new file mode 100644 index 00000000000000..ec4e39a612816d --- /dev/null +++ b/homeassistant/components/ness_alarm/strings.json @@ -0,0 +1,28 @@ +{ + "services": { + "aux": { + "name": "Aux", + "description": "Trigger an aux output.", + "fields": { + "output_id": { + "name": "Output ID", + "description": "The aux output you wish to change." + }, + "state": { + "name": "State", + "description": "The On/Off State. If P14xE 8E is enabled then a value of true will pulse output x for the time specified in P14(x+4)E." + } + } + }, + "panic": { + "name": "Panic", + "description": "Triggers a panic.", + "fields": { + "code": { + "name": "Code", + "description": "The user code to use to trigger the panic." + } + } + } + } +} diff --git a/homeassistant/components/nest/services.yaml b/homeassistant/components/nest/services.yaml index 24b7290668fb03..5f68bd6a1f2db6 100644 --- a/homeassistant/components/nest/services.yaml +++ b/homeassistant/components/nest/services.yaml @@ -1,12 +1,8 @@ # Describes the format for available Nest services set_away_mode: - name: Set away mode - description: Set the away mode for a Nest structure. fields: away_mode: - name: Away mode - description: New mode to set. required: true selector: select: @@ -14,55 +10,37 @@ set_away_mode: - "away" - "home" structure: - name: Structure - description: Name(s) of structure(s) to change. Defaults to all structures if not specified. example: "Apartment" selector: object: set_eta: - name: Set estimated time of arrival - description: Set or update the estimated time of arrival window for a Nest structure. fields: eta: - name: ETA - description: Estimated time of arrival from now. required: true selector: time: eta_window: - name: ETA window - description: Estimated time of arrival window. default: "00:01" selector: time: trip_id: - name: Trip ID - description: Unique ID for the trip. Default is auto-generated using a timestamp. example: "Leave Work" selector: text: structure: - name: Structure - description: Name(s) of structure(s) to change. Defaults to all structures if not specified. example: "Apartment" selector: object: cancel_eta: - name: Cancel ETA - description: Cancel an existing estimated time of arrival window for a Nest structure. fields: trip_id: - name: Trip ID - description: Unique ID for the trip. required: true example: "Leave Work" selector: text: structure: - name: Structure - description: Name(s) of structure(s) to change. Defaults to all structures if not specified. example: "Apartment" selector: object: diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 86650bbbe9aa51..b6941f51392ff3 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -68,5 +68,57 @@ "title": "Legacy Works With Nest has been removed", "description": "Legacy Works With Nest has been removed from Home Assistant, and the API shuts down as of September 2023.\n\nYou must take action to use the SDM API. Remove all `nest` configuration from `configuration.yaml` and restart Home Assistant, then see the Nest [integration instructions]({documentation_url}) for set up instructions and supported devices." } + }, + "services": { + "set_away_mode": { + "name": "Set away mode", + "description": "Sets the away mode for a Nest structure.", + "fields": { + "away_mode": { + "name": "Away mode", + "description": "New mode to set." + }, + "structure": { + "name": "Structure", + "description": "Name(s) of structure(s) to change. Defaults to all structures if not specified." + } + } + }, + "set_eta": { + "name": "Set estimated time of arrival", + "description": "Sets or update the estimated time of arrival window for a Nest structure.", + "fields": { + "eta": { + "name": "ETA", + "description": "Estimated time of arrival from now." + }, + "eta_window": { + "name": "ETA window", + "description": "Estimated time of arrival window." + }, + "trip_id": { + "name": "Trip ID", + "description": "Unique ID for the trip. Default is auto-generated using a timestamp." + }, + "structure": { + "name": "Structure", + "description": "Name(s) of structure(s) to change. Defaults to all structures if not specified." + } + } + }, + "cancel_eta": { + "name": "Cancel ETA", + "description": "Cancels an existing estimated time of arrival window for a Nest structure.", + "fields": { + "trip_id": { + "name": "Trip ID", + "description": "Unique ID for the trip." + }, + "structure": { + "name": "Structure", + "description": "Name(s) of structure(s) to change. Defaults to all structures if not specified." + } + } + } } } diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index e61e893e19999f..726d6867d2d43b 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -1,15 +1,11 @@ # Describes the format for available Netatmo services set_camera_light: - name: Set camera light mode - description: Sets the light mode for a Netatmo Outdoor camera light. target: entity: integration: netatmo domain: light fields: camera_light_mode: - name: Camera light mode - description: Outdoor camera light mode. required: true selector: select: @@ -19,60 +15,39 @@ set_camera_light: - "auto" set_schedule: - name: Set heating schedule - description: - Set the heating schedule for Netatmo climate device. The schedule name must - match a schedule configured at Netatmo. target: entity: integration: netatmo domain: climate fields: schedule_name: - description: Schedule name example: Standard required: true selector: text: set_persons_home: - name: Set persons at home - description: - Set a list of persons as at home. Person's name must match a name known by - the Netatmo Indoor (Welcome) Camera. target: entity: integration: netatmo domain: camera fields: persons: - description: List of names example: "[Alice, Bob]" required: true selector: object: set_person_away: - name: Set person away - description: - Set a person as away. If no person is set the home will be marked as empty. - Person's name must match a name known by the Netatmo Indoor (Welcome) - Camera. target: entity: integration: netatmo domain: camera fields: person: - description: Person's name. example: Bob selector: text: register_webhook: - name: Register webhook - description: Register the webhook to the Netatmo backend. - unregister_webhook: - name: Unregister webhook - description: Unregister the webhook from the Netatmo backend. diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index 617d813007c5de..05d0e716ef464e 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -66,5 +66,55 @@ "cancel_set_point": "{entity_name} has resumed its schedule", "therm_mode": "{entity_name} switched to \"{subtype}\"" } + }, + "services": { + "set_camera_light": { + "name": "Set camera light mode", + "description": "Sets the light mode for a Netatmo Outdoor camera light.", + "fields": { + "camera_light_mode": { + "name": "Camera light mode", + "description": "Outdoor camera light mode." + } + } + }, + "set_schedule": { + "name": "Set heating schedule", + "description": "Sets the heating schedule for Netatmo climate device. The schedule name must match a schedule configured at Netatmo.", + "fields": { + "schedule_name": { + "name": "Schedule", + "description": "Schedule name." + } + } + }, + "set_persons_home": { + "name": "Set persons at home", + "description": "Sets a list of persons as at home. Person's name must match a name known by the Netatmo Indoor (Welcome) Camera.", + "fields": { + "persons": { + "name": "Persons", + "description": "List of names." + } + } + }, + "set_person_away": { + "name": "Set person away", + "description": "Sets a person as away. If no person is set the home will be marked as empty. Person's name must match a name known by the Netatmo Indoor (Welcome) Camera.", + "fields": { + "person": { + "name": "Person", + "description": "Person's name." + } + } + }, + "register_webhook": { + "name": "Register webhook", + "description": "Registers the webhook to the Netatmo backend." + }, + "unregister_webhook": { + "name": "Unregister webhook", + "description": "Unregisters the webhook from the Netatmo backend." + } } } diff --git a/homeassistant/components/netgear_lte/services.yaml b/homeassistant/components/netgear_lte/services.yaml index bed9647a1b7d2c..05cf8cc3c97089 100644 --- a/homeassistant/components/netgear_lte/services.yaml +++ b/homeassistant/components/netgear_lte/services.yaml @@ -1,34 +1,22 @@ delete_sms: - name: Delete SMS - description: Delete messages from the modem inbox. fields: host: - name: Host - description: The modem that should have a message deleted. example: 192.168.5.1 selector: text: sms_id: - name: SMS ID - description: Integer or list of integers with inbox IDs of messages to delete. required: true example: 7 selector: object: set_option: - name: Set option - description: Set options on the modem. fields: host: - name: Host - description: The modem to set options on. example: 192.168.5.1 selector: text: failover: - name: Failover - description: Failover mode. selector: select: options: @@ -36,8 +24,6 @@ set_option: - "mobile" - "wire" autoconnect: - name: Auto-connect - description: Auto-connect mode. selector: select: options: @@ -46,22 +32,15 @@ set_option: - "never" connect_lte: - name: Connect LTE - description: Ask the modem to establish the LTE connection. fields: host: - name: Host - description: The modem that should connect. example: 192.168.5.1 selector: text: disconnect_lte: - name: Disconnect LTE - description: Ask the modem to close the LTE connection. fields: host: - description: The modem that should disconnect. example: 192.168.5.1 selector: text: diff --git a/homeassistant/components/netgear_lte/strings.json b/homeassistant/components/netgear_lte/strings.json new file mode 100644 index 00000000000000..9c4c67bddf7ba1 --- /dev/null +++ b/homeassistant/components/netgear_lte/strings.json @@ -0,0 +1,57 @@ +{ + "services": { + "delete_sms": { + "name": "Delete SMS", + "description": "Deletes messages from the modem inbox.", + "fields": { + "host": { + "name": "Host", + "description": "The modem that should have a message deleted." + }, + "sms_id": { + "name": "SMS ID", + "description": "Integer or list of integers with inbox IDs of messages to delete." + } + } + }, + "set_option": { + "name": "Set option", + "description": "Sets options on the modem.", + "fields": { + "host": { + "name": "Host", + "description": "The modem to set options on." + }, + "failover": { + "name": "Failover", + "description": "Failover mode." + }, + "autoconnect": { + "name": "Auto-connect", + "description": "Auto-connect mode." + } + } + }, + "connect_lte": { + "name": "Connect LTE", + "description": "Asks the modem to establish the LTE connection.", + "fields": { + "host": { + "name": "Host", + "description": "The modem that should connect." + } + } + }, + "disconnect_lte": { + "name": "Disconnect LTE", + "description": "Asks the modem to close the LTE connection.", + "fields": { + "host": { + "name": "Host", + "description": "The modem that should disconnect." + } + } + } + }, + "selector": {} +} diff --git a/homeassistant/components/nexia/services.yaml b/homeassistant/components/nexia/services.yaml index 0deb5225cd3985..ede1f311acf34a 100644 --- a/homeassistant/components/nexia/services.yaml +++ b/homeassistant/components/nexia/services.yaml @@ -1,14 +1,10 @@ set_aircleaner_mode: - name: Set air cleaner mode - description: "The air cleaner mode." target: entity: integration: nexia domain: climate fields: aircleaner_mode: - name: Air cleaner mode - description: "The air cleaner mode to set." required: true selector: select: @@ -18,16 +14,12 @@ set_aircleaner_mode: - "quick" set_humidify_setpoint: - name: Set humidify set point - description: "The humidification set point." target: entity: integration: nexia domain: climate fields: humidity: - name: Humidify - description: "The humidification setpoint." required: true selector: number: @@ -36,16 +28,12 @@ set_humidify_setpoint: unit_of_measurement: "%" set_hvac_run_mode: - name: Set hvac run mode - description: "The hvac run mode." target: entity: integration: nexia domain: climate fields: run_mode: - name: Run mode - description: "Run the schedule or hold. If not specified, the current run mode will be used." required: false selector: select: @@ -53,8 +41,6 @@ set_hvac_run_mode: - "permanent_hold" - "run_schedule" hvac_mode: - name: Hvac mode - description: "The hvac mode to use for the schedule or hold. If not specified, the current hvac mode will be used." required: false selector: select: diff --git a/homeassistant/components/nexia/strings.json b/homeassistant/components/nexia/strings.json index c9bc84243da819..f3d343ffda32ec 100644 --- a/homeassistant/components/nexia/strings.json +++ b/homeassistant/components/nexia/strings.json @@ -17,5 +17,41 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "services": { + "set_aircleaner_mode": { + "name": "Set air cleaner mode", + "description": "The air cleaner mode.", + "fields": { + "aircleaner_mode": { + "name": "Air cleaner mode", + "description": "The air cleaner mode to set." + } + } + }, + "set_humidify_setpoint": { + "name": "Set humidify set point", + "description": "The humidification set point.", + "fields": { + "humidity": { + "name": "Humidify", + "description": "The humidification setpoint." + } + } + }, + "set_hvac_run_mode": { + "name": "Set hvac run mode", + "description": "The HVAC run mode.", + "fields": { + "run_mode": { + "name": "Run mode", + "description": "Run the schedule or hold. If not specified, the current run mode will be used." + }, + "hvac_mode": { + "name": "HVAC mode", + "description": "The HVAC mode to use for the schedule or hold. If not specified, the current HVAC mode will be used." + } + } + } } } diff --git a/homeassistant/components/nissan_leaf/services.yaml b/homeassistant/components/nissan_leaf/services.yaml index 901e70de414fbb..d49480726679de 100644 --- a/homeassistant/components/nissan_leaf/services.yaml +++ b/homeassistant/components/nissan_leaf/services.yaml @@ -1,29 +1,16 @@ # Describes the format for available services for nissan_leaf start_charge: - name: Start charge - description: > - Start the vehicle charging. It must be plugged in first! fields: vin: - name: VIN - description: > - The vehicle identification number (VIN) of the vehicle, 17 characters required: true example: WBANXXXXXX1234567 selector: text: update: - name: Update - description: > - Fetch the last state of the vehicle of all your accounts, requesting - an update from of the state from the car if possible. fields: vin: - name: VIN - description: > - The vehicle identification number (VIN) of the vehicle, 17 characters required: true example: WBANXXXXXX1234567 selector: diff --git a/homeassistant/components/nissan_leaf/strings.json b/homeassistant/components/nissan_leaf/strings.json new file mode 100644 index 00000000000000..4dae6cb898b784 --- /dev/null +++ b/homeassistant/components/nissan_leaf/strings.json @@ -0,0 +1,24 @@ +{ + "services": { + "start_charge": { + "name": "Start charge", + "description": "Starts the vehicle charging. It must be plugged in first!\n.", + "fields": { + "vin": { + "name": "VIN", + "description": "The vehicle identification number (VIN) of the vehicle, 17 characters\n." + } + } + }, + "update": { + "name": "Update", + "description": "Fetches the last state of the vehicle of all your accounts, requesting an update from of the state from the car if possible.\n.", + "fields": { + "vin": { + "name": "VIN", + "description": "The vehicle identification number (VIN) of the vehicle, 17 characters\n." + } + } + } + } +} diff --git a/homeassistant/components/nuki/services.yaml b/homeassistant/components/nuki/services.yaml index c43f081dbf73c5..2002ab8614a7e6 100644 --- a/homeassistant/components/nuki/services.yaml +++ b/homeassistant/components/nuki/services.yaml @@ -1,28 +1,20 @@ lock_n_go: - name: Lock 'n' go - description: "Nuki Lock 'n' Go" target: entity: integration: nuki domain: lock fields: unlatch: - name: Unlatch - description: Whether to unlatch the lock. default: false selector: boolean: set_continuous_mode: - name: Set Continuous Mode - description: "Enable or disable Continuous Mode on Nuki Opener" target: entity: integration: nuki domain: lock fields: enable: - name: Enable - description: Whether to enable or disable the feature default: false selector: boolean: diff --git a/homeassistant/components/nuki/strings.json b/homeassistant/components/nuki/strings.json index 4629f6a2a3b534..68ab508141b378 100644 --- a/homeassistant/components/nuki/strings.json +++ b/homeassistant/components/nuki/strings.json @@ -45,5 +45,27 @@ } } } + }, + "services": { + "lock_n_go": { + "name": "Lock 'n' go", + "description": "Nuki Lock 'n' Go.", + "fields": { + "unlatch": { + "name": "Unlatch", + "description": "Whether to unlatch the lock." + } + } + }, + "set_continuous_mode": { + "name": "Set continuous code", + "description": "Enables or disables continuous mode on Nuki Opener.", + "fields": { + "enable": { + "name": "Enable", + "description": "Whether to enable or disable the feature." + } + } + } } } diff --git a/homeassistant/components/nx584/services.yaml b/homeassistant/components/nx584/services.yaml index a5c49f6d6a6bdb..da5c0638a4fe1e 100644 --- a/homeassistant/components/nx584/services.yaml +++ b/homeassistant/components/nx584/services.yaml @@ -1,16 +1,12 @@ # Describes the format for available nx584 services bypass_zone: - name: Bypass zone - description: Bypass a zone. target: entity: integration: nx584 domain: alarm_control_panel fields: zone: - name: Zone - description: The number of the zone to be bypassed. required: true selector: number: @@ -18,16 +14,12 @@ bypass_zone: max: 255 unbypass_zone: - name: Un-bypass zone - description: Un-Bypass a zone. target: entity: integration: nx584 domain: alarm_control_panel fields: zone: - name: Zone - description: The number of the zone to be un-bypassed. required: true selector: number: diff --git a/homeassistant/components/nx584/strings.json b/homeassistant/components/nx584/strings.json new file mode 100644 index 00000000000000..11f94e7a72c538 --- /dev/null +++ b/homeassistant/components/nx584/strings.json @@ -0,0 +1,24 @@ +{ + "services": { + "bypass_zone": { + "name": "Bypass zone", + "description": "Bypasses a zone.", + "fields": { + "zone": { + "name": "Zone", + "description": "The number of the zone to be bypassed." + } + } + }, + "unbypass_zone": { + "name": "Un-bypass zone", + "description": "Un-Bypasses a zone.", + "fields": { + "zone": { + "name": "Zone", + "description": "The number of the zone to be un-bypassed." + } + } + } + } +} diff --git a/homeassistant/components/nzbget/services.yaml b/homeassistant/components/nzbget/services.yaml index 46439b761e1c28..0131bb6ae3a336 100644 --- a/homeassistant/components/nzbget/services.yaml +++ b/homeassistant/components/nzbget/services.yaml @@ -1,20 +1,10 @@ # Describes the format for available nzbget services pause: - name: Pause - description: Pause download queue. - resume: - name: Resume - description: Resume download queue. - set_speed: - name: Set speed - description: Set download speed limit fields: speed: - name: Speed - description: Speed limit. 0 is unlimited. default: 1000 selector: number: diff --git a/homeassistant/components/nzbget/strings.json b/homeassistant/components/nzbget/strings.json index fc7d8508a1225f..5a96d2f8951cf6 100644 --- a/homeassistant/components/nzbget/strings.json +++ b/homeassistant/components/nzbget/strings.json @@ -31,5 +31,25 @@ } } } + }, + "services": { + "pause": { + "name": "Pause", + "description": "Pauses download queue." + }, + "resume": { + "name": "Resume", + "description": "Resumes download queue." + }, + "set_speed": { + "name": "Set speed", + "description": "Sets download speed limit.", + "fields": { + "speed": { + "name": "Speed", + "description": "Speed limit. 0 is unlimited." + } + } + } } } diff --git a/homeassistant/components/ombi/services.yaml b/homeassistant/components/ombi/services.yaml index d7e7068e84c1e9..8803d2788bf4e3 100644 --- a/homeassistant/components/ombi/services.yaml +++ b/homeassistant/components/ombi/services.yaml @@ -1,30 +1,20 @@ # Ombi services.yaml entries submit_movie_request: - name: Sumbit movie request - description: Searches for a movie and requests the first result. fields: name: - name: Name - description: Search parameter required: true example: "beverly hills cop" selector: text: submit_tv_request: - name: Submit tv request - description: Searches for a TV show and requests the first result. fields: name: - name: Name - description: Search parameter required: true example: "breaking bad" selector: text: season: - name: Season - description: Which season(s) to request. default: latest selector: select: @@ -34,12 +24,8 @@ submit_tv_request: - "latest" submit_music_request: - name: Submit music request - description: Searches for a music album and requests the first result. fields: name: - name: Name - description: Search parameter required: true example: "nevermind" selector: diff --git a/homeassistant/components/ombi/strings.json b/homeassistant/components/ombi/strings.json new file mode 100644 index 00000000000000..70a3767c889e97 --- /dev/null +++ b/homeassistant/components/ombi/strings.json @@ -0,0 +1,38 @@ +{ + "services": { + "submit_movie_request": { + "name": "Sumbit movie request", + "description": "Searches for a movie and requests the first result.", + "fields": { + "name": { + "name": "Name", + "description": "Search parameter." + } + } + }, + "submit_tv_request": { + "name": "Submit TV request", + "description": "Searches for a TV show and requests the first result.", + "fields": { + "name": { + "name": "Name", + "description": "Search parameter." + }, + "season": { + "name": "Season", + "description": "Which season(s) to request." + } + } + }, + "submit_music_request": { + "name": "Submit music request", + "description": "Searches for a music album and requests the first result.", + "fields": { + "name": { + "name": "Name", + "description": "Search parameter." + } + } + } + } +} diff --git a/homeassistant/components/omnilogic/services.yaml b/homeassistant/components/omnilogic/services.yaml index 94ba0d2982ef7f..c82ea7ebbbf427 100644 --- a/homeassistant/components/omnilogic/services.yaml +++ b/homeassistant/components/omnilogic/services.yaml @@ -1,14 +1,10 @@ set_pump_speed: - name: Set pump speed - description: Set the run speed of a variable speed pump. target: entity: integration: omnilogic domain: switch fields: speed: - name: Speed - description: Speed for the VSP between min and max speed. required: true selector: number: diff --git a/homeassistant/components/omnilogic/strings.json b/homeassistant/components/omnilogic/strings.json index 2bbb927fd27734..454644be244329 100644 --- a/homeassistant/components/omnilogic/strings.json +++ b/homeassistant/components/omnilogic/strings.json @@ -27,5 +27,17 @@ } } } + }, + "services": { + "set_pump_speed": { + "name": "Set pump speed", + "description": "Sets the run speed of a variable speed pump.", + "fields": { + "speed": { + "name": "Speed", + "description": "Speed for the VSP between min and max speed." + } + } + } } } diff --git a/homeassistant/components/onvif/services.yaml b/homeassistant/components/onvif/services.yaml index 9d753b2fe77b4d..9cf3a1fc4c17ae 100644 --- a/homeassistant/components/onvif/services.yaml +++ b/homeassistant/components/onvif/services.yaml @@ -1,38 +1,28 @@ ptz: - name: PTZ - description: If your ONVIF camera supports PTZ, you will be able to pan, tilt or zoom your camera. target: entity: integration: onvif domain: camera fields: tilt: - name: Tilt - description: "Tilt direction." selector: select: options: - "DOWN" - "UP" pan: - name: Pan - description: "Pan direction." selector: select: options: - "LEFT" - "RIGHT" zoom: - name: Zoom - description: "Zoom." selector: select: options: - "ZOOM_IN" - "ZOOM_OUT" distance: - name: Distance - description: "Distance coefficient. Sets how much PTZ should be executed in one request." default: 0.1 selector: number: @@ -40,8 +30,6 @@ ptz: max: 1 step: 0.01 speed: - name: Speed - description: "Speed coefficient. Sets how fast PTZ will be executed." default: 0.5 selector: number: @@ -49,8 +37,6 @@ ptz: max: 1 step: 0.01 continuous_duration: - name: Continuous duration - description: "Set ContinuousMove delay in seconds before stopping the move" default: 0.5 selector: number: @@ -58,15 +44,11 @@ ptz: max: 1 step: 0.01 preset: - name: Preset - description: "PTZ preset profile token. Sets the preset profile token which is executed with GotoPreset" example: "1" default: "0" selector: text: move_mode: - name: Move Mode - description: "PTZ moving mode." default: "RelativeMove" selector: select: diff --git a/homeassistant/components/onvif/strings.json b/homeassistant/components/onvif/strings.json index 8e989f1dfa098b..cabab347264bf9 100644 --- a/homeassistant/components/onvif/strings.json +++ b/homeassistant/components/onvif/strings.json @@ -67,5 +67,45 @@ "title": "ONVIF Device Options" } } + }, + "services": { + "ptz": { + "name": "PTZ", + "description": "If your ONVIF camera supports PTZ, you will be able to pan, tilt or zoom your camera.", + "fields": { + "tilt": { + "name": "Tilt", + "description": "Tilt direction." + }, + "pan": { + "name": "Pan", + "description": "Pan direction." + }, + "zoom": { + "name": "Zoom", + "description": "Zoom." + }, + "distance": { + "name": "Distance", + "description": "Distance coefficient. Sets how much PTZ should be executed in one request." + }, + "speed": { + "name": "Speed", + "description": "Speed coefficient. Sets how fast PTZ will be executed." + }, + "continuous_duration": { + "name": "Continuous duration", + "description": "Set ContinuousMove delay in seconds before stopping the move." + }, + "preset": { + "name": "Preset", + "description": "PTZ preset profile token. Sets the preset profile token which is executed with GotoPreset." + }, + "move_mode": { + "name": "Move Mode", + "description": "PTZ moving mode." + } + } + } } } diff --git a/homeassistant/components/openhome/services.yaml b/homeassistant/components/openhome/services.yaml index 0fa951452872bf..7ccba4fb497876 100644 --- a/homeassistant/components/openhome/services.yaml +++ b/homeassistant/components/openhome/services.yaml @@ -1,16 +1,12 @@ # Describes the format for available openhome services invoke_pin: - name: Invoke PIN - description: Invoke a pin on the specified device. target: entity: integration: openhome domain: media_player fields: pin: - name: PIN - description: Which pin to invoke required: true selector: number: diff --git a/homeassistant/components/openhome/strings.json b/homeassistant/components/openhome/strings.json new file mode 100644 index 00000000000000..b13fb997b7f89c --- /dev/null +++ b/homeassistant/components/openhome/strings.json @@ -0,0 +1,14 @@ +{ + "services": { + "invoke_pin": { + "name": "Invoke PIN", + "description": "Invokes a pin on the specified device.", + "fields": { + "pin": { + "name": "PIN", + "description": "Which pin to invoke." + } + } + } + } +} diff --git a/homeassistant/components/opentherm_gw/services.yaml b/homeassistant/components/opentherm_gw/services.yaml index 77ef501f9d87e6..d68624e07630d7 100644 --- a/homeassistant/components/opentherm_gw/services.yaml +++ b/homeassistant/components/opentherm_gw/services.yaml @@ -1,84 +1,49 @@ # Describes the format for available opentherm_gw services reset_gateway: - name: Reset gateway - description: Reset the OpenTherm Gateway. fields: gateway_id: - name: Gateway ID - description: The gateway_id of the OpenTherm Gateway. required: true example: "opentherm_gateway" selector: text: set_central_heating_ovrd: - name: Set central heating override - description: > - Set the central heating override option on the gateway. - When overriding the control setpoint (via a set_control_setpoint service call with a value other than 0), the gateway automatically enables the central heating override to start heating. - This service can then be used to control the central heating override status. - To return control of the central heating to the thermostat, call the set_control_setpoint service with temperature value 0. - You will only need this if you are writing your own software thermostat. fields: gateway_id: - name: Gateway ID - description: The gateway_id of the OpenTherm Gateway. required: true example: "opentherm_gateway" selector: text: ch_override: - name: Central heating override - description: > - The desired boolean value for the central heating override. required: true selector: boolean: set_clock: - name: Set clock - description: Set the clock and day of the week on the connected thermostat. fields: gateway_id: - name: Gateway ID - description: The gateway_id of the OpenTherm Gateway. required: true example: "opentherm_gateway" selector: text: date: - name: Date - description: Optional date from which the day of the week will be extracted. Defaults to today. example: "2018-10-23" selector: text: time: - name: Time - description: Optional time in 24h format which will be provided to the thermostat. Defaults to the current time. example: "19:34" selector: text: set_control_setpoint: - name: Set control set point - description: > - Set the central heating control setpoint override on the gateway. - You will only need this if you are writing your own software thermostat. fields: gateway_id: - name: Gateway ID - description: The gateway_id of the OpenTherm Gateway. required: true example: "opentherm_gateway" selector: text: temperature: - name: Temperature - description: > - The central heating setpoint to set on the gateway. - Values between 0 and 90 are accepted, but not all boilers support this range. - A value of 0 disables the central heating setpoint override. required: true selector: number: @@ -88,49 +53,26 @@ set_control_setpoint: unit_of_measurement: "°" set_hot_water_ovrd: - name: Set hot water override - description: > - Set the domestic hot water enable option on the gateway. fields: gateway_id: - name: Gateway ID - description: The gateway_id of the OpenTherm Gateway. required: true example: "opentherm_gateway" selector: text: dhw_override: - name: Domestic hot water override - description: > - Control the domestic hot water enable option. If the boiler has - been configured to let the room unit control when to keep a - small amount of water preheated, this command can influence - that. - Value should be 0 or 1 to enable the override in off or on - state, or "A" to disable the override. required: true example: "1" selector: text: set_hot_water_setpoint: - name: Set hot water set point - description: > - Set the domestic hot water setpoint on the gateway. fields: gateway_id: - name: Gateway ID - description: The gateway_id of the OpenTherm Gateway. required: true example: "opentherm_gateway" selector: text: temperature: - name: Temperature - description: > - The domestic hot water setpoint to set on the gateway. Not all boilers support this feature. - Values between 0 and 90 are accepted, but not all boilers support this range. - Check the values of the slave_dhw_min_setp and slave_dhw_max_setp sensors to see the supported range on your boiler. selector: number: min: 0 @@ -139,19 +81,13 @@ set_hot_water_setpoint: unit_of_measurement: "°" set_gpio_mode: - name: Set gpio mode - description: Change the function of the GPIO pins of the gateway. fields: gateway_id: - name: Gateway ID - description: The gateway_id of the OpenTherm Gateway. required: true example: "opentherm_gateway" selector: text: id: - name: ID - description: The ID of the GPIO pin. required: true selector: select: @@ -159,10 +95,6 @@ set_gpio_mode: - "A" - "B" mode: - name: Mode - description: > - Mode to set on the GPIO pin. Values 0 through 6 are accepted for both GPIOs, 7 is only accepted for GPIO "B". - See https://www.home-assistant.io/integrations/opentherm_gw/#gpio-modes for an explanation of the values. required: true selector: number: @@ -170,19 +102,13 @@ set_gpio_mode: max: 7 set_led_mode: - name: Set LED mode - description: Change the function of the LEDs of the gateway. fields: gateway_id: - name: Gateway ID - description: The gateway_id of the OpenTherm Gateway. required: true example: "opentherm_gateway" selector: text: id: - name: ID - description: The ID of the LED. required: true selector: select: @@ -194,10 +120,6 @@ set_led_mode: - "E" - "F" mode: - name: Mode - description: > - The function to assign to the LED. - See https://www.home-assistant.io/integrations/opentherm_gw/#led-modes for an explanation of the values. required: true selector: select: @@ -216,23 +138,13 @@ set_led_mode: - "X" set_max_modulation: - name: Set max modulation - description: > - Override the maximum relative modulation level. - You will only need this if you are writing your own software thermostat. fields: gateway_id: - name: Gateway ID - description: The gateway_id of the OpenTherm Gateway. required: true example: "opentherm_gateway" selector: text: level: - name: Level - description: > - The modulation level to provide to the gateway. - Provide a value of -1 to clear the override and forward the value from the thermostat again. required: true selector: number: @@ -240,24 +152,13 @@ set_max_modulation: max: 100 set_outside_temperature: - name: Set outside temperature - description: > - Provide an outside temperature to the thermostat. - If your thermostat is unable to display an outside temperature and does not support OTC (Outside Temperature Correction), this has no effect. fields: gateway_id: - name: Gateway ID - description: The gateway_id of the OpenTherm Gateway. required: true example: "opentherm_gateway" selector: text: temperature: - name: Temperature - description: > - The temperature to provide to the thermostat. - Values between -40.0 and 64.0 will be accepted, but not all thermostats can display the full range. - Any value above 64.0 will clear a previously configured value (suggestion: 99) required: true selector: number: @@ -266,19 +167,13 @@ set_outside_temperature: unit_of_measurement: "°" set_setback_temperature: - name: Set setback temperature - description: Configure the setback temperature to be used with the GPIO away mode function. fields: gateway_id: - name: Gateway ID - description: The gateway_id of the OpenTherm Gateway. required: true example: "opentherm_gateway" selector: text: temperature: - name: Temperature - description: The setback temperature to configure on the gateway. required: true selector: number: diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json index a80a059481dcfe..d23fe1c09246d4 100644 --- a/homeassistant/components/opentherm_gw/strings.json +++ b/homeassistant/components/opentherm_gw/strings.json @@ -27,5 +27,169 @@ } } } + }, + "services": { + "reset_gateway": { + "name": "Reset gateway", + "description": "Resets the OpenTherm Gateway.", + "fields": { + "gateway_id": { + "name": "Gateway ID", + "description": "The gateway_id of the OpenTherm Gateway." + } + } + }, + "set_central_heating_ovrd": { + "name": "Set central heating override", + "description": "Sets the central heating override option on the gateway. When overriding the control setpoint (via a set_control_setpoint service call with a value other than 0), the gateway automatically enables the central heating override to start heating. This service can then be used to control the central heating override status. To return control of the central heating to the thermostat, call the set_control_setpoint service with temperature value 0. You will only need this if you are writing your own software thermostat.\n.", + "fields": { + "gateway_id": { + "name": "Gateway ID", + "description": "The gateway_id of the OpenTherm Gateway." + }, + "ch_override": { + "name": "Central heating override", + "description": "The desired boolean value for the central heating override." + } + } + }, + "set_clock": { + "name": "Set clock", + "description": "Sets the clock and day of the week on the connected thermostat.", + "fields": { + "gateway_id": { + "name": "Gateway ID", + "description": "The gateway_id of the OpenTherm Gateway." + }, + "date": { + "name": "Date", + "description": "Optional date from which the day of the week will be extracted. Defaults to today." + }, + "time": { + "name": "Time", + "description": "Optional time in 24h format which will be provided to the thermostat. Defaults to the current time." + } + } + }, + "set_control_setpoint": { + "name": "Set control set point", + "description": "Sets the central heating control setpoint override on the gateway. You will only need this if you are writing your own software thermostat.\n.", + "fields": { + "gateway_id": { + "name": "Gateway ID", + "description": "The gateway_id of the OpenTherm Gateway." + }, + "temperature": { + "name": "Temperature", + "description": "The central heating setpoint to set on the gateway. Values between 0 and 90 are accepted, but not all boilers support this range. A value of 0 disables the central heating setpoint override.\n." + } + } + }, + "set_hot_water_ovrd": { + "name": "Set hot water override", + "description": "Sets the domestic hot water enable option on the gateway.", + "fields": { + "gateway_id": { + "name": "Gateway ID", + "description": "The gateway_id of the OpenTherm Gateway." + }, + "dhw_override": { + "name": "Domestic hot water override", + "description": "Control the domestic hot water enable option. If the boiler has been configured to let the room unit control when to keep a small amount of water preheated, this command can influence that. Value should be 0 or 1 to enable the override in off or on state, or \"A\" to disable the override.\n." + } + } + }, + "set_hot_water_setpoint": { + "name": "Set hot water set point", + "description": "Sets the domestic hot water setpoint on the gateway.", + "fields": { + "gateway_id": { + "name": "Gateway ID", + "description": "The gateway_id of the OpenTherm Gateway." + }, + "temperature": { + "name": "Temperature", + "description": "The domestic hot water setpoint to set on the gateway. Not all boilers support this feature. Values between 0 and 90 are accepted, but not all boilers support this range. Check the values of the slave_dhw_min_setp and slave_dhw_max_setp sensors to see the supported range on your boiler.\n." + } + } + }, + "set_gpio_mode": { + "name": "Set gpio mode", + "description": "Changes the function of the GPIO pins of the gateway.", + "fields": { + "gateway_id": { + "name": "Gateway ID", + "description": "The gateway_id of the OpenTherm Gateway." + }, + "id": { + "name": "ID", + "description": "The ID of the GPIO pin." + }, + "mode": { + "name": "Mode", + "description": "Mode to set on the GPIO pin. Values 0 through 6 are accepted for both GPIOs, 7 is only accepted for GPIO \"B\". See https://www.home-assistant.io/integrations/opentherm_gw/#gpio-modes for an explanation of the values.\n." + } + } + }, + "set_led_mode": { + "name": "Set LED mode", + "description": "Changes the function of the LEDs of the gateway.", + "fields": { + "gateway_id": { + "name": "Gateway ID", + "description": "The gateway_id of the OpenTherm Gateway." + }, + "id": { + "name": "ID", + "description": "The ID of the LED." + }, + "mode": { + "name": "Mode", + "description": "The function to assign to the LED. See https://www.home-assistant.io/integrations/opentherm_gw/#led-modes for an explanation of the values.\n." + } + } + }, + "set_max_modulation": { + "name": "Set max modulation", + "description": "Overrides the maximum relative modulation level. You will only need this if you are writing your own software thermostat.\n.", + "fields": { + "gateway_id": { + "name": "Gateway ID", + "description": "The gateway_id of the OpenTherm Gateway." + }, + "level": { + "name": "Level", + "description": "The modulation level to provide to the gateway. Provide a value of -1 to clear the override and forward the value from the thermostat again.\n." + } + } + }, + "set_outside_temperature": { + "name": "Set outside temperature", + "description": "Provides an outside temperature to the thermostat. If your thermostat is unable to display an outside temperature and does not support OTC (Outside Temperature Correction), this has no effect.\n.", + "fields": { + "gateway_id": { + "name": "Gateway ID", + "description": "The gateway_id of the OpenTherm Gateway." + }, + "temperature": { + "name": "Temperature", + "description": "The temperature to provide to the thermostat. Values between -40.0 and 64.0 will be accepted, but not all thermostats can display the full range. Any value above 64.0 will clear a previously configured value (suggestion: 99)\n." + } + } + }, + "set_setback_temperature": { + "name": "Set setback temperature", + "description": "Configures the setback temperature to be used with the GPIO away mode function.", + "fields": { + "gateway_id": { + "name": "Gateway ID", + "description": "The gateway_id of the OpenTherm Gateway." + }, + "temperature": { + "name": "Temperature", + "description": "The setback temperature to configure on the gateway." + } + } + } } } diff --git a/homeassistant/components/pi_hole/services.yaml b/homeassistant/components/pi_hole/services.yaml index 1b5da9f0d4fe73..9c8d8921b128eb 100644 --- a/homeassistant/components/pi_hole/services.yaml +++ b/homeassistant/components/pi_hole/services.yaml @@ -1,14 +1,10 @@ disable: - name: Disable - description: Disable configured Pi-hole(s) for an amount of time target: entity: integration: pi_hole domain: switch fields: duration: - name: Duration - description: Time that the Pi-hole should be disabled for required: true example: "00:00:15" selector: diff --git a/homeassistant/components/pi_hole/strings.json b/homeassistant/components/pi_hole/strings.json index eb12811722bc98..1ed271931c36bf 100644 --- a/homeassistant/components/pi_hole/strings.json +++ b/homeassistant/components/pi_hole/strings.json @@ -35,23 +35,61 @@ }, "entity": { "binary_sensor": { - "status": { "name": "Status" } + "status": { + "name": "Status" + } }, "sensor": { - "ads_blocked_today": { "name": "Ads blocked today" }, - "ads_percentage_today": { "name": "Ads percentage blocked today" }, - "clients_ever_seen": { "name": "Seen clients" }, - "dns_queries_today": { "name": "DNS queries today" }, - "domains_being_blocked": { "name": "Domains blocked" }, - "queries_cached": { "name": "DNS queries cached" }, - "queries_forwarded": { "name": "DNS queries forwarded" }, - "unique_clients": { "name": "DNS unique clients" }, - "unique_domains": { "name": "DNS unique domains" } + "ads_blocked_today": { + "name": "Ads blocked today" + }, + "ads_percentage_today": { + "name": "Ads percentage blocked today" + }, + "clients_ever_seen": { + "name": "Seen clients" + }, + "dns_queries_today": { + "name": "DNS queries today" + }, + "domains_being_blocked": { + "name": "Domains blocked" + }, + "queries_cached": { + "name": "DNS queries cached" + }, + "queries_forwarded": { + "name": "DNS queries forwarded" + }, + "unique_clients": { + "name": "DNS unique clients" + }, + "unique_domains": { + "name": "DNS unique domains" + } }, "update": { - "core_update_available": { "name": "Core update available" }, - "ftl_update_available": { "name": "FTL update available" }, - "web_update_available": { "name": "Web update available" } + "core_update_available": { + "name": "Core update available" + }, + "ftl_update_available": { + "name": "FTL update available" + }, + "web_update_available": { + "name": "Web update available" + } + } + }, + "services": { + "disable": { + "name": "Disable", + "description": "Disables configured Pi-hole(s) for an amount of time.", + "fields": { + "duration": { + "name": "Duration", + "description": "Time that the Pi-hole should be disabled for." + } + } } } } diff --git a/homeassistant/components/picnic/services.yaml b/homeassistant/components/picnic/services.yaml index 9af2cb4829118e..e7afe71bb31fd1 100644 --- a/homeassistant/components/picnic/services.yaml +++ b/homeassistant/components/picnic/services.yaml @@ -1,34 +1,21 @@ add_product: - name: Add a product to the cart - description: >- - Adds a product to the cart based on a search string or product ID. - The search string and product ID are exclusive. - fields: config_entry_id: - name: Picnic service - description: The product will be added to the selected service. required: true selector: config_entry: integration: picnic product_id: - name: Product ID - description: The product ID of a Picnic product. required: false example: "10510201" selector: text: product_name: - name: Product name - description: Search for a product and add the first result required: false example: "Yoghurt" selector: text: amount: - name: Amount - description: Amount to add of the selected product required: false default: 1 selector: diff --git a/homeassistant/components/picnic/strings.json b/homeassistant/components/picnic/strings.json index f0e0d93231c1de..0fd107609d1287 100644 --- a/homeassistant/components/picnic/strings.json +++ b/homeassistant/components/picnic/strings.json @@ -71,5 +71,29 @@ "name": "End of next delivery's slot" } } + }, + "services": { + "add_product": { + "name": "Add a product to the cart", + "description": "Adds a product to the cart based on a search string or product ID. The search string and product ID are exclusive.", + "fields": { + "config_entry_id": { + "name": "Picnic service", + "description": "The product will be added to the selected service." + }, + "product_id": { + "name": "Product ID", + "description": "The product ID of a Picnic product." + }, + "product_name": { + "name": "Product name", + "description": "Search for a product and add the first result." + }, + "amount": { + "name": "Amount", + "description": "Amount to add of the selected product." + } + } + } } } diff --git a/homeassistant/components/pilight/services.yaml b/homeassistant/components/pilight/services.yaml index 6dc052043bf97c..b877ae88b0a994 100644 --- a/homeassistant/components/pilight/services.yaml +++ b/homeassistant/components/pilight/services.yaml @@ -1,10 +1,6 @@ send: - name: Send - description: Send RF code to Pilight device fields: protocol: - name: Protocol - description: "Protocol that Pilight recognizes. See https://manual.pilight.org/protocols/index.html for supported protocols and additional parameters that each protocol supports" required: true example: "lirc" selector: diff --git a/homeassistant/components/pilight/strings.json b/homeassistant/components/pilight/strings.json new file mode 100644 index 00000000000000..4cd819859a3ffa --- /dev/null +++ b/homeassistant/components/pilight/strings.json @@ -0,0 +1,14 @@ +{ + "services": { + "send": { + "name": "Send", + "description": "Sends RF code to Pilight device.", + "fields": { + "protocol": { + "name": "Protocol", + "description": "Protocol that Pilight recognizes. See https://manual.pilight.org/protocols/index.html for supported protocols and additional parameters that each protocol supports." + } + } + } + } +} diff --git a/homeassistant/components/ping/services.yaml b/homeassistant/components/ping/services.yaml index 1f7e523e6851d6..c983a105c93977 100644 --- a/homeassistant/components/ping/services.yaml +++ b/homeassistant/components/ping/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload all ping entities. diff --git a/homeassistant/components/ping/strings.json b/homeassistant/components/ping/strings.json new file mode 100644 index 00000000000000..2bd9229b607973 --- /dev/null +++ b/homeassistant/components/ping/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "reload": { + "name": "Reload", + "description": "Reloads ping sensors from the YAML-configuration." + } + } +} diff --git a/homeassistant/components/plex/services.yaml b/homeassistant/components/plex/services.yaml index 782a4d17c189f7..5ed655b7d78ad7 100644 --- a/homeassistant/components/plex/services.yaml +++ b/homeassistant/components/plex/services.yaml @@ -1,21 +1,13 @@ refresh_library: - name: Refresh library - description: Refresh a Plex library to scan for new and updated media. fields: server_name: - name: Server name - description: Name of a Plex server if multiple Plex servers configured. example: "My Plex Server" selector: text: library_name: - name: Library name - description: Name of the Plex library to refresh. required: true example: "TV Shows" selector: text: scan_for_clients: - name: Scan for clients - description: Scan for available clients from the Plex server(s), local network, and plex.tv. diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index f08b6f598623dc..9cba83653fd1ad 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -56,5 +56,25 @@ } } } + }, + "services": { + "refresh_library": { + "name": "Refresh library", + "description": "Refreshes a Plex library to scan for new and updated media.", + "fields": { + "server_name": { + "name": "Server name", + "description": "Name of a Plex server if multiple Plex servers configured." + }, + "library_name": { + "name": "Library name", + "description": "Name of the Plex library to refresh." + } + } + }, + "scan_for_clients": { + "name": "Scan for clients", + "description": "Scans for available clients from the Plex server(s), local network, and plex.tv." + } } } diff --git a/homeassistant/components/profiler/services.yaml b/homeassistant/components/profiler/services.yaml index 3bd6d7636ac6f1..311325fa404a1d 100644 --- a/homeassistant/components/profiler/services.yaml +++ b/homeassistant/components/profiler/services.yaml @@ -1,10 +1,6 @@ start: - name: Start - description: Start the Profiler fields: seconds: - name: Seconds - description: The number of seconds to run the profiler. default: 60.0 selector: number: @@ -12,12 +8,8 @@ start: max: 3600 unit_of_measurement: seconds memory: - name: Memory - description: Start the Memory Profiler fields: seconds: - name: Seconds - description: The number of seconds to run the memory profiler. default: 60.0 selector: number: @@ -25,12 +17,8 @@ memory: max: 3600 unit_of_measurement: seconds start_log_objects: - name: Start logging objects - description: Start logging growth of objects in memory fields: scan_interval: - name: Scan interval - description: The number of seconds between logging objects. default: 30.0 selector: number: @@ -38,26 +26,16 @@ start_log_objects: max: 3600 unit_of_measurement: seconds stop_log_objects: - name: Stop logging objects - description: Stop logging growth of objects in memory. dump_log_objects: - name: Dump log objects - description: Dump the repr of all matching objects to the log. fields: type: - name: Type - description: The type of objects to dump to the log. required: true example: State selector: text: start_log_object_sources: - name: Start logging object sources - description: Start logging sources of new objects in memory fields: scan_interval: - name: Scan interval - description: The number of seconds between logging objects. default: 30.0 selector: number: @@ -65,8 +43,6 @@ start_log_object_sources: max: 3600 unit_of_measurement: seconds max_objects: - name: Maximum objects - description: The maximum number of objects to log. default: 5 selector: number: @@ -74,14 +50,6 @@ start_log_object_sources: max: 30 unit_of_measurement: objects stop_log_object_sources: - name: Stop logging object sources - description: Stop logging sources of new objects in memory. lru_stats: - name: Log LRU stats - description: Log the stats of all lru caches. log_thread_frames: - name: Log thread frames - description: Log the current frames for all threads. log_event_loop_scheduled: - name: Log event loop scheduled - description: Log what is scheduled in the event loop. diff --git a/homeassistant/components/profiler/strings.json b/homeassistant/components/profiler/strings.json index 394c46563cd442..ee6f215e59bced 100644 --- a/homeassistant/components/profiler/strings.json +++ b/homeassistant/components/profiler/strings.json @@ -8,5 +8,81 @@ "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } + }, + "services": { + "start": { + "name": "Start", + "description": "Starts the Profiler.", + "fields": { + "seconds": { + "name": "Seconds", + "description": "The number of seconds to run the profiler." + } + } + }, + "memory": { + "name": "Memory", + "description": "Starts the Memory Profiler.", + "fields": { + "seconds": { + "name": "Seconds", + "description": "The number of seconds to run the memory profiler." + } + } + }, + "start_log_objects": { + "name": "Start logging objects", + "description": "Starts logging growth of objects in memory.", + "fields": { + "scan_interval": { + "name": "Scan interval", + "description": "The number of seconds between logging objects." + } + } + }, + "stop_log_objects": { + "name": "Stop logging objects", + "description": "Stops logging growth of objects in memory." + }, + "dump_log_objects": { + "name": "Dump log objects", + "description": "Dumps the repr of all matching objects to the log.", + "fields": { + "type": { + "name": "Type", + "description": "The type of objects to dump to the log." + } + } + }, + "start_log_object_sources": { + "name": "Start logging object sources", + "description": "Starts logging sources of new objects in memory.", + "fields": { + "scan_interval": { + "name": "Scan interval", + "description": "The number of seconds between logging objects." + }, + "max_objects": { + "name": "Maximum objects", + "description": "The maximum number of objects to log." + } + } + }, + "stop_log_object_sources": { + "name": "Stop logging object sources", + "description": "Stops logging sources of new objects in memory." + }, + "lru_stats": { + "name": "Log LRU stats", + "description": "Logs the stats of all lru caches." + }, + "log_thread_frames": { + "name": "Log thread frames", + "description": "Logs the current frames for all threads." + }, + "log_event_loop_scheduled": { + "name": "Log event loop scheduled", + "description": "Logs what is scheduled in the event loop." + } } } diff --git a/homeassistant/components/prosegur/services.yaml b/homeassistant/components/prosegur/services.yaml index 0db63cb7adf886..e02eb2e60e5d14 100644 --- a/homeassistant/components/prosegur/services.yaml +++ b/homeassistant/components/prosegur/services.yaml @@ -1,6 +1,4 @@ request_image: - name: Request Camera image - description: Request a new image from a Prosegur Camera target: entity: domain: camera diff --git a/homeassistant/components/prosegur/strings.json b/homeassistant/components/prosegur/strings.json index a6c7fcc4a76f1d..9b9ac45fc85c29 100644 --- a/homeassistant/components/prosegur/strings.json +++ b/homeassistant/components/prosegur/strings.json @@ -30,5 +30,11 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "services": { + "request_image": { + "name": "Request camera image", + "description": "Requests a new image from a Prosegur camera." + } } } diff --git a/homeassistant/components/ps4/services.yaml b/homeassistant/components/ps4/services.yaml index f1f20506edb3f6..0a93f87a2495e9 100644 --- a/homeassistant/components/ps4/services.yaml +++ b/homeassistant/components/ps4/services.yaml @@ -1,18 +1,12 @@ send_command: - name: Send command - description: Emulate button press for PlayStation 4. fields: entity_id: - name: Entity - description: Name of entity to send command. required: true selector: entity: integration: ps4 domain: media_player command: - name: Command - description: Button to press. required: true selector: select: diff --git a/homeassistant/components/ps4/strings.json b/homeassistant/components/ps4/strings.json index 9518af77dbcfd9..644b2d61216100 100644 --- a/homeassistant/components/ps4/strings.json +++ b/homeassistant/components/ps4/strings.json @@ -38,5 +38,21 @@ "port_987_bind_error": "Could not bind to port 987. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.", "port_997_bind_error": "Could not bind to port 997. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info." } + }, + "services": { + "send_command": { + "name": "Send command", + "description": "Emulates button press for PlayStation 4.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name of entity to send command." + }, + "command": { + "name": "Command", + "description": "Button to press." + } + } + } } } diff --git a/homeassistant/components/python_script/services.yaml b/homeassistant/components/python_script/services.yaml index e9f860f1a6289f..613c6cbc9e21f7 100644 --- a/homeassistant/components/python_script/services.yaml +++ b/homeassistant/components/python_script/services.yaml @@ -1,5 +1,3 @@ # Describes the format for available python_script services reload: - name: Reload - description: Reload all available python_scripts diff --git a/homeassistant/components/python_script/strings.json b/homeassistant/components/python_script/strings.json new file mode 100644 index 00000000000000..9898a8ad8665eb --- /dev/null +++ b/homeassistant/components/python_script/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "reload": { + "name": "Reload", + "description": "Reloads all available Python scripts." + } + } +} From ea28bd3c9c144b70375f747f918aceca35b48f69 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 00:34:45 +0200 Subject: [PATCH 0392/1009] Update pre-commit to 3.3.3 (#96359) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 11920917a5913f..2834ea5967271e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,7 +12,7 @@ coverage==7.2.4 freezegun==1.2.2 mock-open==1.4.0 mypy==1.4.1 -pre-commit==3.1.0 +pre-commit==3.3.3 pydantic==1.10.11 pylint==2.17.4 pylint-per-file-ignores==1.2.1 From c6b36b6db44e175f1aa2fc71c6c6acd09f21f411 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 01:08:31 +0200 Subject: [PATCH 0393/1009] 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 63aa2f2f9162bb..ea153be11cf1f9 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 27884a263f06ca..0cb289d748fb75 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -121,7 +121,7 @@ PyXiaomiGateway==0.14.3 RachioPy==1.0.3 # homeassistant.components.python_script -RestrictedPython==6.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 5210e0e335b340..04065df19c1307 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 62fe4957c96447a1c0b86a530ae846fca2837d97 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 01:18:22 +0200 Subject: [PATCH 0394/1009] Migrate integration services (Q-S) to support translations (#96378) --- .../components/qvr_pro/services.yaml | 8 - homeassistant/components/qvr_pro/strings.json | 24 +++ homeassistant/components/rachio/services.yaml | 22 --- homeassistant/components/rachio/strings.json | 56 ++++++ .../components/rainbird/services.yaml | 10 -- .../components/rainbird/strings.json | 26 +++ .../components/rainmachine/services.yaml | 86 --------- .../components/rainmachine/strings.json | 166 ++++++++++++++++++ .../remember_the_milk/services.yaml | 13 -- .../components/remember_the_milk/strings.json | 28 +++ .../components/renault/services.yaml | 15 -- homeassistant/components/renault/strings.json | 44 +++++ homeassistant/components/rest/services.yaml | 2 - homeassistant/components/rest/strings.json | 8 + homeassistant/components/rflink/services.yaml | 6 - homeassistant/components/rflink/strings.json | 18 ++ homeassistant/components/rfxtrx/services.yaml | 4 - homeassistant/components/rfxtrx/strings.json | 12 ++ homeassistant/components/ring/services.yaml | 2 - homeassistant/components/ring/strings.json | 6 + homeassistant/components/roku/services.yaml | 4 - homeassistant/components/roku/strings.json | 12 ++ homeassistant/components/roon/services.yaml | 4 - homeassistant/components/roon/strings.json | 12 ++ .../components/route53/services.yaml | 2 - homeassistant/components/route53/strings.json | 8 + .../components/sabnzbd/services.yaml | 14 -- homeassistant/components/sabnzbd/strings.json | 36 ++++ .../components/screenlogic/services.yaml | 4 - .../components/screenlogic/strings.json | 12 ++ .../components/sensibo/services.yaml | 46 ----- homeassistant/components/sensibo/strings.json | 104 +++++++++++ .../components/shopping_list/services.yaml | 28 --- .../components/shopping_list/strings.json | 64 +++++++ .../components/simplisafe/services.yaml | 36 ---- .../components/simplisafe/strings.json | 80 +++++++++ .../components/smarttub/services.yaml | 16 -- .../components/smarttub/strings.json | 46 +++++ homeassistant/components/smtp/services.yaml | 2 - homeassistant/components/smtp/strings.json | 8 + .../components/snapcast/services.yaml | 16 -- .../components/snapcast/strings.json | 38 ++++ homeassistant/components/snips/services.yaml | 28 --- homeassistant/components/snips/strings.json | 68 +++++++ homeassistant/components/snooz/services.yaml | 10 -- homeassistant/components/snooz/strings.json | 26 +++ .../components/songpal/services.yaml | 6 - homeassistant/components/songpal/strings.json | 16 ++ homeassistant/components/sonos/services.yaml | 38 ---- homeassistant/components/sonos/strings.json | 90 ++++++++++ .../components/soundtouch/services.yaml | 22 --- .../components/soundtouch/strings.json | 54 ++++++ .../components/squeezebox/services.yaml | 22 --- .../components/squeezebox/strings.json | 44 +++++ .../components/starline/services.yaml | 13 -- .../components/starline/strings.json | 26 +++ .../components/streamlabswater/services.yaml | 4 - .../components/streamlabswater/strings.json | 14 ++ homeassistant/components/subaru/services.yaml | 4 - homeassistant/components/subaru/strings.json | 13 +- .../components/surepetcare/services.yaml | 10 -- .../components/surepetcare/strings.json | 30 ++++ .../components/switcher_kis/services.yaml | 8 - .../components/switcher_kis/strings.json | 22 +++ .../components/synology_dsm/services.yaml | 8 - .../components/synology_dsm/strings.json | 142 +++++++++++---- .../components/system_bridge/services.yaml | 24 --- .../components/system_bridge/strings.json | 58 ++++++ 68 files changed, 1380 insertions(+), 568 deletions(-) create mode 100644 homeassistant/components/qvr_pro/strings.json create mode 100644 homeassistant/components/remember_the_milk/strings.json create mode 100644 homeassistant/components/rest/strings.json create mode 100644 homeassistant/components/rflink/strings.json create mode 100644 homeassistant/components/route53/strings.json create mode 100644 homeassistant/components/smtp/strings.json create mode 100644 homeassistant/components/snips/strings.json create mode 100644 homeassistant/components/streamlabswater/strings.json diff --git a/homeassistant/components/qvr_pro/services.yaml b/homeassistant/components/qvr_pro/services.yaml index edb879c784a2bb..0dad311f899550 100644 --- a/homeassistant/components/qvr_pro/services.yaml +++ b/homeassistant/components/qvr_pro/services.yaml @@ -1,22 +1,14 @@ start_record: - name: Start record - description: Start QVR Pro recording on specified channel. fields: guid: - name: GUID - description: GUID of the channel to start recording. required: true example: "245EBE933C0A597EBE865C0A245E0002" selector: text: stop_record: - name: Stop record - description: Stop QVR Pro recording on specified channel. fields: guid: - name: GUID - description: GUID of the channel to stop recording. required: true example: "245EBE933C0A597EBE865C0A245E0002" selector: diff --git a/homeassistant/components/qvr_pro/strings.json b/homeassistant/components/qvr_pro/strings.json new file mode 100644 index 00000000000000..6f37bcce85e185 --- /dev/null +++ b/homeassistant/components/qvr_pro/strings.json @@ -0,0 +1,24 @@ +{ + "services": { + "start_record": { + "name": "Start record", + "description": "Starts QVR Pro recording on specified channel.", + "fields": { + "guid": { + "name": "GUID", + "description": "GUID of the channel to start recording." + } + } + }, + "stop_record": { + "name": "Stop record", + "description": "Stops QVR Pro recording on specified channel.", + "fields": { + "guid": { + "name": "GUID", + "description": "GUID of the channel to stop recording." + } + } + } + } +} diff --git a/homeassistant/components/rachio/services.yaml b/homeassistant/components/rachio/services.yaml index 67463a2217221a..6a6a8bf5cf68cb 100644 --- a/homeassistant/components/rachio/services.yaml +++ b/homeassistant/components/rachio/services.yaml @@ -1,14 +1,10 @@ set_zone_moisture_percent: - name: Set zone moisture percent - description: Set the moisture percentage of a zone or list of zones. target: entity: integration: rachio domain: switch fields: percent: - name: Percent - description: Set the desired zone moisture percentage. required: true selector: number: @@ -16,33 +12,23 @@ set_zone_moisture_percent: max: 100 unit_of_measurement: "%" start_multiple_zone_schedule: - name: Start multiple zones - description: Create a custom schedule of zones and runtimes. Note that all zones should be on the same controller to avoid issues. target: entity: integration: rachio domain: switch fields: duration: - name: Duration - description: Number of minutes to run the zone(s). If only 1 duration is given, that time will be used for all zones. If given a list of durations, the durations will apply to the respective zones listed above. example: 15, 20 required: true selector: object: pause_watering: - name: Pause watering - description: Pause any currently running zones or schedules. fields: devices: - name: Devices - description: Name of controllers to pause. Defaults to all controllers on the account if not provided. example: "Main House" selector: text: duration: - name: Duration - description: The time to pause running schedules. default: 60 selector: number: @@ -50,22 +36,14 @@ pause_watering: max: 60 unit_of_measurement: "minutes" resume_watering: - name: Resume watering - description: Resume any paused zone runs or schedules. fields: devices: - name: Devices - description: Name of controllers to resume. Defaults to all controllers on the account if not provided. example: "Main House" selector: text: stop_watering: - name: Stop watering - description: Stop any currently running zones or schedules. fields: devices: - name: Devices - description: Name of controllers to stop. Defaults to all controllers on the account if not provided. example: "Main House" selector: text: diff --git a/homeassistant/components/rachio/strings.json b/homeassistant/components/rachio/strings.json index 697b0bce2db2a4..3d776193432e80 100644 --- a/homeassistant/components/rachio/strings.json +++ b/homeassistant/components/rachio/strings.json @@ -26,5 +26,61 @@ } } } + }, + "services": { + "set_zone_moisture_percent": { + "name": "Set zone moisture percent", + "description": "Sets the moisture percentage of a zone or list of zones.", + "fields": { + "percent": { + "name": "Percent", + "description": "Set the desired zone moisture percentage." + } + } + }, + "start_multiple_zone_schedule": { + "name": "Start multiple zones", + "description": "Creates a custom schedule of zones and runtimes. Note that all zones should be on the same controller to avoid issues.", + "fields": { + "duration": { + "name": "Duration", + "description": "Number of minutes to run the zone(s). If only 1 duration is given, that time will be used for all zones. If given a list of durations, the durations will apply to the respective zones listed above." + } + } + }, + "pause_watering": { + "name": "Pause watering", + "description": "Pause any currently running zones or schedules.", + "fields": { + "devices": { + "name": "Devices", + "description": "Name of controllers to pause. Defaults to all controllers on the account if not provided." + }, + "duration": { + "name": "Duration", + "description": "The time to pause running schedules." + } + } + }, + "resume_watering": { + "name": "Resume watering", + "description": "Resume any paused zone runs or schedules.", + "fields": { + "devices": { + "name": "Devices", + "description": "Name of controllers to resume. Defaults to all controllers on the account if not provided." + } + } + }, + "stop_watering": { + "name": "Stop watering", + "description": "Stop any currently running zones or schedules.", + "fields": { + "devices": { + "name": "Devices", + "description": "Name of controllers to stop. Defaults to all controllers on the account if not provided." + } + } + } } } diff --git a/homeassistant/components/rainbird/services.yaml b/homeassistant/components/rainbird/services.yaml index 34f89ec279bdd5..11226966b0af01 100644 --- a/homeassistant/components/rainbird/services.yaml +++ b/homeassistant/components/rainbird/services.yaml @@ -1,14 +1,10 @@ start_irrigation: - name: Start irrigation - description: Start the irrigation target: entity: integration: rainbird domain: switch fields: duration: - name: Duration - description: Duration for this sprinkler to be turned on required: true selector: number: @@ -16,19 +12,13 @@ start_irrigation: max: 1440 unit_of_measurement: "minutes" set_rain_delay: - name: Set rain delay - description: Set how long automatic irrigation is turned off. fields: config_entry_id: - name: Rainbird Controller Configuration Entry - description: The setting will be adjusted on the specified controller required: true selector: config_entry: integration: rainbird duration: - name: Duration - description: Duration for this system to be turned off. required: true selector: number: diff --git a/homeassistant/components/rainbird/strings.json b/homeassistant/components/rainbird/strings.json index a98baead976ae6..9f4d0c2e34dc16 100644 --- a/homeassistant/components/rainbird/strings.json +++ b/homeassistant/components/rainbird/strings.json @@ -44,5 +44,31 @@ "name": "Raindelay" } } + }, + "services": { + "start_irrigation": { + "name": "Start irrigation", + "description": "Starts the irrigation.", + "fields": { + "duration": { + "name": "Duration", + "description": "Duration for this sprinkler to be turned on." + } + } + }, + "set_rain_delay": { + "name": "Set rain delay", + "description": "Sets how long automatic irrigation is turned off.", + "fields": { + "config_entry_id": { + "name": "Rainbird Controller Configuration Entry", + "description": "The setting will be adjusted on the specified controller." + }, + "duration": { + "name": "Duration", + "description": "Duration for this system to be turned off." + } + } + } } } diff --git a/homeassistant/components/rainmachine/services.yaml b/homeassistant/components/rainmachine/services.yaml index 9aa2bb7f50a475..2f799afd028d18 100644 --- a/homeassistant/components/rainmachine/services.yaml +++ b/homeassistant/components/rainmachine/services.yaml @@ -1,18 +1,12 @@ # Describes the format for available RainMachine services pause_watering: - name: Pause All Watering - description: Pause all watering activities for a number of seconds fields: device_id: - name: Controller - description: The controller whose watering activities should be paused required: true selector: device: integration: rainmachine seconds: - name: Duration - description: The amount of time (in seconds) to pause watering required: true selector: number: @@ -20,41 +14,29 @@ pause_watering: max: 43200 unit_of_measurement: seconds restrict_watering: - name: Restrict All Watering - description: Restrict all watering activities from starting for a time period fields: device_id: - name: Controller - description: The controller whose watering activities should be restricted required: true selector: device: integration: rainmachine duration: - name: Duration - description: The time period to restrict watering activities from starting required: true default: "01:00:00" selector: text: start_program: - name: Start Program - description: Start a program target: entity: integration: rainmachine domain: switch start_zone: - name: Start Zone - description: Start a zone target: entity: integration: rainmachine domain: switch fields: zone_run_time: - name: Run Time - description: The amount of time (in seconds) to run the zone default: 600 selector: number: @@ -62,55 +44,37 @@ start_zone: max: 86400 mode: box stop_all: - name: Stop All Watering - description: Stop all watering activities fields: device_id: - name: Controller - description: The controller whose watering activities should be stopped required: true selector: device: integration: rainmachine stop_program: - name: Stop Program - description: Stop a program target: entity: integration: rainmachine domain: switch stop_zone: - name: Stop Zone - description: Stop a zone target: entity: integration: rainmachine domain: switch unpause_watering: - name: Unpause All Watering - description: Unpause all paused watering activities fields: device_id: - name: Controller - description: The controller whose watering activities should be unpaused required: true selector: device: integration: rainmachine push_flow_meter_data: - name: Push Flow Meter Data - description: Push Flow Meter data to the RainMachine device. fields: device_id: - name: Controller - description: The controller to send flow meter data to required: true selector: device: integration: rainmachine value: - name: Value - description: The flow meter value to send required: true selector: number: @@ -119,8 +83,6 @@ push_flow_meter_data: step: 0.1 mode: box unit_of_measurement: - name: Unit of Measurement - description: The flow meter units to send selector: select: options: @@ -129,30 +91,16 @@ push_flow_meter_data: - "litre" - "m3" push_weather_data: - name: Push Weather Data - description: >- - Push Weather Data from Home Assistant to the RainMachine device. - - Local Weather Push service should be enabled from Settings > Weather > Developer tab for RainMachine to consider the values being sent. - Units must be sent in metric; no conversions are performed by the integraion. - - See details of RainMachine API Here: https://rainmachine.docs.apiary.io/#reference/weather-services/parserdata/post fields: device_id: - name: Controller - description: The controller for the weather data to be pushed. required: true selector: device: integration: rainmachine timestamp: - name: Timestamp - description: UNIX Timestamp for the Weather Data. If omitted, the RainMachine device's local time at the time of the call is used. selector: text: mintemp: - name: Min Temp - description: Minimum Temperature (°C). selector: number: min: -40 @@ -160,8 +108,6 @@ push_weather_data: step: 0.1 unit_of_measurement: "°C" maxtemp: - name: Max Temp - description: Maximum Temperature (°C). selector: number: min: -40 @@ -169,8 +115,6 @@ push_weather_data: step: 0.1 unit_of_measurement: "°C" temperature: - name: Temperature - description: Current Temperature (°C). selector: number: min: -40 @@ -178,16 +122,12 @@ push_weather_data: step: 0.1 unit_of_measurement: "°C" wind: - name: Wind Speed - description: Wind Speed (m/s) selector: number: min: 0 max: 65 unit_of_measurement: "m/s" solarrad: - name: Solar Radiation - description: Solar Radiation (MJ/m²/h) selector: number: min: 0 @@ -195,67 +135,45 @@ push_weather_data: step: 0.1 unit_of_measurement: "MJ/m²/h" et: - name: Evapotranspiration - description: Evapotranspiration (mm) selector: number: min: 0 max: 1000 unit_of_measurement: "mm" qpf: - name: Quantitative Precipitation Forecast - description: >- - Quantitative Precipitation Forecast (mm), or QPF. Note: QPF values shouldn't - be send as cumulative values but the measured/forecasted values for each hour or day. - The RainMachine Mixer will sum all QPF values in the current day to have the day total QPF. selector: number: min: 0 max: 1000 unit_of_measurement: "mm" rain: - name: Measured Rainfall - description: >- - Measured Rainfail (mm). Note: RAIN values shouldn't be send as cumulative values but the - measured/forecasted values for each hour or day. The RainMachine Mixer will sum all RAIN values - in the current day to have the day total RAIN. selector: number: min: 0 max: 1000 unit_of_measurement: "mm" minrh: - name: Min Relative Humidity - description: Min Relative Humidity (%RH) selector: number: min: 0 max: 100 unit_of_measurement: "%" maxrh: - name: Max Relative Humidity - description: Max Relative Humidity (%RH) selector: number: min: 0 max: 100 unit_of_measurement: "%" condition: - name: Weather Condition Code - description: Current weather condition code (WNUM). selector: text: pressure: - name: Barametric Pressure - description: Barametric Pressure (kPa) selector: number: min: 60 max: 110 unit_of_measurement: "kPa" dewpoint: - name: Dew Point - description: Dew Point (°C). selector: number: min: -40 @@ -263,12 +181,8 @@ push_weather_data: step: 0.1 unit_of_measurement: "°C" unrestrict_watering: - name: Unrestrict All Watering - description: Unrestrict all watering activities fields: device_id: - name: Controller - description: The controller whose watering activities should be unrestricted required: true selector: device: diff --git a/homeassistant/components/rainmachine/strings.json b/homeassistant/components/rainmachine/strings.json index 884d05359a61bb..783c876fe627fe 100644 --- a/homeassistant/components/rainmachine/strings.json +++ b/homeassistant/components/rainmachine/strings.json @@ -97,5 +97,171 @@ "name": "Firmware" } } + }, + "services": { + "pause_watering": { + "name": "Pause all watering", + "description": "Pauses all watering activities for a number of seconds.", + "fields": { + "device_id": { + "name": "Controller", + "description": "The controller whose watering activities should be paused." + }, + "seconds": { + "name": "Duration", + "description": "The amount of time (in seconds) to pause watering." + } + } + }, + "restrict_watering": { + "name": "Restrict all watering", + "description": "Restricts all watering activities from starting for a time period.", + "fields": { + "device_id": { + "name": "Controller", + "description": "The controller whose watering activities should be restricted." + }, + "duration": { + "name": "Duration", + "description": "The time period to restrict watering activities from starting." + } + } + }, + "start_program": { + "name": "Start program", + "description": "Starts a program." + }, + "start_zone": { + "name": "Start zone", + "description": "Starts a zone.", + "fields": { + "zone_run_time": { + "name": "Run time", + "description": "The amount of time (in seconds) to run the zone." + } + } + }, + "stop_all": { + "name": "Stop all watering", + "description": "Stops all watering activities.", + "fields": { + "device_id": { + "name": "Controller", + "description": "The controller whose watering activities should be stopped." + } + } + }, + "stop_program": { + "name": "Stop program", + "description": "Stops a program." + }, + "stop_zone": { + "name": "Stop zone", + "description": "Stops a zone." + }, + "unpause_watering": { + "name": "Unpause all watering", + "description": "Unpauses all paused watering activities.", + "fields": { + "device_id": { + "name": "Controller", + "description": "The controller whose watering activities should be unpaused." + } + } + }, + "push_flow_meter_data": { + "name": "Push flow meter data", + "description": "Push flow meter data to the RainMachine device.", + "fields": { + "device_id": { + "name": "Controller", + "description": "The controller to send flow meter data to." + }, + "value": { + "name": "Value", + "description": "The flow meter value to send." + }, + "unit_of_measurement": { + "name": "Unit of measurement", + "description": "The flow meter units to send." + } + } + }, + "push_weather_data": { + "name": "Push weather data", + "description": "Push weather data from Home Assistant to the RainMachine device.\nLocal Weather Push service should be enabled from Settings > Weather > Developer tab for RainMachine to consider the values being sent. Units must be sent in metric; no conversions are performed by the integraion.\nSee details of RainMachine API Here: https://rainmachine.docs.apiary.io/#reference/weather-services/parserdata/post.", + "fields": { + "device_id": { + "name": "Controller", + "description": "The controller for the weather data to be pushed." + }, + "timestamp": { + "name": "Timestamp", + "description": "UNIX Timestamp for the weather data. If omitted, the RainMachine device's local time at the time of the call is used." + }, + "mintemp": { + "name": "Min temp", + "description": "Minimum temperature (\u00b0C)." + }, + "maxtemp": { + "name": "Max temp", + "description": "Maximum temperature (\u00b0C)." + }, + "temperature": { + "name": "Temperature", + "description": "Current temperature (\u00b0C)." + }, + "wind": { + "name": "Wind speed", + "description": "Wind speed (m/s)." + }, + "solarrad": { + "name": "Solar radiation", + "description": "Solar radiation (MJ/m\u00b2/h)." + }, + "et": { + "name": "Evapotranspiration", + "description": "Evapotranspiration (mm)." + }, + "qpf": { + "name": "Quantitative Precipitation Forecast", + "description": "Quantitative Precipitation Forecast (mm), or QPF. Note: QPF values shouldn't be send as cumulative values but the measured/forecasted values for each hour or day. The RainMachine Mixer will sum all QPF values in the current day to have the day total QPF." + }, + "rain": { + "name": "Measured rainfall", + "description": "Measured rainfail (mm). Note: RAIN values shouldn't be send as cumulative values but the measured/forecasted values for each hour or day. The RainMachine Mixer will sum all RAIN values in the current day to have the day total RAIN." + }, + "minrh": { + "name": "Min relative humidity", + "description": "Min relative humidity (%RH)." + }, + "maxrh": { + "name": "Max relative humidity", + "description": "Max relative humidity (%RH)." + }, + "condition": { + "name": "Weather condition code", + "description": "Current weather condition code (WNUM)." + }, + "pressure": { + "name": "Barametric pressure", + "description": "Barametric pressure (kPa)." + }, + "dewpoint": { + "name": "Dew point", + "description": "Dew point (\u00b0C)." + } + } + }, + "unrestrict_watering": { + "name": "Unrestrict all watering", + "description": "Unrestrict all watering activities.", + "fields": { + "device_id": { + "name": "Controller", + "description": "The controller whose watering activities should be unrestricted." + } + } + } } } diff --git a/homeassistant/components/remember_the_milk/services.yaml b/homeassistant/components/remember_the_milk/services.yaml index 1458075fbd5635..5e94b2bf7d479e 100644 --- a/homeassistant/components/remember_the_milk/services.yaml +++ b/homeassistant/components/remember_the_milk/services.yaml @@ -1,33 +1,20 @@ # Describes the format for available Remember The Milk services create_task: - name: Create task - description: >- - Create (or update) a new task in your Remember The Milk account. If you want to update a task - later on, you have to set an "id" when creating the task. - Note: Updating a tasks does not support the smart syntax. fields: name: - name: Name - description: name of the new task, you can use the smart syntax here required: true example: "do this ^today #from_hass" selector: text: id: - name: ID - description: Identifier for the task you're creating, can be used to update or complete the task later on example: myid selector: text: complete_task: - name: Complete task - description: Complete a tasks that was privously created. fields: id: - name: ID - description: identifier that was defined when creating the task required: true example: myid selector: diff --git a/homeassistant/components/remember_the_milk/strings.json b/homeassistant/components/remember_the_milk/strings.json new file mode 100644 index 00000000000000..15ca4c36da87e6 --- /dev/null +++ b/homeassistant/components/remember_the_milk/strings.json @@ -0,0 +1,28 @@ +{ + "services": { + "create_task": { + "name": "Create task", + "description": "Creates (or update) a new task in your Remember The Milk account. If you want to update a task later on, you have to set an \"id\" when creating the task. Note: Updating a tasks does not support the smart syntax.", + "fields": { + "name": { + "name": "Name", + "description": "Name of the new task, you can use the smart syntax here." + }, + "id": { + "name": "ID", + "description": "Identifier for the task you're creating, can be used to update or complete the task later on." + } + } + }, + "complete_task": { + "name": "Complete task", + "description": "Completes a tasks that was privously created.", + "fields": { + "id": { + "name": "ID", + "description": "Identifier that was defined when creating the task." + } + } + } + } +} diff --git a/homeassistant/components/renault/services.yaml b/homeassistant/components/renault/services.yaml index 5911c453c95863..2dc99833d5f93d 100644 --- a/homeassistant/components/renault/services.yaml +++ b/homeassistant/components/renault/services.yaml @@ -1,16 +1,11 @@ ac_start: - name: Start A/C - description: Start A/C on vehicle. fields: vehicle: - name: Vehicle - description: The vehicle to send the command to. required: true selector: device: integration: renault temperature: - description: Target A/C temperature in °C. example: "21" required: true selector: @@ -20,36 +15,26 @@ ac_start: step: 0.5 unit_of_measurement: °C when: - description: Timestamp for the start of the A/C (optional - defaults to now). example: "2020-05-01T17:45:00" selector: text: ac_cancel: - name: Cancel A/C - description: Cancel A/C on vehicle. fields: vehicle: - name: Vehicle - description: The vehicle to send the command to. required: true selector: device: integration: renault charge_set_schedules: - name: Update charge schedule - description: Update charge schedule on vehicle. fields: vehicle: - name: Vehicle - description: The vehicle to send the command to. required: true selector: device: integration: renault schedules: - description: Schedule details. example: >- [ { diff --git a/homeassistant/components/renault/strings.json b/homeassistant/components/renault/strings.json index 7cf016187be0c9..e0b8cb0cdf03ed 100644 --- a/homeassistant/components/renault/strings.json +++ b/homeassistant/components/renault/strings.json @@ -151,5 +151,49 @@ "name": "Remote engine start code" } } + }, + "services": { + "ac_start": { + "name": "Start A/C", + "description": "Starts A/C on vehicle.", + "fields": { + "vehicle": { + "name": "Vehicle", + "description": "The vehicle to send the command to." + }, + "temperature": { + "name": "Temperature", + "description": "Target A/C temperature in \u00b0C." + }, + "when": { + "name": "When", + "description": "Timestamp for the start of the A/C (optional - defaults to now)." + } + } + }, + "ac_cancel": { + "name": "Cancel A/C", + "description": "Canceles A/C on vehicle.", + "fields": { + "vehicle": { + "name": "Vehicle", + "description": "The vehicle to send the command to." + } + } + }, + "charge_set_schedules": { + "name": "Update charge schedule", + "description": "Updates charge schedule on vehicle.", + "fields": { + "vehicle": { + "name": "Vehicle", + "description": "The vehicle to send the command to." + }, + "schedules": { + "name": "Schedules", + "description": "Schedule details." + } + } + } } } diff --git a/homeassistant/components/rest/services.yaml b/homeassistant/components/rest/services.yaml index 9ba509b63f6f47..c983a105c93977 100644 --- a/homeassistant/components/rest/services.yaml +++ b/homeassistant/components/rest/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload all rest entities and notify services diff --git a/homeassistant/components/rest/strings.json b/homeassistant/components/rest/strings.json new file mode 100644 index 00000000000000..afbab8d8040ab5 --- /dev/null +++ b/homeassistant/components/rest/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "reload": { + "name": "Reload", + "description": "Reloads REST entities from the YAML-configuration." + } + } +} diff --git a/homeassistant/components/rflink/services.yaml b/homeassistant/components/rflink/services.yaml index 8e233bc7aac5d9..1b06a142a59f79 100644 --- a/homeassistant/components/rflink/services.yaml +++ b/homeassistant/components/rflink/services.yaml @@ -1,17 +1,11 @@ send_command: - name: Send command - description: Send device command through RFLink. fields: command: - name: Command - description: The command to be sent. required: true example: "on" selector: text: device_id: - name: Device ID - description: RFLink device ID. required: true example: newkaku_0000c6c2_1 selector: diff --git a/homeassistant/components/rflink/strings.json b/homeassistant/components/rflink/strings.json new file mode 100644 index 00000000000000..2c8eb584ca8aac --- /dev/null +++ b/homeassistant/components/rflink/strings.json @@ -0,0 +1,18 @@ +{ + "services": { + "send_command": { + "name": "Send command", + "description": "Sends device command through RFLink.", + "fields": { + "command": { + "name": "Command", + "description": "The command to be sent." + }, + "device_id": { + "name": "Device ID", + "description": "RFLink device ID." + } + } + } + } +} diff --git a/homeassistant/components/rfxtrx/services.yaml b/homeassistant/components/rfxtrx/services.yaml index 43695554ed0e3d..00640a2ff59dda 100644 --- a/homeassistant/components/rfxtrx/services.yaml +++ b/homeassistant/components/rfxtrx/services.yaml @@ -1,10 +1,6 @@ send: - name: Send - description: Sends a raw event on radio. fields: event: - name: Event - description: A hexadecimal string to send. required: true example: "0b11009e00e6116202020070" selector: diff --git a/homeassistant/components/rfxtrx/strings.json b/homeassistant/components/rfxtrx/strings.json index 7e68f960fca6db..6c49fb38d6cae5 100644 --- a/homeassistant/components/rfxtrx/strings.json +++ b/homeassistant/components/rfxtrx/strings.json @@ -136,5 +136,17 @@ "name": "UV index" } } + }, + "services": { + "send": { + "name": "Send", + "description": "Sends a raw event on radio.", + "fields": { + "event": { + "name": "Event", + "description": "A hexadecimal string to send." + } + } + } } } diff --git a/homeassistant/components/ring/services.yaml b/homeassistant/components/ring/services.yaml index c648f02139b0be..91b8669505b380 100644 --- a/homeassistant/components/ring/services.yaml +++ b/homeassistant/components/ring/services.yaml @@ -1,3 +1 @@ update: - name: Update - description: Updates the data we have for all your ring devices diff --git a/homeassistant/components/ring/strings.json b/homeassistant/components/ring/strings.json index 43209a5a6a3c76..b300e335b191f7 100644 --- a/homeassistant/components/ring/strings.json +++ b/homeassistant/components/ring/strings.json @@ -64,5 +64,11 @@ "name": "[%key:component::siren::title%]" } } + }, + "services": { + "update": { + "name": "Update", + "description": "Updates the data we have for all your ring devices." + } } } diff --git a/homeassistant/components/roku/services.yaml b/homeassistant/components/roku/services.yaml index 16fd51ea95be4c..4a28db94fa4d45 100644 --- a/homeassistant/components/roku/services.yaml +++ b/homeassistant/components/roku/services.yaml @@ -1,14 +1,10 @@ search: - name: Search - description: Emulates opening the search screen and entering the search keyword. target: entity: integration: roku domain: media_player fields: keyword: - name: Keyword - description: The keyword to search for. required: true example: "Space Jam" selector: diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json index 04c504def034bf..3510a43c604da5 100644 --- a/homeassistant/components/roku/strings.json +++ b/homeassistant/components/roku/strings.json @@ -20,5 +20,17 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "unknown": "[%key:common::config_flow::error::unknown%]" } + }, + "services": { + "search": { + "name": "Search", + "description": "Emulates opening the search screen and entering the search keyword.", + "fields": { + "keyword": { + "name": "Keyword", + "description": "The keyword to search for." + } + } + } } } diff --git a/homeassistant/components/roon/services.yaml b/homeassistant/components/roon/services.yaml index 9d9d02f0efc457..1de3e14bbc9ed6 100644 --- a/homeassistant/components/roon/services.yaml +++ b/homeassistant/components/roon/services.yaml @@ -1,14 +1,10 @@ transfer: - name: Transfer - description: Transfer playback from one player to another. target: entity: integration: roon domain: media_player fields: transfer_id: - name: Transfer ID - description: id of the destination player. required: true selector: entity: diff --git a/homeassistant/components/roon/strings.json b/homeassistant/components/roon/strings.json index ce5827e2c6c6cd..f67779e9eaaba7 100644 --- a/homeassistant/components/roon/strings.json +++ b/homeassistant/components/roon/strings.json @@ -21,5 +21,17 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "services": { + "transfer": { + "name": "Transfer", + "description": "Transfers playback from one player to another.", + "fields": { + "transfer_id": { + "name": "Transfer ID", + "description": "ID of the destination player." + } + } + } } } diff --git a/homeassistant/components/route53/services.yaml b/homeassistant/components/route53/services.yaml index 4936a499764010..e800a3a3eeebb7 100644 --- a/homeassistant/components/route53/services.yaml +++ b/homeassistant/components/route53/services.yaml @@ -1,3 +1 @@ update_records: - name: Update records - description: Trigger update of records. diff --git a/homeassistant/components/route53/strings.json b/homeassistant/components/route53/strings.json new file mode 100644 index 00000000000000..12b372d0ce2fd0 --- /dev/null +++ b/homeassistant/components/route53/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "update_records": { + "name": "Update records", + "description": "Triggers update of records." + } + } +} diff --git a/homeassistant/components/sabnzbd/services.yaml b/homeassistant/components/sabnzbd/services.yaml index 2221eed169f404..f1eea1c9469a8b 100644 --- a/homeassistant/components/sabnzbd/services.yaml +++ b/homeassistant/components/sabnzbd/services.yaml @@ -1,36 +1,22 @@ pause: - name: Pause - description: Pauses downloads. fields: api_key: - name: Sabnzbd API key - description: The Sabnzbd API key to pause downloads required: true selector: text: resume: - name: Resume - description: Resumes downloads. fields: api_key: - name: Sabnzbd API key - description: The Sabnzbd API key to resume downloads required: true selector: text: set_speed: - name: Set speed - description: Sets the download speed limit. fields: api_key: - name: Sabnzbd API key - description: The Sabnzbd API key to set speed limit required: true selector: text: speed: - name: Speed - description: Speed limit. If specified as a number with no units, will be interpreted as a percent. If units are provided (e.g., 500K) will be interpreted absolutely. example: 100 default: 100 selector: diff --git a/homeassistant/components/sabnzbd/strings.json b/homeassistant/components/sabnzbd/strings.json index 501e0d33faff58..2989ee5d00be27 100644 --- a/homeassistant/components/sabnzbd/strings.json +++ b/homeassistant/components/sabnzbd/strings.json @@ -13,5 +13,41 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" } + }, + "services": { + "pause": { + "name": "Pause", + "description": "Pauses downloads.", + "fields": { + "api_key": { + "name": "SABnzbd API key", + "description": "The SABnzbd API key to pause downloads." + } + } + }, + "resume": { + "name": "Resume", + "description": "Resumes downloads.", + "fields": { + "api_key": { + "name": "SABnzbd API key", + "description": "The SABnzbd API key to resume downloads." + } + } + }, + "set_speed": { + "name": "Set speed", + "description": "Sets the download speed limit.", + "fields": { + "api_key": { + "name": "SABnzbd API key", + "description": "The SABnzbd API key to set speed limit." + }, + "speed": { + "name": "Speed", + "description": "Speed limit. If specified as a number with no units, will be interpreted as a percent. If units are provided (e.g., 500K) will be interpreted absolutely." + } + } + } } } diff --git a/homeassistant/components/screenlogic/services.yaml b/homeassistant/components/screenlogic/services.yaml index 439d020a432f97..8e4a82a1079911 100644 --- a/homeassistant/components/screenlogic/services.yaml +++ b/homeassistant/components/screenlogic/services.yaml @@ -1,14 +1,10 @@ # ScreenLogic Services set_color_mode: - name: Set Color Mode - description: Sets the color mode for all color-capable lights attached to this ScreenLogic gateway. target: device: integration: screenlogic fields: color_mode: - name: Color Mode - description: The ScreenLogic color mode to set required: true selector: select: diff --git a/homeassistant/components/screenlogic/strings.json b/homeassistant/components/screenlogic/strings.json index b0958d31727002..79b633e28b617a 100644 --- a/homeassistant/components/screenlogic/strings.json +++ b/homeassistant/components/screenlogic/strings.json @@ -35,5 +35,17 @@ } } } + }, + "services": { + "set_color_mode": { + "name": "Set Color Mode", + "description": "Sets the color mode for all color-capable lights attached to this ScreenLogic gateway.", + "fields": { + "color_mode": { + "name": "Color Mode", + "description": "The ScreenLogic color mode to set." + } + } + } } } diff --git a/homeassistant/components/sensibo/services.yaml b/homeassistant/components/sensibo/services.yaml index fbd2625961bc92..7f8252af82029d 100644 --- a/homeassistant/components/sensibo/services.yaml +++ b/homeassistant/components/sensibo/services.yaml @@ -1,14 +1,10 @@ assume_state: - name: Assume state - description: Set Sensibo device to external state target: entity: integration: sensibo domain: climate fields: state: - name: State - description: State to set required: true example: "on" selector: @@ -17,16 +13,12 @@ assume_state: - "on" - "off" enable_timer: - name: Enable Timer - description: Enable the timer with custom time target: entity: integration: sensibo domain: climate fields: minutes: - name: Minutes - description: Countdown for timer (for timer state on) required: false example: 30 selector: @@ -35,44 +27,32 @@ enable_timer: step: 1 mode: box enable_pure_boost: - name: Enable Pure Boost - description: Enable and configure Pure Boost settings target: entity: integration: sensibo domain: climate fields: ac_integration: - name: AC Integration - description: Integrate with Air Conditioner required: true example: true selector: boolean: geo_integration: - name: Geo Integration - description: Integrate with Presence required: true example: true selector: boolean: indoor_integration: - name: Indoor Air Quality - description: Integrate with checking indoor air quality required: true example: true selector: boolean: outdoor_integration: - name: Outdoor Air Quality - description: Integrate with checking outdoor air quality required: true example: true selector: boolean: sensitivity: - name: Sensitivity - description: Set the sensitivity for Pure Boost required: true example: "Normal" selector: @@ -81,16 +61,12 @@ enable_pure_boost: - "Normal" - "Sensitive" full_state: - name: Set full state - description: Set full state for Sensibo device target: entity: integration: sensibo domain: climate fields: mode: - name: HVAC mode - description: HVAC mode to set required: true example: "heat" selector: @@ -103,8 +79,6 @@ full_state: - "dry" - "off" target_temperature: - name: Target Temperature - description: Optionally set target temperature required: false example: 23 selector: @@ -113,32 +87,24 @@ full_state: step: 1 mode: box fan_mode: - name: Fan mode - description: Optionally set fan mode required: false example: "low" selector: text: type: text swing_mode: - name: swing mode - description: Optionally set swing mode required: false example: "fixedBottom" selector: text: type: text horizontal_swing_mode: - name: Horizontal swing mode - description: Optionally set horizontal swing mode required: false example: "fixedLeft" selector: text: type: text light: - name: Light - description: Set light on or off required: false example: "on" selector: @@ -148,16 +114,12 @@ full_state: - "off" - "dim" enable_climate_react: - name: Enable Climate React - description: Enable and configure Climate React target: entity: integration: sensibo domain: climate fields: high_temperature_threshold: - name: Threshold high - description: When temp/humidity goes above required: true example: 24 selector: @@ -167,14 +129,10 @@ enable_climate_react: step: 0.1 mode: box high_temperature_state: - name: State high threshold - description: What should happen at high threshold. Requires full state required: true selector: object: low_temperature_threshold: - name: Threshold low - description: When temp/humidity goes below required: true example: 19 selector: @@ -184,14 +142,10 @@ enable_climate_react: step: 0.1 mode: box low_temperature_state: - name: State low threshold - description: What should happen at low threshold. Requires full state required: true selector: object: smart_type: - name: Trigger type - description: Choose between temperature/feels like/humidity required: true example: "temperature" selector: diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json index 2379e2c2b38c83..6946b21761c37a 100644 --- a/homeassistant/components/sensibo/strings.json +++ b/homeassistant/components/sensibo/strings.json @@ -157,5 +157,109 @@ "name": "Update available" } } + }, + "services": { + "assume_state": { + "name": "Assume state", + "description": "Sets Sensibo device to external state.", + "fields": { + "state": { + "name": "State", + "description": "State to set." + } + } + }, + "enable_timer": { + "name": "Enable timer", + "description": "Enables the timer with custom time.", + "fields": { + "minutes": { + "name": "Minutes", + "description": "Countdown for timer (for timer state on)." + } + } + }, + "enable_pure_boost": { + "name": "Enable pure boost", + "description": "Enables and configures Pure Boost settings.", + "fields": { + "ac_integration": { + "name": "AC integration", + "description": "Integrate with Air Conditioner." + }, + "geo_integration": { + "name": "Geo integration", + "description": "Integrate with Presence." + }, + "indoor_integration": { + "name": "Indoor air quality", + "description": "Integrate with checking indoor air quality." + }, + "outdoor_integration": { + "name": "Outdoor air quality", + "description": "Integrate with checking outdoor air quality." + }, + "sensitivity": { + "name": "Sensitivity", + "description": "Set the sensitivity for Pure Boost." + } + } + }, + "full_state": { + "name": "Set full state", + "description": "Sets full state for Sensibo device.", + "fields": { + "mode": { + "name": "HVAC mode", + "description": "HVAC mode to set." + }, + "target_temperature": { + "name": "Target temperature", + "description": "Set target temperature." + }, + "fan_mode": { + "name": "Fan mode", + "description": "set fan mode." + }, + "swing_mode": { + "name": "Swing mode", + "description": "Set swing mode." + }, + "horizontal_swing_mode": { + "name": "Horizontal swing mode", + "description": "Set horizontal swing mode." + }, + "light": { + "name": "Light", + "description": "Set light on or off." + } + } + }, + "enable_climate_react": { + "name": "Enable climate react", + "description": "Enables and configures climate react.", + "fields": { + "high_temperature_threshold": { + "name": "Threshold high", + "description": "When temp/humidity goes above." + }, + "high_temperature_state": { + "name": "State high threshold", + "description": "What should happen at high threshold. Requires full state." + }, + "low_temperature_threshold": { + "name": "Threshold low", + "description": "When temp/humidity goes below." + }, + "low_temperature_state": { + "name": "State low threshold", + "description": "What should happen at low threshold. Requires full state." + }, + "smart_type": { + "name": "Trigger type", + "description": "Choose between temperature/feels like/humidity." + } + } + } } } diff --git a/homeassistant/components/shopping_list/services.yaml b/homeassistant/components/shopping_list/services.yaml index 250912f49cdd59..402a6c24aeb9e9 100644 --- a/homeassistant/components/shopping_list/services.yaml +++ b/homeassistant/components/shopping_list/services.yaml @@ -1,69 +1,41 @@ add_item: - name: Add item - description: Add an item to the shopping list. fields: name: - name: Name - description: The name of the item to add. required: true example: Beer selector: text: remove_item: - name: Remove item - description: Remove the first item with matching name from the shopping list. fields: name: - name: Name - description: The name of the item to remove. required: true example: Beer selector: text: complete_item: - name: Complete item - description: Mark the first item with matching name as completed in the shopping list. fields: name: - name: Name - description: The name of the item to mark as completed (without removing). required: true example: Beer selector: text: incomplete_item: - name: Incomplete item - description: Mark the first item with matching name as incomplete in the shopping list. fields: name: - description: The name of the item to mark as incomplete. example: Beer required: true selector: text: complete_all: - name: Complete all - description: Mark all items as completed in the shopping list (without removing them from the list). - incomplete_all: - name: Incomplete all - description: Mark all items as incomplete in the shopping list. - clear_completed_items: - name: Clear completed items - description: Clear completed items from the shopping list. - sort: - name: Sort all items - description: Sort all items by name in the shopping list. fields: reverse: - name: Sort reverse - description: Whether to sort in reverse (descending) order. default: false selector: boolean: diff --git a/homeassistant/components/shopping_list/strings.json b/homeassistant/components/shopping_list/strings.json index 5b8197177a02be..598a2bddfffff7 100644 --- a/homeassistant/components/shopping_list/strings.json +++ b/homeassistant/components/shopping_list/strings.json @@ -10,5 +10,69 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + }, + "services": { + "add_item": { + "name": "Add item", + "description": "Adds an item to the shopping list.", + "fields": { + "name": { + "name": "Name", + "description": "The name of the item to add." + } + } + }, + "remove_item": { + "name": "Remove item", + "description": "Removes the first item with matching name from the shopping list.", + "fields": { + "name": { + "name": "Name", + "description": "The name of the item to remove." + } + } + }, + "complete_item": { + "name": "Complete item", + "description": "Marks the first item with matching name as completed in the shopping list.", + "fields": { + "name": { + "name": "Name", + "description": "The name of the item to mark as completed (without removing)." + } + } + }, + "incomplete_item": { + "name": "Incomplete item", + "description": "Marks the first item with matching name as incomplete in the shopping list.", + "fields": { + "name": { + "name": "Name", + "description": "The name of the item to mark as incomplete." + } + } + }, + "complete_all": { + "name": "Complete all", + "description": "Marks all items as completed in the shopping list (without removing them from the list)." + }, + "incomplete_all": { + "name": "Incomplete all", + "description": "Marks all items as incomplete in the shopping list." + }, + "clear_completed_items": { + "name": "Clear completed items", + "description": "Clears completed items from the shopping list." + }, + "sort": { + "name": "Sort all items", + "description": "Sorts all items by name in the shopping list.", + "fields": { + "reverse": { + "name": "Sort reverse", + "description": "Whether to sort in reverse (descending) order." + } + } + } } } diff --git a/homeassistant/components/simplisafe/services.yaml b/homeassistant/components/simplisafe/services.yaml index 8aeefcf7846f24..de4d8fbe534674 100644 --- a/homeassistant/components/simplisafe/services.yaml +++ b/homeassistant/components/simplisafe/services.yaml @@ -1,11 +1,7 @@ # Describes the format for available SimpliSafe services remove_pin: - name: Remove PIN - description: Remove a PIN by its label or value. fields: device_id: - name: System - description: The system to remove the PIN from required: true selector: device: @@ -13,19 +9,13 @@ remove_pin: entity: domain: alarm_control_panel label_or_pin: - name: Label/PIN - description: The label/value to remove. required: true example: Test PIN selector: text: set_pin: - name: Set PIN - description: Set/update a PIN fields: device_id: - name: System - description: The system to set the PIN on required: true selector: device: @@ -33,26 +23,18 @@ set_pin: entity: domain: alarm_control_panel label: - name: Label - description: The label of the PIN required: true example: Test PIN selector: text: pin: - name: PIN - description: The value of the PIN required: true example: 1256 selector: text: set_system_properties: - name: Set system properties - description: Set one or more system properties fields: device_id: - name: System - description: The system whose properties should be set required: true selector: device: @@ -60,16 +42,12 @@ set_system_properties: entity: domain: alarm_control_panel alarm_duration: - name: Alarm duration - description: The length of a triggered alarm selector: number: min: 30 max: 480 unit_of_measurement: seconds alarm_volume: - name: Alarm volume - description: The volume level of a triggered alarm selector: select: options: @@ -78,8 +56,6 @@ set_system_properties: - "high" - "off" chime_volume: - name: Chime volume - description: The volume level of the door chime selector: select: options: @@ -88,45 +64,33 @@ set_system_properties: - "high" - "off" entry_delay_away: - name: Entry delay away - description: How long to delay when entering while "away" selector: number: min: 30 max: 255 unit_of_measurement: seconds entry_delay_home: - name: Entry delay home - description: How long to delay when entering while "home" selector: number: min: 0 max: 255 unit_of_measurement: seconds exit_delay_away: - name: Exit delay away - description: How long to delay when exiting while "away" selector: number: min: 45 max: 255 unit_of_measurement: seconds exit_delay_home: - name: Exit delay home - description: How long to delay when exiting while "home" selector: number: min: 0 max: 255 unit_of_measurement: seconds light: - name: Light - description: Whether the armed light should be visible selector: boolean: voice_prompt_volume: - name: Voice prompt volume - description: The volume level of the voice prompt selector: select: options: diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 4f230442f8567a..4be806ebbbd761 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -36,5 +36,85 @@ "name": "Clear notifications" } } + }, + "services": { + "remove_pin": { + "name": "Remove PIN", + "description": "Removes a PIN by its label or value.", + "fields": { + "device_id": { + "name": "System", + "description": "The system to remove the PIN from." + }, + "label_or_pin": { + "name": "Label/PIN", + "description": "The label/value to remove." + } + } + }, + "set_pin": { + "name": "Set PIN", + "description": "Sets/updates a PIN.", + "fields": { + "device_id": { + "name": "System", + "description": "The system to set the PIN on." + }, + "label": { + "name": "Label", + "description": "The label of the PIN." + }, + "pin": { + "name": "PIN", + "description": "The value of the PIN." + } + } + }, + "set_system_properties": { + "name": "Set system properties", + "description": "Sets one or more system properties.", + "fields": { + "device_id": { + "name": "System", + "description": "The system whose properties should be set." + }, + "alarm_duration": { + "name": "Alarm duration", + "description": "The length of a triggered alarm." + }, + "alarm_volume": { + "name": "Alarm volume", + "description": "The volume level of a triggered alarm." + }, + "chime_volume": { + "name": "Chime volume", + "description": "The volume level of the door chime." + }, + "entry_delay_away": { + "name": "Entry delay away", + "description": "How long to delay when entering while \"away\"." + }, + "entry_delay_home": { + "name": "Entry delay home", + "description": "How long to delay when entering while \"home\"." + }, + "exit_delay_away": { + "name": "Exit delay away", + "description": "How long to delay when exiting while \"away\"." + }, + "exit_delay_home": { + "name": "Exit delay home", + "description": "How long to delay when exiting while \"home\"." + }, + "light": { + "name": "Light", + "description": "Whether the armed light should be visible." + }, + "voice_prompt_volume": { + "name": "Voice prompt volume", + "description": "The volume level of the voice prompt." + } + } + } } } diff --git a/homeassistant/components/smarttub/services.yaml b/homeassistant/components/smarttub/services.yaml index d9890dba35a738..65bd4afb8b7132 100644 --- a/homeassistant/components/smarttub/services.yaml +++ b/homeassistant/components/smarttub/services.yaml @@ -1,14 +1,10 @@ set_primary_filtration: - name: Update primary filtration settings - description: Updates the primary filtration settings target: entity: integration: smarttub domain: sensor fields: duration: - name: Duration - description: The desired duration of the primary filtration cycle default: 8 selector: number: @@ -18,7 +14,6 @@ set_primary_filtration: mode: slider example: 8 start_hour: - description: The hour of the day at which to begin the primary filtration cycle default: 0 example: 2 selector: @@ -28,15 +23,12 @@ set_primary_filtration: unit_of_measurement: "hour" set_secondary_filtration: - name: Update secondary filtration settings - description: Updates the secondary filtration settings target: entity: integration: smarttub domain: sensor fields: mode: - description: The secondary filtration mode. selector: select: options: @@ -47,16 +39,12 @@ set_secondary_filtration: example: "frequent" snooze_reminder: - name: Snooze a reminder - description: Delay a reminder, so that it won't trigger again for a period of time. target: entity: integration: smarttub domain: binary_sensor fields: days: - name: Days - description: The number of days to delay the reminder. required: true example: 7 selector: @@ -66,16 +54,12 @@ snooze_reminder: unit_of_measurement: days reset_reminder: - name: Reset a reminder - description: Reset a reminder, and set the next time it will be triggered. target: entity: integration: smarttub domain: binary_sensor fields: days: - name: Days - description: The number of days when the next reminder should trigger. required: true example: 180 selector: diff --git a/homeassistant/components/smarttub/strings.json b/homeassistant/components/smarttub/strings.json index 25528b8a374d6b..c130feaa620b22 100644 --- a/homeassistant/components/smarttub/strings.json +++ b/homeassistant/components/smarttub/strings.json @@ -21,5 +21,51 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "services": { + "set_primary_filtration": { + "name": "Update primary filtration settings", + "description": "Updates the primary filtration settings.", + "fields": { + "duration": { + "name": "Duration", + "description": "The desired duration of the primary filtration cycle." + }, + "start_hour": { + "name": "Start hour", + "description": "The hour of the day at which to begin the primary filtration cycle." + } + } + }, + "set_secondary_filtration": { + "name": "Update secondary filtration settings", + "description": "Updates the secondary filtration settings.", + "fields": { + "mode": { + "name": "Mode", + "description": "The secondary filtration mode." + } + } + }, + "snooze_reminder": { + "name": "Snooze a reminder", + "description": "Delay a reminder, so that it won't trigger again for a period of time.", + "fields": { + "days": { + "name": "Days", + "description": "The number of days to delay the reminder." + } + } + }, + "reset_reminder": { + "name": "Reset a reminder", + "description": "Reset a reminder, and set the next time it will be triggered.", + "fields": { + "days": { + "name": "Days", + "description": "The number of days when the next reminder should trigger." + } + } + } } } diff --git a/homeassistant/components/smtp/services.yaml b/homeassistant/components/smtp/services.yaml index c4380a4fc62381..c983a105c93977 100644 --- a/homeassistant/components/smtp/services.yaml +++ b/homeassistant/components/smtp/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload smtp notify services. diff --git a/homeassistant/components/smtp/strings.json b/homeassistant/components/smtp/strings.json new file mode 100644 index 00000000000000..3c72a1a50d1a75 --- /dev/null +++ b/homeassistant/components/smtp/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "reload": { + "name": "Reload", + "description": "Reloads smtp notify services." + } + } +} diff --git a/homeassistant/components/snapcast/services.yaml b/homeassistant/components/snapcast/services.yaml index f80b22dba7edec..aa1a26c353754b 100644 --- a/homeassistant/components/snapcast/services.yaml +++ b/homeassistant/components/snapcast/services.yaml @@ -1,18 +1,12 @@ join: - name: Join - description: Group players together. fields: master: - name: Master - description: Entity ID of the player to synchronize to. required: true selector: entity: integration: snapcast domain: media_player entity_id: - name: Entity - description: The players to join to the "master". selector: target: entity: @@ -20,40 +14,30 @@ join: domain: media_player unjoin: - name: Unjoin - description: Unjoin the player from a group. target: entity: integration: snapcast domain: media_player snapshot: - name: Snapshot - description: Take a snapshot of the media player. target: entity: integration: snapcast domain: media_player restore: - name: Restore - description: Restore a snapshot of the media player. target: entity: integration: snapcast domain: media_player set_latency: - name: Set latency - description: Set client set_latency target: entity: integration: snapcast domain: media_player fields: latency: - name: Latency - description: Latency in master required: true selector: number: diff --git a/homeassistant/components/snapcast/strings.json b/homeassistant/components/snapcast/strings.json index 766bca634955d6..242bf62ab04f8a 100644 --- a/homeassistant/components/snapcast/strings.json +++ b/homeassistant/components/snapcast/strings.json @@ -17,5 +17,43 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_host": "[%key:common::config_flow::error::invalid_host%]" } + }, + "services": { + "join": { + "name": "Join", + "description": "Groups players together.", + "fields": { + "master": { + "name": "Master", + "description": "Entity ID of the player to synchronize to." + }, + "entity_id": { + "name": "Entity", + "description": "The players to join to the \"master\"." + } + } + }, + "unjoin": { + "name": "Unjoin", + "description": "Unjoins the player from a group." + }, + "snapshot": { + "name": "Snapshot", + "description": "Takes a snapshot of the media player." + }, + "restore": { + "name": "Restore", + "description": "Restores a snapshot of the media player." + }, + "set_latency": { + "name": "Set latency", + "description": "Sets client set_latency.", + "fields": { + "latency": { + "name": "Latency", + "description": "Latency in master." + } + } + } } } diff --git a/homeassistant/components/snips/services.yaml b/homeassistant/components/snips/services.yaml index df3a46281c87a9..522e1b5b348c8c 100644 --- a/homeassistant/components/snips/services.yaml +++ b/homeassistant/components/snips/services.yaml @@ -1,83 +1,55 @@ feedback_off: - name: Feedback off - description: Turns feedback sounds off. fields: site_id: - name: Site ID - description: Site to turn sounds on, defaults to all sites. example: bedroom default: default selector: text: feedback_on: - name: Feedback on - description: Turns feedback sounds on. fields: site_id: - name: Site ID - description: Site to turn sounds on, defaults to all sites. example: bedroom default: default selector: text: say: - name: Say - description: Send a TTS message to Snips. fields: custom_data: - name: Custom data - description: custom data that will be included with all messages in this session example: user=UserName default: "" selector: text: site_id: - name: Site ID - description: Site to use to start session, defaults to default. example: bedroom default: default selector: text: text: - name: Text - description: Text to say. required: true example: My name is snips selector: text: say_action: - name: Say action - description: Send a TTS message to Snips to listen for a response. fields: can_be_enqueued: - name: Can be enqueued - description: If True, session waits for an open session to end, if False session is dropped if one is running default: true selector: boolean: custom_data: - name: Custom data - description: custom data that will be included with all messages in this session example: user=UserName default: "" selector: text: intent_filter: - name: Intent filter - description: Optional Array of Strings - A list of intents names to restrict the NLU resolution to on the first query. example: "turnOnLights, turnOffLights" selector: object: site_id: - name: Site ID - description: Site to use to start session, defaults to default. example: bedroom default: default selector: text: text: - name: Text - description: Text to say required: true example: My name is snips selector: diff --git a/homeassistant/components/snips/strings.json b/homeassistant/components/snips/strings.json new file mode 100644 index 00000000000000..d6c9f4d53f65b7 --- /dev/null +++ b/homeassistant/components/snips/strings.json @@ -0,0 +1,68 @@ +{ + "services": { + "feedback_off": { + "name": "Feedback off", + "description": "Turns feedback sounds off.", + "fields": { + "site_id": { + "name": "Site ID", + "description": "Site to turn sounds on, defaults to all sites." + } + } + }, + "feedback_on": { + "name": "Feedback on", + "description": "Turns feedback sounds on.", + "fields": { + "site_id": { + "name": "Site ID", + "description": "Site to turn sounds on, defaults to all sites." + } + } + }, + "say": { + "name": "Say", + "description": "Sends a TTS message to Snips.", + "fields": { + "custom_data": { + "name": "Custom data", + "description": "Custom data that will be included with all messages in this session." + }, + "site_id": { + "name": "Site ID", + "description": "Site to use to start session, defaults to default." + }, + "text": { + "name": "Text", + "description": "Text to say." + } + } + }, + "say_action": { + "name": "Say action", + "description": "Sends a TTS message to Snips to listen for a response.", + "fields": { + "can_be_enqueued": { + "name": "Can be enqueued", + "description": "If True, session waits for an open session to end, if False session is dropped if one is running." + }, + "custom_data": { + "name": "Custom data", + "description": "Custom data that will be included with all messages in this session." + }, + "intent_filter": { + "name": "Intent filter", + "description": "Optional Array of Strings - A list of intents names to restrict the NLU resolution to on the first query." + }, + "site_id": { + "name": "Site ID", + "description": "Site to use to start session, defaults to default." + }, + "text": { + "name": "Text", + "description": "Text to say." + } + } + } + } +} diff --git a/homeassistant/components/snooz/services.yaml b/homeassistant/components/snooz/services.yaml index f795edf213ab11..ca9f4883a69d13 100644 --- a/homeassistant/components/snooz/services.yaml +++ b/homeassistant/components/snooz/services.yaml @@ -1,14 +1,10 @@ transition_on: - name: Transition on - description: Transition to a target volume level over time. target: entity: integration: snooz domain: fan fields: duration: - name: Transition duration - description: Time it takes to reach the target volume level. selector: number: min: 1 @@ -16,8 +12,6 @@ transition_on: unit_of_measurement: seconds mode: box volume: - name: Target volume - description: If not specified, the volume level is read from the device. selector: number: min: 1 @@ -25,16 +19,12 @@ transition_on: unit_of_measurement: "%" transition_off: - name: Transition off - description: Transition volume off over time. target: entity: integration: snooz domain: fan fields: duration: - name: Transition duration - description: Time it takes to turn off. selector: number: min: 1 diff --git a/homeassistant/components/snooz/strings.json b/homeassistant/components/snooz/strings.json index 2f957f87072a16..878341f23bc596 100644 --- a/homeassistant/components/snooz/strings.json +++ b/homeassistant/components/snooz/strings.json @@ -23,5 +23,31 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "services": { + "transition_on": { + "name": "Transition on", + "description": "Transitions to a target volume level over time.", + "fields": { + "duration": { + "name": "Transition duration", + "description": "Time it takes to reach the target volume level." + }, + "volume": { + "name": "Target volume", + "description": "If not specified, the volume level is read from the device." + } + } + }, + "transition_off": { + "name": "Transition off", + "description": "Transitions volume off over time.", + "fields": { + "duration": { + "name": "Transition duration", + "description": "Time it takes to turn off." + } + } + } } } diff --git a/homeassistant/components/songpal/services.yaml b/homeassistant/components/songpal/services.yaml index 93485ce47883e2..26da134acddaba 100644 --- a/homeassistant/components/songpal/services.yaml +++ b/homeassistant/components/songpal/services.yaml @@ -1,21 +1,15 @@ set_sound_setting: - name: Set sound setting - description: Change sound setting. target: entity: integration: songpal domain: media_player fields: name: - name: Name - description: Name of the setting. required: true example: "nightMode" selector: text: value: - name: Value - description: Value to set. required: true example: "on" selector: diff --git a/homeassistant/components/songpal/strings.json b/homeassistant/components/songpal/strings.json index 62bff00c78692c..a4df830f1fe8cd 100644 --- a/homeassistant/components/songpal/strings.json +++ b/homeassistant/components/songpal/strings.json @@ -18,5 +18,21 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "not_songpal_device": "Not a Songpal device" } + }, + "services": { + "set_sound_setting": { + "name": "Sets sound setting", + "description": "Change sound setting.", + "fields": { + "name": { + "name": "Name", + "description": "Name of the setting." + }, + "value": { + "name": "Value", + "description": "Value to set." + } + } + } } } diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml index 9d61c20f7cb569..f6df83ef6ed287 100644 --- a/homeassistant/components/sonos/services.yaml +++ b/homeassistant/components/sonos/services.yaml @@ -1,49 +1,33 @@ snapshot: - name: Snapshot - description: Take a snapshot of the media player. fields: entity_id: - name: Entity - description: Name of entity that will be snapshot. selector: entity: integration: sonos domain: media_player with_group: - name: With group - description: True or False. Also snapshot the group layout. default: true selector: boolean: restore: - name: Restore - description: Restore a snapshot of the media player. fields: entity_id: - name: Entity - description: Name of entity that will be restored. selector: entity: integration: sonos domain: media_player with_group: - name: With group - description: True or False. Also restore the group layout. default: true selector: boolean: set_sleep_timer: - name: Set timer - description: Set a Sonos timer. target: device: integration: sonos fields: sleep_time: - name: Sleep Time - description: Number of seconds to set the timer. selector: number: min: 0 @@ -51,22 +35,16 @@ set_sleep_timer: unit_of_measurement: seconds clear_sleep_timer: - name: Clear timer - description: Clear a Sonos timer. target: device: integration: sonos play_queue: - name: Play queue - description: Start playing the queue from the first item. target: device: integration: sonos fields: queue_position: - name: Queue position - description: Position of the song in the queue to start playing from. selector: number: min: 0 @@ -74,15 +52,11 @@ play_queue: mode: box remove_from_queue: - name: Remove from queue - description: Removes an item from the queue. target: device: integration: sonos fields: queue_position: - name: Queue position - description: Position in the queue to remove. selector: number: min: 0 @@ -90,15 +64,11 @@ remove_from_queue: mode: box update_alarm: - name: Update alarm - description: Updates an alarm with new time and volume settings. target: device: integration: sonos fields: alarm_id: - name: Alarm ID - description: ID for the alarm to be updated. required: true selector: number: @@ -106,26 +76,18 @@ update_alarm: max: 1440 mode: box time: - name: Time - description: Set time for the alarm. example: "07:00" selector: time: volume: - name: Volume - description: Set alarm volume level. selector: number: min: 0 max: 1 step: 0.01 enabled: - name: Alarm enabled - description: Enable or disable the alarm. selector: boolean: include_linked_zones: - name: Include linked zones - description: Enable or disable including grouped rooms. selector: boolean: diff --git a/homeassistant/components/sonos/strings.json b/homeassistant/components/sonos/strings.json index 75c1b850146aa7..c5b5136e9700ad 100644 --- a/homeassistant/components/sonos/strings.json +++ b/homeassistant/components/sonos/strings.json @@ -16,5 +16,95 @@ "title": "Networking error: subscriptions failed", "description": "Falling back to polling, functionality may be limited.\n\nSonos device at {device_ip} cannot reach Home Assistant at {listener_address}.\n\nSee our [documentation]({sub_fail_url}) for more information on how to solve this issue." } + }, + "services": { + "snapshot": { + "name": "Snapshot", + "description": "Takes a snapshot of the media player.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name of entity that will be snapshot." + }, + "with_group": { + "name": "With group", + "description": "True or False. Also snapshot the group layout." + } + } + }, + "restore": { + "name": "Restore", + "description": "Restores a snapshot of the media player.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name of entity that will be restored." + }, + "with_group": { + "name": "With group", + "description": "True or False. Also restore the group layout." + } + } + }, + "set_sleep_timer": { + "name": "Set timer", + "description": "Sets a Sonos timer.", + "fields": { + "sleep_time": { + "name": "Sleep Time", + "description": "Number of seconds to set the timer." + } + } + }, + "clear_sleep_timer": { + "name": "Clear timer", + "description": "Clears a Sonos timer." + }, + "play_queue": { + "name": "Play queue", + "description": "Start playing the queue from the first item.", + "fields": { + "queue_position": { + "name": "Queue position", + "description": "Position of the song in the queue to start playing from." + } + } + }, + "remove_from_queue": { + "name": "Remove from queue", + "description": "Removes an item from the queue.", + "fields": { + "queue_position": { + "name": "Queue position", + "description": "Position in the queue to remove." + } + } + }, + "update_alarm": { + "name": "Update alarm", + "description": "Updates an alarm with new time and volume settings.", + "fields": { + "alarm_id": { + "name": "Alarm ID", + "description": "ID for the alarm to be updated." + }, + "time": { + "name": "Time", + "description": "Set time for the alarm." + }, + "volume": { + "name": "Volume", + "description": "Set alarm volume level." + }, + "enabled": { + "name": "Alarm enabled", + "description": "Enable or disable the alarm." + }, + "include_linked_zones": { + "name": "Include linked zones", + "description": "Enable or disable including grouped rooms." + } + } + } } } diff --git a/homeassistant/components/soundtouch/services.yaml b/homeassistant/components/soundtouch/services.yaml index 8270905349665b..10ae15a3cb9621 100644 --- a/homeassistant/components/soundtouch/services.yaml +++ b/homeassistant/components/soundtouch/services.yaml @@ -1,10 +1,6 @@ play_everywhere: - name: Play everywhere - description: Play on all Bose SoundTouch devices. fields: master: - name: Master - description: Name of the master entity that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices required: true selector: entity: @@ -12,20 +8,14 @@ play_everywhere: domain: media_player create_zone: - name: Create zone - description: Create a SoundTouch multi-room zone. fields: master: - name: Master - description: Name of the master entity that will coordinate the multi-room zone. Platform dependent. required: true selector: entity: integration: soundtouch domain: media_player slaves: - name: Slaves - description: Name of slaves entities to add to the new zone. required: true selector: entity: @@ -34,20 +24,14 @@ create_zone: domain: media_player add_zone_slave: - name: Add zone slave - description: Add a slave to a SoundTouch multi-room zone. fields: master: - name: Master - description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. required: true selector: entity: integration: soundtouch domain: media_player slaves: - name: Slaves - description: Name of slaves entities to add to the existing zone. required: true selector: entity: @@ -56,20 +40,14 @@ add_zone_slave: domain: media_player remove_zone_slave: - name: Remove zone slave - description: Remove a slave from the SoundTouch multi-room zone. fields: master: - name: Master - description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. required: true selector: entity: integration: soundtouch domain: media_player slaves: - name: Slaves - description: Name of slaves entities to remove from the existing zone. required: true selector: entity: diff --git a/homeassistant/components/soundtouch/strings.json b/homeassistant/components/soundtouch/strings.json index 7ebcd4c528572c..616a4fc5a115fd 100644 --- a/homeassistant/components/soundtouch/strings.json +++ b/homeassistant/components/soundtouch/strings.json @@ -17,5 +17,59 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "services": { + "play_everywhere": { + "name": "Play everywhere", + "description": "Plays on all Bose SoundTouch devices.", + "fields": { + "master": { + "name": "Master", + "description": "Name of the master entity that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices." + } + } + }, + "create_zone": { + "name": "Create zone", + "description": "Creates a SoundTouch multi-room zone.", + "fields": { + "master": { + "name": "Master", + "description": "Name of the master entity that will coordinate the multi-room zone. Platform dependent." + }, + "slaves": { + "name": "Slaves", + "description": "Name of slaves entities to add to the new zone." + } + } + }, + "add_zone_slave": { + "name": "Add zone slave", + "description": "Adds a slave to a SoundTouch multi-room zone.", + "fields": { + "master": { + "name": "Master", + "description": "Name of the master entity that is coordinating the multi-room zone. Platform dependent." + }, + "slaves": { + "name": "Slaves", + "description": "Name of slaves entities to add to the existing zone." + } + } + }, + "remove_zone_slave": { + "name": "Remove zone slave", + "description": "Removes a slave from the SoundTouch multi-room zone.", + "fields": { + "master": { + "name": "Master", + "description": "Name of the master entity that is coordinating the multi-room zone. Platform dependent." + }, + "slaves": { + "name": "Slaves", + "description": "Name of slaves entities to remove from the existing zone." + } + } + } } } diff --git a/homeassistant/components/squeezebox/services.yaml b/homeassistant/components/squeezebox/services.yaml index 4c2d34ba88b327..90f9bf2d7695f0 100644 --- a/homeassistant/components/squeezebox/services.yaml +++ b/homeassistant/components/squeezebox/services.yaml @@ -1,69 +1,47 @@ call_method: - name: Call method - description: Call a custom Squeezebox JSONRPC API. target: entity: integration: squeezebox domain: media_player fields: command: - name: Command - description: Command to pass to Logitech Media Server (p0 in the CLI documentation). required: true example: "playlist" selector: text: parameters: - name: Parameters - description: > - Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation). example: '["loadtracks", "album.titlesearch=Revolver"]' advanced: true selector: object: call_query: - name: Call query - description: > - Call a custom Squeezebox JSONRPC API. Result will be stored in 'query_result' attribute of the Squeezebox entity. target: entity: integration: squeezebox domain: media_player fields: command: - name: Command - description: Command to pass to Logitech Media Server (p0 in the CLI documentation). required: true example: "albums" selector: text: parameters: - name: Parameters - description: > - Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation). example: '["0", "20", "search:Revolver"]' advanced: true selector: object: sync: - name: Sync - description: > - Add another player to this player's sync group. If the other player is already in a sync group, it will leave it. target: entity: integration: squeezebox domain: media_player fields: other_player: - name: Other player - description: Name of the other Squeezebox player to link. required: true example: "media_player.living_room" selector: text: unsync: - name: Unsync - description: Remove this player from its sync group. target: entity: integration: squeezebox diff --git a/homeassistant/components/squeezebox/strings.json b/homeassistant/components/squeezebox/strings.json index 4ae8d69bacd4b2..13fe16aa28c3d4 100644 --- a/homeassistant/components/squeezebox/strings.json +++ b/homeassistant/components/squeezebox/strings.json @@ -27,5 +27,49 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_server_found": "No LMS server found." } + }, + "services": { + "call_method": { + "name": "Call method", + "description": "Calls a custom Squeezebox JSONRPC API.", + "fields": { + "command": { + "name": "Command", + "description": "Command to pass to Logitech Media Server (p0 in the CLI documentation)." + }, + "parameters": { + "name": "Parameters", + "description": "Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation).\n." + } + } + }, + "call_query": { + "name": "Call query", + "description": "Calls a custom Squeezebox JSONRPC API. Result will be stored in 'query_result' attribute of the Squeezebox entity.\n.", + "fields": { + "command": { + "name": "Command", + "description": "Command to pass to Logitech Media Server (p0 in the CLI documentation)." + }, + "parameters": { + "name": "Parameters", + "description": "Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation).\n." + } + } + }, + "sync": { + "name": "Sync", + "description": "Adds another player to this player's sync group. If the other player is already in a sync group, it will leave it.\n.", + "fields": { + "other_player": { + "name": "Other player", + "description": "Name of the other Squeezebox player to link." + } + } + }, + "unsync": { + "name": "Unsync", + "description": "Removes this player from its sync group." + } } } diff --git a/homeassistant/components/starline/services.yaml b/homeassistant/components/starline/services.yaml index 4c3e4d360e814e..1d7041f0eb56cd 100644 --- a/homeassistant/components/starline/services.yaml +++ b/homeassistant/components/starline/services.yaml @@ -1,15 +1,7 @@ update_state: - name: Update state - description: > - Fetch the last state of the devices from the StarLine server. set_scan_interval: - name: Set scan interval - description: > - Set update frequency. fields: scan_interval: - name: Scan interval - description: Update frequency. selector: number: min: 10 @@ -17,13 +9,8 @@ set_scan_interval: step: 5 unit_of_measurement: seconds set_scan_obd_interval: - name: Set scan OBD interval - description: > - Set OBD info update frequency. fields: scan_interval: - name: Scan interval - description: Update frequency. selector: number: min: 180 diff --git a/homeassistant/components/starline/strings.json b/homeassistant/components/starline/strings.json index 10e99f9381440b..292ae55da1fff8 100644 --- a/homeassistant/components/starline/strings.json +++ b/homeassistant/components/starline/strings.json @@ -37,5 +37,31 @@ "error_auth_user": "Incorrect username or password", "error_auth_mfa": "Incorrect code" } + }, + "services": { + "update_state": { + "name": "Update state", + "description": "Fetches the last state of the devices from the StarLine server.\n." + }, + "set_scan_interval": { + "name": "Set scan interval", + "description": "Sets update frequency.", + "fields": { + "scan_interval": { + "name": "Scan interval", + "description": "Update frequency." + } + } + }, + "set_scan_obd_interval": { + "name": "Set scan OBD interval", + "description": "Sets OBD info update frequency.", + "fields": { + "scan_interval": { + "name": "Scan interval", + "description": "Update frequency." + } + } + } } } diff --git a/homeassistant/components/streamlabswater/services.yaml b/homeassistant/components/streamlabswater/services.yaml index b54c2cf15ebbee..7504a9111236f8 100644 --- a/homeassistant/components/streamlabswater/services.yaml +++ b/homeassistant/components/streamlabswater/services.yaml @@ -1,10 +1,6 @@ set_away_mode: - name: Set away mode - description: "Set the home/away mode for a Streamlabs Water Monitor." fields: away_mode: - name: Away mode - description: home or away required: true selector: select: diff --git a/homeassistant/components/streamlabswater/strings.json b/homeassistant/components/streamlabswater/strings.json new file mode 100644 index 00000000000000..56b35ab10442d4 --- /dev/null +++ b/homeassistant/components/streamlabswater/strings.json @@ -0,0 +1,14 @@ +{ + "services": { + "set_away_mode": { + "name": "Set away mode", + "description": "Sets the home/away mode for a Streamlabs Water Monitor.", + "fields": { + "away_mode": { + "name": "Away mode", + "description": "Home or away." + } + } + } + } +} diff --git a/homeassistant/components/subaru/services.yaml b/homeassistant/components/subaru/services.yaml index 58be48f9d18f26..bc760d2469ef39 100644 --- a/homeassistant/components/subaru/services.yaml +++ b/homeassistant/components/subaru/services.yaml @@ -1,14 +1,10 @@ unlock_specific_door: - name: Unlock Specific Door - description: Unlocks specific door(s) target: entity: domain: lock integration: subaru fields: door: - name: Door - description: "One of the following: 'all', 'driver', 'tailgate'" example: driver required: true selector: diff --git a/homeassistant/components/subaru/strings.json b/homeassistant/components/subaru/strings.json index abde396ba75584..2ce3c3835a64a7 100644 --- a/homeassistant/components/subaru/strings.json +++ b/homeassistant/components/subaru/strings.json @@ -46,7 +46,6 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } }, - "options": { "step": { "init": { @@ -57,5 +56,17 @@ } } } + }, + "services": { + "unlock_specific_door": { + "name": "Unlock specific door", + "description": "Unlocks specific door(s).", + "fields": { + "door": { + "name": "Door", + "description": "One of the following: 'all', 'driver', 'tailgate'." + } + } + } } } diff --git a/homeassistant/components/surepetcare/services.yaml b/homeassistant/components/surepetcare/services.yaml index 3c3919f5d01cbf..1d42c8fc102681 100644 --- a/homeassistant/components/surepetcare/services.yaml +++ b/homeassistant/components/surepetcare/services.yaml @@ -1,17 +1,11 @@ set_lock_state: - name: Set lock state - description: Sets lock state fields: flap_id: - name: Flap ID - description: Flap ID to lock/unlock required: true example: "123456" selector: text: lock_state: - name: Lock state - description: New lock state. required: true selector: select: @@ -22,17 +16,13 @@ set_lock_state: - "unlocked" set_pet_location: - name: Set pet location - description: Set pet location fields: pet_name: - description: Name of pet example: My_cat required: true selector: text: location: - description: Pet location (Inside or Outside) example: Inside required: true selector: diff --git a/homeassistant/components/surepetcare/strings.json b/homeassistant/components/surepetcare/strings.json index f7a539fe0e6e0d..6e1ec9643a737b 100644 --- a/homeassistant/components/surepetcare/strings.json +++ b/homeassistant/components/surepetcare/strings.json @@ -16,5 +16,35 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" } + }, + "services": { + "set_lock_state": { + "name": "Set lock state", + "description": "Sets lock state.", + "fields": { + "flap_id": { + "name": "Flap ID", + "description": "Flap ID to lock/unlock." + }, + "lock_state": { + "name": "Lock state", + "description": "New lock state." + } + } + }, + "set_pet_location": { + "name": "Set pet location", + "description": "Sets pet location.", + "fields": { + "pet_name": { + "name": "Pet name", + "description": "Name of pet." + }, + "location": { + "name": "Location", + "description": "Pet location (Inside or Outside)." + } + } + } } } diff --git a/homeassistant/components/switcher_kis/services.yaml b/homeassistant/components/switcher_kis/services.yaml index a7c3df5903e333..1dcb15fa4827b6 100644 --- a/homeassistant/components/switcher_kis/services.yaml +++ b/homeassistant/components/switcher_kis/services.yaml @@ -1,6 +1,4 @@ set_auto_off: - name: Set auto off - description: "Update Switcher device auto off setting." target: entity: integration: switcher_kis @@ -8,16 +6,12 @@ set_auto_off: device_class: switch fields: auto_off: - name: Auto off - description: "Time period string containing hours and minutes." required: true example: '"02:30"' selector: text: turn_on_with_timer: - name: Turn on with timer - description: "Turn on the Switcher device with timer." target: entity: integration: switcher_kis @@ -25,8 +19,6 @@ turn_on_with_timer: device_class: switch fields: timer_minutes: - name: Timer - description: "Time to turn on." required: true selector: number: diff --git a/homeassistant/components/switcher_kis/strings.json b/homeassistant/components/switcher_kis/strings.json index ad8f0f41ae7b29..4c4080a83943f3 100644 --- a/homeassistant/components/switcher_kis/strings.json +++ b/homeassistant/components/switcher_kis/strings.json @@ -9,5 +9,27 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } + }, + "services": { + "set_auto_off": { + "name": "Set auto off", + "description": "Updates Switcher device auto off setting.", + "fields": { + "auto_off": { + "name": "Auto off", + "description": "Time period string containing hours and minutes." + } + } + }, + "turn_on_with_timer": { + "name": "Turn on with timer", + "description": "Turns on the Switcher device with timer.", + "fields": { + "timer_minutes": { + "name": "Timer", + "description": "Time to turn on." + } + } + } } } diff --git a/homeassistant/components/synology_dsm/services.yaml b/homeassistant/components/synology_dsm/services.yaml index 245d45fc8007b2..32baeec11c191c 100644 --- a/homeassistant/components/synology_dsm/services.yaml +++ b/homeassistant/components/synology_dsm/services.yaml @@ -1,23 +1,15 @@ # synology-dsm service entries description. reboot: - name: Reboot - description: Reboot the NAS. This service is deprecated and will be removed in future release. Please use the corresponding button entity. fields: serial: - name: Serial - description: serial of the NAS to reboot; required when multiple NAS are configured. example: 1NDVC86409 selector: text: shutdown: - name: Shutdown - description: Shutdown the NAS. This service is deprecated and will be removed in future release. Please use the corresponding button entity. fields: serial: - name: Serial - description: serial of the NAS to shutdown; required when multiple NAS are configured. example: 1NDVC86409 selector: text: diff --git a/homeassistant/components/synology_dsm/strings.json b/homeassistant/components/synology_dsm/strings.json index 92903b1d2aec2e..24ed1aaf568cb1 100644 --- a/homeassistant/components/synology_dsm/strings.json +++ b/homeassistant/components/synology_dsm/strings.json @@ -63,48 +63,130 @@ }, "entity": { "binary_sensor": { - "disk_below_remain_life_thr": { "name": "Below min remaining life" }, - "disk_exceed_bad_sector_thr": { "name": "Exceeded max bad sectors" }, - "status": { "name": "Security status" } + "disk_below_remain_life_thr": { + "name": "Below min remaining life" + }, + "disk_exceed_bad_sector_thr": { + "name": "Exceeded max bad sectors" + }, + "status": { + "name": "Security status" + } }, "sensor": { - "cpu_15min_load": { "name": "CPU load average (15 min)" }, - "cpu_1min_load": { "name": "CPU load average (1 min)" }, - "cpu_5min_load": { "name": "CPU load average (5 min)" }, - "cpu_other_load": { "name": "CPU utilization (other)" }, - "cpu_system_load": { "name": "CPU utilization (system)" }, - "cpu_total_load": { "name": "CPU utilization (total)" }, - "cpu_user_load": { "name": "CPU utilization (user)" }, - "disk_smart_status": { "name": "Status (smart)" }, - "disk_status": { "name": "Status" }, + "cpu_15min_load": { + "name": "CPU load average (15 min)" + }, + "cpu_1min_load": { + "name": "CPU load average (1 min)" + }, + "cpu_5min_load": { + "name": "CPU load average (5 min)" + }, + "cpu_other_load": { + "name": "CPU utilization (other)" + }, + "cpu_system_load": { + "name": "CPU utilization (system)" + }, + "cpu_total_load": { + "name": "CPU utilization (total)" + }, + "cpu_user_load": { + "name": "CPU utilization (user)" + }, + "disk_smart_status": { + "name": "Status (smart)" + }, + "disk_status": { + "name": "Status" + }, "disk_temp": { "name": "[%key:component::sensor::entity_component::temperature::name%]" }, - "memory_available_real": { "name": "Memory available (real)" }, - "memory_available_swap": { "name": "Memory available (swap)" }, - "memory_cached": { "name": "Memory cached" }, - "memory_real_usage": { "name": "Memory usage (real)" }, - "memory_size": { "name": "Memory size" }, - "memory_total_real": { "name": "Memory total (real)" }, - "memory_total_swap": { "name": "Memory total (swap)" }, - "network_down": { "name": "Download throughput" }, - "network_up": { "name": "Upload throughput" }, + "memory_available_real": { + "name": "Memory available (real)" + }, + "memory_available_swap": { + "name": "Memory available (swap)" + }, + "memory_cached": { + "name": "Memory cached" + }, + "memory_real_usage": { + "name": "Memory usage (real)" + }, + "memory_size": { + "name": "Memory size" + }, + "memory_total_real": { + "name": "Memory total (real)" + }, + "memory_total_swap": { + "name": "Memory total (swap)" + }, + "network_down": { + "name": "Download throughput" + }, + "network_up": { + "name": "Upload throughput" + }, "temperature": { "name": "[%key:component::sensor::entity_component::temperature::name%]" }, - "uptime": { "name": "Last boot" }, - "volume_disk_temp_avg": { "name": "Average disk temp" }, - "volume_disk_temp_max": { "name": "Maximum disk temp" }, - "volume_percentage_used": { "name": "Volume used" }, - "volume_size_total": { "name": "Total size" }, - "volume_size_used": { "name": "Used space" }, - "volume_status": { "name": "Status" } + "uptime": { + "name": "Last boot" + }, + "volume_disk_temp_avg": { + "name": "Average disk temp" + }, + "volume_disk_temp_max": { + "name": "Maximum disk temp" + }, + "volume_percentage_used": { + "name": "Volume used" + }, + "volume_size_total": { + "name": "Total size" + }, + "volume_size_used": { + "name": "Used space" + }, + "volume_status": { + "name": "Status" + } }, "switch": { - "home_mode": { "name": "Home mode" } + "home_mode": { + "name": "Home mode" + } }, "update": { - "update": { "name": "DSM update" } + "update": { + "name": "DSM update" + } + } + }, + "services": { + "reboot": { + "name": "Reboot", + "description": "Reboots the NAS. This service is deprecated and will be removed in future release. Please use the corresponding button entity.", + "fields": { + "serial": { + "name": "Serial", + "description": "Serial of the NAS to reboot; required when multiple NAS are configured." + } + } + }, + "shutdown": { + "name": "Shutdown", + "description": "Shutdowns the NAS. This service is deprecated and will be removed in future release. Please use the corresponding button entity.", + "fields": { + "serial": { + "name": "Serial", + "description": "Serial of the NAS to shutdown; required when multiple NAS are configured." + } + } } } } diff --git a/homeassistant/components/system_bridge/services.yaml b/homeassistant/components/system_bridge/services.yaml index d33235ffba46b0..78d6e87f2184a4 100644 --- a/homeassistant/components/system_bridge/services.yaml +++ b/homeassistant/components/system_bridge/services.yaml @@ -1,71 +1,47 @@ open_path: - name: Open Path - description: Open a file on the server using the default application. fields: bridge: - name: Bridge - description: The server to talk to. required: true selector: device: integration: system_bridge path: - name: Path - description: Path to open. required: true example: "C:\\test\\image.png" selector: text: open_url: - name: Open URL - description: Open a URL on the server using the default application. fields: bridge: - name: Bridge - description: The server to talk to. required: true selector: device: integration: system_bridge url: - name: URL - description: URL to open. required: true example: "https://www.home-assistant.io" selector: text: send_keypress: - name: Send Keyboard Keypress - description: Sends a keyboard keypress. fields: bridge: - name: Bridge - description: The server to send the command to. required: true selector: device: integration: system_bridge key: - name: Key - description: "Key to press. List available here: http://robotjs.io/docs/syntax#keys" required: true example: "audio_play" selector: text: send_text: - name: Send Keyboard Text - description: Sends text for the server to type. fields: bridge: - name: Bridge - description: The server to send the command to. required: true selector: device: integration: system_bridge text: - name: Text - description: "Text to type." required: true example: "Hello world" selector: diff --git a/homeassistant/components/system_bridge/strings.json b/homeassistant/components/system_bridge/strings.json index 209bce9078ad82..e4b2b40637cf4d 100644 --- a/homeassistant/components/system_bridge/strings.json +++ b/homeassistant/components/system_bridge/strings.json @@ -27,5 +27,63 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" } + }, + "services": { + "open_path": { + "name": "Open path", + "description": "Opens a file on the server using the default application.", + "fields": { + "bridge": { + "name": "Bridge", + "description": "The server to talk to." + }, + "path": { + "name": "Path", + "description": "Path to open." + } + } + }, + "open_url": { + "name": "Open URL", + "description": "Opens a URL on the server using the default application.", + "fields": { + "bridge": { + "name": "Bridge", + "description": "The server to talk to." + }, + "url": { + "name": "URL", + "description": "URL to open." + } + } + }, + "send_keypress": { + "name": "Send keyboard keypress", + "description": "Sends a keyboard keypress.", + "fields": { + "bridge": { + "name": "Bridge", + "description": "The server to send the command to." + }, + "key": { + "name": "Key", + "description": "Key to press. List available here: http://robotjs.io/docs/syntax#keys." + } + } + }, + "send_text": { + "name": "Send keyboard text", + "description": "Sends text for the server to type.", + "fields": { + "bridge": { + "name": "Bridge", + "description": "The server to send the command to." + }, + "text": { + "name": "Text", + "description": "Text to type." + } + } + } } } From 78a8f904887e461688038de187f7427a72265364 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Jul 2023 15:20:41 -1000 Subject: [PATCH 0395/1009] Add additional tplink kasa OUI (#96383) Found on another test device --- homeassistant/components/tplink/manifest.json | 4 ++++ homeassistant/generated/dhcp.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 0a9b0254f91d0c..581360050535f9 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -143,6 +143,10 @@ { "hostname": "k[lp]*", "macaddress": "54AF97*" + }, + { + "hostname": "k[lp]*", + "macaddress": "AC15A2*" } ], "documentation": "https://www.home-assistant.io/integrations/tplink", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 05b53acba5ff84..63a0bb43d2aa1c 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -765,6 +765,11 @@ "hostname": "k[lp]*", "macaddress": "54AF97*", }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "AC15A2*", + }, { "domain": "tuya", "macaddress": "105A17*", From 8d360611d1bb9046295340d6b8e51f00648ae786 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 07:36:51 +0200 Subject: [PATCH 0396/1009] Migrate integration services (W-Z) to support translations (#96381) --- .../components/wake_on_lan/services.yaml | 8 - .../components/wake_on_lan/strings.json | 22 ++ .../components/webostv/services.yaml | 25 -- homeassistant/components/webostv/strings.json | 48 ++++ homeassistant/components/wemo/services.yaml | 6 - homeassistant/components/wemo/strings.json | 16 ++ .../components/wilight/services.yaml | 14 - homeassistant/components/wilight/strings.json | 36 +++ homeassistant/components/wled/services.yaml | 16 -- homeassistant/components/wled/strings.json | 38 +++ .../components/xiaomi_aqara/services.yaml | 28 -- .../components/xiaomi_aqara/strings.json | 54 ++++ .../components/xiaomi_miio/services.yaml | 98 ------- .../components/xiaomi_miio/strings.json | 266 ++++++++++++++++++ homeassistant/components/yamaha/services.yaml | 14 - homeassistant/components/yamaha/strings.json | 38 +++ .../components/yeelight/services.yaml | 95 ++----- .../components/yeelight/strings.json | 133 +++++++++ .../components/zoneminder/services.yaml | 4 - .../components/zoneminder/strings.json | 14 + 20 files changed, 686 insertions(+), 287 deletions(-) create mode 100644 homeassistant/components/wake_on_lan/strings.json create mode 100644 homeassistant/components/yamaha/strings.json create mode 100644 homeassistant/components/zoneminder/strings.json diff --git a/homeassistant/components/wake_on_lan/services.yaml b/homeassistant/components/wake_on_lan/services.yaml index ea374a88b8f196..48d3df5c4f952f 100644 --- a/homeassistant/components/wake_on_lan/services.yaml +++ b/homeassistant/components/wake_on_lan/services.yaml @@ -1,23 +1,15 @@ send_magic_packet: - name: Send magic packet - description: Send a 'magic packet' to wake up a device with 'Wake-On-LAN' capabilities. fields: mac: - name: MAC address - description: MAC address of the device to wake up. required: true example: "aa:bb:cc:dd:ee:ff" selector: text: broadcast_address: - name: Broadcast address - description: Broadcast IP where to send the magic packet. example: 192.168.255.255 selector: text: broadcast_port: - name: Broadcast port - description: Port where to send the magic packet. default: 9 selector: number: diff --git a/homeassistant/components/wake_on_lan/strings.json b/homeassistant/components/wake_on_lan/strings.json new file mode 100644 index 00000000000000..8395bc7503a6c4 --- /dev/null +++ b/homeassistant/components/wake_on_lan/strings.json @@ -0,0 +1,22 @@ +{ + "services": { + "send_magic_packet": { + "name": "Send magic packet", + "description": "Sends a 'magic packet' to wake up a device with 'Wake-On-LAN' capabilities.", + "fields": { + "mac": { + "name": "MAC address", + "description": "MAC address of the device to wake up." + }, + "broadcast_address": { + "name": "Broadcast address", + "description": "Broadcast IP where to send the magic packet." + }, + "broadcast_port": { + "name": "Broadcast port", + "description": "Port where to send the magic packet." + } + } + } + } +} diff --git a/homeassistant/components/webostv/services.yaml b/homeassistant/components/webostv/services.yaml index 1985857d128bf8..c3297dd8902e32 100644 --- a/homeassistant/components/webostv/services.yaml +++ b/homeassistant/components/webostv/services.yaml @@ -1,52 +1,33 @@ # Describes the format for available webostv services button: - name: Button - description: "Send a button press command." fields: entity_id: - name: Entity - description: Name(s) of the webostv entities where to run the API method. required: true selector: entity: integration: webostv domain: media_player button: - name: Button - description: >- - Name of the button to press. Known possible values are - LEFT, RIGHT, DOWN, UP, HOME, MENU, BACK, ENTER, DASH, INFO, ASTERISK, CC, EXIT, - MUTE, RED, GREEN, BLUE, YELLOW, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN, - PLAY, PAUSE, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 required: true example: "LEFT" selector: text: command: - name: Command - description: "Send a command." fields: entity_id: - name: Entity - description: Name(s) of the webostv entities where to run the API method. required: true selector: entity: integration: webostv domain: media_player command: - name: Command - description: Endpoint of the command. required: true example: "system.launcher/open" selector: text: payload: - name: Payload - description: >- - An optional payload to provide to the endpoint in the format of key value pair(s). example: >- target: https://www.google.com advanced: true @@ -54,20 +35,14 @@ command: object: select_sound_output: - name: Select Sound Output - description: "Send the TV the command to change sound output." fields: entity_id: - name: Entity - description: Name(s) of the webostv entities to change sound output on. required: true selector: entity: integration: webostv domain: media_player sound_output: - name: Sound Output - description: Name of the sound output to switch to. required: true example: "external_speaker" selector: diff --git a/homeassistant/components/webostv/strings.json b/homeassistant/components/webostv/strings.json index c623effe22b6b1..985edb05645a5a 100644 --- a/homeassistant/components/webostv/strings.json +++ b/homeassistant/components/webostv/strings.json @@ -48,5 +48,53 @@ "trigger_type": { "webostv.turn_on": "Device is requested to turn on" } + }, + "services": { + "button": { + "name": "Button", + "description": "Sends a button press command.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name(s) of the webostv entities where to run the API method." + }, + "button": { + "name": "Button", + "description": "Name of the button to press. Known possible values are LEFT, RIGHT, DOWN, UP, HOME, MENU, BACK, ENTER, DASH, INFO, ASTERISK, CC, EXIT, MUTE, RED, GREEN, BLUE, YELLOW, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN, PLAY, PAUSE, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9." + } + } + }, + "command": { + "name": "Command", + "description": "Sends a command.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name(s) of the webostv entities where to run the API method." + }, + "command": { + "name": "Command", + "description": "Endpoint of the command." + }, + "payload": { + "name": "Payload", + "description": "An optional payload to provide to the endpoint in the format of key value pair(s)." + } + } + }, + "select_sound_output": { + "name": "Select sound output", + "description": "Sends the TV the command to change sound output.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name(s) of the webostv entities to change sound output on." + }, + "sound_output": { + "name": "Sound output", + "description": "Name of the sound output to switch to." + } + } + } } } diff --git a/homeassistant/components/wemo/services.yaml b/homeassistant/components/wemo/services.yaml index 58305798cf9194..59f38ca77a0a09 100644 --- a/homeassistant/components/wemo/services.yaml +++ b/homeassistant/components/wemo/services.yaml @@ -1,14 +1,10 @@ set_humidity: - name: Set humidity - description: Set the target humidity of WeMo humidifier devices. target: entity: integration: wemo domain: fan fields: target_humidity: - name: Target humidity - description: Target humidity. required: true selector: number: @@ -18,8 +14,6 @@ set_humidity: unit_of_measurement: "%" reset_filter_life: - name: Reset filter life - description: Reset the WeMo Humidifier's filter life to 100%. target: entity: integration: wemo diff --git a/homeassistant/components/wemo/strings.json b/homeassistant/components/wemo/strings.json index b218f7589853a7..66fa656ebfe6b4 100644 --- a/homeassistant/components/wemo/strings.json +++ b/homeassistant/components/wemo/strings.json @@ -28,5 +28,21 @@ "trigger_type": { "long_press": "Wemo button was pressed for 2 seconds" } + }, + "services": { + "set_humidity": { + "name": "Set humidity", + "description": "Sets the target humidity of WeMo humidifier devices.", + "fields": { + "target_humidity": { + "name": "Target humidity", + "description": "Target humidity." + } + } + }, + "reset_filter_life": { + "name": "Reset filter life", + "description": "Resets the WeMo Humidifier's filter life to 100%." + } } } diff --git a/homeassistant/components/wilight/services.yaml b/homeassistant/components/wilight/services.yaml index b6c538bf9fb749..044a46784ef3b1 100644 --- a/homeassistant/components/wilight/services.yaml +++ b/homeassistant/components/wilight/services.yaml @@ -1,31 +1,17 @@ set_watering_time: - name: Set watering time - description: Sets time for watering target: fields: watering_time: - name: Duration - description: Duration for this irrigation to be turned on. example: 30 set_pause_time: - name: Set pause time - description: Sets time to pause. target: fields: pause_time: - name: Duration - description: Duration for this irrigation to be paused. example: 24 set_trigger: - name: Set trigger - description: Set the trigger to use. target: fields: trigger_index: - name: Trigger index - description: Index of Trigger from 1 to 4 example: "1" trigger: - name: Trigger rules - description: Configuration of trigger. example: "'12707001'" diff --git a/homeassistant/components/wilight/strings.json b/homeassistant/components/wilight/strings.json index 0449a900c29ead..a287104e7adb5c 100644 --- a/homeassistant/components/wilight/strings.json +++ b/homeassistant/components/wilight/strings.json @@ -11,5 +11,41 @@ "not_supported_device": "This WiLight is currently not supported", "not_wilight_device": "This Device is not WiLight" } + }, + "services": { + "set_watering_time": { + "name": "Set watering time", + "description": "Sets time for watering.", + "fields": { + "watering_time": { + "name": "Duration", + "description": "Duration for this irrigation to be turned on." + } + } + }, + "set_pause_time": { + "name": "Set pause time", + "description": "Sets time to pause.", + "fields": { + "pause_time": { + "name": "Duration", + "description": "Duration for this irrigation to be paused." + } + } + }, + "set_trigger": { + "name": "Set trigger", + "description": "Sets the trigger to use.", + "fields": { + "trigger_index": { + "name": "Trigger index", + "description": "Index of Trigger from 1 to 4." + }, + "trigger": { + "name": "Trigger rules", + "description": "Configuration of trigger." + } + } + } } } diff --git a/homeassistant/components/wled/services.yaml b/homeassistant/components/wled/services.yaml index 9ca73fac0a3e1c..40170fd54e98e9 100644 --- a/homeassistant/components/wled/services.yaml +++ b/homeassistant/components/wled/services.yaml @@ -1,55 +1,39 @@ effect: - name: Set effect - description: Control the effect settings of WLED. target: entity: integration: wled domain: light fields: effect: - name: Effect - description: Name or ID of the WLED light effect. example: "Rainbow" selector: text: intensity: - name: Effect intensity - description: Intensity of the effect. Number between 0 and 255. selector: number: min: 0 max: 255 palette: - name: Color palette - description: Name or ID of the WLED light palette. example: "Tiamat" selector: text: speed: - name: Effect speed - description: Speed of the effect. selector: number: min: 0 max: 255 reverse: - name: Reverse effect - description: Reverse the effect. Either true to reverse or false otherwise. default: false selector: boolean: preset: - name: Set preset (deprecated) - description: Set a preset for the WLED device. target: entity: integration: wled domain: light fields: preset: - name: Preset ID - description: ID of the WLED preset selector: number: min: -1 diff --git a/homeassistant/components/wled/strings.json b/homeassistant/components/wled/strings.json index eed62ab04994a1..9fc6573b112d25 100644 --- a/homeassistant/components/wled/strings.json +++ b/homeassistant/components/wled/strings.json @@ -41,5 +41,43 @@ } } } + }, + "services": { + "effect": { + "name": "Set effect", + "description": "Controls the effect settings of WLED.", + "fields": { + "effect": { + "name": "Effect", + "description": "Name or ID of the WLED light effect." + }, + "intensity": { + "name": "Effect intensity", + "description": "Intensity of the effect. Number between 0 and 255." + }, + "palette": { + "name": "Color palette", + "description": "Name or ID of the WLED light palette." + }, + "speed": { + "name": "Effect speed", + "description": "Speed of the effect." + }, + "reverse": { + "name": "Reverse effect", + "description": "Reverse the effect. Either true to reverse or false otherwise." + } + } + }, + "preset": { + "name": "Set preset (deprecated)", + "description": "Sets a preset for the WLED device.", + "fields": { + "preset": { + "name": "Preset ID", + "description": "ID of the WLED preset." + } + } + } } } diff --git a/homeassistant/components/xiaomi_aqara/services.yaml b/homeassistant/components/xiaomi_aqara/services.yaml index 75a9b9156c1590..dcf79ebc215124 100644 --- a/homeassistant/components/xiaomi_aqara/services.yaml +++ b/homeassistant/components/xiaomi_aqara/services.yaml @@ -1,70 +1,42 @@ add_device: - name: Add device - description: - Enables the join permission of the Xiaomi Aqara Gateway for 30 seconds. - A new device can be added afterwards by pressing the pairing button once. fields: gw_mac: - name: Gateway MAC - description: MAC address of the Xiaomi Aqara Gateway. required: true example: 34ce00880088 selector: text: play_ringtone: - name: play ringtone - description: - Play a specific ringtone. The version of the gateway firmware must - be 1.4.1_145 at least. fields: gw_mac: - name: Gateway MAC - description: MAC address of the Xiaomi Aqara Gateway. required: true example: 34ce00880088 selector: text: ringtone_id: - name: Ringtone ID - description: One of the allowed ringtone ids. required: true example: 8 selector: text: ringtone_vol: - name: Ringtone volume - description: The volume in percent. selector: number: min: 0 max: 100 remove_device: - name: Remove device - description: - Removes a specific device. The removal is required if a device shall - be paired with another gateway. fields: device_id: - name: Device ID - description: Hardware address of the device to remove. required: true example: 158d0000000000 selector: text: gw_mac: - name: Gateway MAC - description: MAC address of the Xiaomi Aqara Gateway. required: true example: 34ce00880088 selector: text: stop_ringtone: - name: Stop ringtone - description: Stops a playing ringtone immediately. fields: gw_mac: - name: Gateway MAC - description: MAC address of the Xiaomi Aqara Gateway. required: true example: 34ce00880088 selector: diff --git a/homeassistant/components/xiaomi_aqara/strings.json b/homeassistant/components/xiaomi_aqara/strings.json index 63fb48542c9311..0944c91fd830f4 100644 --- a/homeassistant/components/xiaomi_aqara/strings.json +++ b/homeassistant/components/xiaomi_aqara/strings.json @@ -37,5 +37,59 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "not_xiaomi_aqara": "Not a Xiaomi Aqara Gateway, discovered device did not match known gateways" } + }, + "services": { + "add_device": { + "name": "Add device", + "description": "Enables the join permission of the Xiaomi Aqara Gateway for 30 seconds. A new device can be added afterwards by pressing the pairing button once.", + "fields": { + "gw_mac": { + "name": "Gateway MAC", + "description": "MAC address of the Xiaomi Aqara Gateway." + } + } + }, + "play_ringtone": { + "name": "Play ringtone", + "description": "Plays a specific ringtone. The version of the gateway firmware must be 1.4.1_145 at least.", + "fields": { + "gw_mac": { + "name": "Gateway MAC", + "description": "MAC address of the Xiaomi Aqara Gateway." + }, + "ringtone_id": { + "name": "Ringtone ID", + "description": "One of the allowed ringtone ids." + }, + "ringtone_vol": { + "name": "Ringtone volume", + "description": "The volume in percent." + } + } + }, + "remove_device": { + "name": "Remove device", + "description": "Removes a specific device. The removal is required if a device shall be paired with another gateway.", + "fields": { + "device_id": { + "name": "Device ID", + "description": "Hardware address of the device to remove." + }, + "gw_mac": { + "name": "Gateway MAC", + "description": "MAC address of the Xiaomi Aqara Gateway." + } + } + }, + "stop_ringtone": { + "name": "Stop ringtone", + "description": "Stops a playing ringtone immediately.", + "fields": { + "gw_mac": { + "name": "Gateway MAC", + "description": "MAC address of the Xiaomi Aqara Gateway." + } + } + } } } diff --git a/homeassistant/components/xiaomi_miio/services.yaml b/homeassistant/components/xiaomi_miio/services.yaml index e1cf03ba4ee1e8..0b3bd6435e414d 100644 --- a/homeassistant/components/xiaomi_miio/services.yaml +++ b/homeassistant/components/xiaomi_miio/services.yaml @@ -1,27 +1,19 @@ fan_reset_filter: - name: Fan reset filter - description: Reset the filter lifetime and usage. fields: entity_id: - description: Name of the xiaomi miio entity. selector: entity: integration: xiaomi_miio domain: fan fan_set_extra_features: - name: Fan set extra features - description: Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called "turbo mode" is unlocked in the app on value 1. fields: entity_id: - description: Name of the xiaomi miio entity. selector: entity: integration: xiaomi_miio domain: fan features: - name: Features - description: Integer, known values are 0 (default) and 1 (turbo mode). required: true selector: number: @@ -29,18 +21,13 @@ fan_set_extra_features: max: 1 light_set_scene: - name: Light set scene - description: Set a fixed scene. fields: entity_id: - description: Name of the light entity. selector: entity: integration: xiaomi_miio domain: light scene: - name: Scene - description: Number of the fixed scene. required: true selector: number: @@ -48,108 +35,79 @@ light_set_scene: max: 6 light_set_delayed_turn_off: - name: Light set delayed turn off - description: Delayed turn off. fields: entity_id: - description: Name of the light entity. selector: entity: integration: xiaomi_miio domain: light time_period: - name: Time period - description: Time period for the delayed turn off. required: true example: "5, '0:05', {'minutes': 5}" selector: object: light_reminder_on: - name: Light reminder on - description: Enable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). fields: entity_id: - description: "Name of the entity to act on." selector: entity: integration: xiaomi_miio domain: light light_reminder_off: - name: Light reminder off - description: Disable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). fields: entity_id: - description: "Name of the entity to act on." selector: entity: integration: xiaomi_miio domain: light light_night_light_mode_on: - name: Night light mode on - description: Turn the eyecare mode on (EYECARE SMART LAMP 2 ONLY). fields: entity_id: - description: "Name of the entity to act on." selector: entity: integration: xiaomi_miio domain: light light_night_light_mode_off: - name: Night light mode off - description: Turn the eyecare mode fan_set_dry_off (EYECARE SMART LAMP 2 ONLY). fields: entity_id: - description: "Name of the entity to act on." selector: entity: integration: xiaomi_miio domain: light light_eyecare_mode_on: - name: Light eyecare mode on - description: Enable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). fields: entity_id: - description: "Name of the entity to act on." selector: entity: integration: xiaomi_miio domain: light light_eyecare_mode_off: - name: Light eyecare mode off - description: Disable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). fields: entity_id: - description: "Name of the entity to act on." selector: entity: integration: xiaomi_miio domain: light remote_learn_command: - name: Remote learn command - description: 'Learn an IR command, press "Call Service", point the remote at the IR device, and the learned command will be shown as a notification in Overview.' target: entity: integration: xiaomi_miio domain: remote fields: slot: - name: Slot - description: "Define the slot used to save the IR command." default: 1 selector: number: min: 1 max: 1000000 timeout: - name: Timeout - description: "Define the timeout, before which the command must be learned." default: 10 selector: number: @@ -158,56 +116,41 @@ remote_learn_command: unit_of_measurement: seconds remote_set_led_on: - name: Remote set LED on - description: "Turn on blue LED." target: entity: integration: xiaomi_miio domain: remote remote_set_led_off: - name: Remote set LED off - description: "Turn off blue LED." target: entity: integration: xiaomi_miio domain: remote switch_set_wifi_led_on: - name: Switch set Wi-fi LED on - description: Turn the wifi led on. fields: entity_id: - description: Name of the xiaomi miio entity. selector: entity: integration: xiaomi_miio domain: switch switch_set_wifi_led_off: - name: Switch set Wi-fi LED off - description: Turn the wifi led off. fields: entity_id: - description: Name of the xiaomi miio entity. selector: entity: integration: xiaomi_miio domain: switch switch_set_power_price: - name: Switch set power price - description: Set the power price. fields: entity_id: - description: Name of the xiaomi miio entity. selector: entity: integration: xiaomi_miio domain: switch mode: - name: Mode - description: Power price. required: true selector: number: @@ -215,18 +158,13 @@ switch_set_power_price: max: 999 switch_set_power_mode: - name: Switch set power mode - description: Set the power mode. fields: entity_id: - description: Name of the xiaomi miio entity. selector: entity: integration: xiaomi_miio domain: switch mode: - name: Mode - description: Power mode. required: true selector: select: @@ -235,48 +173,36 @@ switch_set_power_mode: - "normal" vacuum_remote_control_start: - name: Vacuum remote control start - description: Start remote control of the vacuum cleaner. You can then move it with `remote_control_move`, when done call `remote_control_stop`. target: entity: integration: xiaomi_miio domain: vacuum vacuum_remote_control_stop: - name: Vacuum remote control stop - description: Stop remote control mode of the vacuum cleaner. target: entity: integration: xiaomi_miio domain: vacuum vacuum_remote_control_move: - name: Vacuum remote control move - description: Remote control the vacuum cleaner, make sure you first set it in remote control mode with `remote_control_start`. target: entity: integration: xiaomi_miio domain: vacuum fields: velocity: - name: Velocity - description: Speed. selector: number: min: -0.29 max: 0.29 step: 0.01 rotation: - name: Rotation - description: Rotation, between -179 degrees and 179 degrees. selector: number: min: -179 max: 179 unit_of_measurement: "°" duration: - name: Duration - description: Duration of the movement. selector: number: min: 1 @@ -284,32 +210,24 @@ vacuum_remote_control_move: unit_of_measurement: seconds vacuum_remote_control_move_step: - name: Vacuum remote control move step - description: Remote control the vacuum cleaner, only makes one move and then stops. target: entity: integration: xiaomi_miio domain: vacuum fields: velocity: - name: Velocity - description: Speed. selector: number: min: -0.29 max: 0.29 step: 0.01 rotation: - name: Rotation - description: Rotation. selector: number: min: -179 max: 179 unit_of_measurement: "°" duration: - name: Duration - description: Duration of the movement. selector: number: min: 1 @@ -317,59 +235,43 @@ vacuum_remote_control_move_step: unit_of_measurement: seconds vacuum_clean_zone: - name: Vacuum clean zone - description: Start the cleaning operation in the selected areas for the number of repeats indicated. target: entity: integration: xiaomi_miio domain: vacuum fields: zone: - name: Zone - description: Array of zones. Each zone is an array of 4 integer values. example: "[[23510,25311,25110,26362]]" selector: object: repeats: - name: Repeats - description: Number of cleaning repeats for each zone. selector: number: min: 1 max: 3 vacuum_goto: - name: Vacuum go to - description: Go to the specified coordinates. target: entity: integration: xiaomi_miio domain: vacuum fields: x_coord: - name: X coordinate - description: x-coordinate. example: 27500 selector: text: y_coord: - name: Y coordinate - description: y-coordinate. example: 32000 selector: text: vacuum_clean_segment: - name: Vacuum clean segment - description: Start cleaning of the specified segment(s). target: entity: integration: xiaomi_miio domain: vacuum fields: segments: - name: Segments - description: Segments. example: "[1,2]" selector: object: diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index 15c89498bc7b45..578d2a96ff83f4 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -94,5 +94,271 @@ } } } + }, + "services": { + "fan_reset_filter": { + "name": "Fan reset filter", + "description": "Resets the filter lifetime and usage.", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Name of the xiaomi miio entity." + } + } + }, + "fan_set_extra_features": { + "name": "Fan set extra features", + "description": "Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called \"turbo mode\" is unlocked in the app on value 1.", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Name of the xiaomi miio entity." + }, + "features": { + "name": "Features", + "description": "Integer, known values are 0 (default) and 1 (turbo mode)." + } + } + }, + "light_set_scene": { + "name": "Light set scene", + "description": "Sets a fixed scene.", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Name of the light entity." + }, + "scene": { + "name": "Scene", + "description": "Number of the fixed scene." + } + } + }, + "light_set_delayed_turn_off": { + "name": "Light set delayed turn off", + "description": "Delayed turn off.", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Name of the light entity." + }, + "time_period": { + "name": "Time period", + "description": "Time period for the delayed turn off." + } + } + }, + "light_reminder_on": { + "name": "Light reminder on", + "description": "Enables the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY).", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Name of the entity to act on." + } + } + }, + "light_reminder_off": { + "name": "Light reminder off", + "description": "Disables the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY).", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Name of the entity to act on." + } + } + }, + "light_night_light_mode_on": { + "name": "Night light mode on", + "description": "Turns the eyecare mode on (EYECARE SMART LAMP 2 ONLY).", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Name of the entity to act on." + } + } + }, + "light_night_light_mode_off": { + "name": "Night light mode off", + "description": "Turns the eyecare mode fan_set_dry_off (EYECARE SMART LAMP 2 ONLY).", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Name of the entity to act on." + } + } + }, + "light_eyecare_mode_on": { + "name": "Light eyecare mode on", + "description": "Enables the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY).", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Name of the entity to act on." + } + } + }, + "light_eyecare_mode_off": { + "name": "Light eyecare mode off", + "description": "Disables the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY).", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Name of the entity to act on." + } + } + }, + "remote_learn_command": { + "name": "Remote learn command", + "description": "Learns an IR command, press \"Call Service\", point the remote at the IR device, and the learned command will be shown as a notification in Overview.", + "fields": { + "slot": { + "name": "Slot", + "description": "Define the slot used to save the IR command." + }, + "timeout": { + "name": "Timeout", + "description": "Define the timeout, before which the command must be learned." + } + } + }, + "remote_set_led_on": { + "name": "Remote set LED on", + "description": "Turns on blue LED." + }, + "remote_set_led_off": { + "name": "Remote set LED off", + "description": "Turns off blue LED." + }, + "switch_set_wifi_led_on": { + "name": "Switch set Wi-Fi LED on", + "description": "Turns the wifi led on.", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Name of the xiaomi miio entity." + } + } + }, + "switch_set_wifi_led_off": { + "name": "Switch set Wi-Fi LED off", + "description": "Turn the Wi-Fi led off.", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Name of the xiaomi miio entity." + } + } + }, + "switch_set_power_price": { + "name": "Switch set power price", + "description": "Sets the power price.", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Name of the xiaomi miio entity." + }, + "mode": { + "name": "Mode", + "description": "Power price." + } + } + }, + "switch_set_power_mode": { + "name": "Switch set power mode", + "description": "Sets the power mode.", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Name of the xiaomi miio entity." + }, + "mode": { + "name": "Mode", + "description": "Power mode." + } + } + }, + "vacuum_remote_control_start": { + "name": "Vacuum remote control start", + "description": "Starts remote control of the vacuum cleaner. You can then move it with `remote_control_move`, when done call `remote_control_stop`." + }, + "vacuum_remote_control_stop": { + "name": "Vacuum remote control stop", + "description": "Stops remote control mode of the vacuum cleaner." + }, + "vacuum_remote_control_move": { + "name": "Vacuum remote control move", + "description": "Remote controls the vacuum cleaner, make sure you first set it in remote control mode with `remote_control_start`.", + "fields": { + "velocity": { + "name": "Velocity", + "description": "Speed." + }, + "rotation": { + "name": "Rotation", + "description": "Rotation, between -179 degrees and 179 degrees." + }, + "duration": { + "name": "Duration", + "description": "Duration of the movement." + } + } + }, + "vacuum_remote_control_move_step": { + "name": "Vacuum remote control move step", + "description": "Remote controls the vacuum cleaner, only makes one move and then stops.", + "fields": { + "velocity": { + "name": "Velocity", + "description": "Speed." + }, + "rotation": { + "name": "Rotation", + "description": "Rotation." + }, + "duration": { + "name": "Duration", + "description": "Duration of the movement." + } + } + }, + "vacuum_clean_zone": { + "name": "Vacuum clean zone", + "description": "Starts the cleaning operation in the selected areas for the number of repeats indicated.", + "fields": { + "zone": { + "name": "Zone", + "description": "Array of zones. Each zone is an array of 4 integer values." + }, + "repeats": { + "name": "Repeats", + "description": "Number of cleaning repeats for each zone." + } + } + }, + "vacuum_goto": { + "name": "Vacuum go to", + "description": "Go to the specified coordinates.", + "fields": { + "x_coord": { + "name": "X coordinate", + "description": "X-coordinate." + }, + "y_coord": { + "name": "Y coordinate", + "description": "Y-coordinate." + } + } + }, + "vacuum_clean_segment": { + "name": "Vacuum clean segment", + "description": "Starts cleaning of the specified segment(s).", + "fields": { + "segments": { + "name": "Segments", + "description": "Segments." + } + } + } } } diff --git a/homeassistant/components/yamaha/services.yaml b/homeassistant/components/yamaha/services.yaml index 8d25d5925c13db..705f2996a3cd8d 100644 --- a/homeassistant/components/yamaha/services.yaml +++ b/homeassistant/components/yamaha/services.yaml @@ -1,49 +1,35 @@ enable_output: - name: Enable output - description: Enable or disable an output port target: entity: integration: yamaha domain: media_player fields: port: - name: Port - description: Name of port to enable/disable. required: true example: "hdmi1" selector: text: enabled: - name: Enabled - description: Indicate if port should be enabled or not. required: true selector: boolean: menu_cursor: - name: Menu cursor - description: Control the cursor in a menu target: entity: integration: yamaha domain: media_player fields: cursor: - name: Cursor - description: Name of the cursor key to press ('up', 'down', 'left', 'right', 'select', 'return') example: down selector: text: select_scene: - name: Select scene - description: "Select a scene on the receiver" target: entity: integration: yamaha domain: media_player fields: scene: - name: Scene - description: Name of the scene. Standard for RX-V437 is 'BD/DVD Movie Viewing', 'TV Viewing', 'NET Audio Listening' or 'Radio Listening' required: true example: "TV Viewing" selector: diff --git a/homeassistant/components/yamaha/strings.json b/homeassistant/components/yamaha/strings.json new file mode 100644 index 00000000000000..0896f43b1b515e --- /dev/null +++ b/homeassistant/components/yamaha/strings.json @@ -0,0 +1,38 @@ +{ + "services": { + "enable_output": { + "name": "Enable output", + "description": "Enables or disables an output port.", + "fields": { + "port": { + "name": "Port", + "description": "Name of port to enable/disable." + }, + "enabled": { + "name": "Enabled", + "description": "Indicate if port should be enabled or not." + } + } + }, + "menu_cursor": { + "name": "Menu cursor", + "description": "Controls the cursor in a menu.", + "fields": { + "cursor": { + "name": "Cursor", + "description": "Name of the cursor key to press ('up', 'down', 'left', 'right', 'select', 'return')." + } + } + }, + "select_scene": { + "name": "Select scene", + "description": "Selects a scene on the receiver.", + "fields": { + "scene": { + "name": "Scene", + "description": "Name of the scene. Standard for RX-V437 is 'BD/DVD Movie Viewing', 'TV Viewing', 'NET Audio Listening' or 'Radio Listening'." + } + } + } + } +} diff --git a/homeassistant/components/yeelight/services.yaml b/homeassistant/components/yeelight/services.yaml index d7850b34607546..ccfd46ef6804bb 100644 --- a/homeassistant/components/yeelight/services.yaml +++ b/homeassistant/components/yeelight/services.yaml @@ -1,85 +1,60 @@ set_mode: - name: Set mode - description: Set a operation mode. target: entity: integration: yeelight domain: light fields: mode: - name: Mode - description: Operation mode. required: true selector: select: options: - - label: "Color Flow" - value: "color_flow" - - label: "HSV" - value: "hsv" - - label: "Last" - value: "last" - - label: "Moonlight" - value: "moonlight" - - label: "Normal" - value: "normal" - - label: "RGB" - value: "rgb" + - "color_flow" + - "hsv" + - "last" + - "moonlight" + - "normal" + - "rgb" + translation_key: mode set_color_scene: - name: Set color scene - description: Changes the light to the specified RGB color and brightness. If the light is off, it will be turned on. target: entity: integration: yeelight domain: light fields: rgb_color: - name: RGB color - description: Color for the light in RGB-format. example: "[255, 100, 100]" selector: object: brightness: - name: Brightness - description: The brightness value to set. selector: number: min: 0 max: 100 unit_of_measurement: "%" set_hsv_scene: - name: Set HSV scene - description: Changes the light to the specified HSV color and brightness. If the light is off, it will be turned on. target: entity: integration: yeelight domain: light fields: hs_color: - name: Hue/sat color - description: Color for the light in hue/sat format. Hue is 0-359 and Sat is 0-100. example: "[300, 70]" selector: object: brightness: - name: Brightness - description: The brightness value to set. selector: number: min: 0 max: 100 unit_of_measurement: "%" set_color_temp_scene: - name: Set color temperature scene - description: Changes the light to the specified color temperature. If the light is off, it will be turned on. target: entity: integration: yeelight domain: light fields: kelvin: - name: Kelvin - description: Color temperature for the light in Kelvin. selector: number: min: 1700 @@ -87,118 +62,90 @@ set_color_temp_scene: step: 100 unit_of_measurement: K brightness: - name: Brightness - description: The brightness value to set. selector: number: min: 0 max: 100 unit_of_measurement: "%" set_color_flow_scene: - name: Set color flow scene - description: starts a color flow. If the light is off, it will be turned on. target: entity: integration: yeelight domain: light fields: count: - name: Count - description: The number of times to run this flow (0 to run forever). default: 0 selector: number: min: 0 max: 100 action: - name: Action - description: The action to take after the flow stops. default: "recover" selector: select: options: - - label: "Off" - value: "off" - - label: "Recover" - value: "recover" - - label: "Stay" - value: "stay" + - "off" + - "recover" + - "stay" + translation_key: action transitions: - name: Transitions - description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html - example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]' + example: + '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": + [1900, 1000, 10] }]' selector: object: set_auto_delay_off_scene: - name: Set auto delay off scene - description: Turns the light on to the specified brightness and sets a timer to turn it back off after the given number of minutes. If the light is off, Set a color scene, if light is off, it will be turned on. target: entity: integration: yeelight domain: light fields: minutes: - name: Minutes - description: The time to wait before automatically turning the light off. selector: number: min: 1 max: 60 unit_of_measurement: minutes brightness: - name: Brightness - description: The brightness value to set. selector: number: min: 0 max: 100 unit_of_measurement: "%" start_flow: - name: Start flow - description: Start a custom flow, using transitions from https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects target: entity: integration: yeelight domain: light fields: count: - name: Count - description: The number of times to run this flow (0 to run forever). default: 0 selector: number: min: 0 max: 100 action: - name: Action - description: The action to take after the flow stops. default: "recover" selector: select: options: - - label: "Off" - value: "off" - - label: "Recover" - value: "recover" - - label: "Stay" - value: "stay" + - "off" + - "recover" + - "stay" + translation_key: action transitions: - name: Transitions - description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html - example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]' + example: + '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": + [1900, 1000, 10] }]' selector: object: set_music_mode: - name: Set music mode - description: Enable or disable music_mode target: entity: integration: yeelight domain: light fields: music_mode: - name: Music mode - description: Use true or false to enable / disable music_mode required: true selector: boolean: diff --git a/homeassistant/components/yeelight/strings.json b/homeassistant/components/yeelight/strings.json index 0ecbd134b6af94..18b762057a78bb 100644 --- a/homeassistant/components/yeelight/strings.json +++ b/homeassistant/components/yeelight/strings.json @@ -37,5 +37,138 @@ } } } + }, + "services": { + "set_mode": { + "name": "Set mode", + "description": "Sets a operation mode.", + "fields": { + "mode": { + "name": "Mode", + "description": "Operation mode." + } + } + }, + "set_color_scene": { + "name": "Set color scene", + "description": "Changes the light to the specified RGB color and brightness. If the light is off, it will be turned on.", + "fields": { + "rgb_color": { + "name": "RGB color", + "description": "Color for the light in RGB-format." + }, + "brightness": { + "name": "Brightness", + "description": "The brightness value to set." + } + } + }, + "set_hsv_scene": { + "name": "Set HSV scene", + "description": "Changes the light to the specified HSV color and brightness. If the light is off, it will be turned on.", + "fields": { + "hs_color": { + "name": "Hue/sat color", + "description": "Color for the light in hue/sat format. Hue is 0-359 and Sat is 0-100." + }, + "brightness": { + "name": "Brightness", + "description": "The brightness value to set." + } + } + }, + "set_color_temp_scene": { + "name": "Set color temperature scene", + "description": "Changes the light to the specified color temperature. If the light is off, it will be turned on.", + "fields": { + "kelvin": { + "name": "Kelvin", + "description": "Color temperature for the light in Kelvin." + }, + "brightness": { + "name": "Brightness", + "description": "The brightness value to set." + } + } + }, + "set_color_flow_scene": { + "name": "Set color flow scene", + "description": "Starts a color flow. If the light is off, it will be turned on.", + "fields": { + "count": { + "name": "Count", + "description": "The number of times to run this flow (0 to run forever)." + }, + "action": { + "name": "Action", + "description": "The action to take after the flow stops." + }, + "transitions": { + "name": "Transitions", + "description": "Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html." + } + } + }, + "set_auto_delay_off_scene": { + "name": "Set auto delay off scene", + "description": "Turns the light on to the specified brightness and sets a timer to turn it back off after the given number of minutes. If the light is off, Set a color scene, if light is off, it will be turned on.", + "fields": { + "minutes": { + "name": "Minutes", + "description": "The time to wait before automatically turning the light off." + }, + "brightness": { + "name": "Brightness", + "description": "The brightness value to set." + } + } + }, + "start_flow": { + "name": "Start flow", + "description": "Start a custom flow, using transitions from https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects.", + "fields": { + "count": { + "name": "Count", + "description": "The number of times to run this flow (0 to run forever)." + }, + "action": { + "name": "Action", + "description": "The action to take after the flow stops." + }, + "transitions": { + "name": "Transitions", + "description": "Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html." + } + } + }, + "set_music_mode": { + "name": "Set music mode", + "description": "Enables or disables music_mode.", + "fields": { + "music_mode": { + "name": "Music mode", + "description": "Use true or false to enable / disable music_mode." + } + } + } + }, + "selector": { + "mode": { + "options": { + "color_flow": "Color Flow", + "hsv": "HSV", + "last": "Last", + "moonlight": "Moonlight", + "normal": "Normal", + "rgb": "RGB" + } + }, + "action": { + "options": { + "off": "Off", + "recover": "Recover", + "stay": "Stay" + } + } } } diff --git a/homeassistant/components/zoneminder/services.yaml b/homeassistant/components/zoneminder/services.yaml index 74ab0cf594585b..30e6672957da20 100644 --- a/homeassistant/components/zoneminder/services.yaml +++ b/homeassistant/components/zoneminder/services.yaml @@ -1,10 +1,6 @@ set_run_state: - name: Set run state - description: Set the ZoneMinder run state fields: name: - name: Name - description: The string name of the ZoneMinder run state to set as active. required: true example: "Home" selector: diff --git a/homeassistant/components/zoneminder/strings.json b/homeassistant/components/zoneminder/strings.json new file mode 100644 index 00000000000000..1e2e41d274107e --- /dev/null +++ b/homeassistant/components/zoneminder/strings.json @@ -0,0 +1,14 @@ +{ + "services": { + "set_run_state": { + "name": "Set run state", + "description": "Sets the ZoneMinder run state.", + "fields": { + "name": { + "name": "Name", + "description": "The string name of the ZoneMinder run state to set as active." + } + } + } + } +} From 4edec696376393976bcd165f10f90249c1b46daf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 07:37:13 +0200 Subject: [PATCH 0397/1009] Migrate integration services (T-V) to support translations (#96379) --- homeassistant/components/tado/services.yaml | 18 - homeassistant/components/tado/strings.json | 44 ++ .../components/telegram/services.yaml | 2 - .../components/telegram/strings.json | 8 + .../components/telegram_bot/services.yaml | 327 +--------- .../components/telegram_bot/strings.json | 596 ++++++++++++++++++ homeassistant/components/timer/services.yaml | 12 - homeassistant/components/timer/strings.json | 34 + .../components/todoist/services.yaml | 24 - homeassistant/components/todoist/strings.json | 54 ++ homeassistant/components/toon/services.yaml | 4 - homeassistant/components/toon/strings.json | 12 + .../components/totalconnect/services.yaml | 4 - .../components/totalconnect/strings.json | 10 + homeassistant/components/tplink/services.yaml | 32 - homeassistant/components/tplink/strings.json | 94 +++ .../components/transmission/services.yaml | 26 - .../components/transmission/strings.json | 62 ++ homeassistant/components/unifi/services.yaml | 6 - homeassistant/components/unifi/strings.json | 16 + .../components/unifiprotect/services.yaml | 25 - .../components/unifiprotect/strings.json | 58 ++ homeassistant/components/upb/services.yaml | 38 -- homeassistant/components/upb/strings.json | 88 +++ .../components/utility_meter/services.yaml | 6 - .../components/utility_meter/strings.json | 16 + homeassistant/components/vallox/services.yaml | 12 - homeassistant/components/vallox/strings.json | 32 + homeassistant/components/velbus/services.yaml | 30 - homeassistant/components/velbus/strings.json | 54 ++ homeassistant/components/velux/services.yaml | 2 - homeassistant/components/velux/strings.json | 8 + .../components/verisure/services.yaml | 6 - .../components/verisure/strings.json | 14 + homeassistant/components/vesync/services.yaml | 2 - homeassistant/components/vesync/strings.json | 6 + homeassistant/components/vicare/services.yaml | 4 - homeassistant/components/vicare/strings.json | 12 + homeassistant/components/vizio/services.yaml | 12 - homeassistant/components/vizio/strings.json | 20 + 40 files changed, 1272 insertions(+), 558 deletions(-) create mode 100644 homeassistant/components/telegram/strings.json create mode 100644 homeassistant/components/telegram_bot/strings.json create mode 100644 homeassistant/components/todoist/strings.json create mode 100644 homeassistant/components/velux/strings.json diff --git a/homeassistant/components/tado/services.yaml b/homeassistant/components/tado/services.yaml index 211ae4cd1ff79a..0f66798f864c5e 100644 --- a/homeassistant/components/tado/services.yaml +++ b/homeassistant/components/tado/services.yaml @@ -1,14 +1,10 @@ set_climate_timer: - name: Set climate timer - description: Turn on climate entities for a set time. target: entity: integration: tado domain: climate fields: temperature: - name: Temperature - description: Temperature to set climate entity to required: true selector: number: @@ -17,15 +13,11 @@ set_climate_timer: step: 0.5 unit_of_measurement: "°" time_period: - name: Time period - description: Choose this or Overlay. Set the time period for the change if you want to be specific. Alternatively use Overlay required: false example: "01:30:00" selector: text: requested_overlay: - name: Overlay - description: Choose this or Time Period. Allows you to choose an overlay. MANUAL:=Overlay until user removes; NEXT_TIME_BLOCK:=Overlay until next timeblock; TADO_DEFAULT:=Overlay based on tado app setting required: false example: "MANUAL" selector: @@ -36,24 +28,18 @@ set_climate_timer: - "TADO_DEFAULT" set_water_heater_timer: - name: Set water heater timer - description: Turn on water heater for a set time. target: entity: integration: tado domain: water_heater fields: time_period: - name: Time period - description: Set the time period for the boost. required: true example: "01:30:00" default: "01:00:00" selector: text: temperature: - name: Temperature - description: Temperature to set heater to selector: number: min: 0 @@ -62,16 +48,12 @@ set_water_heater_timer: unit_of_measurement: "°" set_climate_temperature_offset: - name: Set climate temperature offset - description: Set the temperature offset of climate entities target: entity: integration: tado domain: climate fields: offset: - name: Offset - description: Offset you would like (depending on your device). default: 0 selector: number: diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index 3decfe3cd0c925..70ff38b10be719 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -42,5 +42,49 @@ } } } + }, + "services": { + "set_climate_timer": { + "name": "Set climate timer", + "description": "Turns on climate entities for a set time.", + "fields": { + "temperature": { + "name": "Temperature", + "description": "Temperature to set climate entity to." + }, + "time_period": { + "name": "Time period", + "description": "Choose this or Overlay. Set the time period for the change if you want to be specific. Alternatively use Overlay." + }, + "requested_overlay": { + "name": "Overlay", + "description": "Choose this or Time Period. Allows you to choose an overlay. MANUAL:=Overlay until user removes; NEXT_TIME_BLOCK:=Overlay until next timeblock; TADO_DEFAULT:=Overlay based on tado app setting." + } + } + }, + "set_water_heater_timer": { + "name": "Set water heater timer", + "description": "Turns on water heater for a set time.", + "fields": { + "time_period": { + "name": "Time period", + "description": "Set the time period for the boost." + }, + "temperature": { + "name": "Temperature", + "description": "Temperature to set heater to." + } + } + }, + "set_climate_temperature_offset": { + "name": "Set climate temperature offset", + "description": "Sets the temperature offset of climate entities.", + "fields": { + "offset": { + "name": "Offset", + "description": "Offset you would like (depending on your device)." + } + } + } } } diff --git a/homeassistant/components/telegram/services.yaml b/homeassistant/components/telegram/services.yaml index bbdd82768f596e..c983a105c93977 100644 --- a/homeassistant/components/telegram/services.yaml +++ b/homeassistant/components/telegram/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload telegram notify services. diff --git a/homeassistant/components/telegram/strings.json b/homeassistant/components/telegram/strings.json new file mode 100644 index 00000000000000..9e09a3904cd702 --- /dev/null +++ b/homeassistant/components/telegram/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "reload": { + "name": "Reload", + "description": "Reloads telegram notify services." + } + } +} diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index 31876bd542d912..cdb50d55943ba4 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -1,31 +1,21 @@ # Describes the format for available Telegram bot services send_message: - name: Send message - description: Send a notification. fields: message: - name: Message - description: Message body of the notification. required: true example: The garage door has been open for 10 minutes. selector: text: title: - name: Title - description: Optional title for your notification. Will be composed as '%title\n%message' example: "Your Garage Door Friend" selector: text: target: - name: Target - description: An array of pre-authorized chat_ids to send the notification to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" selector: object: parse_mode: - name: Parse mode - description: "Parser for the message text." selector: select: options: @@ -33,18 +23,12 @@ send_message: - "markdown" - "markdown2" disable_notification: - name: Disable notification - description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. selector: boolean: disable_web_page_preview: - name: Disable web page preview - description: Disables link previews for links in the message. selector: boolean: timeout: - name: Timeout - description: Timeout for send message. Will help with timeout errors (poor internet connection, etc)s selector: number: min: 1 @@ -52,61 +36,44 @@ send_message: unit_of_measurement: seconds keyboard: - name: Keyboard - description: List of rows of commands, comma-separated, to make a custom keyboard. Empty list clears a previously set keyboard. example: '["/command1, /command2", "/command3"]' selector: object: inline_keyboard: - name: Inline keyboard - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. - example: '["/button1, /button2", "/button3"] or ["Text button1:/button1, Text button2:/button2", "Text button3:/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + example: + '["/button1, /button2", "/button3"] or ["Text button1:/button1, Text + button2:/button2", "Text button3:/button3"] or [[["Text button1", "/button1"], + ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' selector: object: message_tag: - name: Message tag - description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}" example: "msg_to_edit" selector: text: send_photo: - name: Send photo - description: Send a photo. fields: url: - name: URL - description: Remote path to an image. example: "http://example.org/path/to/the/image.png" selector: text: file: - name: File - description: Local path to an image. example: "/path/to/the/image.png" selector: text: caption: - name: Caption - description: The title of the image. example: "My image" selector: text: username: - name: Username - description: Username for a URL which require HTTP authentication. example: myuser selector: text: password: - name: Password - description: Password (or bearer token) for a URL which require HTTP authentication. example: myuser_pwd selector: text: authentication: - name: Authentication method - description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`. default: digest selector: select: @@ -114,14 +81,10 @@ send_photo: - "digest" - "bearer_token" target: - name: Target - description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" selector: object: parse_mode: - name: Parse mode - description: "Parser for the message text." selector: select: options: @@ -129,79 +92,55 @@ send_photo: - "markdown" - "markdown2" disable_notification: - name: Disable notification - description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. selector: boolean: verify_ssl: - name: Verify SSL - description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. selector: boolean: timeout: - name: Timeout - description: Timeout for send photo. Will help with timeout errors (poor internet connection, etc) selector: number: min: 1 max: 3600 unit_of_measurement: seconds keyboard: - name: Keyboard - description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' selector: object: inline_keyboard: - name: Inline keyboard - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. - example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + example: + '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], + ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' selector: object: message_tag: - name: Message tag - description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}" example: "msg_to_edit" selector: text: send_sticker: - name: Send sticker - description: Send a sticker. fields: url: - name: URL - description: Remote path to a static .webp or animated .tgs sticker. example: "http://example.org/path/to/the/sticker.webp" selector: text: file: - name: File - description: Local path to a static .webp or animated .tgs sticker. example: "/path/to/the/sticker.webp" selector: text: sticker_id: - name: Sticker ID - description: ID of a sticker that exists on telegram servers example: CAACAgIAAxkBAAEDDldhZD-hqWclr6krLq-FWSfCrGNmOQAC9gAD9HsZAAFeYY-ltPYnrCEE selector: text: username: - name: Username - description: Username for a URL which require HTTP authentication. example: myuser selector: text: password: - name: Password - description: Password (or bearer token) for a URL which require HTTP authentication. example: myuser_pwd selector: text: authentication: - name: Authentication method - description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`. default: digest selector: select: @@ -209,85 +148,59 @@ send_sticker: - "digest" - "bearer_token" target: - name: Target - description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" selector: object: disable_notification: - name: Disable notification - description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. selector: boolean: verify_ssl: - name: Verify SSL - description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. selector: boolean: timeout: - name: Timeout - description: Timeout for send sticker. Will help with timeout errors (poor internet connection, etc) selector: number: min: 1 max: 3600 unit_of_measurement: seconds keyboard: - name: Keyboard - description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' selector: object: inline_keyboard: - name: Inline keyboard - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. - example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + example: + '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], + ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' selector: object: message_tag: - name: Message tag - description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}" example: "msg_to_edit" selector: text: send_animation: - name: Send animation - description: Send an anmiation. fields: url: - name: URL - description: Remote path to a GIF or H.264/MPEG-4 AVC video without sound. example: "http://example.org/path/to/the/animation.gif" selector: text: file: - name: File - description: Local path to a GIF or H.264/MPEG-4 AVC video without sound. example: "/path/to/the/animation.gif" selector: text: caption: - name: Caption - description: The title of the animation. example: "My animation" selector: text: username: - name: Username - description: Username for a URL which require HTTP authentication. example: myuser selector: text: password: - name: Password - description: Password (or bearer token) for a URL which require HTTP authentication. example: myuser_pwd selector: text: authentication: - name: Authentication method - description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`. default: digest selector: select: @@ -295,14 +208,10 @@ send_animation: - "digest" - "bearer_token" target: - name: Target - description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" selector: object: parse_mode: - name: Parse Mode - description: "Parser for the message text." selector: select: options: @@ -310,73 +219,51 @@ send_animation: - "markdown" - "markdown2" disable_notification: - name: Disable notification - description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. selector: boolean: verify_ssl: - name: Verify SSL - description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. selector: boolean: timeout: - name: Timeout - description: Timeout for send sticker. Will help with timeout errors (poor internet connection, etc) selector: number: min: 1 max: 3600 unit_of_measurement: seconds keyboard: - name: Keyboard - description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' selector: object: inline_keyboard: - name: Inline keyboard - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. - example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + example: + '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], + ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' selector: object: send_video: - name: Send video - description: Send a video. fields: url: - name: URL - description: Remote path to a video. example: "http://example.org/path/to/the/video.mp4" selector: text: file: - name: File - description: Local path to a video. example: "/path/to/the/video.mp4" selector: text: caption: - name: Caption - description: The title of the video. example: "My video" selector: text: username: - name: Username - description: Username for a URL which require HTTP authentication. example: myuser selector: text: password: - name: Password - description: Password (or bearer token) for a URL which require HTTP authentication. example: myuser_pwd selector: text: authentication: - name: Authentication method - description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`. default: digest selector: select: @@ -384,14 +271,10 @@ send_video: - "digest" - "bearer_token" target: - name: Target - description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" selector: object: parse_mode: - name: Parse mode - description: "Parser for the message text." selector: select: options: @@ -399,79 +282,55 @@ send_video: - "markdown" - "markdown2" disable_notification: - name: Disable notification - description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. selector: boolean: verify_ssl: - name: Verify SSL - description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. selector: boolean: timeout: - name: Timeout - description: Timeout for send video. Will help with timeout errors (poor internet connection, etc) selector: number: min: 1 max: 3600 unit_of_measurement: seconds keyboard: - name: Keyboard - description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' selector: object: inline_keyboard: - name: Inline keyboard - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. - example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + example: + '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], + ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' selector: object: message_tag: - name: Message tag - description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}" example: "msg_to_edit" selector: text: send_voice: - name: Send voice - description: Send a voice message. fields: url: - name: URL - description: Remote path to a voice message. example: "http://example.org/path/to/the/voice.opus" selector: text: file: - name: File - description: Local path to a voice message. example: "/path/to/the/voice.opus" selector: text: caption: - name: Caption - description: The title of the voice message. example: "My microphone recording" selector: text: username: - name: Username - description: Username for a URL which require HTTP authentication. example: myuser selector: text: password: - name: Password - description: Password (or bearer token) for a URL which require HTTP authentication. example: myuser_pwd selector: text: authentication: - name: Authentication method - description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`. default: digest selector: select: @@ -479,85 +338,59 @@ send_voice: - "digest" - "bearer_token" target: - name: Target - description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" selector: object: disable_notification: - name: Disable notification - description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. selector: boolean: verify_ssl: - name: Verify SSL - description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. selector: boolean: timeout: - name: Timeout - description: Timeout for send voice. Will help with timeout errors (poor internet connection, etc) selector: number: min: 1 max: 3600 unit_of_measurement: seconds keyboard: - name: Keyboard - description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' selector: object: inline_keyboard: - name: Inline keyboard - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. - example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + example: + '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], + ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' selector: object: message_tag: - name: Message tag - description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}" example: "msg_to_edit" selector: text: send_document: - name: Send document - description: Send a document. fields: url: - name: URL - description: Remote path to a document. example: "http://example.org/path/to/the/document.odf" selector: text: file: - name: File - description: Local path to a document. example: "/tmp/whatever.odf" selector: text: caption: - name: Caption - description: The title of the document. example: Document Title xy selector: text: username: - name: Username - description: Username for a URL which require HTTP authentication. example: myuser selector: text: password: - name: Password - description: Password (or bearer token) for a URL which require HTTP authentication. example: myuser_pwd selector: text: authentication: - name: Authentication method - description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`. default: digest selector: select: @@ -565,14 +398,10 @@ send_document: - "digest" - "bearer_token" target: - name: Target - description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" selector: object: parse_mode: - name: Parse mode - description: "Parser for the message text." selector: select: options: @@ -580,49 +409,35 @@ send_document: - "markdown" - "markdown2" disable_notification: - name: Disable notification - description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. selector: boolean: verify_ssl: - name: Verify SSL - description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. selector: boolean: timeout: - name: Timeout - description: Timeout for send document. Will help with timeout errors (poor internet connection, etc) selector: number: min: 1 max: 3600 unit_of_measurement: seconds keyboard: - name: Keyboard - description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' selector: object: inline_keyboard: - name: Inline keyboard - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. - example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + example: + '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], + ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' selector: object: message_tag: - name: Message tag - description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}" example: "msg_to_edit" selector: text: send_location: - name: Send location - description: Send a location. fields: latitude: - name: Latitude - description: The latitude to send. required: true selector: number: @@ -631,8 +446,6 @@ send_location: step: 0.001 unit_of_measurement: "°" longitude: - name: Longitude - description: The longitude to send. required: true selector: number: @@ -641,91 +454,63 @@ send_location: step: 0.001 unit_of_measurement: "°" target: - name: Target - description: An array of pre-authorized chat_ids to send the location to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" selector: object: disable_notification: - name: Disable notification - description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. selector: boolean: timeout: - name: Timeout - description: Timeout for send photo. Will help with timeout errors (poor internet connection, etc) selector: number: min: 1 max: 3600 unit_of_measurement: seconds keyboard: - name: Keyboard - description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' selector: object: inline_keyboard: - name: Inline keyboard - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. - example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + example: + '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], + ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' selector: object: message_tag: - name: Message tag - description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}" example: "msg_to_edit" selector: text: send_poll: - name: Send poll - description: Send a poll. fields: target: - name: Target - description: An array of pre-authorized chat_ids to send the location to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" selector: object: question: - name: Question - description: Poll question, 1-300 characters required: true selector: text: options: - name: Options - description: List of answer options, 2-10 strings 1-100 characters each required: true selector: object: is_anonymous: - name: Is Anonymous - description: If the poll needs to be anonymous, defaults to True selector: boolean: allows_multiple_answers: - name: Allow Multiple Answers - description: If the poll allows multiple answers, defaults to False selector: boolean: open_period: - name: Open Period - description: Amount of time in seconds the poll will be active after creation, 5-600. selector: number: min: 5 max: 600 unit_of_measurement: seconds disable_notification: - name: Disable notification - description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. selector: boolean: timeout: - name: Timeout - description: Timeout for send poll. Will help with timeout errors (poor internet connection, etc) selector: number: min: 1 @@ -733,38 +518,26 @@ send_poll: unit_of_measurement: seconds edit_message: - name: Edit message - description: Edit a previously sent message. fields: message_id: - name: Message ID - description: id of the message to edit. required: true example: "{{ trigger.event.data.message.message_id }}" selector: text: chat_id: - name: Chat ID - description: The chat_id where to edit the message. required: true example: 12345 selector: text: message: - name: Message - description: Message body of the notification. example: The garage door has been open for 10 minutes. selector: text: title: - name: Title - description: Optional title for your notification. Will be composed as '%title\n%message' example: "Your Garage Door Friend" selector: text: parse_mode: - name: Parse mode - description: "Parser for the message text." selector: select: options: @@ -772,102 +545,76 @@ edit_message: - "markdown" - "markdown2" disable_web_page_preview: - name: Disable web page preview - description: Disables link previews for links in the message. selector: boolean: inline_keyboard: - name: Inline keyboard - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. - example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + example: + '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], + ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' selector: object: edit_caption: - name: Edit caption - description: Edit the caption of a previously sent message. fields: message_id: - name: Message ID - description: id of the message to edit. required: true example: "{{ trigger.event.data.message.message_id }}" selector: text: chat_id: - name: Chat ID - description: The chat_id where to edit the caption. required: true example: 12345 selector: text: caption: - name: Caption - description: Message body of the notification. required: true example: The garage door has been open for 10 minutes. selector: text: inline_keyboard: - name: Inline keyboard - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. - example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + example: + '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], + ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' selector: object: edit_replymarkup: - name: Edit reply markup - description: Edit the inline keyboard of a previously sent message. fields: message_id: - name: Message ID - description: id of the message to edit. required: true example: "{{ trigger.event.data.message.message_id }}" selector: text: chat_id: - name: Chat ID - description: The chat_id where to edit the reply_markup. required: true example: 12345 selector: text: inline_keyboard: - name: Inline keyboard - description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. required: true - example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + example: + '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], + ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' selector: object: answer_callback_query: - name: Answer callback query - description: Respond to a callback query originated by clicking on an online keyboard button. The answer will be displayed to the user as a notification at the top of the chat screen or as an alert. fields: message: - name: Message - description: Unformatted text message body of the notification. required: true example: "OK, I'm listening" selector: text: callback_query_id: - name: Callback query ID - description: Unique id of the callback response. required: true example: "{{ trigger.event.data.id }}" selector: text: show_alert: - name: Show alert - description: Show a permanent notification. required: true selector: boolean: timeout: - name: Timeout - description: Timeout for sending the answer. Will help with timeout errors (poor internet connection, etc) selector: number: min: 1 @@ -875,19 +622,13 @@ answer_callback_query: unit_of_measurement: seconds delete_message: - name: Delete message - description: Delete a previously sent message. fields: message_id: - name: Message ID - description: id of the message to delete. required: true example: "{{ trigger.event.data.message.message_id }}" selector: text: chat_id: - name: Chat ID - description: The chat_id where to delete the message. required: true example: 12345 selector: diff --git a/homeassistant/components/telegram_bot/strings.json b/homeassistant/components/telegram_bot/strings.json new file mode 100644 index 00000000000000..8104fdd285e369 --- /dev/null +++ b/homeassistant/components/telegram_bot/strings.json @@ -0,0 +1,596 @@ +{ + "services": { + "send_message": { + "name": "Send message", + "description": "Sends a notification.", + "fields": { + "message": { + "name": "Message", + "description": "Message body of the notification." + }, + "title": { + "name": "Title", + "description": "Optional title for your notification. Will be composed as '%title\\n%message'." + }, + "target": { + "name": "Target", + "description": "An array of pre-authorized chat_ids to send the notification to. If not present, first allowed chat_id is the default." + }, + "parse_mode": { + "name": "Parse mode", + "description": "Parser for the message text." + }, + "disable_notification": { + "name": "Disable notification", + "description": "Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound." + }, + "disable_web_page_preview": { + "name": "Disable web page preview", + "description": "Disables link previews for links in the message." + }, + "timeout": { + "name": "Timeout", + "description": "Timeout for send message. Will help with timeout errors (poor internet connection, etc)s." + }, + "keyboard": { + "name": "Keyboard", + "description": "List of rows of commands, comma-separated, to make a custom keyboard. Empty list clears a previously set keyboard." + }, + "inline_keyboard": { + "name": "Inline keyboard", + "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + }, + "message_tag": { + "name": "Message tag", + "description": "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}." + } + } + }, + "send_photo": { + "name": "Send photo", + "description": "Sends a photo.", + "fields": { + "url": { + "name": "URL", + "description": "Remote path to an image." + }, + "file": { + "name": "File", + "description": "Local path to an image." + }, + "caption": { + "name": "Caption", + "description": "The title of the image." + }, + "username": { + "name": "Username", + "description": "Username for a URL which require HTTP authentication." + }, + "password": { + "name": "Password", + "description": "Password (or bearer token) for a URL which require HTTP authentication." + }, + "authentication": { + "name": "Authentication method", + "description": "Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`." + }, + "target": { + "name": "Target", + "description": "An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default." + }, + "parse_mode": { + "name": "Parse mode", + "description": "Parser for the message text." + }, + "disable_notification": { + "name": "Disable notification", + "description": "Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound." + }, + "verify_ssl": { + "name": "Verify SSL", + "description": "Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server." + }, + "timeout": { + "name": "Timeout", + "description": "Timeout for send photo. Will help with timeout errors (poor internet connection, etc)." + }, + "keyboard": { + "name": "Keyboard", + "description": "List of rows of commands, comma-separated, to make a custom keyboard." + }, + "inline_keyboard": { + "name": "Inline keyboard", + "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + }, + "message_tag": { + "name": "Message tag", + "description": "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}." + } + } + }, + "send_sticker": { + "name": "Send sticker", + "description": "Sends a sticker.", + "fields": { + "url": { + "name": "URL", + "description": "Remote path to a static .webp or animated .tgs sticker." + }, + "file": { + "name": "File", + "description": "Local path to a static .webp or animated .tgs sticker." + }, + "sticker_id": { + "name": "Sticker ID", + "description": "ID of a sticker that exists on telegram servers." + }, + "username": { + "name": "Username", + "description": "Username for a URL which require HTTP authentication." + }, + "password": { + "name": "Password", + "description": "Password (or bearer token) for a URL which require HTTP authentication." + }, + "authentication": { + "name": "Authentication method", + "description": "Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`." + }, + "target": { + "name": "Target", + "description": "An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default." + }, + "disable_notification": { + "name": "Disable notification", + "description": "Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound." + }, + "verify_ssl": { + "name": "Verify SSL", + "description": "Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server." + }, + "timeout": { + "name": "Timeout", + "description": "Timeout for send sticker. Will help with timeout errors (poor internet connection, etc)." + }, + "keyboard": { + "name": "Keyboard", + "description": "List of rows of commands, comma-separated, to make a custom keyboard." + }, + "inline_keyboard": { + "name": "Inline keyboard", + "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + }, + "message_tag": { + "name": "Message tag", + "description": "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}." + } + } + }, + "send_animation": { + "name": "Send animation", + "description": "Sends an anmiation.", + "fields": { + "url": { + "name": "URL", + "description": "Remote path to a GIF or H.264/MPEG-4 AVC video without sound." + }, + "file": { + "name": "File", + "description": "Local path to a GIF or H.264/MPEG-4 AVC video without sound." + }, + "caption": { + "name": "Caption", + "description": "The title of the animation." + }, + "username": { + "name": "Username", + "description": "Username for a URL which require HTTP authentication." + }, + "password": { + "name": "Password", + "description": "Password (or bearer token) for a URL which require HTTP authentication." + }, + "authentication": { + "name": "Authentication method", + "description": "Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`." + }, + "target": { + "name": "Target", + "description": "An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default." + }, + "parse_mode": { + "name": "Parse Mode", + "description": "Parser for the message text." + }, + "disable_notification": { + "name": "Disable notification", + "description": "Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound." + }, + "verify_ssl": { + "name": "Verify SSL", + "description": "Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server." + }, + "timeout": { + "name": "Timeout", + "description": "Timeout for send sticker. Will help with timeout errors (poor internet connection, etc)." + }, + "keyboard": { + "name": "Keyboard", + "description": "List of rows of commands, comma-separated, to make a custom keyboard." + }, + "inline_keyboard": { + "name": "Inline keyboard", + "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + } + } + }, + "send_video": { + "name": "Send video", + "description": "Sends a video.", + "fields": { + "url": { + "name": "URL", + "description": "Remote path to a video." + }, + "file": { + "name": "File", + "description": "Local path to a video." + }, + "caption": { + "name": "Caption", + "description": "The title of the video." + }, + "username": { + "name": "Username", + "description": "Username for a URL which require HTTP authentication." + }, + "password": { + "name": "Password", + "description": "Password (or bearer token) for a URL which require HTTP authentication." + }, + "authentication": { + "name": "Authentication method", + "description": "Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`." + }, + "target": { + "name": "Target", + "description": "An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default." + }, + "parse_mode": { + "name": "Parse mode", + "description": "Parser for the message text." + }, + "disable_notification": { + "name": "Disable notification", + "description": "Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound." + }, + "verify_ssl": { + "name": "Verify SSL", + "description": "Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server." + }, + "timeout": { + "name": "Timeout", + "description": "Timeout for send video. Will help with timeout errors (poor internet connection, etc)." + }, + "keyboard": { + "name": "Keyboard", + "description": "List of rows of commands, comma-separated, to make a custom keyboard." + }, + "inline_keyboard": { + "name": "Inline keyboard", + "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + }, + "message_tag": { + "name": "Message tag", + "description": "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}." + } + } + }, + "send_voice": { + "name": "Send voice", + "description": "Sends a voice message.", + "fields": { + "url": { + "name": "URL", + "description": "Remote path to a voice message." + }, + "file": { + "name": "File", + "description": "Local path to a voice message." + }, + "caption": { + "name": "Caption", + "description": "The title of the voice message." + }, + "username": { + "name": "Username", + "description": "Username for a URL which require HTTP authentication." + }, + "password": { + "name": "Password", + "description": "Password (or bearer token) for a URL which require HTTP authentication." + }, + "authentication": { + "name": "Authentication method", + "description": "Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`." + }, + "target": { + "name": "Target", + "description": "An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default." + }, + "disable_notification": { + "name": "Disable notification", + "description": "Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound." + }, + "verify_ssl": { + "name": "Verify SSL", + "description": "Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server." + }, + "timeout": { + "name": "Timeout", + "description": "Timeout for send voice. Will help with timeout errors (poor internet connection, etc)." + }, + "keyboard": { + "name": "Keyboard", + "description": "List of rows of commands, comma-separated, to make a custom keyboard." + }, + "inline_keyboard": { + "name": "Inline keyboard", + "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + }, + "message_tag": { + "name": "Message tag", + "description": "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}." + } + } + }, + "send_document": { + "name": "Send document", + "description": "Sends a document.", + "fields": { + "url": { + "name": "URL", + "description": "Remote path to a document." + }, + "file": { + "name": "File", + "description": "Local path to a document." + }, + "caption": { + "name": "Caption", + "description": "The title of the document." + }, + "username": { + "name": "Username", + "description": "Username for a URL which require HTTP authentication." + }, + "password": { + "name": "Password", + "description": "Password (or bearer token) for a URL which require HTTP authentication." + }, + "authentication": { + "name": "Authentication method", + "description": "Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`." + }, + "target": { + "name": "Target", + "description": "An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default." + }, + "parse_mode": { + "name": "Parse mode", + "description": "Parser for the message text." + }, + "disable_notification": { + "name": "Disable notification", + "description": "Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound." + }, + "verify_ssl": { + "name": "Verify SSL", + "description": "Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server." + }, + "timeout": { + "name": "Timeout", + "description": "Timeout for send document. Will help with timeout errors (poor internet connection, etc)." + }, + "keyboard": { + "name": "Keyboard", + "description": "List of rows of commands, comma-separated, to make a custom keyboard." + }, + "inline_keyboard": { + "name": "Inline keyboard", + "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + }, + "message_tag": { + "name": "Message tag", + "description": "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}." + } + } + }, + "send_location": { + "name": "Send location", + "description": "Sends a location.", + "fields": { + "latitude": { + "name": "Latitude", + "description": "The latitude to send." + }, + "longitude": { + "name": "Longitude", + "description": "The longitude to send." + }, + "target": { + "name": "Target", + "description": "An array of pre-authorized chat_ids to send the location to. If not present, first allowed chat_id is the default." + }, + "disable_notification": { + "name": "Disable notification", + "description": "Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound." + }, + "timeout": { + "name": "Timeout", + "description": "Timeout for send photo. Will help with timeout errors (poor internet connection, etc)." + }, + "keyboard": { + "name": "Keyboard", + "description": "List of rows of commands, comma-separated, to make a custom keyboard." + }, + "inline_keyboard": { + "name": "Inline keyboard", + "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + }, + "message_tag": { + "name": "Message tag", + "description": "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}." + } + } + }, + "send_poll": { + "name": "Send poll", + "description": "Sends a poll.", + "fields": { + "target": { + "name": "Target", + "description": "An array of pre-authorized chat_ids to send the location to. If not present, first allowed chat_id is the default." + }, + "question": { + "name": "Question", + "description": "Poll question, 1-300 characters." + }, + "options": { + "name": "Options", + "description": "List of answer options, 2-10 strings 1-100 characters each." + }, + "is_anonymous": { + "name": "Is anonymous", + "description": "If the poll needs to be anonymous, defaults to True." + }, + "allows_multiple_answers": { + "name": "Allow multiple answers", + "description": "If the poll allows multiple answers, defaults to False." + }, + "open_period": { + "name": "Open period", + "description": "Amount of time in seconds the poll will be active after creation, 5-600." + }, + "disable_notification": { + "name": "Disable notification", + "description": "Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound." + }, + "timeout": { + "name": "Timeout", + "description": "Timeout for send poll. Will help with timeout errors (poor internet connection, etc)." + } + } + }, + "edit_message": { + "name": "Edit message", + "description": "Edits a previously sent message.", + "fields": { + "message_id": { + "name": "Message ID", + "description": "Id of the message to edit." + }, + "chat_id": { + "name": "Chat ID", + "description": "The chat_id where to edit the message." + }, + "message": { + "name": "Message", + "description": "Message body of the notification." + }, + "title": { + "name": "Title", + "description": "Optional title for your notification. Will be composed as '%title\\n%message'." + }, + "parse_mode": { + "name": "Parse mode", + "description": "Parser for the message text." + }, + "disable_web_page_preview": { + "name": "Disable web page preview", + "description": "Disables link previews for links in the message." + }, + "inline_keyboard": { + "name": "Inline keyboard", + "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + } + } + }, + "edit_caption": { + "name": "Edit caption", + "description": "Edits the caption of a previously sent message.", + "fields": { + "message_id": { + "name": "Message ID", + "description": "Id of the message to edit." + }, + "chat_id": { + "name": "Chat ID", + "description": "The chat_id where to edit the caption." + }, + "caption": { + "name": "Caption", + "description": "Message body of the notification." + }, + "inline_keyboard": { + "name": "Inline keyboard", + "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + } + } + }, + "edit_replymarkup": { + "name": "Edit reply markup", + "description": "Edit the inline keyboard of a previously sent message.", + "fields": { + "message_id": { + "name": "Message ID", + "description": "Id of the message to edit." + }, + "chat_id": { + "name": "Chat ID", + "description": "The chat_id where to edit the reply_markup." + }, + "inline_keyboard": { + "name": "Inline keyboard", + "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + } + } + }, + "answer_callback_query": { + "name": "Answer callback query", + "description": "Responds to a callback query originated by clicking on an online keyboard button. The answer will be displayed to the user as a notification at the top of the chat screen or as an alert.", + "fields": { + "message": { + "name": "Message", + "description": "Unformatted text message body of the notification." + }, + "callback_query_id": { + "name": "Callback query ID", + "description": "Unique id of the callback response." + }, + "show_alert": { + "name": "Show alert", + "description": "Show a permanent notification." + }, + "timeout": { + "name": "Timeout", + "description": "Timeout for sending the answer. Will help with timeout errors (poor internet connection, etc)." + } + } + }, + "delete_message": { + "name": "Delete message", + "description": "Deletes a previously sent message.", + "fields": { + "message_id": { + "name": "Message ID", + "description": "Id of the message to delete." + }, + "chat_id": { + "name": "Chat ID", + "description": "The chat_id where to delete the message." + } + } + } + } +} diff --git a/homeassistant/components/timer/services.yaml b/homeassistant/components/timer/services.yaml index 68caa44a69907f..74eeae22b23051 100644 --- a/homeassistant/components/timer/services.yaml +++ b/homeassistant/components/timer/services.yaml @@ -1,48 +1,36 @@ # Describes the format for available timer services start: - name: Start - description: Start a timer target: entity: domain: timer fields: duration: - description: Duration the timer requires to finish. [optional] example: "00:01:00 or 60" selector: text: pause: - name: Pause - description: Pause a timer. target: entity: domain: timer cancel: - name: Cancel - description: Cancel a timer. target: entity: domain: timer finish: - name: Finish - description: Finish a timer. target: entity: domain: timer change: - name: Change - description: Change a timer target: entity: domain: timer fields: duration: - description: Duration to add or subtract to the running timer default: 0 required: true example: "00:01:00, 60 or -60" diff --git a/homeassistant/components/timer/strings.json b/homeassistant/components/timer/strings.json index 217de09a534ced..e21f0d2ca82484 100644 --- a/homeassistant/components/timer/strings.json +++ b/homeassistant/components/timer/strings.json @@ -29,5 +29,39 @@ } } } + }, + "services": { + "start": { + "name": "Start", + "description": "Starts a timer.", + "fields": { + "duration": { + "name": "Duration", + "description": "Duration the timer requires to finish. [optional]." + } + } + }, + "pause": { + "name": "Pause", + "description": "Pauses a timer." + }, + "cancel": { + "name": "Cancel", + "description": "Cancels a timer." + }, + "finish": { + "name": "Finish", + "description": "Finishes a timer." + }, + "change": { + "name": "Change", + "description": "Changes a timer.", + "fields": { + "duration": { + "name": "Duration", + "description": "Duration to add or subtract to the running timer." + } + } + } } } diff --git a/homeassistant/components/todoist/services.yaml b/homeassistant/components/todoist/services.yaml index 3cab4d2bf673b7..9593b6bb6a40ed 100644 --- a/homeassistant/components/todoist/services.yaml +++ b/homeassistant/components/todoist/services.yaml @@ -1,49 +1,33 @@ new_task: - name: New task - description: Create a new task and add it to a project. fields: content: - name: Content - description: The name of the task. required: true example: Pick up the mail. selector: text: project: - name: Project - description: The name of the project this task should belong to. example: Errands default: Inbox selector: text: labels: - name: Labels - description: Any labels that you want to apply to this task, separated by a comma. example: Chores,Delivieries selector: text: assignee: - name: Assignee - description: A members username of a shared project to assign this task to. example: username selector: text: priority: - name: Priority - description: The priority of this task, from 1 (normal) to 4 (urgent). selector: number: min: 1 max: 4 due_date_string: - name: Due date string - description: The day this task is due, in natural language. example: Tomorrow selector: text: due_date_lang: - name: Due data language - description: The language of due_date_string. selector: select: options: @@ -62,20 +46,14 @@ new_task: - "sv" - "zh" due_date: - name: Due date - description: The time this task is due, in format YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS, in UTC timezone. example: "2019-10-22" selector: text: reminder_date_string: - name: Reminder date string - description: When should user be reminded of this task, in natural language. example: Tomorrow selector: text: reminder_date_lang: - name: Reminder data language - description: The language of reminder_date_string. selector: select: options: @@ -94,8 +72,6 @@ new_task: - "sv" - "zh" reminder_date: - name: Reminder date - description: When should user be reminded of this task, in format YYYY-MM-DDTHH:MM:SS, in UTC timezone. example: "2019-10-22T10:30:00" selector: text: diff --git a/homeassistant/components/todoist/strings.json b/homeassistant/components/todoist/strings.json new file mode 100644 index 00000000000000..1ed092e5cf6e11 --- /dev/null +++ b/homeassistant/components/todoist/strings.json @@ -0,0 +1,54 @@ +{ + "services": { + "new_task": { + "name": "New task", + "description": "Creates a new task and add it to a project.", + "fields": { + "content": { + "name": "Content", + "description": "The name of the task." + }, + "project": { + "name": "Project", + "description": "The name of the project this task should belong to." + }, + "labels": { + "name": "Labels", + "description": "Any labels that you want to apply to this task, separated by a comma." + }, + "assignee": { + "name": "Assignee", + "description": "A members username of a shared project to assign this task to." + }, + "priority": { + "name": "Priority", + "description": "The priority of this task, from 1 (normal) to 4 (urgent)." + }, + "due_date_string": { + "name": "Due date string", + "description": "The day this task is due, in natural language." + }, + "due_date_lang": { + "name": "Due data language", + "description": "The language of due_date_string." + }, + "due_date": { + "name": "Due date", + "description": "The time this task is due, in format YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS, in UTC timezone." + }, + "reminder_date_string": { + "name": "Reminder date string", + "description": "When should user be reminded of this task, in natural language." + }, + "reminder_date_lang": { + "name": "Reminder data language", + "description": "The language of reminder_date_string." + }, + "reminder_date": { + "name": "Reminder date", + "description": "When should user be reminded of this task, in format YYYY-MM-DDTHH:MM:SS, in UTC timezone." + } + } + } + } +} diff --git a/homeassistant/components/toon/services.yaml b/homeassistant/components/toon/services.yaml index d01cf32994b5ca..1b75dd4957aa1e 100644 --- a/homeassistant/components/toon/services.yaml +++ b/homeassistant/components/toon/services.yaml @@ -1,10 +1,6 @@ update: - name: Update - description: Update all entities with fresh data from Toon fields: display: - name: Display - description: Toon display to update. advanced: true example: eneco-001-123456 selector: diff --git a/homeassistant/components/toon/strings.json b/homeassistant/components/toon/strings.json index 60d5ed3312c27a..620a7f51113c6e 100644 --- a/homeassistant/components/toon/strings.json +++ b/homeassistant/components/toon/strings.json @@ -20,5 +20,17 @@ "no_agreements": "This account has no Toon displays.", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" } + }, + "services": { + "update": { + "name": "Update", + "description": "Updates all entities with fresh data from Toon.", + "fields": { + "display": { + "name": "Display", + "description": "Toon display to update." + } + } + } } } diff --git a/homeassistant/components/totalconnect/services.yaml b/homeassistant/components/totalconnect/services.yaml index 0e8f8f8e217fcf..3ab4faf0c30e82 100644 --- a/homeassistant/components/totalconnect/services.yaml +++ b/homeassistant/components/totalconnect/services.yaml @@ -1,14 +1,10 @@ arm_away_instant: - name: Arm Away Instant - description: Arm Away with zero entry delay. target: entity: integration: totalconnect domain: alarm_control_panel arm_home_instant: - name: Arm Home Instant - description: Arm Home with zero entry delay. target: entity: integration: totalconnect diff --git a/homeassistant/components/totalconnect/strings.json b/homeassistant/components/totalconnect/strings.json index 346ea7ef4030cf..922962c9866291 100644 --- a/homeassistant/components/totalconnect/strings.json +++ b/homeassistant/components/totalconnect/strings.json @@ -39,5 +39,15 @@ } } } + }, + "services": { + "arm_away_instant": { + "name": "Arm away instant", + "description": "Arms Away with zero entry delay." + }, + "arm_home_instant": { + "name": "Arm home instant", + "description": "Arms Home with zero entry delay." + } } } diff --git a/homeassistant/components/tplink/services.yaml b/homeassistant/components/tplink/services.yaml index 16166278565aed..1850df9a06017c 100644 --- a/homeassistant/components/tplink/services.yaml +++ b/homeassistant/components/tplink/services.yaml @@ -1,14 +1,10 @@ sequence_effect: - name: Sequence effect - description: Set a sequence effect target: entity: integration: tplink domain: light fields: sequence: - name: Sequence - description: List of HSV sequences (Max 16) example: | - [340, 20, 50] - [20, 50, 50] @@ -17,16 +13,12 @@ sequence_effect: selector: object: segments: - name: Segments - description: List of Segments (0 for all) example: 0, 2, 4, 6, 8 default: 0 required: false selector: object: brightness: - name: Brightness - description: Initial brightness example: 80 default: 100 required: false @@ -37,8 +29,6 @@ sequence_effect: max: 100 unit_of_measurement: "%" duration: - name: Duration - description: Duration example: 0 default: 0 required: false @@ -49,8 +39,6 @@ sequence_effect: max: 5000 unit_of_measurement: "ms" repeat_times: - name: Repetitions - description: Repetitions (0 for continuous) example: 0 default: 0 required: false @@ -60,8 +48,6 @@ sequence_effect: step: 1 max: 10 transition: - name: Transition - description: Transition example: 2000 default: 0 required: false @@ -72,8 +58,6 @@ sequence_effect: max: 6000 unit_of_measurement: "ms" spread: - name: Spread - description: Speed of spread example: 1 default: 0 required: false @@ -83,8 +67,6 @@ sequence_effect: step: 1 max: 16 direction: - name: Direction - description: Direction example: 1 default: 4 required: false @@ -94,21 +76,17 @@ sequence_effect: step: 1 max: 4 random_effect: - name: Random effect - description: Set a random effect target: entity: integration: tplink domain: light fields: init_states: - description: Initial HSV sequence example: [199, 99, 96] required: true selector: object: backgrounds: - description: List of HSV sequences (Max 16) example: | - [199, 89, 50] - [160, 50, 50] @@ -117,14 +95,12 @@ random_effect: selector: object: segments: - description: List of segments (0 for all) example: 0, 2, 4, 6, 8 default: 0 required: false selector: object: brightness: - description: Initial brightness example: 90 default: 100 required: false @@ -135,7 +111,6 @@ random_effect: max: 100 unit_of_measurement: "%" duration: - description: Duration example: 0 default: 0 required: false @@ -146,7 +121,6 @@ random_effect: max: 5000 unit_of_measurement: "ms" transition: - description: Transition example: 2000 default: 0 required: false @@ -157,7 +131,6 @@ random_effect: max: 6000 unit_of_measurement: "ms" fadeoff: - description: Fade off example: 2000 default: 0 required: false @@ -168,31 +141,26 @@ random_effect: max: 3000 unit_of_measurement: "ms" hue_range: - description: Range of hue example: 340, 360 required: false selector: object: saturation_range: - description: Range of saturation example: 40, 95 required: false selector: object: brightness_range: - description: Range of brightness example: 90, 100 required: false selector: object: transition_range: - description: Range of transition example: 2000, 6000 required: false selector: object: random_seed: - description: Random seed example: 80 default: 100 required: false diff --git a/homeassistant/components/tplink/strings.json b/homeassistant/components/tplink/strings.json index afc595a3adcec1..b5279804d0a3d3 100644 --- a/homeassistant/components/tplink/strings.json +++ b/homeassistant/components/tplink/strings.json @@ -24,5 +24,99 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } + }, + "services": { + "sequence_effect": { + "name": "Sequence effect", + "description": "Sets a sequence effect.", + "fields": { + "sequence": { + "name": "Sequence", + "description": "List of HSV sequences (Max 16)." + }, + "segments": { + "name": "Segments", + "description": "List of Segments (0 for all)." + }, + "brightness": { + "name": "Brightness", + "description": "Initial brightness." + }, + "duration": { + "name": "Duration", + "description": "Duration." + }, + "repeat_times": { + "name": "Repetitions", + "description": "Repetitions (0 for continuous)." + }, + "transition": { + "name": "Transition", + "description": "Transition." + }, + "spread": { + "name": "Spread", + "description": "Speed of spread." + }, + "direction": { + "name": "Direction", + "description": "Direction." + } + } + }, + "random_effect": { + "name": "Random effect", + "description": "Sets a random effect.", + "fields": { + "init_states": { + "name": "Initial states", + "description": "Initial HSV sequence." + }, + "backgrounds": { + "name": "Backgrounds", + "description": "List of HSV sequences (Max 16)." + }, + "segments": { + "name": "Segments", + "description": "List of segments (0 for all)." + }, + "brightness": { + "name": "Brightness", + "description": "Initial brightness." + }, + "duration": { + "name": "Duration", + "description": "Duration." + }, + "transition": { + "name": "Transition", + "description": "Transition." + }, + "fadeoff": { + "name": "Fade off", + "description": "Fade off." + }, + "hue_range": { + "name": "Hue range", + "description": "Range of hue." + }, + "saturation_range": { + "name": "Saturation range", + "description": "Range of saturation." + }, + "brightness_range": { + "name": "Brightness range", + "description": "Range of brightness." + }, + "transition_range": { + "name": "Transition range", + "description": "Range of transition." + }, + "random_seed": { + "name": "Random seed", + "description": "Random seed." + } + } + } } } diff --git a/homeassistant/components/transmission/services.yaml b/homeassistant/components/transmission/services.yaml index 34a885284119d2..2d61bda442feb0 100644 --- a/homeassistant/components/transmission/services.yaml +++ b/homeassistant/components/transmission/services.yaml @@ -1,75 +1,49 @@ add_torrent: - name: Add torrent - description: Add a new torrent to download (URL, magnet link or Base64 encoded). fields: entry_id: - name: Transmission entry - description: Config entry id selector: config_entry: integration: transmission torrent: - name: Torrent - description: URL, magnet link or Base64 encoded file. required: true example: http://releases.ubuntu.com/19.04/ubuntu-19.04-desktop-amd64.iso.torrent selector: text: remove_torrent: - name: Remove torrent - description: Remove a torrent fields: entry_id: - name: Transmission entry - description: Config entry id selector: config_entry: integration: transmission id: - name: ID - description: ID of a torrent required: true example: 123 selector: text: delete_data: - name: Delete data - description: Delete torrent data default: false selector: boolean: start_torrent: - name: Start torrent - description: Start a torrent fields: entry_id: - name: Transmission entry - description: Config entry id selector: config_entry: integration: transmission id: - name: ID - description: ID of a torrent example: 123 selector: text: stop_torrent: - name: Stop torrent - description: Stop a torrent fields: entry_id: - name: Transmission entry - description: Config entry id selector: config_entry: integration: transmission id: - name: ID - description: ID of a torrent required: true example: 123 selector: diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index e2c144d54232f8..c3fdcc8f1f4d84 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -52,5 +52,67 @@ } } } + }, + "services": { + "add_torrent": { + "name": "Add torrent", + "description": "Adds a new torrent to download (URL, magnet link or Base64 encoded).", + "fields": { + "entry_id": { + "name": "Transmission entry", + "description": "Config entry id." + }, + "torrent": { + "name": "Torrent", + "description": "URL, magnet link or Base64 encoded file." + } + } + }, + "remove_torrent": { + "name": "Remove torrent", + "description": "Removes a torrent.", + "fields": { + "entry_id": { + "name": "Transmission entry", + "description": "Config entry id." + }, + "id": { + "name": "ID", + "description": "ID of a torrent." + }, + "delete_data": { + "name": "Delete data", + "description": "Delete torrent data." + } + } + }, + "start_torrent": { + "name": "Start torrent", + "description": "Starts a torrent.", + "fields": { + "entry_id": { + "name": "Transmission entry", + "description": "Config entry id." + }, + "id": { + "name": "ID", + "description": "ID of a torrent." + } + } + }, + "stop_torrent": { + "name": "Stop torrent", + "description": "Stops a torrent.", + "fields": { + "entry_id": { + "name": "Transmission entry", + "description": "Config entry id." + }, + "id": { + "name": "ID", + "description": "ID of a torrent." + } + } + } } } diff --git a/homeassistant/components/unifi/services.yaml b/homeassistant/components/unifi/services.yaml index c6a4de3072a10e..fd69b8eb708ac1 100644 --- a/homeassistant/components/unifi/services.yaml +++ b/homeassistant/components/unifi/services.yaml @@ -1,15 +1,9 @@ reconnect_client: - name: Reconnect wireless client - description: Try to get wireless client to reconnect to UniFi Network fields: device_id: - name: Device - description: Try reconnect client to wireless network required: true selector: device: integration: unifi remove_clients: - name: Remove clients from the UniFi Network - description: Clean up clients that has only been associated with the controller for a short period of time. diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index 02b64f3c50ed1b..6afae5ffe7b83e 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -68,5 +68,21 @@ "title": "UniFi Network options 3/3" } } + }, + "services": { + "reconnect_client": { + "name": "Reconnect wireless client", + "description": "Tries to get wireless client to reconnect to UniFi Network.", + "fields": { + "device_id": { + "name": "Device", + "description": "Try reconnect client to wireless network." + } + } + }, + "remove_clients": { + "name": "Remove clients from the UniFi Network", + "description": "Cleans up clients that has only been associated with the controller for a short period of time." + } } } diff --git a/homeassistant/components/unifiprotect/services.yaml b/homeassistant/components/unifiprotect/services.yaml index 9f9031d6543019..6998f540471cc3 100644 --- a/homeassistant/components/unifiprotect/services.yaml +++ b/homeassistant/components/unifiprotect/services.yaml @@ -1,65 +1,42 @@ add_doorbell_text: - name: Add Custom Doorbell Text - description: Adds a new custom message for Doorbells. fields: device_id: - name: UniFi Protect NVR - description: Any device from the UniFi Protect instance you want to change. In case you have multiple Protect Instances. required: true selector: device: integration: unifiprotect message: - name: Custom Message - description: New custom message to add for Doorbells. Must be less than 30 characters. example: Come In required: true selector: text: remove_doorbell_text: - name: Remove Custom Doorbell Text - description: Removes an existing message for Doorbells. fields: device_id: - name: UniFi Protect NVR - description: Any device from the UniFi Protect instance you want to change. In case you have multiple Protect Instances. required: true selector: device: integration: unifiprotect message: - name: Custom Message - description: Existing custom message to remove for Doorbells. example: Go Away! required: true selector: text: set_default_doorbell_text: - name: Set Default Doorbell Text - description: Sets the default doorbell message. This will be the message that is automatically selected when a message "expires". fields: device_id: - name: UniFi Protect NVR - description: Any device from the UniFi Protect instance you want to change. In case you have multiple Protect Instances. required: true selector: device: integration: unifiprotect message: - name: Default Message - description: The default message for your Doorbell. Must be less than 30 characters. example: Welcome! required: true selector: text: set_chime_paired_doorbells: - name: Set Chime Paired Doorbells - description: > - Use to set the paired doorbell(s) with a smart chime. fields: device_id: - name: Chime - description: The Chimes to link to the doorbells to required: true selector: device: @@ -67,8 +44,6 @@ set_chime_paired_doorbells: entity: device_class: unifiprotect__chime_button doorbells: - name: Doorbells - description: The Doorbells to link to the chime example: "binary_sensor.front_doorbell_doorbell" required: false selector: diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index b7be12233df742..fd2287e08be23f 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -85,5 +85,63 @@ } } } + }, + "services": { + "add_doorbell_text": { + "name": "Add custom doorbell text", + "description": "Adds a new custom message for doorbells.", + "fields": { + "device_id": { + "name": "UniFi Protect NVR", + "description": "Any device from the UniFi Protect instance you want to change. In case you have multiple Protect Instances." + }, + "message": { + "name": "Custom message", + "description": "New custom message to add for doorbells. Must be less than 30 characters." + } + } + }, + "remove_doorbell_text": { + "name": "Remove custom doorbell text", + "description": "Removes an existing message for doorbells.", + "fields": { + "device_id": { + "name": "UniFi Protect NVR", + "description": "Any device from the UniFi Protect instance you want to change. In case you have multiple Protect Instances." + }, + "message": { + "name": "Custom message", + "description": "Existing custom message to remove for doorbells." + } + } + }, + "set_default_doorbell_text": { + "name": "Set default doorbell text", + "description": "Sets the default doorbell message. This will be the message that is automatically selected when a message \"expires\".", + "fields": { + "device_id": { + "name": "UniFi Protect NVR", + "description": "Any device from the UniFi Protect instance you want to change. In case you have multiple Protect Instances." + }, + "message": { + "name": "Default message", + "description": "The default message for your doorbell. Must be less than 30 characters." + } + } + }, + "set_chime_paired_doorbells": { + "name": "Set chime paired doorbells", + "description": "Use to set the paired doorbell(s) with a smart chime.", + "fields": { + "device_id": { + "name": "Chime", + "description": "The chimes to link to the doorbells to." + }, + "doorbells": { + "name": "Doorbells", + "description": "The doorbells to link to the chime." + } + } + } } } diff --git a/homeassistant/components/upb/services.yaml b/homeassistant/components/upb/services.yaml index af8eb81d9b04f9..cf415705d72e95 100644 --- a/homeassistant/components/upb/services.yaml +++ b/homeassistant/components/upb/services.yaml @@ -1,29 +1,21 @@ light_fade_start: - name: Start light fade - description: Start fading a light either up or down from current brightness. target: entity: integration: upb domain: light fields: brightness: - name: Brightness - description: Number indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness. selector: number: min: 0 max: 255 brightness_pct: - name: Brightness percentage - description: Number indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness. selector: number: min: 0 max: 100 unit_of_measurement: "%" rate: - name: Rate - description: Rate for light to transition to new brightness default: -1 selector: number: @@ -33,24 +25,18 @@ light_fade_start: unit_of_measurement: seconds light_fade_stop: - name: Stop light fade - description: Stop a light fade. target: entity: integration: upb domain: light light_blink: - name: Blink light - description: Blink a light target: entity: integration: upb domain: light fields: rate: - name: Rate - description: Amount of time that the link flashes on. default: 0.5 selector: number: @@ -60,39 +46,29 @@ light_blink: unit_of_measurement: seconds link_deactivate: - name: Deactivate link - description: Deactivate a UPB scene. target: entity: integration: upb domain: light link_goto: - name: Go to link - description: Set scene to brightness. target: entity: integration: upb domain: scene fields: brightness: - name: Brightness - description: Number indicating brightness, where 0 turns the scene off, 1 is the minimum brightness and 255 is the maximum brightness. selector: number: min: 0 max: 255 brightness_pct: - name: Brightness percentage - description: Number indicating percentage of full brightness, where 0 turns the scene off, 1 is the minimum brightness and 100 is the maximum brightness. selector: number: min: 0 max: 100 unit_of_measurement: "%" rate: - name: Rate - description: Amount of time for scene to transition to new brightness selector: number: min: -1 @@ -101,31 +77,23 @@ link_goto: unit_of_measurement: seconds link_fade_start: - name: Start link fade - description: Start fading a link either up or down from current brightness. target: entity: integration: upb domain: scene fields: brightness: - name: Brightness - description: Number indicating brightness, where 0 turns the scene off, 1 is the minimum brightness and 255 is the maximum brightness. selector: number: min: 0 max: 255 brightness_pct: - name: Brightness percentage - description: Number indicating percentage of full brightness, where 0 turns the scene off, 1 is the minimum brightness and 100 is the maximum brightness. selector: number: min: 0 max: 100 unit_of_measurement: "%" rate: - name: Rate - description: Amount of time for scene to transition to new brightness selector: number: min: -1 @@ -134,24 +102,18 @@ link_fade_start: unit_of_measurement: seconds link_fade_stop: - name: Stop link fade - description: Stop a link fade. target: entity: integration: upb domain: scene link_blink: - name: Blink link - description: Blink a link. target: entity: integration: upb domain: scene fields: blink_rate: - name: Blink rate - description: Amount of time that the link flashes on. default: 0.5 selector: number: diff --git a/homeassistant/components/upb/strings.json b/homeassistant/components/upb/strings.json index 9b2cc0a1b12f16..b5b6dea93d5f7c 100644 --- a/homeassistant/components/upb/strings.json +++ b/homeassistant/components/upb/strings.json @@ -19,5 +19,93 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "services": { + "light_fade_start": { + "name": "Start light fade", + "description": "Starts fading a light either up or down from current brightness.", + "fields": { + "brightness": { + "name": "Brightness", + "description": "Number indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness." + }, + "brightness_pct": { + "name": "Brightness percentage", + "description": "Number indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness." + }, + "rate": { + "name": "Rate", + "description": "Rate for light to transition to new brightness." + } + } + }, + "light_fade_stop": { + "name": "Stop light fade", + "description": "Stops a light fade." + }, + "light_blink": { + "name": "Blink light", + "description": "Blinks a light.", + "fields": { + "rate": { + "name": "Rate", + "description": "Amount of time that the link flashes on." + } + } + }, + "link_deactivate": { + "name": "Deactivate link", + "description": "Deactivates a UPB scene." + }, + "link_goto": { + "name": "Go to link", + "description": "Set scene to brightness.", + "fields": { + "brightness": { + "name": "Brightness", + "description": "Number indicating brightness, where 0 turns the scene off, 1 is the minimum brightness and 255 is the maximum brightness." + }, + "brightness_pct": { + "name": "Brightness percentage", + "description": "Number indicating percentage of full brightness, where 0 turns the scene off, 1 is the minimum brightness and 100 is the maximum brightness." + }, + "rate": { + "name": "Rate", + "description": "Amount of time for scene to transition to new brightness." + } + } + }, + "link_fade_start": { + "name": "Start link fade", + "description": "Starts fading a link either up or down from current brightness.", + "fields": { + "brightness": { + "name": "Brightness", + "description": "Number indicating brightness, where 0 turns the scene off, 1 is the minimum brightness and 255 is the maximum brightness." + }, + "brightness_pct": { + "name": "Brightness percentage", + "description": "Number indicating percentage of full brightness, where 0 turns the scene off, 1 is the minimum brightness and 100 is the maximum brightness." + }, + "rate": { + "name": "Rate", + "description": "Amount of time for scene to transition to new brightness." + } + } + }, + "link_fade_stop": { + "name": "Stop link fade", + "description": "Stops a link fade." + }, + "link_blink": { + "name": "Blink link", + "description": "Blinks a link.", + "fields": { + "blink_rate": { + "name": "Blink rate", + "description": "Amount of time that the link flashes on." + } + } + } } } diff --git a/homeassistant/components/utility_meter/services.yaml b/homeassistant/components/utility_meter/services.yaml index 4252f7961991ba..918c51cee390a0 100644 --- a/homeassistant/components/utility_meter/services.yaml +++ b/homeassistant/components/utility_meter/services.yaml @@ -1,23 +1,17 @@ # Describes the format for available switch services reset: - name: Reset - description: Resets all counters of a utility meter. target: entity: domain: select calibrate: - name: Calibrate - description: Calibrates a utility meter sensor. target: entity: domain: sensor integration: utility_meter fields: value: - name: Value - description: Value to which set the meter example: "100" required: true selector: diff --git a/homeassistant/components/utility_meter/strings.json b/homeassistant/components/utility_meter/strings.json index 1eeacbae800364..09b9dd0954046a 100644 --- a/homeassistant/components/utility_meter/strings.json +++ b/homeassistant/components/utility_meter/strings.json @@ -52,5 +52,21 @@ "yearly": "Yearly" } } + }, + "services": { + "reset": { + "name": "Reset", + "description": "Resets all counters of a utility meter." + }, + "calibrate": { + "name": "Calibrate", + "description": "Calibrates a utility meter sensor.", + "fields": { + "value": { + "name": "Value", + "description": "Value to which set the meter." + } + } + } } } diff --git a/homeassistant/components/vallox/services.yaml b/homeassistant/components/vallox/services.yaml index 15ce6c88b5581b..e6bd3edad116ca 100644 --- a/homeassistant/components/vallox/services.yaml +++ b/homeassistant/components/vallox/services.yaml @@ -1,10 +1,6 @@ set_profile_fan_speed_home: - name: Set profile fan speed home - description: Set the fan speed of the Home profile. fields: fan_speed: - name: Fan speed - description: Fan speed. required: true selector: number: @@ -13,12 +9,8 @@ set_profile_fan_speed_home: unit_of_measurement: "%" set_profile_fan_speed_away: - name: Set profile fan speed away - description: Set the fan speed of the Away profile. fields: fan_speed: - name: Fan speed - description: Fan speed. required: true selector: number: @@ -27,12 +19,8 @@ set_profile_fan_speed_away: unit_of_measurement: "%" set_profile_fan_speed_boost: - name: Set profile fan speed boost - description: Set the fan speed of the Boost profile. fields: fan_speed: - name: Fan speed - description: Fan speed. required: true selector: number: diff --git a/homeassistant/components/vallox/strings.json b/homeassistant/components/vallox/strings.json index cada5a7febdf56..b33cef0026aaf1 100644 --- a/homeassistant/components/vallox/strings.json +++ b/homeassistant/components/vallox/strings.json @@ -18,5 +18,37 @@ "invalid_host": "[%key:common::config_flow::error::invalid_host%]", "unknown": "[%key:common::config_flow::error::unknown%]" } + }, + "services": { + "set_profile_fan_speed_home": { + "name": "Set profile fan speed home", + "description": "Sets the fan speed of the Home profile.", + "fields": { + "fan_speed": { + "name": "Fan speed", + "description": "Fan speed." + } + } + }, + "set_profile_fan_speed_away": { + "name": "Set profile fan speed away", + "description": "Sets the fan speed of the Away profile.", + "fields": { + "fan_speed": { + "name": "Fan speed", + "description": "Fan speed." + } + } + }, + "set_profile_fan_speed_boost": { + "name": "Set profile fan speed boost", + "description": "Sets the fan speed of the Boost profile.", + "fields": { + "fan_speed": { + "name": "Fan speed", + "description": "Fan speed." + } + } + } } } diff --git a/homeassistant/components/velbus/services.yaml b/homeassistant/components/velbus/services.yaml index 32cda00f708301..e3ecc3556f01b0 100644 --- a/homeassistant/components/velbus/services.yaml +++ b/homeassistant/components/velbus/services.yaml @@ -1,10 +1,6 @@ sync_clock: - name: Sync clock - description: Sync the velbus modules clock to the Home Assistant clock, this is the same as the 'sync clock' from VelbusLink fields: interface: - name: Interface - description: The velbus interface to send the command to, this will be the same value as used during configuration required: true example: "192.168.1.5:27015" default: "" @@ -12,12 +8,8 @@ sync_clock: text: scan: - name: Scan - description: Scan the velbus modules, this will be need if you see unknown module warnings in the logs, or when you added new modules fields: interface: - name: Interface - description: The velbus interface to send the command to, this will be the same value as used during configuration required: true example: "192.168.1.5:27015" default: "" @@ -25,22 +17,14 @@ scan: text: clear_cache: - name: Clear cache - description: Clears the velbuscache and then starts a new scan fields: interface: - name: Interface - description: The velbus interface to send the command to, this will be the same value as used during configuration required: true example: "192.168.1.5:27015" default: "" selector: text: address: - name: Address - description: > - The module address in decimal format, if this is provided we only clear this module, if nothing is provided we clear the whole cache directory (all modules) - The decimal addresses are displayed in front of the modules listed at the integration page. required: false selector: number: @@ -48,34 +32,20 @@ clear_cache: max: 254 set_memo_text: - name: Set memo text - description: > - Set the memo text to the display of modules like VMBGPO, VMBGPOD - Be sure the page(s) of the module is configured to display the memo text. fields: interface: - name: Interface - description: The velbus interface to send the command to, this will be the same value as used during configuration required: true example: "192.168.1.5:27015" default: "" selector: text: address: - name: Address - description: > - The module address in decimal format. - The decimal addresses are displayed in front of the modules listed at the integration page. required: true selector: number: min: 1 max: 254 memo_text: - name: Memo text - description: > - The actual text to be displayed. - Text is limited to 64 characters. example: "Do not forget trash" default: "" selector: diff --git a/homeassistant/components/velbus/strings.json b/homeassistant/components/velbus/strings.json index 6eb44d8cb0c0eb..bef853001a1744 100644 --- a/homeassistant/components/velbus/strings.json +++ b/homeassistant/components/velbus/strings.json @@ -16,5 +16,59 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "services": { + "sync_clock": { + "name": "Sync clock", + "description": "Syncs the velbus modules clock to the Home Assistant clock, this is the same as the 'sync clock' from VelbusLink.", + "fields": { + "interface": { + "name": "Interface", + "description": "The velbus interface to send the command to, this will be the same value as used during configuration." + } + } + }, + "scan": { + "name": "Scan", + "description": "Scans the velbus modules, this will be need if you see unknown module warnings in the logs, or when you added new modules.", + "fields": { + "interface": { + "name": "Interface", + "description": "The velbus interface to send the command to, this will be the same value as used during configuration." + } + } + }, + "clear_cache": { + "name": "Clear cache", + "description": "Clears the velbuscache and then starts a new scan.", + "fields": { + "interface": { + "name": "Interface", + "description": "The velbus interface to send the command to, this will be the same value as used during configuration." + }, + "address": { + "name": "Address", + "description": "The module address in decimal format, if this is provided we only clear this module, if nothing is provided we clear the whole cache directory (all modules) The decimal addresses are displayed in front of the modules listed at the integration page.\n." + } + } + }, + "set_memo_text": { + "name": "Set memo text", + "description": "Sets the memo text to the display of modules like VMBGPO, VMBGPOD Be sure the page(s) of the module is configured to display the memo text.\n.", + "fields": { + "interface": { + "name": "Interface", + "description": "The velbus interface to send the command to, this will be the same value as used during configuration." + }, + "address": { + "name": "Address", + "description": "The module address in decimal format. The decimal addresses are displayed in front of the modules listed at the integration page.\n." + }, + "memo_text": { + "name": "Memo text", + "description": "The actual text to be displayed. Text is limited to 64 characters.\n." + } + } + } } } diff --git a/homeassistant/components/velux/services.yaml b/homeassistant/components/velux/services.yaml index 46aee795890ad2..7aee1694061aae 100644 --- a/homeassistant/components/velux/services.yaml +++ b/homeassistant/components/velux/services.yaml @@ -1,5 +1,3 @@ # Velux Integration services reboot_gateway: - name: Reboot gateway - description: Reboots the KLF200 Gateway. diff --git a/homeassistant/components/velux/strings.json b/homeassistant/components/velux/strings.json new file mode 100644 index 00000000000000..6a7e8c6e1ec963 --- /dev/null +++ b/homeassistant/components/velux/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "reboot_gateway": { + "name": "Reboot gateway", + "description": "Reboots the KLF200 Gateway." + } + } +} diff --git a/homeassistant/components/verisure/services.yaml b/homeassistant/components/verisure/services.yaml index 2a4e2a008bee65..ccfc7726bc6455 100644 --- a/homeassistant/components/verisure/services.yaml +++ b/homeassistant/components/verisure/services.yaml @@ -1,22 +1,16 @@ capture_smartcam: - name: Capture SmartCam image - description: Capture a new image from a Verisure SmartCam target: entity: integration: verisure domain: camera enable_autolock: - name: Enable autolock - description: Enable autolock of a Verisure Lockguard Smartlock target: entity: integration: verisure domain: lock disable_autolock: - name: Disable autolock - description: Disable autolock of a Verisure Lockguard Smartlock target: entity: integration: verisure diff --git a/homeassistant/components/verisure/strings.json b/homeassistant/components/verisure/strings.json index 85b3f4015b54bd..335daa68ee8a70 100644 --- a/homeassistant/components/verisure/strings.json +++ b/homeassistant/components/verisure/strings.json @@ -63,5 +63,19 @@ "name": "Ethernet status" } } + }, + "services": { + "capture_smartcam": { + "name": "Capture SmartCam image", + "description": "Captures a new image from a Verisure SmartCam." + }, + "enable_autolock": { + "name": "Enable autolock", + "description": "Enables autolock of a Verisure Lockguard Smartlock." + }, + "disable_autolock": { + "name": "Disable autolock", + "description": "Disables autolock of a Verisure Lockguard Smartlock." + } } } diff --git a/homeassistant/components/vesync/services.yaml b/homeassistant/components/vesync/services.yaml index da264ea3b5d9ee..52ee0382dbe029 100644 --- a/homeassistant/components/vesync/services.yaml +++ b/homeassistant/components/vesync/services.yaml @@ -1,3 +1 @@ update_devices: - name: Update devices - description: Add new VeSync devices to Home Assistant diff --git a/homeassistant/components/vesync/strings.json b/homeassistant/components/vesync/strings.json index 8359691effef14..08cbdf943f67b9 100644 --- a/homeassistant/components/vesync/strings.json +++ b/homeassistant/components/vesync/strings.json @@ -15,5 +15,11 @@ "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } + }, + "services": { + "update_devices": { + "name": "Update devices", + "description": "Adds new VeSync devices to Home Assistant." + } } } diff --git a/homeassistant/components/vicare/services.yaml b/homeassistant/components/vicare/services.yaml index 1fc1e61b6eeec3..b4df8a1bb0e388 100644 --- a/homeassistant/components/vicare/services.yaml +++ b/homeassistant/components/vicare/services.yaml @@ -1,14 +1,10 @@ set_vicare_mode: - name: Set vicare mode - description: Set a ViCare mode. target: entity: integration: vicare domain: climate fields: vicare_mode: - name: Vicare Mode - description: ViCare mode. required: true selector: select: diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json index d54956f3e103d6..0700d5d6f0e630 100644 --- a/homeassistant/components/vicare/strings.json +++ b/homeassistant/components/vicare/strings.json @@ -19,5 +19,17 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "unknown": "[%key:common::config_flow::error::unknown%]" } + }, + "services": { + "set_vicare_mode": { + "name": "Set ViCare mode", + "description": "Set a ViCare mode.", + "fields": { + "vicare_mode": { + "name": "ViCare mode", + "description": "ViCare mode." + } + } + } } } diff --git a/homeassistant/components/vizio/services.yaml b/homeassistant/components/vizio/services.yaml index 7a2ea859b7dab5..2f5da4659f0fe9 100644 --- a/homeassistant/components/vizio/services.yaml +++ b/homeassistant/components/vizio/services.yaml @@ -1,32 +1,20 @@ update_setting: - name: Update setting - description: Update the value of a setting on a Vizio media player device. target: entity: integration: vizio domain: media_player fields: setting_type: - name: Setting type - description: - The type of setting to be changed. Available types are listed in the - 'setting_types' property. required: true example: "audio" selector: text: setting_name: - name: Setting name - description: - The name of the setting to be changed. Available settings for a given - setting_type are listed in the '_settings' property. required: true example: "eq" selector: text: new_value: - name: New value - description: The new value for the setting. required: true example: "Music" selector: diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json index 665e03b531ac6a..314f6f8b4e5f8f 100644 --- a/homeassistant/components/vizio/strings.json +++ b/homeassistant/components/vizio/strings.json @@ -50,5 +50,25 @@ } } } + }, + "services": { + "update_setting": { + "name": "Update setting", + "description": "Updates the value of a setting on a Vizio media player device.", + "fields": { + "setting_type": { + "name": "Setting type", + "description": "The type of setting to be changed. Available types are listed in the 'setting_types' property." + }, + "setting_name": { + "name": "Setting name", + "description": "The name of the setting to be changed. Available settings for a given setting_type are listed in the '[setting_type]_settings' property." + }, + "new_value": { + "name": "New value", + "description": "The new value for the setting." + } + } + } } } From ce3c23cb3ac4a0c895cafea16984e96dff4a7811 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Wed, 12 Jul 2023 10:56:08 +0200 Subject: [PATCH 0398/1009] Add Nut commands to diagnostics data (#96285) * Add Nut commands to diagnostics data * Add test for diagnostics --- homeassistant/components/nut/diagnostics.py | 9 ++++- tests/components/nut/test_diagnostics.py | 43 +++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 tests/components/nut/test_diagnostics.py diff --git a/homeassistant/components/nut/diagnostics.py b/homeassistant/components/nut/diagnostics.py index e8c0a0711dc78b..9ee430a655b0c8 100644 --- a/homeassistant/components/nut/diagnostics.py +++ b/homeassistant/components/nut/diagnostics.py @@ -12,7 +12,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from . import PyNUTData -from .const import DOMAIN, PYNUT_DATA, PYNUT_UNIQUE_ID +from .const import DOMAIN, PYNUT_DATA, PYNUT_UNIQUE_ID, USER_AVAILABLE_COMMANDS TO_REDACT = {CONF_PASSWORD, CONF_USERNAME} @@ -26,7 +26,12 @@ async def async_get_config_entry_diagnostics( # Get information from Nut library nut_data: PyNUTData = hass_data[PYNUT_DATA] - data["nut_data"] = {"ups_list": nut_data.ups_list, "status": nut_data.status} + nut_cmd: set[str] = hass_data[USER_AVAILABLE_COMMANDS] + data["nut_data"] = { + "ups_list": nut_data.ups_list, + "status": nut_data.status, + "commands": nut_cmd, + } # Gather information how this Nut device is represented in Home Assistant device_registry = dr.async_get(hass) diff --git a/tests/components/nut/test_diagnostics.py b/tests/components/nut/test_diagnostics.py new file mode 100644 index 00000000000000..f91269f5196053 --- /dev/null +++ b/tests/components/nut/test_diagnostics.py @@ -0,0 +1,43 @@ +"""Tests for the diagnostics data provided by the Nut integration.""" + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.components.nut.diagnostics import TO_REDACT +from homeassistant.core import HomeAssistant + +from .util import async_init_integration + +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, +) -> None: + """Test diagnostics.""" + list_commands: set[str] = ["beeper.enable"] + list_commands_return_value = { + supported_command: supported_command for supported_command in list_commands + } + + mock_config_entry = await async_init_integration( + hass, + username="someuser", + password="somepassword", + list_vars={"ups.status": "OL"}, + list_ups={"ups1": "UPS 1"}, + list_commands_return_value=list_commands_return_value, + ) + + entry_dict = async_redact_data(mock_config_entry.as_dict(), TO_REDACT) + nut_data_dict = { + "ups_list": {"ups1": "UPS 1"}, + "status": {"ups.status": "OL"}, + "commands": list_commands, + } + + result = await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) + assert result["entry"] == entry_dict + assert result["nut_data"] == nut_data_dict From a3a2e6cc8dfac9889dde2ed52cfc8e93fb7d6a4e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 12:23:39 +0200 Subject: [PATCH 0399/1009] Migrate time services to support translations (#96402) --- homeassistant/components/time/services.yaml | 4 ---- homeassistant/components/time/strings.json | 12 ++++++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/time/services.yaml b/homeassistant/components/time/services.yaml index a8c843ab55ac1d..ee3d9150870ec6 100644 --- a/homeassistant/components/time/services.yaml +++ b/homeassistant/components/time/services.yaml @@ -1,13 +1,9 @@ set_value: - name: Set Time - description: Set the time for a time entity. target: entity: domain: time fields: time: - name: Time - description: The time to set. required: true example: "22:15" selector: diff --git a/homeassistant/components/time/strings.json b/homeassistant/components/time/strings.json index e8d92a30e2eac8..1b7c53b1a8a80e 100644 --- a/homeassistant/components/time/strings.json +++ b/homeassistant/components/time/strings.json @@ -4,5 +4,17 @@ "_": { "name": "[%key:component::time::title%]" } + }, + "services": { + "set_value": { + "name": "Set Time", + "description": "Sets the time.", + "fields": { + "time": { + "name": "Time", + "description": "The time to set." + } + } + } } } From 7bc90297d25133ff5ad739955c91aa723fbb7d59 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 12:31:26 +0200 Subject: [PATCH 0400/1009] Migrate integration services (G-H) to support translations (#96372) --- .../components/geniushub/services.yaml | 23 ---- .../components/geniushub/strings.json | 46 +++++++ homeassistant/components/google/services.yaml | 36 ----- homeassistant/components/google/strings.json | 78 +++++++++++ .../components/google_assistant/services.yaml | 4 - .../components/google_assistant/strings.json | 14 ++ .../google_assistant_sdk/services.yaml | 6 - .../google_assistant_sdk/strings.json | 16 +++ .../components/google_mail/services.yaml | 18 --- .../components/google_mail/strings.json | 40 ++++++ .../components/google_sheets/services.yaml | 8 -- .../components/google_sheets/strings.json | 20 +++ .../components/guardian/services.yaml | 22 --- .../components/guardian/strings.json | 52 +++++++ .../components/habitica/services.yaml | 8 -- .../components/habitica/strings.json | 24 +++- .../components/harmony/services.yaml | 6 - homeassistant/components/harmony/strings.json | 16 +++ .../components/hdmi_cec/services.yaml | 33 ----- .../components/hdmi_cec/strings.json | 70 ++++++++++ homeassistant/components/heos/services.yaml | 8 -- homeassistant/components/heos/strings.json | 20 +++ homeassistant/components/hive/services.yaml | 24 ---- homeassistant/components/hive/strings.json | 58 ++++++++ .../components/home_connect/services.yaml | 56 -------- .../components/home_connect/strings.json | 128 ++++++++++++++++++ .../components/homekit/services.yaml | 7 - homeassistant/components/homekit/strings.json | 18 ++- .../components/homematic/services.yaml | 57 -------- .../components/homematic/strings.json | 126 +++++++++++++++++ .../homematicip_cloud/services.yaml | 46 ------- .../components/homematicip_cloud/strings.json | 110 +++++++++++++++ homeassistant/components/html5/services.yaml | 6 - homeassistant/components/html5/strings.json | 18 +++ .../components/huawei_lte/services.yaml | 19 --- .../components/huawei_lte/strings.json | 42 ++++++ homeassistant/components/hue/services.yaml | 19 --- homeassistant/components/hue/strings.json | 43 +++++- 38 files changed, 934 insertions(+), 411 deletions(-) create mode 100644 homeassistant/components/geniushub/strings.json create mode 100644 homeassistant/components/google_assistant/strings.json create mode 100644 homeassistant/components/hdmi_cec/strings.json create mode 100644 homeassistant/components/homematic/strings.json create mode 100644 homeassistant/components/html5/strings.json diff --git a/homeassistant/components/geniushub/services.yaml b/homeassistant/components/geniushub/services.yaml index 7d4cd14b19e9c0..48b45a0811fa69 100644 --- a/homeassistant/components/geniushub/services.yaml +++ b/homeassistant/components/geniushub/services.yaml @@ -2,21 +2,14 @@ # Describes the format for available services set_zone_mode: - name: Set zone mode - description: >- - Set the zone to an operating mode. fields: entity_id: - name: Entity - description: The zone's entity_id. required: true selector: entity: integration: geniushub domain: climate mode: - name: Mode - description: "One of: off, timer or footprint." required: true selector: select: @@ -26,21 +19,14 @@ set_zone_mode: - "footprint" set_zone_override: - name: Set zone override - description: >- - Override the zone's set point for a given duration. fields: entity_id: - name: Entity - description: The zone's entity_id. required: true selector: entity: integration: geniushub domain: climate temperature: - name: Temperature - description: The target temperature. required: true selector: number: @@ -49,26 +35,17 @@ set_zone_override: step: 0.1 unit_of_measurement: "°" duration: - name: Duration - description: >- - The duration of the override. Optional, default 1 hour, maximum 24 hours. example: '{"minutes": 135}' selector: object: set_switch_override: - name: Set switch override - description: >- - Override switch for a given duration. target: entity: integration: geniushub domain: switch fields: duration: - name: Duration - description: >- - The duration of the override. Optional, default 1 hour, maximum 24 hours. example: '{"minutes": 135}' selector: object: diff --git a/homeassistant/components/geniushub/strings.json b/homeassistant/components/geniushub/strings.json new file mode 100644 index 00000000000000..1c1092ee256dd7 --- /dev/null +++ b/homeassistant/components/geniushub/strings.json @@ -0,0 +1,46 @@ +{ + "services": { + "set_zone_mode": { + "name": "Set zone mode", + "description": "Set the zone to an operating mode.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "The zone's entity_id." + }, + "mode": { + "name": "Mode", + "description": "One of: off, timer or footprint." + } + } + }, + "set_zone_override": { + "name": "Set zone override", + "description": "Overrides the zone's set point for a given duration.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "The zone's entity_id." + }, + "temperature": { + "name": "Temperature", + "description": "The target temperature." + }, + "duration": { + "name": "Duration", + "description": "The duration of the override. Optional, default 1 hour, maximum 24 hours." + } + } + }, + "set_switch_override": { + "name": "Set switch override", + "description": "Overrides switch for a given duration.", + "fields": { + "duration": { + "name": "Duration", + "description": "The duration of the override. Optional, default 1 hour, maximum 24 hours." + } + } + } + } +} diff --git a/homeassistant/components/google/services.yaml b/homeassistant/components/google/services.yaml index e7eeef7594755e..f715679dff8648 100644 --- a/homeassistant/components/google/services.yaml +++ b/homeassistant/components/google/services.yaml @@ -1,111 +1,75 @@ add_event: - name: Add event - description: Add a new calendar event. fields: calendar_id: - name: Calendar ID - description: The id of the calendar you want. required: true example: "Your email" selector: text: summary: - name: Summary - description: Acts as the title of the event. required: true example: "Bowling" selector: text: description: - name: Description - description: The description of the event. Optional. example: "Birthday bowling" selector: text: start_date_time: - name: Start time - description: The date and time the event should start. example: "2019-03-22 20:00:00" selector: text: end_date_time: - name: End time - description: The date and time the event should end. example: "2019-03-22 22:00:00" selector: text: start_date: - name: Start date - description: The date the whole day event should start. example: "2019-03-10" selector: text: end_date: - name: End date - description: The date the whole day event should end. example: "2019-03-11" selector: text: in: - name: In - description: Days or weeks that you want to create the event in. example: '"days": 2 or "weeks": 2' selector: object: create_event: - name: Create event - description: Add a new calendar event. target: entity: integration: google domain: calendar fields: summary: - name: Summary - description: Acts as the title of the event. required: true example: "Bowling" selector: text: description: - name: Description - description: The description of the event. Optional. example: "Birthday bowling" selector: text: start_date_time: - name: Start time - description: The date and time the event should start. example: "2022-03-22 20:00:00" selector: text: end_date_time: - name: End time - description: The date and time the event should end. example: "2022-03-22 22:00:00" selector: text: start_date: - name: Start date - description: The date the whole day event should start. example: "2022-03-10" selector: text: end_date: - name: End date - description: The date the whole day event should end. example: "2022-03-11" selector: text: in: - name: In - description: Days or weeks that you want to create the event in. example: '"days": 2 or "weeks": 2' selector: object: location: - name: Location - description: The location of the event. Optional. example: "Conference Room - F123, Bldg. 002" selector: text: diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json index 5c9b642447367a..7fa1569992f6d8 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -42,5 +42,83 @@ }, "application_credentials": { "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Calendar. You also need to create Application Credentials linked to your Calendar:\n1. Go to [Credentials]({oauth_creds_url}) and click **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **TV and Limited Input devices** for the Application Type.\n\n" + }, + "services": { + "add_event": { + "name": "Add event", + "description": "Adds a new calendar event.", + "fields": { + "calendar_id": { + "name": "Calendar ID", + "description": "The id of the calendar you want." + }, + "summary": { + "name": "Summary", + "description": "Acts as the title of the event." + }, + "description": { + "name": "Description", + "description": "The description of the event. Optional." + }, + "start_date_time": { + "name": "Start time", + "description": "The date and time the event should start." + }, + "end_date_time": { + "name": "End time", + "description": "The date and time the event should end." + }, + "start_date": { + "name": "Start date", + "description": "The date the whole day event should start." + }, + "end_date": { + "name": "End date", + "description": "The date the whole day event should end." + }, + "in": { + "name": "In", + "description": "Days or weeks that you want to create the event in." + } + } + }, + "create_event": { + "name": "Creates event", + "description": "Add a new calendar event.", + "fields": { + "summary": { + "name": "Summary", + "description": "Acts as the title of the event." + }, + "description": { + "name": "Description", + "description": "The description of the event. Optional." + }, + "start_date_time": { + "name": "Start time", + "description": "The date and time the event should start." + }, + "end_date_time": { + "name": "End time", + "description": "The date and time the event should end." + }, + "start_date": { + "name": "Start date", + "description": "The date the whole day event should start." + }, + "end_date": { + "name": "End date", + "description": "The date the whole day event should end." + }, + "in": { + "name": "In", + "description": "Days or weeks that you want to create the event in." + }, + "location": { + "name": "Location", + "description": "The location of the event. Optional." + } + } + } } } diff --git a/homeassistant/components/google_assistant/services.yaml b/homeassistant/components/google_assistant/services.yaml index fe5ef51c2ce9ca..321eae3b2e906f 100644 --- a/homeassistant/components/google_assistant/services.yaml +++ b/homeassistant/components/google_assistant/services.yaml @@ -1,9 +1,5 @@ request_sync: - name: Request sync - description: Send a request_sync command to Google. fields: agent_user_id: - name: Agent user ID - description: "Only needed for automations. Specific Home Assistant user id (not username, ID in configuration > users > under username) to sync with Google Assistant. Do not need when you call this service through Home Assistant front end or API. Used in automation script or other place where context.user_id is missing." selector: text: diff --git a/homeassistant/components/google_assistant/strings.json b/homeassistant/components/google_assistant/strings.json new file mode 100644 index 00000000000000..cb01a0febf5282 --- /dev/null +++ b/homeassistant/components/google_assistant/strings.json @@ -0,0 +1,14 @@ +{ + "services": { + "request_sync": { + "name": "Request sync", + "description": "Sends a request_sync command to Google.", + "fields": { + "agent_user_id": { + "name": "Agent user ID", + "description": "Only needed for automations. Specific Home Assistant user id (not username, ID in configuration > users > under username) to sync with Google Assistant. Do not need when you call this service through Home Assistant front end or API. Used in automation script or other place where context.user_id is missing." + } + } + } + } +} diff --git a/homeassistant/components/google_assistant_sdk/services.yaml b/homeassistant/components/google_assistant_sdk/services.yaml index fc2a3ad264f561..f8853ec93ea327 100644 --- a/homeassistant/components/google_assistant_sdk/services.yaml +++ b/homeassistant/components/google_assistant_sdk/services.yaml @@ -1,16 +1,10 @@ send_text_command: - name: Send text command - description: Send a command as a text query to Google Assistant. fields: command: - name: Command - description: Command(s) to send to Google Assistant. example: turn off kitchen TV selector: text: media_player: - name: Media Player Entity - description: Name(s) of media player entities to play response on example: media_player.living_room_speaker selector: entity: diff --git a/homeassistant/components/google_assistant_sdk/strings.json b/homeassistant/components/google_assistant_sdk/strings.json index 66a2b975b5e15a..e9e2b7d4c0932f 100644 --- a/homeassistant/components/google_assistant_sdk/strings.json +++ b/homeassistant/components/google_assistant_sdk/strings.json @@ -38,5 +38,21 @@ }, "application_credentials": { "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Assistant SDK. You also need to create Application Credentials linked to your account:\n1. Go to [Credentials]({oauth_creds_url}) and click **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **Web application** for the Application Type.\n\n" + }, + "services": { + "send_text_command": { + "name": "Send text command", + "description": "Sends a command as a text query to Google Assistant.", + "fields": { + "command": { + "name": "Command", + "description": "Command(s) to send to Google Assistant." + }, + "media_player": { + "name": "Media player entity", + "description": "Name(s) of media player entities to play response on." + } + } + } } } diff --git a/homeassistant/components/google_mail/services.yaml b/homeassistant/components/google_mail/services.yaml index 76ef40fa3aa781..9ce1c41f27a3a5 100644 --- a/homeassistant/components/google_mail/services.yaml +++ b/homeassistant/components/google_mail/services.yaml @@ -1,6 +1,4 @@ set_vacation: - name: Set Vacation - description: Set vacation responder settings for Google Mail. target: device: integration: google_mail @@ -8,46 +6,30 @@ set_vacation: integration: google_mail fields: enabled: - name: Enabled required: true default: true - description: Turn this off to end vacation responses. selector: boolean: title: - name: Title - description: The subject for the email selector: text: message: - name: Message - description: Body of the email required: true selector: text: plain_text: - name: Plain text default: true - description: Choose to send message in plain text or HTML. selector: boolean: restrict_contacts: - name: Restrict to Contacts - description: Restrict automatic reply to contacts. selector: boolean: restrict_domain: - name: Restrict to Domain - description: Restrict automatic reply to domain. This only affects GSuite accounts. selector: boolean: start: - name: start - description: First day of the vacation selector: date: end: - name: end - description: Last day of the vacation selector: date: diff --git a/homeassistant/components/google_mail/strings.json b/homeassistant/components/google_mail/strings.json index 2f76806dfd351a..db242479783ad9 100644 --- a/homeassistant/components/google_mail/strings.json +++ b/homeassistant/components/google_mail/strings.json @@ -37,5 +37,45 @@ "name": "Vacation end date" } } + }, + "services": { + "set_vacation": { + "name": "Set vacation", + "description": "Sets vacation responder settings for Google Mail.", + "fields": { + "enabled": { + "name": "Enabled", + "description": "Turn this off to end vacation responses." + }, + "title": { + "name": "Title", + "description": "The subject for the email." + }, + "message": { + "name": "Message", + "description": "Body of the email." + }, + "plain_text": { + "name": "Plain text", + "description": "Choose to send message in plain text or HTML." + }, + "restrict_contacts": { + "name": "Restrict to contacts", + "description": "Restrict automatic reply to contacts." + }, + "restrict_domain": { + "name": "Restrict to domain", + "description": "Restrict automatic reply to domain. This only affects GSuite accounts." + }, + "start": { + "name": "Start", + "description": "First day of the vacation." + }, + "end": { + "name": "End", + "description": "Last day of the vacation." + } + } + } } } diff --git a/homeassistant/components/google_sheets/services.yaml b/homeassistant/components/google_sheets/services.yaml index 7524ba50fb5aa0..169352d1bac163 100644 --- a/homeassistant/components/google_sheets/services.yaml +++ b/homeassistant/components/google_sheets/services.yaml @@ -1,23 +1,15 @@ append_sheet: - name: Append to Sheet - description: Append data to a worksheet in Google Sheets. fields: config_entry: - name: Sheet - description: The sheet to add data to required: true selector: config_entry: integration: google_sheets worksheet: - name: Worksheet - description: Name of the worksheet. Defaults to the first one in the document. example: "Sheet1" selector: text: data: - name: Data - description: Data to be appended to the worksheet. This puts the values on a new row underneath the matching column (key). Any new key is placed on the top of a new column. required: true example: '{"hello": world, "cool": True, "count": 5}' selector: diff --git a/homeassistant/components/google_sheets/strings.json b/homeassistant/components/google_sheets/strings.json index 602301758f8b8b..b2cba19031ee50 100644 --- a/homeassistant/components/google_sheets/strings.json +++ b/homeassistant/components/google_sheets/strings.json @@ -31,5 +31,25 @@ }, "application_credentials": { "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Sheets. You also need to create Application Credentials linked to your account:\n1. Go to [Credentials]({oauth_creds_url}) and click **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **Web application** for the Application Type.\n\n" + }, + "services": { + "append_sheet": { + "name": "Append to sheet", + "description": "Appends data to a worksheet in Google Sheets.", + "fields": { + "config_entry": { + "name": "Sheet", + "description": "The sheet to add data to." + }, + "worksheet": { + "name": "Worksheet", + "description": "Name of the worksheet. Defaults to the first one in the document." + }, + "data": { + "name": "Data", + "description": "Data to be appended to the worksheet. This puts the values on a new row underneath the matching column (key). Any new key is placed on the top of a new column." + } + } + } } } diff --git a/homeassistant/components/guardian/services.yaml b/homeassistant/components/guardian/services.yaml index 7415ac626a9247..c8f2414a87bbc9 100644 --- a/homeassistant/components/guardian/services.yaml +++ b/homeassistant/components/guardian/services.yaml @@ -1,68 +1,46 @@ # Describes the format for available Elexa Guardians services pair_sensor: - name: Pair Sensor - description: Add a new paired sensor to the valve controller. fields: device_id: - name: Valve Controller - description: The valve controller to add the sensor to required: true selector: device: integration: guardian uid: - name: UID - description: The UID of the paired sensor required: true example: 5410EC688BCF selector: text: unpair_sensor: - name: Unpair Sensor - description: Remove a paired sensor from the valve controller. fields: device_id: - name: Valve Controller - description: The valve controller to remove the sensor from required: true selector: device: integration: guardian uid: - name: UID - description: The UID of the paired sensor required: true example: 5410EC688BCF selector: text: upgrade_firmware: - name: Upgrade firmware - description: Upgrade the device firmware. fields: device_id: - name: Valve Controller - description: The valve controller whose firmware should be upgraded required: true selector: device: integration: guardian url: - name: URL - description: The URL of the server hosting the firmware file. example: https://repo.guardiancloud.services/gvc/fw selector: text: port: - name: Port - description: The port on which the firmware file is served. example: 443 selector: number: min: 1 max: 65535 filename: - name: Filename - description: The firmware filename. example: latest.bin selector: text: diff --git a/homeassistant/components/guardian/strings.json b/homeassistant/components/guardian/strings.json index ec2ad8d77cca71..f416adac027a01 100644 --- a/homeassistant/components/guardian/strings.json +++ b/homeassistant/components/guardian/strings.json @@ -45,5 +45,57 @@ "name": "Valve controller" } } + }, + "services": { + "pair_sensor": { + "name": "Pair sensor", + "description": "Adds a new paired sensor to the valve controller.", + "fields": { + "device_id": { + "name": "Valve controller", + "description": "The valve controller to add the sensor to." + }, + "uid": { + "name": "UID", + "description": "The UID of the paired sensor." + } + } + }, + "unpair_sensor": { + "name": "Unpair sensor", + "description": "Removes a paired sensor from the valve controller.", + "fields": { + "device_id": { + "name": "Valve controller", + "description": "The valve controller to remove the sensor from." + }, + "uid": { + "name": "UID", + "description": "The UID of the paired sensor." + } + } + }, + "upgrade_firmware": { + "name": "Upgrade firmware", + "description": "Upgrades the device firmware.", + "fields": { + "device_id": { + "name": "Valve controller", + "description": "The valve controller whose firmware should be upgraded." + }, + "url": { + "name": "URL", + "description": "The URL of the server hosting the firmware file." + }, + "port": { + "name": "Port", + "description": "The port on which the firmware file is served." + }, + "filename": { + "name": "Filename", + "description": "The firmware filename." + } + } + } } } diff --git a/homeassistant/components/habitica/services.yaml b/homeassistant/components/habitica/services.yaml index e60e2238088196..a7ef39eb5299f6 100644 --- a/homeassistant/components/habitica/services.yaml +++ b/homeassistant/components/habitica/services.yaml @@ -1,25 +1,17 @@ # Describes the format for Habitica service api_call: - name: API name - description: Call Habitica API fields: name: - name: Name - description: Habitica's username to call for required: true example: "xxxNotAValidNickxxx" selector: text: path: - name: Path - description: "Items from API URL in form of an array with method attached at the end. Consult https://habitica.com/apidoc/. Example uses https://habitica.com/apidoc/#api-Task-CreateUserTasks" required: true example: '["tasks", "user", "post"]' selector: object: args: - name: Args - description: Any additional JSON or URL parameter arguments. See apidoc mentioned for path. Example uses same API endpoint example: '{"text": "Use API from Home Assistant", "type": "todo"}' selector: object: diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index 3fe73d8466777b..8d2fb38517d977 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -11,12 +11,32 @@ "user": { "data": { "url": "[%key:common::config_flow::data::url%]", - "name": "Override for Habitica’s username. Will be used for service calls", - "api_user": "Habitica’s API user ID", + "name": "Override for Habitica\u2019s username. Will be used for service calls", + "api_user": "Habitica\u2019s API user ID", "api_key": "[%key:common::config_flow::data::api_key%]" }, "description": "Connect your Habitica profile to allow monitoring of your user's profile and tasks. Note that api_id and api_key must be gotten from https://habitica.com/user/settings/api" } } + }, + "services": { + "api_call": { + "name": "API name", + "description": "Calls Habitica API.", + "fields": { + "name": { + "name": "Name", + "description": "Habitica's username to call for." + }, + "path": { + "name": "Path", + "description": "Items from API URL in form of an array with method attached at the end. Consult https://habitica.com/apidoc/. Example uses https://habitica.com/apidoc/#api-Task-CreateUserTasks." + }, + "args": { + "name": "Args", + "description": "Any additional JSON or URL parameter arguments. See apidoc mentioned for path. Example uses same API endpoint." + } + } + } } } diff --git a/homeassistant/components/harmony/services.yaml b/homeassistant/components/harmony/services.yaml index fd53912397a3fc..be2a3178a8b7ff 100644 --- a/homeassistant/components/harmony/services.yaml +++ b/homeassistant/components/harmony/services.yaml @@ -1,22 +1,16 @@ sync: - name: Sync - description: Syncs the remote's configuration. target: entity: integration: harmony domain: remote change_channel: - name: Change channel - description: Sends change channel command to the Harmony HUB target: entity: integration: harmony domain: remote fields: channel: - name: Channel - description: Channel number to change to required: true selector: number: diff --git a/homeassistant/components/harmony/strings.json b/homeassistant/components/harmony/strings.json index 62de202372b7bf..8e2b435483f4e2 100644 --- a/homeassistant/components/harmony/strings.json +++ b/homeassistant/components/harmony/strings.json @@ -41,5 +41,21 @@ } } } + }, + "services": { + "sync": { + "name": "Sync", + "description": "Syncs the remote's configuration." + }, + "change_channel": { + "name": "Change channel", + "description": "Sends change channel command to the Harmony HUB.", + "fields": { + "channel": { + "name": "Channel", + "description": "Channel number to change to." + } + } + } } } diff --git a/homeassistant/components/hdmi_cec/services.yaml b/homeassistant/components/hdmi_cec/services.yaml index 7ad8b36473f859..e4102c44208329 100644 --- a/homeassistant/components/hdmi_cec/services.yaml +++ b/homeassistant/components/hdmi_cec/services.yaml @@ -1,74 +1,43 @@ power_on: - name: Power on - description: Power on all devices which supports it. select_device: - name: Select device - description: Select HDMI device. fields: device: - name: Device - description: Address of device to select. Can be entity_id, physical address or alias from configuration. required: true example: '"switch.hdmi_1" or "1.1.0.0" or "01:10"' selector: text: send_command: - name: Send command - description: Sends CEC command into HDMI CEC capable adapter. fields: att: - name: Att - description: Optional parameters. example: [0, 2] selector: object: cmd: - name: Command - description: 'Command itself. Could be decimal number or string with hexadeximal notation: "0x10".' example: 144 or "0x90" selector: text: dst: - name: Destination - description: 'Destination for command. Could be decimal number or string with hexadeximal notation: "0x10".' example: 5 or "0x5" selector: text: raw: - name: Raw - description: >- - Raw CEC command in format "00:00:00:00" where first two digits - are source and destination, second byte is command and optional other bytes - are command parameters. If raw command specified, other params are ignored. example: '"10:36"' selector: text: src: - name: Source - description: 'Source of command. Could be decimal number or string with hexadeximal notation: "0x10".' example: 12 or "0xc" selector: text: standby: - name: Standby - description: Standby all devices which supports it. update: - name: Update - description: Update devices state from network. volume: - name: Volume - description: Increase or decrease volume of system. fields: down: - name: Down - description: Decreases volume x levels. selector: number: min: 1 max: 100 mute: - name: Mute - description: Mutes audio system. selector: select: options: @@ -76,8 +45,6 @@ volume: - "on" - "toggle" up: - name: Up - description: Increases volume x levels. selector: number: min: 1 diff --git a/homeassistant/components/hdmi_cec/strings.json b/homeassistant/components/hdmi_cec/strings.json new file mode 100644 index 00000000000000..6efc9ec4272cb7 --- /dev/null +++ b/homeassistant/components/hdmi_cec/strings.json @@ -0,0 +1,70 @@ +{ + "services": { + "power_on": { + "name": "Power on", + "description": "Power on all devices which supports it." + }, + "select_device": { + "name": "Select device", + "description": "Select HDMI device.", + "fields": { + "device": { + "name": "Device", + "description": "Address of device to select. Can be entity_id, physical address or alias from configuration." + } + } + }, + "send_command": { + "name": "Send command", + "description": "Sends CEC command into HDMI CEC capable adapter.", + "fields": { + "att": { + "name": "Att", + "description": "Optional parameters." + }, + "cmd": { + "name": "Command", + "description": "Command itself. Could be decimal number or string with hexadeximal notation: \"0x10\"." + }, + "dst": { + "name": "Destination", + "description": "Destination for command. Could be decimal number or string with hexadeximal notation: \"0x10\"." + }, + "raw": { + "name": "Raw", + "description": "Raw CEC command in format \"00:00:00:00\" where first two digits are source and destination, second byte is command and optional other bytes are command parameters. If raw command specified, other params are ignored." + }, + "src": { + "name": "Source", + "description": "Source of command. Could be decimal number or string with hexadeximal notation: \"0x10\"." + } + } + }, + "standby": { + "name": "Standby", + "description": "Standby all devices which supports it." + }, + "update": { + "name": "Update", + "description": "Updates devices state from network." + }, + "volume": { + "name": "Volume", + "description": "Increases or decreases volume of system.", + "fields": { + "down": { + "name": "Down", + "description": "Decreases volume x levels." + }, + "mute": { + "name": "Mute", + "description": "Mutes audio system." + }, + "up": { + "name": "Up", + "description": "Increases volume x levels." + } + } + } + } +} diff --git a/homeassistant/components/heos/services.yaml b/homeassistant/components/heos/services.yaml index 320ed297873c9e..8dc222d65baf22 100644 --- a/homeassistant/components/heos/services.yaml +++ b/homeassistant/components/heos/services.yaml @@ -1,22 +1,14 @@ sign_in: - name: Sign in - description: Sign the controller in to a HEOS account. fields: username: - name: Username - description: The username or email of the HEOS account. required: true example: "example@example.com" selector: text: password: - name: Password - description: The password of the HEOS account. required: true example: "password" selector: text: sign_out: - name: Sign out - description: Sign the controller out of the HEOS account. diff --git a/homeassistant/components/heos/strings.json b/homeassistant/components/heos/strings.json index 09ada292afde62..635fe08cccc4c0 100644 --- a/homeassistant/components/heos/strings.json +++ b/homeassistant/components/heos/strings.json @@ -15,5 +15,25 @@ "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } + }, + "services": { + "sign_in": { + "name": "Sign in", + "description": "Signs the controller in to a HEOS account.", + "fields": { + "username": { + "name": "Username", + "description": "The username or email of the HEOS account." + }, + "password": { + "name": "Password", + "description": "The password of the HEOS account." + } + } + }, + "sign_out": { + "name": "Sign out", + "description": "Signs the controller out of the HEOS account." + } } } diff --git a/homeassistant/components/hive/services.yaml b/homeassistant/components/hive/services.yaml index d0de9645c6a152..9606624623028d 100644 --- a/homeassistant/components/hive/services.yaml +++ b/homeassistant/components/hive/services.yaml @@ -1,21 +1,15 @@ boost_heating: - name: Boost Heating (To be deprecated) - description: To be deprecated please use boost_heating_on. target: entity: integration: hive domain: climate fields: time_period: - name: Time Period - description: Set the time period for the boost. required: true example: 01:30:00 selector: time: temperature: - name: Temperature - description: Set the target temperature for the boost period. default: 25.0 selector: number: @@ -24,23 +18,17 @@ boost_heating: step: 0.5 unit_of_measurement: ° boost_heating_on: - name: Boost Heating On - description: Set the boost mode ON defining the period of time and the desired target temperature for the boost. target: entity: integration: hive domain: climate fields: time_period: - name: Time Period - description: Set the time period for the boost. required: true example: 01:30:00 selector: time: temperature: - name: Temperature - description: Set the target temperature for the boost period. default: 25.0 selector: number: @@ -49,39 +37,27 @@ boost_heating_on: step: 0.5 unit_of_measurement: ° boost_heating_off: - name: Boost Heating Off - description: Set the boost mode OFF. fields: entity_id: - name: Entity ID - description: Select entity_id to turn boost off. required: true selector: entity: integration: hive domain: climate boost_hot_water: - name: Boost Hotwater - description: Set the boost mode ON or OFF defining the period of time for the boost. fields: entity_id: - name: Entity ID - description: Select entity_id to boost. required: true selector: entity: integration: hive domain: water_heater time_period: - name: Time Period - description: Set the time period for the boost. required: true example: 01:30:00 selector: time: on_off: - name: Mode - description: Set the boost function on or off. required: true selector: select: diff --git a/homeassistant/components/hive/strings.json b/homeassistant/components/hive/strings.json index 3435517aec70bc..495c5dad1cc133 100644 --- a/homeassistant/components/hive/strings.json +++ b/homeassistant/components/hive/strings.json @@ -56,5 +56,63 @@ } } } + }, + "services": { + "boost_heating": { + "name": "Boost heating (to be deprecated)", + "description": "To be deprecated please use boost_heating_on.", + "fields": { + "time_period": { + "name": "Time period", + "description": "Set the time period for the boost." + }, + "temperature": { + "name": "Temperature", + "description": "Set the target temperature for the boost period." + } + } + }, + "boost_heating_on": { + "name": "Boost heating on", + "description": "Sets the boost mode ON defining the period of time and the desired target temperature for the boost.", + "fields": { + "time_period": { + "name": "Time Period", + "description": "Set the time period for the boost." + }, + "temperature": { + "name": "Temperature", + "description": "Set the target temperature for the boost period." + } + } + }, + "boost_heating_off": { + "name": "Boost heating off", + "description": "Sets the boost mode OFF.", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Select entity_id to turn boost off." + } + } + }, + "boost_hot_water": { + "name": "Boost hotwater", + "description": "Sets the boost mode ON or OFF defining the period of time for the boost.", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Select entity_id to boost." + }, + "time_period": { + "name": "Time period", + "description": "Set the time period for the boost." + }, + "on_off": { + "name": "Mode", + "description": "Set the boost function on or off." + } + } + } } } diff --git a/homeassistant/components/home_connect/services.yaml b/homeassistant/components/home_connect/services.yaml index 06a646dd481460..0738b58595a76a 100644 --- a/homeassistant/components/home_connect/services.yaml +++ b/homeassistant/components/home_connect/services.yaml @@ -1,168 +1,112 @@ start_program: - name: Start program - description: Selects a program and starts it. fields: device_id: - name: Device ID - description: Id of the device. required: true selector: device: integration: home_connect program: - name: Program - description: Program to select example: "Dishcare.Dishwasher.Program.Auto2" required: true selector: text: key: - name: Option key - description: Key of the option. example: "BSH.Common.Option.StartInRelative" selector: text: value: - name: Option value - description: Value of the option. example: 1800 selector: object: unit: - name: Option unit - description: Unit for the option. example: "seconds" selector: text: select_program: - name: Select program - description: Selects a program without starting it. fields: device_id: - name: Device ID - description: Id of the device. required: true selector: device: integration: home_connect program: - name: Program - description: Program to select example: "Dishcare.Dishwasher.Program.Auto2" required: true selector: text: key: - name: Option key - description: Key of the option. example: "BSH.Common.Option.StartInRelative" selector: text: value: - name: Option value - description: Value of the option. example: 1800 selector: object: unit: - name: Option unit - description: Unit for the option. example: "seconds" selector: text: pause_program: - name: Pause program - description: Pauses the current running program. fields: device_id: - name: Device ID - description: Id of the device. required: true selector: device: integration: home_connect resume_program: - name: Resume program - description: Resumes a paused program. fields: device_id: - name: Device ID - description: Id of the device. required: true selector: device: integration: home_connect set_option_active: - name: Set active program option - description: Sets an option for the active program. fields: device_id: - name: Device ID - description: Id of the device. required: true selector: device: integration: home_connect key: - name: Key - description: Key of the option. example: "LaundryCare.Dryer.Option.DryingTarget" required: true selector: text: value: - name: Value - description: Value of the option. example: "LaundryCare.Dryer.EnumType.DryingTarget.IronDry" required: true selector: object: set_option_selected: - name: Set selected program option - description: Sets an option for the selected program. fields: device_id: - name: Device ID - description: Id of the device. required: true selector: device: integration: home_connect key: - name: Key - description: Key of the option. example: "LaundryCare.Dryer.Option.DryingTarget" required: true selector: text: value: - name: Value - description: Value of the option. example: "LaundryCare.Dryer.EnumType.DryingTarget.IronDry" required: true selector: object: change_setting: - name: Change setting - description: Changes a setting. fields: device_id: - name: Device ID - description: Id of the device. required: true selector: device: integration: home_connect key: - name: Key - description: Key of the setting. example: "BSH.Common.Setting.ChildLock" required: true selector: text: value: - name: Value - description: Value of the setting. example: "true" required: true selector: diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 79455783edf7e9..41eedbe83a87d8 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -12,5 +12,133 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "services": { + "start_program": { + "name": "Start program", + "description": "Selects a program and starts it.", + "fields": { + "device_id": { + "name": "Device ID", + "description": "Id of the device." + }, + "program": { + "name": "Program", + "description": "Program to select." + }, + "key": { + "name": "Option key", + "description": "Key of the option." + }, + "value": { + "name": "Option value", + "description": "Value of the option." + }, + "unit": { + "name": "Option unit", + "description": "Unit for the option." + } + } + }, + "select_program": { + "name": "Select program", + "description": "Selects a program without starting it.", + "fields": { + "device_id": { + "name": "Device ID", + "description": "Id of the device." + }, + "program": { + "name": "Program", + "description": "Program to select." + }, + "key": { + "name": "Option key", + "description": "Key of the option." + }, + "value": { + "name": "Option value", + "description": "Value of the option." + }, + "unit": { + "name": "Option unit", + "description": "Unit for the option." + } + } + }, + "pause_program": { + "name": "Pause program", + "description": "Pauses the current running program.", + "fields": { + "device_id": { + "name": "Device ID", + "description": "Id of the device." + } + } + }, + "resume_program": { + "name": "Resume program", + "description": "Resumes a paused program.", + "fields": { + "device_id": { + "name": "Device ID", + "description": "Id of the device." + } + } + }, + "set_option_active": { + "name": "Set active program option", + "description": "Sets an option for the active program.", + "fields": { + "device_id": { + "name": "Device ID", + "description": "Id of the device." + }, + "key": { + "name": "Key", + "description": "Key of the option." + }, + "value": { + "name": "Value", + "description": "Value of the option." + } + } + }, + "set_option_selected": { + "name": "Set selected program option", + "description": "Sets an option for the selected program.", + "fields": { + "device_id": { + "name": "Device ID", + "description": "Id of the device." + }, + "key": { + "name": "Key", + "description": "Key of the option." + }, + "value": { + "name": "Value", + "description": "Value of the option." + } + } + }, + "change_setting": { + "name": "Change setting", + "description": "Changes a setting.", + "fields": { + "device_id": { + "name": "Device ID", + "description": "Id of the device." + }, + "key": { + "name": "Key", + "description": "Key of the setting." + }, + "value": { + "name": "Value", + "description": "Value of the setting." + } + } + } } } diff --git a/homeassistant/components/homekit/services.yaml b/homeassistant/components/homekit/services.yaml index a982e9ccf8d5e8..de271db0ad9c70 100644 --- a/homeassistant/components/homekit/services.yaml +++ b/homeassistant/components/homekit/services.yaml @@ -1,18 +1,11 @@ # Describes the format for available HomeKit services reload: - name: Reload - description: Reload homekit and re-process YAML configuration - reset_accessory: - name: Reset accessory - description: Reset a HomeKit accessory target: entity: {} unpair: - name: Unpair an accessory or bridge - description: Forcefully remove all pairings from an accessory to allow re-pairing. Use this service if the accessory is no longer responsive, and you want to avoid deleting and re-adding the entry. Room locations, and accessory preferences will be lost. target: device: integration: homekit diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index 74af388df85659..83177345159690 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -24,14 +24,14 @@ "data": { "entities": "Entities" }, - "description": "Select entities from each domain in “{domains}”. The include will cover the entire domain if you do not select any entities for a given domain.", + "description": "Select entities from each domain in \u201c{domains}\u201d. The include will cover the entire domain if you do not select any entities for a given domain.", "title": "Select the entities to be included" }, "exclude": { "data": { "entities": "[%key:component::homekit::options::step::include::data::entities%]" }, - "description": "All “{domains}” entities will be included except for the excluded entities and categorized entities.", + "description": "All \u201c{domains}\u201d entities will be included except for the excluded entities and categorized entities.", "title": "Select the entities to be excluded" }, "cameras": { @@ -68,5 +68,19 @@ "abort": { "port_name_in_use": "An accessory or bridge with the same name or port is already configured." } + }, + "services": { + "reload": { + "name": "Reload", + "description": "Reloads homekit and re-process YAML-configuration." + }, + "reset_accessory": { + "name": "Reset accessory", + "description": "Resets a HomeKit accessory." + }, + "unpair": { + "name": "Unpair an accessory or bridge", + "description": "Forcefully removes all pairings from an accessory to allow re-pairing. Use this service if the accessory is no longer responsive, and you want to avoid deleting and re-adding the entry. Room locations, and accessory preferences will be lost." + } } } diff --git a/homeassistant/components/homematic/services.yaml b/homeassistant/components/homematic/services.yaml index 28b6577cdf9dfb..529079666883e6 100644 --- a/homeassistant/components/homematic/services.yaml +++ b/homeassistant/components/homematic/services.yaml @@ -1,105 +1,73 @@ # Describes the format for available component services virtualkey: - name: Virtual key - description: Press a virtual key from CCU/Homegear or simulate keypress. fields: address: - name: Address - description: Address of homematic device or BidCoS-RF for virtual remote. required: true example: BidCoS-RF selector: text: channel: - name: Channel - description: Channel for calling a keypress. required: true selector: number: min: 1 max: 6 param: - name: Param - description: Event to send i.e. PRESS_LONG, PRESS_SHORT. required: true example: PRESS_LONG selector: text: interface: - name: Interface - description: Set an interface value. example: Interfaces name from config selector: text: set_variable_value: - name: Set variable value - description: Set the name of a node. fields: entity_id: - name: Entity - description: Name(s) of homematic central to set value. selector: entity: domain: homematic name: - name: Name - description: Name of the variable to set. required: true example: "testvariable" selector: text: value: - name: Value - description: New value required: true example: 1 selector: text: set_device_value: - name: Set device value - description: Set a device property on RPC XML interface. fields: address: - name: Address - description: Address of homematic device or BidCoS-RF for virtual remote required: true example: BidCoS-RF selector: text: channel: - name: Channel - description: Channel for calling a keypress required: true selector: number: min: 1 max: 6 param: - name: Param - description: Event to send i.e. PRESS_LONG, PRESS_SHORT required: true example: PRESS_LONG selector: text: interface: - name: Interface - description: Set an interface value example: Interfaces name from config selector: text: value: - name: Value - description: New value required: true example: 1 selector: text: value_type: - name: Value type - description: Type for new value selector: select: options: @@ -110,31 +78,20 @@ set_device_value: - "string" reconnect: - name: Reconnect - description: Reconnect to all Homematic Hubs. - set_install_mode: - name: Set install mode - description: Set a RPC XML interface into installation mode. fields: interface: - name: Interface - description: Select the given interface into install mode required: true example: Interfaces name from config selector: text: mode: - name: Mode - description: 1= Normal mode / 2= Remove exists old links default: 1 selector: number: min: 1 max: 2 time: - name: Time - description: Time to run in install mode default: 60 selector: number: @@ -142,47 +99,33 @@ set_install_mode: max: 3600 unit_of_measurement: seconds address: - name: Address - description: Address of homematic device or BidCoS-RF to learn example: LEQ3948571 selector: text: put_paramset: - name: Put paramset - description: Call to putParamset in the RPC XML interface fields: interface: - name: Interface - description: The interfaces name from the config required: true example: wireless selector: text: address: - name: Address - description: Address of Homematic device required: true example: LEQ3948571:0 selector: text: paramset_key: - name: Paramset key - description: The paramset_key argument to putParamset required: true example: MASTER selector: text: paramset: - name: Paramset - description: A paramset dictionary required: true example: '{"WEEK_PROGRAM_POINTER": 1}' selector: object: rx_mode: - name: RX mode - description: The receive mode used. example: BURST selector: text: diff --git a/homeassistant/components/homematic/strings.json b/homeassistant/components/homematic/strings.json new file mode 100644 index 00000000000000..14f723694fca6c --- /dev/null +++ b/homeassistant/components/homematic/strings.json @@ -0,0 +1,126 @@ +{ + "services": { + "virtualkey": { + "name": "Virtual key", + "description": "Presses a virtual key from CCU/Homegear or simulate keypress.", + "fields": { + "address": { + "name": "Address", + "description": "Address of homematic device or BidCoS-RF for virtual remote." + }, + "channel": { + "name": "Channel", + "description": "Channel for calling a keypress." + }, + "param": { + "name": "Param", + "description": "Event to send i.e. PRESS_LONG, PRESS_SHORT." + }, + "interface": { + "name": "Interface", + "description": "Set an interface value." + } + } + }, + "set_variable_value": { + "name": "Set variable value", + "description": "Sets the name of a node.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name(s) of homematic central to set value." + }, + "name": { + "name": "Name", + "description": "Name of the variable to set." + }, + "value": { + "name": "Value", + "description": "New value." + } + } + }, + "set_device_value": { + "name": "Set device value", + "description": "Sets a device property on RPC XML interface.", + "fields": { + "address": { + "name": "Address", + "description": "Address of homematic device or BidCoS-RF for virtual remote." + }, + "channel": { + "name": "Channel", + "description": "Channel for calling a keypress." + }, + "param": { + "name": "Param", + "description": "Event to send i.e. PRESS_LONG, PRESS_SHORT." + }, + "interface": { + "name": "Interface", + "description": "Set an interface value." + }, + "value": { + "name": "Value", + "description": "New value." + }, + "value_type": { + "name": "Value type", + "description": "Type for new value." + } + } + }, + "reconnect": { + "name": "Reconnect", + "description": "Reconnects to all Homematic Hubs." + }, + "set_install_mode": { + "name": "Set install mode", + "description": "Set a RPC XML interface into installation mode.", + "fields": { + "interface": { + "name": "Interface", + "description": "Select the given interface into install mode." + }, + "mode": { + "name": "Mode", + "description": "1= Normal mode / 2= Remove exists old links." + }, + "time": { + "name": "Time", + "description": "Time to run in install mode." + }, + "address": { + "name": "Address", + "description": "Address of homematic device or BidCoS-RF to learn." + } + } + }, + "put_paramset": { + "name": "Put paramset", + "description": "Calls to putParamset in the RPC XML interface.", + "fields": { + "interface": { + "name": "Interface", + "description": "The interfaces name from the config." + }, + "address": { + "name": "Address", + "description": "Address of Homematic device." + }, + "paramset_key": { + "name": "Paramset key", + "description": "The paramset_key argument to putParamset." + }, + "paramset": { + "name": "Paramset", + "description": "A paramset dictionary." + }, + "rx_mode": { + "name": "RX mode", + "description": "The receive mode used." + } + } + } + } +} diff --git a/homeassistant/components/homematicip_cloud/services.yaml b/homeassistant/components/homematicip_cloud/services.yaml index ebb83a0845f5d3..9e8313397877f2 100644 --- a/homeassistant/components/homematicip_cloud/services.yaml +++ b/homeassistant/components/homematicip_cloud/services.yaml @@ -1,12 +1,8 @@ # Describes the format for available component services activate_eco_mode_with_duration: - name: Activate eco mode with duration - description: Activate eco mode with period. fields: duration: - name: Duration - description: The duration of eco mode in minutes. required: true selector: number: @@ -14,44 +10,30 @@ activate_eco_mode_with_duration: max: 1440 unit_of_measurement: "minutes" accesspoint_id: - name: Accesspoint ID - description: The ID of the Homematic IP Access Point example: 3014xxxxxxxxxxxxxxxxxxxx selector: text: activate_eco_mode_with_period: - name: Activate eco more with period - description: Activate eco mode with period. fields: endtime: - name: Endtime - description: The time when the eco mode should automatically be disabled. required: true example: 2019-02-17 14:00 selector: text: accesspoint_id: - name: Accesspoint ID - description: The ID of the Homematic IP Access Point example: 3014xxxxxxxxxxxxxxxxxxxx selector: text: activate_vacation: - name: Activate vacation - description: Activates the vacation mode until the given time. fields: endtime: - name: Endtime - description: The time when the vacation mode should automatically be disabled. required: true example: 2019-09-17 14:00 selector: text: temperature: - name: Temperature - description: the set temperature during the vacation mode. required: true default: 18 selector: @@ -61,48 +43,32 @@ activate_vacation: step: 0.5 unit_of_measurement: "°" accesspoint_id: - name: Accesspoint ID - description: The ID of the Homematic IP Access Point example: 3014xxxxxxxxxxxxxxxxxxxx selector: text: deactivate_eco_mode: - name: Deactivate eco mode - description: Deactivates the eco mode immediately. fields: accesspoint_id: - name: Accesspoint ID - description: The ID of the Homematic IP Access Point example: 3014xxxxxxxxxxxxxxxxxxxx selector: text: deactivate_vacation: - name: Deactivate vacation - description: Deactivates the vacation mode immediately. fields: accesspoint_id: - name: Accesspoint ID - description: The ID of the Homematic IP Access Point example: 3014xxxxxxxxxxxxxxxxxxxx selector: text: set_active_climate_profile: - name: Set active climate profile - description: Set the active climate profile index. fields: entity_id: - name: Entity - description: The ID of the climate entity. Use 'all' keyword to switch the profile for all entities. required: true example: climate.livingroom selector: text: climate_profile_index: - name: Climate profile index - description: The index of the climate profile. required: true selector: number: @@ -110,36 +76,24 @@ set_active_climate_profile: max: 100 dump_hap_config: - name: Dump hap config - description: Dump the configuration of the Homematic IP Access Point(s). fields: config_output_path: - name: Config output path - description: (Default is 'Your home-assistant config directory') Path where to store the config. example: "/config" selector: text: config_output_file_prefix: - name: Config output file prefix - description: Name of the config file. The SGTIN of the AP will always be appended. example: "hmip-config" default: "hmip-config" selector: text: anonymize: - name: Anonymize - description: Should the Configuration be anonymized? default: true selector: boolean: reset_energy_counter: - name: Reset energy counter - description: Reset the energy counter of a measuring entity. fields: entity_id: - name: Entity - description: The ID of the measuring entity. Use 'all' keyword to reset all energy counters. required: true example: switch.livingroom selector: diff --git a/homeassistant/components/homematicip_cloud/strings.json b/homeassistant/components/homematicip_cloud/strings.json index 3e3c967f972f20..6a20c5f8a54293 100644 --- a/homeassistant/components/homematicip_cloud/strings.json +++ b/homeassistant/components/homematicip_cloud/strings.json @@ -25,5 +25,115 @@ "connection_aborted": "[%key:common::config_flow::error::cannot_connect%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "services": { + "activate_eco_mode_with_duration": { + "name": "Activate eco mode with duration", + "description": "Activates eco mode with period.", + "fields": { + "duration": { + "name": "Duration", + "description": "The duration of eco mode in minutes." + }, + "accesspoint_id": { + "name": "Accesspoint ID", + "description": "The ID of the Homematic IP Access Point." + } + } + }, + "activate_eco_mode_with_period": { + "name": "Activate eco more with period", + "description": "Activates eco mode with period.", + "fields": { + "endtime": { + "name": "Endtime", + "description": "The time when the eco mode should automatically be disabled." + }, + "accesspoint_id": { + "name": "Accesspoint ID", + "description": "The ID of the Homematic IP Access Point." + } + } + }, + "activate_vacation": { + "name": "Activate vacation", + "description": "Activates the vacation mode until the given time.", + "fields": { + "endtime": { + "name": "Endtime", + "description": "The time when the vacation mode should automatically be disabled." + }, + "temperature": { + "name": "Temperature", + "description": "The set temperature during the vacation mode." + }, + "accesspoint_id": { + "name": "Accesspoint ID", + "description": "The ID of the Homematic IP Access Point." + } + } + }, + "deactivate_eco_mode": { + "name": "Deactivate eco mode", + "description": "Deactivates the eco mode immediately.", + "fields": { + "accesspoint_id": { + "name": "Accesspoint ID", + "description": "The ID of the Homematic IP Access Point." + } + } + }, + "deactivate_vacation": { + "name": "Deactivate vacation", + "description": "Deactivates the vacation mode immediately.", + "fields": { + "accesspoint_id": { + "name": "Accesspoint ID", + "description": "The ID of the Homematic IP Access Point." + } + } + }, + "set_active_climate_profile": { + "name": "Set active climate profile", + "description": "Sets the active climate profile index.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "The ID of the climate entity. Use 'all' keyword to switch the profile for all entities." + }, + "climate_profile_index": { + "name": "Climate profile index", + "description": "The index of the climate profile." + } + } + }, + "dump_hap_config": { + "name": "Dump hap config", + "description": "Dumps the configuration of the Homematic IP Access Point(s).", + "fields": { + "config_output_path": { + "name": "Config output path", + "description": "(Default is 'Your home-assistant config directory') Path where to store the config." + }, + "config_output_file_prefix": { + "name": "Config output file prefix", + "description": "Name of the config file. The SGTIN of the AP will always be appended." + }, + "anonymize": { + "name": "Anonymize", + "description": "Should the Configuration be anonymized?" + } + } + }, + "reset_energy_counter": { + "name": "Reset energy counter", + "description": "Resets the energy counter of a measuring entity.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "The ID of the measuring entity. Use 'all' keyword to reset all energy counters." + } + } + } } } diff --git a/homeassistant/components/html5/services.yaml b/homeassistant/components/html5/services.yaml index f6b76e67cd7952..929eb5a2dc1251 100644 --- a/homeassistant/components/html5/services.yaml +++ b/homeassistant/components/html5/services.yaml @@ -1,16 +1,10 @@ dismiss: - name: Dismiss - description: Dismiss a html5 notification. fields: target: - name: Target - description: An array of targets. example: ["my_phone", "my_tablet"] selector: object: data: - name: Data - description: Extended information of notification. Supports tag. example: '{ "tag": "tagname" }' selector: object: diff --git a/homeassistant/components/html5/strings.json b/homeassistant/components/html5/strings.json new file mode 100644 index 00000000000000..fa69025c43c1b8 --- /dev/null +++ b/homeassistant/components/html5/strings.json @@ -0,0 +1,18 @@ +{ + "services": { + "dismiss": { + "name": "Dismiss", + "description": "Dismisses a html5 notification.", + "fields": { + "target": { + "name": "Target", + "description": "An array of targets." + }, + "data": { + "name": "Data", + "description": "Extended information of notification. Supports tag." + } + } + } + } +} diff --git a/homeassistant/components/huawei_lte/services.yaml b/homeassistant/components/huawei_lte/services.yaml index 711064b435e24c..9d0cf5d91e658d 100644 --- a/homeassistant/components/huawei_lte/services.yaml +++ b/homeassistant/components/huawei_lte/services.yaml @@ -1,46 +1,27 @@ clear_traffic_statistics: - name: Clear traffic statistics - description: Clear traffic statistics. fields: url: - name: URL - description: URL of router to clear; optional when only one is configured. example: http://192.168.100.1/ selector: text: reboot: - name: Reboot - description: Reboot router. fields: url: - name: URL - description: URL of router to reboot; optional when only one is configured. example: http://192.168.100.1/ selector: text: resume_integration: - name: Resume integration - description: Resume suspended integration. fields: url: - name: URL - description: URL of router to resume integration for; optional when only one is configured. example: http://192.168.100.1/ selector: text: suspend_integration: - name: Suspend integration - description: > - Suspend integration. Suspending logs the integration out from the router, and stops accessing it. - Useful e.g. if accessing the router web interface from another source such as a web browser is temporarily required. - Invoke the resume_integration service to resume. fields: url: - name: URL - description: URL of router to resume integration for; optional when only one is configured. example: http://192.168.100.1/ selector: text: diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 0eb68c959ac6fc..6f85187cfeb480 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -48,5 +48,47 @@ } } } + }, + "services": { + "clear_traffic_statistics": { + "name": "Clear traffic statistics", + "description": "Clears traffic statistics.", + "fields": { + "url": { + "name": "URL", + "description": "URL of router to clear; optional when only one is configured." + } + } + }, + "reboot": { + "name": "Reboot", + "description": "Reboots router.", + "fields": { + "url": { + "name": "URL", + "description": "URL of router to reboot; optional when only one is configured." + } + } + }, + "resume_integration": { + "name": "Resume integration", + "description": "Resumes suspended integration.", + "fields": { + "url": { + "name": "URL", + "description": "URL of router to resume integration for; optional when only one is configured." + } + } + }, + "suspend_integration": { + "name": "Suspend integration", + "description": "Suspends integration. Suspending logs the integration out from the router, and stops accessing it. Useful e.g. if accessing the router web interface from another source such as a web browser is temporarily required. Invoke the resume_integration service to resume.\n.", + "fields": { + "url": { + "name": "URL", + "description": "URL of router to resume integration for; optional when only one is configured." + } + } + } } } diff --git a/homeassistant/components/hue/services.yaml b/homeassistant/components/hue/services.yaml index b06c3934152257..a9ea57d7828e12 100644 --- a/homeassistant/components/hue/services.yaml +++ b/homeassistant/components/hue/services.yaml @@ -2,61 +2,42 @@ # legacy hue_activate_scene to activate a scene hue_activate_scene: - name: Activate scene - description: Activate a hue scene stored in the hue hub. fields: group_name: - name: Group - description: Name of hue group/room from the hue app. example: "Living Room" selector: text: scene_name: - name: Scene - description: Name of hue scene from the hue app. example: "Energize" selector: text: dynamic: - name: Dynamic - description: Enable dynamic mode of the scene (V2 bridges and supported scenes only). selector: boolean: # entity service to activate a Hue scene (V2) activate_scene: - name: Activate Hue Scene - description: Activate a Hue scene with more control over the options. target: entity: domain: scene integration: hue fields: transition: - name: Transition - description: Transition duration it takes to bring devices to the state - defined in the scene. selector: number: min: 0 max: 3600 unit_of_measurement: seconds dynamic: - name: Dynamic - description: Enable dynamic mode of the scene. selector: boolean: speed: - name: Speed - description: Speed of dynamic palette for this scene advanced: true selector: number: min: 0 max: 100 brightness: - name: Brightness - description: Set brightness for the scene. advanced: true selector: number: diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index a44eea0fe335d7..54895b6e3b2435 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -59,7 +59,6 @@ "remote_button_short_release": "\"{subtype}\" released", "remote_double_button_long_press": "Both \"{subtype}\" released after long press", "remote_double_button_short_press": "Both \"{subtype}\" released", - "initial_press": "\"{subtype}\" pressed initially", "repeat": "\"{subtype}\" held down", "short_release": "\"{subtype}\" released after short press", @@ -79,5 +78,47 @@ } } } + }, + "services": { + "hue_activate_scene": { + "name": "Activate scene", + "description": "Activates a hue scene stored in the hue hub.", + "fields": { + "group_name": { + "name": "Group", + "description": "Name of hue group/room from the hue app." + }, + "scene_name": { + "name": "Scene", + "description": "Name of hue scene from the hue app." + }, + "dynamic": { + "name": "Dynamic", + "description": "Enable dynamic mode of the scene (V2 bridges and supported scenes only)." + } + } + }, + "activate_scene": { + "name": "Activate Hue scene", + "description": "Activates a Hue scene with more control over the options.", + "fields": { + "transition": { + "name": "Transition", + "description": "Transition duration it takes to bring devices to the state defined in the scene." + }, + "dynamic": { + "name": "Dynamic", + "description": "Enable dynamic mode of the scene." + }, + "speed": { + "name": "Speed", + "description": "Speed of dynamic palette for this scene." + }, + "brightness": { + "name": "Brightness", + "description": "Set brightness for the scene." + } + } + } } } From eb3b56798d1a831a2db828c2acb5a515b118f4c7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 12:32:25 +0200 Subject: [PATCH 0401/1009] Migrate conversation services to support translations (#96365) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- .../components/conversation/services.yaml | 8 ------- .../components/conversation/strings.json | 24 ++++++++++++++++++- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/conversation/services.yaml b/homeassistant/components/conversation/services.yaml index 1a28044dcb5d9f..7b6717eec6d3e9 100644 --- a/homeassistant/components/conversation/services.yaml +++ b/homeassistant/components/conversation/services.yaml @@ -1,24 +1,16 @@ # Describes the format for available component services process: - name: Process - description: Launch a conversation from a transcribed text. fields: text: - name: Text - description: Transcribed text example: Turn all lights on required: true selector: text: language: - name: Language - description: Language of text. Defaults to server language example: NL selector: text: agent_id: - name: Agent - description: Assist engine to process your request example: homeassistant selector: conversation_agent: diff --git a/homeassistant/components/conversation/strings.json b/homeassistant/components/conversation/strings.json index dc6f2b5f52bdd0..15e783c0d90afb 100644 --- a/homeassistant/components/conversation/strings.json +++ b/homeassistant/components/conversation/strings.json @@ -1 +1,23 @@ -{ "title": "Conversation" } +{ + "title": "Conversation", + "services": { + "process": { + "name": "Process", + "description": "Launches a conversation from a transcribed text.", + "fields": { + "text": { + "name": "Text", + "description": "Transcribed text input." + }, + "language": { + "name": "Language", + "description": "Language of text. Defaults to server language." + }, + "agent_id": { + "name": "Agent", + "description": "Conversation agent to process your request. The conversation agent is the brains of your assistant. It processes the incoming text commands." + } + } + } + } +} From d0258c8fc8cf43050ae2c37e4ab4bd3f95615356 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 12:53:24 +0200 Subject: [PATCH 0402/1009] Migrate switch services to support translations (#96405) --- homeassistant/components/switch/services.yaml | 6 ------ homeassistant/components/switch/strings.json | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/switch/services.yaml b/homeassistant/components/switch/services.yaml index 33f66070bfbfda..5da203d8a80007 100644 --- a/homeassistant/components/switch/services.yaml +++ b/homeassistant/components/switch/services.yaml @@ -1,22 +1,16 @@ # Describes the format for available switch services turn_on: - name: Turn on - description: Turn a switch on target: entity: domain: switch turn_off: - name: Turn off - description: Turn a switch off target: entity: domain: switch toggle: - name: Toggle - description: Toggles a switch state target: entity: domain: switch diff --git a/homeassistant/components/switch/strings.json b/homeassistant/components/switch/strings.json index 70cd45f4d2194f..ae5a3165cd966e 100644 --- a/homeassistant/components/switch/strings.json +++ b/homeassistant/components/switch/strings.json @@ -30,5 +30,19 @@ "outlet": { "name": "Outlet" } + }, + "services": { + "turn_on": { + "name": "Turn on", + "description": "Turns a switch on." + }, + "turn_off": { + "name": "Turn off", + "description": "Turns a switch off." + }, + "toggle": { + "name": "Toggle", + "description": "Toggles a switch on/off." + } } } From c6a9c6c94872f0d259c1acb8ba8a64c98c7e7587 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 13:42:29 +0200 Subject: [PATCH 0403/1009] Migrate date services to support translations (#96317) --- homeassistant/components/date/services.yaml | 4 ---- homeassistant/components/date/strings.json | 12 ++++++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/date/services.yaml b/homeassistant/components/date/services.yaml index 7ce1210f809256..aebf5630205b1b 100644 --- a/homeassistant/components/date/services.yaml +++ b/homeassistant/components/date/services.yaml @@ -1,13 +1,9 @@ set_value: - name: Set Date - description: Set the date for a date entity. target: entity: domain: date fields: date: - name: Date - description: The date to set. required: true example: "2022/11/01" selector: diff --git a/homeassistant/components/date/strings.json b/homeassistant/components/date/strings.json index 110a4cabb921fb..9e88d3b567641f 100644 --- a/homeassistant/components/date/strings.json +++ b/homeassistant/components/date/strings.json @@ -4,5 +4,17 @@ "_": { "name": "[%key:component::date::title%]" } + }, + "services": { + "set_value": { + "name": "Set date", + "description": "Sets the date.", + "fields": { + "date": { + "name": "Date", + "description": "The date to set." + } + } + } } } From 0ca8a2618475d9bab6b6fc8e0655be6e4b2421fc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 13:42:53 +0200 Subject: [PATCH 0404/1009] Migrate datetime services to support translations (#96318) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/datetime/services.yaml | 4 ---- homeassistant/components/datetime/strings.json | 12 ++++++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/datetime/services.yaml b/homeassistant/components/datetime/services.yaml index b5cce19e88b70e..fb6f798e9bd89b 100644 --- a/homeassistant/components/datetime/services.yaml +++ b/homeassistant/components/datetime/services.yaml @@ -1,13 +1,9 @@ set_value: - name: Set Date/Time - description: Set the date/time for a datetime entity. target: entity: domain: datetime fields: datetime: - name: Date & Time - description: The date/time to set. The time zone of the Home Assistant instance is assumed. required: true example: "2022/11/01 22:15" selector: diff --git a/homeassistant/components/datetime/strings.json b/homeassistant/components/datetime/strings.json index 3b97559018c770..503d7a2ca9e564 100644 --- a/homeassistant/components/datetime/strings.json +++ b/homeassistant/components/datetime/strings.json @@ -4,5 +4,17 @@ "_": { "name": "[%key:component::datetime::title%]" } + }, + "services": { + "set_value": { + "name": "Set date/time", + "description": "Sets the date/time for a datetime entity.", + "fields": { + "datetime": { + "name": "Date & Time", + "description": "The date/time to set. The time zone of the Home Assistant instance is assumed." + } + } + } } } From cbddade4bf38b57d5e0b72717ebadcfb982173a3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 13:44:15 +0200 Subject: [PATCH 0405/1009] Migrate logbook services to support translations (#96341) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- .../components/logbook/services.yaml | 10 ------- homeassistant/components/logbook/strings.json | 26 +++++++++++++++++++ 2 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/logbook/strings.json diff --git a/homeassistant/components/logbook/services.yaml b/homeassistant/components/logbook/services.yaml index 3f6886280325df..c6722dad10bdda 100644 --- a/homeassistant/components/logbook/services.yaml +++ b/homeassistant/components/logbook/services.yaml @@ -1,29 +1,19 @@ log: - name: Log - description: Create a custom entry in your logbook. fields: name: - name: Name - description: Custom name for an entity, can be referenced with entity_id. required: true example: "Kitchen" selector: text: message: - name: Message - description: Message of the custom logbook entry. required: true example: "is being used" selector: text: entity_id: - name: Entity ID - description: Entity to reference in custom logbook entry. selector: entity: domain: - name: Domain - description: Icon of domain to display in custom logbook entry. example: "light" selector: text: diff --git a/homeassistant/components/logbook/strings.json b/homeassistant/components/logbook/strings.json new file mode 100644 index 00000000000000..10ebcc68f64b3e --- /dev/null +++ b/homeassistant/components/logbook/strings.json @@ -0,0 +1,26 @@ +{ + "services": { + "log": { + "name": "Log", + "description": "Creates a custom entry in the logbook.", + "fields": { + "name": { + "name": "Name", + "description": "Custom name for an entity, can be referenced using an `entity_id`." + }, + "message": { + "name": "Message", + "description": "Message of the logbook entry." + }, + "entity_id": { + "name": "Entity ID", + "description": "Entity to reference in the logbook entry." + }, + "domain": { + "name": "Domain", + "description": "Determines which icon is used in the logbook entry. The icon illustrates the integration domain related to this logbook entry." + } + } + } + } +} From 6a1cd628aa4c45394550e6c70b9f768fe8d1a82a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 13:45:38 +0200 Subject: [PATCH 0406/1009] Migrate script services to support translations (#96401) --- homeassistant/components/script/services.yaml | 9 --------- homeassistant/components/script/strings.json | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/script/services.yaml b/homeassistant/components/script/services.yaml index 1d3c0e8a8a9bbb..6fc3d81f19625e 100644 --- a/homeassistant/components/script/services.yaml +++ b/homeassistant/components/script/services.yaml @@ -1,26 +1,17 @@ # Describes the format for available python_script services reload: - name: Reload - description: Reload all the available scripts - turn_on: - name: Turn on - description: Turn on script target: entity: domain: script turn_off: - name: Turn off - description: Turn off script target: entity: domain: script toggle: - name: Toggle - description: Toggle script target: entity: domain: script diff --git a/homeassistant/components/script/strings.json b/homeassistant/components/script/strings.json index b9624f16a31348..e4f1b3fcd4f4bb 100644 --- a/homeassistant/components/script/strings.json +++ b/homeassistant/components/script/strings.json @@ -31,5 +31,23 @@ } } } + }, + "services": { + "reload": { + "name": "Reload", + "description": "Reloads all the available scripts." + }, + "turn_on": { + "name": "Turn on", + "description": "Runs the sequence of actions defined in a script." + }, + "turn_off": { + "name": "Turn off", + "description": "Stops a running script." + }, + "toggle": { + "name": "Toggle", + "description": "Toggle a script. Starts it, if isn't running, stops it otherwise." + } } } From ce5246a8cdc7ddba85e9bc07a556c3ad08726726 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 13:47:47 +0200 Subject: [PATCH 0407/1009] Migrate homeassistant services to support translations (#96388) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- .../components/homeassistant/services.yaml | 41 ------------ .../components/homeassistant/strings.json | 66 +++++++++++++++++++ 2 files changed, 66 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/homeassistant/services.yaml b/homeassistant/components/homeassistant/services.yaml index 2fe27769c3fbc5..899fee357fd527 100644 --- a/homeassistant/components/homeassistant/services.yaml +++ b/homeassistant/components/homeassistant/services.yaml @@ -1,88 +1,47 @@ check_config: - name: Check configuration - description: - Check the Home Assistant configuration files for errors. Errors will be - displayed in the Home Assistant log. - reload_core_config: - name: Reload core configuration - description: Reload the core configuration. - restart: - name: Restart - description: Restart the Home Assistant service. - set_location: - name: Set location - description: Update the Home Assistant location. fields: latitude: - name: Latitude - description: Latitude of your location. required: true example: 32.87336 selector: text: longitude: - name: Longitude - description: Longitude of your location. required: true example: 117.22743 selector: text: stop: - name: Stop - description: Stop the Home Assistant service. - toggle: - name: Generic toggle - description: Generic service to toggle devices on/off under any domain target: entity: {} turn_on: - name: Generic turn on - description: Generic service to turn devices on under any domain. target: entity: {} turn_off: - name: Generic turn off - description: Generic service to turn devices off under any domain. target: entity: {} update_entity: - name: Update entity - description: Force one or more entities to update its data target: entity: {} reload_custom_templates: - name: Reload custom Jinja2 templates - description: >- - Reload Jinja2 templates found in the custom_templates folder in your config. - New values will be applied on the next render of the template. - reload_config_entry: - name: Reload config entry - description: Reload a config entry that matches a target. target: entity: {} device: {} fields: entry_id: advanced: true - name: Config entry id - description: A configuration entry id required: false example: 8955375327824e14ba89e4b29cc3ec9a selector: text: save_persistent_states: - name: Save Persistent States - description: - Save the persistent states (for entities derived from RestoreEntity) immediately. - Maintain the normal periodic saving interval. diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 89da615cf31111..5a02cd196659b4 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -45,5 +45,71 @@ "version": "Version", "virtualenv": "Virtual Environment" } + }, + "services": { + "check_config": { + "name": "Check configuration", + "description": "Checks the Home Assistant YAML-configuration files for errors. Errors will be shown in the Home Assistant logs." + }, + "reload_core_config": { + "name": "Reload core configuration", + "description": "Reloads the core configuration from the YAML-configuration." + }, + "restart": { + "name": "Restart", + "description": "Restarts Home Assistant." + }, + "set_location": { + "name": "Set location", + "description": "Updates the Home Assistant location.", + "fields": { + "latitude": { + "name": "Latitude", + "description": "Latitude of your location." + }, + "longitude": { + "name": "Longitude", + "description": "Longitude of your location." + } + } + }, + "stop": { + "name": "Stop", + "description": "Stops Home Assistant." + }, + "toggle": { + "name": "Generic toggle", + "description": "Generic service to toggle devices on/off under any domain." + }, + "turn_on": { + "name": "Generic turn on", + "description": "Generic service to turn devices on under any domain." + }, + "turn_off": { + "name": "Generic turn off", + "description": "Generic service to turn devices off under any domain." + }, + "update_entity": { + "name": "Update entity", + "description": "Forces one or more entities to update its data." + }, + "reload_custom_templates": { + "name": "Reload custom Jinja2 templates", + "description": "Reloads Jinja2 templates found in the `custom_templates` folder in your config. New values will be applied on the next render of the template." + }, + "reload_config_entry": { + "name": "Reload config entry", + "description": "Reloads the specified config entry.", + "fields": { + "entry_id": { + "name": "Config entry ID", + "description": "The configuration entry ID of the entry to be reloaded." + } + } + }, + "save_persistent_states": { + "name": "Save persistent states", + "description": "Saves the persistent states immediately. Maintains the normal periodic saving interval." + } } } From 22b23b2c34fbe9a53606fa41579b3c53f57b663b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 13:47:58 +0200 Subject: [PATCH 0408/1009] Migrate hassio services to support translations (#96386) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/hassio/services.yaml | 70 ------- homeassistant/components/hassio/strings.json | 196 +++++++++++++++++- 2 files changed, 186 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/hassio/services.yaml b/homeassistant/components/hassio/services.yaml index 60b547354932f6..33eb1e88ed3764 100644 --- a/homeassistant/components/hassio/services.yaml +++ b/homeassistant/components/hassio/services.yaml @@ -1,193 +1,123 @@ addon_start: - name: Start add-on - description: Start add-on. fields: addon: - name: Add-on required: true - description: The add-on slug. example: core_ssh selector: addon: addon_restart: - name: Restart add-on. - description: Restart add-on. fields: addon: - name: Add-on required: true - description: The add-on slug. example: core_ssh selector: addon: addon_stdin: - name: Write data to add-on stdin. - description: Write data to add-on stdin. fields: addon: - name: Add-on required: true - description: The add-on slug. example: core_ssh selector: addon: addon_stop: - name: Stop add-on. - description: Stop add-on. fields: addon: - name: Add-on required: true - description: The add-on slug. example: core_ssh selector: addon: addon_update: - name: Update add-on. - description: Update add-on. This service should be used with caution since add-on updates can contain breaking changes. It is highly recommended that you review release notes/change logs before updating an add-on. fields: addon: - name: Add-on required: true - description: The add-on slug. example: core_ssh selector: addon: host_reboot: - name: Reboot the host system. - description: Reboot the host system. - host_shutdown: - name: Poweroff the host system. - description: Poweroff the host system. - backup_full: - name: Create a full backup. - description: Create a full backup. fields: name: - name: Name - description: Optional (default = current date and time). example: "Backup 1" selector: text: password: - name: Password - description: Optional password. example: "password" selector: text: compressed: - name: Compressed - description: Use compressed archives default: true selector: boolean: location: - name: Location - description: Name of a backup network storage to put backup (or /backup) example: my_backup_mount selector: backup_location: backup_partial: - name: Create a partial backup. - description: Create a partial backup. fields: homeassistant: - name: Home Assistant settings - description: Backup Home Assistant settings selector: boolean: addons: - name: Add-ons - description: Optional list of add-on slugs. example: ["core_ssh", "core_samba", "core_mosquitto"] selector: object: folders: - name: Folders - description: Optional list of directories. example: ["homeassistant", "share"] selector: object: name: - name: Name - description: Optional (default = current date and time). example: "Partial backup 1" selector: text: password: - name: Password - description: Optional password. example: "password" selector: text: compressed: - name: Compressed - description: Use compressed archives default: true selector: boolean: location: - name: Location - description: Name of a backup network storage to put backup (or /backup) example: my_backup_mount selector: backup_location: restore_full: - name: Restore from full backup. - description: Restore from full backup. fields: slug: - name: Slug required: true - description: Slug of backup to restore from. selector: text: password: - name: Password - description: Optional password. example: "password" selector: text: restore_partial: - name: Restore from partial backup. - description: Restore from partial backup. fields: slug: - name: Slug required: true - description: Slug of backup to restore from. selector: text: homeassistant: - name: Home Assistant settings - description: Restore Home Assistant selector: boolean: folders: - name: Folders - description: Optional list of directories. example: ["homeassistant", "share"] selector: object: addons: - name: Add-ons - description: Optional list of add-on slugs. example: ["core_ssh", "core_samba", "core_mosquitto"] selector: object: password: - name: Password - description: Optional password. example: "password" selector: text: diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index f9c212f946c912..fa8fc2d2da84d8 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -184,18 +184,194 @@ }, "entity": { "binary_sensor": { - "state": { "name": "Running" } + "state": { + "name": "Running" + } }, "sensor": { - "agent_version": { "name": "OS Agent version" }, - "apparmor_version": { "name": "Apparmor version" }, - "cpu_percent": { "name": "CPU percent" }, - "disk_free": { "name": "Disk free" }, - "disk_total": { "name": "Disk total" }, - "disk_used": { "name": "Disk used" }, - "memory_percent": { "name": "Memory percent" }, - "version": { "name": "Version" }, - "version_latest": { "name": "Newest version" } + "agent_version": { + "name": "OS Agent version" + }, + "apparmor_version": { + "name": "Apparmor version" + }, + "cpu_percent": { + "name": "CPU percent" + }, + "disk_free": { + "name": "Disk free" + }, + "disk_total": { + "name": "Disk total" + }, + "disk_used": { + "name": "Disk used" + }, + "memory_percent": { + "name": "Memory percent" + }, + "version": { + "name": "Version" + }, + "version_latest": { + "name": "Newest version" + } + } + }, + "services": { + "addon_start": { + "name": "Start add-on", + "description": "Starts an add-on.", + "fields": { + "addon": { + "name": "Add-on", + "description": "The add-on slug." + } + } + }, + "addon_restart": { + "name": "Restart add-on.", + "description": "Restarts an add-on.", + "fields": { + "addon": { + "name": "[%key:component::hassio::services::addon_start::fields::addon::name%]", + "description": "[%key:component::hassio::services::addon_start::fields::addon::description%]" + } + } + }, + "addon_stdin": { + "name": "Write data to add-on stdin.", + "description": "Writes data to add-on stdin.", + "fields": { + "addon": { + "name": "[%key:component::hassio::services::addon_start::fields::addon::name%]", + "description": "[%key:component::hassio::services::addon_start::fields::addon::description%]" + } + } + }, + "addon_stop": { + "name": "Stop add-on.", + "description": "Stops an add-on.", + "fields": { + "addon": { + "name": "[%key:component::hassio::services::addon_start::fields::addon::name%]", + "description": "[%key:component::hassio::services::addon_start::fields::addon::description%]" + } + } + }, + "addon_update": { + "name": "Update add-on.", + "description": "Updates an add-on. This service should be used with caution since add-on updates can contain breaking changes. It is highly recommended that you review release notes/change logs before updating an add-on.", + "fields": { + "addon": { + "name": "[%key:component::hassio::services::addon_start::fields::addon::name%]", + "description": "[%key:component::hassio::services::addon_start::fields::addon::description%]" + } + } + }, + "host_reboot": { + "name": "Reboot the host system.", + "description": "Reboots the host system." + }, + "host_shutdown": { + "name": "Power off the host system.", + "description": "Powers off the host system." + }, + "backup_full": { + "name": "Create a full backup.", + "description": "Creates a full backup.", + "fields": { + "name": { + "name": "Name", + "description": "Optional (default = current date and time)." + }, + "password": { + "name": "Password", + "description": "Password to protect the backup with." + }, + "compressed": { + "name": "Compressed", + "description": "Compresses the backup files." + }, + "location": { + "name": "Location", + "description": "Name of a backup network storage to host backups." + } + } + }, + "backup_partial": { + "name": "Create a partial backup.", + "description": "Creates a partial backup.", + "fields": { + "homeassistant": { + "name": "Home Assistant settings", + "description": "Includes Home Assistant settings in the backup." + }, + "addons": { + "name": "Add-ons", + "description": "List of add-ons to include in the backup. Use the name slug of the add-on." + }, + "folders": { + "name": "Folders", + "description": "List of directories to include in the backup." + }, + "name": { + "name": "[%key:component::hassio::services::backup_full::fields::name::name%]", + "description": "[%key:component::hassio::services::backup_full::fields::name::description%]" + }, + "password": { + "name": "[%key:component::hassio::services::backup_full::fields::password::name%]", + "description": "[%key:component::hassio::services::backup_full::fields::password::description%]" + }, + "compressed": { + "name": "[%key:component::hassio::services::backup_full::fields::compressed::name%]", + "description": "[%key:component::hassio::services::backup_full::fields::compressed::description%]" + }, + "location": { + "name": "[%key:component::hassio::services::backup_full::fields::location::name%]", + "description": "[%key:component::hassio::services::backup_full::fields::location::description%]" + } + } + }, + "restore_full": { + "name": "Restore from full backup.", + "description": "Restores from full backup.", + "fields": { + "slug": { + "name": "Slug", + "description": "Slug of backup to restore from." + }, + "password": { + "name": "[%key:component::hassio::services::backup_full::fields::password::name%]", + "description": "Optional password." + } + } + }, + "restore_partial": { + "name": "Restore from partial backup.", + "description": "Restores from a partial backup.", + "fields": { + "slug": { + "name": "[%key:component::hassio::services::restore_full::fields::slug::name%]", + "description": "[%key:component::hassio::services::restore_full::fields::slug::description%]" + }, + "homeassistant": { + "name": "[%key:component::hassio::services::backup_partial::fields::homeassistant::name%]", + "description": "Restores Home Assistant." + }, + "folders": { + "name": "[%key:component::hassio::services::backup_partial::fields::folders::name%]", + "description": "[%key:component::hassio::services::backup_partial::fields::folders::description%]" + }, + "addons": { + "name": "[%key:component::hassio::services::backup_partial::fields::addons::name%]", + "description": "[%key:component::hassio::services::backup_partial::fields::addons::description%]" + }, + "password": { + "name": "[%key:component::hassio::services::backup_full::fields::password::name%]", + "description": "[%key:component::hassio::services::restore_full::fields::password::description%]" + } + } } } } From 9ef62c75990835bdaca52128e8b3acc417a1aa7d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 13:49:32 +0200 Subject: [PATCH 0409/1009] Migrate scene services to support translations (#96390) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/scene/services.yaml | 23 --------- homeassistant/components/scene/strings.json | 52 +++++++++++++++++++- 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml index 202b4a98aa9464..acd98b102553a7 100644 --- a/homeassistant/components/scene/services.yaml +++ b/homeassistant/components/scene/services.yaml @@ -1,16 +1,11 @@ # Describes the format for available scene services turn_on: - name: Activate - description: Activate a scene. target: entity: domain: scene fields: transition: - name: Transition - description: Transition duration it takes to bring devices to the state - defined in the scene. selector: number: min: 0 @@ -18,16 +13,9 @@ turn_on: unit_of_measurement: seconds reload: - name: Reload - description: Reload the scene configuration. - apply: - name: Apply - description: Activate a scene with configuration. fields: entities: - name: Entities state - description: The entities and the state that they need to be. required: true example: | light.kitchen: "on" @@ -37,9 +25,6 @@ apply: selector: object: transition: - name: Transition - description: Transition duration it takes to bring devices to the state - defined in the scene. selector: number: min: 0 @@ -47,19 +32,13 @@ apply: unit_of_measurement: seconds create: - name: Create - description: Creates a new scene. fields: scene_id: - name: Scene entity ID - description: The entity_id of the new scene. required: true example: all_lights selector: text: entities: - name: Entities state - description: The entities to control with the scene. example: | light.tv_back_light: "on" light.ceiling: @@ -68,8 +47,6 @@ create: selector: object: snapshot_entities: - name: Snapshot entities - description: The entities of which a snapshot is to be taken example: | - light.ceiling - light.kitchen diff --git a/homeassistant/components/scene/strings.json b/homeassistant/components/scene/strings.json index c92838ea322f71..f4011860c785ca 100644 --- a/homeassistant/components/scene/strings.json +++ b/homeassistant/components/scene/strings.json @@ -1 +1,51 @@ -{ "title": "Scene" } +{ + "title": "Scene", + "services": { + "turn_on": { + "name": "Activate", + "description": "Activates a scene.", + "fields": { + "transition": { + "name": "Transition", + "description": "Time it takes the devices to transition into the states defined in the scene." + } + } + }, + "reload": { + "name": "Reload", + "description": "Reloads the scenes from the YAML-configuration." + }, + "apply": { + "name": "Apply", + "description": "Activates a scene with configuration.", + "fields": { + "entities": { + "name": "Entities state", + "description": "List of entities and their target state." + }, + "transition": { + "name": "Transition", + "description": "Time it takes the devices to transition into the states defined in the scene." + } + } + }, + "create": { + "name": "Create", + "description": "Creates a new scene.", + "fields": { + "scene_id": { + "name": "Scene entity ID", + "description": "The entity ID of the new scene." + }, + "entities": { + "name": "Entities state", + "description": "List of entities and their target state. If your entities are already in the target state right now, use `snapshot_entities` instead." + }, + "snapshot_entities": { + "name": "Snapshot entities", + "description": "List of entities to be included in the snapshot. By taking a snapshot, you record the current state of those entities. If you do not want to use the current state of all your entities for this scene, you can combine the `snapshot_entities` with `entities`." + } + } + } + } +} From aca91db8b574f912351a3d2dc8a9a34c58698231 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 13:49:51 +0200 Subject: [PATCH 0410/1009] Migrate water_heater services to support translations (#96389) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- .../components/water_heater/services.yaml | 14 -------- .../components/water_heater/strings.json | 36 +++++++++++++++++++ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/water_heater/services.yaml b/homeassistant/components/water_heater/services.yaml index a3b372f219e6f4..b42109ee649960 100644 --- a/homeassistant/components/water_heater/services.yaml +++ b/homeassistant/components/water_heater/services.yaml @@ -1,29 +1,21 @@ # Describes the format for available water_heater services set_away_mode: - name: Set away mode - description: Turn away mode on/off for water_heater device. target: entity: domain: water_heater fields: away_mode: - name: Away mode - description: New value of away mode. required: true selector: boolean: set_temperature: - name: Set temperature - description: Set target temperature of water_heater device. target: entity: domain: water_heater fields: temperature: - name: Temperature - description: New target temperature for water heater. required: true selector: number: @@ -32,22 +24,16 @@ set_temperature: step: 0.5 unit_of_measurement: "°" operation_mode: - name: Operation mode - description: New value of operation mode. example: eco selector: text: set_operation_mode: - name: Set operation mode - description: Set operation mode for water_heater device. target: entity: domain: water_heater fields: operation_mode: - name: Operation mode - description: New value of operation mode. required: true example: eco selector: diff --git a/homeassistant/components/water_heater/strings.json b/homeassistant/components/water_heater/strings.json index b0a625d0016a15..a03e93cde41e54 100644 --- a/homeassistant/components/water_heater/strings.json +++ b/homeassistant/components/water_heater/strings.json @@ -18,5 +18,41 @@ "performance": "Performance" } } + }, + "services": { + "set_away_mode": { + "name": "Set away mode", + "description": "Turns away mode on/off.", + "fields": { + "away_mode": { + "name": "Away mode", + "description": "New value of away mode." + } + } + }, + "set_temperature": { + "name": "Set temperature", + "description": "Sets the target temperature.", + "fields": { + "temperature": { + "name": "Temperature", + "description": "New target temperature for the water heater." + }, + "operation_mode": { + "name": "Operation mode", + "description": "New value of the operation mode. For a list of possible modes, refer to the integration documentation." + } + } + }, + "set_operation_mode": { + "name": "Set operation mode", + "description": "Sets the operation mode.", + "fields": { + "operation_mode": { + "name": "[%key:component::water_heater::services::set_temperature::fields::operation_mode::name%]", + "description": "[%key:component::water_heater::services::set_temperature::fields::operation_mode::description%]" + } + } + } } } From 352ca0b7f8d996bfbca9937eaa162ba95ee6dbcb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 13:54:06 +0200 Subject: [PATCH 0411/1009] Migrate fan services to support translations (#96325) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/fan/services.yaml | 40 +--------- homeassistant/components/fan/strings.json | 92 ++++++++++++++++++++++ 2 files changed, 95 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index db3bea9cad3a8d..8bd329ac8fec24 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -1,7 +1,5 @@ # Describes the format for available fan services set_preset_mode: - name: Set preset mode - description: Set preset mode for a fan device. target: entity: domain: fan @@ -9,16 +7,12 @@ set_preset_mode: - fan.FanEntityFeature.PRESET_MODE fields: preset_mode: - name: Preset mode - description: New value of preset mode. required: true example: "auto" selector: text: set_percentage: - name: Set speed percentage - description: Set fan speed percentage. target: entity: domain: fan @@ -26,8 +20,6 @@ set_percentage: - fan.FanEntityFeature.SET_SPEED fields: percentage: - name: Percentage - description: Percentage speed setting. required: true selector: number: @@ -36,15 +28,11 @@ set_percentage: unit_of_measurement: "%" turn_on: - name: Turn on - description: Turn fan on. target: entity: domain: fan fields: percentage: - name: Percentage - description: Percentage speed setting. filter: supported_features: - fan.FanEntityFeature.SET_SPEED @@ -54,8 +42,6 @@ turn_on: max: 100 unit_of_measurement: "%" preset_mode: - name: Preset mode - description: Preset mode setting. example: "auto" filter: supported_features: @@ -64,15 +50,11 @@ turn_on: text: turn_off: - name: Turn off - description: Turn fan off. target: entity: domain: fan oscillate: - name: Oscillate - description: Oscillate the fan. target: entity: domain: fan @@ -80,22 +62,16 @@ oscillate: - fan.FanEntityFeature.OSCILLATE fields: oscillating: - name: Oscillating - description: Flag to turn on/off oscillation. required: true selector: boolean: toggle: - name: Toggle - description: Toggle the fan on/off. target: entity: domain: fan set_direction: - name: Set direction - description: Set the fan rotation. target: entity: domain: fan @@ -103,20 +79,14 @@ set_direction: - fan.FanEntityFeature.DIRECTION fields: direction: - name: Direction - description: The direction to rotate. required: true selector: select: options: - - label: "Forward" - value: "forward" - - label: "Reverse" - value: "reverse" - + - "forward" + - "reverse" + translation_key: direction increase_speed: - name: Increase speed - description: Increase the speed of the fan by one speed or a percentage_step. target: entity: domain: fan @@ -126,7 +96,6 @@ increase_speed: percentage_step: advanced: true required: false - description: Increase speed by a percentage. selector: number: min: 0 @@ -134,8 +103,6 @@ increase_speed: unit_of_measurement: "%" decrease_speed: - name: Decrease speed - description: Decrease the speed of the fan by one speed or a percentage_step. target: entity: domain: fan @@ -145,7 +112,6 @@ decrease_speed: percentage_step: advanced: true required: false - description: Decrease speed by a percentage. selector: number: min: 0 diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index 0f3b88fd7f204d..d3a06edbee1b23 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -52,5 +52,97 @@ } } } + }, + "services": { + "set_preset_mode": { + "name": "Set preset mode", + "description": "Sets preset mode.", + "fields": { + "preset_mode": { + "name": "Preset mode", + "description": "Preset mode." + } + } + }, + "set_percentage": { + "name": "Set speed", + "description": "Sets the fan speed.", + "fields": { + "percentage": { + "name": "Percentage", + "description": "Speed of the fan." + } + } + }, + "turn_on": { + "name": "Turn on", + "description": "Turns fan on.", + "fields": { + "percentage": { + "name": "[%key:component::fan::services::set_percentage::fields::percentage::name%]", + "description": "[%key:component::fan::services::set_percentage::fields::percentage::description%]" + }, + "preset_mode": { + "name": "[%key:component::fan::services::set_preset_mode::fields::preset_mode::name%]", + "description": "[%key:component::fan::services::set_preset_mode::fields::preset_mode::description%]" + } + } + }, + "turn_off": { + "name": "Turn off", + "description": "Turns fan off." + }, + "oscillate": { + "name": "Oscillate", + "description": "Controls oscillatation of the fan.", + "fields": { + "oscillating": { + "name": "Oscillating", + "description": "Turn on/off oscillation." + } + } + }, + "toggle": { + "name": "Toggle", + "description": "Toggles the fan on/off." + }, + "set_direction": { + "name": "Set direction", + "description": "Sets the fan rotation direction.", + "fields": { + "direction": { + "name": "Direction", + "description": "Direction to rotate." + } + } + }, + "increase_speed": { + "name": "Increase speed", + "description": "Increases the speed of the fan.", + "fields": { + "percentage_step": { + "name": "Increment", + "description": "Increases the speed by a percentage step." + } + } + }, + "decrease_speed": { + "name": "Decrease speed", + "description": "Decreases the speed of the fan.", + "fields": { + "percentage_step": { + "name": "Decrement", + "description": "Decreases the speed by a percentage step." + } + } + } + }, + "selector": { + "direction": { + "options": { + "forward": "Forward", + "reverse": "Reverse" + } + } } } From c3871cc5aec171c76e2094a39d52545334b61e86 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 14:06:14 +0200 Subject: [PATCH 0412/1009] Migrate template services to support translations (#96414) --- homeassistant/components/template/services.yaml | 2 -- homeassistant/components/template/strings.json | 8 ++++++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/template/strings.json diff --git a/homeassistant/components/template/services.yaml b/homeassistant/components/template/services.yaml index 6186bc6dccbef8..c983a105c93977 100644 --- a/homeassistant/components/template/services.yaml +++ b/homeassistant/components/template/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload all template entities. diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json new file mode 100644 index 00000000000000..3222a0f1bdf5fc --- /dev/null +++ b/homeassistant/components/template/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "reload": { + "name": "Reload", + "description": "Reloads template entities from the YAML-configuration." + } + } +} From cccf436326e189a4bd9935ec7852e29f11a6424c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 15:14:10 +0200 Subject: [PATCH 0413/1009] Migrate LaMetric services to support translations (#96415) --- .../components/lametric/services.yaml | 194 +++++------------- .../components/lametric/strings.json | 134 ++++++++++++ 2 files changed, 191 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/lametric/services.yaml b/homeassistant/components/lametric/services.yaml index 3299245fbc0d8c..af04eac5f339ba 100644 --- a/homeassistant/components/lametric/services.yaml +++ b/homeassistant/components/lametric/services.yaml @@ -1,129 +1,70 @@ chart: - name: Display a chart - description: Display a chart on a LaMetric device. fields: device_id: &device_id - name: Device - description: The LaMetric device to display the chart on. required: true selector: device: integration: lametric data: - name: Data - description: The list of data points in the chart required: true example: "[1,2,3,4,5,4,3,2,1]" selector: object: sound: &sound - name: Sound - description: The notification sound to play. required: false selector: select: options: - - label: "Alarm 1" - value: "alarm1" - - label: "Alarm 2" - value: "alarm2" - - label: "Alarm 3" - value: "alarm3" - - label: "Alarm 4" - value: "alarm4" - - label: "Alarm 5" - value: "alarm5" - - label: "Alarm 6" - value: "alarm6" - - label: "Alarm 7" - value: "alarm7" - - label: "Alarm 8" - value: "alarm8" - - label: "Alarm 9" - value: "alarm9" - - label: "Alarm 10" - value: "alarm10" - - label: "Alarm 11" - value: "alarm11" - - label: "Alarm 12" - value: "alarm12" - - label: "Alarm 13" - value: "alarm13" - - label: "Bicycle" - value: "bicycle" - - label: "Car" - value: "car" - - label: "Cash" - value: "cash" - - label: "Cat" - value: "cat" - - label: "Dog 1" - value: "dog" - - label: "Dog 2" - value: "dog2" - - label: "Energy" - value: "energy" - - label: "Knock knock" - value: "knock-knock" - - label: "Letter email" - value: "letter_email" - - label: "Lose 1" - value: "lose1" - - label: "Lose 2" - value: "lose2" - - label: "Negative 1" - value: "negative1" - - label: "Negative 2" - value: "negative2" - - label: "Negative 3" - value: "negative3" - - label: "Negative 4" - value: "negative4" - - label: "Negative 5" - value: "negative5" - - label: "Notification 1" - value: "notification" - - label: "Notification 2" - value: "notification2" - - label: "Notification 3" - value: "notification3" - - label: "Notification 4" - value: "notification4" - - label: "Open door" - value: "open_door" - - label: "Positive 1" - value: "positive1" - - label: "Positive 2" - value: "positive2" - - label: "Positive 3" - value: "positive3" - - label: "Positive 4" - value: "positive4" - - label: "Positive 5" - value: "positive5" - - label: "Positive 6" - value: "positive6" - - label: "Statistic" - value: "statistic" - - label: "Thunder" - value: "thunder" - - label: "Water 1" - value: "water1" - - label: "Water 2" - value: "water2" - - label: "Win 1" - value: "win" - - label: "Win 2" - value: "win2" - - label: "Wind" - value: "wind" - - label: "Wind short" - value: "wind_short" + - "alarm1" + - "alarm2" + - "alarm3" + - "alarm4" + - "alarm5" + - "alarm6" + - "alarm7" + - "alarm8" + - "alarm9" + - "alarm10" + - "alarm11" + - "alarm12" + - "alarm13" + - "bicycle" + - "car" + - "cash" + - "cat" + - "dog" + - "dog2" + - "energy" + - "knock-knock" + - "letter_email" + - "lose1" + - "lose2" + - "negative1" + - "negative2" + - "negative3" + - "negative4" + - "negative5" + - "notification" + - "notification2" + - "notification3" + - "notification4" + - "open_door" + - "positive1" + - "positive2" + - "positive3" + - "positive4" + - "positive5" + - "positive6" + - "statistic" + - "thunder" + - "water1" + - "water2" + - "win" + - "win2" + - "wind" + - "wind_short" + translation_key: sound cycles: &cycles - name: Cycles - description: >- - The number of times to display the message. When set to 0, the message - will be displayed until dismissed. required: false default: 1 selector: @@ -132,56 +73,35 @@ chart: max: 10 mode: slider icon_type: &icon_type - name: Icon type - description: >- - The type of icon to display, indicating the nature of the notification. required: false default: "none" selector: select: mode: dropdown options: - - label: "None" - value: "none" - - label: "Info" - value: "info" - - label: "Alert" - value: "alert" + - "none" + - "info" + - "alert" + translation_key: icon_type priority: &priority - name: Priority - description: >- - The priority of the notification. When the device is running in - screensaver or kiosk mode, only critical priority notifications - will be accepted. required: false default: "info" selector: select: mode: dropdown options: - - label: "Info" - value: "info" - - label: "Warning" - value: "warning" - - label: "Critical" - value: "critical" - + - "info" + - "warning" + - "critical" + translation_key: priority message: - name: Display a message - description: Display a message with an optional icon on a LaMetric device. fields: device_id: *device_id message: - name: Message - description: The message to display. required: true selector: text: icon: - name: Icon - description: >- - The ID number of the icon or animation to display. List of all icons - and their IDs can be found at: https://developer.lametric.com/icons required: false selector: text: diff --git a/homeassistant/components/lametric/strings.json b/homeassistant/components/lametric/strings.json index 21cebe46f26e98..ac06e125b0c302 100644 --- a/homeassistant/components/lametric/strings.json +++ b/homeassistant/components/lametric/strings.json @@ -78,5 +78,139 @@ "name": "Bluetooth" } } + }, + "services": { + "chart": { + "name": "Display a chart", + "description": "Displays a chart on a LaMetric device.", + "fields": { + "device_id": { + "name": "Device", + "description": "The LaMetric device to display the chart on." + }, + "data": { + "name": "Data", + "description": "The list of data points in the chart." + }, + "sound": { + "name": "Sound", + "description": "The notification sound to play." + }, + "cycles": { + "name": "Cycles", + "description": "The number of times to display the message. When set to 0, the message will be displayed until dismissed." + }, + "icon_type": { + "name": "Icon type", + "description": "The type of icon to display, indicating the nature of the notification." + }, + "priority": { + "name": "Priority", + "description": "The priority of the notification. When the device is running in screensaver or kiosk mode, only critical priority notifications will be accepted." + } + } + }, + "message": { + "name": "Display a message", + "description": "Displays a message with an optional icon on a LaMetric device.", + "fields": { + "device_id": { + "name": "[%key:component::lametric::services::chart::fields::device_id::name%]", + "description": "The LaMetric device to display the message on." + }, + "message": { + "name": "Message", + "description": "The message to display." + }, + "icon": { + "name": "Icon", + "description": "The ID number of the icon or animation to display. List of all icons and their IDs can be found at: https://developer.lametric.com/icons." + }, + "sound": { + "name": "[%key:component::lametric::services::chart::fields::sound::name%]", + "description": "[%key:component::lametric::services::chart::fields::sound::description%]" + }, + "cycles": { + "name": "[%key:component::lametric::services::chart::fields::cycles::name%]", + "description": "[%key:component::lametric::services::chart::fields::cycles::description%]" + }, + "icon_type": { + "name": "[%key:component::lametric::services::chart::fields::icon_type::name%]", + "description": "[%key:component::lametric::services::chart::fields::icon_type::description%]" + }, + "priority": { + "name": "[%key:component::lametric::services::chart::fields::priority::name%]", + "description": "[%key:component::lametric::services::chart::fields::priority::description%]" + } + } + } + }, + "selector": { + "sound": { + "options": { + "alarm1": "Alarm 1", + "alarm2": "Alarm 2", + "alarm3": "Alarm 3", + "alarm4": "Alarm 4", + "alarm5": "Alarm 5", + "alarm6": "Alarm 6", + "alarm7": "Alarm 7", + "alarm8": "Alarm 8", + "alarm9": "Alarm 9", + "alarm10": "Alarm 10", + "alarm11": "Alarm 11", + "alarm12": "Alarm 12", + "alarm13": "Alarm 13", + "bicycle": "Bicycle", + "car": "Car", + "cash": "Cash", + "cat": "Cat", + "dog": "Dog 1", + "dog2": "Dog 2", + "energy": "Energy", + "knock-knock": "Knock knock", + "letter_email": "Letter email", + "lose1": "Lose 1", + "lose2": "Lose 2", + "negative1": "Negative 1", + "negative2": "Negative 2", + "negative3": "Negative 3", + "negative4": "Negative 4", + "negative5": "Negative 5", + "notification": "Notification 1", + "notification2": "Notification 2", + "notification3": "Notification 3", + "notification4": "Notification 4", + "open_door": "Open door", + "positive1": "Positive 1", + "positive2": "Positive 2", + "positive3": "Positive 3", + "positive4": "Positive 4", + "positive5": "Positive 5", + "positive6": "Positive 6", + "statistic": "Statistic", + "thunder": "Thunder", + "water1": "Water 1", + "water2": "Water 2", + "win": "Win 1", + "win2": "Win 2", + "wind": "Wind", + "wind_short": "Wind short" + } + }, + "icon_type": { + "options": { + "none": "None", + "info": "Info", + "alert": "Alert" + } + }, + "priority": { + "options": { + "info": "Info", + "warning": "Warning", + "critical": "Critical" + } + } } } From 878ed7cf219b1aa910a276a74b64dae8b72ca923 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 15:30:36 +0200 Subject: [PATCH 0414/1009] Migrate intent_script services to support translations (#96394) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/intent_script/services.yaml | 2 -- homeassistant/components/intent_script/strings.json | 8 ++++++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/intent_script/strings.json diff --git a/homeassistant/components/intent_script/services.yaml b/homeassistant/components/intent_script/services.yaml index bb981dbc69cb4b..c983a105c93977 100644 --- a/homeassistant/components/intent_script/services.yaml +++ b/homeassistant/components/intent_script/services.yaml @@ -1,3 +1 @@ reload: - name: Reload - description: Reload the intent_script configuration. diff --git a/homeassistant/components/intent_script/strings.json b/homeassistant/components/intent_script/strings.json new file mode 100644 index 00000000000000..efd77d225f763b --- /dev/null +++ b/homeassistant/components/intent_script/strings.json @@ -0,0 +1,8 @@ +{ + "services": { + "reload": { + "name": "Reload", + "description": "Reloads the intent script from the YAML-configuration." + } + } +} From 6c4478392736b3a0f1485bcb5a1682d84288ebfc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 15:36:57 +0200 Subject: [PATCH 0415/1009] Migrate Matter services to support translations (#96406) --- homeassistant/components/matter/services.yaml | 5 ----- homeassistant/components/matter/strings.json | 12 ++++++++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/matter/services.yaml b/homeassistant/components/matter/services.yaml index ff59a7efe63d34..c72187b2ffe915 100644 --- a/homeassistant/components/matter/services.yaml +++ b/homeassistant/components/matter/services.yaml @@ -1,11 +1,6 @@ open_commissioning_window: - name: Open Commissioning Window - description: > - Allow adding one of your devices to another Matter network by opening the commissioning window for this Matter device for 60 seconds. fields: device_id: - name: Device - description: The Matter device to add to the other Matter network. required: true selector: device: diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index dc5eb30df51a33..3d5ae9b6a61e13 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -50,5 +50,17 @@ "name": "Flow" } } + }, + "services": { + "open_commissioning_window": { + "name": "Open commissioning window", + "description": "Allows adding one of your devices to another Matter network by opening the commissioning window for this Matter device for 60 seconds.", + "fields": { + "device_id": { + "name": "Device", + "description": "The Matter device to add to the other Matter network." + } + } + } } } From f7ce9b1688a9bc9cb575b411c0d62e62826c016f Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 12 Jul 2023 16:08:15 +0200 Subject: [PATCH 0416/1009] Add support for gardena bluetooth (#95179) Add support for gardena bluetooth based water computers. --- .coveragerc | 4 + CODEOWNERS | 2 + .../components/gardena_bluetooth/__init__.py | 86 ++++++ .../gardena_bluetooth/config_flow.py | 138 ++++++++++ .../components/gardena_bluetooth/const.py | 3 + .../gardena_bluetooth/coordinator.py | 121 ++++++++ .../gardena_bluetooth/manifest.json | 17 ++ .../components/gardena_bluetooth/strings.json | 28 ++ .../components/gardena_bluetooth/switch.py | 74 +++++ homeassistant/generated/bluetooth.py | 6 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + .../components/gardena_bluetooth/__init__.py | 61 +++++ .../components/gardena_bluetooth/conftest.py | 30 ++ .../snapshots/test_config_flow.ambr | 258 ++++++++++++++++++ .../gardena_bluetooth/test_config_flow.py | 134 +++++++++ 18 files changed, 975 insertions(+) create mode 100644 homeassistant/components/gardena_bluetooth/__init__.py create mode 100644 homeassistant/components/gardena_bluetooth/config_flow.py create mode 100644 homeassistant/components/gardena_bluetooth/const.py create mode 100644 homeassistant/components/gardena_bluetooth/coordinator.py create mode 100644 homeassistant/components/gardena_bluetooth/manifest.json create mode 100644 homeassistant/components/gardena_bluetooth/strings.json create mode 100644 homeassistant/components/gardena_bluetooth/switch.py create mode 100644 tests/components/gardena_bluetooth/__init__.py create mode 100644 tests/components/gardena_bluetooth/conftest.py create mode 100644 tests/components/gardena_bluetooth/snapshots/test_config_flow.ambr create mode 100644 tests/components/gardena_bluetooth/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 912c472de3e19b..e10a23e9c31404 100644 --- a/.coveragerc +++ b/.coveragerc @@ -404,6 +404,10 @@ omit = homeassistant/components/garages_amsterdam/__init__.py homeassistant/components/garages_amsterdam/binary_sensor.py homeassistant/components/garages_amsterdam/sensor.py + homeassistant/components/gardena_bluetooth/__init__.py + homeassistant/components/gardena_bluetooth/const.py + homeassistant/components/gardena_bluetooth/coordinator.py + homeassistant/components/gardena_bluetooth/switch.py homeassistant/components/gc100/* homeassistant/components/geniushub/* homeassistant/components/geocaching/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 16c0426d87f620..01f486e2704307 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -425,6 +425,8 @@ build.json @home-assistant/supervisor /tests/components/fully_kiosk/ @cgarwood /homeassistant/components/garages_amsterdam/ @klaasnicolaas /tests/components/garages_amsterdam/ @klaasnicolaas +/homeassistant/components/gardena_bluetooth/ @elupus +/tests/components/gardena_bluetooth/ @elupus /homeassistant/components/gdacs/ @exxamalte /tests/components/gdacs/ @exxamalte /homeassistant/components/generic/ @davet2001 diff --git a/homeassistant/components/gardena_bluetooth/__init__.py b/homeassistant/components/gardena_bluetooth/__init__.py new file mode 100644 index 00000000000000..05ac16381d1ddd --- /dev/null +++ b/homeassistant/components/gardena_bluetooth/__init__.py @@ -0,0 +1,86 @@ +"""The Gardena Bluetooth integration.""" +from __future__ import annotations + +import asyncio +import logging + +from bleak.backends.device import BLEDevice +from gardena_bluetooth.client import CachedConnection, Client +from gardena_bluetooth.const import DeviceConfiguration, DeviceInformation +from gardena_bluetooth.exceptions import CommunicationFailure + +from homeassistant.components import bluetooth +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.entity import DeviceInfo +import homeassistant.util.dt as dt_util + +from .const import DOMAIN +from .coordinator import Coordinator, DeviceUnavailable + +PLATFORMS: list[Platform] = [Platform.SWITCH] +LOGGER = logging.getLogger(__name__) +TIMEOUT = 20.0 +DISCONNECT_DELAY = 5 + + +def get_connection(hass: HomeAssistant, address: str) -> CachedConnection: + """Set up a cached client that keeps connection after last use.""" + + def _device_lookup() -> BLEDevice: + device = bluetooth.async_ble_device_from_address( + hass, address, connectable=True + ) + if not device: + raise DeviceUnavailable("Unable to find device") + return device + + return CachedConnection(DISCONNECT_DELAY, _device_lookup) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Gardena Bluetooth from a config entry.""" + + address = entry.data[CONF_ADDRESS] + client = Client(get_connection(hass, address)) + try: + sw_version = await client.read_char(DeviceInformation.firmware_version, None) + manufacturer = await client.read_char(DeviceInformation.manufacturer_name, None) + model = await client.read_char(DeviceInformation.model_number, None) + name = await client.read_char( + DeviceConfiguration.custom_device_name, entry.title + ) + uuids = await client.get_all_characteristics_uuid() + await client.update_timestamp(dt_util.now()) + except (asyncio.TimeoutError, CommunicationFailure, DeviceUnavailable) as exception: + await client.disconnect() + raise ConfigEntryNotReady( + f"Unable to connect to device {address} due to {exception}" + ) from exception + + device = DeviceInfo( + identifiers={(DOMAIN, address)}, + name=name, + sw_version=sw_version, + manufacturer=manufacturer, + model=model, + ) + + coordinator = Coordinator(hass, LOGGER, client, uuids, device, address) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await coordinator.async_refresh() + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + coordinator: Coordinator = hass.data[DOMAIN].pop(entry.entry_id) + await coordinator.async_shutdown() + + return unload_ok diff --git a/homeassistant/components/gardena_bluetooth/config_flow.py b/homeassistant/components/gardena_bluetooth/config_flow.py new file mode 100644 index 00000000000000..3e9816750573a0 --- /dev/null +++ b/homeassistant/components/gardena_bluetooth/config_flow.py @@ -0,0 +1,138 @@ +"""Config flow for Gardena Bluetooth integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from gardena_bluetooth.client import Client +from gardena_bluetooth.const import DeviceInformation, ScanService +from gardena_bluetooth.exceptions import CharacteristicNotFound, CommunicationFailure +from gardena_bluetooth.parse import ManufacturerData, ProductGroup +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.bluetooth import ( + BluetoothServiceInfo, + async_discovered_service_info, +) +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import AbortFlow, FlowResult + +from . import get_connection +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +def _is_supported(discovery_info: BluetoothServiceInfo): + """Check if device is supported.""" + if ScanService not in discovery_info.service_uuids: + return False + + if not (data := discovery_info.manufacturer_data.get(ManufacturerData.company)): + _LOGGER.debug("Missing manufacturer data: %s", discovery_info) + return False + + manufacturer_data = ManufacturerData.decode(data) + if manufacturer_data.group != ProductGroup.WATER_CONTROL: + _LOGGER.debug("Unsupported device: %s", manufacturer_data) + return False + + return True + + +def _get_name(discovery_info: BluetoothServiceInfo): + if discovery_info.name and discovery_info.name != discovery_info.address: + return discovery_info.name + return "Gardena Device" + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Gardena Bluetooth.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self.devices: dict[str, str] = {} + self.address: str | None + + async def async_read_data(self): + """Try to connect to device and extract information.""" + client = Client(get_connection(self.hass, self.address)) + try: + model = await client.read_char(DeviceInformation.model_number) + _LOGGER.debug("Found device with model: %s", model) + except (CharacteristicNotFound, CommunicationFailure) as exception: + raise AbortFlow( + "cannot_connect", description_placeholders={"error": str(exception)} + ) from exception + finally: + await client.disconnect() + + return {CONF_ADDRESS: self.address} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + _LOGGER.debug("Discovered device: %s", discovery_info) + if not _is_supported(discovery_info): + return self.async_abort(reason="no_devices_found") + + self.address = discovery_info.address + self.devices = {discovery_info.address: _get_name(discovery_info)} + await self.async_set_unique_id(self.address) + self._abort_if_unique_id_configured() + return await self.async_step_confirm() + + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self.address + title = self.devices[self.address] + + if user_input is not None: + data = await self.async_read_data() + return self.async_create_entry(title=title, data=data) + + self.context["title_placeholders"] = { + "name": title, + } + + self._set_confirm_only() + return self.async_show_form( + step_id="confirm", + description_placeholders=self.context["title_placeholders"], + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is not None: + self.address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(self.address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return await self.async_step_confirm() + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or not _is_supported(discovery_info): + continue + + self.devices[address] = _get_name(discovery_info) + + if not self.devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_ADDRESS): vol.In(self.devices), + }, + ), + ) diff --git a/homeassistant/components/gardena_bluetooth/const.py b/homeassistant/components/gardena_bluetooth/const.py new file mode 100644 index 00000000000000..7de4c15b5fa98f --- /dev/null +++ b/homeassistant/components/gardena_bluetooth/const.py @@ -0,0 +1,3 @@ +"""Constants for the Gardena Bluetooth integration.""" + +DOMAIN = "gardena_bluetooth" diff --git a/homeassistant/components/gardena_bluetooth/coordinator.py b/homeassistant/components/gardena_bluetooth/coordinator.py new file mode 100644 index 00000000000000..fa7639dece0177 --- /dev/null +++ b/homeassistant/components/gardena_bluetooth/coordinator.py @@ -0,0 +1,121 @@ +"""Provides the DataUpdateCoordinator.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any + +from gardena_bluetooth.client import Client +from gardena_bluetooth.exceptions import ( + CharacteristicNoAccess, + GardenaBluetoothException, +) +from gardena_bluetooth.parse import Characteristic, CharacteristicType + +from homeassistant.components import bluetooth +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +SCAN_INTERVAL = timedelta(seconds=60) +LOGGER = logging.getLogger(__name__) + + +class DeviceUnavailable(HomeAssistantError): + """Raised if device can't be found.""" + + +class Coordinator(DataUpdateCoordinator[dict[str, bytes]]): + """Class to manage fetching data.""" + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + client: Client, + characteristics: set[str], + device_info: DeviceInfo, + address: str, + ) -> None: + """Initialize global data updater.""" + super().__init__( + hass=hass, + logger=logger, + name="Gardena Bluetooth Data Update Coordinator", + update_interval=SCAN_INTERVAL, + ) + self.address = address + self.data = {} + self.client = client + self.characteristics = characteristics + self.device_info = device_info + + async def async_shutdown(self) -> None: + """Shutdown coordinator and any connection.""" + await super().async_shutdown() + await self.client.disconnect() + + async def _async_update_data(self) -> dict[str, bytes]: + """Poll the device.""" + uuids: set[str] = { + uuid for context in self.async_contexts() for uuid in context + } + if not uuids: + return {} + + data: dict[str, bytes] = {} + for uuid in uuids: + try: + data[uuid] = await self.client.read_char_raw(uuid) + except CharacteristicNoAccess as exception: + LOGGER.debug("Unable to get data for %s due to %s", uuid, exception) + except (GardenaBluetoothException, DeviceUnavailable) as exception: + raise UpdateFailed( + f"Unable to update data for {uuid} due to {exception}" + ) from exception + return data + + def read_cached( + self, char: Characteristic[CharacteristicType] + ) -> CharacteristicType | None: + """Read cached characteristic.""" + if data := self.data.get(char.uuid): + return char.decode(data) + return None + + async def write( + self, char: Characteristic[CharacteristicType], value: CharacteristicType + ) -> None: + """Write characteristic to device.""" + try: + await self.client.write_char(char, value) + except (GardenaBluetoothException, DeviceUnavailable) as exception: + raise HomeAssistantError( + f"Unable to write characteristic {char} dur to {exception}" + ) from exception + + self.data[char.uuid] = char.encode(value) + await self.async_refresh() + + +class GardenaBluetoothEntity(CoordinatorEntity[Coordinator]): + """Coordinator entity for Gardena Bluetooth.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: Coordinator, context: Any = None) -> None: + """Initialize coordinator entity.""" + super().__init__(coordinator, context) + self._attr_device_info = coordinator.device_info + + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and bluetooth.async_address_present( + self.hass, self.coordinator.address, True + ) diff --git a/homeassistant/components/gardena_bluetooth/manifest.json b/homeassistant/components/gardena_bluetooth/manifest.json new file mode 100644 index 00000000000000..cdc43a802c99c7 --- /dev/null +++ b/homeassistant/components/gardena_bluetooth/manifest.json @@ -0,0 +1,17 @@ +{ + "domain": "gardena_bluetooth", + "name": "Gardena Bluetooth", + "bluetooth": [ + { + "manufacturer_id": 1062, + "service_uuid": "98bd0001-0b0e-421a-84e5-ddbf75dc6de4", + "connectable": true + } + ], + "codeowners": ["@elupus"], + "config_flow": true, + "dependencies": ["bluetooth_adapters"], + "documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth", + "iot_class": "local_polling", + "requirements": ["gardena_bluetooth==1.0.1"] +} diff --git a/homeassistant/components/gardena_bluetooth/strings.json b/homeassistant/components/gardena_bluetooth/strings.json new file mode 100644 index 00000000000000..165e336bbecb60 --- /dev/null +++ b/homeassistant/components/gardena_bluetooth/strings.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "error": { + "cannot_connect": "Failed to connect: {error}" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "entity": { + "switch": { + "state": { + "name": "Open" + } + } + } +} diff --git a/homeassistant/components/gardena_bluetooth/switch.py b/homeassistant/components/gardena_bluetooth/switch.py new file mode 100644 index 00000000000000..e3fcc8978c7683 --- /dev/null +++ b/homeassistant/components/gardena_bluetooth/switch.py @@ -0,0 +1,74 @@ +"""Support for switch entities.""" +from __future__ import annotations + +from typing import Any + +from gardena_bluetooth.const import Valve + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import Coordinator, GardenaBluetoothEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up switch based on a config entry.""" + coordinator: Coordinator = hass.data[DOMAIN][entry.entry_id] + entities = [] + if GardenaBluetoothValveSwitch.characteristics.issubset( + coordinator.characteristics + ): + entities.append(GardenaBluetoothValveSwitch(coordinator)) + + async_add_entities(entities) + + +class GardenaBluetoothValveSwitch(GardenaBluetoothEntity, SwitchEntity): + """Representation of a valve switch.""" + + characteristics = { + Valve.state.uuid, + Valve.manual_watering_time.uuid, + Valve.manual_watering_time.uuid, + } + + def __init__( + self, + coordinator: Coordinator, + ) -> None: + """Initialize the switch.""" + super().__init__( + coordinator, {Valve.state.uuid, Valve.manual_watering_time.uuid} + ) + self._attr_unique_id = f"{coordinator.address}-{Valve.state.uuid}" + self._attr_translation_key = "state" + self._attr_is_on = None + + def _handle_coordinator_update(self) -> None: + if data := self.coordinator.data.get(Valve.state.uuid): + self._attr_is_on = Valve.state.decode(data) + else: + self._attr_is_on = None + super()._handle_coordinator_update() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + if not (data := self.coordinator.data.get(Valve.manual_watering_time.uuid)): + raise HomeAssistantError("Unable to get manual activation time.") + + value = Valve.manual_watering_time.decode(data) + await self.coordinator.write(Valve.remaining_open_time, value) + self._attr_is_on = True + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self.coordinator.write(Valve.remaining_open_time, 0) + self._attr_is_on = False + self.async_write_ha_state() diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 24215a8a0c4d50..64fae252975a66 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -83,6 +83,12 @@ ], "manufacturer_id": 20296, }, + { + "connectable": True, + "domain": "gardena_bluetooth", + "manufacturer_id": 1062, + "service_uuid": "98bd0001-0b0e-421a-84e5-ddbf75dc6de4", + }, { "connectable": False, "domain": "govee_ble", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d8ffa25b765da0..6d9a132a29a8eb 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -155,6 +155,7 @@ "frontier_silicon", "fully_kiosk", "garages_amsterdam", + "gardena_bluetooth", "gdacs", "generic", "geo_json_events", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index d1f68271c55169..21f7acd59e39c0 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1884,6 +1884,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "gardena_bluetooth": { + "name": "Gardena Bluetooth", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "gaviota": { "name": "Gaviota", "integration_type": "virtual", diff --git a/requirements_all.txt b/requirements_all.txt index 0cb289d748fb75..d33f01b34be376 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,6 +819,9 @@ fritzconnection[qr]==1.12.2 # homeassistant.components.google_translate gTTS==2.2.4 +# homeassistant.components.gardena_bluetooth +gardena_bluetooth==1.0.1 + # homeassistant.components.google_assistant_sdk gassist-text==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04065df19c1307..f35021c3e006af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -641,6 +641,9 @@ fritzconnection[qr]==1.12.2 # homeassistant.components.google_translate gTTS==2.2.4 +# homeassistant.components.gardena_bluetooth +gardena_bluetooth==1.0.1 + # homeassistant.components.google_assistant_sdk gassist-text==0.0.10 diff --git a/tests/components/gardena_bluetooth/__init__.py b/tests/components/gardena_bluetooth/__init__.py new file mode 100644 index 00000000000000..6a064409e9e2f3 --- /dev/null +++ b/tests/components/gardena_bluetooth/__init__.py @@ -0,0 +1,61 @@ +"""Tests for the Gardena Bluetooth integration.""" + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +WATER_TIMER_SERVICE_INFO = BluetoothServiceInfo( + name="Timer", + address="00000000-0000-0000-0000-000000000001", + rssi=-63, + service_data={}, + manufacturer_data={ + 1062: b"\x02\x07d\x02\x05\x01\x02\x08\x00\x02\t\x01\x04\x06\x12\x00\x01" + }, + service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"], + source="local", +) + +WATER_TIMER_UNNAMED_SERVICE_INFO = BluetoothServiceInfo( + name=None, + address="00000000-0000-0000-0000-000000000002", + rssi=-63, + service_data={}, + manufacturer_data={ + 1062: b"\x02\x07d\x02\x05\x01\x02\x08\x00\x02\t\x01\x04\x06\x12\x00\x01" + }, + service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"], + source="local", +) + +MISSING_SERVICE_SERVICE_INFO = BluetoothServiceInfo( + name="Missing Service Info", + address="00000000-0000-0000-0001-000000000000", + rssi=-63, + service_data={}, + manufacturer_data={ + 1062: b"\x02\x07d\x02\x05\x01\x02\x08\x00\x02\t\x01\x04\x06\x12\x00\x01" + }, + service_uuids=[], + source="local", +) + +MISSING_MANUFACTURER_DATA_SERVICE_INFO = BluetoothServiceInfo( + name="Missing Manufacturer Data", + address="00000000-0000-0000-0001-000000000001", + rssi=-63, + service_data={}, + manufacturer_data={}, + service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"], + source="local", +) + +UNSUPPORTED_GROUP_SERVICE_INFO = BluetoothServiceInfo( + name="Unsupported Group", + address="00000000-0000-0000-0001-000000000002", + rssi=-63, + service_data={}, + manufacturer_data={ + 1062: b"\x02\x07d\x02\x05\x01\x02\x08\x00\x02\t\x01\x04\x06\x10\x00\x01" + }, + service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"], + source="local", +) diff --git a/tests/components/gardena_bluetooth/conftest.py b/tests/components/gardena_bluetooth/conftest.py new file mode 100644 index 00000000000000..f09a274742f617 --- /dev/null +++ b/tests/components/gardena_bluetooth/conftest.py @@ -0,0 +1,30 @@ +"""Common fixtures for the Gardena Bluetooth tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, Mock, patch + +from gardena_bluetooth.client import Client +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.gardena_bluetooth.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture(autouse=True) +def mock_client(enable_bluetooth): + """Auto mock bluetooth.""" + + client = Mock(spec_set=Client) + client.get_all_characteristics_uuid.return_value = set() + + with patch( + "homeassistant.components.gardena_bluetooth.config_flow.Client", + return_value=client, + ): + yield client diff --git a/tests/components/gardena_bluetooth/snapshots/test_config_flow.ambr b/tests/components/gardena_bluetooth/snapshots/test_config_flow.ambr new file mode 100644 index 00000000000000..fde70b60a01d39 --- /dev/null +++ b/tests/components/gardena_bluetooth/snapshots/test_config_flow.ambr @@ -0,0 +1,258 @@ +# serializer version: 1 +# name: test_bluetooth + FlowResultSnapshot({ + 'data_schema': None, + 'description_placeholders': dict({ + 'name': 'Timer', + }), + 'errors': None, + 'flow_id': , + 'handler': 'gardena_bluetooth', + 'last_step': None, + 'step_id': 'confirm', + 'type': , + }) +# --- +# name: test_bluetooth.1 + FlowResultSnapshot({ + 'context': dict({ + 'confirm_only': True, + 'source': 'bluetooth', + 'title_placeholders': dict({ + 'name': 'Timer', + }), + 'unique_id': '00000000-0000-0000-0000-000000000001', + }), + 'data': dict({ + 'address': '00000000-0000-0000-0000-000000000001', + }), + 'description': None, + 'description_placeholders': None, + 'flow_id': , + 'handler': 'gardena_bluetooth', + 'options': dict({ + }), + 'result': ConfigEntrySnapshot({ + 'data': dict({ + 'address': '00000000-0000-0000-0000-000000000001', + }), + 'disabled_by': None, + 'domain': 'gardena_bluetooth', + 'entry_id': , + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'bluetooth', + 'title': 'Timer', + 'unique_id': '00000000-0000-0000-0000-000000000001', + 'version': 1, + }), + 'title': 'Timer', + 'type': , + 'version': 1, + }) +# --- +# name: test_bluetooth_invalid + FlowResultSnapshot({ + 'description_placeholders': None, + 'flow_id': , + 'handler': 'gardena_bluetooth', + 'reason': 'no_devices_found', + 'type': , + }) +# --- +# name: test_bluetooth_lost + FlowResultSnapshot({ + 'data_schema': None, + 'description_placeholders': dict({ + 'name': 'Timer', + }), + 'errors': None, + 'flow_id': , + 'handler': 'gardena_bluetooth', + 'last_step': None, + 'step_id': 'confirm', + 'type': , + }) +# --- +# name: test_bluetooth_lost.1 + FlowResultSnapshot({ + 'context': dict({ + 'confirm_only': True, + 'source': 'bluetooth', + 'title_placeholders': dict({ + 'name': 'Timer', + }), + 'unique_id': '00000000-0000-0000-0000-000000000001', + }), + 'data': dict({ + 'address': '00000000-0000-0000-0000-000000000001', + }), + 'description': None, + 'description_placeholders': None, + 'flow_id': , + 'handler': 'gardena_bluetooth', + 'options': dict({ + }), + 'result': ConfigEntrySnapshot({ + 'data': dict({ + 'address': '00000000-0000-0000-0000-000000000001', + }), + 'disabled_by': None, + 'domain': 'gardena_bluetooth', + 'entry_id': , + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'bluetooth', + 'title': 'Timer', + 'unique_id': '00000000-0000-0000-0000-000000000001', + 'version': 1, + }), + 'title': 'Timer', + 'type': , + 'version': 1, + }) +# --- +# name: test_failed_connect + FlowResultSnapshot({ + 'data_schema': list([ + dict({ + 'name': 'address', + 'options': list([ + tuple( + '00000000-0000-0000-0000-000000000001', + 'Timer', + ), + ]), + 'required': True, + 'type': 'select', + }), + ]), + 'description_placeholders': None, + 'errors': None, + 'flow_id': , + 'handler': 'gardena_bluetooth', + 'last_step': None, + 'step_id': 'user', + 'type': , + }) +# --- +# name: test_failed_connect.1 + FlowResultSnapshot({ + 'data_schema': None, + 'description_placeholders': dict({ + 'name': 'Timer', + }), + 'errors': None, + 'flow_id': , + 'handler': 'gardena_bluetooth', + 'last_step': None, + 'step_id': 'confirm', + 'type': , + }) +# --- +# name: test_failed_connect.2 + FlowResultSnapshot({ + 'description_placeholders': dict({ + 'error': 'something went wrong', + }), + 'flow_id': , + 'handler': 'gardena_bluetooth', + 'reason': 'cannot_connect', + 'type': , + }) +# --- +# name: test_no_devices + FlowResultSnapshot({ + 'description_placeholders': None, + 'flow_id': , + 'handler': 'gardena_bluetooth', + 'reason': 'no_devices_found', + 'type': , + }) +# --- +# name: test_user_selection + FlowResultSnapshot({ + 'data_schema': list([ + dict({ + 'name': 'address', + 'options': list([ + tuple( + '00000000-0000-0000-0000-000000000001', + 'Timer', + ), + tuple( + '00000000-0000-0000-0000-000000000002', + 'Gardena Device', + ), + ]), + 'required': True, + 'type': 'select', + }), + ]), + 'description_placeholders': None, + 'errors': None, + 'flow_id': , + 'handler': 'gardena_bluetooth', + 'last_step': None, + 'step_id': 'user', + 'type': , + }) +# --- +# name: test_user_selection.1 + FlowResultSnapshot({ + 'data_schema': None, + 'description_placeholders': dict({ + 'name': 'Timer', + }), + 'errors': None, + 'flow_id': , + 'handler': 'gardena_bluetooth', + 'last_step': None, + 'step_id': 'confirm', + 'type': , + }) +# --- +# name: test_user_selection.2 + FlowResultSnapshot({ + 'context': dict({ + 'confirm_only': True, + 'source': 'user', + 'title_placeholders': dict({ + 'name': 'Timer', + }), + 'unique_id': '00000000-0000-0000-0000-000000000001', + }), + 'data': dict({ + 'address': '00000000-0000-0000-0000-000000000001', + }), + 'description': None, + 'description_placeholders': None, + 'flow_id': , + 'handler': 'gardena_bluetooth', + 'options': dict({ + }), + 'result': ConfigEntrySnapshot({ + 'data': dict({ + 'address': '00000000-0000-0000-0000-000000000001', + }), + 'disabled_by': None, + 'domain': 'gardena_bluetooth', + 'entry_id': , + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Timer', + 'unique_id': '00000000-0000-0000-0000-000000000001', + 'version': 1, + }), + 'title': 'Timer', + 'type': , + 'version': 1, + }) +# --- diff --git a/tests/components/gardena_bluetooth/test_config_flow.py b/tests/components/gardena_bluetooth/test_config_flow.py new file mode 100644 index 00000000000000..0f0e297c4d7fa6 --- /dev/null +++ b/tests/components/gardena_bluetooth/test_config_flow.py @@ -0,0 +1,134 @@ +"""Test the Gardena Bluetooth config flow.""" +from unittest.mock import Mock + +from gardena_bluetooth.exceptions import CharacteristicNotFound +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant import config_entries +from homeassistant.components.gardena_bluetooth.const import DOMAIN +from homeassistant.core import HomeAssistant + +from . import ( + MISSING_MANUFACTURER_DATA_SERVICE_INFO, + MISSING_SERVICE_SERVICE_INFO, + UNSUPPORTED_GROUP_SERVICE_INFO, + WATER_TIMER_SERVICE_INFO, + WATER_TIMER_UNNAMED_SERVICE_INFO, +) + +from tests.components.bluetooth import ( + inject_bluetooth_service_info, +) + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +async def test_user_selection( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Test we can select a device.""" + + inject_bluetooth_service_info(hass, WATER_TIMER_SERVICE_INFO) + inject_bluetooth_service_info(hass, WATER_TIMER_UNNAMED_SERVICE_INFO) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result == snapshot + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "00000000-0000-0000-0000-000000000001"}, + ) + assert result == snapshot + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + assert result == snapshot + + +async def test_failed_connect( + hass: HomeAssistant, + mock_client: Mock, + snapshot: SnapshotAssertion, +) -> None: + """Test we can select a device.""" + + inject_bluetooth_service_info(hass, WATER_TIMER_SERVICE_INFO) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result == snapshot + + mock_client.read_char.side_effect = CharacteristicNotFound("something went wrong") + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "00000000-0000-0000-0000-000000000001"}, + ) + assert result == snapshot + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + assert result == snapshot + + +async def test_no_devices( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Test missing device.""" + + inject_bluetooth_service_info(hass, MISSING_MANUFACTURER_DATA_SERVICE_INFO) + inject_bluetooth_service_info(hass, MISSING_SERVICE_SERVICE_INFO) + inject_bluetooth_service_info(hass, UNSUPPORTED_GROUP_SERVICE_INFO) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result == snapshot + + +async def test_bluetooth( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Test bluetooth device discovery.""" + + inject_bluetooth_service_info(hass, WATER_TIMER_SERVICE_INFO) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=WATER_TIMER_SERVICE_INFO, + ) + assert result == snapshot + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + assert result == snapshot + + +async def test_bluetooth_invalid( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Test bluetooth device discovery with invalid data.""" + + inject_bluetooth_service_info(hass, UNSUPPORTED_GROUP_SERVICE_INFO) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=UNSUPPORTED_GROUP_SERVICE_INFO, + ) + assert result == snapshot From c236d1734366bda28f1d2fc64b685885f7fde2fc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 16:10:32 +0200 Subject: [PATCH 0417/1009] Migrate cover services to support translations (#96315) * Migrate cover services to support translations * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/cover/services.yaml | 24 --------- homeassistant/components/cover/strings.json | 54 ++++++++++++++++++++ 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/cover/services.yaml b/homeassistant/components/cover/services.yaml index 8ab42c6d3e97c5..9f9e37941e2ccb 100644 --- a/homeassistant/components/cover/services.yaml +++ b/homeassistant/components/cover/services.yaml @@ -1,8 +1,6 @@ # Describes the format for available cover services open_cover: - name: Open - description: Open all or specified cover. target: entity: domain: cover @@ -10,8 +8,6 @@ open_cover: - cover.CoverEntityFeature.OPEN close_cover: - name: Close - description: Close all or specified cover. target: entity: domain: cover @@ -19,8 +15,6 @@ close_cover: - cover.CoverEntityFeature.CLOSE toggle: - name: Toggle - description: Toggle a cover open/closed. target: entity: domain: cover @@ -29,8 +23,6 @@ toggle: - cover.CoverEntityFeature.OPEN set_cover_position: - name: Set position - description: Move to specific position all or specified cover. target: entity: domain: cover @@ -38,8 +30,6 @@ set_cover_position: - cover.CoverEntityFeature.SET_POSITION fields: position: - name: Position - description: Position of the cover required: true selector: number: @@ -48,8 +38,6 @@ set_cover_position: unit_of_measurement: "%" stop_cover: - name: Stop - description: Stop all or specified cover. target: entity: domain: cover @@ -57,8 +45,6 @@ stop_cover: - cover.CoverEntityFeature.STOP open_cover_tilt: - name: Open tilt - description: Open all or specified cover tilt. target: entity: domain: cover @@ -66,8 +52,6 @@ open_cover_tilt: - cover.CoverEntityFeature.OPEN_TILT close_cover_tilt: - name: Close tilt - description: Close all or specified cover tilt. target: entity: domain: cover @@ -75,8 +59,6 @@ close_cover_tilt: - cover.CoverEntityFeature.CLOSE_TILT toggle_cover_tilt: - name: Toggle tilt - description: Toggle a cover tilt open/closed. target: entity: domain: cover @@ -85,8 +67,6 @@ toggle_cover_tilt: - cover.CoverEntityFeature.OPEN_TILT set_cover_tilt_position: - name: Set tilt position - description: Move to specific position all or specified cover tilt. target: entity: domain: cover @@ -94,8 +74,6 @@ set_cover_tilt_position: - cover.CoverEntityFeature.SET_TILT_POSITION fields: tilt_position: - name: Tilt position - description: Tilt position of the cover. required: true selector: number: @@ -104,8 +82,6 @@ set_cover_tilt_position: unit_of_measurement: "%" stop_cover_tilt: - name: Stop tilt - description: Stop all or specified cover. target: entity: domain: cover diff --git a/homeassistant/components/cover/strings.json b/homeassistant/components/cover/strings.json index 2f61bd95083e94..5ed02a84e0df7d 100644 --- a/homeassistant/components/cover/strings.json +++ b/homeassistant/components/cover/strings.json @@ -76,5 +76,59 @@ "window": { "name": "Window" } + }, + "services": { + "open_cover": { + "name": "Open", + "description": "Opens a cover." + }, + "close_cover": { + "name": "Close", + "description": "Closes a cover." + }, + "toggle": { + "name": "Toggle", + "description": "Toggles a cover open/closed." + }, + "set_cover_position": { + "name": "Set position", + "description": "Moves a cover to a specific position.", + "fields": { + "position": { + "name": "Position", + "description": "Target position." + } + } + }, + "stop_cover": { + "name": "Stop", + "description": "Stops the cover movement." + }, + "open_cover_tilt": { + "name": "Open tilt", + "description": "Tilts a cover open." + }, + "close_cover_tilt": { + "name": "Close tilt", + "description": "Tilts a cover to close." + }, + "toggle_cover_tilt": { + "name": "Toggle tilt", + "description": "Toggles a cover tilt open/closed." + }, + "set_cover_tilt_position": { + "name": "Set tilt position", + "description": "Moves a cover tilt to a specific position.", + "fields": { + "tilt_position": { + "name": "Tilt position", + "description": "Target tilt positition." + } + } + }, + "stop_cover_tilt": { + "name": "Stop tilt", + "description": "Stops a tilting cover movement." + } } } From 2d474813c01a7ed0ec52289bacb67e44839862b4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 16:11:01 +0200 Subject: [PATCH 0418/1009] Migrate siren services to support translations (#96400) * Migrate siren services to support translations * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/siren/services.yaml | 9 ------- homeassistant/components/siren/strings.json | 28 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/siren/services.yaml b/homeassistant/components/siren/services.yaml index 154ffff78a3a12..4c2f612bcbc98f 100644 --- a/homeassistant/components/siren/services.yaml +++ b/homeassistant/components/siren/services.yaml @@ -1,8 +1,6 @@ # Describes the format for available siren services turn_on: - name: Turn on - description: Turn siren on. target: entity: domain: siren @@ -10,7 +8,6 @@ turn_on: - siren.SirenEntityFeature.TURN_ON fields: tone: - description: The tone to emit when turning the siren on. When `available_tones` property is a map, either the key or the value can be used. Must be supported by the integration. example: fire filter: supported_features: @@ -19,7 +16,6 @@ turn_on: selector: text: volume_level: - description: The volume level of the noise to emit when turning the siren on. Must be supported by the integration. example: 0.5 filter: supported_features: @@ -31,7 +27,6 @@ turn_on: max: 1 step: 0.05 duration: - description: The duration in seconds of the noise to emit when turning the siren on. Must be supported by the integration. example: 15 filter: supported_features: @@ -41,8 +36,6 @@ turn_on: text: turn_off: - name: Turn off - description: Turn siren off. target: entity: domain: siren @@ -50,8 +43,6 @@ turn_off: - siren.SirenEntityFeature.TURN_OFF toggle: - name: Toggle - description: Toggles a siren. target: entity: domain: siren diff --git a/homeassistant/components/siren/strings.json b/homeassistant/components/siren/strings.json index 60d8843c1515e5..171a853f74c2fe 100644 --- a/homeassistant/components/siren/strings.json +++ b/homeassistant/components/siren/strings.json @@ -13,5 +13,33 @@ } } } + }, + "services": { + "turn_on": { + "name": "Turn on", + "description": "Turns the siren on.", + "fields": { + "tone": { + "name": "Tone", + "description": "The tone to emit. When `available_tones` property is a map, either the key or the value can be used. Must be supported by the integration." + }, + "volume_level": { + "name": "Volume", + "description": "The volume. 0 is inaudible, 1 is the maximum volume. Must be supported by the integration." + }, + "duration": { + "name": "Duration", + "description": "Number of seconds the sound is played. Must be supported by the integration." + } + } + }, + "turn_off": { + "name": "Turn off", + "description": "Turns the siren off." + }, + "toggle": { + "name": "Toggle", + "description": "Toggles the siren on/off." + } } } From 7ca539fcd041cc1cd22e13254e88b375013f663e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 16:11:28 +0200 Subject: [PATCH 0419/1009] Migrate persistent notification services to support translations (#96391) * Migrate persistent notification services to support translations * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- .../persistent_notification/services.yaml | 14 -------- .../persistent_notification/strings.json | 36 +++++++++++++++++++ 2 files changed, 36 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/persistent_notification/strings.json diff --git a/homeassistant/components/persistent_notification/services.yaml b/homeassistant/components/persistent_notification/services.yaml index 046ea237560233..c335d96260087d 100644 --- a/homeassistant/components/persistent_notification/services.yaml +++ b/homeassistant/components/persistent_notification/services.yaml @@ -1,39 +1,25 @@ create: - name: Create - description: Show a notification in the frontend. fields: message: - name: Message - 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. example: Test notification selector: text: notification_id: - name: Notification ID - description: Target ID of the notification, will replace a notification with the same ID. example: 1234 selector: text: dismiss: - name: Dismiss - description: Remove a notification from the frontend. fields: notification_id: - name: Notification ID - description: Target ID of the notification, which should be removed. required: true example: 1234 selector: text: dismiss_all: - name: Dismiss All - description: Remove all notifications. diff --git a/homeassistant/components/persistent_notification/strings.json b/homeassistant/components/persistent_notification/strings.json new file mode 100644 index 00000000000000..6b8ddb46c497be --- /dev/null +++ b/homeassistant/components/persistent_notification/strings.json @@ -0,0 +1,36 @@ +{ + "services": { + "create": { + "name": "Create", + "description": "Shows a notification on the **Notifications** panel.", + "fields": { + "message": { + "name": "Message", + "description": "Message body of the notification." + }, + "title": { + "name": "Title", + "description": "Optional title of the notification." + }, + "notification_id": { + "name": "Notification ID", + "description": "ID of the notification. This new notification will overwrite an existing notification with the same ID." + } + } + }, + "dismiss": { + "name": "Dismiss", + "description": "Removes a notification from the **Notifications** panel.", + "fields": { + "notification_id": { + "name": "Notification ID", + "description": "ID of the notification to be removed." + } + } + }, + "dismiss_all": { + "name": "Dismiss all", + "description": "Removes all notifications from the **Notifications** panel." + } + } +} From 18cc56ae96c0e630fd3185275ad085e789574412 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 16:25:43 +0200 Subject: [PATCH 0420/1009] Migrate media player services to support translations (#96408) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- .../components/media_player/services.yaml | 98 +--------- .../components/media_player/strings.json | 173 ++++++++++++++++++ 2 files changed, 182 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 97605886036a27..7338747b545a03 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -1,8 +1,6 @@ # Describes the format for available media player services turn_on: - name: Turn on - description: Turn a media player power on. target: entity: domain: media_player @@ -10,8 +8,6 @@ turn_on: - media_player.MediaPlayerEntityFeature.TURN_ON turn_off: - name: Turn off - description: Turn a media player power off. target: entity: domain: media_player @@ -19,8 +15,6 @@ turn_off: - media_player.MediaPlayerEntityFeature.TURN_OFF toggle: - name: Toggle - description: Toggles a media player power state. target: entity: domain: media_player @@ -29,8 +23,6 @@ toggle: - media_player.MediaPlayerEntityFeature.TURN_ON volume_up: - name: Turn up volume - description: Turn a media player volume up. target: entity: domain: media_player @@ -39,8 +31,6 @@ volume_up: - media_player.MediaPlayerEntityFeature.VOLUME_STEP volume_down: - name: Turn down volume - description: Turn a media player volume down. target: entity: domain: media_player @@ -49,8 +39,6 @@ volume_down: - media_player.MediaPlayerEntityFeature.VOLUME_STEP volume_mute: - name: Mute volume - description: Mute a media player's volume. target: entity: domain: media_player @@ -58,15 +46,11 @@ volume_mute: - media_player.MediaPlayerEntityFeature.VOLUME_MUTE fields: is_volume_muted: - name: Muted - description: True/false for mute/unmute. required: true selector: boolean: volume_set: - name: Set volume - description: Set a media player's volume level. target: entity: domain: media_player @@ -74,8 +58,6 @@ volume_set: - media_player.MediaPlayerEntityFeature.VOLUME_SET fields: volume_level: - name: Level - description: Volume level to set as float. required: true selector: number: @@ -84,8 +66,6 @@ volume_set: step: 0.01 media_play_pause: - name: Play/Pause - description: Toggle media player play/pause state. target: entity: domain: media_player @@ -94,8 +74,6 @@ media_play_pause: - media_player.MediaPlayerEntityFeature.PLAY media_play: - name: Play - description: Send the media player the command for play. target: entity: domain: media_player @@ -103,8 +81,6 @@ media_play: - media_player.MediaPlayerEntityFeature.PLAY media_pause: - name: Pause - description: Send the media player the command for pause. target: entity: domain: media_player @@ -112,8 +88,6 @@ media_pause: - media_player.MediaPlayerEntityFeature.PAUSE media_stop: - name: Stop - description: Send the media player the stop command. target: entity: domain: media_player @@ -121,8 +95,6 @@ media_stop: - media_player.MediaPlayerEntityFeature.STOP media_next_track: - name: Next - description: Send the media player the command for next track. target: entity: domain: media_player @@ -130,8 +102,6 @@ media_next_track: - media_player.MediaPlayerEntityFeature.NEXT_TRACK media_previous_track: - name: Previous - description: Send the media player the command for previous track. target: entity: domain: media_player @@ -139,8 +109,6 @@ media_previous_track: - media_player.MediaPlayerEntityFeature.PREVIOUS_TRACK media_seek: - name: Seek - description: Send the media player the command to seek in current playing media. target: entity: domain: media_player @@ -148,8 +116,6 @@ media_seek: - media_player.MediaPlayerEntityFeature.SEEK fields: seek_position: - name: Position - description: Position to seek to. The format is platform dependent. required: true selector: number: @@ -159,8 +125,6 @@ media_seek: mode: box play_media: - name: Play media - description: Send the media player the command for playing media. target: entity: domain: media_player @@ -168,26 +132,18 @@ play_media: - media_player.MediaPlayerEntityFeature.PLAY_MEDIA fields: media_content_id: - name: Content ID - description: The ID of the content to play. Platform dependent. required: true example: "https://home-assistant.io/images/cast/splash.png" selector: text: media_content_type: - name: Content type - description: - The type of the content to play. Like image, music, tvshow, video, - episode, channel or playlist. required: true example: "music" selector: text: enqueue: - name: Enqueue - description: If the content should be played now or be added to the queue. filter: supported_features: - media_player.MediaPlayerEntityFeature.MEDIA_ENQUEUE @@ -195,17 +151,12 @@ play_media: selector: select: options: - - label: "Play now" - value: "play" - - label: "Play next" - value: "next" - - label: "Add to queue" - value: "add" - - label: "Play now and clear queue" - value: "replace" + - "play" + - "next" + - "add" + - "replace" + translation_key: enqueue announce: - name: Announce - description: If the media should be played as an announcement. filter: supported_features: - media_player.MediaPlayerEntityFeature.MEDIA_ANNOUNCE @@ -215,8 +166,6 @@ play_media: boolean: select_source: - name: Select source - description: Send the media player the command to change input source. target: entity: domain: media_player @@ -224,16 +173,12 @@ select_source: - media_player.MediaPlayerEntityFeature.SELECT_SOURCE fields: source: - name: Source - description: Name of the source to switch to. Platform dependent. required: true example: "video1" selector: text: select_sound_mode: - name: Select sound mode - description: Send the media player the command to change sound mode. target: entity: domain: media_player @@ -241,15 +186,11 @@ select_sound_mode: - media_player.MediaPlayerEntityFeature.SELECT_SOUND_MODE fields: sound_mode: - name: Sound mode - description: Name of the sound mode to switch to. example: "Music" selector: text: clear_playlist: - name: Clear playlist - description: Send the media player the command to clear players playlist. target: entity: domain: media_player @@ -257,8 +198,6 @@ clear_playlist: - media_player.MediaPlayerEntityFeature.CLEAR_PLAYLIST shuffle_set: - name: Shuffle - description: Set shuffling state. target: entity: domain: media_player @@ -266,15 +205,11 @@ shuffle_set: - media_player.MediaPlayerEntityFeature.SHUFFLE_SET fields: shuffle: - name: Shuffle - description: True/false for enabling/disabling shuffle. required: true selector: boolean: repeat_set: - name: Repeat - description: Set repeat mode target: entity: domain: media_player @@ -282,24 +217,15 @@ repeat_set: - media_player.MediaPlayerEntityFeature.REPEAT_SET fields: repeat: - name: Repeat mode - description: Repeat mode to set. required: true selector: select: options: - - label: "Off" - value: "off" - - label: "Repeat all" - value: "all" - - label: "Repeat one" - value: "one" - + - "off" + - "all" + - "one" + translation_key: repeat join: - name: Join - description: - Group players together. Only works on platforms with support for player - groups. target: entity: domain: media_player @@ -307,8 +233,6 @@ join: - media_player.MediaPlayerEntityFeature.GROUPING fields: group_members: - name: Group members - description: The players which will be synced with the target player. required: true example: | - media_player.multiroom_player2 @@ -319,10 +243,6 @@ join: domain: media_player unjoin: - description: - Unjoin the player from a group. Only works on platforms with support for - player groups. - name: Unjoin target: entity: domain: media_player diff --git a/homeassistant/components/media_player/strings.json b/homeassistant/components/media_player/strings.json index 67c92d7ce072f3..10148f99fef52c 100644 --- a/homeassistant/components/media_player/strings.json +++ b/homeassistant/components/media_player/strings.json @@ -159,5 +159,178 @@ "receiver": { "name": "Receiver" } + }, + "services": { + "turn_on": { + "name": "Turn on", + "description": "Turns on the power of the media player." + }, + "turn_off": { + "name": "Turn off", + "description": "Turns off the power of the media player." + }, + "toggle": { + "name": "Toggle", + "description": "Toggles a media player on/off." + }, + "volume_up": { + "name": "Turn up volume", + "description": "Turns up the volume." + }, + "volume_down": { + "name": "Turn down volume", + "description": "Turns down the volume." + }, + "volume_mute": { + "name": "Mute/unmute volume", + "description": "Mutes or unmutes the media player.", + "fields": { + "is_volume_muted": { + "name": "Muted", + "description": "Defines whether or not it is muted." + } + } + }, + "volume_set": { + "name": "Set volume", + "description": "Sets the volume level.", + "fields": { + "volume_level": { + "name": "Level", + "description": "The volume. 0 is inaudible, 1 is the maximum volume." + } + } + }, + "media_play_pause": { + "name": "Play/Pause", + "description": "Toggles play/pause." + }, + "media_play": { + "name": "Play", + "description": "Starts playing." + }, + "media_pause": { + "name": "Pause", + "description": "Pauses." + }, + "media_stop": { + "name": "Stop", + "description": "Stops playing." + }, + "media_next_track": { + "name": "Next", + "description": "Selects the next track." + }, + "media_previous_track": { + "name": "Previous", + "description": "Selects the previous track." + }, + "media_seek": { + "name": "Seek", + "description": "Allows you to go to a different part of the media that is currently playing.", + "fields": { + "seek_position": { + "name": "Position", + "description": "Target position in the currently playing media. The format is platform dependent." + } + } + }, + "play_media": { + "name": "Play media", + "description": "Starts playing specified media.", + "fields": { + "media_content_id": { + "name": "Content ID", + "description": "The ID of the content to play. Platform dependent." + }, + "media_content_type": { + "name": "Content type", + "description": "The type of the content to play. Such as image, music, tv show, video, episode, channel, or playlist." + }, + "enqueue": { + "name": "Enqueue", + "description": "If the content should be played now or be added to the queue." + }, + "announce": { + "name": "Announce", + "description": "If the media should be played as an announcement." + } + } + }, + "select_source": { + "name": "Select source", + "description": "Sends the media player the command to change input source.", + "fields": { + "source": { + "name": "Source", + "description": "Name of the source to switch to. Platform dependent." + } + } + }, + "select_sound_mode": { + "name": "Select sound mode", + "description": "Selects a specific sound mode.", + "fields": { + "sound_mode": { + "name": "Sound mode", + "description": "Name of the sound mode to switch to." + } + } + }, + "clear_playlist": { + "name": "Clear playlist", + "description": "Clears the playlist." + }, + "shuffle_set": { + "name": "Shuffle", + "description": "Playback mode that selects the media in randomized order.", + "fields": { + "shuffle": { + "name": "Shuffle", + "description": "Whether or not shuffle mode is enabled." + } + } + }, + "repeat_set": { + "name": "Repeat", + "description": "Playback mode that plays the media in a loop.", + "fields": { + "repeat": { + "name": "Repeat mode", + "description": "Repeat mode to set." + } + } + }, + "join": { + "name": "Join", + "description": "Groups media players together for synchronous playback. Only works on supported multiroom audio systems.", + "fields": { + "group_members": { + "name": "Group members", + "description": "The players which will be synced with the playback specified in `target`." + } + } + }, + "unjoin": { + "name": "Unjoin", + "description": "Removes the player from a group. Only works on platforms which support player groups." + } + }, + "selector": { + "enqueue": { + "options": { + "play": "Play", + "next": "Play next", + "add": "Add to queue", + "replace": "Play now and clear queue" + } + }, + "repeat": { + "options": { + "off": "Off", + "all": "Repeat all", + "one": "Repeat one" + } + } } } From 594d240a968d3ad1fed356c7e2706d32fe2ebbba Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 16:37:18 +0200 Subject: [PATCH 0421/1009] Migrate & fix logger services to support translations (#96393) * Migrate logger services to support translations * Fix tests and schema validation * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/logger/services.yaml | 26 +++++----------- homeassistant/components/logger/strings.json | 30 +++++++++++++++++++ homeassistant/helpers/service.py | 2 +- tests/helpers/test_service.py | 3 ++ 4 files changed, 41 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/logger/strings.json diff --git a/homeassistant/components/logger/services.yaml b/homeassistant/components/logger/services.yaml index c20d1171bb2528..d7d2a5b32e8fcc 100644 --- a/homeassistant/components/logger/services.yaml +++ b/homeassistant/components/logger/services.yaml @@ -1,26 +1,14 @@ set_default_level: - name: Set default level - description: Set the default log level for integrations. fields: level: - name: Level - description: Default severity level for all integrations. selector: select: options: - - label: "Debug" - value: "debug" - - label: "Info" - value: "info" - - label: "Warning" - value: "warning" - - label: "Error" - value: "error" - - label: "Fatal" - value: "fatal" - - label: "Critical" - value: "critical" - + - "debug" + - "info" + - "warning" + - "error" + - "fatal" + - "critical" + translation_key: level set_level: - name: Set level - description: Set log level for integrations. diff --git a/homeassistant/components/logger/strings.json b/homeassistant/components/logger/strings.json new file mode 100644 index 00000000000000..aedaec420351bc --- /dev/null +++ b/homeassistant/components/logger/strings.json @@ -0,0 +1,30 @@ +{ + "services": { + "set_default_level": { + "name": "Set default level", + "description": "Sets the default log level for integrations.", + "fields": { + "level": { + "name": "Level", + "description": "Default severity level for all integrations." + } + } + }, + "set_level": { + "name": "Set level", + "description": "Sets the log level for one or more integrations." + } + }, + "selector": { + "level": { + "options": { + "debug": "Debug", + "info": "Info", + "warning": "Warning", + "error": "Error", + "fatal": "Fatal", + "critical": "Critical" + } + } + } +} diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 946340ea69cae7..ab0b4ea32e9610 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -173,7 +173,7 @@ def validate_supported_feature(supported_feature: str) -> Any: extra=vol.ALLOW_EXTRA, ) -_SERVICES_SCHEMA = vol.Schema({cv.slug: _SERVICE_SCHEMA}) +_SERVICES_SCHEMA = vol.Schema({cv.slug: vol.Any(None, _SERVICE_SCHEMA)}) class ServiceParams(TypedDict): diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index a99f303f6c99f7..674d2e1af4c243 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -677,6 +677,9 @@ async def test_async_get_all_descriptions_failing_integration( with patch( "homeassistant.helpers.service.async_get_integrations", return_value={"logger": ImportError}, + ), patch( + "homeassistant.helpers.service.translation.async_get_translations", + return_value={}, ): descriptions = await service.async_get_all_descriptions(hass) From dc2406ae09164bf9e207c30037d5c8a77a0d0d07 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 16:37:30 +0200 Subject: [PATCH 0422/1009] Migrate alarm control panel services to support translations (#96305) * Migrate alarm control panel services to support translations * String references * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- .../alarm_control_panel/services.yaml | 28 -------- .../alarm_control_panel/strings.json | 72 +++++++++++++++++++ 2 files changed, 72 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index c3022b87eb7684..f7a3854b6b3353 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -1,22 +1,16 @@ # Describes the format for available alarm control panel services alarm_disarm: - name: Disarm - description: Send the alarm the command for disarm. target: entity: domain: alarm_control_panel fields: code: - name: Code - description: An optional code to disarm the alarm control panel with. example: "1234" selector: text: alarm_arm_custom_bypass: - name: Arm with custom bypass - description: Send arm custom bypass command. target: entity: domain: alarm_control_panel @@ -24,15 +18,11 @@ alarm_arm_custom_bypass: - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS fields: code: - name: Code - description: An optional code to arm custom bypass the alarm control panel with. example: "1234" selector: text: alarm_arm_home: - name: Arm home - description: Send the alarm the command for arm home. target: entity: domain: alarm_control_panel @@ -40,15 +30,11 @@ alarm_arm_home: - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME fields: code: - name: Code - description: An optional code to arm home the alarm control panel with. example: "1234" selector: text: alarm_arm_away: - name: Arm away - description: Send the alarm the command for arm away. target: entity: domain: alarm_control_panel @@ -56,15 +42,11 @@ alarm_arm_away: - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY fields: code: - name: Code - description: An optional code to arm away the alarm control panel with. example: "1234" selector: text: alarm_arm_night: - name: Arm night - description: Send the alarm the command for arm night. target: entity: domain: alarm_control_panel @@ -72,15 +54,11 @@ alarm_arm_night: - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT fields: code: - name: Code - description: An optional code to arm night the alarm control panel with. example: "1234" selector: text: alarm_arm_vacation: - name: Arm vacation - description: Send the alarm the command for arm vacation. target: entity: domain: alarm_control_panel @@ -88,15 +66,11 @@ alarm_arm_vacation: - alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION fields: code: - name: Code - description: An optional code to arm vacation the alarm control panel with. example: "1234" selector: text: alarm_trigger: - name: Trigger - description: Send the alarm the command for trigger. target: entity: domain: alarm_control_panel @@ -104,8 +78,6 @@ alarm_trigger: - alarm_control_panel.AlarmControlPanelEntityFeature.TRIGGER fields: code: - name: Code - description: An optional code to trigger the alarm control panel with. example: "1234" selector: text: diff --git a/homeassistant/components/alarm_control_panel/strings.json b/homeassistant/components/alarm_control_panel/strings.json index 6b01cab2beccfe..deaab6d75eedad 100644 --- a/homeassistant/components/alarm_control_panel/strings.json +++ b/homeassistant/components/alarm_control_panel/strings.json @@ -62,5 +62,77 @@ } } } + }, + "services": { + "alarm_disarm": { + "name": "Disarm", + "description": "Disarms the alarm.", + "fields": { + "code": { + "name": "Code", + "description": "Code to disarm the alarm." + } + } + }, + "alarm_arm_custom_bypass": { + "name": "Arm with custom bypass", + "description": "Arms the alarm while allowing to bypass a custom area.", + "fields": { + "code": { + "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]", + "description": "Code to arm the alarm." + } + } + }, + "alarm_arm_home": { + "name": "Arm home", + "description": "Sets the alarm to: _armed, but someone is home_.", + "fields": { + "code": { + "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]", + "description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]" + } + } + }, + "alarm_arm_away": { + "name": "Arm away", + "description": "Sets the alarm to: _armed, no one home_.", + "fields": { + "code": { + "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]", + "description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]" + } + } + }, + "alarm_arm_night": { + "name": "Arm night", + "description": "Sets the alarm to: _armed for the night_.", + "fields": { + "code": { + "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]", + "description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]" + } + } + }, + "alarm_arm_vacation": { + "name": "Arm vacation", + "description": "Sets the alarm to: _armed for vacation_.", + "fields": { + "code": { + "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]", + "description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]" + } + } + }, + "alarm_trigger": { + "name": "Trigger", + "description": "Enables an external alarm trigger.", + "fields": { + "code": { + "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]", + "description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]" + } + } + } } } From d0b7a477687926b2ea64a940c308786be68e82e0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 16:37:59 +0200 Subject: [PATCH 0423/1009] Migrate mqtt services to support translations (#96396) * Migrate mqtt services to support translations * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/mqtt/services.yaml | 22 ---------- homeassistant/components/mqtt/strings.json | 46 +++++++++++++++++++++ 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/mqtt/services.yaml b/homeassistant/components/mqtt/services.yaml index 07507035c57eff..4960cf9fb82636 100644 --- a/homeassistant/components/mqtt/services.yaml +++ b/homeassistant/components/mqtt/services.yaml @@ -1,32 +1,22 @@ # Describes the format for available MQTT services publish: - name: Publish - description: Publish a message to an MQTT topic. fields: topic: - name: Topic - description: Topic to publish payload. required: true example: /homeassistant/hello selector: text: payload: - name: Payload - description: Payload to publish. example: This is great selector: text: payload_template: - name: Payload Template - description: Template to render as payload value. Ignored if payload given. advanced: true example: "{{ states('sensor.temperature') }}" selector: object: qos: - name: QoS - description: Quality of Service to use. advanced: true default: 0 selector: @@ -36,27 +26,17 @@ publish: - "1" - "2" retain: - name: Retain - description: If message should have the retain flag set. default: false selector: boolean: dump: - name: Dump - description: - Dump messages on a topic selector to the 'mqtt_dump.txt' file in your - configuration folder. fields: topic: - name: Topic - description: topic to listen to example: "OpenZWave/#" selector: text: duration: - name: Duration - description: how long we should listen for messages in seconds default: 5 selector: number: @@ -65,5 +45,3 @@ dump: unit_of_measurement: "seconds" reload: - name: Reload - description: Reload all MQTT entities from YAML. diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 3423b2cd470102..61d2b40314b135 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -145,5 +145,51 @@ "custom": "Custom" } } + }, + "services": { + "publish": { + "name": "Publish", + "description": "Publishes a message to an MQTT topic.", + "fields": { + "topic": { + "name": "Topic", + "description": "Topic to publish to." + }, + "payload": { + "name": "Payload", + "description": "The payload to publish." + }, + "payload_template": { + "name": "Payload template", + "description": "Template to render as a payload value. If a payload is provided, the template is ignored." + }, + "qos": { + "name": "QoS", + "description": "Quality of Service to use. O. At most once. 1: At least once. 2: Exactly once." + }, + "retain": { + "name": "Retain", + "description": "If the message should have the retain flag set. If set, the broker stores the most recent message on a topic." + } + } + }, + "dump": { + "name": "Export", + "description": "Writes all messages on a specific topic into the `mqtt_dump.txt` file in your configuration folder.", + "fields": { + "topic": { + "name": "Topic", + "description": "Topic to listen to." + }, + "duration": { + "name": "Duration", + "description": "How long we should listen for messages in seconds." + } + } + }, + "reload": { + "name": "Reload", + "description": "Reloads MQTT entities from the YAML-configuration." + } } } From 6c40004061039ee5bc8a9e88c78aede0bd9e7988 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 16:38:47 +0200 Subject: [PATCH 0424/1009] Migrate integration services (I-K) to support translations (#96373) * Migrate integration services (I-K) to support translations * Apply suggestions from code review Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/kodi/strings.json Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/icloud/services.yaml | 30 ----- homeassistant/components/icloud/strings.json | 70 ++++++++++ homeassistant/components/ifttt/services.yaml | 15 --- homeassistant/components/ifttt/strings.json | 38 ++++++ homeassistant/components/ihc/services.yaml | 38 ------ homeassistant/components/ihc/strings.json | 72 +++++++++++ .../components/insteon/services.yaml | 46 ------- homeassistant/components/insteon/strings.json | 114 +++++++++++++++++ homeassistant/components/iperf3/services.yaml | 4 - homeassistant/components/iperf3/strings.json | 14 ++ homeassistant/components/isy994/services.yaml | 63 --------- homeassistant/components/isy994/strings.json | 120 +++++++++++++++++- homeassistant/components/izone/services.yaml | 8 -- homeassistant/components/izone/strings.json | 22 ++++ homeassistant/components/keba/services.yaml | 44 ------- homeassistant/components/keba/strings.json | 62 +++++++++ homeassistant/components/kef/services.yaml | 40 ------ homeassistant/components/kef/strings.json | 98 ++++++++++++++ .../components/keyboard/services.yaml | 29 ----- .../components/keyboard/strings.json | 28 ++++ .../components/keymitt_ble/services.yaml | 10 -- .../components/keymitt_ble/strings.json | 24 ++++ homeassistant/components/knx/services.yaml | 38 ------ homeassistant/components/knx/strings.json | 86 +++++++++++++ homeassistant/components/kodi/services.yaml | 14 -- homeassistant/components/kodi/strings.json | 34 +++++ 26 files changed, 781 insertions(+), 380 deletions(-) create mode 100644 homeassistant/components/ihc/strings.json create mode 100644 homeassistant/components/iperf3/strings.json create mode 100644 homeassistant/components/keba/strings.json create mode 100644 homeassistant/components/kef/strings.json create mode 100644 homeassistant/components/keyboard/strings.json diff --git a/homeassistant/components/icloud/services.yaml b/homeassistant/components/icloud/services.yaml index ddeae448f8a51e..5ffbc2a49ae715 100644 --- a/homeassistant/components/icloud/services.yaml +++ b/homeassistant/components/icloud/services.yaml @@ -1,93 +1,63 @@ update: - name: Update - description: Update iCloud devices. fields: account: - name: Account - description: Your iCloud account username (email) or account name. required: true example: "steve@apple.com" selector: text: play_sound: - name: Play sound - description: Play sound on an Apple device. fields: account: - name: Account - description: Your iCloud account username (email) or account name. required: true example: "steve@apple.com" selector: text: device_name: - name: Device Name - description: The name of the Apple device to play a sound. required: true example: "stevesiphone" selector: text: display_message: - name: Display message - description: Display a message on an Apple device. fields: account: - name: Account - description: Your iCloud account username (email) or account name. required: true example: "steve@apple.com" selector: text: device_name: - name: Device Name - description: The name of the Apple device to display the message. required: true example: "stevesiphone" selector: text: message: - name: Message - description: The content of your message. required: true example: "Hey Steve !" selector: text: sound: - name: Sound - description: To make a sound when displaying the message. selector: boolean: lost_device: - name: Lost device - description: Make an Apple device in lost state. fields: account: - name: Account - description: Your iCloud account username (email) or account name. required: true example: "steve@apple.com" selector: text: device_name: - name: Device Name - description: The name of the Apple device to set lost. required: true example: "stevesiphone" selector: text: number: - name: Number - description: The phone number to call in lost mode (must contain country code). required: true example: "+33450020100" selector: text: message: - name: Message - description: The message to display in lost mode. required: true example: "Call me" selector: diff --git a/homeassistant/components/icloud/strings.json b/homeassistant/components/icloud/strings.json index 385dc74a0ab83e..9bc7750790f308 100644 --- a/homeassistant/components/icloud/strings.json +++ b/homeassistant/components/icloud/strings.json @@ -42,5 +42,75 @@ "no_device": "None of your devices have \"Find my iPhone\" activated", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "services": { + "update": { + "name": "Update", + "description": "Updates iCloud devices.", + "fields": { + "account": { + "name": "Account", + "description": "Your iCloud account username (email) or account name." + } + } + }, + "play_sound": { + "name": "Play sound", + "description": "Plays sound on an Apple device.", + "fields": { + "account": { + "name": "Account", + "description": "Your iCloud account username (email) or account name." + }, + "device_name": { + "name": "Device name", + "description": "The name of the Apple device to play a sound." + } + } + }, + "display_message": { + "name": "Display message", + "description": "Displays a message on an Apple device.", + "fields": { + "account": { + "name": "Account", + "description": "Your iCloud account username (email) or account name." + }, + "device_name": { + "name": "Device name", + "description": "The name of the Apple device to display the message." + }, + "message": { + "name": "Message", + "description": "The content of your message." + }, + "sound": { + "name": "Sound", + "description": "To make a sound when displaying the message." + } + } + }, + "lost_device": { + "name": "Lost device", + "description": "Makes an Apple device in lost state.", + "fields": { + "account": { + "name": "Account", + "description": "Your iCloud account username (email) or account name." + }, + "device_name": { + "name": "Device name", + "description": "The name of the Apple device to set lost." + }, + "number": { + "name": "Number", + "description": "The phone number to call in lost mode (must contain country code)." + }, + "message": { + "name": "Message", + "description": "The message to display in lost mode." + } + } + } } } diff --git a/homeassistant/components/ifttt/services.yaml b/homeassistant/components/ifttt/services.yaml index 9c02284d4f883b..550aecad56be58 100644 --- a/homeassistant/components/ifttt/services.yaml +++ b/homeassistant/components/ifttt/services.yaml @@ -1,49 +1,34 @@ # Describes the format for available ifttt services push_alarm_state: - name: Push alarm state - description: Update the alarm state to the specified value. fields: entity_id: - description: Name of the alarm control panel which state has to be updated. required: true selector: entity: domain: alarm_control_panel state: - name: State - description: The state to which the alarm control panel has to be set. required: true example: "armed_night" selector: text: trigger: - name: Trigger - description: Triggers the configured IFTTT Webhook. fields: event: - name: Event - description: The name of the event to send. required: true example: "MY_HA_EVENT" selector: text: value1: - name: Value 1 - description: Generic field to send data via the event. example: "Hello World" selector: text: value2: - name: Value 2 - description: Generic field to send data via the event. example: "some additional data" selector: text: value3: - name: Value 3 - description: Generic field to send data via the event. example: "even more data" selector: text: diff --git a/homeassistant/components/ifttt/strings.json b/homeassistant/components/ifttt/strings.json index 179d62b463c5cc..e52a0882eb1b0d 100644 --- a/homeassistant/components/ifttt/strings.json +++ b/homeassistant/components/ifttt/strings.json @@ -14,5 +14,43 @@ "create_entry": { "default": "To send events to Home Assistant, you will need to use the \"Make a web request\" action from the [IFTTT Webhook applet]({applet_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data." } + }, + "services": { + "push_alarm_state": { + "name": "Push alarm state", + "description": "Updates the alarm state to the specified value.", + "fields": { + "entity_id": { + "name": "Entity ID", + "description": "Name of the alarm control panel which state has to be updated." + }, + "state": { + "name": "State", + "description": "The state to which the alarm control panel has to be set." + } + } + }, + "trigger": { + "name": "Trigger", + "description": "Triggers the configured IFTTT Webhook.", + "fields": { + "event": { + "name": "Event", + "description": "The name of the event to send." + }, + "value1": { + "name": "Value 1", + "description": "Generic field to send data via the event." + }, + "value2": { + "name": "Value 2", + "description": "Generic field to send data via the event." + }, + "value3": { + "name": "Value 3", + "description": "Generic field to send data via the event." + } + } + } } } diff --git a/homeassistant/components/ihc/services.yaml b/homeassistant/components/ihc/services.yaml index 33f6c8ca31d6db..1e1727abea87dd 100644 --- a/homeassistant/components/ihc/services.yaml +++ b/homeassistant/components/ihc/services.yaml @@ -1,22 +1,14 @@ # Describes the format for available IHC services set_runtime_value_bool: - name: Set runtime value boolean - description: Set a boolean runtime value on the IHC controller. fields: controller_id: - name: Controller ID - description: | - If you have multiple controller, this is the index of you controller - starting with 0. default: 0 selector: number: min: 0 max: 100 ihc_id: - name: IHC ID - description: The integer IHC resource ID. required: true selector: number: @@ -24,29 +16,19 @@ set_runtime_value_bool: max: 1000000 mode: box value: - name: Value - description: The boolean value to set. required: true selector: boolean: set_runtime_value_int: - name: Set runtime value integer - description: Set an integer runtime value on the IHC controller. fields: controller_id: - name: Controller ID - description: | - If you have multiple controller, this is the index of you controller - starting with 0. default: 0 selector: number: min: 0 max: 100 ihc_id: - name: IHC ID - description: The integer IHC resource ID. required: true selector: number: @@ -54,8 +36,6 @@ set_runtime_value_int: max: 1000000 mode: box value: - name: Value - description: The integer value to set. required: true selector: number: @@ -64,22 +44,14 @@ set_runtime_value_int: mode: box set_runtime_value_float: - name: Set runtime value float - description: Set a float runtime value on the IHC controller. fields: controller_id: - name: Controller ID - description: | - If you have multiple controller, this is the index of you controller - starting with 0. default: 0 selector: number: min: 0 max: 100 ihc_id: - name: IHC ID - description: The integer IHC resource ID. required: true selector: number: @@ -87,8 +59,6 @@ set_runtime_value_float: max: 1000000 mode: box value: - name: Value - description: The float value to set. required: true selector: number: @@ -98,22 +68,14 @@ set_runtime_value_float: mode: box pulse: - name: Pulse - description: Pulses an input on the IHC controller. fields: controller_id: - name: Controller ID - description: | - If you have multiple controller, this is the index of you controller - starting with 0. default: 0 selector: number: min: 0 max: 100 ihc_id: - name: IHC ID - description: The integer IHC resource ID. required: true selector: number: diff --git a/homeassistant/components/ihc/strings.json b/homeassistant/components/ihc/strings.json new file mode 100644 index 00000000000000..3ee45a4f4649ec --- /dev/null +++ b/homeassistant/components/ihc/strings.json @@ -0,0 +1,72 @@ +{ + "services": { + "set_runtime_value_bool": { + "name": "Set runtime value boolean", + "description": "Sets a boolean runtime value on the IHC controller.", + "fields": { + "controller_id": { + "name": "Controller ID", + "description": "If you have multiple controller, this is the index of you controller\nstarting with 0.\n." + }, + "ihc_id": { + "name": "IHC ID", + "description": "The integer IHC resource ID." + }, + "value": { + "name": "Value", + "description": "The boolean value to set." + } + } + }, + "set_runtime_value_int": { + "name": "Set runtime value integer", + "description": "Sets an integer runtime value on the IHC controller.", + "fields": { + "controller_id": { + "name": "Controller ID", + "description": "If you have multiple controller, this is the index of you controller\nstarting with 0.\n." + }, + "ihc_id": { + "name": "IHC ID", + "description": "The integer IHC resource ID." + }, + "value": { + "name": "Value", + "description": "The integer value to set." + } + } + }, + "set_runtime_value_float": { + "name": "Set runtime value float", + "description": "Sets a float runtime value on the IHC controller.", + "fields": { + "controller_id": { + "name": "Controller ID", + "description": "If you have multiple controller, this is the index of you controller\nstarting with 0.\n." + }, + "ihc_id": { + "name": "IHC ID", + "description": "The integer IHC resource ID." + }, + "value": { + "name": "Value", + "description": "The float value to set." + } + } + }, + "pulse": { + "name": "Pulse", + "description": "Pulses an input on the IHC controller.", + "fields": { + "controller_id": { + "name": "Controller ID", + "description": "If you have multiple controller, this is the index of you controller\nstarting with 0.\n." + }, + "ihc_id": { + "name": "IHC ID", + "description": "The integer IHC resource ID." + } + } + } + } +} diff --git a/homeassistant/components/insteon/services.yaml b/homeassistant/components/insteon/services.yaml index 164c917c793c64..a58dfb4b8ce0a4 100644 --- a/homeassistant/components/insteon/services.yaml +++ b/homeassistant/components/insteon/services.yaml @@ -1,18 +1,12 @@ add_all_link: - name: Add all link - description: Tells the Insteom Modem (IM) start All-Linking mode. Once the IM is in All-Linking mode, press the link button on the device to complete All-Linking. fields: group: - name: Group - description: All-Link group number. required: true selector: number: min: 0 max: 255 mode: - name: Mode - description: Linking mode controller - IM is controller responder - IM is responder required: true selector: select: @@ -20,55 +14,35 @@ add_all_link: - "controller" - "responder" delete_all_link: - name: Delete all link - description: Tells the Insteon Modem (IM) to remove an All-Link record from the All-Link Database of the IM and a device. Once the IM is set to delete the link, press the link button on the corresponding device to complete the process. fields: group: - name: Group - description: All-Link group number. required: true selector: number: min: 0 max: 255 load_all_link_database: - name: Load all link database - description: Load the All-Link Database for a device. WARNING - Loading a device All-LInk database is very time consuming and inconsistent. This may take a LONG time and may need to be repeated to obtain all records. fields: entity_id: - name: Entity - description: Name of the device to load. Use "all" to load the database of all devices. required: true example: "light.1a2b3c" selector: text: reload: - name: Reload - description: Reload all records. If true the current records are cleared from memory (does not effect the device) and the records are reloaded. If false the existing records are left in place and only missing records are added. Default is false. default: false selector: boolean: print_all_link_database: - name: Print all link database - description: Print the All-Link Database for a device. Requires that the All-Link Database is loaded into memory. fields: entity_id: - name: Entity - description: Name of the device to print required: true selector: entity: integration: insteon print_im_all_link_database: - name: Print IM all link database - description: Print the All-Link Database for the INSTEON Modem (IM). x10_all_units_off: - name: X10 all units off - description: Send X10 All Units Off command fields: housecode: - name: Housecode - description: X10 house code required: true selector: select: @@ -90,12 +64,8 @@ x10_all_units_off: - "o" - "p" x10_all_lights_on: - name: X10 all lights on - description: Send X10 All Lights On command fields: housecode: - name: Housecode - description: X10 house code required: true selector: select: @@ -117,12 +87,8 @@ x10_all_lights_on: - "o" - "p" x10_all_lights_off: - name: X10 all lights off - description: Send X10 All Lights Off command fields: housecode: - name: Housecode - description: X10 house code required: true selector: select: @@ -144,36 +110,24 @@ x10_all_lights_off: - "o" - "p" scene_on: - name: Scene on - description: Trigger an INSTEON scene to turn ON. fields: group: - name: Group - description: INSTEON group or scene number required: true selector: number: min: 0 max: 255 scene_off: - name: Scene off - description: Trigger an INSTEON scene to turn OFF. fields: group: - name: Group - description: INSTEON group or scene number required: true selector: number: min: 0 max: 255 add_default_links: - name: Add default links - description: Add the default links between the device and the Insteon Modem (IM) fields: entity_id: - name: Entity - description: Name of the device to load. Use "all" to load the database of all devices. required: true example: "light.1a2b3c" selector: diff --git a/homeassistant/components/insteon/strings.json b/homeassistant/components/insteon/strings.json index a93ba4a7476aa9..3f3e3df78c7bde 100644 --- a/homeassistant/components/insteon/strings.json +++ b/homeassistant/components/insteon/strings.json @@ -109,5 +109,119 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "input_error": "Invalid entries, please check your values." } + }, + "services": { + "add_all_link": { + "name": "Add all link", + "description": "Tells the Insteom Modem (IM) start All-Linking mode. Once the IM is in All-Linking mode, press the link button on the device to complete All-Linking.", + "fields": { + "group": { + "name": "Group", + "description": "All-Link group number." + }, + "mode": { + "name": "Mode", + "description": "Linking mode controller - IM is controller responder - IM is responder." + } + } + }, + "delete_all_link": { + "name": "Delete all link", + "description": "Tells the Insteon Modem (IM) to remove an All-Link record from the All-Link Database of the IM and a device. Once the IM is set to delete the link, press the link button on the corresponding device to complete the process.", + "fields": { + "group": { + "name": "Group", + "description": "All-Link group number." + } + } + }, + "load_all_link_database": { + "name": "Load all link database", + "description": "Load the All-Link Database for a device. WARNING - Loading a device All-LInk database is very time consuming and inconsistent. This may take a LONG time and may need to be repeated to obtain all records.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name of the device to load. Use \"all\" to load the database of all devices." + }, + "reload": { + "name": "Reload", + "description": "Reloads all records. If true the current records are cleared from memory (does not effect the device) and the records are reloaded. If false the existing records are left in place and only missing records are added. Default is false." + } + } + }, + "print_all_link_database": { + "name": "Print all link database", + "description": "Prints the All-Link Database for a device. Requires that the All-Link Database is loaded into memory.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name of the device to print." + } + } + }, + "print_im_all_link_database": { + "name": "Print IM all link database", + "description": "Prints the All-Link Database for the INSTEON Modem (IM)." + }, + "x10_all_units_off": { + "name": "X10 all units off", + "description": "Tells the Insteom Modem (IM) start All-Linking mode. Once the IM is in All-Linking mode, press the link button on the device to complete All-Linking.", + "fields": { + "housecode": { + "name": "Housecode", + "description": "X10 house code." + } + } + }, + "x10_all_lights_on": { + "name": "X10 all lights on", + "description": "Sends X10 All Lights On command.", + "fields": { + "housecode": { + "name": "Housecode", + "description": "X10 house code." + } + } + }, + "x10_all_lights_off": { + "name": "X10 all lights off", + "description": "Sends X10 All Lights Off command.", + "fields": { + "housecode": { + "name": "Housecode", + "description": "X10 house code." + } + } + }, + "scene_on": { + "name": "Scene on", + "description": "Triggers an INSTEON scene to turn ON.", + "fields": { + "group": { + "name": "Group", + "description": "INSTEON group or scene number." + } + } + }, + "scene_off": { + "name": "Scene off", + "description": "Triggers an INSTEON scene to turn OFF.", + "fields": { + "group": { + "name": "Group", + "description": "INSTEON group or scene number." + } + } + }, + "add_default_links": { + "name": "Add default links", + "description": "Adds the default links between the device and the Insteon Modem (IM).", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name of the device to load. Use \"all\" to load the database of all devices." + } + } + } } } diff --git a/homeassistant/components/iperf3/services.yaml b/homeassistant/components/iperf3/services.yaml index ba0fdb89712897..b0cc4f11639d89 100644 --- a/homeassistant/components/iperf3/services.yaml +++ b/homeassistant/components/iperf3/services.yaml @@ -1,10 +1,6 @@ speedtest: - name: Speedtest - description: Immediately execute a speed test with iperf3 fields: host: - name: Host - description: The host name of the iperf3 server (already configured) to run a test with. example: "iperf.he.net" default: None selector: diff --git a/homeassistant/components/iperf3/strings.json b/homeassistant/components/iperf3/strings.json new file mode 100644 index 00000000000000..be8535daec67cb --- /dev/null +++ b/homeassistant/components/iperf3/strings.json @@ -0,0 +1,14 @@ +{ + "services": { + "speedtest": { + "name": "Speedtest", + "description": "Immediately executes a speed test with iperf3.", + "fields": { + "host": { + "name": "Host", + "description": "The host name of the iperf3 server (already configured) to run a test with." + } + } + } + } +} diff --git a/homeassistant/components/isy994/services.yaml b/homeassistant/components/isy994/services.yaml index b84fcdd73ef342..7ce44f9edae3f6 100644 --- a/homeassistant/components/isy994/services.yaml +++ b/homeassistant/components/isy994/services.yaml @@ -4,52 +4,36 @@ # flooding the ISY with requests. To control multiple devices with a service call # the recommendation is to add a scene in the ISY and control that scene. send_raw_node_command: - name: Send raw node command - description: Send a "raw" ISY REST Device Command to a Node using its Home Assistant Entity ID. target: entity: integration: isy994 fields: command: - name: Command - description: The ISY REST Command to be sent to the device required: true example: "DON" selector: text: value: - name: Value - description: The integer value to be sent with the command. selector: number: min: 0 max: 255 parameters: - name: Parameters - description: A dict of parameters to be sent in the query string (e.g. for controlling colored bulbs). example: { GV2: 0, GV3: 0, GV4: 255 } default: {} selector: object: unit_of_measurement: - name: Unit of measurement - description: The ISY Unit of Measurement (UOM) to send with the command, if required. selector: number: min: 0 max: 120 send_node_command: - name: Send node command - description: >- - Send a command to an ISY Device using its Home Assistant entity ID. Valid commands are: beep, brighten, dim, disable, - enable, fade_down, fade_stop, fade_up, fast_off, fast_on, and query. target: entity: integration: isy994 fields: command: - name: Command - description: The command to be sent to the device. required: true selector: select: @@ -66,34 +50,22 @@ send_node_command: - "fast_on" - "query" get_zwave_parameter: - name: Get Z-Wave Parameter - description: >- - Request a Z-Wave Device parameter via the ISY. The parameter value will be returned as a entity extra state attribute with the name "ZW_#" - where "#" is the parameter number. target: entity: integration: isy994 fields: parameter: - name: Parameter - description: The parameter number to retrieve from the device. example: 8 selector: number: min: 1 max: 255 set_zwave_parameter: - name: Set Z-Wave Parameter - description: >- - Update a Z-Wave Device parameter via the ISY. The parameter value will also be returned as a entity extra state attribute with the name "ZW_#" - where "#" is the parameter number. target: entity: integration: isy994 fields: parameter: - name: Parameter - description: The parameter number to set on the end device. required: true example: 8 selector: @@ -101,15 +73,11 @@ set_zwave_parameter: min: 1 max: 255 value: - name: Value - description: The value to set for the parameter. May be an integer or byte string (e.g. "0xFFFF"). required: true example: 33491663 selector: text: size: - name: Size - description: The size of the parameter, either 1, 2, or 4 bytes. required: true example: 4 selector: @@ -119,17 +87,12 @@ set_zwave_parameter: - "2" - "4" set_zwave_lock_user_code: - name: Set Z-Wave Lock User Code - description: >- - Set a Z-Wave Lock User Code via the ISY. target: entity: integration: isy994 domain: lock fields: user_num: - name: User Number - description: The user slot number on the lock required: true example: 8 selector: @@ -137,8 +100,6 @@ set_zwave_lock_user_code: min: 1 max: 255 code: - name: Code - description: The code to set for the user. required: true example: 33491663 selector: @@ -147,17 +108,12 @@ set_zwave_lock_user_code: max: 99999999 mode: box delete_zwave_lock_user_code: - name: Delete Z-Wave Lock User Code - description: >- - Delete a Z-Wave Lock User Code via the ISY. target: entity: integration: isy994 domain: lock fields: user_num: - name: User Number - description: The user slot number on the lock required: true example: 8 selector: @@ -165,43 +121,26 @@ delete_zwave_lock_user_code: min: 1 max: 255 rename_node: - name: Rename Node on ISY - description: >- - Rename a node or group (scene) on the ISY. Note: this will not automatically change the Home Assistant Entity Name or Entity ID to match. - The entity name and ID will only be updated after calling `isy994.reload` or restarting Home Assistant, and ONLY IF you have not already customized the - name within Home Assistant. target: entity: integration: isy994 fields: name: - name: New Name - description: The new name to use within the ISY. required: true example: "Front Door Light" selector: text: send_program_command: - name: Send program command - description: >- - Send a command to control an ISY program or folder. Valid commands are run, run_then, run_else, stop, enable, disable, - enable_run_at_startup, and disable_run_at_startup. fields: address: - name: Address - description: The address of the program to control (use either address or name). example: "04B1" selector: text: name: - name: Name - description: The name of the program to control (use either address or name). example: "My Program" selector: text: command: - name: Command - description: The ISY Program Command to be sent. required: true selector: select: @@ -215,8 +154,6 @@ send_program_command: - "run_then" - "stop" isy: - name: ISY - description: If you have more than one ISY connected, provide the name of the ISY to query (as shown on the Device Registry or as the top-first node in the ISY Admin Console). If you have the same program name or address on multiple ISYs, omitting this will run the command on them all. example: "ISY" selector: text: diff --git a/homeassistant/components/isy994/strings.json b/homeassistant/components/isy994/strings.json index 821f8889978688..542df60f13fd60 100644 --- a/homeassistant/components/isy994/strings.json +++ b/homeassistant/components/isy994/strings.json @@ -36,7 +36,7 @@ "step": { "init": { "title": "ISY Options", - "description": "Set the options for the ISY Integration: \n • Node Sensor String: Any device or folder that contains 'Node Sensor String' in the name will be treated as a sensor or binary sensor. \n • Ignore String: Any device with 'Ignore String' in the name will be ignored. \n • Variable Sensor String: Any variable that contains 'Variable Sensor String' will be added as a sensor. \n • Restore Light Brightness: If enabled, the previous brightness will be restored when turning on a light instead of the device's built-in On-Level.", + "description": "Set the options for the ISY Integration: \n \u2022 Node Sensor String: Any device or folder that contains 'Node Sensor String' in the name will be treated as a sensor or binary sensor. \n \u2022 Ignore String: Any device with 'Ignore String' in the name will be ignored. \n \u2022 Variable Sensor String: Any variable that contains 'Variable Sensor String' will be added as a sensor. \n \u2022 Restore Light Brightness: If enabled, the previous brightness will be restored when turning on a light instead of the device's built-in On-Level.", "data": { "sensor_string": "Node Sensor String", "ignore_string": "Ignore String", @@ -53,5 +53,123 @@ "last_heartbeat": "Last Heartbeat Time", "websocket_status": "Event Socket Status" } + }, + "services": { + "send_raw_node_command": { + "name": "Send raw node command", + "description": "Set the options for the ISY Integration: \n \u2022 Node Sensor String: Any device or folder that contains 'Node Sensor String' in the name will be treated as a sensor or binary sensor. \n \u2022 Ignore String: Any device with 'Ignore String' in the name will be ignored. \n \u2022 Variable Sensor String: Any variable that contains 'Variable Sensor String' will be added as a sensor. \n \u2022 Restore Light Brightness: If enabled, the previous brightness will be restored when turning on a light instead of the device's built-in On-Level.", + "fields": { + "command": { + "name": "Command", + "description": "The ISY REST Command to be sent to the device." + }, + "value": { + "name": "Value", + "description": "The integer value to be sent with the command." + }, + "parameters": { + "name": "Parameters", + "description": "A dict of parameters to be sent in the query string (e.g. for controlling colored bulbs)." + }, + "unit_of_measurement": { + "name": "Unit of measurement", + "description": "The ISY Unit of Measurement (UOM) to send with the command, if required." + } + } + }, + "send_node_command": { + "name": "Send node command", + "description": "Sends a command to an ISY Device using its Home Assistant entity ID. Valid commands are: beep, brighten, dim, disable, enable, fade_down, fade_stop, fade_up, fast_off, fast_on, and query.", + "fields": { + "command": { + "name": "Command", + "description": "The command to be sent to the device." + } + } + }, + "get_zwave_parameter": { + "name": "Get Z-Wave Parameter", + "description": "Requests a Z-Wave Device parameter via the ISY. The parameter value will be returned as a entity extra state attribute with the name \"ZW_#\" where \"#\" is the parameter number.", + "fields": { + "parameter": { + "name": "Parameter", + "description": "The parameter number to retrieve from the device." + } + } + }, + "set_zwave_parameter": { + "name": "Set Z-Wave Parameter", + "description": "Updates a Z-Wave Device parameter via the ISY. The parameter value will also be returned as a entity extra state attribute with the name \"ZW_#\" where \"#\" is the parameter number.", + "fields": { + "parameter": { + "name": "Parameter", + "description": "The parameter number to set on the end device." + }, + "value": { + "name": "Value", + "description": "The value to set for the parameter. May be an integer or byte string (e.g. \"0xFFFF\")." + }, + "size": { + "name": "Size", + "description": "The size of the parameter, either 1, 2, or 4 bytes." + } + } + }, + "set_zwave_lock_user_code": { + "name": "Set Z-Wave Lock User Code", + "description": "Sets a Z-Wave Lock User Code via the ISY.", + "fields": { + "user_num": { + "name": "User Number", + "description": "The user slot number on the lock." + }, + "code": { + "name": "Code", + "description": "The code to set for the user." + } + } + }, + "delete_zwave_lock_user_code": { + "name": "Delete Z-Wave Lock User Code", + "description": "Delete a Z-Wave Lock User Code via the ISY.", + "fields": { + "user_num": { + "name": "User Number", + "description": "The user slot number on the lock." + } + } + }, + "rename_node": { + "name": "Rename Node on ISY", + "description": "Renames a node or group (scene) on the ISY. Note: this will not automatically change the Home Assistant Entity Name or Entity ID to match. The entity name and ID will only be updated after calling `isy994.reload` or restarting Home Assistant, and ONLY IF you have not already customized the name within Home Assistant.", + "fields": { + "name": { + "name": "New Name", + "description": "The new name to use within the ISY." + } + } + }, + "send_program_command": { + "name": "Send program command", + "description": "Sends a command to control an ISY program or folder. Valid commands are run, run_then, run_else, stop, enable, disable, enable_run_at_startup, and disable_run_at_startup.", + "fields": { + "address": { + "name": "Address", + "description": "The address of the program to control (use either address or name)." + }, + "name": { + "name": "Name", + "description": "The name of the program to control (use either address or name)." + }, + "command": { + "name": "Command", + "description": "The ISY Program Command to be sent." + }, + "isy": { + "name": "ISY", + "description": "If you have more than one ISY connected, provide the name of the ISY to query (as shown on the Device Registry or as the top-first node in the ISY Admin Console). If you have the same program name or address on multiple ISYs, omitting this will run the command on them all." + } + } + } } } diff --git a/homeassistant/components/izone/services.yaml b/homeassistant/components/izone/services.yaml index 5cecbb68a9f903..f1a8fe5c8e5342 100644 --- a/homeassistant/components/izone/services.yaml +++ b/homeassistant/components/izone/services.yaml @@ -1,14 +1,10 @@ airflow_min: - name: Set minimum airflow - description: Set the airflow minimum percent for a zone target: entity: integration: izone domain: climate fields: airflow: - name: Percent - description: Airflow percent. required: true selector: number: @@ -17,16 +13,12 @@ airflow_min: step: 5 unit_of_measurement: "%" airflow_max: - name: Set maximum airflow - description: Set the airflow maximum percent for a zone target: entity: integration: izone domain: climate fields: airflow: - name: Percent - description: Airflow percent. required: true selector: number: diff --git a/homeassistant/components/izone/strings.json b/homeassistant/components/izone/strings.json index 7d1e8f1d4768ab..3906dcb89fe4fb 100644 --- a/homeassistant/components/izone/strings.json +++ b/homeassistant/components/izone/strings.json @@ -9,5 +9,27 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } + }, + "services": { + "airflow_min": { + "name": "Set minimum airflow", + "description": "Sets the airflow minimum percent for a zone.", + "fields": { + "airflow": { + "name": "Percent", + "description": "Airflow percent." + } + } + }, + "airflow_max": { + "name": "Set maximum airflow", + "description": "Sets the airflow maximum percent for a zone.", + "fields": { + "airflow": { + "name": "Percent", + "description": "Airflow percent." + } + } + } } } diff --git a/homeassistant/components/keba/services.yaml b/homeassistant/components/keba/services.yaml index 8e5e8cd91f88cd..daa1749a34cafa 100644 --- a/homeassistant/components/keba/services.yaml +++ b/homeassistant/components/keba/services.yaml @@ -1,28 +1,11 @@ # Describes the format for available services for KEBA charging staitons request_data: - name: Request data - description: > - Request new data from the charging station. - authorize: - name: Authorize - description: > - Authorizes a charging process with the predefined RFID tag of the configuration file. - deauthorize: - name: Deauthorize - description: > - Deauthorizes the running charging process with the predefined RFID tag of the configuration file. - set_energy: - name: Set energy - description: Sets the energy target after which the charging process stops. fields: energy: - name: Energy - description: > - The energy target to stop charging. Setting 0 disables the limit. selector: number: min: 0 @@ -31,15 +14,8 @@ set_energy: unit_of_measurement: "kWh" set_current: - name: Set current - description: Sets the maximum current for charging processes. fields: current: - name: Current - description: > - The maximum current used for the charging process. - The value is depending on the DIP-switch settings and the used cable of the - charging station. default: 6 selector: number: @@ -49,24 +25,10 @@ set_current: unit_of_measurement: "A" enable: - name: Enable - description: > - Starts a charging process if charging station is authorized. - disable: - name: Disable - description: > - Stops the charging process if charging station is authorized. - set_failsafe: - name: Set failsafe - description: > - Set the failsafe mode of the charging station. If all parameters are 0, the failsafe mode will be disabled. fields: failsafe_timeout: - name: Failsafe timeout - description: > - Timeout after which the failsafe mode is triggered, if set_current was not executed during this time. default: 30 selector: number: @@ -74,9 +36,6 @@ set_failsafe: max: 3600 unit_of_measurement: seconds failsafe_fallback: - name: Failsafe fallback - description: > - Fallback current to be set after timeout. default: 6 selector: number: @@ -85,9 +44,6 @@ set_failsafe: step: 0.1 unit_of_measurement: "A" failsafe_persist: - name: Failsafe persist - description: > - If failsafe_persist is 0, the failsafe option is only until charging station reboot. If failsafe_persist is 1, the failsafe option will survive a reboot. default: 0 selector: number: diff --git a/homeassistant/components/keba/strings.json b/homeassistant/components/keba/strings.json new file mode 100644 index 00000000000000..140ab6ea9490ce --- /dev/null +++ b/homeassistant/components/keba/strings.json @@ -0,0 +1,62 @@ +{ + "services": { + "request_data": { + "name": "Request data", + "description": "Requesta new data from the charging station." + }, + "authorize": { + "name": "Authorize", + "description": "Authorizes a charging process with the predefined RFID tag of the configuration file." + }, + "deauthorize": { + "name": "Deauthorize", + "description": "Deauthorizes the running charging process with the predefined RFID tag of the configuration file." + }, + "set_energy": { + "name": "Set energy", + "description": "Sets the energy target after which the charging process stops.", + "fields": { + "energy": { + "name": "Energy", + "description": "The energy target to stop charging. Setting 0 disables the limit." + } + } + }, + "set_current": { + "name": "Set current", + "description": "Sets the maximum current for charging processes.", + "fields": { + "current": { + "name": "Current", + "description": "The maximum current used for the charging process. The value is depending on the DIP-switch settings and the used cable of the charging station." + } + } + }, + "enable": { + "name": "Enable", + "description": "Starts a charging process if charging station is authorized." + }, + "disable": { + "name": "Disable", + "description": "Stops the charging process if charging station is authorized." + }, + "set_failsafe": { + "name": "Set failsafe", + "description": "Sets the failsafe mode of the charging station. If all parameters are 0, the failsafe mode will be disabled.", + "fields": { + "failsafe_timeout": { + "name": "Failsafe timeout", + "description": "Timeout after which the failsafe mode is triggered, if set_current was not executed during this time." + }, + "failsafe_fallback": { + "name": "Failsafe fallback", + "description": "Fallback current to be set after timeout." + }, + "failsafe_persist": { + "name": "Failsafe persist", + "description": "If failsafe_persist is 0, the failsafe option is only until charging station reboot. If failsafe_persist is 1, the failsafe option will survive a reboot." + } + } + } + } +} diff --git a/homeassistant/components/kef/services.yaml b/homeassistant/components/kef/services.yaml index cf364edcf217a5..9c5e5083794131 100644 --- a/homeassistant/components/kef/services.yaml +++ b/homeassistant/components/kef/services.yaml @@ -1,14 +1,10 @@ update_dsp: - name: Update DSP - description: Update all DSP settings. target: entity: integration: kef domain: media_player set_mode: - name: Set mode - description: Set the mode of the speaker. target: entity: integration: kef @@ -16,36 +12,24 @@ set_mode: fields: desk_mode: - name: Desk mode - description: Desk mode. selector: boolean: wall_mode: - name: Wall mode - description: Wall mode. selector: boolean: phase_correction: - name: Phase correction - description: Phase correction. selector: boolean: high_pass: - name: High pass - description: High-pass mode". selector: boolean: sub_polarity: - name: Subwoofer polarity - description: Sub polarity. selector: select: options: - "-" - "+" bass_extension: - name: Base extension - description: Bass extension. selector: select: options: @@ -54,16 +38,12 @@ set_mode: - "Extra" set_desk_db: - name: Set desk dB - description: Set the "Desk mode" slider of the speaker in dB. target: entity: integration: kef domain: media_player fields: db_value: - name: dB value - description: Value of the slider example: 0.0 selector: number: @@ -73,16 +53,12 @@ set_desk_db: unit_of_measurement: dB set_wall_db: - name: Set wall dB - description: Set the "Wall mode" slider of the speaker in dB. target: entity: integration: kef domain: media_player fields: db_value: - name: dB value - description: Value of the slider. selector: number: min: -6 @@ -91,16 +67,12 @@ set_wall_db: unit_of_measurement: dB set_treble_db: - name: Set treble dB - description: Set desk the "Treble trim" slider of the speaker in dB. target: entity: integration: kef domain: media_player fields: db_value: - name: dB value - description: Value of the slider. selector: number: min: -2 @@ -109,16 +81,12 @@ set_treble_db: unit_of_measurement: dB set_high_hz: - name: Set high hertz - description: Set the "High-pass mode" slider of the speaker in Hz. target: entity: integration: kef domain: media_player fields: hz_value: - name: Hertz value - description: Value of the slider. selector: number: min: 50 @@ -127,16 +95,12 @@ set_high_hz: unit_of_measurement: Hz set_low_hz: - name: Set low Hertz - description: Set the "Sub out low-pass frequency" slider of the speaker in Hz. target: entity: integration: kef domain: media_player fields: hz_value: - name: Hertz value - description: Value of the slider. selector: number: min: 40 @@ -145,16 +109,12 @@ set_low_hz: unit_of_measurement: Hz set_sub_db: - name: Set subwoofer dB - description: Set the "Sub gain" slider of the speaker in dB. target: entity: integration: kef domain: media_player fields: db_value: - name: dB value - description: Value of the slider. selector: number: min: -10 diff --git a/homeassistant/components/kef/strings.json b/homeassistant/components/kef/strings.json new file mode 100644 index 00000000000000..7307caa6bb328f --- /dev/null +++ b/homeassistant/components/kef/strings.json @@ -0,0 +1,98 @@ +{ + "services": { + "update_dsp": { + "name": "Update DSP", + "description": "Updates all DSP settings." + }, + "set_mode": { + "name": "Set mode", + "description": "Sets the mode of the speaker.", + "fields": { + "desk_mode": { + "name": "Desk mode", + "description": "Desk mode." + }, + "wall_mode": { + "name": "Wall mode", + "description": "Wall mode." + }, + "phase_correction": { + "name": "Phase correction", + "description": "Phase correction." + }, + "high_pass": { + "name": "High pass", + "description": "High-pass mode\"." + }, + "sub_polarity": { + "name": "Subwoofer polarity", + "description": "Sub polarity." + }, + "bass_extension": { + "name": "Base extension", + "description": "Bass extension." + } + } + }, + "set_desk_db": { + "name": "Set desk dB", + "description": "Sets the \"Desk mode\" slider of the speaker in dB.", + "fields": { + "db_value": { + "name": "DB value", + "description": "Value of the slider." + } + } + }, + "set_wall_db": { + "name": "Set wall dB", + "description": "Sets the \"Wall mode\" slider of the speaker in dB.", + "fields": { + "db_value": { + "name": "DB value", + "description": "Value of the slider." + } + } + }, + "set_treble_db": { + "name": "Set treble dB", + "description": "Sets desk the \"Treble trim\" slider of the speaker in dB.", + "fields": { + "db_value": { + "name": "DB value", + "description": "Value of the slider." + } + } + }, + "set_high_hz": { + "name": "Set high hertz", + "description": "Sets the \"High-pass mode\" slider of the speaker in Hz.", + "fields": { + "hz_value": { + "name": "Hertz value", + "description": "Value of the slider." + } + } + }, + "set_low_hz": { + "name": "Sets low Hertz", + "description": "Set the \"Sub out low-pass frequency\" slider of the speaker in Hz.", + "fields": { + "hz_value": { + "name": "Hertz value", + "description": "Value of the slider." + } + } + }, + "set_sub_db": { + "name": "Sets subwoofer dB", + "description": "Set the \"Sub gain\" slider of the speaker in dB.", + "fields": { + "db_value": { + "name": "DB value", + "description": "Value of the slider." + } + } + } + } +} diff --git a/homeassistant/components/keyboard/services.yaml b/homeassistant/components/keyboard/services.yaml index 07f02959c395a1..b236f8eb80eb20 100644 --- a/homeassistant/components/keyboard/services.yaml +++ b/homeassistant/components/keyboard/services.yaml @@ -1,35 +1,6 @@ volume_up: - name: Volume up - description: - Simulates a key press of the "Volume Up" button on Home Assistant's host - machine - volume_down: - name: Volume down - description: - Simulates a key press of the "Volume Down" button on Home Assistant's host - machine - volume_mute: - name: Volume mute - description: - Simulates a key press of the "Volume Mute" button on Home Assistant's host - machine - media_play_pause: - name: Media play/pause - description: - Simulates a key press of the "Media Play/Pause" button on Home Assistant's - host machine - media_next_track: - name: Media next track - description: - Simulates a key press of the "Media Next Track" button on Home Assistant's - host machine - media_prev_track: - name: Media previous track - description: - Simulates a key press of the "Media Previous Track" button on Home - Assistant's host machine diff --git a/homeassistant/components/keyboard/strings.json b/homeassistant/components/keyboard/strings.json new file mode 100644 index 00000000000000..1b744cb7a71e0c --- /dev/null +++ b/homeassistant/components/keyboard/strings.json @@ -0,0 +1,28 @@ +{ + "services": { + "volume_up": { + "name": "Volume up", + "description": "Simulates a key press of the \"Volume Up\" button on Home Assistant's host machine." + }, + "volume_down": { + "name": "Volume down", + "description": "Simulates a key press of the \"Volume Down\" button on Home Assistant's host machine." + }, + "volume_mute": { + "name": "Volume mute", + "description": "Simulates a key press of the \"Volume Mute\" button on Home Assistant's host machine." + }, + "media_play_pause": { + "name": "Media play/pause", + "description": "Simulates a key press of the \"Media Play/Pause\" button on Home Assistant's host machine." + }, + "media_next_track": { + "name": "Media next track", + "description": "Simulates a key press of the \"Media Next Track\" button on Home Assistant's host machine." + }, + "media_prev_track": { + "name": "Media previous track", + "description": "Simulates a key press of the \"Media Previous Track\" button on Home Assistant's host machine." + } + } +} diff --git a/homeassistant/components/keymitt_ble/services.yaml b/homeassistant/components/keymitt_ble/services.yaml index c611577eb26d91..2be5c07c804c0e 100644 --- a/homeassistant/components/keymitt_ble/services.yaml +++ b/homeassistant/components/keymitt_ble/services.yaml @@ -1,17 +1,11 @@ calibrate: - name: Calibrate - description: Calibration - Set depth, press & hold duration, and operation mode. Warning - this will send a push command to the device fields: entity_id: - name: Entity - description: Name of entity to calibrate selector: entity: integration: keymitt_ble domain: switch depth: - name: Depth - description: Depth in percent example: 50 required: true selector: @@ -22,8 +16,6 @@ calibrate: max: 100 unit_of_measurement: "%" duration: - name: Duration - description: Duration in seconds example: 1 required: true selector: @@ -34,8 +26,6 @@ calibrate: max: 60 unit_of_measurement: seconds mode: - name: Mode - description: normal | invert | toggle example: "normal" required: true selector: diff --git a/homeassistant/components/keymitt_ble/strings.json b/homeassistant/components/keymitt_ble/strings.json index fd8e1f4825d294..57e7fc68582c8e 100644 --- a/homeassistant/components/keymitt_ble/strings.json +++ b/homeassistant/components/keymitt_ble/strings.json @@ -23,5 +23,29 @@ "unknown": "[%key:common::config_flow::error::unknown%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } + }, + "services": { + "calibrate": { + "name": "Calibrate", + "description": "Calibration - Set depth, press & hold duration, and operation mode. Warning - this will send a push command to the device.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Name of entity to calibrate." + }, + "depth": { + "name": "Depth", + "description": "Depth in percent." + }, + "duration": { + "name": "Duration", + "description": "Duration in seconds." + }, + "mode": { + "name": "Mode", + "description": "Normal | invert | toggle." + } + } + } } } diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index 0ad497a30a261c..813bf758eb0b04 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -1,111 +1,73 @@ send: - name: "Send to KNX bus" - description: "Send arbitrary data directly to the KNX bus." fields: address: - name: "Group address" - description: "Group address(es) to write to. Lists will send to multiple group addresses successively." required: true example: "1/1/0" selector: object: payload: - name: "Payload" - description: "Payload to send to the bus. Integers are treated as DPT 1/2/3 payloads. For DPTs > 6 bits send a list. Each value represents 1 octet (0-255). Pad with 0 to DPT byte length." required: true example: "[0, 4]" selector: object: type: - name: "Value type" - description: "If set, the payload will not be sent as raw bytes, but encoded as given DPT. KNX sensor types are valid values (see https://www.home-assistant.io/integrations/knx/#value-types)." required: false example: "temperature" selector: text: response: - name: "Send as Response" - description: "If set to `True`, the telegram will be sent as a `GroupValueResponse` instead of a `GroupValueWrite`." default: false selector: boolean: read: - name: "Read from KNX bus" - description: "Send GroupValueRead requests to the KNX bus. Response can be used from `knx_event` and will be processed in KNX entities." fields: address: - name: "Group address" - description: "Group address(es) to send read request to. Lists will read multiple group addresses." required: true example: "1/1/0" selector: object: event_register: - name: "Register knx_event" - description: "Add or remove group addresses to knx_event filter for triggering `knx_event`s. Only addresses added with this service can be removed." fields: address: - name: "Group address" - description: "Group address(es) that shall be added or removed. Lists are allowed." required: true example: "1/1/0" selector: object: type: - name: "Value type" - description: "If set, the payload will be decoded as given DPT in the event data `value` key. KNX sensor types are valid values (see https://www.home-assistant.io/integrations/knx/#value-types)." required: false example: "2byte_float" selector: text: remove: - name: "Remove event registration" - description: "If `True` the group address(es) will be removed." default: false selector: boolean: exposure_register: - name: "Expose to KNX bus" - description: "Add or remove exposures to KNX bus. Only exposures added with this service can be removed." fields: address: - name: "Group address" - description: "Group address state or attribute updates will be sent to. GroupValueRead requests will be answered. Per address only one exposure can be registered." required: true example: "1/1/0" selector: text: type: - name: "Value type" - description: "Telegrams will be encoded as given DPT. 'binary' and all KNX sensor types are valid values (see https://www.home-assistant.io/integrations/knx/#value-types)" required: true example: "percentU8" selector: text: entity_id: - name: "Entity" - description: "Entity id whose state or attribute shall be exposed." required: true selector: entity: attribute: - name: "Entity attribute" - description: "Attribute of the entity that shall be sent to the KNX bus. If not set the state will be sent. Eg. for a light the state is eigther “on” or “off” - with attribute you can expose its “brightness”." example: "brightness" selector: text: default: - name: "Default value" - description: "Default value to send to the bus if the state or attribute value is None. Eg. a light with state “off” has no brightness attribute so a default value of 0 could be used. If not set (or None) no value would be sent to the bus and a GroupReadRequest to the address would return the last known value." example: "0" selector: object: remove: - name: "Remove exposure" - description: "If `True` the exposure will be removed. Only `address` is required for removal." default: false selector: boolean: reload: - name: Reload - description: Reload the KNX integration. diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index cdd61379567d55..9a17fed506c652 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -288,5 +288,91 @@ "trigger_type": { "telegram": "Telegram sent or received" } + }, + "services": { + "send": { + "name": "Send to KNX bus", + "description": "Sends arbitrary data directly to the KNX bus.", + "fields": { + "address": { + "name": "Group address", + "description": "Group address(es) to write to. Lists will send to multiple group addresses successively." + }, + "payload": { + "name": "Payload", + "description": "Payload to send to the bus. Integers are treated as DPT 1/2/3 payloads. For DPTs > 6 bits send a list. Each value represents 1 octet (0-255). Pad with 0 to DPT byte length." + }, + "type": { + "name": "Value type", + "description": "If set, the payload will not be sent as raw bytes, but encoded as given DPT. KNX sensor types are valid values (see https://www.home-assistant.io/integrations/knx/#value-types)." + }, + "response": { + "name": "Send as Response", + "description": "If set to `True`, the telegram will be sent as a `GroupValueResponse` instead of a `GroupValueWrite`." + } + } + }, + "read": { + "name": "Reads from KNX bus", + "description": "Send GroupValueRead requests to the KNX bus. Response can be used from `knx_event` and will be processed in KNX entities.", + "fields": { + "address": { + "name": "Group address", + "description": "Group address(es) to send read request to. Lists will read multiple group addresses." + } + } + }, + "event_register": { + "name": "Registers knx_event", + "description": "Add or remove group addresses to knx_event filter for triggering `knx_event`s. Only addresses added with this service can be removed.", + "fields": { + "address": { + "name": "Group address", + "description": "Group address(es) that shall be added or removed. Lists are allowed." + }, + "type": { + "name": "Value type", + "description": "If set, the payload will be decoded as given DPT in the event data `value` key. KNX sensor types are valid values (see https://www.home-assistant.io/integrations/knx/#value-types)." + }, + "remove": { + "name": "Remove event registration", + "description": "If `True` the group address(es) will be removed." + } + } + }, + "exposure_register": { + "name": "Expose to KNX bus", + "description": "Adds or remove exposures to KNX bus. Only exposures added with this service can be removed.", + "fields": { + "address": { + "name": "Group address", + "description": "Group address state or attribute updates will be sent to. GroupValueRead requests will be answered. Per address only one exposure can be registered." + }, + "type": { + "name": "Value type", + "description": "Telegrams will be encoded as given DPT. 'binary' and all KNX sensor types are valid values (see https://www.home-assistant.io/integrations/knx/#value-types)." + }, + "entity_id": { + "name": "Entity", + "description": "Entity id whose state or attribute shall be exposed." + }, + "attribute": { + "name": "Entity attribute", + "description": "Attribute of the entity that shall be sent to the KNX bus. If not set the state will be sent. Eg. for a light the state is eigther \u201con\u201d or \u201coff\u201d - with attribute you can expose its \u201cbrightness\u201d." + }, + "default": { + "name": "Default value", + "description": "Default value to send to the bus if the state or attribute value is None. Eg. a light with state \u201coff\u201d has no brightness attribute so a default value of 0 could be used. If not set (or None) no value would be sent to the bus and a GroupReadRequest to the address would return the last known value." + }, + "remove": { + "name": "Remove exposure", + "description": "If `True` the exposure will be removed. Only `address` is required for removal." + } + } + }, + "reload": { + "name": "Reload", + "description": "Reloads the KNX integration." + } } } diff --git a/homeassistant/components/kodi/services.yaml b/homeassistant/components/kodi/services.yaml index cf6cdfc240db07..76ed0aca22d60e 100644 --- a/homeassistant/components/kodi/services.yaml +++ b/homeassistant/components/kodi/services.yaml @@ -1,50 +1,36 @@ # Describes the format for available Kodi services add_to_playlist: - name: Add to playlist - description: Add music to the default playlist (i.e. playlistid=0). target: entity: integration: kodi domain: media_player fields: media_type: - name: Media type - description: Media type identifier. It must be one of SONG or ALBUM. required: true example: ALBUM selector: text: media_id: - name: Media ID - description: Unique Id of the media entry to add (`songid` or albumid`). If not defined, `media_name` and `artist_name` are needed to search the Kodi music library. example: 123456 selector: text: media_name: - name: Media Name - description: Optional media name for filtering media. Can be 'ALL' when `media_type` is 'ALBUM' and `artist_name` is specified, to add all songs from one artist. example: "Highway to Hell" selector: text: artist_name: - name: Artist name - description: Optional artist name for filtering media. example: "AC/DC" selector: text: call_method: - name: Call method - description: "Call a Kodi JSONRPC API method with optional parameters. Results of the Kodi API call will be redirected in a Home Assistant event: `kodi_call_method_result`." target: entity: integration: kodi domain: media_player fields: method: - name: Method - description: Name of the Kodi JSONRPC API method to be called. required: true example: "VideoLibrary.GetRecentlyAddedEpisodes" selector: diff --git a/homeassistant/components/kodi/strings.json b/homeassistant/components/kodi/strings.json index 8097eb6336b75d..f7ee375f9902f0 100644 --- a/homeassistant/components/kodi/strings.json +++ b/homeassistant/components/kodi/strings.json @@ -46,5 +46,39 @@ "turn_on": "[%key:common::device_automation::action_type::turn_on%]", "turn_off": "[%key:common::device_automation::action_type::turn_off%]" } + }, + "services": { + "add_to_playlist": { + "name": "Add to playlist", + "description": "Adds music to the default playlist (i.e. playlistid=0).", + "fields": { + "media_type": { + "name": "Media type", + "description": "Media type identifier. It must be one of SONG or ALBUM." + }, + "media_id": { + "name": "Media ID", + "description": "Unique Id of the media entry to add (`songid` or albumid`). If not defined, `media_name` and `artist_name` are needed to search the Kodi music library." + }, + "media_name": { + "name": "Media name", + "description": "Optional media name for filtering media. Can be 'ALL' when `media_type` is 'ALBUM' and `artist_name` is specified, to add all songs from one artist." + }, + "artist_name": { + "name": "Artist name", + "description": "Optional artist name for filtering media." + } + } + }, + "call_method": { + "name": "Call method", + "description": "Calls a Kodi JSONRPC API method with optional parameters. Results of the Kodi API call will be redirected in a Home Assistant event: `kodi_call_method_result`.", + "fields": { + "method": { + "name": "Method", + "description": "Name of the Kodi JSONRPC API method to be called." + } + } + } } } From e8c292185289806efe5036c2771bf225bd58662f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 12 Jul 2023 16:40:03 +0200 Subject: [PATCH 0425/1009] 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 22a52e61b63aa1..94f445f1ec10d3 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 e513b7d0ebb3927ca66297661c22fb15e75fa599 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 12 Jul 2023 16:58:35 +0200 Subject: [PATCH 0426/1009] Add condition selector for blueprint (#96350) * Add condition selector for blueprint * Add tests and validation * Update comment --- homeassistant/helpers/selector.py | 21 +++++++++++++++++++++ tests/helpers/test_selector.py | 26 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index abd4d2e623ee12..c996fcaf524f6e 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -453,6 +453,27 @@ def __call__(self, data: Any) -> int: return value +class ConditionSelectorConfig(TypedDict): + """Class to represent an action selector config.""" + + +@SELECTORS.register("condition") +class ConditionSelector(Selector[ConditionSelectorConfig]): + """Selector of an condition sequence (script syntax).""" + + selector_type = "condition" + + CONFIG_SCHEMA = vol.Schema({}) + + def __init__(self, config: ConditionSelectorConfig | None = None) -> None: + """Instantiate a selector.""" + super().__init__(config) + + def __call__(self, data: Any) -> Any: + """Validate the passed selection.""" + return vol.Schema(cv.CONDITIONS_SCHEMA)(data) + + class ConfigEntrySelectorConfig(TypedDict, total=False): """Class to represent a config entry selector config.""" diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index fd2dba4b08459e..09cf79116a0af2 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -1017,3 +1017,29 @@ def test_conversation_agent_selector_schema( ) -> None: """Test conversation agent selector.""" _test_selector("conversation_agent", schema, valid_selections, invalid_selections) + + +@pytest.mark.parametrize( + ("schema", "valid_selections", "invalid_selections"), + ( + ( + {}, + ( + [ + { + "condition": "numeric_state", + "entity_id": ["sensor.temperature"], + "below": 20, + } + ], + [], + ), + ("abc"), + ), + ), +) +def test_condition_selector_schema( + schema, valid_selections, invalid_selections +) -> None: + """Test condition sequence selector.""" + _test_selector("condition", schema, valid_selections, invalid_selections) From c5cd7e58979d32d602d7fa91762262ad771b2257 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 16:59:45 +0200 Subject: [PATCH 0427/1009] Migrate update services to support translations (#96395) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/update/services.yaml | 10 -------- homeassistant/components/update/strings.json | 24 +++++++++++++++++++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/update/services.yaml b/homeassistant/components/update/services.yaml index 9b16dbd2713147..036af10150a0a2 100644 --- a/homeassistant/components/update/services.yaml +++ b/homeassistant/components/update/services.yaml @@ -1,34 +1,24 @@ install: - name: Install update - description: Install an update for this device or service target: entity: domain: update fields: version: - name: Version - description: Version to install, if omitted, the latest version will be installed. required: false example: "1.0.0" selector: text: backup: - name: Backup - description: Backup before installing the update, if supported by the integration. required: false selector: boolean: skip: - name: Skip update - description: Mark currently available update as skipped. target: entity: domain: update clear_skipped: - name: Clear skipped update - description: Removes the skipped version marker from an update. target: entity: domain: update diff --git a/homeassistant/components/update/strings.json b/homeassistant/components/update/strings.json index b69e0acf65e5f2..1d238d3dd513d7 100644 --- a/homeassistant/components/update/strings.json +++ b/homeassistant/components/update/strings.json @@ -14,5 +14,29 @@ "firmware": { "name": "Firmware" } + }, + "services": { + "install": { + "name": "Install update", + "description": "Installs an update for this device or service.", + "fields": { + "version": { + "name": "Version", + "description": "The version to install. If omitted, the latest version will be installed." + }, + "backup": { + "name": "Backup", + "description": "If supported by the integration, this creates a backup before starting the update ." + } + } + }, + "skip": { + "name": "Skip update", + "description": "Marks currently available update as skipped." + }, + "clear_skipped": { + "name": "Clear skipped update", + "description": "Removes the skipped version marker from an update." + } } } From b39660df3b40ba05ea8329220d257917ac29ae49 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 17:04:22 +0200 Subject: [PATCH 0428/1009] Migrate lovelace services to support translations (#96340) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/lovelace/services.yaml | 2 -- homeassistant/components/lovelace/strings.json | 6 ++++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lovelace/services.yaml b/homeassistant/components/lovelace/services.yaml index f9fc5999da6d35..7cf6d8e40277ae 100644 --- a/homeassistant/components/lovelace/services.yaml +++ b/homeassistant/components/lovelace/services.yaml @@ -1,5 +1,3 @@ # Describes the format for available lovelace services reload_resources: - name: Reload resources - description: Reload Lovelace resources from YAML configuration diff --git a/homeassistant/components/lovelace/strings.json b/homeassistant/components/lovelace/strings.json index 87f8407d93c9e2..64718308325786 100644 --- a/homeassistant/components/lovelace/strings.json +++ b/homeassistant/components/lovelace/strings.json @@ -6,5 +6,11 @@ "resources": "Resources", "views": "Views" } + }, + "services": { + "reload_resources": { + "name": "Reload resources", + "description": "Reloads dashboard resources from the YAML-configuration." + } } } From d6771e6f8a928032422279cd1954fb420c80b092 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 17:12:22 +0200 Subject: [PATCH 0429/1009] Migrate input helpers services to support translations (#96392) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- .../components/input_boolean/services.yaml | 8 --- .../components/input_boolean/strings.json | 18 +++++++ .../components/input_button/services.yaml | 2 - .../components/input_button/strings.json | 6 +++ .../components/input_datetime/services.yaml | 14 ----- .../components/input_datetime/strings.json | 28 ++++++++++ .../components/input_number/services.yaml | 10 ---- .../components/input_number/strings.json | 24 +++++++++ .../components/input_select/services.yaml | 22 -------- .../components/input_select/strings.json | 54 +++++++++++++++++++ .../components/input_text/services.yaml | 6 --- .../components/input_text/strings.json | 16 ++++++ 12 files changed, 146 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/input_boolean/services.yaml b/homeassistant/components/input_boolean/services.yaml index d294d61fd4d3e8..9de0368ba3598c 100644 --- a/homeassistant/components/input_boolean/services.yaml +++ b/homeassistant/components/input_boolean/services.yaml @@ -1,24 +1,16 @@ toggle: - name: Toggle - description: Toggle an input boolean target: entity: domain: input_boolean turn_off: - name: Turn off - description: Turn off an input boolean target: entity: domain: input_boolean turn_on: - name: Turn on - description: Turn on an input boolean target: entity: domain: input_boolean reload: - name: Reload - description: Reload the input_boolean configuration diff --git a/homeassistant/components/input_boolean/strings.json b/homeassistant/components/input_boolean/strings.json index d8e1e133f55a53..9288de04f2c1f5 100644 --- a/homeassistant/components/input_boolean/strings.json +++ b/homeassistant/components/input_boolean/strings.json @@ -17,5 +17,23 @@ } } } + }, + "services": { + "toggle": { + "name": "Toggle", + "description": "Toggles the helper on/off." + }, + "turn_off": { + "name": "Turn off", + "description": "Turns off the helper." + }, + "turn_on": { + "name": "Turn on", + "description": "Turns on the helper." + }, + "reload": { + "name": "Reload", + "description": "Reloads helpers from the YAML-configuration." + } } } diff --git a/homeassistant/components/input_button/services.yaml b/homeassistant/components/input_button/services.yaml index 899ead91cb56bc..7c57fcff272155 100644 --- a/homeassistant/components/input_button/services.yaml +++ b/homeassistant/components/input_button/services.yaml @@ -1,6 +1,4 @@ press: - name: Press - description: Press the input button entity. target: entity: domain: input_button diff --git a/homeassistant/components/input_button/strings.json b/homeassistant/components/input_button/strings.json index cfd616fd5e73d0..b51d04926f5d2d 100644 --- a/homeassistant/components/input_button/strings.json +++ b/homeassistant/components/input_button/strings.json @@ -13,5 +13,11 @@ } } } + }, + "services": { + "press": { + "name": "Press", + "description": "Mimics the physical button press on the device." + } } } diff --git a/homeassistant/components/input_datetime/services.yaml b/homeassistant/components/input_datetime/services.yaml index 51b1d6b00c14ae..386f0096a5f715 100644 --- a/homeassistant/components/input_datetime/services.yaml +++ b/homeassistant/components/input_datetime/services.yaml @@ -1,33 +1,21 @@ set_datetime: - name: Set - description: This can be used to dynamically set the date and/or time. target: entity: domain: input_datetime fields: date: - name: Date - description: The target date the entity should be set to. example: '"2019-04-20"' selector: text: time: - name: Time - description: The target time the entity should be set to. example: '"05:04:20"' selector: time: datetime: - name: Date & Time - description: The target date & time the entity should be set to. example: '"2019-04-20 05:04:20"' selector: text: timestamp: - name: Timestamp - description: - The target date & time the entity should be set to as expressed by a - UNIX timestamp. selector: number: min: 0 @@ -35,5 +23,3 @@ set_datetime: mode: box reload: - name: Reload - description: Reload the input_datetime configuration. diff --git a/homeassistant/components/input_datetime/strings.json b/homeassistant/components/input_datetime/strings.json index 0c3a4b0b0d2fde..f657508a4c4688 100644 --- a/homeassistant/components/input_datetime/strings.json +++ b/homeassistant/components/input_datetime/strings.json @@ -34,5 +34,33 @@ } } } + }, + "services": { + "set_datetime": { + "name": "Set", + "description": "Sets the date and/or time.", + "fields": { + "date": { + "name": "Date", + "description": "The target date." + }, + "time": { + "name": "Time", + "description": "The target time." + }, + "datetime": { + "name": "Date & time", + "description": "The target date & time." + }, + "timestamp": { + "name": "Timestamp", + "description": "The target date & time, expressed by a UNIX timestamp." + } + } + }, + "reload": { + "name": "Reload", + "description": "Reloads helpers from the YAML-configuration." + } } } diff --git a/homeassistant/components/input_number/services.yaml b/homeassistant/components/input_number/services.yaml index 41164a7ccf52f2..e5de48a1262479 100644 --- a/homeassistant/components/input_number/services.yaml +++ b/homeassistant/components/input_number/services.yaml @@ -1,27 +1,19 @@ decrement: - name: Decrement - description: Decrement the value of an input number entity by its stepping. target: entity: domain: input_number increment: - name: Increment - description: Increment the value of an input number entity by its stepping. target: entity: domain: input_number set_value: - name: Set - description: Set the value of an input number entity. target: entity: domain: input_number fields: value: - name: Value - description: The target value the entity should be set to. required: true selector: number: @@ -31,5 +23,3 @@ set_value: mode: box reload: - name: Reload - description: Reload the input_number configuration. diff --git a/homeassistant/components/input_number/strings.json b/homeassistant/components/input_number/strings.json index 11ed2f8bf10d7c..020544c5d4eaac 100644 --- a/homeassistant/components/input_number/strings.json +++ b/homeassistant/components/input_number/strings.json @@ -33,5 +33,29 @@ } } } + }, + "services": { + "decrement": { + "name": "Decrement", + "description": "Decrements the current value by 1 step." + }, + "increment": { + "name": "Increment", + "description": "Increments the value by 1 step." + }, + "set_value": { + "name": "Set", + "description": "Sets the value.", + "fields": { + "value": { + "name": "Value", + "description": "The target value." + } + } + }, + "reload": { + "name": "Reload", + "description": "Reloads helpers from the YAML-configuration." + } } } diff --git a/homeassistant/components/input_select/services.yaml b/homeassistant/components/input_select/services.yaml index 8b8828eaa92c3f..92279e58a5404c 100644 --- a/homeassistant/components/input_select/services.yaml +++ b/homeassistant/components/input_select/services.yaml @@ -1,75 +1,53 @@ select_next: - name: Next - description: Select the next options of an input select entity. target: entity: domain: input_select fields: cycle: - name: Cycle - description: If the option should cycle from the last to the first. default: true selector: boolean: select_option: - name: Select - description: Select an option of an input select entity. target: entity: domain: input_select fields: option: - name: Option - description: Option to be selected. required: true example: '"Item A"' selector: text: select_previous: - name: Previous - description: Select the previous options of an input select entity. target: entity: domain: input_select fields: cycle: - name: Cycle - description: If the option should cycle from the first to the last. default: true selector: boolean: select_first: - name: First - description: Select the first option of an input select entity. target: entity: domain: input_select select_last: - name: Last - description: Select the last option of an input select entity. target: entity: domain: input_select set_options: - name: Set options - description: Set the options of an input select entity. target: entity: domain: input_select fields: options: - name: Options - description: Options for the input select entity. required: true example: '["Item A", "Item B", "Item C"]' selector: object: reload: - name: Reload - description: Reload the input_select configuration. diff --git a/homeassistant/components/input_select/strings.json b/homeassistant/components/input_select/strings.json index f0dead7a1dd37f..68970933346509 100644 --- a/homeassistant/components/input_select/strings.json +++ b/homeassistant/components/input_select/strings.json @@ -16,5 +16,59 @@ } } } + }, + "services": { + "select_next": { + "name": "Next", + "description": "Select the next option.", + "fields": { + "cycle": { + "name": "Cycle", + "description": "If the option should cycle from the last to the first option on the list." + } + } + }, + "select_option": { + "name": "Select", + "description": "Selects an option.", + "fields": { + "option": { + "name": "Option", + "description": "Option to be selected." + } + } + }, + "select_previous": { + "name": "Previous", + "description": "Selects the previous option.", + "fields": { + "cycle": { + "name": "[%key:component::input_select::services::select_next::fields::cycle::name%]", + "description": "[%key:component::input_select::services::select_next::fields::cycle::description%]" + } + } + }, + "select_first": { + "name": "First", + "description": "Selects the first option." + }, + "select_last": { + "name": "Last", + "description": "Selects the last option." + }, + "set_options": { + "name": "Set options", + "description": "Sets the options.", + "fields": { + "options": { + "name": "Options", + "description": "List of options." + } + } + }, + "reload": { + "name": "Reload", + "description": "Reloads helpers from the YAML-configuration." + } } } diff --git a/homeassistant/components/input_text/services.yaml b/homeassistant/components/input_text/services.yaml index cf19e15d7ae119..6cb5c1352c66c7 100644 --- a/homeassistant/components/input_text/services.yaml +++ b/homeassistant/components/input_text/services.yaml @@ -1,18 +1,12 @@ set_value: - name: Set - description: Set the value of an input text entity. target: entity: domain: input_text fields: value: - name: Value - description: The target value the entity should be set to. required: true example: This is an example text selector: text: reload: - name: Reload - description: Reload the input_text configuration. diff --git a/homeassistant/components/input_text/strings.json b/homeassistant/components/input_text/strings.json index d713c395b67ed9..a4dc6d929f5fed 100644 --- a/homeassistant/components/input_text/strings.json +++ b/homeassistant/components/input_text/strings.json @@ -29,5 +29,21 @@ } } } + }, + "services": { + "set_value": { + "name": "Set", + "description": "Sets the value.", + "fields": { + "value": { + "name": "Value", + "description": "The target value." + } + } + }, + "reload": { + "name": "Reload", + "description": "Reloads helpers from the YAML-configuration." + } } } From d3eda12af4345e8b32530f5632947812adea5f7f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 17:28:05 +0200 Subject: [PATCH 0430/1009] Migrate recorder services to support translations (#96409) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- .../components/recorder/services.yaml | 21 --------- .../components/recorder/strings.json | 46 +++++++++++++++++++ 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/recorder/services.yaml b/homeassistant/components/recorder/services.yaml index f099cede9f21b1..b74dcc2a4946e2 100644 --- a/homeassistant/components/recorder/services.yaml +++ b/homeassistant/components/recorder/services.yaml @@ -1,12 +1,8 @@ # Describes the format for available recorder services purge: - name: Purge - description: Start purge task - to clean up old data from your database. fields: keep_days: - name: Days to keep - description: Number of history days to keep in database after purge. selector: number: min: 0 @@ -14,28 +10,20 @@ purge: unit_of_measurement: days repack: - name: Repack - description: Attempt to save disk space by rewriting the entire database file. default: false selector: boolean: apply_filter: - name: Apply filter - description: Apply entity_id and event_type filter in addition to time based purge. default: false selector: boolean: purge_entities: - name: Purge Entities - description: Start purge task to remove specific entities from your database. target: entity: {} fields: domains: - name: Domains to remove - description: List the domains that need to be removed from the recorder database. example: "sun" required: false default: [] @@ -43,8 +31,6 @@ purge_entities: object: entity_globs: - name: Entity Globs to remove - description: List the glob patterns to select entities for removal from the recorder database. example: "domain*.object_id*" required: false default: [] @@ -52,8 +38,6 @@ purge_entities: object: keep_days: - name: Days to keep - description: Number of history days to keep in database of matching rows. The default of 0 days will remove all matching rows. default: 0 selector: number: @@ -62,9 +46,4 @@ purge_entities: unit_of_measurement: days disable: - name: Disable - description: Stop the recording of events and state changes - enable: - name: Enable - description: Start the recording of events and state changes diff --git a/homeassistant/components/recorder/strings.json b/homeassistant/components/recorder/strings.json index 7af67f10e25656..a55f13b27c4f73 100644 --- a/homeassistant/components/recorder/strings.json +++ b/homeassistant/components/recorder/strings.json @@ -13,5 +13,51 @@ "title": "Update MariaDB to {min_version} or later resolve a significant performance issue", "description": "Older versions of MariaDB suffer from a significant performance regression when retrieving history data or purging the database. Update to MariaDB version {min_version} or later and restart Home Assistant. If you are using the MariaDB core add-on, make sure to update it to the latest version." } + }, + "services": { + "purge": { + "name": "Purge", + "description": "Starts purge task - to clean up old data from your database.", + "fields": { + "keep_days": { + "name": "Days to keep", + "description": "Number of days to keep the data in the database. Starting today, counting backward. A value of `7` means that everything older than a week will be purged." + }, + "repack": { + "name": "Repack", + "description": "Attempt to save disk space by rewriting the entire database file." + }, + "apply_filter": { + "name": "Apply filter", + "description": "Applys `entity_id` and `event_type` filters in addition to time-based purge." + } + } + }, + "purge_entities": { + "name": "Purge entities", + "description": "Starts a purge task to remove the data related to specific entities from your database.", + "fields": { + "domains": { + "name": "Domains to remove", + "description": "List of domains for which the data needs to be removed from the recorder database." + }, + "entity_globs": { + "name": "Entity globs to remove", + "description": "List of glob patterns used to select the entities for which the data is to be removed from the recorder database." + }, + "keep_days": { + "name": "Days to keep", + "description": "Number of days to keep the data for rows matching the filter. Starting today, counting backward. A value of `7` means that everything older than a week will be purged. The default of 0 days will remove all matching rows immediately." + } + } + }, + "disable": { + "name": "Disable", + "description": "Stops the recording of events and state changes." + }, + "enable": { + "name": "Enable", + "description": "Starts the recording of events and state changes." + } } } From 848221a1d7c34aba5b11edabb63c4afc58d74d09 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 18:05:51 +0200 Subject: [PATCH 0431/1009] Migrate humidifier services to support translations (#96327) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- .../components/humidifier/services.yaml | 12 ------- .../components/humidifier/strings.json | 34 +++++++++++++++++++ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/humidifier/services.yaml b/homeassistant/components/humidifier/services.yaml index d498f0a2c14cd2..75e34cf5049e33 100644 --- a/homeassistant/components/humidifier/services.yaml +++ b/homeassistant/components/humidifier/services.yaml @@ -1,8 +1,6 @@ # Describes the format for available humidifier services set_mode: - name: Set mode - description: Set mode for humidifier device. target: entity: domain: humidifier @@ -10,21 +8,17 @@ set_mode: - humidifier.HumidifierEntityFeature.MODES fields: mode: - description: New mode required: true example: "away" selector: text: set_humidity: - name: Set humidity - description: Set target humidity of humidifier device. target: entity: domain: humidifier fields: humidity: - description: New target humidity for humidifier device. required: true selector: number: @@ -33,22 +27,16 @@ set_humidity: unit_of_measurement: "%" turn_on: - name: Turn on - description: Turn humidifier device on. target: entity: domain: humidifier turn_off: - name: Turn off - description: Turn humidifier device off. target: entity: domain: humidifier toggle: - name: Toggle - description: Toggles a humidifier device. target: entity: domain: humidifier diff --git a/homeassistant/components/humidifier/strings.json b/homeassistant/components/humidifier/strings.json index 7512b2abec7bcf..d3cf946f5bf7cc 100644 --- a/homeassistant/components/humidifier/strings.json +++ b/homeassistant/components/humidifier/strings.json @@ -74,5 +74,39 @@ "humidifier": { "name": "[%key:component::humidifier::entity_component::_::name%]" } + }, + "services": { + "set_mode": { + "name": "Set mode", + "description": "Sets the humidifier operation mode.", + "fields": { + "mode": { + "name": "Mode", + "description": "Operation mode. For example, _normal_, _eco_, or _away_. For a list of possible values, refer to the integration documentation." + } + } + }, + "set_humidity": { + "name": "Set humidity", + "description": "Sets the target humidity.", + "fields": { + "humidity": { + "name": "Humidity", + "description": "Target humidity." + } + } + }, + "turn_on": { + "name": "Turn on", + "description": "Turns the humidifier on." + }, + "turn_off": { + "name": "Turn off", + "description": "Turns the humidifier off." + }, + "toggle": { + "name": "Toggle", + "description": "Toggles the humidifier on/off." + } } } From 06adace7ca76a875effa4b276b0f5634d568647b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 18:06:16 +0200 Subject: [PATCH 0432/1009] Migrate vacuum services to support translations (#96417) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/vacuum/services.yaml | 30 --------- homeassistant/components/vacuum/strings.json | 62 +++++++++++++++++++ 2 files changed, 62 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml index c517f1aeaaf913..aab35b4207739a 100644 --- a/homeassistant/components/vacuum/services.yaml +++ b/homeassistant/components/vacuum/services.yaml @@ -1,8 +1,6 @@ # Describes the format for available vacuum services turn_on: - name: Turn on - description: Start a new cleaning task. target: entity: domain: vacuum @@ -10,8 +8,6 @@ turn_on: - vacuum.VacuumEntityFeature.TURN_ON turn_off: - name: Turn off - description: Stop the current cleaning task and return to home. target: entity: domain: vacuum @@ -19,8 +15,6 @@ turn_off: - vacuum.VacuumEntityFeature.TURN_OFF stop: - name: Stop - description: Stop the current cleaning task. target: entity: domain: vacuum @@ -28,8 +22,6 @@ stop: - vacuum.VacuumEntityFeature.STOP locate: - name: Locate - description: Locate the vacuum cleaner robot. target: entity: domain: vacuum @@ -37,8 +29,6 @@ locate: - vacuum.VacuumEntityFeature.LOCATE start_pause: - name: Start/Pause - description: Start, pause, or resume the cleaning task. target: entity: domain: vacuum @@ -46,8 +36,6 @@ start_pause: - vacuum.VacuumEntityFeature.PAUSE start: - name: Start - description: Start or resume the cleaning task. target: entity: domain: vacuum @@ -55,8 +43,6 @@ start: - vacuum.VacuumEntityFeature.START pause: - name: Pause - description: Pause the cleaning task. target: entity: domain: vacuum @@ -64,8 +50,6 @@ pause: - vacuum.VacuumEntityFeature.PAUSE return_to_base: - name: Return to base - description: Tell the vacuum cleaner to return to its dock. target: entity: domain: vacuum @@ -73,45 +57,31 @@ return_to_base: - vacuum.VacuumEntityFeature.RETURN_HOME clean_spot: - name: Clean spot - description: Tell the vacuum cleaner to do a spot clean-up. target: entity: domain: vacuum send_command: - name: Send command - description: Send a raw command to the vacuum cleaner. target: entity: domain: vacuum fields: command: - name: Command - description: Command to execute. required: true example: "set_dnd_timer" selector: text: params: - name: Parameters - description: Parameters for the command. example: '{ "key": "value" }' selector: object: set_fan_speed: - name: Set fan speed - description: Set the fan speed of the vacuum cleaner. target: entity: domain: vacuum fields: fan_speed: - name: Fan speed - description: - Platform dependent vacuum cleaner fan speed, with speed steps, like - 'medium' or by percentage, between 0 and 100. required: true example: "low" selector: diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index 93ef1e8584caf5..3bdf650ddd38d1 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -34,5 +34,67 @@ "title": "The {platform} custom integration is using deprecated vacuum feature", "description": "The custom integration `{platform}` is extending the deprecated base class `VacuumEntity` instead of `StateVacuumEntity`.\n\nPlease report it to the author of the `{platform}` custom integration.\n\nOnce an updated version of `{platform}` is available, install it and restart Home Assistant to fix this issue." } + }, + "services": { + "turn_on": { + "name": "Turn on", + "description": "Starts a new cleaning task." + }, + "turn_off": { + "name": "Turn off", + "description": "Stops the current cleaning task and returns to its dock." + }, + "stop": { + "name": "Stop", + "description": "Stops the current cleaning task." + }, + "locate": { + "name": "Locate", + "description": "Locates the vacuum cleaner robot." + }, + "start_pause": { + "name": "Start/pause", + "description": "Starts, pauses, or resumes the cleaning task." + }, + "start": { + "name": "Start", + "description": "Starts or resumes the cleaning task." + }, + "pause": { + "name": "Pause", + "description": "Pauses the cleaning task." + }, + "return_to_base": { + "name": "Return to base", + "description": "Tells the vacuum cleaner to return to its dock." + }, + "clean_spot": { + "name": "Clean spot", + "description": "Tells the vacuum cleaner to do a spot clean-up." + }, + "send_command": { + "name": "Send command", + "description": "Sends a raw command to the vacuum cleaner.", + "fields": { + "command": { + "name": "Command", + "description": "Command to execute. The commands are integration-specific." + }, + "params": { + "name": "Parameters", + "description": "Parameters for the command. The parameters are integration-specific." + } + } + }, + "set_fan_speed": { + "name": "Set fan speed", + "description": "Sets the fan speed of the vacuum cleaner.", + "fields": { + "fan_speed": { + "name": "Fan speed", + "description": "Fan speed. The value depends on the integration. Some integrations have speed steps, like 'medium'. Some use a percentage, between 0 and 100." + } + } + } } } From 80eb4747ff6c943e8c3b7e4f52df1f21c394ab9c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 18:06:31 +0200 Subject: [PATCH 0433/1009] Migrate remote services to support translations (#96410) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/remote/services.yaml | 38 -------- homeassistant/components/remote/strings.json | 86 +++++++++++++++++++ 2 files changed, 86 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index a2b648d9eb31a0..0d8ef63bfc3753 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -1,15 +1,11 @@ # Describes the format for available remote services turn_on: - name: Turn On - description: Sends the Power On Command. target: entity: domain: remote fields: activity: - name: Activity - description: Activity ID or Activity Name to start. example: "BedroomTV" filter: supported_features: @@ -18,50 +14,36 @@ turn_on: text: toggle: - name: Toggle - description: Toggles a device. target: entity: domain: remote turn_off: - name: Turn Off - description: Sends the Power Off Command. target: entity: domain: remote send_command: - name: Send Command - description: Sends a command or a list of commands to a device. target: entity: domain: remote fields: device: - name: Device - description: Device ID to send command to. example: "32756745" selector: text: command: - name: Command - description: A single command or a list of commands to send. required: true example: "Play" selector: object: num_repeats: - name: Repeats - description: The number of times you want to repeat the command(s). default: 1 selector: number: min: 0 max: 255 delay_secs: - name: Delay Seconds - description: The time you want to wait in between repeated commands. default: 0.4 selector: number: @@ -70,8 +52,6 @@ send_command: step: 0.1 unit_of_measurement: seconds hold_secs: - name: Hold Seconds - description: The time you want to have it held before the release is send. default: 0 selector: number: @@ -81,27 +61,19 @@ send_command: unit_of_measurement: seconds learn_command: - name: Learn Command - description: Learns a command or a list of commands from a device. target: entity: domain: remote fields: device: - name: Device - description: Device ID to learn command from. example: "television" selector: text: command: - name: Command - description: A single command or a list of commands to learn. example: "Turn on" selector: object: command_type: - name: Command Type - description: The type of command to be learned. default: "ir" selector: select: @@ -109,13 +81,9 @@ learn_command: - "ir" - "rf" alternative: - name: Alternative - description: If code must be stored as alternative (useful for discrete remotes). selector: boolean: timeout: - name: Timeout - description: Timeout for the command to be learned. selector: number: min: 0 @@ -124,21 +92,15 @@ learn_command: unit_of_measurement: seconds delete_command: - name: Delete Command - description: Deletes a command or a list of commands from the database. target: entity: domain: remote fields: device: - name: Device - description: Name of the device from which commands will be deleted. example: "television" selector: text: command: - name: Command - description: A single command or a list of commands to delete. required: true example: "Mute" selector: diff --git a/homeassistant/components/remote/strings.json b/homeassistant/components/remote/strings.json index bf8a669af50428..14331c5cded7b6 100644 --- a/homeassistant/components/remote/strings.json +++ b/homeassistant/components/remote/strings.json @@ -24,5 +24,91 @@ "on": "[%key:common::state::on%]" } } + }, + "services": { + "turn_on": { + "name": "Turn on", + "description": "Sends the power on command.", + "fields": { + "activity": { + "name": "Activity", + "description": "Activity ID or activity name to be started." + } + } + }, + "toggle": { + "name": "Toggle", + "description": "Toggles a device on/off." + }, + "turn_off": { + "name": "Turn off", + "description": "Turns the device off." + }, + "send_command": { + "name": "Send command", + "description": "Sends a command or a list of commands to a device.", + "fields": { + "device": { + "name": "Device", + "description": "Device ID to send command to." + }, + "command": { + "name": "Command", + "description": "A single command or a list of commands to send." + }, + "num_repeats": { + "name": "Repeats", + "description": "The number of times you want to repeat the commands." + }, + "delay_secs": { + "name": "Delay seconds", + "description": "The time you want to wait in between repeated commands." + }, + "hold_secs": { + "name": "Hold seconds", + "description": "The time you want to have it held before the release is send." + } + } + }, + "learn_command": { + "name": "Learn command", + "description": "Learns a command or a list of commands from a device.", + "fields": { + "device": { + "name": "Device", + "description": "Device ID to learn command from." + }, + "command": { + "name": "Command", + "description": "A single command or a list of commands to learn." + }, + "command_type": { + "name": "Command type", + "description": "The type of command to be learned." + }, + "alternative": { + "name": "Alternative", + "description": "If code must be stored as an alternative. This is useful for discrete codes. Discrete codes are used for toggles that only perform one function. For example, a code to only turn a device on. If it is on already, sending the code won't change the state." + }, + "timeout": { + "name": "Timeout", + "description": "Timeout for the command to be learned." + } + } + }, + "delete_command": { + "name": "Delete command", + "description": "Deletes a command or a list of commands from the database.", + "fields": { + "device": { + "name": "Device", + "description": "Device from which commands will be deleted." + }, + "command": { + "name": "Command", + "description": "The single command or the list of commands to be deleted." + } + } + } } } From 5792301cf1caf7c37635ff81842e89fe016c5163 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 18:16:30 +0200 Subject: [PATCH 0434/1009] Migrate lock services to support translations (#96416) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/lock/services.yaml | 12 -------- homeassistant/components/lock/strings.json | 32 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml index 992c58cf5f6c1b..c80517d1fe19c3 100644 --- a/homeassistant/components/lock/services.yaml +++ b/homeassistant/components/lock/services.yaml @@ -1,22 +1,16 @@ # Describes the format for available lock services lock: - name: Lock - description: Lock all or specified locks. target: entity: domain: lock fields: code: - name: Code - description: An optional code to lock the lock with. example: 1234 selector: text: open: - name: Open - description: Open all or specified locks. target: entity: domain: lock @@ -24,22 +18,16 @@ open: - lock.LockEntityFeature.OPEN fields: code: - name: Code - description: An optional code to open the lock with. example: 1234 selector: text: unlock: - name: Unlock - description: Unlock all or specified locks. target: entity: domain: lock fields: code: - name: Code - description: An optional code to unlock the lock with. example: 1234 selector: text: diff --git a/homeassistant/components/lock/strings.json b/homeassistant/components/lock/strings.json index da4b5217b86294..9e20b0cad2bd5d 100644 --- a/homeassistant/components/lock/strings.json +++ b/homeassistant/components/lock/strings.json @@ -34,5 +34,37 @@ } } } + }, + "services": { + "lock": { + "name": "Lock", + "description": "Locks a lock.", + "fields": { + "code": { + "name": "Code", + "description": "Code used to lock the lock." + } + } + }, + "open": { + "name": "Open", + "description": "Opens a lock.", + "fields": { + "code": { + "name": "[%key:component::lock::services::lock::fields::code::name%]", + "description": "Code used to open the lock." + } + } + }, + "unlock": { + "name": "Unlock", + "description": "Unlocks a lock.", + "fields": { + "code": { + "name": "[%key:component::lock::services::lock::fields::code::name%]", + "description": "Code used to unlock the lock." + } + } + } } } From 899adfa74c31b5bacedfe2fcd4c5b7cec3e4c695 Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Wed, 12 Jul 2023 18:33:56 +0200 Subject: [PATCH 0435/1009] Add Ezviz select entity (#93625) * Initial commit * Add select entity * coveragerc * Cleanup * Commit suggestions. * Raise issue before try except * Add translation key * Update camera.py * Update camera.py * Disable old sensor by default instead of removing. * Apply suggestions from code review Co-authored-by: G Johansson * IR fix flow * Fix conflict * run black --------- Co-authored-by: G Johansson --- .coveragerc | 1 + homeassistant/components/ezviz/__init__.py | 1 + homeassistant/components/ezviz/camera.py | 10 +++ homeassistant/components/ezviz/select.py | 99 +++++++++++++++++++++ homeassistant/components/ezviz/sensor.py | 5 +- homeassistant/components/ezviz/strings.json | 23 +++++ 6 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/ezviz/select.py diff --git a/.coveragerc b/.coveragerc index e10a23e9c31404..703523ed3648f8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -321,6 +321,7 @@ omit = homeassistant/components/ezviz/coordinator.py homeassistant/components/ezviz/number.py homeassistant/components/ezviz/entity.py + homeassistant/components/ezviz/select.py homeassistant/components/ezviz/sensor.py homeassistant/components/ezviz/switch.py homeassistant/components/ezviz/update.py diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index 9386a407acb491..9aeba56360e8d8 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -37,6 +37,7 @@ Platform.CAMERA, Platform.LIGHT, Platform.NUMBER, + Platform.SELECT, Platform.SENSOR, Platform.SWITCH, Platform.UPDATE, diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 6150a657c1ad7f..01e8425c13bbdc 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -290,6 +290,16 @@ def perform_wake_device(self) -> None: def perform_alarm_sound(self, level: int) -> None: """Enable/Disable movement sound alarm.""" + ir.async_create_issue( + self.hass, + DOMAIN, + "service_deprecation_alarm_sound_level", + breaks_in_ha_version="2024.2.0", + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="service_deprecation_alarm_sound_level", + ) try: self.coordinator.ezviz_client.alarm_sound(self._serial, level, 1) except HTTPError as err: diff --git a/homeassistant/components/ezviz/select.py b/homeassistant/components/ezviz/select.py new file mode 100644 index 00000000000000..0f6a52ef578502 --- /dev/null +++ b/homeassistant/components/ezviz/select.py @@ -0,0 +1,99 @@ +"""Support for EZVIZ select controls.""" +from __future__ import annotations + +from dataclasses import dataclass + +from pyezviz.constants import DeviceSwitchType, SoundMode +from pyezviz.exceptions import HTTPError, PyEzvizError + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DATA_COORDINATOR, DOMAIN +from .coordinator import EzvizDataUpdateCoordinator +from .entity import EzvizEntity + +PARALLEL_UPDATES = 1 + + +@dataclass +class EzvizSelectEntityDescriptionMixin: + """Mixin values for EZVIZ Select entities.""" + + supported_switch: int + + +@dataclass +class EzvizSelectEntityDescription( + SelectEntityDescription, EzvizSelectEntityDescriptionMixin +): + """Describe a EZVIZ Select entity.""" + + +SELECT_TYPE = EzvizSelectEntityDescription( + key="alarm_sound_mod", + translation_key="alarm_sound_mode", + icon="mdi:alarm", + entity_category=EntityCategory.CONFIG, + options=["soft", "intensive", "silent"], + supported_switch=DeviceSwitchType.ALARM_TONE.value, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up EZVIZ select entities based on a config entry.""" + coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR + ] + + async_add_entities( + EzvizSensor(coordinator, camera) + for camera in coordinator.data + for switch in coordinator.data[camera]["switches"] + if switch == SELECT_TYPE.supported_switch + ) + + +class EzvizSensor(EzvizEntity, SelectEntity): + """Representation of a EZVIZ select entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: EzvizDataUpdateCoordinator, + serial: str, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator, serial) + self._attr_unique_id = f"{serial}_{SELECT_TYPE.key}" + self.entity_description = SELECT_TYPE + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + sound_mode_value = getattr( + SoundMode, self.data[self.entity_description.key] + ).value + if sound_mode_value in [0, 1, 2]: + return self.options[sound_mode_value] + + return None + + def select_option(self, option: str) -> None: + """Change the selected option.""" + sound_mode_value = self.options.index(option) + + try: + self.coordinator.ezviz_client.alarm_sound(self._serial, sound_mode_value, 1) + + except (HTTPError, PyEzvizError) as err: + raise HomeAssistantError( + f"Cannot set Warning sound level for {self.entity_id}" + ) from err diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py index 11412c1fc70d07..075fe6bd6d1b7d 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -24,7 +24,10 @@ native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, ), - "alarm_sound_mod": SensorEntityDescription(key="alarm_sound_mod"), + "alarm_sound_mod": SensorEntityDescription( + key="alarm_sound_mod", + entity_registry_enabled_default=False, + ), "last_alarm_time": SensorEntityDescription(key="last_alarm_time"), "Seconds_Last_Trigger": SensorEntityDescription( key="Seconds_Last_Trigger", diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index 5355fcc377ca3a..aec1f892b1fbc2 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -70,6 +70,29 @@ } } } + }, + "service_deprecation_alarm_sound_level": { + "title": "Ezviz Alarm sound level service is being removed", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::ezviz::issues::service_deprecation_alarm_sound_level::title%]", + "description": "Ezviz Alarm sound level service is deprecated and will be removed in Home Assistant 2024.2.\nTo set the Alarm sound level, you can instead use the `select.select_option` service targetting the Warning sound entity.\n\nPlease remove the use of this service from your automations and scripts and select **submit** to close this issue." + } + } + } + } + }, + "entity": { + "select": { + "alarm_sound_mode": { + "name": "Warning sound", + "state": { + "soft": "Soft", + "intensive": "Intensive", + "silent": "Silent" + } + } } }, "services": { From c67a1a326f8da183bec50b21807b03e2689d940e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jul 2023 06:39:32 -1000 Subject: [PATCH 0436/1009] Improve chances of recovering stuck down bluetooth adapters (#96382) --- homeassistant/components/bluetooth/manifest.json | 4 ++-- homeassistant/components/bluetooth/scanner.py | 1 + homeassistant/package_constraints.txt | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index dbe8ac3f1ab7df..bed677ebd306f0 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -16,8 +16,8 @@ "requirements": [ "bleak==0.20.2", "bleak-retry-connector==3.0.2", - "bluetooth-adapters==0.15.3", - "bluetooth-auto-recovery==1.2.0", + "bluetooth-adapters==0.16.0", + "bluetooth-auto-recovery==1.2.1", "bluetooth-data-tools==1.3.0", "dbus-fast==1.86.0" ] diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 911862a4221f6c..35efbdf3cbe035 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -53,6 +53,7 @@ "org.bluez.Error.Failed", "org.bluez.Error.InProgress", "org.bluez.Error.NotReady", + "not found", ] # When the adapter is still initializing, the scanner will raise an exception diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b82a7315648f40..8ae3ba06985ba3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,8 +10,8 @@ awesomeversion==22.9.0 bcrypt==4.0.1 bleak-retry-connector==3.0.2 bleak==0.20.2 -bluetooth-adapters==0.15.3 -bluetooth-auto-recovery==1.2.0 +bluetooth-adapters==0.16.0 +bluetooth-auto-recovery==1.2.1 bluetooth-data-tools==1.3.0 certifi>=2021.5.30 ciso8601==2.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index d33f01b34be376..26d4911dddaad0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -525,10 +525,10 @@ bluemaestro-ble==0.2.3 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.15.3 +bluetooth-adapters==0.16.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.2.0 +bluetooth-auto-recovery==1.2.1 # homeassistant.components.bluetooth # homeassistant.components.esphome diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f35021c3e006af..864938446e8d7a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -439,10 +439,10 @@ blinkpy==0.21.0 bluemaestro-ble==0.2.3 # homeassistant.components.bluetooth -bluetooth-adapters==0.15.3 +bluetooth-adapters==0.16.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.2.0 +bluetooth-auto-recovery==1.2.1 # homeassistant.components.bluetooth # homeassistant.components.esphome From 7021daf9fba7d8c9360c0bc8d4361b05d92873c9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 18:55:22 +0200 Subject: [PATCH 0437/1009] Migrate select services to support translations (#96411) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/select/services.yaml | 16 -------- homeassistant/components/select/strings.json | 40 +++++++++++++++++++ 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/select/services.yaml b/homeassistant/components/select/services.yaml index 8fb55936fc933a..dc6d4c6815a562 100644 --- a/homeassistant/components/select/services.yaml +++ b/homeassistant/components/select/services.yaml @@ -1,56 +1,40 @@ select_first: - name: First - description: Select the first option of an select entity. target: entity: domain: select select_last: - name: Last - description: Select the last option of an select entity. target: entity: domain: select select_next: - name: Next - description: Select the next options of an select entity. target: entity: domain: select fields: cycle: - name: Cycle - description: If the option should cycle from the last to the first. default: true selector: boolean: select_option: - name: Select - description: Select an option of an select entity. target: entity: domain: select fields: option: - name: Option - description: Option to be selected. required: true example: '"Item A"' selector: text: select_previous: - name: Previous - description: Select the previous options of an select entity. target: entity: domain: select fields: cycle: - name: Cycle - description: If the option should cycle from the first to the last. default: true selector: boolean: diff --git a/homeassistant/components/select/strings.json b/homeassistant/components/select/strings.json index 9080b940b2a5f7..d058ff6e6f2c7f 100644 --- a/homeassistant/components/select/strings.json +++ b/homeassistant/components/select/strings.json @@ -24,5 +24,45 @@ } } } + }, + "services": { + "select_first": { + "name": "First", + "description": "Selects the first option." + }, + "select_last": { + "name": "Last", + "description": "Selects the last option." + }, + "select_next": { + "name": "Next", + "description": "Selects the next option.", + "fields": { + "cycle": { + "name": "Cycle", + "description": "If the option should cycle from the last to the first." + } + } + }, + "select_option": { + "name": "Select", + "description": "Selects an option.", + "fields": { + "option": { + "name": "Option", + "description": "Option to be selected." + } + } + }, + "select_previous": { + "name": "Previous", + "description": "Selects the previous option.", + "fields": { + "cycle": { + "name": "Cycle", + "description": "If the option should cycle from the first to the last." + } + } + } } } From 021aaa999498b7f0cbed9f427612947a7d369da1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 18:55:34 +0200 Subject: [PATCH 0438/1009] Migrate tts services to support translations (#96412) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/tts/services.yaml | 30 ----------- homeassistant/components/tts/strings.json | 60 ++++++++++++++++++++++ 2 files changed, 60 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/tts/strings.json diff --git a/homeassistant/components/tts/services.yaml b/homeassistant/components/tts/services.yaml index 99e0bcca4d4b31..03b176eaab3f3b 100644 --- a/homeassistant/components/tts/services.yaml +++ b/homeassistant/components/tts/services.yaml @@ -1,88 +1,58 @@ # Describes the format for available TTS services say: - name: Say a TTS message - description: Say something using text-to-speech on a media player. fields: entity_id: - name: Entity - description: Name(s) of media player entities. required: true selector: entity: domain: media_player message: - name: Message - description: Text to speak on devices. example: "My name is hanna" required: true selector: text: cache: - name: Cache - description: Control file cache of this message. default: false selector: boolean: language: - name: Language - description: Language to use for speech generation. example: "ru" selector: text: options: - name: Options - description: - A dictionary containing platform-specific options. Optional depending on - the platform. advanced: true example: platform specific selector: object: speak: - name: Speak - description: Speak something using text-to-speech on a media player. target: entity: domain: tts fields: media_player_entity_id: - name: Media Player Entity - description: Name(s) of media player entities. required: true selector: entity: domain: media_player message: - name: Message - description: Text to speak on devices. example: "My name is hanna" required: true selector: text: cache: - name: Cache - description: Control file cache of this message. default: true selector: boolean: language: - name: Language - description: Language to use for speech generation. example: "ru" selector: text: options: - name: Options - description: - A dictionary containing platform-specific options. Optional depending on - the platform. advanced: true example: platform specific selector: object: clear_cache: - name: Clear TTS cache - description: Remove all text-to-speech cache files and RAM cache. diff --git a/homeassistant/components/tts/strings.json b/homeassistant/components/tts/strings.json new file mode 100644 index 00000000000000..2f0208ef8b57b4 --- /dev/null +++ b/homeassistant/components/tts/strings.json @@ -0,0 +1,60 @@ +{ + "services": { + "say": { + "name": "Say a TTS message", + "description": "Says something using text-to-speech on a media player.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Media players to play the message." + }, + "message": { + "name": "Message", + "description": "The text you want to convert into speech so that you can listen to it on your device." + }, + "cache": { + "name": "Cache", + "description": "Stores this message locally so that when the text is requested again, the output can be produced more quickly." + }, + "language": { + "name": "Language", + "description": "Language to use for speech generation." + }, + "options": { + "name": "Options", + "description": "A dictionary containing integration-specific options." + } + } + }, + "speak": { + "name": "Speak", + "description": "Speaks something using text-to-speech on a media player.", + "fields": { + "media_player_entity_id": { + "name": "Media player entity", + "description": "Media players to play the message." + }, + "message": { + "name": "[%key:component::tts::services::say::fields::message::name%]", + "description": "[%key:component::tts::services::say::fields::message::description%]" + }, + "cache": { + "name": "[%key:component::tts::services::say::fields::cache::name%]", + "description": "[%key:component::tts::services::say::fields::cache::description%]" + }, + "language": { + "name": "[%key:component::tts::services::say::fields::language::name%]", + "description": "[%key:component::tts::services::say::fields::language::description%]" + }, + "options": { + "name": "[%key:component::tts::services::say::fields::options::name%]", + "description": "[%key:component::tts::services::say::fields::options::description%]" + } + } + }, + "clear_cache": { + "name": "Clear TTS cache", + "description": "Removes all cached text-to-speech files and purges the memory." + } + } +} From 728a5ff99b4e87aef6c4994bc21ce862ce6afbbc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 18:56:08 +0200 Subject: [PATCH 0439/1009] Migrate system_log services to support translations (#96398) --- .../components/system_log/services.yaml | 28 +++----------- .../components/system_log/strings.json | 37 +++++++++++++++++++ 2 files changed, 43 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/system_log/strings.json diff --git a/homeassistant/components/system_log/services.yaml b/homeassistant/components/system_log/services.yaml index 0f9ae61ba4c8dd..9ab3bb6bce3203 100644 --- a/homeassistant/components/system_log/services.yaml +++ b/homeassistant/components/system_log/services.yaml @@ -1,39 +1,23 @@ clear: - name: Clear all - description: Clear all log entries. - write: - name: Write - description: Write log entry. fields: message: - name: Message - description: Message to log. required: true example: Something went wrong selector: text: level: - name: Level - description: "Log level." default: error selector: select: options: - - label: "Debug" - value: "debug" - - label: "Info" - value: "info" - - label: "Warning" - value: "warning" - - label: "Error" - value: "error" - - label: "Critical" - value: "critical" + - "debug" + - "info" + - "warning" + - "error" + - "critical" + translation_key: level logger: - name: Logger - description: Logger name under which to log the message. Defaults to - 'system_log.external'. example: mycomponent.myplatform selector: text: diff --git a/homeassistant/components/system_log/strings.json b/homeassistant/components/system_log/strings.json new file mode 100644 index 00000000000000..ed1ca79fe0795f --- /dev/null +++ b/homeassistant/components/system_log/strings.json @@ -0,0 +1,37 @@ +{ + "services": { + "clear": { + "name": "Clear all", + "description": "Clears all log entries." + }, + "write": { + "name": "Write", + "description": "Write log entry.", + "fields": { + "message": { + "name": "Message", + "description": "Message to log." + }, + "level": { + "name": "Level", + "description": "Log level." + }, + "logger": { + "name": "Logger", + "description": "Logger name under which to log the message. Defaults to `system_log.external`." + } + } + } + }, + "selector": { + "level": { + "options": { + "debug": "Debug", + "info": "Info", + "warning": "Warning", + "error": "Error", + "critical": "Critical" + } + } + } +} From 11cd7692a13daba35d8eab3745b6d7a0997e4896 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 19:58:08 +0200 Subject: [PATCH 0440/1009] Migrate group services to support translations (#96369) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Co-authored-by: Joost Lekkerkerker --- homeassistant/components/group/services.yaml | 23 --------- homeassistant/components/group/strings.json | 50 ++++++++++++++++++++ 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/group/services.yaml b/homeassistant/components/group/services.yaml index fdb1a1af014912..e5ac921cc77da9 100644 --- a/homeassistant/components/group/services.yaml +++ b/homeassistant/components/group/services.yaml @@ -1,62 +1,39 @@ # Describes the format for available group services reload: - name: Reload - description: Reload group configuration, entities, and notify services. - set: - name: Set - description: Create/Update a user group. fields: object_id: - name: Object ID - description: Group id and part of entity id. required: true example: "test_group" selector: text: name: - name: Name - description: Name of group example: "My test group" selector: text: icon: - name: Icon - description: Name of icon for the group. example: "mdi:camera" selector: icon: entities: - name: Entities - description: List of all members in the group. Not compatible with 'delta'. example: domain.entity_id1, domain.entity_id2 selector: object: add_entities: - name: Add Entities - description: List of members that will change on group listening. example: domain.entity_id1, domain.entity_id2 selector: object: remove_entities: - name: Remove Entities - description: List of members that will be removed from group listening. example: domain.entity_id1, domain.entity_id2 selector: object: all: - name: All - description: Enable this option if the group should only turn on when all entities are on. selector: boolean: remove: - name: Remove - description: Remove a user group. fields: object_id: - name: Object ID - description: Group id and part of entity id. required: true example: "test_group" selector: diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index 192823cef6515b..7b49eaf4186356 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -190,5 +190,55 @@ "product": "Product" } } + }, + "services": { + "reload": { + "name": "Reload", + "description": "Reloads group configuration, entities, and notify services from YAML-configuration." + }, + "set": { + "name": "Set", + "description": "Creates/Updates a user group.", + "fields": { + "object_id": { + "name": "Object ID", + "description": "Object ID of this group. This object ID is used as part of the entity ID. Entity ID format: [domain].[object_id]." + }, + "name": { + "name": "Name", + "description": "Name of the group." + }, + "icon": { + "name": "Icon", + "description": "Name of the icon for the group." + }, + "entities": { + "name": "Entities", + "description": "List of all members in the group. Cannot be used in combination with `Add entities` or `Remove entities`." + }, + "add_entities": { + "name": "Add entities", + "description": "List of members to be added to the group. Cannot be used in combination with `Entities` or `Remove entities`." + }, + "remove_entities": { + "name": "Remove entities", + "description": "List of members to be removed from a group. Cannot be used in combination with `Entities` or `Add entities`." + }, + "all": { + "name": "All", + "description": "Enable this option if the group should only be used when all entities are in state `on`." + } + } + }, + "remove": { + "name": "Remove", + "description": "Removes a group.", + "fields": { + "object_id": { + "name": "Object ID", + "description": "Object ID of this group. This object ID is used as part of the entity ID. Entity ID format: [domain].[object_id]." + } + } + } } } From 273e80cc456c47e0da515f685d888ada48ee49da Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 20:24:21 +0200 Subject: [PATCH 0441/1009] Migrate text services to support translations (#96397) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/text/services.yaml | 4 ---- homeassistant/components/text/strings.json | 12 ++++++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/text/services.yaml b/homeassistant/components/text/services.yaml index 00dd0ecafd28e0..b8461037b8bd18 100644 --- a/homeassistant/components/text/services.yaml +++ b/homeassistant/components/text/services.yaml @@ -1,13 +1,9 @@ set_value: - name: Set value - description: Set value of a text entity. target: entity: domain: text fields: value: - name: Value - description: Value to set. required: true example: "Hello world!" selector: diff --git a/homeassistant/components/text/strings.json b/homeassistant/components/text/strings.json index 034f1ab315b820..e6b3d99ced4a5d 100644 --- a/homeassistant/components/text/strings.json +++ b/homeassistant/components/text/strings.json @@ -27,5 +27,17 @@ } } } + }, + "services": { + "set_value": { + "name": "Set value", + "description": "Sets the value.", + "fields": { + "value": { + "name": "Value", + "description": "Enter your text." + } + } + } } } From a96ee22afa243c410cc715314b02837fd41343c1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 20:37:45 +0200 Subject: [PATCH 0442/1009] Migrate notify services to support translations (#96413) * Migrate notify services to support translations * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/notify/services.yaml | 24 ---------- homeassistant/components/notify/strings.json | 46 ++++++++++++++++++- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index 9311acf2ba9536..8d053e3af5835f 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -1,61 +1,37 @@ # Describes the format for available notification services notify: - name: Send a notification - description: Sends a notification message to selected notify platforms. fields: message: - name: Message - description: Message body of the notification. required: true example: The garage door has been open for 10 minutes. selector: text: title: - name: Title - description: Title for your notification. example: "Your Garage Door Friend" selector: text: target: - name: Target - description: - An array of targets to send the notification to. Optional depending on - the platform. example: platform specific selector: object: data: - name: Data - description: - Extended information for notification. Optional depending on the - platform. example: platform specific selector: object: persistent_notification: - name: Send a persistent notification - description: Sends a notification that is visible in the front-end. fields: message: - name: Message - description: Message body of the notification. required: true example: The garage door has been open for 10 minutes. selector: text: title: - name: Title - description: Title for your notification. example: "Your Garage Door Friend" selector: text: data: - name: Data - description: - Extended information for notification. Optional depending on the - platform. example: platform specific selector: object: diff --git a/homeassistant/components/notify/strings.json b/homeassistant/components/notify/strings.json index 02027a84d8f4a4..cff7b265c37d7b 100644 --- a/homeassistant/components/notify/strings.json +++ b/homeassistant/components/notify/strings.json @@ -1 +1,45 @@ -{ "title": "Notifications" } +{ + "title": "Notifications", + "services": { + "notify": { + "name": "Send a notification", + "description": "Sends a notification message to selected targets.", + "fields": { + "message": { + "name": "Message", + "description": "Message body of the notification." + }, + "title": { + "name": "Title", + "description": "Title for your notification." + }, + "target": { + "name": "Target", + "description": "Some integrations allow you to specify the targets that receive the notification. For more information, refer to the integration documentation." + }, + "data": { + "name": "Data", + "description": "Some integrations provide extended functionality. For information on how to use _data_, refer to the integration documentation." + } + } + }, + "persistent_notification": { + "name": "Send a persistent notification", + "description": "Sends a notification that is visible in the **Notifications** panel.", + "fields": { + "message": { + "name": "Message", + "description": "Message body of the notification." + }, + "title": { + "name": "Title", + "description": "Title of the notification." + }, + "data": { + "name": "Data", + "description": "Some integrations provide extended functionality. For information on how to use _data_, refer to the integration documentation.." + } + } + } + } +} From e95c4f7e65f9d6dbc6df868ba1b78cff36712eac Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Jul 2023 20:49:36 +0200 Subject: [PATCH 0443/1009] Migrate zha services to support translations (#96418) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/zha/services.yaml | 132 ----------- homeassistant/components/zha/strings.json | 262 ++++++++++++++++++++- 2 files changed, 260 insertions(+), 134 deletions(-) diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index 132dae6e745a82..027653a4a6fdb9 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -1,12 +1,8 @@ # Describes the format for available zha services permit: - name: Permit - description: Allow nodes to join the Zigbee network. fields: duration: - name: Duration - description: Time to permit joins, in seconds default: 60 selector: number: @@ -14,73 +10,46 @@ permit: max: 254 unit_of_measurement: seconds ieee: - name: IEEE - description: IEEE address of the node permitting new joins example: "00:0d:6f:00:05:7d:2d:34" selector: text: source_ieee: - name: Source IEEE - description: IEEE address of the joining device (must be used with install code) example: "00:0a:bf:00:01:10:23:35" selector: text: install_code: - name: Install Code - description: Install code of the joining device (must be used with source_ieee) example: "1234-5678-1234-5678-AABB-CCDD-AABB-CCDD-EEFF" selector: text: qr_code: - name: QR Code - description: value of the QR install code (different between vendors) example: "Z:000D6FFFFED4163B$I:52797BF4A5084DAA8E1712B61741CA024051" selector: text: remove: - name: Remove - description: Remove a node from the Zigbee network. fields: ieee: - name: IEEE - description: IEEE address of the node to remove required: true example: "00:0d:6f:00:05:7d:2d:34" selector: text: reconfigure_device: - name: Reconfigure device - description: >- - Reconfigure ZHA device (heal device). Use this if you are having issues - with the device. If the device in question is a battery powered device - please ensure it is awake and accepting commands when you use this - service. fields: ieee: - name: IEEE - description: IEEE address of the device to reconfigure required: true example: "00:0d:6f:00:05:7d:2d:34" selector: text: set_zigbee_cluster_attribute: - name: Set zigbee cluster attribute - description: >- - Set attribute value for the specified cluster on the specified entity. fields: ieee: - name: IEEE - description: IEEE address for the device required: true example: "00:0d:6f:00:05:7d:2d:34" selector: text: endpoint_id: - name: Endpoint ID - description: Endpoint id for the cluster required: true selector: number: @@ -88,16 +57,12 @@ set_zigbee_cluster_attribute: max: 65535 mode: box cluster_id: - name: Cluster ID - description: ZCL cluster to retrieve attributes for required: true selector: number: min: 1 max: 65535 cluster_type: - name: Cluster Type - description: type of the cluster default: "in" selector: select: @@ -105,8 +70,6 @@ set_zigbee_cluster_attribute: - "in" - "out" attribute: - name: Attribute - description: id of the attribute to set required: true example: 0 selector: @@ -114,50 +77,35 @@ set_zigbee_cluster_attribute: min: 1 max: 65535 value: - name: Value - description: value to write to the attribute required: true example: 0x0001 selector: text: manufacturer: - name: Manufacturer - description: manufacturer code example: 0x00FC selector: text: issue_zigbee_cluster_command: - name: Issue zigbee cluster command - description: >- - Issue command on the specified cluster on the specified entity. fields: ieee: - name: IEEE - description: IEEE address for the device required: true example: "00:0d:6f:00:05:7d:2d:34" selector: text: endpoint_id: - name: Endpoint ID - description: Endpoint id for the cluster required: true selector: number: min: 1 max: 65535 cluster_id: - name: Cluster ID - description: ZCL cluster to retrieve attributes for required: true selector: number: min: 1 max: 65535 cluster_type: - name: Cluster Type - description: type of the cluster default: "in" selector: select: @@ -165,16 +113,12 @@ issue_zigbee_cluster_command: - "in" - "out" command: - name: Command - description: id of the command to execute required: true selector: number: min: 1 max: 65535 command_type: - name: Command Type - description: type of the command to execute required: true selector: select: @@ -182,46 +126,31 @@ issue_zigbee_cluster_command: - "client" - "server" args: - name: Args - description: args to pass to the command example: "[arg1, arg2, argN]" selector: object: params: - name: Params - description: parameters to pass to the command selector: object: manufacturer: - name: Manufacturer - description: manufacturer code example: 0x00FC selector: text: issue_zigbee_group_command: - name: Issue zigbee group command - description: >- - Issue command on the specified cluster on the specified group. fields: group: - name: Group - description: Hexadecimal address of the group required: true example: 0x0222 selector: text: cluster_id: - name: Cluster ID - description: ZCL cluster to send command to required: true selector: number: min: 1 max: 65535 cluster_type: - name: Cluster Type - description: type of the cluster default: "in" selector: select: @@ -229,42 +158,28 @@ issue_zigbee_group_command: - "in" - "out" command: - name: Command - description: id of the command to execute required: true selector: number: min: 1 max: 65535 args: - name: Args - description: args to pass to the command example: "[arg1, arg2, argN]" selector: object: manufacturer: - name: Manufacturer - description: manufacturer code example: 0x00FC selector: text: warning_device_squawk: - name: Warning device squawk - description: >- - This service uses the WD capabilities to emit a quick audible/visible pulse called a "squawk". The squawk command has no effect if the WD is currently active (warning in progress). fields: ieee: - name: IEEE - description: IEEE address for the device required: true example: "00:0d:6f:00:05:7d:2d:34" selector: text: mode: - name: Mode - description: >- - The Squawk Mode field is used as a 4-bit enumeration, and can have one of the values shown in Table 8-24 of the ZCL spec - Squawk Mode Field. The exact operation of each mode (how the WD “squawks”) is implementation specific. default: 0 selector: number: @@ -272,9 +187,6 @@ warning_device_squawk: max: 1 mode: box strobe: - name: Strobe - description: >- - The strobe field is used as a Boolean, and determines if the visual indication is also required in addition to the audible squawk, as shown in Table 8-25 of the ZCL spec - Strobe Bit. default: 1 selector: number: @@ -282,9 +194,6 @@ warning_device_squawk: max: 1 mode: box level: - name: Level - description: >- - The squawk level field is used as a 2-bit enumeration, and determines the intensity of audible squawk sound as shown in Table 8-26 of the ZCL spec - Squawk Level Field Values. default: 2 selector: number: @@ -293,21 +202,13 @@ warning_device_squawk: mode: box warning_device_warn: - name: Warning device warn - description: >- - This service starts the WD operation. The WD alerts the surrounding area by audible (siren) and visual (strobe) signals. fields: ieee: - name: IEEE - description: IEEE address for the device required: true example: "00:0d:6f:00:05:7d:2d:34" selector: text: mode: - name: Mode - description: >- - The Warning Mode field is used as an 4-bit enumeration, can have one of the values 0-6 defined below in table 8-20 of the ZCL spec. The exact behavior of the WD device in each mode is according to the relevant security standards. default: 3 selector: number: @@ -315,9 +216,6 @@ warning_device_warn: max: 6 mode: box strobe: - name: Strobe - description: >- - The Strobe field is used as a 2-bit enumeration, and determines if the visual indication is required in addition to the audible siren, as indicated in Table 8-21 of the ZCL spec. "0" means no strobe, "1" means strobe. If the strobe field is “1” and the Warning Mode is “0” (“Stop”) then only the strobe is activated. default: 1 selector: number: @@ -325,9 +223,6 @@ warning_device_warn: max: 1 mode: box level: - name: Level - description: >- - The Siren Level field is used as a 2-bit enumeration, and indicates the intensity of audible squawk sound as shown in Table 8-22 of the ZCL spec. default: 2 selector: number: @@ -335,9 +230,6 @@ warning_device_warn: max: 3 mode: box duration: - name: Duration - description: >- - Requested duration of warning, in seconds (16 bit). If both Strobe and Warning Mode are "0" this field SHALL be ignored. default: 5 selector: number: @@ -345,9 +237,6 @@ warning_device_warn: max: 65535 unit_of_measurement: seconds duty_cycle: - name: Duty cycle - description: >- - Indicates the length of the flash cycle. This provides a means of varying the flash duration for different alarm types (e.g., fire, police, burglar). Valid range is 0-100 in increments of 10. All other values SHALL be rounded to the nearest valid value. Strobe SHALL calculate duty cycle over a duration of one second. The ON state SHALL precede the OFF state. For example, if Strobe Duty Cycle Field specifies “40,” then the strobe SHALL flash ON for 4/10ths of a second and then turn OFF for 6/10ths of a second. default: 0 selector: number: @@ -355,9 +244,6 @@ warning_device_warn: max: 100 step: 10 intensity: - name: Intensity - description: >- - Indicates the intensity of the strobe as shown in Table 8-23 of the ZCL spec. This attribute is designed to vary the output of the strobe (i.e., brightness) and not its frequency, which is detailed in section 8.4.2.3.1.6 of the ZCL spec. default: 2 selector: number: @@ -366,71 +252,53 @@ warning_device_warn: mode: box clear_lock_user_code: - name: Clear lock user - description: Clear a user code from a lock target: entity: domain: lock integration: zha fields: code_slot: - name: Code slot - description: Code slot to clear code from required: true example: 1 selector: text: enable_lock_user_code: - name: Enable lock user - description: Enable a user code on a lock target: entity: domain: lock integration: zha fields: code_slot: - name: Code slot - description: Code slot to enable required: true example: 1 selector: text: disable_lock_user_code: - name: Disable lock user - description: Disable a user code on a lock target: entity: domain: lock integration: zha fields: code_slot: - name: Code slot - description: Code slot to disable required: true example: 1 selector: text: set_lock_user_code: - name: Set lock user code - description: Set a user code on a lock target: entity: domain: lock integration: zha fields: code_slot: - name: Code slot - description: Code slot to set the code in required: true example: 1 selector: text: user_code: - name: Code - description: Code to set required: true example: 1234 selector: diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index cbdc9cf8477c3f..efc71df7adc5f0 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -4,7 +4,9 @@ "step": { "choose_serial_port": { "title": "Select a Serial Port", - "data": { "path": "Serial Device Path" }, + "data": { + "path": "Serial Device Path" + }, "description": "Select the serial port for your Zigbee radio" }, "confirm": { @@ -14,7 +16,9 @@ "description": "Do you want to set up {name}?" }, "manual_pick_radio_type": { - "data": { "radio_type": "Radio Type" }, + "data": { + "radio_type": "Radio Type" + }, "title": "Radio Type", "description": "Pick your Zigbee radio type" }, @@ -244,5 +248,259 @@ "face_5": "With face 5 activated", "face_6": "With face 6 activated" } + }, + "services": { + "permit": { + "name": "Permit", + "description": "Allows nodes to join the Zigbee network.", + "fields": { + "duration": { + "name": "Duration", + "description": "Time to permit joins." + }, + "ieee": { + "name": "IEEE", + "description": "IEEE address of the node permitting new joins." + }, + "source_ieee": { + "name": "Source IEEE", + "description": "IEEE address of the joining device (must be used with the install code)." + }, + "install_code": { + "name": "Install code", + "description": "Install code of the joining device (must be used with the source_ieee)." + }, + "qr_code": { + "name": "QR code", + "description": "Value of the QR install code (different between vendors)." + } + } + }, + "remove": { + "name": "Remove", + "description": "Removes a node from the Zigbee network.", + "fields": { + "ieee": { + "name": "[%key:component::zha::services::permit::fields::ieee::name%]", + "description": "IEEE address of the node to remove." + } + } + }, + "reconfigure_device": { + "name": "Reconfigure device", + "description": "Reconfigures a ZHA device (heal device). Use this if you are having issues with the device. If the device in question is a battery-powered device, ensure it is awake and accepting commands when you use this service.", + "fields": { + "ieee": { + "name": "[%key:component::zha::services::permit::fields::ieee::name%]", + "description": "IEEE address of the device to reconfigure." + } + } + }, + "set_zigbee_cluster_attribute": { + "name": "Set zigbee cluster attribute", + "description": "Sets an attribute value for the specified cluster on the specified entity.", + "fields": { + "ieee": { + "name": "[%key:component::zha::services::permit::fields::ieee::name%]", + "description": "IEEE address for the device." + }, + "endpoint_id": { + "name": "Endpoint ID", + "description": "Endpoint ID for the cluster." + }, + "cluster_id": { + "name": "Cluster ID", + "description": "ZCL cluster to retrieve attributes for." + }, + "cluster_type": { + "name": "Cluster Type", + "description": "Type of the cluster." + }, + "attribute": { + "name": "Attribute", + "description": "ID of the attribute to set." + }, + "value": { + "name": "Value", + "description": "Value to write to the attribute." + }, + "manufacturer": { + "name": "Manufacturer", + "description": "Manufacturer code." + } + } + }, + "issue_zigbee_cluster_command": { + "name": "Issue zigbee cluster command", + "description": "Issues a command on the specified cluster on the specified entity.", + "fields": { + "ieee": { + "name": "[%key:component::zha::services::permit::fields::ieee::name%]", + "description": "[%key:component::zha::services::set_zigbee_cluster_attribute::fields::ieee::description%]" + }, + "endpoint_id": { + "name": "[%key:component::zha::services::set_zigbee_cluster_attribute::fields::endpoint_id::name%]", + "description": "[%key:component::zha::services::set_zigbee_cluster_attribute::fields::endpoint_id::description%]" + }, + "cluster_id": { + "name": "[%key:component::zha::services::set_zigbee_cluster_attribute::fields::cluster_id::name%]", + "description": "[%key:component::zha::services::set_zigbee_cluster_attribute::fields::cluster_id::description%]" + }, + "cluster_type": { + "name": "[%key:component::zha::services::set_zigbee_cluster_attribute::fields::cluster_type::name%]", + "description": "[%key:component::zha::services::set_zigbee_cluster_attribute::fields::cluster_type::description%]" + }, + "command": { + "name": "Command", + "description": "ID of the command to execute." + }, + "command_type": { + "name": "Command Type", + "description": "Type of the command to execute." + }, + "args": { + "name": "Args", + "description": "Arguments to pass to the command." + }, + "params": { + "name": "Params", + "description": "Parameters to pass to the command." + }, + "manufacturer": { + "name": "Manufacturer", + "description": "Manufacturer code." + } + } + }, + "issue_zigbee_group_command": { + "name": "Issue zigbee group command", + "description": "Issue command on the specified cluster on the specified group.", + "fields": { + "group": { + "name": "Group", + "description": "Hexadecimal address of the group." + }, + "cluster_id": { + "name": "Cluster ID", + "description": "ZCL cluster to send command to." + }, + "cluster_type": { + "name": "Cluster type", + "description": "Type of the cluster." + }, + "command": { + "name": "Command", + "description": "ID of the command to execute." + }, + "args": { + "name": "Args", + "description": "Arguments to pass to the command." + }, + "manufacturer": { + "name": "Manufacturer", + "description": "Manufacturer code." + } + } + }, + "warning_device_squawk": { + "name": "Warning device squawk", + "description": "This service uses the WD capabilities to emit a quick audible/visible pulse called a \"squawk\". The squawk command has no effect if the WD is currently active (warning in progress).", + "fields": { + "ieee": { + "name": "[%key:component::zha::services::permit::fields::ieee::name%]", + "description": "IEEE address for the device." + }, + "mode": { + "name": "Mode", + "description": "The Squawk Mode field is used as a 4-bit enumeration, and can have one of the values shown in Table 8-24 of the ZCL spec - Squawk Mode Field. The exact operation of each mode (how the WD \u201csquawks\u201d) is implementation specific." + }, + "strobe": { + "name": "Strobe", + "description": "The strobe field is used as a Boolean, and determines if the visual indication is also required in addition to the audible squawk, as shown in Table 8-25 of the ZCL spec - Strobe Bit." + }, + "level": { + "name": "Level", + "description": "The squawk level field is used as a 2-bit enumeration, and determines the intensity of audible squawk sound as shown in Table 8-26 of the ZCL spec - Squawk Level Field Values." + } + } + }, + "warning_device_warn": { + "name": "Warning device warn", + "description": "This service starts the WD operation. The WD alerts the surrounding area by audible (siren) and visual (strobe) signals.", + "fields": { + "ieee": { + "name": "[%key:component::zha::services::permit::fields::ieee::name%]", + "description": "IEEE address for the device." + }, + "mode": { + "name": "Mode", + "description": "The Warning Mode field is used as an 4-bit enumeration, can have one of the values 0-6 defined below in table 8-20 of the ZCL spec. The exact behavior of the WD device in each mode is according to the relevant security standards." + }, + "strobe": { + "name": "Strobe", + "description": "The Strobe field is used as a 2-bit enumeration, and determines if the visual indication is required in addition to the audible siren, as indicated in Table 8-21 of the ZCL spec. \"0\" means no strobe, \"1\" means strobe. If the strobe field is \u201c1\u201d and the Warning Mode is \u201c0\u201d (\u201cStop\u201d) then only the strobe is activated." + }, + "level": { + "name": "Level", + "description": "The Siren Level field is used as a 2-bit enumeration, and indicates the intensity of audible squawk sound as shown in Table 8-22 of the ZCL spec." + }, + "duration": { + "name": "Duration", + "description": "Requested duration of warning, in seconds (16 bit). If both Strobe and Warning Mode are \"0\" this field SHALL be ignored." + }, + "duty_cycle": { + "name": "Duty cycle", + "description": "Indicates the length of the flash cycle. This allows you to vary the flash duration for different alarm types (e.g., fire, police, burglar). The valid range is 0-100 in increments of 10. All other values must be rounded to the nearest valid value. Strobe calculates a duty cycle over a duration of one second. The ON state must precede the OFF state. For example, if Strobe Duty Cycle Field specifies \u201c40,\u201d, then the strobe flashes ON for 4/10ths of a second and then turns OFF for 6/10ths of a second." + }, + "intensity": { + "name": "Intensity", + "description": "Indicates the intensity of the strobe as shown in Table 8-23 of the ZCL spec. This attribute is designed to vary the output of the strobe (i.e., brightness) and not its frequency, which is detailed in section 8.4.2.3.1.6 of the ZCL spec." + } + } + }, + "clear_lock_user_code": { + "name": "Clear lock user", + "description": "Clears a user code from a lock.", + "fields": { + "code_slot": { + "name": "Code slot", + "description": "Code slot to clear code from." + } + } + }, + "enable_lock_user_code": { + "name": "Enable lock user", + "description": "Enables a user code on a lock.", + "fields": { + "code_slot": { + "name": "[%key:component::zha::services::clear_lock_user_code::fields::code_slot::name%]", + "description": "Code slot to enable." + } + } + }, + "disable_lock_user_code": { + "name": "Disable lock user", + "description": "Disables a user code on a lock.", + "fields": { + "code_slot": { + "name": "[%key:component::zha::services::clear_lock_user_code::fields::code_slot::name%]", + "description": "Code slot to disable." + } + } + }, + "set_lock_user_code": { + "name": "Set lock user code", + "description": "Sets a user code on a lock.", + "fields": { + "code_slot": { + "name": "[%key:component::zha::services::clear_lock_user_code::fields::code_slot::name%]", + "description": "Code slot to set the code in." + }, + "user_code": { + "name": "Code", + "description": "Code to set." + } + } + } } } From 400c513209470889ee1d9a5525393bce27fbd2b3 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 0444/1009] 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 597dd8ddb5390f..d14c562bd7652b 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 b64d8601a8aa42..452aab2a887230 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 709d5241ecfbb00490ed27aa012e71bf888acbdf Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 12 Jul 2023 18:52:17 -0400 Subject: [PATCH 0445/1009] Include a warning when changing channels with multi-PAN (#96351) * Inform users of the dangers of changing channels with multi-PAN * Update homeassistant/components/homeassistant_hardware/strings.json Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> * Remove double spaces --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/homeassistant_hardware/strings.json | 3 ++- .../components/homeassistant_sky_connect/strings.json | 3 ++- homeassistant/components/homeassistant_yellow/strings.json | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homeassistant_hardware/strings.json b/homeassistant/components/homeassistant_hardware/strings.json index 06221fc7b97f91..45e85f5a4746ed 100644 --- a/homeassistant/components/homeassistant_hardware/strings.json +++ b/homeassistant/components/homeassistant_hardware/strings.json @@ -22,7 +22,8 @@ "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::title%]", "data": { "channel": "Channel" - } + }, + "description": "Start a channel change for your Zigbee and Thread networks.\n\nNote: this is an advanced operation and can leave your Thread and Zigbee networks inoperable if the new channel is congested. Depending on existing network conditions, many of your devices may not migrate to the new channel and will require re-joining before they start working again. Use with caution.\n\nOnce you selected **Submit**, the channel change starts quietly in the background and will finish after a few minutes. " }, "install_addon": { "title": "The Silicon Labs Multiprotocol add-on installation has started" diff --git a/homeassistant/components/homeassistant_sky_connect/strings.json b/homeassistant/components/homeassistant_sky_connect/strings.json index 047130e787cac1..9bc1a49125b8fc 100644 --- a/homeassistant/components/homeassistant_sky_connect/strings.json +++ b/homeassistant/components/homeassistant_sky_connect/strings.json @@ -21,7 +21,8 @@ "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::change_channel::title%]", "data": { "channel": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::change_channel::data::channel%]" - } + }, + "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::change_channel::description%]" }, "install_addon": { "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_addon::title%]" diff --git a/homeassistant/components/homeassistant_yellow/strings.json b/homeassistant/components/homeassistant_yellow/strings.json index 617e61336a5ef1..644a3c045531c8 100644 --- a/homeassistant/components/homeassistant_yellow/strings.json +++ b/homeassistant/components/homeassistant_yellow/strings.json @@ -21,7 +21,8 @@ "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::change_channel::title%]", "data": { "channel": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::change_channel::data::channel%]" - } + }, + "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::change_channel::description%]" }, "hardware_settings": { "title": "Configure hardware settings", From 70096832267ea523f990cd18fa3f4aef8ee2dace Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jul 2023 14:37:28 -1000 Subject: [PATCH 0446/1009] 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 c9d74f0b30c62a..4cbb9cbe847281 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -93,13 +93,6 @@ async def async_set_dashboard_info( 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 d16bf7c4d0073d..d8732ea0453f0b 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 08af42b00ee3d9b2593a8afda53da7ababd1d76c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jul 2023 14:39:51 -1000 Subject: [PATCH 0447/1009] 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 82ea72281570d0..54f44d0998c546 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1770,7 +1770,7 @@ def supports_response(self, domain: str, service: str) -> SupportsResponse: 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 ab0b4ea32e9610..16a79b3ae12b60 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -701,6 +701,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 674d2e1af4c243..d41b55c0b482b4 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -762,6 +762,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 b367c95c8179dfb02890c782b82bd64414599200 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 13 Jul 2023 04:00:05 +0200 Subject: [PATCH 0448/1009] Add more common translations (#96429) * Add common translations * Add common translations * Add common translations * Add common translations * Add common translations * Add common translations * Add common translations * Add common translations --- .../components/advantage_air/strings.json | 2 +- homeassistant/components/alert/strings.json | 6 ++--- .../components/automation/strings.json | 8 +++--- .../components/bayesian/strings.json | 2 +- homeassistant/components/bond/strings.json | 2 +- homeassistant/components/button/strings.json | 2 +- homeassistant/components/camera/strings.json | 8 +++--- homeassistant/components/climate/strings.json | 4 +-- .../components/color_extractor/strings.json | 2 +- .../components/command_line/strings.json | 2 +- homeassistant/components/cover/strings.json | 8 +++--- homeassistant/components/debugpy/strings.json | 2 +- homeassistant/components/deconz/strings.json | 8 +++--- homeassistant/components/fan/strings.json | 6 ++--- homeassistant/components/ffmpeg/strings.json | 6 ++--- homeassistant/components/filter/strings.json | 2 +- homeassistant/components/generic/strings.json | 2 +- .../generic_thermostat/strings.json | 2 +- .../components/google_mail/strings.json | 2 +- homeassistant/components/group/strings.json | 2 +- homeassistant/components/hassio/strings.json | 2 +- .../components/history_stats/strings.json | 2 +- .../components/homeassistant/strings.json | 4 +-- homeassistant/components/homekit/strings.json | 2 +- homeassistant/components/hue/strings.json | 4 +-- .../components/humidifier/strings.json | 6 ++--- .../components/input_boolean/strings.json | 8 +++--- .../components/input_datetime/strings.json | 2 +- .../components/input_number/strings.json | 2 +- .../components/input_select/strings.json | 2 +- .../components/input_text/strings.json | 2 +- homeassistant/components/insteon/strings.json | 2 +- .../components/intent_script/strings.json | 2 +- homeassistant/components/keba/strings.json | 4 +-- homeassistant/components/knx/strings.json | 2 +- homeassistant/components/lifx/strings.json | 2 +- homeassistant/components/light/strings.json | 6 ++--- .../components/litterrobot/strings.json | 2 +- homeassistant/components/lock/strings.json | 2 +- .../components/media_player/strings.json | 10 +++---- homeassistant/components/min_max/strings.json | 2 +- homeassistant/components/modbus/strings.json | 6 ++--- homeassistant/components/mqtt/strings.json | 6 ++--- homeassistant/components/nuki/strings.json | 2 +- homeassistant/components/nzbget/strings.json | 2 +- homeassistant/components/person/strings.json | 2 +- homeassistant/components/pi_hole/strings.json | 2 +- homeassistant/components/ping/strings.json | 2 +- .../components/profiler/strings.json | 2 +- .../components/python_script/strings.json | 2 +- .../components/recorder/strings.json | 4 +-- homeassistant/components/remote/strings.json | 6 ++--- homeassistant/components/rest/strings.json | 2 +- homeassistant/components/sabnzbd/strings.json | 2 +- homeassistant/components/scene/strings.json | 2 +- .../components/schedule/strings.json | 2 +- homeassistant/components/script/strings.json | 8 +++--- homeassistant/components/siren/strings.json | 6 ++--- homeassistant/components/smtp/strings.json | 2 +- .../components/snapcast/strings.json | 2 +- .../components/statistics/strings.json | 2 +- homeassistant/components/switch/strings.json | 6 ++--- .../components/telegram/strings.json | 2 +- .../components/template/strings.json | 2 +- homeassistant/components/timer/strings.json | 4 +-- .../trafikverket_ferry/strings.json | 14 +++++----- .../trafikverket_train/strings.json | 14 +++++----- homeassistant/components/trend/strings.json | 2 +- .../components/universal/strings.json | 2 +- homeassistant/components/vacuum/strings.json | 10 +++---- .../components/wolflink/strings.json | 6 ++--- homeassistant/components/workday/strings.json | 14 +++++----- homeassistant/components/yamaha/strings.json | 2 +- homeassistant/components/zha/strings.json | 8 +++--- homeassistant/components/zone/strings.json | 2 +- homeassistant/strings.json | 27 +++++++++++++++++++ 76 files changed, 177 insertions(+), 150 deletions(-) diff --git a/homeassistant/components/advantage_air/strings.json b/homeassistant/components/advantage_air/strings.json index 39681201766a2f..de8bde6897e60f 100644 --- a/homeassistant/components/advantage_air/strings.json +++ b/homeassistant/components/advantage_air/strings.json @@ -13,7 +13,7 @@ "port": "[%key:common::config_flow::data::port%]" }, "description": "Connect to the API of your Advantage Air wall mounted tablet.", - "title": "Connect" + "title": "[%key:common::action::connect%]" } } }, diff --git a/homeassistant/components/alert/strings.json b/homeassistant/components/alert/strings.json index 16192d5d59561d..f8c1b2ede72eb9 100644 --- a/homeassistant/components/alert/strings.json +++ b/homeassistant/components/alert/strings.json @@ -12,15 +12,15 @@ }, "services": { "toggle": { - "name": "Toggle", + "name": "[%key:common::action::toggle%]", "description": "Toggles alert's notifications." }, "turn_off": { - "name": "Turn off", + "name": "[%key:common::action::turn_off%]", "description": "Silences alert's notifications." }, "turn_on": { - "name": "Turn on", + "name": "[%key:common::action::turn_on%]", "description": "Resets alert's notifications." } } diff --git a/homeassistant/components/automation/strings.json b/homeassistant/components/automation/strings.json index cfeafa856d2e14..31bd812a947430 100644 --- a/homeassistant/components/automation/strings.json +++ b/homeassistant/components/automation/strings.json @@ -47,11 +47,11 @@ }, "services": { "turn_on": { - "name": "Turn on", + "name": "[%key:common::action::turn_on%]", "description": "Enables an automation." }, "turn_off": { - "name": "Turn off", + "name": "[%key:common::action::turn_off%]", "description": "Disables an automation.", "fields": { "stop_actions": { @@ -61,7 +61,7 @@ } }, "toggle": { - "name": "Toggle", + "name": "[%key:common::action::toggle%]", "description": "Toggles (enable / disable) an automation." }, "trigger": { @@ -75,7 +75,7 @@ } }, "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads the automation configuration." } } diff --git a/homeassistant/components/bayesian/strings.json b/homeassistant/components/bayesian/strings.json index f7c12523b2c8c5..9ebccedc88d77c 100644 --- a/homeassistant/components/bayesian/strings.json +++ b/homeassistant/components/bayesian/strings.json @@ -11,7 +11,7 @@ }, "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads bayesian sensors from the YAML-configuration." } } diff --git a/homeassistant/components/bond/strings.json b/homeassistant/components/bond/strings.json index 9cbd895683c563..04be198d149683 100644 --- a/homeassistant/components/bond/strings.json +++ b/homeassistant/components/bond/strings.json @@ -91,7 +91,7 @@ "description": "Start decreasing the brightness of the light. (deprecated)." }, "stop": { - "name": "Stop", + "name": "[%key:common::action::stop%]", "description": "Stop any in-progress action and empty the queue. (deprecated)." } } diff --git a/homeassistant/components/button/strings.json b/homeassistant/components/button/strings.json index 39456cdf42757a..f552e9ae12b576 100644 --- a/homeassistant/components/button/strings.json +++ b/homeassistant/components/button/strings.json @@ -16,7 +16,7 @@ "name": "Identify" }, "restart": { - "name": "Restart" + "name": "[%key:common::action::restart%]" }, "update": { "name": "Update" diff --git a/homeassistant/components/camera/strings.json b/homeassistant/components/camera/strings.json index ac061194d5cc49..90b053ec0877d4 100644 --- a/homeassistant/components/camera/strings.json +++ b/homeassistant/components/camera/strings.json @@ -25,8 +25,8 @@ "motion_detection": { "name": "Motion detection", "state": { - "true": "Enabled", - "false": "Disabled" + "true": "[%key:common::state::enabled%]", + "false": "[%key:common::state::disabled%]" } }, "model_name": { @@ -37,11 +37,11 @@ }, "services": { "turn_off": { - "name": "Turn off", + "name": "[%key:common::action::turn_off%]", "description": "Turns off the camera." }, "turn_on": { - "name": "Turn on", + "name": "[%key:common::action::turn_on%]", "description": "Turns on the camera." }, "enable_motion_detection": { diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index bfe0f490cda653..c517bfd7a20b06 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -213,11 +213,11 @@ } }, "turn_on": { - "name": "Turn on", + "name": "[%key:common::action::turn_on%]", "description": "Turns climate device on." }, "turn_off": { - "name": "Turn off", + "name": "[%key:common::action::turn_off%]", "description": "Turns climate device off." } }, diff --git a/homeassistant/components/color_extractor/strings.json b/homeassistant/components/color_extractor/strings.json index df720586631552..f56a4e514b7af7 100644 --- a/homeassistant/components/color_extractor/strings.json +++ b/homeassistant/components/color_extractor/strings.json @@ -1,7 +1,7 @@ { "services": { "turn_on": { - "name": "Turn on", + "name": "[%key:common::action::turn_on%]", "description": "Sets the light RGB to the predominant color found in the image provided by URL or file path.", "fields": { "color_extract_url": { diff --git a/homeassistant/components/command_line/strings.json b/homeassistant/components/command_line/strings.json index e249ad877d56e1..9fc0de2ab28908 100644 --- a/homeassistant/components/command_line/strings.json +++ b/homeassistant/components/command_line/strings.json @@ -7,7 +7,7 @@ }, "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads command line configuration from the YAML-configuration." } } diff --git a/homeassistant/components/cover/strings.json b/homeassistant/components/cover/strings.json index 5ed02a84e0df7d..979835fcfd27f1 100644 --- a/homeassistant/components/cover/strings.json +++ b/homeassistant/components/cover/strings.json @@ -79,15 +79,15 @@ }, "services": { "open_cover": { - "name": "Open", + "name": "[%key:common::action::open%]", "description": "Opens a cover." }, "close_cover": { - "name": "Close", + "name": "[%key:common::action::close%]", "description": "Closes a cover." }, "toggle": { - "name": "Toggle", + "name": "[%key:common::action::toggle%]", "description": "Toggles a cover open/closed." }, "set_cover_position": { @@ -101,7 +101,7 @@ } }, "stop_cover": { - "name": "Stop", + "name": "[%key:common::action::stop%]", "description": "Stops the cover movement." }, "open_cover_tilt": { diff --git a/homeassistant/components/debugpy/strings.json b/homeassistant/components/debugpy/strings.json index b03a57a51dcf20..74334a15f4a5f9 100644 --- a/homeassistant/components/debugpy/strings.json +++ b/homeassistant/components/debugpy/strings.json @@ -1,7 +1,7 @@ { "services": { "start": { - "name": "Start", + "name": "[%key:common::action::stop%]", "description": "Starts the Remote Python Debugger." } } diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 448a221b2ca58e..632fe832aa8e55 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -79,14 +79,14 @@ "remote_rotate_from_side_6": "Device rotated from \"side 6\" to \"{subtype}\"" }, "trigger_subtype": { - "turn_on": "Turn on", - "turn_off": "Turn off", + "turn_on": "[%key:common::action::turn_on%]", + "turn_off": "[%key:common::action::turn_off%]", "dim_up": "Dim up", "dim_down": "Dim down", "left": "Left", "right": "Right", - "open": "Open", - "close": "Close", + "open": "[%key:common::action::open%]", + "close": "[%key:common::action::close%]", "both_buttons": "Both buttons", "top_buttons": "Top buttons", "bottom_buttons": "Bottom buttons", diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index d3a06edbee1b23..674dcc2b92ef90 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -75,7 +75,7 @@ } }, "turn_on": { - "name": "Turn on", + "name": "[%key:common::action::turn_on%]", "description": "Turns fan on.", "fields": { "percentage": { @@ -89,7 +89,7 @@ } }, "turn_off": { - "name": "Turn off", + "name": "[%key:common::action::turn_off%]", "description": "Turns fan off." }, "oscillate": { @@ -103,7 +103,7 @@ } }, "toggle": { - "name": "Toggle", + "name": "[%key:common::action::toggle%]", "description": "Toggles the fan on/off." }, "set_direction": { diff --git a/homeassistant/components/ffmpeg/strings.json b/homeassistant/components/ffmpeg/strings.json index 9aaff2d1e9343b..66c1f19de5b285 100644 --- a/homeassistant/components/ffmpeg/strings.json +++ b/homeassistant/components/ffmpeg/strings.json @@ -1,7 +1,7 @@ { "services": { "restart": { - "name": "Restart", + "name": "[%key:common::action::restart%]", "description": "Sends a restart command to a ffmpeg based sensor.", "fields": { "entity_id": { @@ -11,7 +11,7 @@ } }, "start": { - "name": "Start", + "name": "[%key:common::action::start%]", "description": "Sends a start command to a ffmpeg based sensor.", "fields": { "entity_id": { @@ -21,7 +21,7 @@ } }, "stop": { - "name": "Stop", + "name": "[%key:common::action::stop%]", "description": "Sends a stop command to a ffmpeg based sensor.", "fields": { "entity_id": { diff --git a/homeassistant/components/filter/strings.json b/homeassistant/components/filter/strings.json index 078e5b35980def..461eed9aefa182 100644 --- a/homeassistant/components/filter/strings.json +++ b/homeassistant/components/filter/strings.json @@ -1,7 +1,7 @@ { "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads filters from the YAML-configuration." } } diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index d23bb605c7b44e..a1519fa0f48d0e 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -86,7 +86,7 @@ }, "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads generic cameras from the YAML-configuration." } } diff --git a/homeassistant/components/generic_thermostat/strings.json b/homeassistant/components/generic_thermostat/strings.json index f1525b2516da05..8834892b7abf83 100644 --- a/homeassistant/components/generic_thermostat/strings.json +++ b/homeassistant/components/generic_thermostat/strings.json @@ -1,7 +1,7 @@ { "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads generic thermostats from the YAML-configuration." } } diff --git a/homeassistant/components/google_mail/strings.json b/homeassistant/components/google_mail/strings.json index db242479783ad9..83537c6b1de3c5 100644 --- a/homeassistant/components/google_mail/strings.json +++ b/homeassistant/components/google_mail/strings.json @@ -44,7 +44,7 @@ "description": "Sets vacation responder settings for Google Mail.", "fields": { "enabled": { - "name": "Enabled", + "name": "[%key:common::state::enabled%]", "description": "Turn this off to end vacation responses." }, "title": { diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index 7b49eaf4186356..bbf521b06e3968 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -193,7 +193,7 @@ }, "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads group configuration, entities, and notify services from YAML-configuration." }, "set": { diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index fa8fc2d2da84d8..e954c0cccf61fe 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -24,7 +24,7 @@ "fix_menu": { "description": "Could not connect to `{reference}`. Check host logs for errors from the mount service for more details.\n\nUse reload to try to connect again. If you need to update `{reference}`, go to [storage]({storage_url}).", "menu_options": { - "mount_execute_reload": "Reload", + "mount_execute_reload": "[%key:common::action::reload%]", "mount_execute_remove": "Remove" } } diff --git a/homeassistant/components/history_stats/strings.json b/homeassistant/components/history_stats/strings.json index cb4601f2a096b6..ea1c94b6ec3b87 100644 --- a/homeassistant/components/history_stats/strings.json +++ b/homeassistant/components/history_stats/strings.json @@ -1,7 +1,7 @@ { "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads history stats sensors from the YAML-configuration." } } diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 5a02cd196659b4..57cb5c3eb566de 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -56,7 +56,7 @@ "description": "Reloads the core configuration from the YAML-configuration." }, "restart": { - "name": "Restart", + "name": "[%key:common::action::restart%]", "description": "Restarts Home Assistant." }, "set_location": { @@ -74,7 +74,7 @@ } }, "stop": { - "name": "Stop", + "name": "[%key:common::action::stop%]", "description": "Stops Home Assistant." }, "toggle": { diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index 83177345159690..f57536263cafea 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -71,7 +71,7 @@ }, "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads homekit and re-process YAML-configuration." }, "reset_accessory": { diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index 54895b6e3b2435..2c3f493e2c8599 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -44,8 +44,8 @@ "double_buttons_2_4": "Second and Fourth buttons", "dim_down": "Dim down", "dim_up": "Dim up", - "turn_off": "Turn off", - "turn_on": "Turn on", + "turn_off": "[%key:common::action::turn_off%]", + "turn_on": "[%key:common::action::turn_on%]", "1": "First button", "2": "Second button", "3": "Third button", diff --git a/homeassistant/components/humidifier/strings.json b/homeassistant/components/humidifier/strings.json index d3cf946f5bf7cc..19a9a8eab77233 100644 --- a/homeassistant/components/humidifier/strings.json +++ b/homeassistant/components/humidifier/strings.json @@ -97,15 +97,15 @@ } }, "turn_on": { - "name": "Turn on", + "name": "[%key:common::action::turn_on%]", "description": "Turns the humidifier on." }, "turn_off": { - "name": "Turn off", + "name": "[%key:common::action::turn_off%]", "description": "Turns the humidifier off." }, "toggle": { - "name": "Toggle", + "name": "[%key:common::action::toggle%]", "description": "Toggles the humidifier on/off." } } diff --git a/homeassistant/components/input_boolean/strings.json b/homeassistant/components/input_boolean/strings.json index 9288de04f2c1f5..a2087f1247a579 100644 --- a/homeassistant/components/input_boolean/strings.json +++ b/homeassistant/components/input_boolean/strings.json @@ -20,19 +20,19 @@ }, "services": { "toggle": { - "name": "Toggle", + "name": "[%key:common::action::toggle%]", "description": "Toggles the helper on/off." }, "turn_off": { - "name": "Turn off", + "name": "[%key:common::action::turn_off%]", "description": "Turns off the helper." }, "turn_on": { - "name": "Turn on", + "name": "[%key:common::action::turn_on%]", "description": "Turns on the helper." }, "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads helpers from the YAML-configuration." } } diff --git a/homeassistant/components/input_datetime/strings.json b/homeassistant/components/input_datetime/strings.json index f657508a4c4688..e4a2b6349b7ef6 100644 --- a/homeassistant/components/input_datetime/strings.json +++ b/homeassistant/components/input_datetime/strings.json @@ -59,7 +59,7 @@ } }, "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads helpers from the YAML-configuration." } } diff --git a/homeassistant/components/input_number/strings.json b/homeassistant/components/input_number/strings.json index 020544c5d4eaac..8a2351ebad46b4 100644 --- a/homeassistant/components/input_number/strings.json +++ b/homeassistant/components/input_number/strings.json @@ -54,7 +54,7 @@ } }, "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads helpers from the YAML-configuration." } } diff --git a/homeassistant/components/input_select/strings.json b/homeassistant/components/input_select/strings.json index 68970933346509..faa47c979a16e8 100644 --- a/homeassistant/components/input_select/strings.json +++ b/homeassistant/components/input_select/strings.json @@ -67,7 +67,7 @@ } }, "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads helpers from the YAML-configuration." } } diff --git a/homeassistant/components/input_text/strings.json b/homeassistant/components/input_text/strings.json index a4dc6d929f5fed..49eab33848c312 100644 --- a/homeassistant/components/input_text/strings.json +++ b/homeassistant/components/input_text/strings.json @@ -42,7 +42,7 @@ } }, "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads helpers from the YAML-configuration." } } diff --git a/homeassistant/components/insteon/strings.json b/homeassistant/components/insteon/strings.json index 3f3e3df78c7bde..3ba996adff772e 100644 --- a/homeassistant/components/insteon/strings.json +++ b/homeassistant/components/insteon/strings.json @@ -144,7 +144,7 @@ "description": "Name of the device to load. Use \"all\" to load the database of all devices." }, "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads all records. If true the current records are cleared from memory (does not effect the device) and the records are reloaded. If false the existing records are left in place and only missing records are added. Default is false." } } diff --git a/homeassistant/components/intent_script/strings.json b/homeassistant/components/intent_script/strings.json index efd77d225f763b..74ddd45c1af2d4 100644 --- a/homeassistant/components/intent_script/strings.json +++ b/homeassistant/components/intent_script/strings.json @@ -1,7 +1,7 @@ { "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads the intent script from the YAML-configuration." } } diff --git a/homeassistant/components/keba/strings.json b/homeassistant/components/keba/strings.json index 140ab6ea9490ce..ed8594a1068919 100644 --- a/homeassistant/components/keba/strings.json +++ b/homeassistant/components/keba/strings.json @@ -33,11 +33,11 @@ } }, "enable": { - "name": "Enable", + "name": "[%key:common::action::enable%]", "description": "Starts a charging process if charging station is authorized." }, "disable": { - "name": "Disable", + "name": "[%key:common::action::disable%]", "description": "Stops the charging process if charging station is authorized." }, "set_failsafe": { diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index 9a17fed506c652..56ff9018530e3a 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -371,7 +371,7 @@ } }, "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads the KNX integration." } } diff --git a/homeassistant/components/lifx/strings.json b/homeassistant/components/lifx/strings.json index cff9b572cc6285..dfbc0b4e384519 100644 --- a/homeassistant/components/lifx/strings.json +++ b/homeassistant/components/lifx/strings.json @@ -52,7 +52,7 @@ "description": "Controls the HEV LEDs on a LIFX Clean bulb.", "fields": { "power": { - "name": "Enable", + "name": "[%key:common::action::enable%]", "description": "Start or stop a Clean cycle." }, "duration": { diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index a4a46d2ca94140..5398d38ca5d786 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -243,7 +243,7 @@ }, "services": { "turn_on": { - "name": "Turn on", + "name": "[%key:common::action::turn_on%]", "description": "Turn on one or more lights and adjust properties of the light, even when they are turned on already.", "fields": { "transition": { @@ -317,7 +317,7 @@ } }, "turn_off": { - "name": "Turn off", + "name": "[%key:common::action::turn_off%]", "description": "Turn off one or more lights.", "fields": { "transition": { @@ -331,7 +331,7 @@ } }, "toggle": { - "name": "Toggle", + "name": "[%key:common::action::toggle%]", "description": "Toggles one or more lights, from on to off, or, off to on, based on their current state.", "fields": { "transition": { diff --git a/homeassistant/components/litterrobot/strings.json b/homeassistant/components/litterrobot/strings.json index e5cd35703f3edb..fe9cc3b528ac61 100644 --- a/homeassistant/components/litterrobot/strings.json +++ b/homeassistant/components/litterrobot/strings.json @@ -144,7 +144,7 @@ "description": "Sets the sleep mode and start time.", "fields": { "enabled": { - "name": "Enabled", + "name": "[%key:common::state::enabled%]", "description": "Whether sleep mode should be enabled." }, "start_time": { diff --git a/homeassistant/components/lock/strings.json b/homeassistant/components/lock/strings.json index 9e20b0cad2bd5d..d041d6ac61aa01 100644 --- a/homeassistant/components/lock/strings.json +++ b/homeassistant/components/lock/strings.json @@ -47,7 +47,7 @@ } }, "open": { - "name": "Open", + "name": "[%key:common::action::open%]", "description": "Opens a lock.", "fields": { "code": { diff --git a/homeassistant/components/media_player/strings.json b/homeassistant/components/media_player/strings.json index 10148f99fef52c..bcf594a2675abd 100644 --- a/homeassistant/components/media_player/strings.json +++ b/homeassistant/components/media_player/strings.json @@ -162,15 +162,15 @@ }, "services": { "turn_on": { - "name": "Turn on", + "name": "[%key:common::action::turn_on%]", "description": "Turns on the power of the media player." }, "turn_off": { - "name": "Turn off", + "name": "[%key:common::action::turn_off%]", "description": "Turns off the power of the media player." }, "toggle": { - "name": "Toggle", + "name": "[%key:common::action::toggle%]", "description": "Toggles a media player on/off." }, "volume_up": { @@ -210,11 +210,11 @@ "description": "Starts playing." }, "media_pause": { - "name": "Pause", + "name": "[%key:common::action::pause%]", "description": "Pauses." }, "media_stop": { - "name": "Stop", + "name": "[%key:common::action::stop%]", "description": "Stops playing." }, "media_next_track": { diff --git a/homeassistant/components/min_max/strings.json b/homeassistant/components/min_max/strings.json index 464d01b90b4c91..ce18a4d153f014 100644 --- a/homeassistant/components/min_max/strings.json +++ b/homeassistant/components/min_max/strings.json @@ -46,7 +46,7 @@ }, "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads min/max sensors from the YAML-configuration." } } diff --git a/homeassistant/components/modbus/strings.json b/homeassistant/components/modbus/strings.json index ad07a4d7565966..c9cf755ad13416 100644 --- a/homeassistant/components/modbus/strings.json +++ b/homeassistant/components/modbus/strings.json @@ -1,7 +1,7 @@ { "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads all modbus entities." }, "write_coil": { @@ -49,7 +49,7 @@ } }, "stop": { - "name": "Stop", + "name": "[%key:common::action::stop%]", "description": "Stops modbus hub.", "fields": { "hub": { @@ -59,7 +59,7 @@ } }, "restart": { - "name": "Restart", + "name": "[%key:common::action::restart%]", "description": "Restarts modbus hub (if running stop then start).", "fields": { "hub": { diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 61d2b40314b135..ae47b33774d655 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -70,8 +70,8 @@ "button_quintuple_press": "\"{subtype}\" quintuple clicked" }, "trigger_subtype": { - "turn_on": "Turn on", - "turn_off": "Turn off", + "turn_on": "[%key:common::action::turn_on%]", + "turn_off": "[%key:common::action::turn_off%]", "button_1": "First button", "button_2": "Second button", "button_3": "Third button", @@ -188,7 +188,7 @@ } }, "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads MQTT entities from the YAML-configuration." } } diff --git a/homeassistant/components/nuki/strings.json b/homeassistant/components/nuki/strings.json index 68ab508141b378..11c19bbee3ff2b 100644 --- a/homeassistant/components/nuki/strings.json +++ b/homeassistant/components/nuki/strings.json @@ -62,7 +62,7 @@ "description": "Enables or disables continuous mode on Nuki Opener.", "fields": { "enable": { - "name": "Enable", + "name": "[%key:common::action::enable%]", "description": "Whether to enable or disable the feature." } } diff --git a/homeassistant/components/nzbget/strings.json b/homeassistant/components/nzbget/strings.json index 5a96d2f8951cf6..7a3c438d11fcad 100644 --- a/homeassistant/components/nzbget/strings.json +++ b/homeassistant/components/nzbget/strings.json @@ -34,7 +34,7 @@ }, "services": { "pause": { - "name": "Pause", + "name": "[%key:common::action::pause%]", "description": "Pauses download queue." }, "resume": { diff --git a/homeassistant/components/person/strings.json b/homeassistant/components/person/strings.json index 10a982535f2f02..27c41df6b4e046 100644 --- a/homeassistant/components/person/strings.json +++ b/homeassistant/components/person/strings.json @@ -28,7 +28,7 @@ }, "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads persons from the YAML-configuration." } } diff --git a/homeassistant/components/pi_hole/strings.json b/homeassistant/components/pi_hole/strings.json index 1ed271931c36bf..b76b61f1903d0d 100644 --- a/homeassistant/components/pi_hole/strings.json +++ b/homeassistant/components/pi_hole/strings.json @@ -82,7 +82,7 @@ }, "services": { "disable": { - "name": "Disable", + "name": "[%key:common::action::disable%]", "description": "Disables configured Pi-hole(s) for an amount of time.", "fields": { "duration": { diff --git a/homeassistant/components/ping/strings.json b/homeassistant/components/ping/strings.json index 2bd9229b607973..5b5c5da46bc1d3 100644 --- a/homeassistant/components/ping/strings.json +++ b/homeassistant/components/ping/strings.json @@ -1,7 +1,7 @@ { "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads ping sensors from the YAML-configuration." } } diff --git a/homeassistant/components/profiler/strings.json b/homeassistant/components/profiler/strings.json index ee6f215e59bced..7b9f6789c79bc7 100644 --- a/homeassistant/components/profiler/strings.json +++ b/homeassistant/components/profiler/strings.json @@ -11,7 +11,7 @@ }, "services": { "start": { - "name": "Start", + "name": "[%key:common::action::stop%]", "description": "Starts the Profiler.", "fields": { "seconds": { diff --git a/homeassistant/components/python_script/strings.json b/homeassistant/components/python_script/strings.json index 9898a8ad8665eb..ccf1b33c767412 100644 --- a/homeassistant/components/python_script/strings.json +++ b/homeassistant/components/python_script/strings.json @@ -1,7 +1,7 @@ { "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads all available Python scripts." } } diff --git a/homeassistant/components/recorder/strings.json b/homeassistant/components/recorder/strings.json index a55f13b27c4f73..17539387a29582 100644 --- a/homeassistant/components/recorder/strings.json +++ b/homeassistant/components/recorder/strings.json @@ -52,11 +52,11 @@ } }, "disable": { - "name": "Disable", + "name": "[%key:common::action::disable%]", "description": "Stops the recording of events and state changes." }, "enable": { - "name": "Enable", + "name": "[%key:common::action::enable%]", "description": "Starts the recording of events and state changes." } } diff --git a/homeassistant/components/remote/strings.json b/homeassistant/components/remote/strings.json index 14331c5cded7b6..e3df487a57bd3f 100644 --- a/homeassistant/components/remote/strings.json +++ b/homeassistant/components/remote/strings.json @@ -27,7 +27,7 @@ }, "services": { "turn_on": { - "name": "Turn on", + "name": "[%key:common::action::turn_on%]", "description": "Sends the power on command.", "fields": { "activity": { @@ -37,11 +37,11 @@ } }, "toggle": { - "name": "Toggle", + "name": "[%key:common::action::toggle%]", "description": "Toggles a device on/off." }, "turn_off": { - "name": "Turn off", + "name": "[%key:common::action::turn_off%]", "description": "Turns the device off." }, "send_command": { diff --git a/homeassistant/components/rest/strings.json b/homeassistant/components/rest/strings.json index afbab8d8040ab5..d2b15461c9ee66 100644 --- a/homeassistant/components/rest/strings.json +++ b/homeassistant/components/rest/strings.json @@ -1,7 +1,7 @@ { "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads REST entities from the YAML-configuration." } } diff --git a/homeassistant/components/sabnzbd/strings.json b/homeassistant/components/sabnzbd/strings.json index 2989ee5d00be27..5711656ef690e0 100644 --- a/homeassistant/components/sabnzbd/strings.json +++ b/homeassistant/components/sabnzbd/strings.json @@ -16,7 +16,7 @@ }, "services": { "pause": { - "name": "Pause", + "name": "[%key:common::action::pause%]", "description": "Pauses downloads.", "fields": { "api_key": { diff --git a/homeassistant/components/scene/strings.json b/homeassistant/components/scene/strings.json index f4011860c785ca..3bfea1b09e7bcb 100644 --- a/homeassistant/components/scene/strings.json +++ b/homeassistant/components/scene/strings.json @@ -12,7 +12,7 @@ } }, "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads the scenes from the YAML-configuration." }, "apply": { diff --git a/homeassistant/components/schedule/strings.json b/homeassistant/components/schedule/strings.json index aea07cc3ff26c6..a40c5814d36f3b 100644 --- a/homeassistant/components/schedule/strings.json +++ b/homeassistant/components/schedule/strings.json @@ -23,7 +23,7 @@ }, "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads schedules from the YAML-configuration." } } diff --git a/homeassistant/components/script/strings.json b/homeassistant/components/script/strings.json index e4f1b3fcd4f4bb..f2d5997ae9d870 100644 --- a/homeassistant/components/script/strings.json +++ b/homeassistant/components/script/strings.json @@ -34,19 +34,19 @@ }, "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads all the available scripts." }, "turn_on": { - "name": "Turn on", + "name": "[%key:common::action::turn_on%]", "description": "Runs the sequence of actions defined in a script." }, "turn_off": { - "name": "Turn off", + "name": "[%key:common::action::turn_off%]", "description": "Stops a running script." }, "toggle": { - "name": "Toggle", + "name": "[%key:common::action::toggle%]", "description": "Toggle a script. Starts it, if isn't running, stops it otherwise." } } diff --git a/homeassistant/components/siren/strings.json b/homeassistant/components/siren/strings.json index 171a853f74c2fe..90725da9e8f23b 100644 --- a/homeassistant/components/siren/strings.json +++ b/homeassistant/components/siren/strings.json @@ -16,7 +16,7 @@ }, "services": { "turn_on": { - "name": "Turn on", + "name": "[%key:common::action::turn_on%]", "description": "Turns the siren on.", "fields": { "tone": { @@ -34,11 +34,11 @@ } }, "turn_off": { - "name": "Turn off", + "name": "[%key:common::action::turn_off%]", "description": "Turns the siren off." }, "toggle": { - "name": "Toggle", + "name": "[%key:common::action::toggle%]", "description": "Toggles the siren on/off." } } diff --git a/homeassistant/components/smtp/strings.json b/homeassistant/components/smtp/strings.json index 3c72a1a50d1a75..b711c2f2009433 100644 --- a/homeassistant/components/smtp/strings.json +++ b/homeassistant/components/smtp/strings.json @@ -1,7 +1,7 @@ { "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads smtp notify services." } } diff --git a/homeassistant/components/snapcast/strings.json b/homeassistant/components/snapcast/strings.json index 242bf62ab04f8a..0d51c7543f1b3d 100644 --- a/homeassistant/components/snapcast/strings.json +++ b/homeassistant/components/snapcast/strings.json @@ -7,7 +7,7 @@ "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]" }, - "title": "Connect" + "title": "[%key:common::action::connect%]" } }, "abort": { diff --git a/homeassistant/components/statistics/strings.json b/homeassistant/components/statistics/strings.json index 6b2a04a85df497..6d7bda36faecc5 100644 --- a/homeassistant/components/statistics/strings.json +++ b/homeassistant/components/statistics/strings.json @@ -1,7 +1,7 @@ { "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads statistics sensors from the YAML-configuration." } } diff --git a/homeassistant/components/switch/strings.json b/homeassistant/components/switch/strings.json index ae5a3165cd966e..b50709ed76f672 100644 --- a/homeassistant/components/switch/strings.json +++ b/homeassistant/components/switch/strings.json @@ -33,15 +33,15 @@ }, "services": { "turn_on": { - "name": "Turn on", + "name": "[%key:common::action::turn_on%]", "description": "Turns a switch on." }, "turn_off": { - "name": "Turn off", + "name": "[%key:common::action::turn_off%]", "description": "Turns a switch off." }, "toggle": { - "name": "Toggle", + "name": "[%key:common::action::toggle%]", "description": "Toggles a switch on/off." } } diff --git a/homeassistant/components/telegram/strings.json b/homeassistant/components/telegram/strings.json index 9e09a3904cd702..34a98f908dc236 100644 --- a/homeassistant/components/telegram/strings.json +++ b/homeassistant/components/telegram/strings.json @@ -1,7 +1,7 @@ { "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads telegram notify services." } } diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 3222a0f1bdf5fc..fce7129353e50f 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -1,7 +1,7 @@ { "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads template entities from the YAML-configuration." } } diff --git a/homeassistant/components/timer/strings.json b/homeassistant/components/timer/strings.json index e21f0d2ca82484..c52f2627253ba3 100644 --- a/homeassistant/components/timer/strings.json +++ b/homeassistant/components/timer/strings.json @@ -32,7 +32,7 @@ }, "services": { "start": { - "name": "Start", + "name": "[%key:common::action::stop%]", "description": "Starts a timer.", "fields": { "duration": { @@ -42,7 +42,7 @@ } }, "pause": { - "name": "Pause", + "name": "[%key:common::action::pause%]", "description": "Pauses a timer." }, "cancel": { diff --git a/homeassistant/components/trafikverket_ferry/strings.json b/homeassistant/components/trafikverket_ferry/strings.json index 3d84e4480b406e..d98d60f4643965 100644 --- a/homeassistant/components/trafikverket_ferry/strings.json +++ b/homeassistant/components/trafikverket_ferry/strings.json @@ -30,13 +30,13 @@ "selector": { "weekday": { "options": { - "mon": "Monday", - "tue": "Tuesday", - "wed": "Wednesday", - "thu": "Thursday", - "fri": "Friday", - "sat": "Saturday", - "sun": "Sunday" + "mon": "[%key:common::time::monday%]", + "tue": "[%key:common::time::tuesday%]", + "wed": "[%key:common::time::wednesday%]", + "thu": "[%key:common::time::thursday%]", + "fri": "[%key:common::time::friday%]", + "sat": "[%key:common::time::saturday%]", + "sun": "[%key:common::time::sunday%]" } } }, diff --git a/homeassistant/components/trafikverket_train/strings.json b/homeassistant/components/trafikverket_train/strings.json index 6c67d881153a27..0089f6db8fc24c 100644 --- a/homeassistant/components/trafikverket_train/strings.json +++ b/homeassistant/components/trafikverket_train/strings.json @@ -32,13 +32,13 @@ "selector": { "weekday": { "options": { - "mon": "Monday", - "tue": "Tuesday", - "wed": "Wednesday", - "thu": "Thursday", - "fri": "Friday", - "sat": "Saturday", - "sun": "Sunday" + "mon": "[%key:common::time::monday%]", + "tue": "[%key:common::time::tuesday%]", + "wed": "[%key:common::time::wednesday%]", + "thu": "[%key:common::time::thursday%]", + "fri": "[%key:common::time::friday%]", + "sat": "[%key:common::time::saturday%]", + "sun": "[%key:common::time::sunday%]" } } } diff --git a/homeassistant/components/trend/strings.json b/homeassistant/components/trend/strings.json index 1715f019f272d6..6af231bb4c537b 100644 --- a/homeassistant/components/trend/strings.json +++ b/homeassistant/components/trend/strings.json @@ -1,7 +1,7 @@ { "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads trend sensors from the YAML-configuration." } } diff --git a/homeassistant/components/universal/strings.json b/homeassistant/components/universal/strings.json index b440d76ebc23ab..a265a7c204ce67 100644 --- a/homeassistant/components/universal/strings.json +++ b/homeassistant/components/universal/strings.json @@ -1,7 +1,7 @@ { "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads universal media players from the YAML-configuration." } } diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index 3bdf650ddd38d1..9822c2fa8216a5 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -37,15 +37,15 @@ }, "services": { "turn_on": { - "name": "Turn on", + "name": "[%key:common::action::turn_on%]", "description": "Starts a new cleaning task." }, "turn_off": { - "name": "Turn off", + "name": "[%key:common::action::turn_off%]", "description": "Stops the current cleaning task and returns to its dock." }, "stop": { - "name": "Stop", + "name": "[%key:common::action::stop%]", "description": "Stops the current cleaning task." }, "locate": { @@ -57,11 +57,11 @@ "description": "Starts, pauses, or resumes the cleaning task." }, "start": { - "name": "Start", + "name": "[%key:common::action::start%]", "description": "Starts or resumes the cleaning task." }, "pause": { - "name": "Pause", + "name": "[%key:common::action::pause%]", "description": "Pauses the cleaning task." }, "return_to_base": { diff --git a/homeassistant/components/wolflink/strings.json b/homeassistant/components/wolflink/strings.json index c8db962215f7bb..3de74cbbf4c782 100644 --- a/homeassistant/components/wolflink/strings.json +++ b/homeassistant/components/wolflink/strings.json @@ -28,10 +28,10 @@ "sensor": { "state": { "state": { - "ein": "Enabled", + "ein": "[%key:common::state::enabled%]", "deaktiviert": "Inactive", - "aus": "Disabled", - "standby": "Standby", + "aus": "[%key:common::state::disabled%]", + "standby": "[%key:common::state::standby%]", "auto": "Auto", "permanent": "Permanent", "initialisierung": "Initialization", diff --git a/homeassistant/components/workday/strings.json b/homeassistant/components/workday/strings.json index 6ea8348812d1ef..4aaf241536fa7c 100644 --- a/homeassistant/components/workday/strings.json +++ b/homeassistant/components/workday/strings.json @@ -73,13 +73,13 @@ }, "days": { "options": { - "mon": "Monday", - "tue": "Tuesday", - "wed": "Wednesday", - "thu": "Thursday", - "fri": "Friday", - "sat": "Saturday", - "sun": "Sunday", + "mon": "[%key:common::time::monday%]", + "tue": "[%key:common::time::tuesday%]", + "wed": "[%key:common::time::wednesday%]", + "thu": "[%key:common::time::thursday%]", + "fri": "[%key:common::time::friday%]", + "sat": "[%key:common::time::saturday%]", + "sun": "[%key:common::time::sunday%]", "holiday": "Holidays" } } diff --git a/homeassistant/components/yamaha/strings.json b/homeassistant/components/yamaha/strings.json index 0896f43b1b515e..ddfee94aa0449e 100644 --- a/homeassistant/components/yamaha/strings.json +++ b/homeassistant/components/yamaha/strings.json @@ -9,7 +9,7 @@ "description": "Name of port to enable/disable." }, "enabled": { - "name": "Enabled", + "name": "[%key:common::state::enabled%]", "description": "Indicate if port should be enabled or not." } } diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index efc71df7adc5f0..50eadfc6667f83 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -224,14 +224,14 @@ "device_offline": "Device offline" }, "trigger_subtype": { - "turn_on": "Turn on", - "turn_off": "Turn off", + "turn_on": "[%key:common::action::turn_on%]", + "turn_off": "[%key:common::action::turn_off%]", "dim_up": "Dim up", "dim_down": "Dim down", "left": "Left", "right": "Right", - "open": "Open", - "close": "Close", + "open": "[%key:common::action::open%]", + "close": "[%key:common::action::close%]", "both_buttons": "Both buttons", "button": "Button", "button_1": "First button", diff --git a/homeassistant/components/zone/strings.json b/homeassistant/components/zone/strings.json index b2f3b5efffa92e..a17059c5eab295 100644 --- a/homeassistant/components/zone/strings.json +++ b/homeassistant/components/zone/strings.json @@ -1,7 +1,7 @@ { "services": { "reload": { - "name": "Reload", + "name": "[%key:common::action::reload%]", "description": "Reloads zones from the YAML-configuration." } } diff --git a/homeassistant/strings.json b/homeassistant/strings.json index 51a5636092af3e..871e1b4ecbce1c 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -20,6 +20,31 @@ "turn_off": "Turn off {entity_name}" } }, + "action": { + "connect": "Connect", + "disconnect": "Disconnect", + "enable": "Enable", + "disable": "Disable", + "open": "Open", + "close": "Close", + "reload": "Reload", + "restart": "Restart", + "start": "Start", + "stop": "Stop", + "pause": "Pause", + "turn_on": "Turn on", + "turn_off": "Turn off", + "toggle": "Toggle" + }, + "time": { + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday", + "sunday": "Sunday" + }, "state": { "off": "Off", "on": "On", @@ -27,6 +52,8 @@ "no": "No", "open": "Open", "closed": "Closed", + "enabled": "Enabled", + "disabled": "Disabled", "connected": "Connected", "disconnected": "Disconnected", "locked": "Locked", From 127fbded18567f488408384399762aef774249db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 13 Jul 2023 05:04:18 +0300 Subject: [PATCH 0449/1009] Fix huawei_lte suspend_integration service URL description (#96450) Copy-pasto from resume_integration. --- homeassistant/components/huawei_lte/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 6f85187cfeb480..50c57e6db3ec29 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -86,7 +86,7 @@ "fields": { "url": { "name": "URL", - "description": "URL of router to resume integration for; optional when only one is configured." + "description": "URL of router to suspend integration for; optional when only one is configured." } } } From ffe81a97166c31268096682aec36f9a0791c44be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jul 2023 16:46:29 -1000 Subject: [PATCH 0450/1009] Improve ESPHome update platform error reporting (#96455) --- homeassistant/components/esphome/update.py | 65 +++++---- tests/components/esphome/conftest.py | 10 +- tests/components/esphome/test_update.py | 149 +++++++++++++++++++-- 3 files changed, 180 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index 6f51b9df74415a..2ac69c3a22d849 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -14,6 +14,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo @@ -27,6 +28,9 @@ KEY_UPDATE_LOCK = "esphome_update_lock" +_LOGGER = logging.getLogger(__name__) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -109,14 +113,10 @@ def available(self) -> bool: During deep sleep the ESP will not be connectable (by design) and thus, even when unavailable, we'll show it as available. """ - return ( - super().available - and ( - self._entry_data.available - or self._entry_data.expected_disconnect - or self._device_info.has_deep_sleep - ) - and self._device_info.name in self.coordinator.data + return super().available and ( + self._entry_data.available + or self._entry_data.expected_disconnect + or self._device_info.has_deep_sleep ) @property @@ -137,33 +137,26 @@ def release_url(self) -> str | None: """URL to the full release notes of the latest version available.""" return "https://esphome.io/changelog/" + @callback + def _async_static_info_updated(self, _: list[EntityInfo]) -> None: + """Handle static info update.""" + self.async_write_ha_state() + async def async_added_to_hass(self) -> None: """Handle entity added to Home Assistant.""" await super().async_added_to_hass() - - @callback - def _static_info_updated(infos: list[EntityInfo]) -> None: - """Handle static info update.""" - self.async_write_ha_state() - self.async_on_remove( async_dispatcher_connect( self.hass, self._entry_data.signal_static_info_updated, - _static_info_updated, + self._async_static_info_updated, ) ) - - @callback - def _on_device_update() -> None: - """Handle update of device state, like availability.""" - self.async_write_ha_state() - self.async_on_remove( async_dispatcher_connect( self.hass, self._entry_data.signal_device_updated, - _on_device_update, + self.async_write_ha_state, ) ) @@ -172,16 +165,20 @@ async def async_install( ) -> None: """Install an update.""" async with self.hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock()): - device = self.coordinator.data.get(self._device_info.name) + coordinator = self.coordinator + api = coordinator.api + device = coordinator.data.get(self._device_info.name) assert device is not None - if not await self.coordinator.api.compile(device["configuration"]): - logging.getLogger(__name__).error( - "Error compiling %s. Try again in ESPHome dashboard for error", - device["configuration"], - ) - if not await self.coordinator.api.upload(device["configuration"], "OTA"): - logging.getLogger(__name__).error( - "Error OTA updating %s. Try again in ESPHome dashboard for error", - device["configuration"], - ) - await self.coordinator.async_request_refresh() + try: + if not await api.compile(device["configuration"]): + raise HomeAssistantError( + f"Error compiling {device['configuration']}; " + "Try again in ESPHome dashboard for more information." + ) + if not await api.upload(device["configuration"], "OTA"): + raise HomeAssistantError( + f"Error updating {device['configuration']} via OTA; " + "Try again in ESPHome dashboard for more information." + ) + finally: + await self.coordinator.async_request_refresh() diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index f4b3bfa3ec7c83..e809089da11a66 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -15,6 +15,7 @@ ReconnectLogic, UserService, ) +import async_timeout import pytest from zeroconf import Zeroconf @@ -53,6 +54,11 @@ async def load_homeassistant(hass) -> None: assert await async_setup_component(hass, "homeassistant", {}) +@pytest.fixture(autouse=True) +def mock_tts(mock_tts_cache_dir): + """Auto mock the tts cache.""" + + @pytest.fixture def mock_config_entry(hass) -> MockConfigEntry: """Return the default mocked config entry.""" @@ -248,10 +254,10 @@ async def mock_try_connect(self): "homeassistant.components.esphome.manager.ReconnectLogic", MockReconnectLogic ): assert await hass.config_entries.async_setup(entry.entry_id) - await try_connect_done.wait() + async with async_timeout.timeout(2): + await try_connect_done.wait() await hass.async_block_till_done() - return mock_device diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index 53ae72e375e064..bd38f4d330258c 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -1,18 +1,35 @@ """Test ESPHome update entities.""" import asyncio +from collections.abc import Awaitable, Callable import dataclasses from unittest.mock import Mock, patch +from aioesphomeapi import ( + APIClient, + EntityInfo, + EntityState, + UserService, +) import pytest -from homeassistant.components.esphome.dashboard import async_get_dashboard +from homeassistant.components.esphome.dashboard import ( + async_get_dashboard, +) from homeassistant.components.update import UpdateEntityFeature -from homeassistant.const import STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ( + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_send +from .conftest import MockESPHomeDevice -@pytest.fixture(autouse=True) + +@pytest.fixture def stub_reconnect(): """Stub reconnect.""" with patch("homeassistant.components.esphome.manager.ReconnectLogic.start"): @@ -30,7 +47,7 @@ def stub_reconnect(): "configuration": "test.yaml", } ], - "on", + STATE_ON, { "latest_version": "2023.2.0-dev", "installed_version": "1.0.0", @@ -44,7 +61,7 @@ def stub_reconnect(): "current_version": "1.0.0", }, ], - "off", + STATE_OFF, { "latest_version": "1.0.0", "installed_version": "1.0.0", @@ -53,13 +70,14 @@ def stub_reconnect(): ), ( [], - "unavailable", + STATE_UNKNOWN, # dashboard is available but device is unknown {"supported_features": 0}, ), ], ) async def test_update_entity( hass: HomeAssistant, + stub_reconnect, mock_config_entry, mock_device_info, mock_dashboard, @@ -88,6 +106,48 @@ async def test_update_entity( if expected_state != "on": return + # Compile failed, don't try to upload + with patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.compile", return_value=False + ) as mock_compile, patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=True + ) as mock_upload, pytest.raises( + HomeAssistantError, match="compiling" + ): + await hass.services.async_call( + "update", + "install", + {"entity_id": "update.none_firmware"}, + blocking=True, + ) + + assert len(mock_compile.mock_calls) == 1 + assert mock_compile.mock_calls[0][1][0] == "test.yaml" + + assert len(mock_upload.mock_calls) == 0 + + # Compile success, upload fails + with patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.compile", return_value=True + ) as mock_compile, patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=False + ) as mock_upload, pytest.raises( + HomeAssistantError, match="OTA" + ): + await hass.services.async_call( + "update", + "install", + {"entity_id": "update.none_firmware"}, + blocking=True, + ) + + assert len(mock_compile.mock_calls) == 1 + assert mock_compile.mock_calls[0][1][0] == "test.yaml" + + assert len(mock_upload.mock_calls) == 1 + assert mock_upload.mock_calls[0][1][0] == "test.yaml" + + # Everything works with patch( "esphome_dashboard_api.ESPHomeDashboardAPI.compile", return_value=True ) as mock_compile, patch( @@ -109,6 +169,7 @@ async def test_update_entity( async def test_update_static_info( hass: HomeAssistant, + stub_reconnect, mock_config_entry, mock_device_info, mock_dashboard, @@ -155,6 +216,7 @@ async def test_update_static_info( ) async def test_update_device_state_for_availability( hass: HomeAssistant, + stub_reconnect, expected_disconnect_state: tuple[bool, str], mock_config_entry, mock_device_info, @@ -210,7 +272,11 @@ async def test_update_device_state_for_availability( async def test_update_entity_dashboard_not_available_startup( - hass: HomeAssistant, mock_config_entry, mock_device_info, mock_dashboard + hass: HomeAssistant, + stub_reconnect, + mock_config_entry, + mock_device_info, + mock_dashboard, ) -> None: """Test ESPHome update entity when dashboard is not available at startup.""" with patch( @@ -225,6 +291,7 @@ async def test_update_entity_dashboard_not_available_startup( mock_config_entry, "update" ) + # We have a dashboard but it is not available state = hass.states.get("update.none_firmware") assert state is None @@ -239,7 +306,7 @@ async def test_update_entity_dashboard_not_available_startup( await hass.async_block_till_done() state = hass.states.get("update.none_firmware") - assert state.state == "on" + assert state.state == STATE_ON expected_attributes = { "latest_version": "2023.2.0-dev", "installed_version": "1.0.0", @@ -247,3 +314,69 @@ async def test_update_entity_dashboard_not_available_startup( } for key, expected_value in expected_attributes.items(): assert state.attributes.get(key) == expected_value + + +async def test_update_entity_dashboard_discovered_after_startup_but_update_failed( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], + mock_dashboard, +) -> None: + """Test ESPHome update entity when dashboard is discovered after startup and the first update fails.""" + with patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.get_devices", + side_effect=asyncio.TimeoutError, + ): + await async_get_dashboard(hass).async_refresh() + await hass.async_block_till_done() + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + ) + await hass.async_block_till_done() + state = hass.states.get("update.test_firmware") + assert state is None + + await mock_device.mock_disconnect(False) + + mock_dashboard["configured"] = [ + { + "name": "test", + "current_version": "2023.2.0-dev", + "configuration": "test.yaml", + } + ] + # Device goes unavailable, and dashboard becomes available + await async_get_dashboard(hass).async_refresh() + await hass.async_block_till_done() + + state = hass.states.get("update.test_firmware") + assert state is None + + # Finally both are available + await mock_device.mock_connect() + await async_get_dashboard(hass).async_refresh() + await hass.async_block_till_done() + state = hass.states.get("update.test_firmware") + assert state is not None + + +async def test_update_entity_not_present_without_dashboard( + hass: HomeAssistant, stub_reconnect, mock_config_entry, mock_device_info +) -> None: + """Test ESPHome update entity does not get created if there is no dashboard.""" + with patch( + "homeassistant.components.esphome.update.DomainData.get_entry_data", + return_value=Mock(available=True, device_info=mock_device_info), + ): + assert await hass.config_entries.async_forward_entry_setup( + mock_config_entry, "update" + ) + + state = hass.states.get("update.none_firmware") + assert state is None From 52c7ad130d0c65d7b8eb9764ebff59980037936d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 13 Jul 2023 06:34:28 +0200 Subject: [PATCH 0451/1009] Add number entity to gardena (#96430) --- .coveragerc | 1 + .../components/gardena_bluetooth/__init__.py | 2 +- .../components/gardena_bluetooth/number.py | 146 ++++++++++++++++++ .../components/gardena_bluetooth/strings.json | 17 ++ 4 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/gardena_bluetooth/number.py diff --git a/.coveragerc b/.coveragerc index 703523ed3648f8..6b2870e84881af 100644 --- a/.coveragerc +++ b/.coveragerc @@ -408,6 +408,7 @@ omit = homeassistant/components/gardena_bluetooth/__init__.py homeassistant/components/gardena_bluetooth/const.py homeassistant/components/gardena_bluetooth/coordinator.py + homeassistant/components/gardena_bluetooth/number.py homeassistant/components/gardena_bluetooth/switch.py homeassistant/components/gc100/* homeassistant/components/geniushub/* diff --git a/homeassistant/components/gardena_bluetooth/__init__.py b/homeassistant/components/gardena_bluetooth/__init__.py index 05ac16381d1ddd..2954a5fe377029 100644 --- a/homeassistant/components/gardena_bluetooth/__init__.py +++ b/homeassistant/components/gardena_bluetooth/__init__.py @@ -20,7 +20,7 @@ from .const import DOMAIN from .coordinator import Coordinator, DeviceUnavailable -PLATFORMS: list[Platform] = [Platform.SWITCH] +PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SWITCH] LOGGER = logging.getLogger(__name__) TIMEOUT = 20.0 DISCONNECT_DELAY = 5 diff --git a/homeassistant/components/gardena_bluetooth/number.py b/homeassistant/components/gardena_bluetooth/number.py new file mode 100644 index 00000000000000..367e2f727bc8fc --- /dev/null +++ b/homeassistant/components/gardena_bluetooth/number.py @@ -0,0 +1,146 @@ +"""Support for number entities.""" +from __future__ import annotations + +from dataclasses import dataclass, field + +from gardena_bluetooth.const import DeviceConfiguration, Valve +from gardena_bluetooth.parse import ( + CharacteristicInt, + CharacteristicLong, + CharacteristicUInt16, +) + +from homeassistant.components.number import ( + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import Coordinator, GardenaBluetoothEntity + + +@dataclass +class GardenaBluetoothNumberEntityDescription(NumberEntityDescription): + """Description of entity.""" + + char: CharacteristicInt | CharacteristicUInt16 | CharacteristicLong = field( + default_factory=lambda: CharacteristicInt("") + ) + + +DESCRIPTIONS = ( + GardenaBluetoothNumberEntityDescription( + key=Valve.manual_watering_time.uuid, + translation_key="manual_watering_time", + native_unit_of_measurement="s", + mode=NumberMode.BOX, + native_min_value=0.0, + native_max_value=24 * 60 * 60, + native_step=60, + entity_category=EntityCategory.CONFIG, + char=Valve.manual_watering_time, + ), + GardenaBluetoothNumberEntityDescription( + key=Valve.remaining_open_time.uuid, + translation_key="remaining_open_time", + native_unit_of_measurement="s", + native_min_value=0.0, + native_max_value=24 * 60 * 60, + native_step=60.0, + entity_category=EntityCategory.DIAGNOSTIC, + char=Valve.remaining_open_time, + ), + GardenaBluetoothNumberEntityDescription( + key=DeviceConfiguration.rain_pause.uuid, + translation_key="rain_pause", + native_unit_of_measurement="d", + mode=NumberMode.BOX, + native_min_value=0.0, + native_max_value=127.0, + native_step=1.0, + entity_category=EntityCategory.CONFIG, + char=DeviceConfiguration.rain_pause, + ), + GardenaBluetoothNumberEntityDescription( + key=DeviceConfiguration.season_pause.uuid, + translation_key="season_pause", + native_unit_of_measurement="d", + mode=NumberMode.BOX, + native_min_value=0.0, + native_max_value=365.0, + native_step=1.0, + entity_category=EntityCategory.CONFIG, + char=DeviceConfiguration.season_pause, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up entity based on a config entry.""" + coordinator: Coordinator = hass.data[DOMAIN][entry.entry_id] + entities: list[NumberEntity] = [ + GardenaBluetoothNumber(coordinator, description) + for description in DESCRIPTIONS + if description.key in coordinator.characteristics + ] + entities.append(GardenaBluetoothRemainingOpenSetNumber(coordinator)) + async_add_entities(entities) + + +class GardenaBluetoothNumber(GardenaBluetoothEntity, NumberEntity): + """Representation of a number.""" + + entity_description: GardenaBluetoothNumberEntityDescription + + def __init__( + self, + coordinator: Coordinator, + description: GardenaBluetoothNumberEntityDescription, + ) -> None: + """Initialize the number entity.""" + super().__init__(coordinator, {description.key}) + self._attr_unique_id = f"{coordinator.address}-{description.key}" + self.entity_description = description + + def _handle_coordinator_update(self) -> None: + if data := self.coordinator.data.get(self.entity_description.char.uuid): + self._attr_native_value = float(self.entity_description.char.decode(data)) + else: + self._attr_native_value = None + super()._handle_coordinator_update() + + async def async_set_native_value(self, value: float) -> None: + """Set new value.""" + await self.coordinator.write(self.entity_description.char, int(value)) + self.async_write_ha_state() + + +class GardenaBluetoothRemainingOpenSetNumber(GardenaBluetoothEntity, NumberEntity): + """Representation of a entity with remaining time.""" + + _attr_translation_key = "remaining_open_set" + _attr_native_unit_of_measurement = "min" + _attr_mode = NumberMode.BOX + _attr_native_min_value = 0.0 + _attr_native_max_value = 24 * 60 + _attr_native_step = 1.0 + + def __init__( + self, + coordinator: Coordinator, + ) -> None: + """Initialize the remaining time entity.""" + super().__init__(coordinator, {Valve.remaining_open_time.uuid}) + self._attr_unique_id = f"{coordinator.address}-remaining_open_set" + + async def async_set_native_value(self, value: float) -> None: + """Set new value.""" + await self.coordinator.write(Valve.remaining_open_time, int(value * 60)) + self.async_write_ha_state() diff --git a/homeassistant/components/gardena_bluetooth/strings.json b/homeassistant/components/gardena_bluetooth/strings.json index 165e336bbecb60..c7a6e9637dffa0 100644 --- a/homeassistant/components/gardena_bluetooth/strings.json +++ b/homeassistant/components/gardena_bluetooth/strings.json @@ -19,6 +19,23 @@ } }, "entity": { + "number": { + "remaining_open_time": { + "name": "Remaining open time" + }, + "remaining_open_set": { + "name": "Open for" + }, + "manual_watering_time": { + "name": "Manual watering time" + }, + "rain_pause": { + "name": "Rain pause" + }, + "season_pause": { + "name": "Season pause" + } + }, "switch": { "state": { "name": "Open" From bc9b763688777377375a51bbce68451539bee6ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Jul 2023 21:44:27 -1000 Subject: [PATCH 0452/1009] Improve performance of http auth logging (#96464) Avoid the argument lookups when debug logging is not enabled --- homeassistant/components/http/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 77ae80b62ffacd..fc7b3c03abebb2 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -224,7 +224,7 @@ async def auth_middleware( authenticated = True auth_type = "signed request" - if authenticated: + if authenticated and _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug( "Authenticated %s for %s using %s", request.remote, From d025b97bb9317c4068b467fdebc86e4a59ed9ca9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 13 Jul 2023 09:49:05 +0200 Subject: [PATCH 0453/1009] Migrate Z-Wave services to support translations (#96361) * Migrate Z-Wave services to support translations * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Co-authored-by: Martin Hjelmare --- .../components/zwave_js/services.yaml | 84 -------- .../components/zwave_js/strings.json | 188 ++++++++++++++++++ 2 files changed, 188 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index 05e2f8bd9fbbf5..e3d59ff43f75a8 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -1,105 +1,77 @@ # Describes the format for available Z-Wave services clear_lock_usercode: - name: Clear a usercode from a lock - description: Clear a usercode from a lock target: entity: domain: lock integration: zwave_js fields: code_slot: - name: Code slot - description: Code slot to clear code from required: true example: 1 selector: text: set_lock_usercode: - name: Set a usercode on a lock - description: Set a usercode on a lock target: entity: domain: lock integration: zwave_js fields: code_slot: - name: Code slot - description: Code slot to set the code. required: true example: 1 selector: text: usercode: - name: Code - description: Code to set. required: true example: 1234 selector: text: set_config_parameter: - name: Set a Z-Wave device configuration parameter - description: Allow for changing configuration parameters of your Z-Wave devices. target: entity: integration: zwave_js fields: endpoint: - name: Endpoint - description: The configuration parameter's endpoint. example: 1 default: 0 required: false selector: text: parameter: - name: Parameter - description: The (name or id of the) configuration parameter you want to configure. example: Minimum brightness level required: true selector: text: bitmask: - name: Bitmask - description: Target a specific bitmask (see the documentation for more information). advanced: true selector: text: value: - name: Value - description: The new value to set for this configuration parameter. example: 5 required: true selector: text: bulk_set_partial_config_parameters: - name: Bulk set partial configuration parameters for a Z-Wave device (Advanced). - description: Allow for bulk setting partial parameters. Useful when multiple partial parameters have to be set at the same time. target: entity: integration: zwave_js fields: endpoint: - name: Endpoint - description: The configuration parameter's endpoint. example: 1 default: 0 required: false selector: text: parameter: - name: Parameter - description: The id of the configuration parameter you want to configure. example: 9 required: true selector: text: value: - name: Value - description: The new value(s) to set for this configuration parameter. Can either be a raw integer value to represent the bulk change or a mapping where the key is the bitmask (either in hex or integer form) and the value is the new value you want to set for that partial parameter. example: | "0x1": 1 "0x10": 1 @@ -110,12 +82,8 @@ bulk_set_partial_config_parameters: object: refresh_value: - name: Refresh value(s) of a Z-Wave entity - description: Force update value(s) for a Z-Wave entity fields: entity_id: - name: Entities - description: Entities to refresh values for. required: true example: | - sensor.family_room_motion @@ -125,184 +93,132 @@ refresh_value: integration: zwave_js multiple: true refresh_all_values: - name: Refresh all values? - description: Whether to refresh all values (true) or just the primary value (false) default: false selector: boolean: set_value: - name: Set a value on a Z-Wave device (Advanced) - description: Allow for changing any value that Z-Wave JS recognizes on a Z-Wave device. This service has minimal validation so only use this service if you know what you are doing. target: entity: integration: zwave_js fields: command_class: - name: Command Class - description: The ID of the command class for the value. example: 117 required: true selector: text: endpoint: - name: Endpoint - description: The endpoint for the value. example: 1 required: false selector: text: property: - name: Property - description: The ID of the property for the value. example: currentValue required: true selector: text: property_key: - name: Property Key - description: The ID of the property key for the value example: 1 required: false selector: text: value: - name: Value - description: The new value to set. example: "ffbb99" required: true selector: object: options: - name: Options - description: Set value options map. Refer to the Z-Wave JS documentation for more information on what options can be set. required: false selector: object: wait_for_result: - name: Wait for result? - description: Whether or not to wait for a response from the node. If not included in the payload, the integration will decide whether to wait or not. If set to `true`, note that the service call can take a while if setting a value on an asleep battery device. required: false selector: boolean: multicast_set_value: - name: Set a value on multiple Z-Wave devices via multicast (Advanced) - description: Allow for changing any value that Z-Wave JS recognizes on multiple Z-Wave devices using multicast, so all devices receive the message simultaneously. This service has minimal validation so only use this service if you know what you are doing. target: entity: integration: zwave_js fields: broadcast: - name: Broadcast? - description: Whether command should be broadcast to all devices on the network. example: true required: false selector: boolean: command_class: - name: Command Class - description: The ID of the command class for the value. example: 117 required: true selector: text: endpoint: - name: Endpoint - description: The endpoint for the value. example: 1 required: false selector: text: property: - name: Property - description: The ID of the property for the value. example: currentValue required: true selector: text: property_key: - name: Property Key - description: The ID of the property key for the value example: 1 required: false selector: text: options: - name: Options - description: Set value options map. Refer to the Z-Wave JS documentation for more information on what options can be set. required: false selector: object: value: - name: Value - description: The new value to set. example: "ffbb99" required: true selector: object: ping: - name: Ping a node - description: Forces Z-Wave JS to try to reach a node. This can be used to update the status of the node in Z-Wave JS when you think it doesn't accurately reflect reality, e.g. reviving a failed/dead node or marking the node as asleep. target: entity: integration: zwave_js reset_meter: - name: Reset meter(s) on a node - description: Resets the meter(s) on a node. target: entity: domain: sensor integration: zwave_js fields: meter_type: - name: Meter Type - description: The type of meter to reset. Not all meters support the ability to pick a meter type to reset. example: 1 required: false selector: text: value: - name: Target Value - description: The value that meter(s) should be reset to. Not all meters support the ability to be reset to a specific value. example: 5 required: false selector: text: invoke_cc_api: - name: Invoke a Command Class API on a node (Advanced) - description: Allows for calling a Command Class API on a node. Some Command Classes can't be fully controlled via the `set_value` service and require direct calls to the Command Class API. target: entity: integration: zwave_js fields: command_class: - name: Command Class - description: The ID of the command class that you want to issue a command to. example: 132 required: true selector: text: endpoint: - name: Endpoint - description: The endpoint to call the API on. If an endpoint is specified, that endpoint will be targeted for all nodes associated with the target areas, devices, and/or entities. If an endpoint is not specified, the root endpoint (0) will be targeted for nodes associated with target areas and devices, and the endpoint for the primary value of each entity will be targeted. example: 1 required: false selector: text: method_name: - name: Method Name - description: The name of the API method to call. Refer to the Z-Wave JS Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for available methods. example: setInterval required: true selector: text: parameters: - name: Parameters - description: A list of parameters to pass to the API method. Refer to the Z-Wave JS Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for parameters. example: "[1, 1]" required: true selector: diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 0bcb209a760f75..37b4577e5dfc10 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -151,5 +151,193 @@ "title": "Newer version of Z-Wave JS Server needed", "description": "The version of Z-Wave JS Server you are currently running is too old for this version of Home Assistant. Please update the Z-Wave JS Server to the latest version to fix this issue." } + }, + "services": { + "clear_lock_usercode": { + "name": "Clear lock user code", + "description": "Clears a user code from a lock.", + "fields": { + "code_slot": { + "name": "Code slot", + "description": "Code slot to clear code from." + } + } + }, + "set_lock_usercode": { + "name": "Set lock user code", + "description": "Sets a user code on a lock.", + "fields": { + "code_slot": { + "name": "[%key:component::zwave_js::services::clear_lock_usercode::fields::code_slot::name%]", + "description": "Code slot to set the code." + }, + "usercode": { + "name": "Code", + "description": "Lock code to set." + } + } + }, + "set_config_parameter": { + "name": "Set device configuration parameter", + "description": "Changes the configuration parameters of your Z-Wave devices.", + "fields": { + "endpoint": { + "name": "Endpoint", + "description": "The configuration parameter's endpoint." + }, + "parameter": { + "name": "Parameter", + "description": "The name (or ID) of the configuration parameter you want to configure." + }, + "bitmask": { + "name": "Bitmask", + "description": "Target a specific bitmask (see the documentation for more information)." + }, + "value": { + "name": "Value", + "description": "The new value to set for this configuration parameter." + } + } + }, + "bulk_set_partial_config_parameters": { + "name": "Bulk set partial configuration parameters (advanced).", + "description": "Allows for bulk setting partial parameters. Useful when multiple partial parameters have to be set at the same time.", + "fields": { + "endpoint": { + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]", + "description": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::description%]" + }, + "parameter": { + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::parameter::name%]", + "description": "[%key:component::zwave_js::services::set_config_parameter::fields::parameter::description%]" + }, + "value": { + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]", + "description": "The new value(s) to set for this configuration parameter. Can either be a raw integer value to represent the bulk change or a mapping where the key is the bitmask (either in hex or integer form) and the value is the new value you want to set for that partial parameter." + } + } + }, + "refresh_value": { + "name": "Refresh values", + "description": "Force updates the values of a Z-Wave entity.", + "fields": { + "entity_id": { + "name": "Entities", + "description": "Entities to refresh." + }, + "refresh_all_values": { + "name": "Refresh all values?", + "description": "Whether to refresh all values (true) or just the primary value (false)." + } + } + }, + "set_value": { + "name": "Set a value (advanced)", + "description": "Changes any value that Z-Wave JS recognizes on a Z-Wave device. This service has minimal validation so only use this service if you know what you are doing.", + "fields": { + "command_class": { + "name": "Command class", + "description": "The ID of the command class for the value." + }, + "endpoint": { + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]", + "description": "The endpoint for the value." + }, + "property": { + "name": "Property", + "description": "The ID of the property for the value." + }, + "property_key": { + "name": "Property key", + "description": "The ID of the property key for the value." + }, + "value": { + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]", + "description": "The new value to set." + }, + "options": { + "name": "Options", + "description": "Set value options map. Refer to the Z-Wave JS documentation for more information on what options can be set." + }, + "wait_for_result": { + "name": "Wait for result?", + "description": "Whether or not to wait for a response from the node. If not included in the payload, the integration will decide whether to wait or not. If set to `true`, note that the service call can take a while if setting a value on an asleep battery device." + } + } + }, + "multicast_set_value": { + "name": "Set a value on multiple devices via multicast (advanced)", + "description": "Changes any value that Z-Wave JS recognizes on multiple Z-Wave devices using multicast, so all devices receive the message simultaneously. This service has minimal validation so only use this service if you know what you are doing.", + "fields": { + "broadcast": { + "name": "Broadcast?", + "description": "Whether command should be broadcast to all devices on the network." + }, + "command_class": { + "name": "[%key:component::zwave_js::services::set_value::fields::command_class::name%]", + "description": "[%key:component::zwave_js::services::set_value::fields::command_class::description%]" + }, + "endpoint": { + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]", + "description": "[%key:component::zwave_js::services::set_value::fields::endpoint::description%]" + }, + "property": { + "name": "[%key:component::zwave_js::services::set_value::fields::property::name%]", + "description": "[%key:component::zwave_js::services::set_value::fields::property::description%]" + }, + "property_key": { + "name": "[%key:component::zwave_js::services::set_value::fields::property_key::name%]", + "description": "[%key:component::zwave_js::services::set_value::fields::property_key::description%]" + }, + "options": { + "name": "[%key:component::zwave_js::services::set_value::fields::options::name%]", + "description": "[%key:component::zwave_js::services::set_value::fields::options::description%]" + }, + "value": { + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::value::name%]", + "description": "[%key:component::zwave_js::services::set_value::fields::value::description%]" + } + } + }, + "ping": { + "name": "Ping a node", + "description": "Forces Z-Wave JS to try to reach a node. This can be used to update the status of the node in Z-Wave JS when you think it doesn't accurately reflect reality, e.g. reviving a failed/dead node or marking the node as asleep." + }, + "reset_meter": { + "name": "Reset meters on a node", + "description": "Resets the meters on a node.", + "fields": { + "meter_type": { + "name": "Meter type", + "description": "The type of meter to reset. Not all meters support the ability to pick a meter type to reset." + }, + "value": { + "name": "Target value", + "description": "The value that meters should be reset to. Not all meters support the ability to be reset to a specific value." + } + } + }, + "invoke_cc_api": { + "name": "Invoke a Command Class API on a node (advanced)", + "description": "Calls a Command Class API on a node. Some Command Classes can't be fully controlled via the `set_value` service and require direct calls to the Command Class API.", + "fields": { + "command_class": { + "name": "[%key:component::zwave_js::services::set_value::fields::command_class::name%]", + "description": "The ID of the command class that you want to issue a command to." + }, + "endpoint": { + "name": "[%key:component::zwave_js::services::set_config_parameter::fields::endpoint::name%]", + "description": "The endpoint to call the API on. If an endpoint is specified, that endpoint will be targeted for all nodes associated with the target areas, devices, and/or entities. If an endpoint is not specified, the root endpoint (0) will be targeted for nodes associated with target areas and devices, and the endpoint for the primary value of each entity will be targeted." + }, + "method_name": { + "name": "Method name", + "description": "The name of the API method to call. Refer to the Z-Wave JS Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for available methods." + }, + "parameters": { + "name": "Parameters", + "description": "A list of parameters to pass to the API method. Refer to the Z-Wave JS Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for parameters." + } + } + } } } From b8bc958070a152d354fb0dabb30e888cc2f79056 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 13 Jul 2023 15:05:55 +0200 Subject: [PATCH 0454/1009] Use device class translations in airvisual pro (#96472) --- homeassistant/components/airvisual_pro/sensor.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/homeassistant/components/airvisual_pro/sensor.py b/homeassistant/components/airvisual_pro/sensor.py index 59de2ae630cefd..5f64e38c4a3c0a 100644 --- a/homeassistant/components/airvisual_pro/sensor.py +++ b/homeassistant/components/airvisual_pro/sensor.py @@ -43,7 +43,6 @@ class AirVisualProMeasurementDescription( SENSOR_DESCRIPTIONS = ( AirVisualProMeasurementDescription( key="air_quality_index", - name="Air quality index", device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda settings, status, measurements: measurements[ @@ -52,7 +51,6 @@ class AirVisualProMeasurementDescription( ), AirVisualProMeasurementDescription( key="battery_level", - name="Battery", device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, @@ -60,7 +58,6 @@ class AirVisualProMeasurementDescription( ), AirVisualProMeasurementDescription( key="carbon_dioxide", - name="C02", device_class=SensorDeviceClass.CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, @@ -68,7 +65,6 @@ class AirVisualProMeasurementDescription( ), AirVisualProMeasurementDescription( key="humidity", - name="Humidity", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, value_fn=lambda settings, status, measurements: measurements["humidity"], @@ -91,7 +87,6 @@ class AirVisualProMeasurementDescription( ), AirVisualProMeasurementDescription( key="particulate_matter_2_5", - name="PM 2.5", device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, @@ -99,7 +94,6 @@ class AirVisualProMeasurementDescription( ), AirVisualProMeasurementDescription( key="temperature", - name="Temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -107,7 +101,6 @@ class AirVisualProMeasurementDescription( ), AirVisualProMeasurementDescription( key="voc", - name="VOC", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, From c54ceb2da21fe0595326923cfdab65206f8a8f57 Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Thu, 13 Jul 2023 17:03:26 +0200 Subject: [PATCH 0455/1009] ImageEntity split load_image_from_url (#96146) * Initial commit * fix async_load_image_from_url --- homeassistant/components/image/__init__.py | 37 +++++++++++++--------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index 8daea2cdd46051..e4bc1664fd9369 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -167,18 +167,14 @@ def image(self) -> bytes | None: """Return bytes of image.""" raise NotImplementedError() - async def _async_load_image_from_url(self, url: str) -> Image | None: - """Load an image by url.""" + async def _fetch_url(self, url: str) -> httpx.Response | None: + """Fetch a URL.""" try: response = await self._client.get( url, timeout=GET_IMAGE_TIMEOUT, follow_redirects=True ) response.raise_for_status() - content_type = response.headers.get("content-type") - return Image( - content=response.content, - content_type=valid_image_content_type(content_type), - ) + return response except httpx.TimeoutException: _LOGGER.error("%s: Timeout getting image from %s", self.entity_id, url) return None @@ -190,14 +186,25 @@ async def _async_load_image_from_url(self, url: str) -> Image | None: err, ) return None - except ImageContentTypeError: - _LOGGER.error( - "%s: Image from %s has invalid content type: %s", - self.entity_id, - url, - content_type, - ) - return None + + async def _async_load_image_from_url(self, url: str) -> Image | None: + """Load an image by url.""" + if response := await self._fetch_url(url): + content_type = response.headers.get("content-type") + try: + return Image( + content=response.content, + content_type=valid_image_content_type(content_type), + ) + except ImageContentTypeError: + _LOGGER.error( + "%s: Image from %s has invalid content type: %s", + self.entity_id, + url, + content_type, + ) + return None + return None async def async_image(self) -> bytes | None: """Return bytes of image.""" From 7859be6481f67384890ff2de44ba4016bec03b17 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 13 Jul 2023 11:52:50 -0400 Subject: [PATCH 0456/1009] Add deduplicate translations script (#96384) * Add deduplicate script * Fix forecast_solar incorrect key with space * Fix utf-8 * Do not create references to other arbitrary other integrations * Add commented code to only allow applying to referencing integrations * Tweak * Bug fix * Add command line arg for limit reference * never suggest to update common keys * Output of script * Apply suggestions from code review Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --------- Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- homeassistant/components/abode/strings.json | 2 +- homeassistant/components/adguard/strings.json | 10 +- .../components/aftership/strings.json | 4 +- homeassistant/components/airly/strings.json | 2 +- .../components/airvisual/strings.json | 4 +- .../components/alarmdecoder/strings.json | 8 +- .../components/amberelectric/strings.json | 2 +- .../components/androidtv/strings.json | 10 +- homeassistant/components/anova/strings.json | 2 +- .../components/apple_tv/strings.json | 2 +- .../components/aussie_broadband/strings.json | 4 +- .../components/azure_devops/strings.json | 2 +- homeassistant/components/baf/strings.json | 2 +- homeassistant/components/blink/strings.json | 4 +- .../components/bluetooth/strings.json | 2 +- homeassistant/components/bond/strings.json | 6 +- .../components/braviatv/strings.json | 4 +- homeassistant/components/brother/strings.json | 2 +- homeassistant/components/browser/strings.json | 2 +- .../components/buienradar/strings.json | 2 +- homeassistant/components/cast/strings.json | 10 +- .../components/co2signal/strings.json | 10 +- .../components/color_extractor/strings.json | 4 +- .../components/cpuspeed/strings.json | 2 +- .../components/crownstone/strings.json | 12 +- homeassistant/components/deconz/strings.json | 10 +- homeassistant/components/demo/strings.json | 2 +- .../components/derivative/strings.json | 2 +- .../devolo_home_control/strings.json | 4 +- homeassistant/components/discord/strings.json | 2 +- .../components/dlna_dmr/strings.json | 2 +- homeassistant/components/dnsip/strings.json | 4 +- .../components/downloader/strings.json | 2 +- homeassistant/components/dsmr/strings.json | 8 +- .../components/dynalite/strings.json | 8 +- homeassistant/components/ecobee/strings.json | 6 +- .../components/eight_sleep/strings.json | 2 +- homeassistant/components/elkm1/strings.json | 14 +- homeassistant/components/elmax/strings.json | 2 +- homeassistant/components/enocean/strings.json | 2 +- homeassistant/components/esphome/strings.json | 2 +- homeassistant/components/evohome/strings.json | 2 +- homeassistant/components/facebox/strings.json | 2 +- .../components/flux_led/strings.json | 4 +- .../components/forecast_solar/strings.json | 8 +- homeassistant/components/fritz/strings.json | 14 +- .../components/fully_kiosk/strings.json | 2 +- .../components/gardena_bluetooth/strings.json | 2 +- .../components/geniushub/strings.json | 6 +- homeassistant/components/google/strings.json | 10 +- .../strings.json | 2 +- .../components/google_mail/strings.json | 2 +- homeassistant/components/group/strings.json | 16 +- .../components/growatt_server/strings.json | 42 +-- .../components/guardian/strings.json | 14 +- .../components/habitica/strings.json | 8 +- homeassistant/components/harmony/strings.json | 2 +- homeassistant/components/hassio/strings.json | 6 +- .../components/hdmi_cec/strings.json | 4 +- homeassistant/components/heos/strings.json | 4 +- .../components/here_travel_time/strings.json | 12 +- homeassistant/components/hive/strings.json | 6 +- .../components/home_connect/strings.json | 36 +-- .../components/homeassistant/strings.json | 4 +- .../homekit_controller/strings.json | 8 +- .../components/homematic/strings.json | 16 +- .../components/homematicip_cloud/strings.json | 20 +- .../components/huawei_lte/strings.json | 8 +- homeassistant/components/hue/strings.json | 16 +- .../hunterdouglas_powerview/strings.json | 2 +- .../components/hvv_departures/strings.json | 2 +- homeassistant/components/icloud/strings.json | 6 +- homeassistant/components/ifttt/strings.json | 4 +- homeassistant/components/ihc/strings.json | 24 +- homeassistant/components/insteon/strings.json | 22 +- .../components/integration/strings.json | 2 +- homeassistant/components/iperf3/strings.json | 2 +- homeassistant/components/ipp/strings.json | 2 +- homeassistant/components/isy994/strings.json | 12 +- homeassistant/components/izone/strings.json | 4 +- .../components/jvc_projector/strings.json | 2 +- .../components/kaleidescape/strings.json | 2 +- homeassistant/components/kef/strings.json | 18 +- .../components/keymitt_ble/strings.json | 4 +- homeassistant/components/knx/strings.json | 20 +- .../components/konnected/strings.json | 4 +- .../components/lametric/strings.json | 4 +- homeassistant/components/lastfm/strings.json | 6 +- homeassistant/components/lcn/strings.json | 50 +-- homeassistant/components/lifx/strings.json | 16 +- .../components/litterrobot/strings.json | 2 +- .../components/local_ip/strings.json | 2 +- homeassistant/components/logbook/strings.json | 2 +- .../components/logi_circle/strings.json | 4 +- .../components/lovelace/strings.json | 2 +- .../components/lutron_caseta/strings.json | 4 +- homeassistant/components/matter/strings.json | 2 +- homeassistant/components/mazda/strings.json | 4 +- .../components/meteo_france/strings.json | 2 +- .../components/microsoft_face/strings.json | 12 +- homeassistant/components/min_max/strings.json | 4 +- homeassistant/components/minio/strings.json | 14 +- homeassistant/components/mjpeg/strings.json | 4 +- homeassistant/components/modbus/strings.json | 18 +- .../components/modem_callerid/strings.json | 2 +- .../components/modern_forms/strings.json | 4 +- .../components/monoprice/strings.json | 12 +- homeassistant/components/mqtt/strings.json | 2 +- .../components/mysensors/strings.json | 30 +- homeassistant/components/nest/strings.json | 12 +- homeassistant/components/netatmo/strings.json | 6 +- .../components/netgear_lte/strings.json | 8 +- .../components/nibe_heatpump/strings.json | 2 +- homeassistant/components/nina/strings.json | 18 +- .../components/nissan_leaf/strings.json | 4 +- homeassistant/components/nx584/strings.json | 2 +- homeassistant/components/ombi/strings.json | 10 +- homeassistant/components/onewire/strings.json | 2 +- .../components/opentherm_gw/strings.json | 44 +-- .../components/openweathermap/strings.json | 2 +- homeassistant/components/overkiz/strings.json | 2 +- .../persistent_notification/strings.json | 2 +- .../components/profiler/strings.json | 2 +- .../components/prusalink/strings.json | 4 +- .../components/purpleair/strings.json | 2 +- homeassistant/components/qnap/strings.json | 6 +- homeassistant/components/qvr_pro/strings.json | 2 +- homeassistant/components/rachio/strings.json | 4 +- .../components/rainbird/strings.json | 2 +- .../components/rainmachine/strings.json | 22 +- .../components/recorder/strings.json | 2 +- .../components/remember_the_milk/strings.json | 2 +- homeassistant/components/renault/strings.json | 10 +- homeassistant/components/rfxtrx/strings.json | 4 +- .../components/roborock/strings.json | 12 +- .../components/rtsp_to_webrtc/strings.json | 4 +- homeassistant/components/sabnzbd/strings.json | 4 +- .../components/screenlogic/strings.json | 4 +- homeassistant/components/sfr_box/strings.json | 2 +- homeassistant/components/shelly/strings.json | 4 +- .../components/shopping_list/strings.json | 10 +- .../components/simplisafe/strings.json | 4 +- .../components/smarttub/strings.json | 4 +- homeassistant/components/sms/strings.json | 30 +- homeassistant/components/snips/strings.json | 10 +- homeassistant/components/snooz/strings.json | 2 +- homeassistant/components/songpal/strings.json | 2 +- homeassistant/components/sonos/strings.json | 4 +- .../components/soundtouch/strings.json | 6 +- homeassistant/components/spotify/strings.json | 6 +- .../components/squeezebox/strings.json | 4 +- .../components/starline/strings.json | 2 +- .../components/starlink/strings.json | 4 +- .../components/steamist/strings.json | 2 +- homeassistant/components/subaru/strings.json | 6 +- .../components/surepetcare/strings.json | 2 +- .../components/synology_dsm/strings.json | 2 +- .../components/system_bridge/strings.json | 14 +- .../components/tankerkoenig/strings.json | 2 +- .../components/telegram_bot/strings.json | 288 +++++++++--------- .../components/threshold/strings.json | 2 +- homeassistant/components/tile/strings.json | 2 +- homeassistant/components/tod/strings.json | 2 +- homeassistant/components/tplink/strings.json | 10 +- .../components/transmission/strings.json | 18 +- homeassistant/components/tuya/strings.json | 12 +- homeassistant/components/unifi/strings.json | 2 +- .../components/unifiprotect/strings.json | 12 +- homeassistant/components/upb/strings.json | 18 +- homeassistant/components/upnp/strings.json | 2 +- .../components/uptimerobot/strings.json | 2 +- .../components/utility_meter/strings.json | 2 +- homeassistant/components/vallox/strings.json | 4 +- homeassistant/components/velbus/strings.json | 12 +- homeassistant/components/vera/strings.json | 4 +- .../components/verisure/strings.json | 4 +- homeassistant/components/vizio/strings.json | 2 +- homeassistant/components/vulcan/strings.json | 6 +- homeassistant/components/webostv/strings.json | 6 +- .../components/whirlpool/strings.json | 2 +- homeassistant/components/wiz/strings.json | 2 +- .../components/wolflink/strings.json | 4 +- homeassistant/components/workday/strings.json | 4 +- .../components/xiaomi_aqara/strings.json | 12 +- .../components/xiaomi_miio/strings.json | 40 +-- .../components/yale_smart_alarm/strings.json | 2 +- homeassistant/components/yamaha/strings.json | 2 +- .../components/yamaha_musiccast/strings.json | 4 +- .../components/yeelight/strings.json | 22 +- homeassistant/components/youtube/strings.json | 4 +- homeassistant/components/zamg/strings.json | 2 +- homeassistant/components/zha/strings.json | 36 +-- .../components/zoneminder/strings.json | 2 +- .../components/zwave_js/strings.json | 38 +-- .../components/zwave_me/strings.json | 2 +- script/translations/deduplicate.py | 131 ++++++++ script/translations/develop.py | 3 +- script/translations/util.py | 10 +- 198 files changed, 1004 insertions(+), 846 deletions(-) create mode 100644 script/translations/deduplicate.py diff --git a/homeassistant/components/abode/strings.json b/homeassistant/components/abode/strings.json index c0c32d487941b2..4b98b69eb19f02 100644 --- a/homeassistant/components/abode/strings.json +++ b/homeassistant/components/abode/strings.json @@ -15,7 +15,7 @@ } }, "reauth_confirm": { - "title": "Fill in your Abode login information", + "title": "[%key:component::abode::config::step::user::title%]", "data": { "username": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" diff --git a/homeassistant/components/adguard/strings.json b/homeassistant/components/adguard/strings.json index 95ce968a67f778..e34a7c88229926 100644 --- a/homeassistant/components/adguard/strings.json +++ b/homeassistant/components/adguard/strings.json @@ -79,11 +79,11 @@ "description": "Add a new filter subscription to AdGuard Home.", "fields": { "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "The name of the filter subscription." }, "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "The filter URL to subscribe to, containing the filter rules." } } @@ -93,7 +93,7 @@ "description": "Removes a filter subscription from AdGuard Home.", "fields": { "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "The filter subscription URL to remove." } } @@ -103,7 +103,7 @@ "description": "Enables a filter subscription in AdGuard Home.", "fields": { "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "The filter subscription URL to enable." } } @@ -113,7 +113,7 @@ "description": "Disables a filter subscription in AdGuard Home.", "fields": { "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "The filter subscription URL to disable." } } diff --git a/homeassistant/components/aftership/strings.json b/homeassistant/components/aftership/strings.json index 602138e82f58aa..a7ccdd48202176 100644 --- a/homeassistant/components/aftership/strings.json +++ b/homeassistant/components/aftership/strings.json @@ -23,11 +23,11 @@ "description": "Removes a tracking number from Aftership.", "fields": { "tracking_number": { - "name": "Tracking number", + "name": "[%key:component::aftership::services::add_tracking::fields::tracking_number::name%]", "description": "Tracking number of the tracking to remove." }, "slug": { - "name": "Slug", + "name": "[%key:component::aftership::services::add_tracking::fields::slug::name%]", "description": "Slug (carrier) of the tracking to remove." } } diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json index 7ec58ccd8e51be..33ee8bbe4c95f4 100644 --- a/homeassistant/components/airly/strings.json +++ b/homeassistant/components/airly/strings.json @@ -17,7 +17,7 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_location%]", - "wrong_location": "No Airly measuring stations in this area." + "wrong_location": "[%key:component::airly::config::error::wrong_location%]" } }, "system_health": { diff --git a/homeassistant/components/airvisual/strings.json b/homeassistant/components/airvisual/strings.json index 0ba99c0984a575..397a41bf24b22e 100644 --- a/homeassistant/components/airvisual/strings.json +++ b/homeassistant/components/airvisual/strings.json @@ -11,7 +11,7 @@ } }, "geography_by_name": { - "title": "Configure a Geography", + "title": "[%key:component::airvisual::config::step::geography_by_coords::title%]", "description": "Use the AirVisual cloud API to monitor a city/state/country.", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", @@ -45,7 +45,7 @@ "options": { "step": { "init": { - "title": "Configure AirVisual", + "title": "[%key:component::airvisual::config::step::user::title%]", "data": { "show_on_map": "Show monitored geography on the map" } diff --git a/homeassistant/components/alarmdecoder/strings.json b/homeassistant/components/alarmdecoder/strings.json index 585db4b1fa32e7..d7ac882bb827d3 100644 --- a/homeassistant/components/alarmdecoder/strings.json +++ b/homeassistant/components/alarmdecoder/strings.json @@ -37,7 +37,7 @@ } }, "arm_settings": { - "title": "Configure AlarmDecoder", + "title": "[%key:component::alarmdecoder::options::step::init::title%]", "data": { "auto_bypass": "Auto Bypass on Arm", "code_arm_required": "Code Required for Arming", @@ -45,14 +45,14 @@ } }, "zone_select": { - "title": "Configure AlarmDecoder", + "title": "[%key:component::alarmdecoder::options::step::init::title%]", "description": "Enter the zone number you'd like to to add, edit, or remove.", "data": { "zone_number": "Zone Number" } }, "zone_details": { - "title": "Configure AlarmDecoder", + "title": "[%key:component::alarmdecoder::options::step::init::title%]", "description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave Zone Name blank.", "data": { "zone_name": "Zone Name", @@ -77,7 +77,7 @@ "description": "Sends custom keypresses to the alarm.", "fields": { "keypress": { - "name": "Key press", + "name": "[%key:component::alarmdecoder::services::alarm_keypress::name%]", "description": "String to send to the alarm panel." } } diff --git a/homeassistant/components/amberelectric/strings.json b/homeassistant/components/amberelectric/strings.json index 5235a8bf325e8f..ccdc2374142f0b 100644 --- a/homeassistant/components/amberelectric/strings.json +++ b/homeassistant/components/amberelectric/strings.json @@ -3,7 +3,7 @@ "step": { "user": { "data": { - "api_token": "API Token", + "api_token": "[%key:common::config_flow::data::api_token%]", "site_id": "Site ID" }, "description": "Go to {api_url} to generate an API key" diff --git a/homeassistant/components/androidtv/strings.json b/homeassistant/components/androidtv/strings.json index 9eb3d14a2253b1..7949c066916360 100644 --- a/homeassistant/components/androidtv/strings.json +++ b/homeassistant/components/androidtv/strings.json @@ -50,7 +50,7 @@ "title": "Configure Android state detection rules", "description": "Configure detection rule for application id {rule_id}", "data": { - "rule_id": "Application ID", + "rule_id": "[%key:component::androidtv::options::step::apps::data::app_id%]", "rule_values": "List of state detection rules (see documentation)", "rule_delete": "Check to delete this rule" } @@ -90,12 +90,12 @@ "description": "Uploads a file from your Home Assistant instance to an Android / Fire TV device.", "fields": { "device_path": { - "name": "Device path", - "description": "The filepath on the Android / Fire TV device." + "name": "[%key:component::androidtv::services::download::fields::device_path::name%]", + "description": "[%key:component::androidtv::services::download::fields::device_path::description%]" }, "local_path": { - "name": "Local path", - "description": "The filepath on your Home Assistant instance." + "name": "[%key:component::androidtv::services::download::fields::local_path::name%]", + "description": "[%key:component::androidtv::services::download::fields::local_path::description%]" } } }, diff --git a/homeassistant/components/anova/strings.json b/homeassistant/components/anova/strings.json index b14246a392d78e..b7762732303087 100644 --- a/homeassistant/components/anova/strings.json +++ b/homeassistant/components/anova/strings.json @@ -29,7 +29,7 @@ "name": "State" }, "mode": { - "name": "Mode" + "name": "[%key:common::config_flow::data::mode%]" }, "target_temperature": { "name": "Target temperature" diff --git a/homeassistant/components/apple_tv/strings.json b/homeassistant/components/apple_tv/strings.json index e5948a54a8d924..8730ffe01d5947 100644 --- a/homeassistant/components/apple_tv/strings.json +++ b/homeassistant/components/apple_tv/strings.json @@ -6,7 +6,7 @@ "title": "Set up a new Apple TV", "description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.", "data": { - "device_input": "Device" + "device_input": "[%key:common::config_flow::data::device%]" } }, "reconfigure": { diff --git a/homeassistant/components/aussie_broadband/strings.json b/homeassistant/components/aussie_broadband/strings.json index 90e4f094ee6fb4..276844a88066be 100644 --- a/homeassistant/components/aussie_broadband/strings.json +++ b/homeassistant/components/aussie_broadband/strings.json @@ -35,9 +35,9 @@ "options": { "step": { "init": { - "title": "Select Services", + "title": "[%key:component::aussie_broadband::config::step::service::title%]", "data": { - "services": "Services" + "services": "[%key:component::aussie_broadband::config::step::service::data::services%]" } } }, diff --git a/homeassistant/components/azure_devops/strings.json b/homeassistant/components/azure_devops/strings.json index 8dfd203c84b626..ad8ebaa016ede8 100644 --- a/homeassistant/components/azure_devops/strings.json +++ b/homeassistant/components/azure_devops/strings.json @@ -18,7 +18,7 @@ }, "reauth": { "data": { - "personal_access_token": "Personal Access Token (PAT)" + "personal_access_token": "[%key:component::azure_devops::config::step::user::data::personal_access_token%]" }, "description": "Authentication failed for {project_url}. Please enter your current credentials.", "title": "Reauthentication" diff --git a/homeassistant/components/baf/strings.json b/homeassistant/components/baf/strings.json index cb322320675b2a..5143b519d2788e 100644 --- a/homeassistant/components/baf/strings.json +++ b/homeassistant/components/baf/strings.json @@ -60,7 +60,7 @@ "name": "Wi-Fi SSID" }, "ip_address": { - "name": "IP Address" + "name": "[%key:common::config_flow::data::ip%]" } }, "switch": { diff --git a/homeassistant/components/blink/strings.json b/homeassistant/components/blink/strings.json index 6c07d1fea5513f..85556bbcd5a132 100644 --- a/homeassistant/components/blink/strings.json +++ b/homeassistant/components/blink/strings.json @@ -63,7 +63,7 @@ "description": "Saves last recorded video clip to local file.", "fields": { "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "Name of camera to grab video from." }, "filename": { @@ -77,7 +77,7 @@ "description": "Saves all recent video clips to local directory with file pattern \"%Y%m%d_%H%M%S_{name}.mp4\".", "fields": { "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "Name of camera to grab recent clips from." }, "file_path": { diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json index cae88ef24c19c8..4b1681262510af 100644 --- a/homeassistant/components/bluetooth/strings.json +++ b/homeassistant/components/bluetooth/strings.json @@ -5,7 +5,7 @@ "user": { "description": "Choose a device to set up", "data": { - "address": "Device" + "address": "[%key:common::config_flow::data::device%]" } }, "bluetooth_confirm": { diff --git a/homeassistant/components/bond/strings.json b/homeassistant/components/bond/strings.json index 04be198d149683..4c7c224bc44edf 100644 --- a/homeassistant/components/bond/strings.json +++ b/homeassistant/components/bond/strings.json @@ -60,11 +60,11 @@ "fields": { "entity_id": { "name": "Entity", - "description": "Name(s) of entities to set the tracked power state of." + "description": "[%key:component::bond::services::set_switch_power_tracked_state::fields::entity_id::description%]" }, "power_state": { - "name": "Power state", - "description": "Power state." + "name": "[%key:component::bond::services::set_switch_power_tracked_state::fields::power_state::name%]", + "description": "[%key:component::bond::services::set_switch_power_tracked_state::fields::power_state::description%]" } } }, diff --git a/homeassistant/components/braviatv/strings.json b/homeassistant/components/braviatv/strings.json index aacaf81465b327..30ad296554cd59 100644 --- a/homeassistant/components/braviatv/strings.json +++ b/homeassistant/components/braviatv/strings.json @@ -15,14 +15,14 @@ } }, "pin": { - "title": "Authorize Sony Bravia TV", + "title": "[%key:component::braviatv::config::step::authorize::title%]", "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device.", "data": { "pin": "[%key:common::config_flow::data::pin%]" } }, "psk": { - "title": "Authorize Sony Bravia TV", + "title": "[%key:component::braviatv::config::step::authorize::title%]", "description": "To set up PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Set «Authentication» to «Normal and Pre-Shared Key» or «Pre-Shared Key» and define your Pre-Shared-Key string (e.g. sony). \n\nThen enter your PSK here.", "data": { "pin": "PSK" diff --git a/homeassistant/components/brother/strings.json b/homeassistant/components/brother/strings.json index 3ee3fe7609ff60..641b1dbadf3319 100644 --- a/homeassistant/components/brother/strings.json +++ b/homeassistant/components/brother/strings.json @@ -12,7 +12,7 @@ "description": "Do you want to add the printer {model} with serial number `{serial_number}` to Home Assistant?", "title": "Discovered Brother Printer", "data": { - "type": "Type of the printer" + "type": "[%key:component::brother::config::step::user::data::type%]" } } }, diff --git a/homeassistant/components/browser/strings.json b/homeassistant/components/browser/strings.json index fafd5fb96b006f..9083ba93795b4e 100644 --- a/homeassistant/components/browser/strings.json +++ b/homeassistant/components/browser/strings.json @@ -5,7 +5,7 @@ "description": "Opens a URL in the default browser on the host machine of Home Assistant.", "fields": { "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "The URL to open." } } diff --git a/homeassistant/components/buienradar/strings.json b/homeassistant/components/buienradar/strings.json index bac4e63e288fe6..f254f7602f889f 100644 --- a/homeassistant/components/buienradar/strings.json +++ b/homeassistant/components/buienradar/strings.json @@ -38,7 +38,7 @@ "name": "Barometer" }, "barometerfcnamenl": { - "name": "Barometer" + "name": "[%key:component::buienradar::entity::sensor::barometerfcname::name%]" }, "condition": { "name": "Condition", diff --git a/homeassistant/components/cast/strings.json b/homeassistant/components/cast/strings.json index 4de0f85851f307..ce622e48aaec3f 100644 --- a/homeassistant/components/cast/strings.json +++ b/homeassistant/components/cast/strings.json @@ -22,15 +22,15 @@ "options": { "step": { "basic_options": { - "title": "Google Cast configuration", - "description": "Known Hosts - A comma-separated list of hostnames or IP-addresses of cast devices, use if mDNS discovery is not working.", + "title": "[%key:component::cast::config::step::config::title%]", + "description": "[%key:component::cast::config::step::config::description%]", "data": { - "known_hosts": "Known hosts" + "known_hosts": "[%key:component::cast::config::step::config::data::known_hosts%]" } }, "advanced_options": { "title": "Advanced Google Cast configuration", - "description": "Allowed UUIDs - A comma-separated list of UUIDs of Cast devices to add to Home Assistant. Use only if you don\u2019t want to add all available cast devices.\nIgnore CEC - A comma-separated list of Chromecasts that should ignore CEC data for determining the active input. This will be passed to pychromecast.IGNORE_CEC.", + "description": "Allowed UUIDs - A comma-separated list of UUIDs of Cast devices to add to Home Assistant. Use only if you don’t want to add all available cast devices.\nIgnore CEC - A comma-separated list of Chromecasts that should ignore CEC data for determining the active input. This will be passed to pychromecast.IGNORE_CEC.", "data": { "ignore_cec": "Ignore CEC", "uuid": "Allowed UUIDs" @@ -38,7 +38,7 @@ } }, "error": { - "invalid_known_hosts": "Known hosts must be a comma separated list of hosts." + "invalid_known_hosts": "[%key:component::cast::config::error::invalid_known_hosts%]" } }, "services": { diff --git a/homeassistant/components/co2signal/strings.json b/homeassistant/components/co2signal/strings.json index 05ea76f3179899..78274b0586cfdf 100644 --- a/homeassistant/components/co2signal/strings.json +++ b/homeassistant/components/co2signal/strings.json @@ -28,13 +28,17 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "unknown": "[%key:common::config_flow::error::unknown%]", - "api_ratelimit": "API Ratelimit exceeded" + "api_ratelimit": "[%key:component::co2signal::config::error::api_ratelimit%]" } }, "entity": { "sensor": { - "carbon_intensity": { "name": "CO2 intensity" }, - "fossil_fuel_percentage": { "name": "Grid fossil fuel percentage" } + "carbon_intensity": { + "name": "CO2 intensity" + }, + "fossil_fuel_percentage": { + "name": "Grid fossil fuel percentage" + } } } } diff --git a/homeassistant/components/color_extractor/strings.json b/homeassistant/components/color_extractor/strings.json index f56a4e514b7af7..3dc02f5603016c 100644 --- a/homeassistant/components/color_extractor/strings.json +++ b/homeassistant/components/color_extractor/strings.json @@ -5,11 +5,11 @@ "description": "Sets the light RGB to the predominant color found in the image provided by URL or file path.", "fields": { "color_extract_url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "The URL of the image we want to extract RGB values from. Must be allowed in allowlist_external_urls." }, "color_extract_path": { - "name": "Path", + "name": "[%key:common::config_flow::data::path%]", "description": "The full system path to the image we want to extract RGB values from. Must be allowed in allowlist_external_dirs." } } diff --git a/homeassistant/components/cpuspeed/strings.json b/homeassistant/components/cpuspeed/strings.json index a64e1be7fcf114..e82c6a0db12603 100644 --- a/homeassistant/components/cpuspeed/strings.json +++ b/homeassistant/components/cpuspeed/strings.json @@ -3,7 +3,7 @@ "config": { "step": { "user": { - "title": "CPU Speed", + "title": "[%key:component::cpuspeed::title%]", "description": "[%key:common::config_flow::description::confirm_setup%]" } }, diff --git a/homeassistant/components/crownstone/strings.json b/homeassistant/components/crownstone/strings.json index bcd818effb08ce..204f43768c7ae3 100644 --- a/homeassistant/components/crownstone/strings.json +++ b/homeassistant/components/crownstone/strings.json @@ -53,22 +53,22 @@ "data": { "usb_path": "[%key:common::config_flow::data::usb_path%]" }, - "title": "Crownstone USB dongle configuration", + "title": "[%key:component::crownstone::config::step::usb_config::title%]", "description": "Select the serial port of the Crownstone USB dongle.\n\nLook for a device with VID 10C4 and PID EA60." }, "usb_manual_config": { "data": { "usb_manual_path": "[%key:common::config_flow::data::usb_path%]" }, - "title": "Crownstone USB dongle manual path", - "description": "Manually enter the path of a Crownstone USB dongle." + "title": "[%key:component::crownstone::config::step::usb_manual_config::title%]", + "description": "[%key:component::crownstone::config::step::usb_manual_config::description%]" }, "usb_sphere_config": { "data": { - "usb_sphere": "Crownstone Sphere" + "usb_sphere": "[%key:component::crownstone::config::step::usb_sphere_config::data::usb_sphere%]" }, - "title": "Crownstone USB Sphere", - "description": "Select a Crownstone Sphere where the USB is located." + "title": "[%key:component::crownstone::config::step::usb_sphere_config::title%]", + "description": "[%key:component::crownstone::config::step::usb_sphere_config::description%]" } } } diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 632fe832aa8e55..e32ab875c28bb6 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -116,7 +116,7 @@ "description": "Represents a specific device endpoint in deCONZ." }, "field": { - "name": "Path", + "name": "[%key:common::config_flow::data::path%]", "description": "String representing a full path to deCONZ endpoint (when entity is not specified) or a subpath of the device path for the entity (when entity is specified)." }, "data": { @@ -134,8 +134,8 @@ "description": "Refreshes available devices from deCONZ.", "fields": { "bridgeid": { - "name": "Bridge identifier", - "description": "Unique string for each deCONZ hardware. It can be found as part of the integration name. Useful if you run multiple deCONZ integrations." + "name": "[%key:component::deconz::services::configure::fields::bridgeid::name%]", + "description": "[%key:component::deconz::services::configure::fields::bridgeid::description%]" } } }, @@ -144,8 +144,8 @@ "description": "Cleans up device and entity registry entries orphaned by deCONZ.", "fields": { "bridgeid": { - "name": "Bridge identifier", - "description": "Unique string for each deCONZ hardware. It can be found as part of the integration name. Useful if you run multiple deCONZ integrations." + "name": "[%key:component::deconz::services::configure::fields::bridgeid::name%]", + "description": "[%key:component::deconz::services::configure::fields::bridgeid::description%]" } } } diff --git a/homeassistant/components/demo/strings.json b/homeassistant/components/demo/strings.json index 2dfb3465d68655..d9b896080722e0 100644 --- a/homeassistant/components/demo/strings.json +++ b/homeassistant/components/demo/strings.json @@ -59,7 +59,7 @@ "thermostat_mode": { "name": "Thermostat mode", "state": { - "away": "Away", + "away": "[%key:common::state::not_home%]", "comfort": "Comfort", "eco": "Eco", "sleep": "Sleep" diff --git a/homeassistant/components/derivative/strings.json b/homeassistant/components/derivative/strings.json index 7a4ee9d4fc3540..ef36d46d8b9abf 100644 --- a/homeassistant/components/derivative/strings.json +++ b/homeassistant/components/derivative/strings.json @@ -6,7 +6,7 @@ "title": "Add Derivative sensor", "description": "Create a sensor that estimates the derivative of a sensor.", "data": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "round": "Precision", "source": "Input sensor", "time_window": "Time window", diff --git a/homeassistant/components/devolo_home_control/strings.json b/homeassistant/components/devolo_home_control/strings.json index 84f05b88384568..eeae9aa2e2f7b6 100644 --- a/homeassistant/components/devolo_home_control/strings.json +++ b/homeassistant/components/devolo_home_control/strings.json @@ -18,9 +18,9 @@ }, "zeroconf_confirm": { "data": { - "username": "Email / devolo ID", + "username": "[%key:component::devolo_home_control::config::step::user::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "mydevolo_url": "mydevolo URL" + "mydevolo_url": "[%key:component::devolo_home_control::config::step::user::data::mydevolo_url%]" } } } diff --git a/homeassistant/components/discord/strings.json b/homeassistant/components/discord/strings.json index 07c8fa8bdb54cf..1cd67d3b021933 100644 --- a/homeassistant/components/discord/strings.json +++ b/homeassistant/components/discord/strings.json @@ -8,7 +8,7 @@ } }, "reauth_confirm": { - "description": "Refer to the documentation on getting your Discord bot key.\n\n{url}", + "description": "[%key:component::discord::config::step::user::description%]", "data": { "api_token": "[%key:common::config_flow::data::api_token%]" } diff --git a/homeassistant/components/dlna_dmr/strings.json b/homeassistant/components/dlna_dmr/strings.json index d646f20f7a1024..48f347a0908000 100644 --- a/homeassistant/components/dlna_dmr/strings.json +++ b/homeassistant/components/dlna_dmr/strings.json @@ -34,7 +34,7 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "not_dmr": "Device is not a supported Digital Media Renderer" + "not_dmr": "[%key:component::dlna_dmr::config::abort::not_dmr%]" } }, "options": { diff --git a/homeassistant/components/dnsip/strings.json b/homeassistant/components/dnsip/strings.json index 713cc84efd4b65..d402e27287c616 100644 --- a/homeassistant/components/dnsip/strings.json +++ b/homeassistant/components/dnsip/strings.json @@ -17,8 +17,8 @@ "step": { "init": { "data": { - "resolver": "Resolver for IPV4 lookup", - "resolver_ipv6": "Resolver for IPV6 lookup" + "resolver": "[%key:component::dnsip::config::step::user::data::resolver%]", + "resolver_ipv6": "[%key:component::dnsip::config::step::user::data::resolver_ipv6%]" } } }, diff --git a/homeassistant/components/downloader/strings.json b/homeassistant/components/downloader/strings.json index 49a7388add2e05..c81b9f0ea39f86 100644 --- a/homeassistant/components/downloader/strings.json +++ b/homeassistant/components/downloader/strings.json @@ -5,7 +5,7 @@ "description": "Downloads a file to the download location.", "fields": { "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "The URL of the file to download." }, "subdir": { diff --git a/homeassistant/components/dsmr/strings.json b/homeassistant/components/dsmr/strings.json index 5724ad643feff5..7dc44e47a98c5e 100644 --- a/homeassistant/components/dsmr/strings.json +++ b/homeassistant/components/dsmr/strings.json @@ -18,15 +18,15 @@ "setup_serial": { "data": { "port": "Select device", - "dsmr_version": "Select DSMR version" + "dsmr_version": "[%key:component::dsmr::config::step::setup_network::data::dsmr_version%]" }, - "title": "Device" + "title": "[%key:common::config_flow::data::device%]" }, "setup_serial_manual_path": { "data": { "port": "[%key:common::config_flow::data::usb_path%]" }, - "title": "Path" + "title": "[%key:common::config_flow::data::path%]" } }, "error": { @@ -37,7 +37,7 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "cannot_communicate": "Failed to communicate" + "cannot_communicate": "[%key:component::dsmr::config::error::cannot_communicate%]" } }, "entity": { diff --git a/homeassistant/components/dynalite/strings.json b/homeassistant/components/dynalite/strings.json index 512e00237d9852..468cdebf0b1887 100644 --- a/homeassistant/components/dynalite/strings.json +++ b/homeassistant/components/dynalite/strings.json @@ -21,7 +21,7 @@ "description": "Requests Dynalite to report the preset for an area.", "fields": { "host": { - "name": "Host", + "name": "[%key:common::config_flow::data::host%]", "description": "Host gateway IP to send to or all configured gateways if not specified." }, "area": { @@ -39,11 +39,11 @@ "description": "Requests Dynalite to report the level of a specific channel.", "fields": { "host": { - "name": "Host", - "description": "Host gateway IP to send to or all configured gateways if not specified." + "name": "[%key:common::config_flow::data::host%]", + "description": "[%key:component::dynalite::services::request_area_preset::fields::host::description%]" }, "area": { - "name": "Area", + "name": "[%key:component::dynalite::services::request_area_preset::fields::area::name%]", "description": "Area for the requested channel." }, "channel": { diff --git a/homeassistant/components/ecobee/strings.json b/homeassistant/components/ecobee/strings.json index 05ae600d4b733a..fc43fc3000ec52 100644 --- a/homeassistant/components/ecobee/strings.json +++ b/homeassistant/components/ecobee/strings.json @@ -85,7 +85,7 @@ "description": "Ecobee thermostat on which to delete the vacation." }, "vacation_name": { - "name": "Vacation name", + "name": "[%key:component::ecobee::services::create_vacation::fields::vacation_name::name%]", "description": "Name of the vacation to delete." } } @@ -110,10 +110,10 @@ "fields": { "entity_id": { "name": "Entity", - "description": "Name(s) of entities to change." + "description": "[%key:component::ecobee::services::resume_program::fields::entity_id::description%]" }, "fan_min_on_time": { - "name": "Fan minimum on time", + "name": "[%key:component::ecobee::services::create_vacation::fields::fan_min_on_time::name%]", "description": "New value of fan min on time." } } diff --git a/homeassistant/components/eight_sleep/strings.json b/homeassistant/components/eight_sleep/strings.json index bd2b4f11b9d966..b2fb73cc020867 100644 --- a/homeassistant/components/eight_sleep/strings.json +++ b/homeassistant/components/eight_sleep/strings.json @@ -13,7 +13,7 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "cannot_connect": "Cannot connect to Eight Sleep cloud: {error}" + "cannot_connect": "[%key:component::eight_sleep::config::error::cannot_connect%]" } }, "services": { diff --git a/homeassistant/components/elkm1/strings.json b/homeassistant/components/elkm1/strings.json index 5ef15827eb9ea8..c854307dd925e7 100644 --- a/homeassistant/components/elkm1/strings.json +++ b/homeassistant/components/elkm1/strings.json @@ -6,7 +6,7 @@ "title": "Connect to Elk-M1 Control", "description": "Choose a discovered system or 'Manual Entry' if no devices have been discovered.", "data": { - "device": "Device" + "device": "[%key:common::config_flow::data::device%]" } }, "manual_connection": { @@ -83,7 +83,7 @@ "fields": { "code": { "name": "Code", - "description": "An code to arm the alarm control panel." + "description": "[%key:component::elkm1::services::alarm_arm_home_instant::fields::code::description%]" } } }, @@ -93,7 +93,7 @@ "fields": { "code": { "name": "Code", - "description": "An code to arm the alarm control panel." + "description": "[%key:component::elkm1::services::alarm_arm_home_instant::fields::code::description%]" } } }, @@ -119,7 +119,7 @@ }, "line2": { "name": "Line 2", - "description": "Up to 16 characters of text (truncated if too long)." + "description": "[%key:component::elkm1::services::alarm_display_message::fields::line1::description%]" } } }, @@ -142,7 +142,7 @@ "description": "Phrase number to speak." }, "prefix": { - "name": "Prefix", + "name": "[%key:component::elkm1::services::set_time::fields::prefix::name%]", "description": "Prefix to identify panel when multiple panels configured." } } @@ -156,8 +156,8 @@ "description": "Word number to speak." }, "prefix": { - "name": "Prefix", - "description": "Prefix to identify panel when multiple panels configured." + "name": "[%key:component::elkm1::services::set_time::fields::prefix::name%]", + "description": "[%key:component::elkm1::services::speak_phrase::fields::prefix::description%]" } } }, diff --git a/homeassistant/components/elmax/strings.json b/homeassistant/components/elmax/strings.json index e8cdbe23a5cced..4bc705adfbefb9 100644 --- a/homeassistant/components/elmax/strings.json +++ b/homeassistant/components/elmax/strings.json @@ -13,7 +13,7 @@ "data": { "panel_name": "Panel Name", "panel_id": "Panel ID", - "panel_pin": "PIN Code" + "panel_pin": "[%key:common::config_flow::data::pin%]" } }, "reauth_confirm": { diff --git a/homeassistant/components/enocean/strings.json b/homeassistant/components/enocean/strings.json index a2aff2a4207e12..97da526185f34e 100644 --- a/homeassistant/components/enocean/strings.json +++ b/homeassistant/components/enocean/strings.json @@ -10,7 +10,7 @@ "manual": { "title": "Enter the path to your ENOcean dongle", "data": { - "path": "USB dongle path" + "path": "[%key:component::enocean::config::step::detect::data::path%]" } } }, diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index 2ec1fe1bc41c07..2bbbb229949aec 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -35,7 +35,7 @@ }, "reauth_confirm": { "data": { - "noise_psk": "Encryption key" + "noise_psk": "[%key:component::esphome::config::step::encryption_key::data::noise_psk%]" }, "description": "The ESPHome device {name} enabled transport encryption or changed the encryption key. Please enter the updated key. You can find it in the ESPHome Dashboard or in your device configuration." }, diff --git a/homeassistant/components/evohome/strings.json b/homeassistant/components/evohome/strings.json index d8214c3aa8bf31..aa38ee170a5db1 100644 --- a/homeassistant/components/evohome/strings.json +++ b/homeassistant/components/evohome/strings.json @@ -5,7 +5,7 @@ "description": "Sets the system mode, either indefinitely, or for a specified period of time, after which it will revert to Auto. Not all systems support all modes.", "fields": { "mode": { - "name": "Mode", + "name": "[%key:common::config_flow::data::mode%]", "description": "Mode to set thermostat." }, "period": { diff --git a/homeassistant/components/facebox/strings.json b/homeassistant/components/facebox/strings.json index 776644c7cfa2f9..1869673b643673 100644 --- a/homeassistant/components/facebox/strings.json +++ b/homeassistant/components/facebox/strings.json @@ -9,7 +9,7 @@ "description": "The facebox entity to teach." }, "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "The name of the face to teach." }, "file_path": { diff --git a/homeassistant/components/flux_led/strings.json b/homeassistant/components/flux_led/strings.json index 7617d56d512194..d1d812cb2102ca 100644 --- a/homeassistant/components/flux_led/strings.json +++ b/homeassistant/components/flux_led/strings.json @@ -114,12 +114,12 @@ "description": "Sets strip zones for Addressable v3 controllers (0xA3).", "fields": { "colors": { - "name": "Colors", + "name": "[%key:component::flux_led::services::set_custom_effect::fields::colors::name%]", "description": "List of colors for each zone (RGB). The length of each zone is the number of pixels per segment divided by the number of colors. (Max 2048 Colors)." }, "speed_pct": { "name": "Speed", - "description": "Effect speed for the custom effect (0-100)." + "description": "[%key:component::flux_led::services::set_custom_effect::fields::speed_pct::description%]" }, "effect": { "name": "Effect", diff --git a/homeassistant/components/forecast_solar/strings.json b/homeassistant/components/forecast_solar/strings.json index a7bc0190f5f7db..7e8c32017ce0ce 100644 --- a/homeassistant/components/forecast_solar/strings.json +++ b/homeassistant/components/forecast_solar/strings.json @@ -8,7 +8,7 @@ "declination": "Declination (0 = Horizontal, 90 = Vertical)", "latitude": "[%key:common::config_flow::data::latitude%]", "longitude": "[%key:common::config_flow::data::longitude%]", - "modules power": "Total Watt peak power of your solar modules", + "modules_power": "Total Watt peak power of your solar modules", "name": "[%key:common::config_flow::data::name%]" } } @@ -23,11 +23,11 @@ "description": "These values allow tweaking the Forecast.Solar result. Please refer to the documentation if a field is unclear.", "data": { "api_key": "Forecast.Solar API Key (optional)", - "azimuth": "Azimuth (360 degrees, 0 = North, 90 = East, 180 = South, 270 = West)", + "azimuth": "[%key:component::forecast_solar::config::step::user::data::azimuth%]", "damping": "Damping factor: adjusts the results in the morning and evening", "inverter_size": "Inverter size (Watt)", - "declination": "Declination (0 = Horizontal, 90 = Vertical)", - "modules power": "Total Watt peak power of your solar modules" + "declination": "[%key:component::forecast_solar::config::step::user::data::declination%]", + "modules power": "[%key:component::forecast_solar::config::step::user::data::modules_power%]" } } } diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index dd845fc2a1b54e..7cbb10a236b21c 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -19,7 +19,7 @@ } }, "user": { - "title": "Set up FRITZ!Box Tools", + "title": "[%key:component::fritz::config::step::confirm::title%]", "description": "Set up FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.", "data": { "host": "[%key:common::config_flow::data::host%]", @@ -126,7 +126,7 @@ }, "services": { "reconnect": { - "name": "Reconnect", + "name": "[%key:component::fritz::entity::button::reconnect::name%]", "description": "Reconnects your FRITZ!Box internet connection.", "fields": { "device_id": { @@ -140,7 +140,7 @@ "description": "Reboots your FRITZ!Box.", "fields": { "device_id": { - "name": "Fritz!Box Device", + "name": "[%key:component::fritz::services::reconnect::fields::device_id::name%]", "description": "Select the Fritz!Box to reboot." } } @@ -150,7 +150,7 @@ "description": "Remove FRITZ!Box stale device_tracker entities.", "fields": { "device_id": { - "name": "Fritz!Box Device", + "name": "[%key:component::fritz::services::reconnect::fields::device_id::name%]", "description": "Select the Fritz!Box to check." } } @@ -160,11 +160,11 @@ "description": "Sets a new password for the guest Wi-Fi. The password must be between 8 and 63 characters long. If no additional parameter is set, the password will be auto-generated with a length of 12 characters.", "fields": { "device_id": { - "name": "Fritz!Box Device", - "description": "Select the Fritz!Box to check." + "name": "[%key:component::fritz::services::reconnect::fields::device_id::name%]", + "description": "Select the Fritz!Box to configure." }, "password": { - "name": "Password", + "name": "[%key:common::config_flow::data::password%]", "description": "New password for the guest Wi-Fi." }, "length": { diff --git a/homeassistant/components/fully_kiosk/strings.json b/homeassistant/components/fully_kiosk/strings.json index 2ecac4a57422ea..d61e8a7b7a89f1 100644 --- a/homeassistant/components/fully_kiosk/strings.json +++ b/homeassistant/components/fully_kiosk/strings.json @@ -112,7 +112,7 @@ "description": "Loads a URL on Fully Kiosk Browser.", "fields": { "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "URL to load." } } diff --git a/homeassistant/components/gardena_bluetooth/strings.json b/homeassistant/components/gardena_bluetooth/strings.json index c7a6e9637dffa0..0a9677b1f92e88 100644 --- a/homeassistant/components/gardena_bluetooth/strings.json +++ b/homeassistant/components/gardena_bluetooth/strings.json @@ -38,7 +38,7 @@ }, "switch": { "state": { - "name": "Open" + "name": "[%key:common::state::open%]" } } } diff --git a/homeassistant/components/geniushub/strings.json b/homeassistant/components/geniushub/strings.json index 1c1092ee256dd7..ac057f5c639dc5 100644 --- a/homeassistant/components/geniushub/strings.json +++ b/homeassistant/components/geniushub/strings.json @@ -9,7 +9,7 @@ "description": "The zone's entity_id." }, "mode": { - "name": "Mode", + "name": "[%key:common::config_flow::data::mode%]", "description": "One of: off, timer or footprint." } } @@ -20,7 +20,7 @@ "fields": { "entity_id": { "name": "Entity", - "description": "The zone's entity_id." + "description": "[%key:component::geniushub::services::set_zone_mode::fields::entity_id::description%]" }, "temperature": { "name": "Temperature", @@ -38,7 +38,7 @@ "fields": { "duration": { "name": "Duration", - "description": "The duration of the override. Optional, default 1 hour, maximum 24 hours." + "description": "[%key:component::geniushub::services::set_zone_override::fields::duration::description%]" } } } diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json index 7fa1569992f6d8..b3594f31510d82 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -88,11 +88,11 @@ "fields": { "summary": { "name": "Summary", - "description": "Acts as the title of the event." + "description": "[%key:component::google::services::add_event::fields::summary::description%]" }, "description": { "name": "Description", - "description": "The description of the event. Optional." + "description": "[%key:component::google::services::add_event::fields::description::description%]" }, "start_date_time": { "name": "Start time", @@ -104,18 +104,18 @@ }, "start_date": { "name": "Start date", - "description": "The date the whole day event should start." + "description": "[%key:component::google::services::add_event::fields::start_date::description%]" }, "end_date": { "name": "End date", - "description": "The date the whole day event should end." + "description": "[%key:component::google::services::add_event::fields::end_date::description%]" }, "in": { "name": "In", "description": "Days or weeks that you want to create the event in." }, "location": { - "name": "Location", + "name": "[%key:common::config_flow::data::location%]", "description": "The location of the event. Optional." } } diff --git a/homeassistant/components/google_generative_ai_conversation/strings.json b/homeassistant/components/google_generative_ai_conversation/strings.json index 2df5398222c877..2b1b41a2c283a8 100644 --- a/homeassistant/components/google_generative_ai_conversation/strings.json +++ b/homeassistant/components/google_generative_ai_conversation/strings.json @@ -18,7 +18,7 @@ "init": { "data": { "prompt": "Prompt Template", - "model": "Model", + "model": "[%key:common::generic::model%]", "temperature": "Temperature", "top_p": "Top P", "top_k": "Top K" diff --git a/homeassistant/components/google_mail/strings.json b/homeassistant/components/google_mail/strings.json index 83537c6b1de3c5..2bd70750ff929f 100644 --- a/homeassistant/components/google_mail/strings.json +++ b/homeassistant/components/google_mail/strings.json @@ -68,7 +68,7 @@ "description": "Restrict automatic reply to domain. This only affects GSuite accounts." }, "start": { - "name": "Start", + "name": "[%key:common::action::start%]", "description": "First day of the vacation." }, "end": { diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index bbf521b06e3968..1c656b46b9e35b 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -23,7 +23,7 @@ "all": "All entities", "entities": "Members", "hide_members": "Hide members", - "name": "Name" + "name": "[%key:common::config_flow::data::name%]" } }, "cover": { @@ -70,9 +70,9 @@ "title": "[%key:component::group::config::step::user::title%]", "data": { "ignore_non_numeric": "Ignore non-numeric", - "entities": "Members", - "hide_members": "Hide members", - "name": "Name", + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]", + "name": "[%key:common::config_flow::data::name%]", "type": "Type", "round_digits": "Round value to number of decimals", "device_class": "Device class", @@ -172,7 +172,7 @@ }, "state_attributes": { "entity_id": { - "name": "Members" + "name": "[%key:component::group::config::step::binary_sensor::data::entities%]" } } } @@ -205,7 +205,7 @@ "description": "Object ID of this group. This object ID is used as part of the entity ID. Entity ID format: [domain].[object_id]." }, "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "Name of the group." }, "icon": { @@ -235,8 +235,8 @@ "description": "Removes a group.", "fields": { "object_id": { - "name": "Object ID", - "description": "Object ID of this group. This object ID is used as part of the entity ID. Entity ID format: [domain].[object_id]." + "name": "[%key:component::group::services::set::fields::object_id::name%]", + "description": "[%key:component::group::services::set::fields::object_id::description%]" } } } diff --git a/homeassistant/components/growatt_server/strings.json b/homeassistant/components/growatt_server/strings.json index d2c196dbfdd395..f507387e6285e7 100644 --- a/homeassistant/components/growatt_server/strings.json +++ b/homeassistant/components/growatt_server/strings.json @@ -188,10 +188,10 @@ "name": "Grid discharged today" }, "storage_load_consumption_today": { - "name": "Load consumption today" + "name": "[%key:component::growatt_server::entity::sensor::mix_load_consumption_today::name%]" }, "storage_load_consumption_lifetime": { - "name": "Lifetime load consumption" + "name": "[%key:component::growatt_server::entity::sensor::mix_load_consumption_lifetime::name%]" }, "storage_grid_charged_today": { "name": "Grid charged today" @@ -215,7 +215,7 @@ "name": "Charge today" }, "storage_import_from_grid": { - "name": "Import from grid" + "name": "[%key:component::growatt_server::entity::sensor::mix_import_from_grid::name%]" }, "storage_import_from_grid_today": { "name": "Import from grid today" @@ -224,7 +224,7 @@ "name": "Import from grid total" }, "storage_load_consumption": { - "name": "Load consumption" + "name": "[%key:component::growatt_server::entity::sensor::mix_load_consumption::name%]" }, "storage_grid_voltage": { "name": "AC input voltage" @@ -263,7 +263,7 @@ "name": "Energy today" }, "tlx_energy_total": { - "name": "Lifetime energy output" + "name": "[%key:component::growatt_server::entity::sensor::inverter_energy_total::name%]" }, "tlx_energy_total_input_1": { "name": "Lifetime total energy input 1" @@ -272,13 +272,13 @@ "name": "Energy Today Input 1" }, "tlx_voltage_input_1": { - "name": "Input 1 voltage" + "name": "[%key:component::growatt_server::entity::sensor::inverter_voltage_input_1::name%]" }, "tlx_amperage_input_1": { - "name": "Input 1 Amperage" + "name": "[%key:component::growatt_server::entity::sensor::inverter_amperage_input_1::name%]" }, "tlx_wattage_input_1": { - "name": "Input 1 Wattage" + "name": "[%key:component::growatt_server::entity::sensor::inverter_wattage_input_1::name%]" }, "tlx_energy_total_input_2": { "name": "Lifetime total energy input 2" @@ -287,13 +287,13 @@ "name": "Energy Today Input 2" }, "tlx_voltage_input_2": { - "name": "Input 2 voltage" + "name": "[%key:component::growatt_server::entity::sensor::inverter_voltage_input_2::name%]" }, "tlx_amperage_input_2": { - "name": "Input 2 Amperage" + "name": "[%key:component::growatt_server::entity::sensor::inverter_amperage_input_2::name%]" }, "tlx_wattage_input_2": { - "name": "Input 2 Wattage" + "name": "[%key:component::growatt_server::entity::sensor::inverter_wattage_input_2::name%]" }, "tlx_energy_total_input_3": { "name": "Lifetime total energy input 3" @@ -302,13 +302,13 @@ "name": "Energy Today Input 3" }, "tlx_voltage_input_3": { - "name": "Input 3 voltage" + "name": "[%key:component::growatt_server::entity::sensor::inverter_voltage_input_3::name%]" }, "tlx_amperage_input_3": { - "name": "Input 3 Amperage" + "name": "[%key:component::growatt_server::entity::sensor::inverter_amperage_input_3::name%]" }, "tlx_wattage_input_3": { - "name": "Input 3 Wattage" + "name": "[%key:component::growatt_server::entity::sensor::inverter_wattage_input_3::name%]" }, "tlx_energy_total_input_4": { "name": "Lifetime total energy input 4" @@ -329,16 +329,16 @@ "name": "Lifetime total solar energy" }, "tlx_internal_wattage": { - "name": "Internal wattage" + "name": "[%key:component::growatt_server::entity::sensor::inverter_internal_wattage::name%]" }, "tlx_reactive_voltage": { - "name": "Reactive voltage" + "name": "[%key:component::growatt_server::entity::sensor::inverter_reactive_voltage::name%]" }, "tlx_frequency": { - "name": "AC frequency" + "name": "[%key:component::growatt_server::entity::sensor::inverter_frequency::name%]" }, "tlx_current_wattage": { - "name": "Output power" + "name": "[%key:component::growatt_server::entity::sensor::inverter_current_wattage::name%]" }, "tlx_temperature_1": { "name": "Temperature 1" @@ -392,13 +392,13 @@ "name": "Lifetime total battery 2 charged" }, "tlx_export_to_grid_today": { - "name": "Export to grid today" + "name": "[%key:component::growatt_server::entity::sensor::mix_export_to_grid_today::name%]" }, "tlx_export_to_grid_total": { "name": "Lifetime total export to grid" }, "tlx_load_consumption_today": { - "name": "Load consumption today" + "name": "[%key:component::growatt_server::entity::sensor::mix_load_consumption_today::name%]" }, "mix_load_consumption_total": { "name": "Lifetime total load consumption" @@ -419,7 +419,7 @@ "name": "Output Power" }, "total_energy_output": { - "name": "Lifetime energy output" + "name": "[%key:component::growatt_server::entity::sensor::inverter_energy_total::name%]" }, "total_maximum_output": { "name": "Maximum power" diff --git a/homeassistant/components/guardian/strings.json b/homeassistant/components/guardian/strings.json index f416adac027a01..59630e87932d52 100644 --- a/homeassistant/components/guardian/strings.json +++ b/homeassistant/components/guardian/strings.json @@ -52,7 +52,7 @@ "description": "Adds a new paired sensor to the valve controller.", "fields": { "device_id": { - "name": "Valve controller", + "name": "[%key:component::guardian::entity::switch::valve_controller::name%]", "description": "The valve controller to add the sensor to." }, "uid": { @@ -66,12 +66,12 @@ "description": "Removes a paired sensor from the valve controller.", "fields": { "device_id": { - "name": "Valve controller", + "name": "[%key:component::guardian::entity::switch::valve_controller::name%]", "description": "The valve controller to remove the sensor from." }, "uid": { - "name": "UID", - "description": "The UID of the paired sensor." + "name": "[%key:component::guardian::services::pair_sensor::fields::uid::name%]", + "description": "[%key:component::guardian::services::pair_sensor::fields::uid::description%]" } } }, @@ -80,15 +80,15 @@ "description": "Upgrades the device firmware.", "fields": { "device_id": { - "name": "Valve controller", + "name": "[%key:component::guardian::entity::switch::valve_controller::name%]", "description": "The valve controller whose firmware should be upgraded." }, "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "The URL of the server hosting the firmware file." }, "port": { - "name": "Port", + "name": "[%key:common::config_flow::data::port%]", "description": "The port on which the firmware file is served." }, "filename": { diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index 8d2fb38517d977..8dacb0e6321c29 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -11,8 +11,8 @@ "user": { "data": { "url": "[%key:common::config_flow::data::url%]", - "name": "Override for Habitica\u2019s username. Will be used for service calls", - "api_user": "Habitica\u2019s API user ID", + "name": "Override for Habitica’s username. Will be used for service calls", + "api_user": "Habitica’s API user ID", "api_key": "[%key:common::config_flow::data::api_key%]" }, "description": "Connect your Habitica profile to allow monitoring of your user's profile and tasks. Note that api_id and api_key must be gotten from https://habitica.com/user/settings/api" @@ -25,11 +25,11 @@ "description": "Calls Habitica API.", "fields": { "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "Habitica's username to call for." }, "path": { - "name": "Path", + "name": "[%key:common::config_flow::data::path%]", "description": "Items from API URL in form of an array with method attached at the end. Consult https://habitica.com/apidoc/. Example uses https://habitica.com/apidoc/#api-Task-CreateUserTasks." }, "args": { diff --git a/homeassistant/components/harmony/strings.json b/homeassistant/components/harmony/strings.json index 8e2b435483f4e2..9ae22090d7fbdb 100644 --- a/homeassistant/components/harmony/strings.json +++ b/homeassistant/components/harmony/strings.json @@ -10,7 +10,7 @@ } }, "link": { - "title": "Set up Logitech Harmony Hub", + "title": "[%key:component::harmony::config::step::user::title%]", "description": "Do you want to set up {name} ({host})?" } }, diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index e954c0cccf61fe..c45d455631bff5 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -282,11 +282,11 @@ "description": "Creates a full backup.", "fields": { "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "Optional (default = current date and time)." }, "password": { - "name": "Password", + "name": "[%key:common::config_flow::data::password%]", "description": "Password to protect the backup with." }, "compressed": { @@ -294,7 +294,7 @@ "description": "Compresses the backup files." }, "location": { - "name": "Location", + "name": "[%key:common::config_flow::data::location%]", "description": "Name of a backup network storage to host backups." } } diff --git a/homeassistant/components/hdmi_cec/strings.json b/homeassistant/components/hdmi_cec/strings.json index 6efc9ec4272cb7..22715907a99b6f 100644 --- a/homeassistant/components/hdmi_cec/strings.json +++ b/homeassistant/components/hdmi_cec/strings.json @@ -9,7 +9,7 @@ "description": "Select HDMI device.", "fields": { "device": { - "name": "Device", + "name": "[%key:common::config_flow::data::device%]", "description": "Address of device to select. Can be entity_id, physical address or alias from configuration." } } @@ -41,7 +41,7 @@ } }, "standby": { - "name": "Standby", + "name": "[%key:common::state::standby%]", "description": "Standby all devices which supports it." }, "update": { diff --git a/homeassistant/components/heos/strings.json b/homeassistant/components/heos/strings.json index 635fe08cccc4c0..7bd362cf3d7caa 100644 --- a/homeassistant/components/heos/strings.json +++ b/homeassistant/components/heos/strings.json @@ -22,11 +22,11 @@ "description": "Signs the controller in to a HEOS account.", "fields": { "username": { - "name": "Username", + "name": "[%key:common::config_flow::data::username%]", "description": "The username or email of the HEOS account." }, "password": { - "name": "Password", + "name": "[%key:common::config_flow::data::password%]", "description": "The password of the HEOS account." } } diff --git a/homeassistant/components/here_travel_time/strings.json b/homeassistant/components/here_travel_time/strings.json index 2c031dc0a02881..124aa070595909 100644 --- a/homeassistant/components/here_travel_time/strings.json +++ b/homeassistant/components/here_travel_time/strings.json @@ -16,13 +16,13 @@ } }, "origin_coordinates": { - "title": "Choose Origin", + "title": "[%key:component::here_travel_time::config::step::origin_menu::title%]", "data": { "origin": "Origin as GPS coordinates" } }, "origin_entity_id": { - "title": "Choose Origin", + "title": "[%key:component::here_travel_time::config::step::origin_menu::title%]", "data": { "origin_entity_id": "Origin using an entity" } @@ -30,18 +30,18 @@ "destination_menu": { "title": "Choose Destination", "menu_options": { - "destination_coordinates": "Using a map location", - "destination_entity": "Using an entity" + "destination_coordinates": "[%key:component::here_travel_time::config::step::origin_menu::menu_options::origin_coordinates%]", + "destination_entity": "[%key:component::here_travel_time::config::step::origin_menu::menu_options::origin_entity%]" } }, "destination_coordinates": { - "title": "Choose Destination", + "title": "[%key:component::here_travel_time::config::step::destination_menu::title%]", "data": { "destination": "Destination as GPS coordinates" } }, "destination_entity_id": { - "title": "Choose Destination", + "title": "[%key:component::here_travel_time::config::step::destination_menu::title%]", "data": { "destination_entity_id": "Destination using an entity" } diff --git a/homeassistant/components/hive/strings.json b/homeassistant/components/hive/strings.json index 495c5dad1cc133..e2a3e9dc7e1eb1 100644 --- a/homeassistant/components/hive/strings.json +++ b/homeassistant/components/hive/strings.json @@ -25,7 +25,7 @@ "title": "Hive Configuration." }, "reauth": { - "title": "Hive Login", + "title": "[%key:component::hive::config::step::user::title%]", "description": "Re-enter your Hive login information.", "data": { "username": "[%key:common::config_flow::data::username%]", @@ -82,7 +82,7 @@ }, "temperature": { "name": "Temperature", - "description": "Set the target temperature for the boost period." + "description": "[%key:component::hive::services::boost_heating::fields::temperature::description%]" } } }, @@ -109,7 +109,7 @@ "description": "Set the time period for the boost." }, "on_off": { - "name": "Mode", + "name": "[%key:common::config_flow::data::mode%]", "description": "Set the boost function on or off." } } diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 41eedbe83a87d8..091f0c182329ab 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -46,23 +46,23 @@ "fields": { "device_id": { "name": "Device ID", - "description": "Id of the device." + "description": "[%key:component::home_connect::services::start_program::fields::device_id::description%]" }, "program": { - "name": "Program", - "description": "Program to select." + "name": "[%key:component::home_connect::services::start_program::fields::program::name%]", + "description": "[%key:component::home_connect::services::start_program::fields::program::description%]" }, "key": { - "name": "Option key", - "description": "Key of the option." + "name": "[%key:component::home_connect::services::start_program::fields::key::name%]", + "description": "[%key:component::home_connect::services::start_program::fields::key::description%]" }, "value": { - "name": "Option value", - "description": "Value of the option." + "name": "[%key:component::home_connect::services::start_program::fields::value::name%]", + "description": "[%key:component::home_connect::services::start_program::fields::value::description%]" }, "unit": { - "name": "Option unit", - "description": "Unit for the option." + "name": "[%key:component::home_connect::services::start_program::fields::unit::name%]", + "description": "[%key:component::home_connect::services::start_program::fields::unit::description%]" } } }, @@ -72,7 +72,7 @@ "fields": { "device_id": { "name": "Device ID", - "description": "Id of the device." + "description": "[%key:component::home_connect::services::start_program::fields::device_id::description%]" } } }, @@ -82,7 +82,7 @@ "fields": { "device_id": { "name": "Device ID", - "description": "Id of the device." + "description": "[%key:component::home_connect::services::start_program::fields::device_id::description%]" } } }, @@ -92,15 +92,15 @@ "fields": { "device_id": { "name": "Device ID", - "description": "Id of the device." + "description": "[%key:component::home_connect::services::start_program::fields::device_id::description%]" }, "key": { "name": "Key", - "description": "Key of the option." + "description": "[%key:component::home_connect::services::start_program::fields::key::description%]" }, "value": { "name": "Value", - "description": "Value of the option." + "description": "[%key:component::home_connect::services::start_program::fields::value::description%]" } } }, @@ -110,15 +110,15 @@ "fields": { "device_id": { "name": "Device ID", - "description": "Id of the device." + "description": "[%key:component::home_connect::services::start_program::fields::device_id::description%]" }, "key": { "name": "Key", - "description": "Key of the option." + "description": "[%key:component::home_connect::services::start_program::fields::key::description%]" }, "value": { "name": "Value", - "description": "Value of the option." + "description": "[%key:component::home_connect::services::start_program::fields::value::description%]" } } }, @@ -128,7 +128,7 @@ "fields": { "device_id": { "name": "Device ID", - "description": "Id of the device." + "description": "[%key:component::home_connect::services::start_program::fields::device_id::description%]" }, "key": { "name": "Key", diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 57cb5c3eb566de..791b1a2192951b 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -64,11 +64,11 @@ "description": "Updates the Home Assistant location.", "fields": { "latitude": { - "name": "Latitude", + "name": "[%key:common::config_flow::data::latitude%]", "description": "Latitude of your location." }, "longitude": { - "name": "Longitude", + "name": "[%key:common::config_flow::data::longitude%]", "description": "Longitude of your location." } } diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index 7420ef7f3f9cfa..e47ae0fca84246 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -7,7 +7,7 @@ "title": "Device selection", "description": "HomeKit Device communicates over the local area network using a secure encrypted connection without a separate HomeKit Controller or iCloud. Select the device you want to pair with:", "data": { - "device": "Device" + "device": "[%key:common::config_flow::data::device%]" } }, "pair": { @@ -74,8 +74,8 @@ "select": { "ecobee_mode": { "state": { - "away": "Away", - "home": "Home", + "away": "[%key:common::state::not_home%]", + "home": "[%key:common::state::home%]", "sleep": "Sleep" } } @@ -96,7 +96,7 @@ "border_router": "Border Router", "child": "Child", "detached": "Detached", - "disabled": "Disabled", + "disabled": "[%key:common::state::disabled%]", "joining": "Joining", "leader": "Leader", "router": "Router" diff --git a/homeassistant/components/homematic/strings.json b/homeassistant/components/homematic/strings.json index 14f723694fca6c..48ebbe5d3457e4 100644 --- a/homeassistant/components/homematic/strings.json +++ b/homeassistant/components/homematic/strings.json @@ -31,7 +31,7 @@ "description": "Name(s) of homematic central to set value." }, "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "Name of the variable to set." }, "value": { @@ -46,23 +46,23 @@ "fields": { "address": { "name": "Address", - "description": "Address of homematic device or BidCoS-RF for virtual remote." + "description": "[%key:component::homematic::services::virtualkey::fields::address::description%]" }, "channel": { "name": "Channel", - "description": "Channel for calling a keypress." + "description": "[%key:component::homematic::services::virtualkey::fields::channel::description%]" }, "param": { - "name": "Param", - "description": "Event to send i.e. PRESS_LONG, PRESS_SHORT." + "name": "[%key:component::homematic::services::virtualkey::fields::param::name%]", + "description": "[%key:component::homematic::services::virtualkey::fields::param::description%]" }, "interface": { "name": "Interface", - "description": "Set an interface value." + "description": "[%key:component::homematic::services::virtualkey::fields::interface::description%]" }, "value": { "name": "Value", - "description": "New value." + "description": "[%key:component::homematic::services::set_variable_value::fields::value::description%]" }, "value_type": { "name": "Value type", @@ -83,7 +83,7 @@ "description": "Select the given interface into install mode." }, "mode": { - "name": "Mode", + "name": "[%key:common::config_flow::data::mode%]", "description": "1= Normal mode / 2= Remove exists old links." }, "time": { diff --git a/homeassistant/components/homematicip_cloud/strings.json b/homeassistant/components/homematicip_cloud/strings.json index 6a20c5f8a54293..3795508d75dcff 100644 --- a/homeassistant/components/homematicip_cloud/strings.json +++ b/homeassistant/components/homematicip_cloud/strings.json @@ -43,15 +43,15 @@ }, "activate_eco_mode_with_period": { "name": "Activate eco more with period", - "description": "Activates eco mode with period.", + "description": "[%key:component::homematicip_cloud::services::activate_eco_mode_with_duration::description%]", "fields": { "endtime": { "name": "Endtime", "description": "The time when the eco mode should automatically be disabled." }, "accesspoint_id": { - "name": "Accesspoint ID", - "description": "The ID of the Homematic IP Access Point." + "name": "[%key:component::homematicip_cloud::services::activate_eco_mode_with_duration::fields::accesspoint_id::name%]", + "description": "[%key:component::homematicip_cloud::services::activate_eco_mode_with_duration::fields::accesspoint_id::description%]" } } }, @@ -60,7 +60,7 @@ "description": "Activates the vacation mode until the given time.", "fields": { "endtime": { - "name": "Endtime", + "name": "[%key:component::homematicip_cloud::services::activate_eco_mode_with_period::fields::endtime::name%]", "description": "The time when the vacation mode should automatically be disabled." }, "temperature": { @@ -68,8 +68,8 @@ "description": "The set temperature during the vacation mode." }, "accesspoint_id": { - "name": "Accesspoint ID", - "description": "The ID of the Homematic IP Access Point." + "name": "[%key:component::homematicip_cloud::services::activate_eco_mode_with_duration::fields::accesspoint_id::name%]", + "description": "[%key:component::homematicip_cloud::services::activate_eco_mode_with_duration::fields::accesspoint_id::description%]" } } }, @@ -78,8 +78,8 @@ "description": "Deactivates the eco mode immediately.", "fields": { "accesspoint_id": { - "name": "Accesspoint ID", - "description": "The ID of the Homematic IP Access Point." + "name": "[%key:component::homematicip_cloud::services::activate_eco_mode_with_duration::fields::accesspoint_id::name%]", + "description": "[%key:component::homematicip_cloud::services::activate_eco_mode_with_duration::fields::accesspoint_id::description%]" } } }, @@ -88,8 +88,8 @@ "description": "Deactivates the vacation mode immediately.", "fields": { "accesspoint_id": { - "name": "Accesspoint ID", - "description": "The ID of the Homematic IP Access Point." + "name": "[%key:component::homematicip_cloud::services::activate_eco_mode_with_duration::fields::accesspoint_id::name%]", + "description": "[%key:component::homematicip_cloud::services::activate_eco_mode_with_duration::fields::accesspoint_id::description%]" } } }, diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 50c57e6db3ec29..41826dc6ae771f 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -55,7 +55,7 @@ "description": "Clears traffic statistics.", "fields": { "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "URL of router to clear; optional when only one is configured." } } @@ -65,7 +65,7 @@ "description": "Reboots router.", "fields": { "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "URL of router to reboot; optional when only one is configured." } } @@ -75,7 +75,7 @@ "description": "Resumes suspended integration.", "fields": { "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "URL of router to resume integration for; optional when only one is configured." } } @@ -85,7 +85,7 @@ "description": "Suspends integration. Suspending logs the integration out from the router, and stops accessing it. Useful e.g. if accessing the router web interface from another source such as a web browser is temporarily required. Invoke the resume_integration service to resume.\n.", "fields": { "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "URL of router to suspend integration for; optional when only one is configured." } } diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index 2c3f493e2c8599..aef5dba19869ba 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -46,10 +46,10 @@ "dim_up": "Dim up", "turn_off": "[%key:common::action::turn_off%]", "turn_on": "[%key:common::action::turn_on%]", - "1": "First button", - "2": "Second button", - "3": "Third button", - "4": "Fourth button", + "1": "[%key:component::hue::device_automation::trigger_subtype::button_1%]", + "2": "[%key:component::hue::device_automation::trigger_subtype::button_2%]", + "3": "[%key:component::hue::device_automation::trigger_subtype::button_3%]", + "4": "[%key:component::hue::device_automation::trigger_subtype::button_4%]", "clock_wise": "Rotation clockwise", "counter_clock_wise": "Rotation counter-clockwise" }, @@ -62,9 +62,9 @@ "initial_press": "\"{subtype}\" pressed initially", "repeat": "\"{subtype}\" held down", "short_release": "\"{subtype}\" released after short press", - "long_release": "\"{subtype}\" released after long press", - "double_short_release": "Both \"{subtype}\" released", - "start": "\"{subtype}\" pressed initially" + "long_release": "[%key:component::hue::device_automation::trigger_type::remote_button_long_release%]", + "double_short_release": "[%key:component::hue::device_automation::trigger_type::remote_double_button_short_press%]", + "start": "[%key:component::hue::device_automation::trigger_type::initial_press%]" } }, "options": { @@ -107,7 +107,7 @@ "description": "Transition duration it takes to bring devices to the state defined in the scene." }, "dynamic": { - "name": "Dynamic", + "name": "[%key:component::hue::services::hue_activate_scene::fields::dynamic::name%]", "description": "Enable dynamic mode of the scene." }, "speed": { diff --git a/homeassistant/components/hunterdouglas_powerview/strings.json b/homeassistant/components/hunterdouglas_powerview/strings.json index 41a16408783138..ec26e423e06e01 100644 --- a/homeassistant/components/hunterdouglas_powerview/strings.json +++ b/homeassistant/components/hunterdouglas_powerview/strings.json @@ -8,7 +8,7 @@ } }, "link": { - "title": "Connect to the PowerView Hub", + "title": "[%key:component::hunterdouglas_powerview::config::step::user::title%]", "description": "Do you want to set up {name} ({host})?" } }, diff --git a/homeassistant/components/hvv_departures/strings.json b/homeassistant/components/hvv_departures/strings.json index 8f9c06f53fbb9b..a9ec58f12ad54f 100644 --- a/homeassistant/components/hvv_departures/strings.json +++ b/homeassistant/components/hvv_departures/strings.json @@ -18,7 +18,7 @@ "station_select": { "title": "Select Station/Address", "data": { - "station": "Station/Address" + "station": "[%key:component::hvv_departures::config::step::station::data::station%]" } } }, diff --git a/homeassistant/components/icloud/strings.json b/homeassistant/components/icloud/strings.json index 9bc7750790f308..96db11d4656f73 100644 --- a/homeassistant/components/icloud/strings.json +++ b/homeassistant/components/icloud/strings.json @@ -60,7 +60,7 @@ "fields": { "account": { "name": "Account", - "description": "Your iCloud account username (email) or account name." + "description": "[%key:component::icloud::services::update::fields::account::description%]" }, "device_name": { "name": "Device name", @@ -74,7 +74,7 @@ "fields": { "account": { "name": "Account", - "description": "Your iCloud account username (email) or account name." + "description": "[%key:component::icloud::services::update::fields::account::description%]" }, "device_name": { "name": "Device name", @@ -96,7 +96,7 @@ "fields": { "account": { "name": "Account", - "description": "Your iCloud account username (email) or account name." + "description": "[%key:component::icloud::services::update::fields::account::description%]" }, "device_name": { "name": "Device name", diff --git a/homeassistant/components/ifttt/strings.json b/homeassistant/components/ifttt/strings.json index e52a0882eb1b0d..5ba0812697f9e3 100644 --- a/homeassistant/components/ifttt/strings.json +++ b/homeassistant/components/ifttt/strings.json @@ -44,11 +44,11 @@ }, "value2": { "name": "Value 2", - "description": "Generic field to send data via the event." + "description": "[%key:component::ifttt::services::trigger::fields::value1::description%]" }, "value3": { "name": "Value 3", - "description": "Generic field to send data via the event." + "description": "[%key:component::ifttt::services::trigger::fields::value1::description%]" } } } diff --git a/homeassistant/components/ihc/strings.json b/homeassistant/components/ihc/strings.json index 3ee45a4f4649ec..af2152a88bb121 100644 --- a/homeassistant/components/ihc/strings.json +++ b/homeassistant/components/ihc/strings.json @@ -23,12 +23,12 @@ "description": "Sets an integer runtime value on the IHC controller.", "fields": { "controller_id": { - "name": "Controller ID", - "description": "If you have multiple controller, this is the index of you controller\nstarting with 0.\n." + "name": "[%key:component::ihc::services::set_runtime_value_bool::fields::controller_id::name%]", + "description": "[%key:component::ihc::services::set_runtime_value_bool::fields::controller_id::description%]" }, "ihc_id": { - "name": "IHC ID", - "description": "The integer IHC resource ID." + "name": "[%key:component::ihc::services::set_runtime_value_bool::fields::ihc_id::name%]", + "description": "[%key:component::ihc::services::set_runtime_value_bool::fields::ihc_id::description%]" }, "value": { "name": "Value", @@ -41,12 +41,12 @@ "description": "Sets a float runtime value on the IHC controller.", "fields": { "controller_id": { - "name": "Controller ID", - "description": "If you have multiple controller, this is the index of you controller\nstarting with 0.\n." + "name": "[%key:component::ihc::services::set_runtime_value_bool::fields::controller_id::name%]", + "description": "[%key:component::ihc::services::set_runtime_value_bool::fields::controller_id::description%]" }, "ihc_id": { - "name": "IHC ID", - "description": "The integer IHC resource ID." + "name": "[%key:component::ihc::services::set_runtime_value_bool::fields::ihc_id::name%]", + "description": "[%key:component::ihc::services::set_runtime_value_bool::fields::ihc_id::description%]" }, "value": { "name": "Value", @@ -59,12 +59,12 @@ "description": "Pulses an input on the IHC controller.", "fields": { "controller_id": { - "name": "Controller ID", - "description": "If you have multiple controller, this is the index of you controller\nstarting with 0.\n." + "name": "[%key:component::ihc::services::set_runtime_value_bool::fields::controller_id::name%]", + "description": "[%key:component::ihc::services::set_runtime_value_bool::fields::controller_id::description%]" }, "ihc_id": { - "name": "IHC ID", - "description": "The integer IHC resource ID." + "name": "[%key:component::ihc::services::set_runtime_value_bool::fields::ihc_id::name%]", + "description": "[%key:component::ihc::services::set_runtime_value_bool::fields::ihc_id::description%]" } } } diff --git a/homeassistant/components/insteon/strings.json b/homeassistant/components/insteon/strings.json index 3ba996adff772e..37cdd5c0343b89 100644 --- a/homeassistant/components/insteon/strings.json +++ b/homeassistant/components/insteon/strings.json @@ -76,7 +76,7 @@ } }, "add_override": { - "description": "Add a device override.", + "description": "[%key:component::insteon::options::step::init::menu_options::add_override%]", "data": { "address": "Device address (i.e. 1a2b3c)", "cat": "Device category (i.e. 0x10)", @@ -101,7 +101,7 @@ "remove_x10": { "description": "Remove an X10 device", "data": { - "address": "Select a device address to remove" + "address": "[%key:component::insteon::options::step::remove_override::data::address%]" } } }, @@ -120,7 +120,7 @@ "description": "All-Link group number." }, "mode": { - "name": "Mode", + "name": "[%key:common::config_flow::data::mode%]", "description": "Linking mode controller - IM is controller responder - IM is responder." } } @@ -131,7 +131,7 @@ "fields": { "group": { "name": "Group", - "description": "All-Link group number." + "description": "[%key:component::insteon::services::add_all_link::fields::group::description%]" } } }, @@ -165,7 +165,7 @@ }, "x10_all_units_off": { "name": "X10 all units off", - "description": "Tells the Insteom Modem (IM) start All-Linking mode. Once the IM is in All-Linking mode, press the link button on the device to complete All-Linking.", + "description": "[%key:component::insteon::services::add_all_link::description%]", "fields": { "housecode": { "name": "Housecode", @@ -178,8 +178,8 @@ "description": "Sends X10 All Lights On command.", "fields": { "housecode": { - "name": "Housecode", - "description": "X10 house code." + "name": "[%key:component::insteon::services::x10_all_units_off::fields::housecode::name%]", + "description": "[%key:component::insteon::services::x10_all_units_off::fields::housecode::description%]" } } }, @@ -188,8 +188,8 @@ "description": "Sends X10 All Lights Off command.", "fields": { "housecode": { - "name": "Housecode", - "description": "X10 house code." + "name": "[%key:component::insteon::services::x10_all_units_off::fields::housecode::name%]", + "description": "[%key:component::insteon::services::x10_all_units_off::fields::housecode::description%]" } } }, @@ -209,7 +209,7 @@ "fields": { "group": { "name": "Group", - "description": "INSTEON group or scene number." + "description": "[%key:component::insteon::services::scene_on::fields::group::description%]" } } }, @@ -219,7 +219,7 @@ "fields": { "entity_id": { "name": "Entity", - "description": "Name of the device to load. Use \"all\" to load the database of all devices." + "description": "[%key:component::insteon::services::load_all_link_database::fields::entity_id::description%]" } } } diff --git a/homeassistant/components/integration/strings.json b/homeassistant/components/integration/strings.json index 3a3940ffc2c4bd..74c2b3ee440641 100644 --- a/homeassistant/components/integration/strings.json +++ b/homeassistant/components/integration/strings.json @@ -7,7 +7,7 @@ "description": "Create a sensor that calculates a Riemann sum to estimate the integral of a sensor.", "data": { "method": "Integration method", - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "round": "Precision", "source": "Input sensor", "unit_prefix": "Metric prefix", diff --git a/homeassistant/components/iperf3/strings.json b/homeassistant/components/iperf3/strings.json index be8535daec67cb..4c6c68b9573735 100644 --- a/homeassistant/components/iperf3/strings.json +++ b/homeassistant/components/iperf3/strings.json @@ -5,7 +5,7 @@ "description": "Immediately executes a speed test with iperf3.", "fields": { "host": { - "name": "Host", + "name": "[%key:common::config_flow::data::host%]", "description": "The host name of the iperf3 server (already configured) to run a test with." } } diff --git a/homeassistant/components/ipp/strings.json b/homeassistant/components/ipp/strings.json index fa7dd9b6bf8a2a..f3ea929c9eccd3 100644 --- a/homeassistant/components/ipp/strings.json +++ b/homeassistant/components/ipp/strings.json @@ -37,7 +37,7 @@ "printer": { "state": { "printing": "Printing", - "idle": "Idle", + "idle": "[%key:common::state::idle%]", "stopped": "Stopped" } } diff --git a/homeassistant/components/isy994/strings.json b/homeassistant/components/isy994/strings.json index 542df60f13fd60..b39bad14d45e5a 100644 --- a/homeassistant/components/isy994/strings.json +++ b/homeassistant/components/isy994/strings.json @@ -36,7 +36,7 @@ "step": { "init": { "title": "ISY Options", - "description": "Set the options for the ISY Integration: \n \u2022 Node Sensor String: Any device or folder that contains 'Node Sensor String' in the name will be treated as a sensor or binary sensor. \n \u2022 Ignore String: Any device with 'Ignore String' in the name will be ignored. \n \u2022 Variable Sensor String: Any variable that contains 'Variable Sensor String' will be added as a sensor. \n \u2022 Restore Light Brightness: If enabled, the previous brightness will be restored when turning on a light instead of the device's built-in On-Level.", + "description": "Set the options for the ISY Integration: \n • Node Sensor String: Any device or folder that contains 'Node Sensor String' in the name will be treated as a sensor or binary sensor. \n • Ignore String: Any device with 'Ignore String' in the name will be ignored. \n • Variable Sensor String: Any variable that contains 'Variable Sensor String' will be added as a sensor. \n • Restore Light Brightness: If enabled, the previous brightness will be restored when turning on a light instead of the device's built-in On-Level.", "data": { "sensor_string": "Node Sensor String", "ignore_string": "Ignore String", @@ -57,7 +57,7 @@ "services": { "send_raw_node_command": { "name": "Send raw node command", - "description": "Set the options for the ISY Integration: \n \u2022 Node Sensor String: Any device or folder that contains 'Node Sensor String' in the name will be treated as a sensor or binary sensor. \n \u2022 Ignore String: Any device with 'Ignore String' in the name will be ignored. \n \u2022 Variable Sensor String: Any variable that contains 'Variable Sensor String' will be added as a sensor. \n \u2022 Restore Light Brightness: If enabled, the previous brightness will be restored when turning on a light instead of the device's built-in On-Level.", + "description": "[%key:component::isy994::options::step::init::description%]", "fields": { "command": { "name": "Command", @@ -102,7 +102,7 @@ "description": "Updates a Z-Wave Device parameter via the ISY. The parameter value will also be returned as a entity extra state attribute with the name \"ZW_#\" where \"#\" is the parameter number.", "fields": { "parameter": { - "name": "Parameter", + "name": "[%key:component::isy994::services::get_zwave_parameter::fields::parameter::name%]", "description": "The parameter number to set on the end device." }, "value": { @@ -134,8 +134,8 @@ "description": "Delete a Z-Wave Lock User Code via the ISY.", "fields": { "user_num": { - "name": "User Number", - "description": "The user slot number on the lock." + "name": "[%key:component::isy994::services::set_zwave_lock_user_code::fields::user_num::name%]", + "description": "[%key:component::isy994::services::set_zwave_lock_user_code::fields::user_num::description%]" } } }, @@ -158,7 +158,7 @@ "description": "The address of the program to control (use either address or name)." }, "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "The name of the program to control (use either address or name)." }, "command": { diff --git a/homeassistant/components/izone/strings.json b/homeassistant/components/izone/strings.json index 3906dcb89fe4fb..707d7d71d34965 100644 --- a/homeassistant/components/izone/strings.json +++ b/homeassistant/components/izone/strings.json @@ -26,8 +26,8 @@ "description": "Sets the airflow maximum percent for a zone.", "fields": { "airflow": { - "name": "Percent", - "description": "Airflow percent." + "name": "[%key:component::izone::services::airflow_min::fields::airflow::name%]", + "description": "[%key:component::izone::services::airflow_min::fields::airflow::description%]" } } } diff --git a/homeassistant/components/jvc_projector/strings.json b/homeassistant/components/jvc_projector/strings.json index 11e2f66f91ea7e..1f85c20fc72222 100644 --- a/homeassistant/components/jvc_projector/strings.json +++ b/homeassistant/components/jvc_projector/strings.json @@ -29,7 +29,7 @@ "error": { "invalid_host": "[%key:common::config_flow::error::invalid_host%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "Password authentication failed" + "invalid_auth": "[%key:component::jvc_projector::config::step::reauth_confirm::description%]" } } } diff --git a/homeassistant/components/kaleidescape/strings.json b/homeassistant/components/kaleidescape/strings.json index 30c22a8ca0ee11..0cebfd4bf5c110 100644 --- a/homeassistant/components/kaleidescape/strings.json +++ b/homeassistant/components/kaleidescape/strings.json @@ -19,7 +19,7 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unsupported": "Unsupported device" + "unsupported": "[%key:component::kaleidescape::config::abort::unsupported%]" } }, "entity": { diff --git a/homeassistant/components/kef/strings.json b/homeassistant/components/kef/strings.json index 7307caa6bb328f..e5ffff681627eb 100644 --- a/homeassistant/components/kef/strings.json +++ b/homeassistant/components/kef/strings.json @@ -49,8 +49,8 @@ "description": "Sets the \"Wall mode\" slider of the speaker in dB.", "fields": { "db_value": { - "name": "DB value", - "description": "Value of the slider." + "name": "[%key:component::kef::services::set_desk_db::fields::db_value::name%]", + "description": "[%key:component::kef::services::set_desk_db::fields::db_value::description%]" } } }, @@ -59,8 +59,8 @@ "description": "Sets desk the \"Treble trim\" slider of the speaker in dB.", "fields": { "db_value": { - "name": "DB value", - "description": "Value of the slider." + "name": "[%key:component::kef::services::set_desk_db::fields::db_value::name%]", + "description": "[%key:component::kef::services::set_desk_db::fields::db_value::description%]" } } }, @@ -70,7 +70,7 @@ "fields": { "hz_value": { "name": "Hertz value", - "description": "Value of the slider." + "description": "[%key:component::kef::services::set_desk_db::fields::db_value::description%]" } } }, @@ -79,8 +79,8 @@ "description": "Set the \"Sub out low-pass frequency\" slider of the speaker in Hz.", "fields": { "hz_value": { - "name": "Hertz value", - "description": "Value of the slider." + "name": "[%key:component::kef::services::set_high_hz::fields::hz_value::name%]", + "description": "[%key:component::kef::services::set_desk_db::fields::db_value::description%]" } } }, @@ -89,8 +89,8 @@ "description": "Set the \"Sub gain\" slider of the speaker in dB.", "fields": { "db_value": { - "name": "DB value", - "description": "Value of the slider." + "name": "[%key:component::kef::services::set_desk_db::fields::db_value::name%]", + "description": "[%key:component::kef::services::set_desk_db::fields::db_value::description%]" } } } diff --git a/homeassistant/components/keymitt_ble/strings.json b/homeassistant/components/keymitt_ble/strings.json index 57e7fc68582c8e..ab2d4ad9440686 100644 --- a/homeassistant/components/keymitt_ble/strings.json +++ b/homeassistant/components/keymitt_ble/strings.json @@ -6,7 +6,7 @@ "title": "Set up MicroBot device", "data": { "address": "Device address", - "name": "Name" + "name": "[%key:common::config_flow::data::name%]" } }, "link": { @@ -42,7 +42,7 @@ "description": "Duration in seconds." }, "mode": { - "name": "Mode", + "name": "[%key:common::config_flow::data::mode%]", "description": "Normal | invert | toggle." } } diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index 56ff9018530e3a..1ff008653d4752 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -75,7 +75,7 @@ }, "secure_routing_manual": { "title": "Secure routing", - "description": "Please enter your IP secure information.", + "description": "[%key:component::knx::config::step::secure_tunnel_manual::description%]", "data": { "backbone_key": "Backbone key", "sync_latency_tolerance": "Network latency tolerance" @@ -130,7 +130,7 @@ } }, "communication_settings": { - "title": "Communication settings", + "title": "[%key:component::knx::options::step::options_init::menu_options::communication_settings%]", "data": { "state_updater": "State updater", "rate_limit": "Rate limit", @@ -144,9 +144,9 @@ }, "connection_type": { "title": "[%key:component::knx::config::step::connection_type::title%]", - "description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing.", + "description": "[%key:component::knx::config::step::connection_type::description%]", "data": { - "connection_type": "KNX Connection Type" + "connection_type": "[%key:component::knx::config::step::connection_type::data::connection_type%]" } }, "tunnel": { @@ -259,7 +259,7 @@ "entity": { "sensor": { "individual_address": { - "name": "Individual address" + "name": "[%key:component::knx::config::step::routing::data::individual_address%]" }, "connected_since": { "name": "Connection established" @@ -317,7 +317,7 @@ "description": "Send GroupValueRead requests to the KNX bus. Response can be used from `knx_event` and will be processed in KNX entities.", "fields": { "address": { - "name": "Group address", + "name": "[%key:component::knx::services::send::fields::address::name%]", "description": "Group address(es) to send read request to. Lists will read multiple group addresses." } } @@ -327,7 +327,7 @@ "description": "Add or remove group addresses to knx_event filter for triggering `knx_event`s. Only addresses added with this service can be removed.", "fields": { "address": { - "name": "Group address", + "name": "[%key:component::knx::services::send::fields::address::name%]", "description": "Group address(es) that shall be added or removed. Lists are allowed." }, "type": { @@ -345,7 +345,7 @@ "description": "Adds or remove exposures to KNX bus. Only exposures added with this service can be removed.", "fields": { "address": { - "name": "Group address", + "name": "[%key:component::knx::services::send::fields::address::name%]", "description": "Group address state or attribute updates will be sent to. GroupValueRead requests will be answered. Per address only one exposure can be registered." }, "type": { @@ -358,11 +358,11 @@ }, "attribute": { "name": "Entity attribute", - "description": "Attribute of the entity that shall be sent to the KNX bus. If not set the state will be sent. Eg. for a light the state is eigther \u201con\u201d or \u201coff\u201d - with attribute you can expose its \u201cbrightness\u201d." + "description": "Attribute of the entity that shall be sent to the KNX bus. If not set the state will be sent. Eg. for a light the state is eigther “on” or “off” - with attribute you can expose its “brightness”." }, "default": { "name": "Default value", - "description": "Default value to send to the bus if the state or attribute value is None. Eg. a light with state \u201coff\u201d has no brightness attribute so a default value of 0 could be used. If not set (or None) no value would be sent to the bus and a GroupReadRequest to the address would return the last known value." + "description": "Default value to send to the bus if the state or attribute value is None. Eg. a light with state “off” has no brightness attribute so a default value of 0 could be used. If not set (or None) no value would be sent to the bus and a GroupReadRequest to the address would return the last known value." }, "remove": { "name": "Remove exposure", diff --git a/homeassistant/components/konnected/strings.json b/homeassistant/components/konnected/strings.json index cd08638c775a97..e1a6863a19999f 100644 --- a/homeassistant/components/konnected/strings.json +++ b/homeassistant/components/konnected/strings.json @@ -69,7 +69,7 @@ }, "options_digital": { "title": "Configure Digital Sensor", - "description": "{zone} options", + "description": "[%key:component::konnected::options::step::options_binary::description%]", "data": { "type": "Sensor Type", "name": "[%key:common::config_flow::data::name%]", @@ -103,7 +103,7 @@ "bad_host": "Invalid Override API host URL" }, "abort": { - "not_konn_panel": "Not a recognized Konnected.io device" + "not_konn_panel": "[%key:component::konnected::config::abort::not_konn_panel%]" } } } diff --git a/homeassistant/components/lametric/strings.json b/homeassistant/components/lametric/strings.json index ac06e125b0c302..21d2bdc84bd128 100644 --- a/homeassistant/components/lametric/strings.json +++ b/homeassistant/components/lametric/strings.json @@ -85,7 +85,7 @@ "description": "Displays a chart on a LaMetric device.", "fields": { "device_id": { - "name": "Device", + "name": "[%key:common::config_flow::data::device%]", "description": "The LaMetric device to display the chart on." }, "data": { @@ -207,7 +207,7 @@ }, "priority": { "options": { - "info": "Info", + "info": "[%key:component::lametric::selector::icon_type::options::info%]", "warning": "Warning", "critical": "Critical" } diff --git a/homeassistant/components/lastfm/strings.json b/homeassistant/components/lastfm/strings.json index fe9a4b6453fefd..006fd5ebcc7379 100644 --- a/homeassistant/components/lastfm/strings.json +++ b/homeassistant/components/lastfm/strings.json @@ -24,15 +24,15 @@ "options": { "step": { "init": { - "description": "Fill in other users you want to add.", + "description": "[%key:component::lastfm::config::step::friends::description%]", "data": { - "users": "Last.fm usernames" + "users": "[%key:component::lastfm::config::step::friends::data::users%]" } } }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "invalid_account": "Invalid username", + "invalid_account": "[%key:component::lastfm::config::error::invalid_account%]", "unknown": "[%key:common::config_flow::error::unknown%]" } } diff --git a/homeassistant/components/lcn/strings.json b/homeassistant/components/lcn/strings.json index 267100eaad631d..e441832926b87e 100644 --- a/homeassistant/components/lcn/strings.json +++ b/homeassistant/components/lcn/strings.json @@ -37,11 +37,11 @@ "fields": { "address": { "name": "Address", - "description": "Module address." + "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" }, "output": { - "name": "Output", - "description": "Output port." + "name": "[%key:component::lcn::services::output_abs::fields::output::name%]", + "description": "[%key:component::lcn::services::output_abs::fields::output::description%]" }, "brightness": { "name": "Brightness", @@ -55,15 +55,15 @@ "fields": { "address": { "name": "Address", - "description": "Module address." + "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" }, "output": { - "name": "Output", - "description": "Output port." + "name": "[%key:component::lcn::services::output_abs::fields::output::name%]", + "description": "[%key:component::lcn::services::output_abs::fields::output::description%]" }, "transition": { "name": "Transition", - "description": "Transition time." + "description": "[%key:component::lcn::services::output_abs::fields::transition::description%]" } } }, @@ -73,7 +73,7 @@ "fields": { "address": { "name": "Address", - "description": "Module address." + "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" }, "state": { "name": "State", @@ -87,10 +87,10 @@ "fields": { "address": { "name": "Address", - "description": "Module address." + "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" }, "led": { - "name": "LED", + "name": "[%key:component::lcn::services::led::name%]", "description": "Led." }, "state": { @@ -105,7 +105,7 @@ "fields": { "address": { "name": "Address", - "description": "Module address." + "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" }, "variable": { "name": "Variable", @@ -127,11 +127,11 @@ "fields": { "address": { "name": "Address", - "description": "Module address." + "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" }, "variable": { - "name": "Variable", - "description": "Variable or setpoint name." + "name": "[%key:component::lcn::services::var_abs::fields::variable::name%]", + "description": "[%key:component::lcn::services::var_abs::fields::variable::description%]" } } }, @@ -141,11 +141,11 @@ "fields": { "address": { "name": "Address", - "description": "Module address." + "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" }, "variable": { - "name": "Variable", - "description": "Variable or setpoint name." + "name": "[%key:component::lcn::services::var_abs::fields::variable::name%]", + "description": "[%key:component::lcn::services::var_abs::fields::variable::description%]" }, "value": { "name": "Value", @@ -153,7 +153,7 @@ }, "unit_of_measurement": { "name": "Unit of measurement", - "description": "Unit of value." + "description": "[%key:component::lcn::services::var_abs::fields::unit_of_measurement::description%]" }, "value_reference": { "name": "Reference value", @@ -167,7 +167,7 @@ "fields": { "address": { "name": "Address", - "description": "Module address." + "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" }, "setpoint": { "name": "Setpoint", @@ -185,7 +185,7 @@ "fields": { "address": { "name": "Address", - "description": "Module address." + "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" }, "keys": { "name": "Keys", @@ -211,7 +211,7 @@ "fields": { "address": { "name": "Address", - "description": "Module address." + "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" }, "table": { "name": "Table", @@ -226,7 +226,7 @@ "description": "Lock interval." }, "time_unit": { - "name": "Time unit", + "name": "[%key:component::lcn::services::send_keys::fields::time_unit::name%]", "description": "Time unit of lock interval." } } @@ -237,7 +237,7 @@ "fields": { "address": { "name": "Address", - "description": "Module address." + "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" }, "row": { "name": "Row", @@ -255,10 +255,10 @@ "fields": { "address": { "name": "Address", - "description": "Module address." + "description": "[%key:component::lcn::services::output_abs::fields::address::description%]" }, "pck": { - "name": "PCK", + "name": "[%key:component::lcn::services::pck::name%]", "description": "PCK command (without address header)." } } diff --git a/homeassistant/components/lifx/strings.json b/homeassistant/components/lifx/strings.json index dfbc0b4e384519..9d155ae32ae15b 100644 --- a/homeassistant/components/lifx/strings.json +++ b/homeassistant/components/lifx/strings.json @@ -10,7 +10,7 @@ }, "pick_device": { "data": { - "device": "Device" + "device": "[%key:common::config_flow::data::device%]" } }, "discovery_confirm": { @@ -88,7 +88,7 @@ "description": "Runs a flash effect by changing to a color and back.", "fields": { "mode": { - "name": "Mode", + "name": "[%key:common::config_flow::data::mode%]", "description": "Decides how colors are changed." }, "brightness": { @@ -142,7 +142,7 @@ "description": "Percentage indicating the maximum saturation of the colors in the loop." }, "period": { - "name": "Period", + "name": "[%key:component::lifx::services::effect_pulse::fields::period::name%]", "description": "Duration between color changes." }, "change": { @@ -155,7 +155,7 @@ }, "power_on": { "name": "Power on", - "description": "Powered off lights are temporarily turned on during the effect." + "description": "[%key:component::lifx::services::effect_pulse::fields::power_on::description%]" } } }, @@ -172,7 +172,7 @@ "description": "Direction the effect will move across the device." }, "theme": { - "name": "Theme", + "name": "[%key:component::lifx::entity::select::theme::name%]", "description": "(Optional) set one of the predefined themes onto the device before starting the effect." }, "power_on": { @@ -191,7 +191,7 @@ }, "power_on": { "name": "Power on", - "description": "Powered off lights will be turned on before starting the effect." + "description": "[%key:component::lifx::services::effect_move::fields::power_on::description%]" } } }, @@ -208,12 +208,12 @@ "description": "List of at least 2 and at most 16 colors as hue (0-360), saturation (0-100), brightness (0-100) and kelvin (1500-900) values to use for this effect. Overrides the theme attribute." }, "theme": { - "name": "Theme", + "name": "[%key:component::lifx::entity::select::theme::name%]", "description": "Predefined color theme to use for the effect. Overridden by the palette attribute." }, "power_on": { "name": "Power on", - "description": "Powered off lights will be turned on before starting the effect." + "description": "[%key:component::lifx::services::effect_move::fields::power_on::description%]" } } }, diff --git a/homeassistant/components/litterrobot/strings.json b/homeassistant/components/litterrobot/strings.json index fe9cc3b528ac61..8436d24902c7a0 100644 --- a/homeassistant/components/litterrobot/strings.json +++ b/homeassistant/components/litterrobot/strings.json @@ -124,7 +124,7 @@ }, "time": { "sleep_mode_start_time": { - "name": "Sleep mode start time" + "name": "[%key:component::litterrobot::entity::sensor::sleep_mode_start_time::name%]" } }, "vacuum": { diff --git a/homeassistant/components/local_ip/strings.json b/homeassistant/components/local_ip/strings.json index 7e214df25924c3..a4d9138d88e563 100644 --- a/homeassistant/components/local_ip/strings.json +++ b/homeassistant/components/local_ip/strings.json @@ -3,7 +3,7 @@ "config": { "step": { "user": { - "title": "Local IP Address", + "title": "[%key:component::local_ip::title%]", "description": "[%key:common::config_flow::description::confirm_setup%]" } }, diff --git a/homeassistant/components/logbook/strings.json b/homeassistant/components/logbook/strings.json index 10ebcc68f64b3e..aad9c122d2377e 100644 --- a/homeassistant/components/logbook/strings.json +++ b/homeassistant/components/logbook/strings.json @@ -5,7 +5,7 @@ "description": "Creates a custom entry in the logbook.", "fields": { "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "Custom name for an entity, can be referenced using an `entity_id`." }, "message": { diff --git a/homeassistant/components/logi_circle/strings.json b/homeassistant/components/logi_circle/strings.json index 9a06fb45ad24ad..4f641238a498e5 100644 --- a/homeassistant/components/logi_circle/strings.json +++ b/homeassistant/components/logi_circle/strings.json @@ -35,7 +35,7 @@ "description": "Name(s) of entities to apply the operation mode to." }, "mode": { - "name": "Mode", + "name": "[%key:common::config_flow::data::mode%]", "description": "Operation mode. Allowed values: LED, RECORDING_MODE." }, "value": { @@ -68,7 +68,7 @@ }, "filename": { "name": "File name", - "description": "Template of a Filename. Variable is entity_id." + "description": "[%key:component::logi_circle::services::livestream_snapshot::fields::filename::description%]" }, "duration": { "name": "Duration", diff --git a/homeassistant/components/lovelace/strings.json b/homeassistant/components/lovelace/strings.json index 64718308325786..d0e456f142bfca 100644 --- a/homeassistant/components/lovelace/strings.json +++ b/homeassistant/components/lovelace/strings.json @@ -2,7 +2,7 @@ "system_health": { "info": { "dashboards": "Dashboards", - "mode": "Mode", + "mode": "[%key:common::config_flow::data::mode%]", "resources": "Resources", "views": "Views" } diff --git a/homeassistant/components/lutron_caseta/strings.json b/homeassistant/components/lutron_caseta/strings.json index bc546321da3fc9..b5ec175d1c9c2a 100644 --- a/homeassistant/components/lutron_caseta/strings.json +++ b/homeassistant/components/lutron_caseta/strings.json @@ -40,9 +40,9 @@ "group_1_button_2": "First Group second button", "group_2_button_1": "Second Group first button", "group_2_button_2": "Second Group second button", - "on": "On", + "on": "[%key:common::state::on%]", "stop": "Stop (favorite)", - "off": "Off", + "off": "[%key:common::state::off%]", "raise": "Raise", "lower": "Lower", "open_all": "Open all", diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index 3d5ae9b6a61e13..61f1ca9180a28b 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -57,7 +57,7 @@ "description": "Allows adding one of your devices to another Matter network by opening the commissioning window for this Matter device for 60 seconds.", "fields": { "device_id": { - "name": "Device", + "name": "[%key:common::config_flow::data::device%]", "description": "The Matter device to add to the other Matter network." } } diff --git a/homeassistant/components/mazda/strings.json b/homeassistant/components/mazda/strings.json index 9c881e6324fc4c..a714d1af00f798 100644 --- a/homeassistant/components/mazda/strings.json +++ b/homeassistant/components/mazda/strings.json @@ -31,11 +31,11 @@ "description": "The vehicle to send the GPS location to." }, "latitude": { - "name": "Latitude", + "name": "[%key:common::config_flow::data::latitude%]", "description": "The latitude of the location to send." }, "longitude": { - "name": "Longitude", + "name": "[%key:common::config_flow::data::longitude%]", "description": "The longitude of the location to send." }, "poi_name": { diff --git a/homeassistant/components/meteo_france/strings.json b/homeassistant/components/meteo_france/strings.json index 3ff8d4308a3a25..944f2b32fab883 100644 --- a/homeassistant/components/meteo_france/strings.json +++ b/homeassistant/components/meteo_france/strings.json @@ -10,7 +10,7 @@ "cities": { "description": "Choose your city from the list", "data": { - "city": "City" + "city": "[%key:component::meteo_france::config::step::user::data::city%]" } } }, diff --git a/homeassistant/components/microsoft_face/strings.json b/homeassistant/components/microsoft_face/strings.json index b1008336992f05..4357276a65041d 100644 --- a/homeassistant/components/microsoft_face/strings.json +++ b/homeassistant/components/microsoft_face/strings.json @@ -5,7 +5,7 @@ "description": "Creates a new person group.", "fields": { "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "Name of the group." } } @@ -19,7 +19,7 @@ "description": "Name of the group." }, "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "Name of the person." } } @@ -29,7 +29,7 @@ "description": "Deletes a new person group.", "fields": { "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "Name of the group." } } @@ -43,8 +43,8 @@ "description": "Name of the group." }, "name": { - "name": "Name", - "description": "Name of the person." + "name": "[%key:common::config_flow::data::name%]", + "description": "[%key:component::microsoft_face::services::create_person::fields::name::description%]" } } }, @@ -62,7 +62,7 @@ }, "person": { "name": "Person", - "description": "Name of the person." + "description": "[%key:component::microsoft_face::services::create_person::fields::name::description%]" } } }, diff --git a/homeassistant/components/min_max/strings.json b/homeassistant/components/min_max/strings.json index ce18a4d153f014..e73fac97bb7e26 100644 --- a/homeassistant/components/min_max/strings.json +++ b/homeassistant/components/min_max/strings.json @@ -3,11 +3,11 @@ "config": { "step": { "user": { - "title": "Combine the state of several sensors", + "title": "[%key:component::min_max::title%]", "description": "Create a sensor that calculates a min, max, mean, median or sum from a list of input sensors.", "data": { "entity_ids": "Input entities", - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "round_digits": "Precision", "type": "Statistic characteristic" }, diff --git a/homeassistant/components/minio/strings.json b/homeassistant/components/minio/strings.json index 21902ad1825b8a..75b8375adb1aee 100644 --- a/homeassistant/components/minio/strings.json +++ b/homeassistant/components/minio/strings.json @@ -23,16 +23,16 @@ "description": "Uploads file to Minio.", "fields": { "bucket": { - "name": "Bucket", - "description": "Bucket to use." + "name": "[%key:component::minio::services::get::fields::bucket::name%]", + "description": "[%key:component::minio::services::get::fields::bucket::description%]" }, "key": { "name": "Key", - "description": "Object key of the file." + "description": "[%key:component::minio::services::get::fields::key::description%]" }, "file_path": { "name": "File path", - "description": "File path on local filesystem." + "description": "[%key:component::minio::services::get::fields::file_path::description%]" } } }, @@ -41,12 +41,12 @@ "description": "Deletes file from Minio.", "fields": { "bucket": { - "name": "Bucket", - "description": "Bucket to use." + "name": "[%key:component::minio::services::get::fields::bucket::name%]", + "description": "[%key:component::minio::services::get::fields::bucket::description%]" }, "key": { "name": "Key", - "description": "Object key of the file." + "description": "[%key:component::minio::services::get::fields::key::description%]" } } } diff --git a/homeassistant/components/mjpeg/strings.json b/homeassistant/components/mjpeg/strings.json index 73e6a150a09feb..0e1e71fd82c6c0 100644 --- a/homeassistant/components/mjpeg/strings.json +++ b/homeassistant/components/mjpeg/strings.json @@ -24,10 +24,10 @@ "step": { "init": { "data": { - "mjpeg_url": "MJPEG URL", + "mjpeg_url": "[%key:component::mjpeg::config::step::user::data::mjpeg_url%]", "name": "[%key:common::config_flow::data::name%]", "password": "[%key:common::config_flow::data::password%]", - "still_image_url": "Still Image URL", + "still_image_url": "[%key:component::mjpeg::config::step::user::data::still_image_url%]", "username": "[%key:common::config_flow::data::username%]", "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" } diff --git a/homeassistant/components/modbus/strings.json b/homeassistant/components/modbus/strings.json index c9cf755ad13416..61694074d7920f 100644 --- a/homeassistant/components/modbus/strings.json +++ b/homeassistant/components/modbus/strings.json @@ -31,20 +31,20 @@ "description": "Writes to a modbus holding register.", "fields": { "address": { - "name": "Address", + "name": "[%key:component::modbus::services::write_coil::fields::address::name%]", "description": "Address of the holding register to write to." }, "slave": { - "name": "Slave", - "description": "Address of the modbus unit/slave." + "name": "[%key:component::modbus::services::write_coil::fields::slave::name%]", + "description": "[%key:component::modbus::services::write_coil::fields::slave::description%]" }, "value": { "name": "Value", "description": "Value (single value or array) to write." }, "hub": { - "name": "Hub", - "description": "Modbus hub name." + "name": "[%key:component::modbus::services::write_coil::fields::hub::name%]", + "description": "[%key:component::modbus::services::write_coil::fields::hub::description%]" } } }, @@ -53,8 +53,8 @@ "description": "Stops modbus hub.", "fields": { "hub": { - "name": "Hub", - "description": "Modbus hub name." + "name": "[%key:component::modbus::services::write_coil::fields::hub::name%]", + "description": "[%key:component::modbus::services::write_coil::fields::hub::description%]" } } }, @@ -63,8 +63,8 @@ "description": "Restarts modbus hub (if running stop then start).", "fields": { "hub": { - "name": "Hub", - "description": "Modbus hub name." + "name": "[%key:component::modbus::services::write_coil::fields::hub::name%]", + "description": "[%key:component::modbus::services::write_coil::fields::hub::description%]" } } } diff --git a/homeassistant/components/modem_callerid/strings.json b/homeassistant/components/modem_callerid/strings.json index bb6ac1879da9f7..2e18ba3654f83e 100644 --- a/homeassistant/components/modem_callerid/strings.json +++ b/homeassistant/components/modem_callerid/strings.json @@ -9,7 +9,7 @@ } }, "usb_confirm": { - "description": "This is an integration for landline calls using a CX93001 voice modem. This can retrieve caller ID information with an option to reject an incoming call." + "description": "[%key:component::modem_callerid::config::step::user::description%]" } }, "error": { diff --git a/homeassistant/components/modern_forms/strings.json b/homeassistant/components/modern_forms/strings.json index 397d7267bc092e..defe412e96dd83 100644 --- a/homeassistant/components/modern_forms/strings.json +++ b/homeassistant/components/modern_forms/strings.json @@ -41,8 +41,8 @@ "description": "Sets a sleep timer on a Modern Forms fan.", "fields": { "sleep_time": { - "name": "Sleep time", - "description": "Number of minutes to set the timer." + "name": "[%key:component::modern_forms::services::set_light_sleep_timer::fields::sleep_time::name%]", + "description": "[%key:component::modern_forms::services::set_light_sleep_timer::fields::sleep_time::description%]" } } }, diff --git a/homeassistant/components/monoprice/strings.json b/homeassistant/components/monoprice/strings.json index 4ecf4cfee45caa..003531518dc950 100644 --- a/homeassistant/components/monoprice/strings.json +++ b/homeassistant/components/monoprice/strings.json @@ -27,12 +27,12 @@ "init": { "title": "Configure sources", "data": { - "source_1": "Name of source #1", - "source_2": "Name of source #2", - "source_3": "Name of source #3", - "source_4": "Name of source #4", - "source_5": "Name of source #5", - "source_6": "Name of source #6" + "source_1": "[%key:component::monoprice::config::step::user::data::source_1%]", + "source_2": "[%key:component::monoprice::config::step::user::data::source_2%]", + "source_3": "[%key:component::monoprice::config::step::user::data::source_3%]", + "source_4": "[%key:component::monoprice::config::step::user::data::source_4%]", + "source_5": "[%key:component::monoprice::config::step::user::data::source_5%]", + "source_6": "[%key:component::monoprice::config::step::user::data::source_6%]" } } } diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index ae47b33774d655..f314ddd47d3c4b 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -178,7 +178,7 @@ "description": "Writes all messages on a specific topic into the `mqtt_dump.txt` file in your configuration folder.", "fields": { "topic": { - "name": "Topic", + "name": "[%key:component::mqtt::services::publish::fields::topic::name%]", "description": "Topic to listen to." }, "duration": { diff --git a/homeassistant/components/mysensors/strings.json b/homeassistant/components/mysensors/strings.json index 7e0ff2c99d6e36..30fe5f46d6bdd8 100644 --- a/homeassistant/components/mysensors/strings.json +++ b/homeassistant/components/mysensors/strings.json @@ -29,7 +29,7 @@ "data": { "device": "Serial port", "baud_rate": "baud rate", - "version": "MySensors version", + "version": "[%key:component::mysensors::config::step::gw_tcp::data::version%]", "persistence_file": "Persistence file (leave empty to auto-generate)" } }, @@ -39,8 +39,8 @@ "retain": "MQTT retain", "topic_in_prefix": "Prefix for input topics (topic_in_prefix)", "topic_out_prefix": "Prefix for output topics (topic_out_prefix)", - "version": "MySensors version", - "persistence_file": "Persistence file (leave empty to auto-generate)" + "version": "[%key:component::mysensors::config::step::gw_tcp::data::version%]", + "persistence_file": "[%key:component::mysensors::config::step::gw_serial::data::persistence_file%]" } } }, @@ -67,20 +67,20 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "invalid_subscribe_topic": "Invalid subscribe topic", - "invalid_publish_topic": "Invalid publish topic", - "duplicate_topic": "Topic already in use", - "same_topic": "Subscribe and publish topics are the same", - "invalid_port": "Invalid port number", - "invalid_persistence_file": "Invalid persistence file", - "duplicate_persistence_file": "Persistence file already in use", + "invalid_subscribe_topic": "[%key:component::mysensors::config::error::invalid_subscribe_topic%]", + "invalid_publish_topic": "[%key:component::mysensors::config::error::invalid_publish_topic%]", + "duplicate_topic": "[%key:component::mysensors::config::error::duplicate_topic%]", + "same_topic": "[%key:component::mysensors::config::error::same_topic%]", + "invalid_port": "[%key:component::mysensors::config::error::invalid_port%]", + "invalid_persistence_file": "[%key:component::mysensors::config::error::invalid_persistence_file%]", + "duplicate_persistence_file": "[%key:component::mysensors::config::error::duplicate_persistence_file%]", "invalid_ip": "Invalid IP address", - "invalid_serial": "Invalid serial port", - "invalid_device": "Invalid device", - "invalid_version": "Invalid MySensors version", + "invalid_serial": "[%key:component::mysensors::config::error::invalid_serial%]", + "invalid_device": "[%key:component::mysensors::config::error::invalid_device%]", + "invalid_version": "[%key:component::mysensors::config::error::invalid_version%]", "mqtt_required": "The MQTT integration is not set up", - "not_a_number": "Please enter a number", - "port_out_of_range": "Port number must be at least 1 and at most 65535", + "not_a_number": "[%key:component::mysensors::config::error::not_a_number%]", + "port_out_of_range": "[%key:component::mysensors::config::error::port_out_of_range%]", "unknown": "[%key:common::config_flow::error::unknown%]" } } diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index b6941f51392ff3..2c2def6b7a38d0 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -29,7 +29,7 @@ "title": "Configure Google Cloud", "description": "Visit the [Cloud Console]({url}) to find your Google Cloud Project ID.", "data": { - "cloud_project_id": "Google Cloud Project ID" + "cloud_project_id": "[%key:component::nest::config::step::cloud_project::data::cloud_project_id%]" } }, "reauth_confirm": { @@ -101,8 +101,8 @@ "description": "Unique ID for the trip. Default is auto-generated using a timestamp." }, "structure": { - "name": "Structure", - "description": "Name(s) of structure(s) to change. Defaults to all structures if not specified." + "name": "[%key:component::nest::services::set_away_mode::fields::structure::name%]", + "description": "[%key:component::nest::services::set_away_mode::fields::structure::description%]" } } }, @@ -111,12 +111,12 @@ "description": "Cancels an existing estimated time of arrival window for a Nest structure.", "fields": { "trip_id": { - "name": "Trip ID", + "name": "[%key:component::nest::services::set_eta::fields::trip_id::name%]", "description": "Unique ID for the trip." }, "structure": { - "name": "Structure", - "description": "Name(s) of structure(s) to change. Defaults to all structures if not specified." + "name": "[%key:component::nest::services::set_away_mode::fields::structure::name%]", + "description": "[%key:component::nest::services::set_away_mode::fields::structure::description%]" } } } diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index 05d0e716ef464e..e9125f33016743 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -41,13 +41,13 @@ "weather_areas": "Weather areas" }, "description": "Configure public weather sensors.", - "title": "Netatmo public weather sensor" + "title": "[%key:component::netatmo::options::step::public_weather::title%]" } } }, "device_automation": { "trigger_subtype": { - "away": "Away", + "away": "[%key:common::state::not_home%]", "schedule": "Schedule", "hg": "Frost guard" }, @@ -83,7 +83,7 @@ "description": "Sets the heating schedule for Netatmo climate device. The schedule name must match a schedule configured at Netatmo.", "fields": { "schedule_name": { - "name": "Schedule", + "name": "[%key:component::netatmo::device_automation::trigger_subtype::schedule%]", "description": "Schedule name." } } diff --git a/homeassistant/components/netgear_lte/strings.json b/homeassistant/components/netgear_lte/strings.json index 9c4c67bddf7ba1..1fd1028299140b 100644 --- a/homeassistant/components/netgear_lte/strings.json +++ b/homeassistant/components/netgear_lte/strings.json @@ -5,7 +5,7 @@ "description": "Deletes messages from the modem inbox.", "fields": { "host": { - "name": "Host", + "name": "[%key:common::config_flow::data::host%]", "description": "The modem that should have a message deleted." }, "sms_id": { @@ -19,7 +19,7 @@ "description": "Sets options on the modem.", "fields": { "host": { - "name": "Host", + "name": "[%key:common::config_flow::data::host%]", "description": "The modem to set options on." }, "failover": { @@ -37,7 +37,7 @@ "description": "Asks the modem to establish the LTE connection.", "fields": { "host": { - "name": "Host", + "name": "[%key:common::config_flow::data::host%]", "description": "The modem that should connect." } } @@ -47,7 +47,7 @@ "description": "Asks the modem to close the LTE connection.", "fields": { "host": { - "name": "Host", + "name": "[%key:common::config_flow::data::host%]", "description": "The modem that should disconnect." } } diff --git a/homeassistant/components/nibe_heatpump/strings.json b/homeassistant/components/nibe_heatpump/strings.json index a863b9596b11e7..6fa421e0855a6c 100644 --- a/homeassistant/components/nibe_heatpump/strings.json +++ b/homeassistant/components/nibe_heatpump/strings.json @@ -22,7 +22,7 @@ "nibegw": { "description": "Before attempting to configure the integration, verify that:\n - The NibeGW unit is connected to a heat pump.\n - The MODBUS40 accessory has been enabled in the heat pump configuration.\n - The pump has not gone into an alarm state about missing MODBUS40 accessory.", "data": { - "model": "Model of Heat Pump", + "model": "[%key:component::nibe_heatpump::config::step::modbus::data::model%]", "ip_address": "Remote address", "remote_read_port": "Remote read port", "remote_write_port": "Remote write port", diff --git a/homeassistant/components/nina/strings.json b/homeassistant/components/nina/strings.json index 23a1fb8dfa6b22..e145f5ea8caea9 100644 --- a/homeassistant/components/nina/strings.json +++ b/homeassistant/components/nina/strings.json @@ -29,19 +29,19 @@ "init": { "title": "Options", "data": { - "_a_to_d": "City/county (A-D)", - "_e_to_h": "City/county (E-H)", - "_i_to_l": "City/county (I-L)", - "_m_to_q": "City/county (M-Q)", - "_r_to_u": "City/county (R-U)", - "_v_to_z": "City/county (V-Z)", - "slots": "Maximum warnings per city/county", - "headline_filter": "Blacklist regex to filter warning headlines" + "_a_to_d": "[%key:component::nina::config::step::user::data::_a_to_d%]", + "_e_to_h": "[%key:component::nina::config::step::user::data::_e_to_h%]", + "_i_to_l": "[%key:component::nina::config::step::user::data::_i_to_l%]", + "_m_to_q": "[%key:component::nina::config::step::user::data::_m_to_q%]", + "_r_to_u": "[%key:component::nina::config::step::user::data::_r_to_u%]", + "_v_to_z": "[%key:component::nina::config::step::user::data::_v_to_z%]", + "slots": "[%key:component::nina::config::step::user::data::slots%]", + "headline_filter": "[%key:component::nina::config::step::user::data::headline_filter%]" } } }, "error": { - "no_selection": "Please select at least one city/county", + "no_selection": "[%key:component::nina::config::error::no_selection%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" } diff --git a/homeassistant/components/nissan_leaf/strings.json b/homeassistant/components/nissan_leaf/strings.json index 4dae6cb898b784..d733e39a0fc99b 100644 --- a/homeassistant/components/nissan_leaf/strings.json +++ b/homeassistant/components/nissan_leaf/strings.json @@ -15,8 +15,8 @@ "description": "Fetches the last state of the vehicle of all your accounts, requesting an update from of the state from the car if possible.\n.", "fields": { "vin": { - "name": "VIN", - "description": "The vehicle identification number (VIN) of the vehicle, 17 characters\n." + "name": "[%key:component::nissan_leaf::services::start_charge::fields::vin::name%]", + "description": "[%key:component::nissan_leaf::services::start_charge::fields::vin::description%]" } } } diff --git a/homeassistant/components/nx584/strings.json b/homeassistant/components/nx584/strings.json index 11f94e7a72c538..b3d0381527810d 100644 --- a/homeassistant/components/nx584/strings.json +++ b/homeassistant/components/nx584/strings.json @@ -15,7 +15,7 @@ "description": "Un-Bypasses a zone.", "fields": { "zone": { - "name": "Zone", + "name": "[%key:component::nx584::services::bypass_zone::fields::zone::name%]", "description": "The number of the zone to be un-bypassed." } } diff --git a/homeassistant/components/ombi/strings.json b/homeassistant/components/ombi/strings.json index 70a3767c889e97..2cf18248ab834c 100644 --- a/homeassistant/components/ombi/strings.json +++ b/homeassistant/components/ombi/strings.json @@ -5,7 +5,7 @@ "description": "Searches for a movie and requests the first result.", "fields": { "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "Search parameter." } } @@ -15,8 +15,8 @@ "description": "Searches for a TV show and requests the first result.", "fields": { "name": { - "name": "Name", - "description": "Search parameter." + "name": "[%key:common::config_flow::data::name%]", + "description": "[%key:component::ombi::services::submit_movie_request::fields::name::description%]" }, "season": { "name": "Season", @@ -29,8 +29,8 @@ "description": "Searches for a music album and requests the first result.", "fields": { "name": { - "name": "Name", - "description": "Search parameter." + "name": "[%key:common::config_flow::data::name%]", + "description": "[%key:component::ombi::services::submit_movie_request::fields::name::description%]" } } } diff --git a/homeassistant/components/onewire/strings.json b/homeassistant/components/onewire/strings.json index 2a7bd307ff880f..f58731a2377acc 100644 --- a/homeassistant/components/onewire/strings.json +++ b/homeassistant/components/onewire/strings.json @@ -251,7 +251,7 @@ "device_selection": { "data": { "clear_device_options": "Clear all device configurations", - "device_selection": "Select devices to configure" + "device_selection": "[%key:component::onewire::options::error::device_not_selected%]" }, "description": "Select what configuration steps to process", "title": "OneWire Device Options" diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json index d23fe1c09246d4..a5b8395b56bd7f 100644 --- a/homeassistant/components/opentherm_gw/strings.json +++ b/homeassistant/components/opentherm_gw/strings.json @@ -44,8 +44,8 @@ "description": "Sets the central heating override option on the gateway. When overriding the control setpoint (via a set_control_setpoint service call with a value other than 0), the gateway automatically enables the central heating override to start heating. This service can then be used to control the central heating override status. To return control of the central heating to the thermostat, call the set_control_setpoint service with temperature value 0. You will only need this if you are writing your own software thermostat.\n.", "fields": { "gateway_id": { - "name": "Gateway ID", - "description": "The gateway_id of the OpenTherm Gateway." + "name": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::name%]", + "description": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::description%]" }, "ch_override": { "name": "Central heating override", @@ -58,8 +58,8 @@ "description": "Sets the clock and day of the week on the connected thermostat.", "fields": { "gateway_id": { - "name": "Gateway ID", - "description": "The gateway_id of the OpenTherm Gateway." + "name": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::name%]", + "description": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::description%]" }, "date": { "name": "Date", @@ -76,8 +76,8 @@ "description": "Sets the central heating control setpoint override on the gateway. You will only need this if you are writing your own software thermostat.\n.", "fields": { "gateway_id": { - "name": "Gateway ID", - "description": "The gateway_id of the OpenTherm Gateway." + "name": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::name%]", + "description": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::description%]" }, "temperature": { "name": "Temperature", @@ -90,8 +90,8 @@ "description": "Sets the domestic hot water enable option on the gateway.", "fields": { "gateway_id": { - "name": "Gateway ID", - "description": "The gateway_id of the OpenTherm Gateway." + "name": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::name%]", + "description": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::description%]" }, "dhw_override": { "name": "Domestic hot water override", @@ -104,8 +104,8 @@ "description": "Sets the domestic hot water setpoint on the gateway.", "fields": { "gateway_id": { - "name": "Gateway ID", - "description": "The gateway_id of the OpenTherm Gateway." + "name": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::name%]", + "description": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::description%]" }, "temperature": { "name": "Temperature", @@ -118,15 +118,15 @@ "description": "Changes the function of the GPIO pins of the gateway.", "fields": { "gateway_id": { - "name": "Gateway ID", - "description": "The gateway_id of the OpenTherm Gateway." + "name": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::name%]", + "description": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::description%]" }, "id": { "name": "ID", "description": "The ID of the GPIO pin." }, "mode": { - "name": "Mode", + "name": "[%key:common::config_flow::data::mode%]", "description": "Mode to set on the GPIO pin. Values 0 through 6 are accepted for both GPIOs, 7 is only accepted for GPIO \"B\". See https://www.home-assistant.io/integrations/opentherm_gw/#gpio-modes for an explanation of the values.\n." } } @@ -136,15 +136,15 @@ "description": "Changes the function of the LEDs of the gateway.", "fields": { "gateway_id": { - "name": "Gateway ID", - "description": "The gateway_id of the OpenTherm Gateway." + "name": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::name%]", + "description": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::description%]" }, "id": { "name": "ID", "description": "The ID of the LED." }, "mode": { - "name": "Mode", + "name": "[%key:common::config_flow::data::mode%]", "description": "The function to assign to the LED. See https://www.home-assistant.io/integrations/opentherm_gw/#led-modes for an explanation of the values.\n." } } @@ -154,8 +154,8 @@ "description": "Overrides the maximum relative modulation level. You will only need this if you are writing your own software thermostat.\n.", "fields": { "gateway_id": { - "name": "Gateway ID", - "description": "The gateway_id of the OpenTherm Gateway." + "name": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::name%]", + "description": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::description%]" }, "level": { "name": "Level", @@ -168,8 +168,8 @@ "description": "Provides an outside temperature to the thermostat. If your thermostat is unable to display an outside temperature and does not support OTC (Outside Temperature Correction), this has no effect.\n.", "fields": { "gateway_id": { - "name": "Gateway ID", - "description": "The gateway_id of the OpenTherm Gateway." + "name": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::name%]", + "description": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::description%]" }, "temperature": { "name": "Temperature", @@ -182,8 +182,8 @@ "description": "Configures the setback temperature to be used with the GPIO away mode function.", "fields": { "gateway_id": { - "name": "Gateway ID", - "description": "The gateway_id of the OpenTherm Gateway." + "name": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::name%]", + "description": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::description%]" }, "temperature": { "name": "Temperature", diff --git a/homeassistant/components/openweathermap/strings.json b/homeassistant/components/openweathermap/strings.json index 12d5c3e21f6c77..a29a8952434a91 100644 --- a/homeassistant/components/openweathermap/strings.json +++ b/homeassistant/components/openweathermap/strings.json @@ -15,7 +15,7 @@ "latitude": "[%key:common::config_flow::data::latitude%]", "longitude": "[%key:common::config_flow::data::longitude%]", "mode": "[%key:common::config_flow::data::mode%]", - "name": "Name" + "name": "[%key:common::config_flow::data::name%]" }, "description": "To generate API key go to https://openweathermap.org/appid" } diff --git a/homeassistant/components/overkiz/strings.json b/homeassistant/components/overkiz/strings.json index a82284c24af2d6..c4daf32499ac5d 100644 --- a/homeassistant/components/overkiz/strings.json +++ b/homeassistant/components/overkiz/strings.json @@ -47,7 +47,7 @@ }, "fan_mode": { "state": { - "away": "Away", + "away": "[%key:common::state::not_home%]", "bypass_boost": "Bypass boost", "home_boost": "Home boost", "kitchen_boost": "Kitchen boost" diff --git a/homeassistant/components/persistent_notification/strings.json b/homeassistant/components/persistent_notification/strings.json index 6b8ddb46c497be..5f256233149941 100644 --- a/homeassistant/components/persistent_notification/strings.json +++ b/homeassistant/components/persistent_notification/strings.json @@ -23,7 +23,7 @@ "description": "Removes a notification from the **Notifications** panel.", "fields": { "notification_id": { - "name": "Notification ID", + "name": "[%key:component::persistent_notification::services::create::fields::notification_id::name%]", "description": "ID of the notification to be removed." } } diff --git a/homeassistant/components/profiler/strings.json b/homeassistant/components/profiler/strings.json index 7b9f6789c79bc7..b9aae585d9f596 100644 --- a/homeassistant/components/profiler/strings.json +++ b/homeassistant/components/profiler/strings.json @@ -60,7 +60,7 @@ "fields": { "scan_interval": { "name": "Scan interval", - "description": "The number of seconds between logging objects." + "description": "[%key:component::profiler::services::start_log_objects::fields::scan_interval::description%]" }, "max_objects": { "name": "Maximum objects", diff --git a/homeassistant/components/prusalink/strings.json b/homeassistant/components/prusalink/strings.json index 53f5f0153fe2d5..aa992b4874f0b3 100644 --- a/homeassistant/components/prusalink/strings.json +++ b/homeassistant/components/prusalink/strings.json @@ -20,8 +20,8 @@ "printer_state": { "state": { "cancelling": "Cancelling", - "idle": "Idle", - "paused": "Paused", + "idle": "[%key:common::state::idle%]", + "paused": "[%key:common::state::paused%]", "pausing": "Pausing", "printing": "Printing" } diff --git a/homeassistant/components/purpleair/strings.json b/homeassistant/components/purpleair/strings.json index 5e7c61c182053c..ff505010713607 100644 --- a/homeassistant/components/purpleair/strings.json +++ b/homeassistant/components/purpleair/strings.json @@ -93,7 +93,7 @@ } }, "settings": { - "title": "Settings", + "title": "[%key:component::purpleair::options::step::init::menu_options::settings%]", "data": { "show_on_map": "Show configured sensor locations on the map" } diff --git a/homeassistant/components/qnap/strings.json b/homeassistant/components/qnap/strings.json index 26ca5dedd34422..64b3f22293aee4 100644 --- a/homeassistant/components/qnap/strings.json +++ b/homeassistant/components/qnap/strings.json @@ -6,9 +6,9 @@ "description": "This qnap sensor allows getting various statistics from your QNAP NAS.", "data": { "host": "Hostname", - "username": "Username", - "password": "Password", - "port": "Port", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]", "ssl": "Enable SSL", "verify_ssl": "Verify SSL" } diff --git a/homeassistant/components/qvr_pro/strings.json b/homeassistant/components/qvr_pro/strings.json index 6f37bcce85e185..de61d38ffea376 100644 --- a/homeassistant/components/qvr_pro/strings.json +++ b/homeassistant/components/qvr_pro/strings.json @@ -15,7 +15,7 @@ "description": "Stops QVR Pro recording on specified channel.", "fields": { "guid": { - "name": "GUID", + "name": "[%key:component::qvr_pro::services::start_record::fields::guid::name%]", "description": "GUID of the channel to stop recording." } } diff --git a/homeassistant/components/rachio/strings.json b/homeassistant/components/rachio/strings.json index 3d776193432e80..2132cab86823fa 100644 --- a/homeassistant/components/rachio/strings.json +++ b/homeassistant/components/rachio/strings.json @@ -67,7 +67,7 @@ "description": "Resume any paused zone runs or schedules.", "fields": { "devices": { - "name": "Devices", + "name": "[%key:component::rachio::services::pause_watering::fields::devices::name%]", "description": "Name of controllers to resume. Defaults to all controllers on the account if not provided." } } @@ -77,7 +77,7 @@ "description": "Stop any currently running zones or schedules.", "fields": { "devices": { - "name": "Devices", + "name": "[%key:component::rachio::services::pause_watering::fields::devices::name%]", "description": "Name of controllers to stop. Defaults to all controllers on the account if not provided." } } diff --git a/homeassistant/components/rainbird/strings.json b/homeassistant/components/rainbird/strings.json index 9f4d0c2e34dc16..6046189ddc485e 100644 --- a/homeassistant/components/rainbird/strings.json +++ b/homeassistant/components/rainbird/strings.json @@ -21,7 +21,7 @@ "options": { "step": { "init": { - "title": "Configure Rain Bird", + "title": "[%key:component::rainbird::config::step::user::title%]", "data": { "duration": "Default irrigation time in minutes" } diff --git a/homeassistant/components/rainmachine/strings.json b/homeassistant/components/rainmachine/strings.json index 783c876fe627fe..fc48ebce4eb14b 100644 --- a/homeassistant/components/rainmachine/strings.json +++ b/homeassistant/components/rainmachine/strings.json @@ -118,7 +118,7 @@ "description": "Restricts all watering activities from starting for a time period.", "fields": { "device_id": { - "name": "Controller", + "name": "[%key:component::rainmachine::services::pause_watering::fields::device_id::name%]", "description": "The controller whose watering activities should be restricted." }, "duration": { @@ -146,7 +146,7 @@ "description": "Stops all watering activities.", "fields": { "device_id": { - "name": "Controller", + "name": "[%key:component::rainmachine::services::pause_watering::fields::device_id::name%]", "description": "The controller whose watering activities should be stopped." } } @@ -164,7 +164,7 @@ "description": "Unpauses all paused watering activities.", "fields": { "device_id": { - "name": "Controller", + "name": "[%key:component::rainmachine::services::pause_watering::fields::device_id::name%]", "description": "The controller whose watering activities should be unpaused." } } @@ -174,7 +174,7 @@ "description": "Push flow meter data to the RainMachine device.", "fields": { "device_id": { - "name": "Controller", + "name": "[%key:component::rainmachine::services::pause_watering::fields::device_id::name%]", "description": "The controller to send flow meter data to." }, "value": { @@ -192,7 +192,7 @@ "description": "Push weather data from Home Assistant to the RainMachine device.\nLocal Weather Push service should be enabled from Settings > Weather > Developer tab for RainMachine to consider the values being sent. Units must be sent in metric; no conversions are performed by the integraion.\nSee details of RainMachine API Here: https://rainmachine.docs.apiary.io/#reference/weather-services/parserdata/post.", "fields": { "device_id": { - "name": "Controller", + "name": "[%key:component::rainmachine::services::pause_watering::fields::device_id::name%]", "description": "The controller for the weather data to be pushed." }, "timestamp": { @@ -201,15 +201,15 @@ }, "mintemp": { "name": "Min temp", - "description": "Minimum temperature (\u00b0C)." + "description": "Minimum temperature (°C)." }, "maxtemp": { "name": "Max temp", - "description": "Maximum temperature (\u00b0C)." + "description": "Maximum temperature (°C)." }, "temperature": { "name": "Temperature", - "description": "Current temperature (\u00b0C)." + "description": "Current temperature (°C)." }, "wind": { "name": "Wind speed", @@ -217,7 +217,7 @@ }, "solarrad": { "name": "Solar radiation", - "description": "Solar radiation (MJ/m\u00b2/h)." + "description": "Solar radiation (MJ/m²/h)." }, "et": { "name": "Evapotranspiration", @@ -249,7 +249,7 @@ }, "dewpoint": { "name": "Dew point", - "description": "Dew point (\u00b0C)." + "description": "Dew point (°C)." } } }, @@ -258,7 +258,7 @@ "description": "Unrestrict all watering activities.", "fields": { "device_id": { - "name": "Controller", + "name": "[%key:component::rainmachine::services::pause_watering::fields::device_id::name%]", "description": "The controller whose watering activities should be unrestricted." } } diff --git a/homeassistant/components/recorder/strings.json b/homeassistant/components/recorder/strings.json index 17539387a29582..24f0d806edd3a2 100644 --- a/homeassistant/components/recorder/strings.json +++ b/homeassistant/components/recorder/strings.json @@ -46,7 +46,7 @@ "description": "List of glob patterns used to select the entities for which the data is to be removed from the recorder database." }, "keep_days": { - "name": "Days to keep", + "name": "[%key:component::recorder::services::purge::fields::keep_days::name%]", "description": "Number of days to keep the data for rows matching the filter. Starting today, counting backward. A value of `7` means that everything older than a week will be purged. The default of 0 days will remove all matching rows immediately." } } diff --git a/homeassistant/components/remember_the_milk/strings.json b/homeassistant/components/remember_the_milk/strings.json index 15ca4c36da87e6..5590691e245e56 100644 --- a/homeassistant/components/remember_the_milk/strings.json +++ b/homeassistant/components/remember_the_milk/strings.json @@ -5,7 +5,7 @@ "description": "Creates (or update) a new task in your Remember The Milk account. If you want to update a task later on, you have to set an \"id\" when creating the task. Note: Updating a tasks does not support the smart syntax.", "fields": { "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "Name of the new task, you can use the smart syntax here." }, "id": { diff --git a/homeassistant/components/renault/strings.json b/homeassistant/components/renault/strings.json index e0b8cb0cdf03ed..0b0c3d87822292 100644 --- a/homeassistant/components/renault/strings.json +++ b/homeassistant/components/renault/strings.json @@ -66,7 +66,7 @@ }, "device_tracker": { "location": { - "name": "Location" + "name": "[%key:common::config_flow::data::location%]" } }, "select": { @@ -74,7 +74,7 @@ "name": "Charge mode", "state": { "always": "Instant", - "always_charging": "Instant", + "always_charging": "[%key:component::renault::entity::select::charge_mode::state::always%]", "schedule_mode": "Planner" } } @@ -163,7 +163,7 @@ }, "temperature": { "name": "Temperature", - "description": "Target A/C temperature in \u00b0C." + "description": "Target A/C temperature in °C." }, "when": { "name": "When", @@ -177,7 +177,7 @@ "fields": { "vehicle": { "name": "Vehicle", - "description": "The vehicle to send the command to." + "description": "[%key:component::renault::services::ac_start::fields::vehicle::description%]" } } }, @@ -187,7 +187,7 @@ "fields": { "vehicle": { "name": "Vehicle", - "description": "The vehicle to send the command to." + "description": "[%key:component::renault::services::ac_start::fields::vehicle::description%]" }, "schedules": { "name": "Schedules", diff --git a/homeassistant/components/rfxtrx/strings.json b/homeassistant/components/rfxtrx/strings.json index 6c49fb38d6cae5..85ddf559cf51dc 100644 --- a/homeassistant/components/rfxtrx/strings.json +++ b/homeassistant/components/rfxtrx/strings.json @@ -25,13 +25,13 @@ "data": { "device": "Select device" }, - "title": "Device" + "title": "[%key:common::config_flow::data::device%]" }, "setup_serial_manual_path": { "data": { "device": "[%key:common::config_flow::data::usb_path%]" }, - "title": "Path" + "title": "[%key:common::config_flow::data::path%]" } } }, diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 63ebd31b34c305..3b3e6221895895 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -4,7 +4,7 @@ "user": { "description": "Enter your Roborock email address.", "data": { - "username": "Email" + "username": "[%key:common::config_flow::data::email%]" } }, "code": { @@ -51,14 +51,14 @@ "state": { "starting": "Starting", "charger_disconnected": "Charger disconnected", - "idle": "Idle", + "idle": "[%key:common::state::idle%]", "remote_control_active": "Remote control active", "cleaning": "Cleaning", "returning_home": "Returning home", "manual_mode": "Manual mode", "charging": "Charging", "charging_problem": "Charging problem", - "paused": "Paused", + "paused": "[%key:common::state::paused%]", "spot_cleaning": "Spot cleaning", "error": "Error", "shutting_down": "Shutting down", @@ -134,7 +134,7 @@ "moderate": "Moderate", "high": "High", "intense": "Intense", - "custom": "Custom" + "custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]" } } }, @@ -156,7 +156,7 @@ "state": { "auto": "Auto", "balanced": "Balanced", - "custom": "Custom", + "custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]", "gentle": "Gentle", "off": "[%key:common::state::off%]", "max": "Max", @@ -164,7 +164,7 @@ "medium": "Medium", "quiet": "Quiet", "silent": "Silent", - "standard": "Standard", + "standard": "[%key:component::roborock::entity::select::mop_mode::state::standard%]", "turbo": "Turbo" } } diff --git a/homeassistant/components/rtsp_to_webrtc/strings.json b/homeassistant/components/rtsp_to_webrtc/strings.json index 939c30766e278d..e52ab554473fa2 100644 --- a/homeassistant/components/rtsp_to_webrtc/strings.json +++ b/homeassistant/components/rtsp_to_webrtc/strings.json @@ -20,8 +20,8 @@ }, "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", - "server_failure": "RTSPtoWebRTC server returned an error. Check logs for more information.", - "server_unreachable": "Unable to communicate with RTSPtoWebRTC server. Check logs for more information." + "server_failure": "[%key:component::rtsp_to_webrtc::config::error::server_failure%]", + "server_unreachable": "[%key:component::rtsp_to_webrtc::config::error::server_unreachable%]" } }, "options": { diff --git a/homeassistant/components/sabnzbd/strings.json b/homeassistant/components/sabnzbd/strings.json index 5711656ef690e0..a8e146eeb27333 100644 --- a/homeassistant/components/sabnzbd/strings.json +++ b/homeassistant/components/sabnzbd/strings.json @@ -30,7 +30,7 @@ "description": "Resumes downloads.", "fields": { "api_key": { - "name": "SABnzbd API key", + "name": "[%key:component::sabnzbd::services::pause::fields::api_key::name%]", "description": "The SABnzbd API key to resume downloads." } } @@ -40,7 +40,7 @@ "description": "Sets the download speed limit.", "fields": { "api_key": { - "name": "SABnzbd API key", + "name": "[%key:component::sabnzbd::services::pause::fields::api_key::name%]", "description": "The SABnzbd API key to set speed limit." }, "speed": { diff --git a/homeassistant/components/screenlogic/strings.json b/homeassistant/components/screenlogic/strings.json index 79b633e28b617a..4894bc6437d70d 100644 --- a/homeassistant/components/screenlogic/strings.json +++ b/homeassistant/components/screenlogic/strings.json @@ -14,7 +14,7 @@ } }, "gateway_select": { - "title": "ScreenLogic", + "title": "[%key:component::screenlogic::config::step::gateway_entry::title%]", "description": "The following ScreenLogic gateways were discovered. Please select one to configure, or choose to manually configure a ScreenLogic gateway.", "data": { "selected_gateway": "Gateway" @@ -28,7 +28,7 @@ "options": { "step": { "init": { - "title": "ScreenLogic", + "title": "[%key:component::screenlogic::config::step::gateway_entry::title%]", "description": "Specify settings for {gateway_name}", "data": { "scan_interval": "Seconds between scans" diff --git a/homeassistant/components/sfr_box/strings.json b/homeassistant/components/sfr_box/strings.json index 3fc9691cc12f47..7ea18304164271 100644 --- a/homeassistant/components/sfr_box/strings.json +++ b/homeassistant/components/sfr_box/strings.json @@ -84,7 +84,7 @@ "dsl_training": { "name": "DSL training", "state": { - "idle": "Idle", + "idle": "[%key:common::state::idle%]", "g_994_training": "G.994 Training", "g_992_started": "G.992 Started", "g_922_channel_analysis": "G.922 Channel Analysis", diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 265184e622715f..eeb2c3d3224fc2 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -76,8 +76,8 @@ "selector": { "ble_scanner_mode": { "options": { - "disabled": "Disabled", - "active": "Active", + "disabled": "[%key:common::state::disabled%]", + "active": "[%key:common::state::active%]", "passive": "Passive" } } diff --git a/homeassistant/components/shopping_list/strings.json b/homeassistant/components/shopping_list/strings.json index 598a2bddfffff7..ddac4713facfda 100644 --- a/homeassistant/components/shopping_list/strings.json +++ b/homeassistant/components/shopping_list/strings.json @@ -3,7 +3,7 @@ "config": { "step": { "user": { - "title": "Shopping List", + "title": "[%key:component::shopping_list::title%]", "description": "Do you want to configure the shopping list?" } }, @@ -17,7 +17,7 @@ "description": "Adds an item to the shopping list.", "fields": { "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "The name of the item to add." } } @@ -27,7 +27,7 @@ "description": "Removes the first item with matching name from the shopping list.", "fields": { "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "The name of the item to remove." } } @@ -37,7 +37,7 @@ "description": "Marks the first item with matching name as completed in the shopping list.", "fields": { "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "The name of the item to mark as completed (without removing)." } } @@ -47,7 +47,7 @@ "description": "Marks the first item with matching name as incomplete in the shopping list.", "fields": { "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "The name of the item to mark as incomplete." } } diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 4be806ebbbd761..992160350809a5 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -57,7 +57,7 @@ "description": "Sets/updates a PIN.", "fields": { "device_id": { - "name": "System", + "name": "[%key:component::simplisafe::services::remove_pin::fields::device_id::name%]", "description": "The system to set the PIN on." }, "label": { @@ -75,7 +75,7 @@ "description": "Sets one or more system properties.", "fields": { "device_id": { - "name": "System", + "name": "[%key:component::simplisafe::services::remove_pin::fields::device_id::name%]", "description": "The system whose properties should be set." }, "alarm_duration": { diff --git a/homeassistant/components/smarttub/strings.json b/homeassistant/components/smarttub/strings.json index c130feaa620b22..974e5fb7d370bf 100644 --- a/homeassistant/components/smarttub/strings.json +++ b/homeassistant/components/smarttub/strings.json @@ -42,7 +42,7 @@ "description": "Updates the secondary filtration settings.", "fields": { "mode": { - "name": "Mode", + "name": "[%key:common::config_flow::data::mode%]", "description": "The secondary filtration mode." } } @@ -62,7 +62,7 @@ "description": "Reset a reminder, and set the next time it will be triggered.", "fields": { "days": { - "name": "Days", + "name": "[%key:component::smarttub::services::snooze_reminder::fields::days::name%]", "description": "The number of days when the next reminder should trigger." } } diff --git a/homeassistant/components/sms/strings.json b/homeassistant/components/sms/strings.json index 6bf8cbcc166c54..c005c241d790f4 100644 --- a/homeassistant/components/sms/strings.json +++ b/homeassistant/components/sms/strings.json @@ -4,7 +4,7 @@ "user": { "title": "Connect to the modem", "data": { - "device": "Device", + "device": "[%key:common::config_flow::data::device%]", "baud_speed": "Baud Speed" } } @@ -20,16 +20,30 @@ }, "entity": { "sensor": { - "bit_error_rate": { "name": "Bit error rate" }, - "cid": { "name": "Cell ID" }, - "lac": { "name": "Local area code" }, - "network_code": { "name": "GSM network code" }, - "network_name": { "name": "Network name" }, - "signal_percent": { "name": "Signal percent" }, + "bit_error_rate": { + "name": "Bit error rate" + }, + "cid": { + "name": "Cell ID" + }, + "lac": { + "name": "Local area code" + }, + "network_code": { + "name": "GSM network code" + }, + "network_name": { + "name": "Network name" + }, + "signal_percent": { + "name": "Signal percent" + }, "signal_strength": { "name": "[%key:component::sensor::entity_component::signal_strength::name%]" }, - "state": { "name": "Network status" } + "state": { + "name": "Network status" + } } } } diff --git a/homeassistant/components/snips/strings.json b/homeassistant/components/snips/strings.json index d6c9f4d53f65b7..724e1a86477266 100644 --- a/homeassistant/components/snips/strings.json +++ b/homeassistant/components/snips/strings.json @@ -16,7 +16,7 @@ "fields": { "site_id": { "name": "Site ID", - "description": "Site to turn sounds on, defaults to all sites." + "description": "[%key:component::snips::services::feedback_off::fields::site_id::description%]" } } }, @@ -47,8 +47,8 @@ "description": "If True, session waits for an open session to end, if False session is dropped if one is running." }, "custom_data": { - "name": "Custom data", - "description": "Custom data that will be included with all messages in this session." + "name": "[%key:component::snips::services::say::fields::custom_data::name%]", + "description": "[%key:component::snips::services::say::fields::custom_data::description%]" }, "intent_filter": { "name": "Intent filter", @@ -56,11 +56,11 @@ }, "site_id": { "name": "Site ID", - "description": "Site to use to start session, defaults to default." + "description": "[%key:component::snips::services::say::fields::site_id::description%]" }, "text": { "name": "Text", - "description": "Text to say." + "description": "[%key:component::snips::services::say::fields::text::description%]" } } } diff --git a/homeassistant/components/snooz/strings.json b/homeassistant/components/snooz/strings.json index 878341f23bc596..bc1e68db02fc6f 100644 --- a/homeassistant/components/snooz/strings.json +++ b/homeassistant/components/snooz/strings.json @@ -44,7 +44,7 @@ "description": "Transitions volume off over time.", "fields": { "duration": { - "name": "Transition duration", + "name": "[%key:component::snooz::services::transition_on::fields::duration::name%]", "description": "Time it takes to turn off." } } diff --git a/homeassistant/components/songpal/strings.json b/homeassistant/components/songpal/strings.json index a4df830f1fe8cd..d6874f94f9515d 100644 --- a/homeassistant/components/songpal/strings.json +++ b/homeassistant/components/songpal/strings.json @@ -25,7 +25,7 @@ "description": "Change sound setting.", "fields": { "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "Name of the setting." }, "value": { diff --git a/homeassistant/components/sonos/strings.json b/homeassistant/components/sonos/strings.json index c5b5136e9700ad..7ce1d727b17f98 100644 --- a/homeassistant/components/sonos/strings.json +++ b/homeassistant/components/sonos/strings.json @@ -41,7 +41,7 @@ "description": "Name of entity that will be restored." }, "with_group": { - "name": "With group", + "name": "[%key:component::sonos::services::snapshot::fields::with_group::name%]", "description": "True or False. Also restore the group layout." } } @@ -75,7 +75,7 @@ "description": "Removes an item from the queue.", "fields": { "queue_position": { - "name": "Queue position", + "name": "[%key:component::sonos::services::play_queue::fields::queue_position::name%]", "description": "Position in the queue to remove." } } diff --git a/homeassistant/components/soundtouch/strings.json b/homeassistant/components/soundtouch/strings.json index 616a4fc5a115fd..7af95aab38c0a3 100644 --- a/homeassistant/components/soundtouch/strings.json +++ b/homeassistant/components/soundtouch/strings.json @@ -52,7 +52,7 @@ "description": "Name of the master entity that is coordinating the multi-room zone. Platform dependent." }, "slaves": { - "name": "Slaves", + "name": "[%key:component::soundtouch::services::create_zone::fields::slaves::name%]", "description": "Name of slaves entities to add to the existing zone." } } @@ -63,10 +63,10 @@ "fields": { "master": { "name": "Master", - "description": "Name of the master entity that is coordinating the multi-room zone. Platform dependent." + "description": "[%key:component::soundtouch::services::add_zone_slave::fields::master::description%]" }, "slaves": { - "name": "Slaves", + "name": "[%key:component::soundtouch::services::create_zone::fields::slaves::name%]", "description": "Name of slaves entities to remove from the existing zone." } } diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index caec5b8a288f58..ec2721aba8b077 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -10,12 +10,14 @@ } }, "abort": { - "authorize_url_timeout": "Timeout generating authorize URL.", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "The Spotify integration is not configured. Please follow the documentation.", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "reauth_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication." }, - "create_entry": { "default": "Successfully authenticated with Spotify." } + "create_entry": { + "default": "Successfully authenticated with Spotify." + } }, "system_health": { "info": { diff --git a/homeassistant/components/squeezebox/strings.json b/homeassistant/components/squeezebox/strings.json index 13fe16aa28c3d4..87881e3414b6a7 100644 --- a/homeassistant/components/squeezebox/strings.json +++ b/homeassistant/components/squeezebox/strings.json @@ -49,11 +49,11 @@ "fields": { "command": { "name": "Command", - "description": "Command to pass to Logitech Media Server (p0 in the CLI documentation)." + "description": "[%key:component::squeezebox::services::call_method::fields::command::description%]" }, "parameters": { "name": "Parameters", - "description": "Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation).\n." + "description": "[%key:component::squeezebox::services::call_method::fields::parameters::description%]" } } }, diff --git a/homeassistant/components/starline/strings.json b/homeassistant/components/starline/strings.json index 292ae55da1fff8..4d2c497dc8b6aa 100644 --- a/homeassistant/components/starline/strings.json +++ b/homeassistant/components/starline/strings.json @@ -59,7 +59,7 @@ "fields": { "scan_interval": { "name": "Scan interval", - "description": "Update frequency." + "description": "[%key:component::starline::services::set_scan_interval::fields::scan_interval::description%]" } } } diff --git a/homeassistant/components/starlink/strings.json b/homeassistant/components/starlink/strings.json index 48f84ea7baf5a3..aa89d87b6beeb1 100644 --- a/homeassistant/components/starlink/strings.json +++ b/homeassistant/components/starlink/strings.json @@ -26,7 +26,7 @@ "name": "Heating" }, "power_save_idle": { - "name": "Idle" + "name": "[%key:common::state::idle%]" }, "mast_near_vertical": { "name": "Mast near vertical" @@ -52,7 +52,7 @@ "name": "Azimuth" }, "elevation": { - "name": "Elevation" + "name": "[%key:common::config_flow::data::elevation%]" }, "uplink_throughput": { "name": "Uplink throughput" diff --git a/homeassistant/components/steamist/strings.json b/homeassistant/components/steamist/strings.json index a3cd4879c6a219..8827df6a08a148 100644 --- a/homeassistant/components/steamist/strings.json +++ b/homeassistant/components/steamist/strings.json @@ -10,7 +10,7 @@ }, "pick_device": { "data": { - "device": "Device" + "device": "[%key:common::config_flow::data::device%]" } }, "discovery_confirm": { diff --git a/homeassistant/components/subaru/strings.json b/homeassistant/components/subaru/strings.json index 2ce3c3835a64a7..8474d391141b81 100644 --- a/homeassistant/components/subaru/strings.json +++ b/homeassistant/components/subaru/strings.json @@ -11,21 +11,21 @@ } }, "two_factor": { - "title": "Subaru Starlink Configuration", + "title": "[%key:component::subaru::config::step::user::title%]", "description": "Two factor authentication required", "data": { "contact_method": "Please select a contact method:" } }, "two_factor_validate": { - "title": "Subaru Starlink Configuration", + "title": "[%key:component::subaru::config::step::user::title%]", "description": "Please enter validation code received", "data": { "validation_code": "Validation code" } }, "pin": { - "title": "Subaru Starlink Configuration", + "title": "[%key:component::subaru::config::step::user::title%]", "description": "Please enter your MySubaru PIN\nNOTE: All vehicles in account must have the same PIN", "data": { "pin": "PIN" diff --git a/homeassistant/components/surepetcare/strings.json b/homeassistant/components/surepetcare/strings.json index 6e1ec9643a737b..2d297cc829e507 100644 --- a/homeassistant/components/surepetcare/strings.json +++ b/homeassistant/components/surepetcare/strings.json @@ -41,7 +41,7 @@ "description": "Name of pet." }, "location": { - "name": "Location", + "name": "[%key:common::config_flow::data::location%]", "description": "Pet location (Inside or Outside)." } } diff --git a/homeassistant/components/synology_dsm/strings.json b/homeassistant/components/synology_dsm/strings.json index 24ed1aaf568cb1..f7ae9c9f238b05 100644 --- a/homeassistant/components/synology_dsm/strings.json +++ b/homeassistant/components/synology_dsm/strings.json @@ -183,7 +183,7 @@ "description": "Shutdowns the NAS. This service is deprecated and will be removed in future release. Please use the corresponding button entity.", "fields": { "serial": { - "name": "Serial", + "name": "[%key:component::synology_dsm::services::reboot::fields::serial::name%]", "description": "Serial of the NAS to shutdown; required when multiple NAS are configured." } } diff --git a/homeassistant/components/system_bridge/strings.json b/homeassistant/components/system_bridge/strings.json index e4b2b40637cf4d..c3e1f949152e69 100644 --- a/homeassistant/components/system_bridge/strings.json +++ b/homeassistant/components/system_bridge/strings.json @@ -38,7 +38,7 @@ "description": "The server to talk to." }, "path": { - "name": "Path", + "name": "[%key:common::config_flow::data::path%]", "description": "Path to open." } } @@ -48,11 +48,11 @@ "description": "Opens a URL on the server using the default application.", "fields": { "bridge": { - "name": "Bridge", - "description": "The server to talk to." + "name": "[%key:component::system_bridge::services::open_path::fields::bridge::name%]", + "description": "[%key:component::system_bridge::services::open_path::fields::bridge::description%]" }, "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "URL to open." } } @@ -62,7 +62,7 @@ "description": "Sends a keyboard keypress.", "fields": { "bridge": { - "name": "Bridge", + "name": "[%key:component::system_bridge::services::open_path::fields::bridge::name%]", "description": "The server to send the command to." }, "key": { @@ -76,8 +76,8 @@ "description": "Sends text for the server to type.", "fields": { "bridge": { - "name": "Bridge", - "description": "The server to send the command to." + "name": "[%key:component::system_bridge::services::open_path::fields::bridge::name%]", + "description": "[%key:component::system_bridge::services::send_keypress::fields::bridge::description%]" }, "text": { "name": "Text", diff --git a/homeassistant/components/tankerkoenig/strings.json b/homeassistant/components/tankerkoenig/strings.json index b68359a51760aa..dea370f45b343a 100644 --- a/homeassistant/components/tankerkoenig/strings.json +++ b/homeassistant/components/tankerkoenig/strings.json @@ -38,7 +38,7 @@ "init": { "title": "Tankerkoenig options", "data": { - "stations": "Stations", + "stations": "[%key:component::tankerkoenig::config::step::select_station::data::stations%]", "show_on_map": "Show stations on map" } } diff --git a/homeassistant/components/telegram_bot/strings.json b/homeassistant/components/telegram_bot/strings.json index 8104fdd285e369..eeca235ab444a8 100644 --- a/homeassistant/components/telegram_bot/strings.json +++ b/homeassistant/components/telegram_bot/strings.json @@ -51,7 +51,7 @@ "description": "Sends a photo.", "fields": { "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "Remote path to an image." }, "file": { @@ -63,11 +63,11 @@ "description": "The title of the image." }, "username": { - "name": "Username", + "name": "[%key:common::config_flow::data::username%]", "description": "Username for a URL which require HTTP authentication." }, "password": { - "name": "Password", + "name": "[%key:common::config_flow::data::password%]", "description": "Password (or bearer token) for a URL which require HTTP authentication." }, "authentication": { @@ -79,12 +79,12 @@ "description": "An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default." }, "parse_mode": { - "name": "Parse mode", - "description": "Parser for the message text." + "name": "[%key:component::telegram_bot::services::send_message::fields::parse_mode::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::parse_mode::description%]" }, "disable_notification": { - "name": "Disable notification", - "description": "Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound." + "name": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::description%]" }, "verify_ssl": { "name": "Verify SSL", @@ -95,16 +95,16 @@ "description": "Timeout for send photo. Will help with timeout errors (poor internet connection, etc)." }, "keyboard": { - "name": "Keyboard", + "name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]", "description": "List of rows of commands, comma-separated, to make a custom keyboard." }, "inline_keyboard": { - "name": "Inline keyboard", - "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + "name": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::description%]" }, "message_tag": { - "name": "Message tag", - "description": "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}." + "name": "[%key:component::telegram_bot::services::send_message::fields::message_tag::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::message_tag::description%]" } } }, @@ -113,11 +113,11 @@ "description": "Sends a sticker.", "fields": { "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "Remote path to a static .webp or animated .tgs sticker." }, "file": { - "name": "File", + "name": "[%key:component::telegram_bot::services::send_photo::fields::file::name%]", "description": "Local path to a static .webp or animated .tgs sticker." }, "sticker_id": { @@ -125,44 +125,44 @@ "description": "ID of a sticker that exists on telegram servers." }, "username": { - "name": "Username", - "description": "Username for a URL which require HTTP authentication." + "name": "[%key:common::config_flow::data::username%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::username::description%]" }, "password": { - "name": "Password", - "description": "Password (or bearer token) for a URL which require HTTP authentication." + "name": "[%key:common::config_flow::data::password%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::password::description%]" }, "authentication": { - "name": "Authentication method", - "description": "Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`." + "name": "[%key:component::telegram_bot::services::send_photo::fields::authentication::name%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::authentication::description%]" }, "target": { "name": "Target", - "description": "An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default." + "description": "[%key:component::telegram_bot::services::send_photo::fields::target::description%]" }, "disable_notification": { - "name": "Disable notification", - "description": "Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound." + "name": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::description%]" }, "verify_ssl": { "name": "Verify SSL", - "description": "Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server." + "description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]" }, "timeout": { "name": "Timeout", "description": "Timeout for send sticker. Will help with timeout errors (poor internet connection, etc)." }, "keyboard": { - "name": "Keyboard", - "description": "List of rows of commands, comma-separated, to make a custom keyboard." + "name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::keyboard::description%]" }, "inline_keyboard": { - "name": "Inline keyboard", - "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + "name": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::description%]" }, "message_tag": { - "name": "Message tag", - "description": "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}." + "name": "[%key:component::telegram_bot::services::send_message::fields::message_tag::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::message_tag::description%]" } } }, @@ -171,56 +171,56 @@ "description": "Sends an anmiation.", "fields": { "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "Remote path to a GIF or H.264/MPEG-4 AVC video without sound." }, "file": { - "name": "File", + "name": "[%key:component::telegram_bot::services::send_photo::fields::file::name%]", "description": "Local path to a GIF or H.264/MPEG-4 AVC video without sound." }, "caption": { - "name": "Caption", + "name": "[%key:component::telegram_bot::services::send_photo::fields::caption::name%]", "description": "The title of the animation." }, "username": { - "name": "Username", - "description": "Username for a URL which require HTTP authentication." + "name": "[%key:common::config_flow::data::username%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::username::description%]" }, "password": { - "name": "Password", - "description": "Password (or bearer token) for a URL which require HTTP authentication." + "name": "[%key:common::config_flow::data::password%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::password::description%]" }, "authentication": { - "name": "Authentication method", - "description": "Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`." + "name": "[%key:component::telegram_bot::services::send_photo::fields::authentication::name%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::authentication::description%]" }, "target": { "name": "Target", - "description": "An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default." + "description": "[%key:component::telegram_bot::services::send_photo::fields::target::description%]" }, "parse_mode": { "name": "Parse Mode", - "description": "Parser for the message text." + "description": "[%key:component::telegram_bot::services::send_message::fields::parse_mode::description%]" }, "disable_notification": { - "name": "Disable notification", - "description": "Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound." + "name": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::description%]" }, "verify_ssl": { "name": "Verify SSL", - "description": "Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server." + "description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]" }, "timeout": { "name": "Timeout", - "description": "Timeout for send sticker. Will help with timeout errors (poor internet connection, etc)." + "description": "[%key:component::telegram_bot::services::send_sticker::fields::timeout::description%]" }, "keyboard": { - "name": "Keyboard", - "description": "List of rows of commands, comma-separated, to make a custom keyboard." + "name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::keyboard::description%]" }, "inline_keyboard": { - "name": "Inline keyboard", - "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + "name": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::description%]" } } }, @@ -229,60 +229,60 @@ "description": "Sends a video.", "fields": { "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "Remote path to a video." }, "file": { - "name": "File", + "name": "[%key:component::telegram_bot::services::send_photo::fields::file::name%]", "description": "Local path to a video." }, "caption": { - "name": "Caption", + "name": "[%key:component::telegram_bot::services::send_photo::fields::caption::name%]", "description": "The title of the video." }, "username": { - "name": "Username", - "description": "Username for a URL which require HTTP authentication." + "name": "[%key:common::config_flow::data::username%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::username::description%]" }, "password": { - "name": "Password", - "description": "Password (or bearer token) for a URL which require HTTP authentication." + "name": "[%key:common::config_flow::data::password%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::password::description%]" }, "authentication": { - "name": "Authentication method", - "description": "Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`." + "name": "[%key:component::telegram_bot::services::send_photo::fields::authentication::name%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::authentication::description%]" }, "target": { "name": "Target", - "description": "An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default." + "description": "[%key:component::telegram_bot::services::send_photo::fields::target::description%]" }, "parse_mode": { - "name": "Parse mode", - "description": "Parser for the message text." + "name": "[%key:component::telegram_bot::services::send_message::fields::parse_mode::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::parse_mode::description%]" }, "disable_notification": { - "name": "Disable notification", - "description": "Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound." + "name": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::description%]" }, "verify_ssl": { "name": "Verify SSL", - "description": "Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server." + "description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]" }, "timeout": { "name": "Timeout", "description": "Timeout for send video. Will help with timeout errors (poor internet connection, etc)." }, "keyboard": { - "name": "Keyboard", - "description": "List of rows of commands, comma-separated, to make a custom keyboard." + "name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::keyboard::description%]" }, "inline_keyboard": { - "name": "Inline keyboard", - "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + "name": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::description%]" }, "message_tag": { - "name": "Message tag", - "description": "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}." + "name": "[%key:component::telegram_bot::services::send_message::fields::message_tag::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::message_tag::description%]" } } }, @@ -291,56 +291,56 @@ "description": "Sends a voice message.", "fields": { "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "Remote path to a voice message." }, "file": { - "name": "File", + "name": "[%key:component::telegram_bot::services::send_photo::fields::file::name%]", "description": "Local path to a voice message." }, "caption": { - "name": "Caption", + "name": "[%key:component::telegram_bot::services::send_photo::fields::caption::name%]", "description": "The title of the voice message." }, "username": { - "name": "Username", - "description": "Username for a URL which require HTTP authentication." + "name": "[%key:common::config_flow::data::username%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::username::description%]" }, "password": { - "name": "Password", - "description": "Password (or bearer token) for a URL which require HTTP authentication." + "name": "[%key:common::config_flow::data::password%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::password::description%]" }, "authentication": { - "name": "Authentication method", - "description": "Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`." + "name": "[%key:component::telegram_bot::services::send_photo::fields::authentication::name%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::authentication::description%]" }, "target": { "name": "Target", - "description": "An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default." + "description": "[%key:component::telegram_bot::services::send_photo::fields::target::description%]" }, "disable_notification": { - "name": "Disable notification", - "description": "Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound." + "name": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::description%]" }, "verify_ssl": { "name": "Verify SSL", - "description": "Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server." + "description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]" }, "timeout": { "name": "Timeout", "description": "Timeout for send voice. Will help with timeout errors (poor internet connection, etc)." }, "keyboard": { - "name": "Keyboard", - "description": "List of rows of commands, comma-separated, to make a custom keyboard." + "name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::keyboard::description%]" }, "inline_keyboard": { - "name": "Inline keyboard", - "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + "name": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::description%]" }, "message_tag": { - "name": "Message tag", - "description": "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}." + "name": "[%key:component::telegram_bot::services::send_message::fields::message_tag::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::message_tag::description%]" } } }, @@ -349,60 +349,60 @@ "description": "Sends a document.", "fields": { "url": { - "name": "URL", + "name": "[%key:common::config_flow::data::url%]", "description": "Remote path to a document." }, "file": { - "name": "File", + "name": "[%key:component::telegram_bot::services::send_photo::fields::file::name%]", "description": "Local path to a document." }, "caption": { - "name": "Caption", + "name": "[%key:component::telegram_bot::services::send_photo::fields::caption::name%]", "description": "The title of the document." }, "username": { - "name": "Username", - "description": "Username for a URL which require HTTP authentication." + "name": "[%key:common::config_flow::data::username%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::username::description%]" }, "password": { - "name": "Password", - "description": "Password (or bearer token) for a URL which require HTTP authentication." + "name": "[%key:common::config_flow::data::password%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::password::description%]" }, "authentication": { - "name": "Authentication method", - "description": "Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`." + "name": "[%key:component::telegram_bot::services::send_photo::fields::authentication::name%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::authentication::description%]" }, "target": { "name": "Target", - "description": "An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default." + "description": "[%key:component::telegram_bot::services::send_photo::fields::target::description%]" }, "parse_mode": { - "name": "Parse mode", - "description": "Parser for the message text." + "name": "[%key:component::telegram_bot::services::send_message::fields::parse_mode::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::parse_mode::description%]" }, "disable_notification": { - "name": "Disable notification", - "description": "Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound." + "name": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::description%]" }, "verify_ssl": { "name": "Verify SSL", - "description": "Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server." + "description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]" }, "timeout": { "name": "Timeout", "description": "Timeout for send document. Will help with timeout errors (poor internet connection, etc)." }, "keyboard": { - "name": "Keyboard", - "description": "List of rows of commands, comma-separated, to make a custom keyboard." + "name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::keyboard::description%]" }, "inline_keyboard": { - "name": "Inline keyboard", - "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + "name": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::description%]" }, "message_tag": { - "name": "Message tag", - "description": "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}." + "name": "[%key:component::telegram_bot::services::send_message::fields::message_tag::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::message_tag::description%]" } } }, @@ -411,11 +411,11 @@ "description": "Sends a location.", "fields": { "latitude": { - "name": "Latitude", + "name": "[%key:common::config_flow::data::latitude%]", "description": "The latitude to send." }, "longitude": { - "name": "Longitude", + "name": "[%key:common::config_flow::data::longitude%]", "description": "The longitude to send." }, "target": { @@ -423,24 +423,24 @@ "description": "An array of pre-authorized chat_ids to send the location to. If not present, first allowed chat_id is the default." }, "disable_notification": { - "name": "Disable notification", - "description": "Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound." + "name": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::description%]" }, "timeout": { "name": "Timeout", - "description": "Timeout for send photo. Will help with timeout errors (poor internet connection, etc)." + "description": "[%key:component::telegram_bot::services::send_photo::fields::timeout::description%]" }, "keyboard": { - "name": "Keyboard", - "description": "List of rows of commands, comma-separated, to make a custom keyboard." + "name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]", + "description": "[%key:component::telegram_bot::services::send_photo::fields::keyboard::description%]" }, "inline_keyboard": { - "name": "Inline keyboard", - "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + "name": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::description%]" }, "message_tag": { - "name": "Message tag", - "description": "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}." + "name": "[%key:component::telegram_bot::services::send_message::fields::message_tag::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::message_tag::description%]" } } }, @@ -450,7 +450,7 @@ "fields": { "target": { "name": "Target", - "description": "An array of pre-authorized chat_ids to send the location to. If not present, first allowed chat_id is the default." + "description": "[%key:component::telegram_bot::services::send_location::fields::target::description%]" }, "question": { "name": "Question", @@ -473,8 +473,8 @@ "description": "Amount of time in seconds the poll will be active after creation, 5-600." }, "disable_notification": { - "name": "Disable notification", - "description": "Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound." + "name": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::description%]" }, "timeout": { "name": "Timeout", @@ -500,19 +500,19 @@ }, "title": { "name": "Title", - "description": "Optional title for your notification. Will be composed as '%title\\n%message'." + "description": "[%key:component::telegram_bot::services::send_message::fields::title::description%]" }, "parse_mode": { - "name": "Parse mode", - "description": "Parser for the message text." + "name": "[%key:component::telegram_bot::services::send_message::fields::parse_mode::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::parse_mode::description%]" }, "disable_web_page_preview": { - "name": "Disable web page preview", - "description": "Disables link previews for links in the message." + "name": "[%key:component::telegram_bot::services::send_message::fields::disable_web_page_preview::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::disable_web_page_preview::description%]" }, "inline_keyboard": { - "name": "Inline keyboard", - "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + "name": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::description%]" } } }, @@ -521,20 +521,20 @@ "description": "Edits the caption of a previously sent message.", "fields": { "message_id": { - "name": "Message ID", - "description": "Id of the message to edit." + "name": "[%key:component::telegram_bot::services::edit_message::fields::message_id::name%]", + "description": "[%key:component::telegram_bot::services::edit_message::fields::message_id::description%]" }, "chat_id": { - "name": "Chat ID", + "name": "[%key:component::telegram_bot::services::edit_message::fields::chat_id::name%]", "description": "The chat_id where to edit the caption." }, "caption": { - "name": "Caption", + "name": "[%key:component::telegram_bot::services::send_photo::fields::caption::name%]", "description": "Message body of the notification." }, "inline_keyboard": { - "name": "Inline keyboard", - "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + "name": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::description%]" } } }, @@ -543,16 +543,16 @@ "description": "Edit the inline keyboard of a previously sent message.", "fields": { "message_id": { - "name": "Message ID", - "description": "Id of the message to edit." + "name": "[%key:component::telegram_bot::services::edit_message::fields::message_id::name%]", + "description": "[%key:component::telegram_bot::services::edit_message::fields::message_id::description%]" }, "chat_id": { - "name": "Chat ID", + "name": "[%key:component::telegram_bot::services::edit_message::fields::chat_id::name%]", "description": "The chat_id where to edit the reply_markup." }, "inline_keyboard": { - "name": "Inline keyboard", - "description": "List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data." + "name": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::name%]", + "description": "[%key:component::telegram_bot::services::send_message::fields::inline_keyboard::description%]" } } }, @@ -583,11 +583,11 @@ "description": "Deletes a previously sent message.", "fields": { "message_id": { - "name": "Message ID", + "name": "[%key:component::telegram_bot::services::edit_message::fields::message_id::name%]", "description": "Id of the message to delete." }, "chat_id": { - "name": "Chat ID", + "name": "[%key:component::telegram_bot::services::edit_message::fields::chat_id::name%]", "description": "The chat_id where to delete the message." } } diff --git a/homeassistant/components/threshold/strings.json b/homeassistant/components/threshold/strings.json index 8bfd9fb96b1662..832f3b4f899c1b 100644 --- a/homeassistant/components/threshold/strings.json +++ b/homeassistant/components/threshold/strings.json @@ -9,7 +9,7 @@ "entity_id": "Input sensor", "hysteresis": "Hysteresis", "lower": "Lower limit", - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "upper": "Upper limit" } } diff --git a/homeassistant/components/tile/strings.json b/homeassistant/components/tile/strings.json index da53f79b697f64..504823c4d16841 100644 --- a/homeassistant/components/tile/strings.json +++ b/homeassistant/components/tile/strings.json @@ -26,7 +26,7 @@ "options": { "step": { "init": { - "title": "Configure Tile", + "title": "[%key:component::tile::config::step::user::title%]", "data": { "show_inactive": "Show inactive Tiles" } diff --git a/homeassistant/components/tod/strings.json b/homeassistant/components/tod/strings.json index 41e40525081d3b..bd4a48df915190 100644 --- a/homeassistant/components/tod/strings.json +++ b/homeassistant/components/tod/strings.json @@ -8,7 +8,7 @@ "data": { "after_time": "On time", "before_time": "Off time", - "name": "Name" + "name": "[%key:common::config_flow::data::name%]" } } } diff --git a/homeassistant/components/tplink/strings.json b/homeassistant/components/tplink/strings.json index b5279804d0a3d3..6daa5c9cb1aecd 100644 --- a/homeassistant/components/tplink/strings.json +++ b/homeassistant/components/tplink/strings.json @@ -10,7 +10,7 @@ }, "pick_device": { "data": { - "device": "Device" + "device": "[%key:common::config_flow::data::device%]" } }, "discovery_confirm": { @@ -74,7 +74,7 @@ }, "backgrounds": { "name": "Backgrounds", - "description": "List of HSV sequences (Max 16)." + "description": "[%key:component::tplink::services::sequence_effect::fields::sequence::description%]" }, "segments": { "name": "Segments", @@ -82,15 +82,15 @@ }, "brightness": { "name": "Brightness", - "description": "Initial brightness." + "description": "[%key:component::tplink::services::sequence_effect::fields::brightness::description%]" }, "duration": { "name": "Duration", - "description": "Duration." + "description": "[%key:component::tplink::services::sequence_effect::fields::duration::description%]" }, "transition": { "name": "Transition", - "description": "Transition." + "description": "[%key:component::tplink::services::sequence_effect::fields::transition::description%]" }, "fadeoff": { "name": "Fade off", diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index c3fdcc8f1f4d84..97741bd65bb0bc 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -45,7 +45,7 @@ "sensor": { "transmission_status": { "state": { - "idle": "Idle", + "idle": "[%key:common::state::idle%]", "up_down": "Up/Down", "seeding": "Seeding", "downloading": "Downloading" @@ -73,8 +73,8 @@ "description": "Removes a torrent.", "fields": { "entry_id": { - "name": "Transmission entry", - "description": "Config entry id." + "name": "[%key:component::transmission::services::add_torrent::fields::entry_id::name%]", + "description": "[%key:component::transmission::services::add_torrent::fields::entry_id::description%]" }, "id": { "name": "ID", @@ -91,12 +91,12 @@ "description": "Starts a torrent.", "fields": { "entry_id": { - "name": "Transmission entry", - "description": "Config entry id." + "name": "[%key:component::transmission::services::add_torrent::fields::entry_id::name%]", + "description": "[%key:component::transmission::services::add_torrent::fields::entry_id::description%]" }, "id": { "name": "ID", - "description": "ID of a torrent." + "description": "[%key:component::transmission::services::remove_torrent::fields::id::description%]" } } }, @@ -105,12 +105,12 @@ "description": "Stops a torrent.", "fields": { "entry_id": { - "name": "Transmission entry", - "description": "Config entry id." + "name": "[%key:component::transmission::services::add_torrent::fields::entry_id::name%]", + "description": "[%key:component::transmission::services::add_torrent::fields::entry_id::description%]" }, "id": { "name": "ID", - "description": "ID of a torrent." + "description": "[%key:component::transmission::services::remove_torrent::fields::id::description%]" } } } diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index f4443e89f76029..ccb7d878a4989f 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -21,7 +21,7 @@ "select": { "basic_anti_flicker": { "state": { - "0": "Disabled", + "0": "[%key:common::state::disabled%]", "1": "50 Hz", "2": "60 Hz" } @@ -61,9 +61,9 @@ }, "motion_sensitivity": { "state": { - "0": "Low sensitivity", + "0": "[%key:component::tuya::entity::select::decibel_sensitivity::state::0%]", "1": "Medium sensitivity", - "2": "High sensitivity" + "2": "[%key:component::tuya::entity::select::decibel_sensitivity::state::1%]" } }, "record_mode": { @@ -75,7 +75,7 @@ "relay_status": { "state": { "last": "Remember last state", - "memory": "Remember last state", + "memory": "[%key:component::tuya::entity::select::relay_status::state::last%]", "off": "[%key:common::state::off%]", "on": "[%key:common::state::on%]", "power_off": "[%key:common::state::off%]", @@ -105,7 +105,7 @@ }, "vacuum_mode": { "state": { - "standby": "Standby", + "standby": "[%key:common::state::standby%]", "random": "Random", "smart": "Smart", "wall_follow": "Follow Wall", @@ -199,7 +199,7 @@ "reserve_1": "Reserve 1", "reserve_2": "Reserve 2", "reserve_3": "Reserve 3", - "standby": "Standby", + "standby": "[%key:common::state::standby%]", "warm": "Heat preservation" } }, diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index 6afae5ffe7b83e..e441d4695ed2c9 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -75,7 +75,7 @@ "description": "Tries to get wireless client to reconnect to UniFi Network.", "fields": { "device_id": { - "name": "Device", + "name": "[%key:common::config_flow::data::device%]", "description": "Try reconnect client to wireless network." } } diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index fd2287e08be23f..73ac6e08c1756c 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -66,7 +66,7 @@ "description": "You are using v{version} of UniFi Protect which is an Early Access version. [Early Access versions are not supported by Home Assistant](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) and it is recommended to go back to a stable release as soon as possible.\n\nBy submitting this form you have either [downgraded UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) or you agree to run an unsupported version of UniFi Protect." }, "confirm": { - "title": "v{version} is an Early Access version", + "title": "[%key:component::unifiprotect::issues::ea_warning::fix_flow::step::start::title%]", "description": "Are you sure you want to run unsupported versions of UniFi Protect? This may cause your Home Assistant integration to break." } } @@ -106,11 +106,11 @@ "description": "Removes an existing message for doorbells.", "fields": { "device_id": { - "name": "UniFi Protect NVR", - "description": "Any device from the UniFi Protect instance you want to change. In case you have multiple Protect Instances." + "name": "[%key:component::unifiprotect::services::add_doorbell_text::fields::device_id::name%]", + "description": "[%key:component::unifiprotect::services::add_doorbell_text::fields::device_id::description%]" }, "message": { - "name": "Custom message", + "name": "[%key:component::unifiprotect::services::add_doorbell_text::fields::message::name%]", "description": "Existing custom message to remove for doorbells." } } @@ -120,8 +120,8 @@ "description": "Sets the default doorbell message. This will be the message that is automatically selected when a message \"expires\".", "fields": { "device_id": { - "name": "UniFi Protect NVR", - "description": "Any device from the UniFi Protect instance you want to change. In case you have multiple Protect Instances." + "name": "[%key:component::unifiprotect::services::add_doorbell_text::fields::device_id::name%]", + "description": "[%key:component::unifiprotect::services::add_doorbell_text::fields::device_id::description%]" }, "message": { "name": "Default message", diff --git a/homeassistant/components/upb/strings.json b/homeassistant/components/upb/strings.json index b5b6dea93d5f7c..7e4590d35a2f80 100644 --- a/homeassistant/components/upb/strings.json +++ b/homeassistant/components/upb/strings.json @@ -48,7 +48,7 @@ "description": "Blinks a light.", "fields": { "rate": { - "name": "Rate", + "name": "[%key:component::upb::services::light_fade_start::fields::rate::name%]", "description": "Amount of time that the link flashes on." } } @@ -66,11 +66,11 @@ "description": "Number indicating brightness, where 0 turns the scene off, 1 is the minimum brightness and 255 is the maximum brightness." }, "brightness_pct": { - "name": "Brightness percentage", + "name": "[%key:component::upb::services::light_fade_start::fields::brightness_pct::name%]", "description": "Number indicating percentage of full brightness, where 0 turns the scene off, 1 is the minimum brightness and 100 is the maximum brightness." }, "rate": { - "name": "Rate", + "name": "[%key:component::upb::services::light_fade_start::fields::rate::name%]", "description": "Amount of time for scene to transition to new brightness." } } @@ -81,15 +81,15 @@ "fields": { "brightness": { "name": "Brightness", - "description": "Number indicating brightness, where 0 turns the scene off, 1 is the minimum brightness and 255 is the maximum brightness." + "description": "[%key:component::upb::services::link_goto::fields::brightness::description%]" }, "brightness_pct": { - "name": "Brightness percentage", - "description": "Number indicating percentage of full brightness, where 0 turns the scene off, 1 is the minimum brightness and 100 is the maximum brightness." + "name": "[%key:component::upb::services::light_fade_start::fields::brightness_pct::name%]", + "description": "[%key:component::upb::services::link_goto::fields::brightness_pct::description%]" }, "rate": { - "name": "Rate", - "description": "Amount of time for scene to transition to new brightness." + "name": "[%key:component::upb::services::light_fade_start::fields::rate::name%]", + "description": "[%key:component::upb::services::link_goto::fields::rate::description%]" } } }, @@ -103,7 +103,7 @@ "fields": { "blink_rate": { "name": "Blink rate", - "description": "Amount of time that the link flashes on." + "description": "[%key:component::upb::services::light_blink::fields::rate::description%]" } } } diff --git a/homeassistant/components/upnp/strings.json b/homeassistant/components/upnp/strings.json index 45d0c7de1c832f..ea052f0b45a3b9 100644 --- a/homeassistant/components/upnp/strings.json +++ b/homeassistant/components/upnp/strings.json @@ -7,7 +7,7 @@ }, "user": { "data": { - "unique_id": "Device" + "unique_id": "[%key:common::config_flow::data::device%]" } } }, diff --git a/homeassistant/components/uptimerobot/strings.json b/homeassistant/components/uptimerobot/strings.json index 8fccc3cb9e9860..588dc3ebf5cbf4 100644 --- a/homeassistant/components/uptimerobot/strings.json +++ b/homeassistant/components/uptimerobot/strings.json @@ -35,7 +35,7 @@ "state": { "down": "Down", "not_checked_yet": "Not checked yet", - "pause": "Pause", + "pause": "[%key:common::action::pause%]", "seems_down": "Seems down", "up": "Up" } diff --git a/homeassistant/components/utility_meter/strings.json b/homeassistant/components/utility_meter/strings.json index 09b9dd0954046a..f38989b536eba3 100644 --- a/homeassistant/components/utility_meter/strings.json +++ b/homeassistant/components/utility_meter/strings.json @@ -8,7 +8,7 @@ "data": { "cycle": "Meter reset cycle", "delta_values": "Delta values", - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "periodically_resetting": "Periodically resetting", "net_consumption": "Net consumption", "offset": "Meter reset offset", diff --git a/homeassistant/components/vallox/strings.json b/homeassistant/components/vallox/strings.json index b33cef0026aaf1..42efaeb053871b 100644 --- a/homeassistant/components/vallox/strings.json +++ b/homeassistant/components/vallox/strings.json @@ -36,7 +36,7 @@ "fields": { "fan_speed": { "name": "Fan speed", - "description": "Fan speed." + "description": "[%key:component::vallox::services::set_profile_fan_speed_home::fields::fan_speed::description%]" } } }, @@ -46,7 +46,7 @@ "fields": { "fan_speed": { "name": "Fan speed", - "description": "Fan speed." + "description": "[%key:component::vallox::services::set_profile_fan_speed_home::fields::fan_speed::description%]" } } } diff --git a/homeassistant/components/velbus/strings.json b/homeassistant/components/velbus/strings.json index bef853001a1744..948c079444de6f 100644 --- a/homeassistant/components/velbus/strings.json +++ b/homeassistant/components/velbus/strings.json @@ -33,8 +33,8 @@ "description": "Scans the velbus modules, this will be need if you see unknown module warnings in the logs, or when you added new modules.", "fields": { "interface": { - "name": "Interface", - "description": "The velbus interface to send the command to, this will be the same value as used during configuration." + "name": "[%key:component::velbus::services::sync_clock::fields::interface::name%]", + "description": "[%key:component::velbus::services::sync_clock::fields::interface::description%]" } } }, @@ -43,8 +43,8 @@ "description": "Clears the velbuscache and then starts a new scan.", "fields": { "interface": { - "name": "Interface", - "description": "The velbus interface to send the command to, this will be the same value as used during configuration." + "name": "[%key:component::velbus::services::sync_clock::fields::interface::name%]", + "description": "[%key:component::velbus::services::sync_clock::fields::interface::description%]" }, "address": { "name": "Address", @@ -57,8 +57,8 @@ "description": "Sets the memo text to the display of modules like VMBGPO, VMBGPOD Be sure the page(s) of the module is configured to display the memo text.\n.", "fields": { "interface": { - "name": "Interface", - "description": "The velbus interface to send the command to, this will be the same value as used during configuration." + "name": "[%key:component::velbus::services::sync_clock::fields::interface::name%]", + "description": "[%key:component::velbus::services::sync_clock::fields::interface::description%]" }, "address": { "name": "Address", diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json index 4e51177910cabf..3bfb58f810412a 100644 --- a/homeassistant/components/vera/strings.json +++ b/homeassistant/components/vera/strings.json @@ -23,8 +23,8 @@ "title": "Vera controller options", "description": "See the vera documentation for details on optional parameters: https://www.home-assistant.io/integrations/vera/. Note: Any changes here will need a restart to the home assistant server. To clear values, provide a space.", "data": { - "lights": "Vera switch device ids to treat as lights in Home Assistant.", - "exclude": "Vera device ids to exclude from Home Assistant." + "lights": "[%key:component::vera::config::step::user::data::lights%]", + "exclude": "[%key:component::vera::config::step::user::data::exclude%]" } } } diff --git a/homeassistant/components/verisure/strings.json b/homeassistant/components/verisure/strings.json index 335daa68ee8a70..f715529b36bb7d 100644 --- a/homeassistant/components/verisure/strings.json +++ b/homeassistant/components/verisure/strings.json @@ -29,8 +29,8 @@ }, "reauth_mfa": { "data": { - "description": "Your account has 2-step verification enabled. Please enter the verification code Verisure sends to you.", - "code": "Verification Code" + "description": "[%key:component::verisure::config::step::mfa::data::description%]", + "code": "[%key:component::verisure::config::step::mfa::data::code%]" } } }, diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json index 314f6f8b4e5f8f..0ff64eeda53e23 100644 --- a/homeassistant/components/vizio/strings.json +++ b/homeassistant/components/vizio/strings.json @@ -23,7 +23,7 @@ "description": "Your VIZIO SmartCast Device is now connected to Home Assistant." }, "pairing_complete_import": { - "title": "Pairing Complete", + "title": "[%key:component::vizio::config::step::pairing_complete::title%]", "description": "Your VIZIO SmartCast Device is now connected to Home Assistant.\n\nYour access token is '**{access_token}**'." } }, diff --git a/homeassistant/components/vulcan/strings.json b/homeassistant/components/vulcan/strings.json index bb9e1d4d8488dc..b2b270e3422fc7 100644 --- a/homeassistant/components/vulcan/strings.json +++ b/homeassistant/components/vulcan/strings.json @@ -25,11 +25,11 @@ } }, "reauth_confirm": { - "description": "Login to your Vulcan Account using mobile app registration page.", + "description": "[%key:component::vulcan::config::step::auth::description%]", "data": { "token": "Token", - "region": "Symbol", - "pin": "Pin" + "region": "[%key:component::vulcan::config::step::auth::data::region%]", + "pin": "[%key:component::vulcan::config::step::auth::data::pin%]" } }, "select_student": { diff --git a/homeassistant/components/webostv/strings.json b/homeassistant/components/webostv/strings.json index 985edb05645a5a..a5e7b73e59e294 100644 --- a/homeassistant/components/webostv/strings.json +++ b/homeassistant/components/webostv/strings.json @@ -15,8 +15,8 @@ "description": "Click submit and accept the pairing request on your TV.\n\n![Image](/static/images/config_webos.png)" }, "reauth_confirm": { - "title": "webOS TV Pairing", - "description": "Click submit and accept the pairing request on your TV.\n\n![Image](/static/images/config_webos.png)" + "title": "[%key:component::webostv::config::step::pairing::title%]", + "description": "[%key:component::webostv::config::step::pairing::description%]" } }, "error": { @@ -70,7 +70,7 @@ "fields": { "entity_id": { "name": "Entity", - "description": "Name(s) of the webostv entities where to run the API method." + "description": "[%key:component::webostv::services::button::fields::entity_id::description%]" }, "command": { "name": "Command", diff --git a/homeassistant/components/whirlpool/strings.json b/homeassistant/components/whirlpool/strings.json index aff89019e4ca63..94dc9aa219fe2a 100644 --- a/homeassistant/components/whirlpool/strings.json +++ b/homeassistant/components/whirlpool/strings.json @@ -27,7 +27,7 @@ "delay_countdown": "Delay Countdown", "delay_paused": "Delay Paused", "smart_delay": "Smart Delay", - "smart_grid_pause": "Smart Delay", + "smart_grid_pause": "[%key:component::whirlpool::entity::sensor::whirlpool_machine::state::smart_delay%]", "pause": "[%key:common::state::paused%]", "running_maincycle": "Running Maincycle", "running_postcycle": "Running Postcycle", diff --git a/homeassistant/components/wiz/strings.json b/homeassistant/components/wiz/strings.json index 2efa3c9a1c3438..656219f13bbe1c 100644 --- a/homeassistant/components/wiz/strings.json +++ b/homeassistant/components/wiz/strings.json @@ -13,7 +13,7 @@ }, "pick_device": { "data": { - "device": "Device" + "device": "[%key:common::config_flow::data::device%]" } } }, diff --git a/homeassistant/components/wolflink/strings.json b/homeassistant/components/wolflink/strings.json index 3de74cbbf4c782..b1c332984a1c12 100644 --- a/homeassistant/components/wolflink/strings.json +++ b/homeassistant/components/wolflink/strings.json @@ -18,7 +18,7 @@ }, "device": { "data": { - "device_name": "Device" + "device_name": "[%key:common::config_flow::data::device%]" }, "title": "Select WOLF device" } @@ -59,7 +59,7 @@ "spreizung_hoch": "dT too wide", "spreizung_kf": "Spread KF", "test": "Test", - "start": "Start", + "start": "[%key:common::action::start%]", "frost_heizkreis": "Heating circuit frost", "frost_warmwasser": "DHW frost", "schornsteinfeger": "Emissions test", diff --git a/homeassistant/components/workday/strings.json b/homeassistant/components/workday/strings.json index 4aaf241536fa7c..a217a7a36b1ab4 100644 --- a/homeassistant/components/workday/strings.json +++ b/homeassistant/components/workday/strings.json @@ -60,8 +60,8 @@ } }, "error": { - "add_holiday_error": "Incorrect format on date (YYYY-MM-DD)", - "remove_holiday_error": "Incorrect format on date (YYYY-MM-DD) or holiday name not found", + "add_holiday_error": "[%key:component::workday::config::error::add_holiday_error%]", + "remove_holiday_error": "[%key:component::workday::config::error::remove_holiday_error%]", "already_configured": "Service with this configuration already exist" } }, diff --git a/homeassistant/components/xiaomi_aqara/strings.json b/homeassistant/components/xiaomi_aqara/strings.json index 0944c91fd830f4..a77b78c5a0961f 100644 --- a/homeassistant/components/xiaomi_aqara/strings.json +++ b/homeassistant/components/xiaomi_aqara/strings.json @@ -54,8 +54,8 @@ "description": "Plays a specific ringtone. The version of the gateway firmware must be 1.4.1_145 at least.", "fields": { "gw_mac": { - "name": "Gateway MAC", - "description": "MAC address of the Xiaomi Aqara Gateway." + "name": "[%key:component::xiaomi_aqara::services::add_device::fields::gw_mac::name%]", + "description": "[%key:component::xiaomi_aqara::services::add_device::fields::gw_mac::description%]" }, "ringtone_id": { "name": "Ringtone ID", @@ -76,8 +76,8 @@ "description": "Hardware address of the device to remove." }, "gw_mac": { - "name": "Gateway MAC", - "description": "MAC address of the Xiaomi Aqara Gateway." + "name": "[%key:component::xiaomi_aqara::services::add_device::fields::gw_mac::name%]", + "description": "[%key:component::xiaomi_aqara::services::add_device::fields::gw_mac::description%]" } } }, @@ -86,8 +86,8 @@ "description": "Stops a playing ringtone immediately.", "fields": { "gw_mac": { - "name": "Gateway MAC", - "description": "MAC address of the Xiaomi Aqara Gateway." + "name": "[%key:component::xiaomi_aqara::services::add_device::fields::gw_mac::name%]", + "description": "[%key:component::xiaomi_aqara::services::add_device::fields::gw_mac::description%]" } } } diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index 578d2a96ff83f4..a9588855818d5e 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -53,7 +53,7 @@ }, "options": { "error": { - "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country" + "cloud_credentials_incomplete": "[%key:component::xiaomi_miio::config::error::cloud_credentials_incomplete%]" }, "step": { "init": { @@ -112,7 +112,7 @@ "fields": { "entity_id": { "name": "Entity ID", - "description": "Name of the xiaomi miio entity." + "description": "[%key:component::xiaomi_miio::services::fan_reset_filter::fields::entity_id::description%]" }, "features": { "name": "Features", @@ -140,7 +140,7 @@ "fields": { "entity_id": { "name": "Entity ID", - "description": "Name of the light entity." + "description": "[%key:component::xiaomi_miio::services::light_set_scene::fields::entity_id::description%]" }, "time_period": { "name": "Time period", @@ -164,7 +164,7 @@ "fields": { "entity_id": { "name": "Entity ID", - "description": "Name of the entity to act on." + "description": "[%key:component::xiaomi_miio::services::light_reminder_on::fields::entity_id::description%]" } } }, @@ -174,7 +174,7 @@ "fields": { "entity_id": { "name": "Entity ID", - "description": "Name of the entity to act on." + "description": "[%key:component::xiaomi_miio::services::light_reminder_on::fields::entity_id::description%]" } } }, @@ -184,27 +184,27 @@ "fields": { "entity_id": { "name": "Entity ID", - "description": "Name of the entity to act on." + "description": "[%key:component::xiaomi_miio::services::light_reminder_on::fields::entity_id::description%]" } } }, "light_eyecare_mode_on": { "name": "Light eyecare mode on", - "description": "Enables the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY).", + "description": "[%key:component::xiaomi_miio::services::light_reminder_on::description%]", "fields": { "entity_id": { "name": "Entity ID", - "description": "Name of the entity to act on." + "description": "[%key:component::xiaomi_miio::services::light_reminder_on::fields::entity_id::description%]" } } }, "light_eyecare_mode_off": { "name": "Light eyecare mode off", - "description": "Disables the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY).", + "description": "[%key:component::xiaomi_miio::services::light_reminder_off::description%]", "fields": { "entity_id": { "name": "Entity ID", - "description": "Name of the entity to act on." + "description": "[%key:component::xiaomi_miio::services::light_reminder_on::fields::entity_id::description%]" } } }, @@ -236,7 +236,7 @@ "fields": { "entity_id": { "name": "Entity ID", - "description": "Name of the xiaomi miio entity." + "description": "[%key:component::xiaomi_miio::services::fan_reset_filter::fields::entity_id::description%]" } } }, @@ -246,7 +246,7 @@ "fields": { "entity_id": { "name": "Entity ID", - "description": "Name of the xiaomi miio entity." + "description": "[%key:component::xiaomi_miio::services::fan_reset_filter::fields::entity_id::description%]" } } }, @@ -256,10 +256,10 @@ "fields": { "entity_id": { "name": "Entity ID", - "description": "Name of the xiaomi miio entity." + "description": "[%key:component::xiaomi_miio::services::fan_reset_filter::fields::entity_id::description%]" }, "mode": { - "name": "Mode", + "name": "[%key:common::config_flow::data::mode%]", "description": "Power price." } } @@ -270,10 +270,10 @@ "fields": { "entity_id": { "name": "Entity ID", - "description": "Name of the xiaomi miio entity." + "description": "[%key:component::xiaomi_miio::services::fan_reset_filter::fields::entity_id::description%]" }, "mode": { - "name": "Mode", + "name": "[%key:common::config_flow::data::mode%]", "description": "Power mode." } } @@ -309,16 +309,16 @@ "description": "Remote controls the vacuum cleaner, only makes one move and then stops.", "fields": { "velocity": { - "name": "Velocity", - "description": "Speed." + "name": "[%key:component::xiaomi_miio::services::vacuum_remote_control_move::fields::velocity::name%]", + "description": "[%key:component::xiaomi_miio::services::vacuum_remote_control_move::fields::velocity::description%]" }, "rotation": { - "name": "Rotation", + "name": "[%key:component::xiaomi_miio::services::vacuum_remote_control_move::fields::rotation::name%]", "description": "Rotation." }, "duration": { "name": "Duration", - "description": "Duration of the movement." + "description": "[%key:component::xiaomi_miio::services::vacuum_remote_control_move::fields::duration::description%]" } } }, diff --git a/homeassistant/components/yale_smart_alarm/strings.json b/homeassistant/components/yale_smart_alarm/strings.json index 5928013e098235..ec0c5d0702a914 100644 --- a/homeassistant/components/yale_smart_alarm/strings.json +++ b/homeassistant/components/yale_smart_alarm/strings.json @@ -22,7 +22,7 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", "name": "[%key:common::config_flow::data::name%]", - "area_id": "Area ID" + "area_id": "[%key:component::yale_smart_alarm::config::step::user::data::area_id%]" } } } diff --git a/homeassistant/components/yamaha/strings.json b/homeassistant/components/yamaha/strings.json index ddfee94aa0449e..ecb69d9fc38d2f 100644 --- a/homeassistant/components/yamaha/strings.json +++ b/homeassistant/components/yamaha/strings.json @@ -5,7 +5,7 @@ "description": "Enables or disables an output port.", "fields": { "port": { - "name": "Port", + "name": "[%key:common::config_flow::data::port%]", "description": "Name of port to enable/disable." }, "enabled": { diff --git a/homeassistant/components/yamaha_musiccast/strings.json b/homeassistant/components/yamaha_musiccast/strings.json index af26ed13b38779..c4f28fc750bf9f 100644 --- a/homeassistant/components/yamaha_musiccast/strings.json +++ b/homeassistant/components/yamaha_musiccast/strings.json @@ -45,7 +45,7 @@ }, "zone_surr_decoder_type": { "state": { - "toggle": "Toggle", + "toggle": "[%key:common::action::toggle%]", "auto": "Auto", "dolby_pl": "Dolby ProLogic", "dolby_pl2x_movie": "Dolby ProLogic 2x Movie", @@ -61,7 +61,7 @@ "state": { "manual": "Manual", "auto": "Auto", - "bypass": "Bypass" + "bypass": "[%key:component::yamaha_musiccast::entity::select::zone_tone_control_mode::state::bypass%]" } }, "zone_link_audio_quality": { diff --git a/homeassistant/components/yeelight/strings.json b/homeassistant/components/yeelight/strings.json index 18b762057a78bb..03a93bd9a5bfe4 100644 --- a/homeassistant/components/yeelight/strings.json +++ b/homeassistant/components/yeelight/strings.json @@ -10,7 +10,7 @@ }, "pick_device": { "data": { - "device": "Device" + "device": "[%key:common::config_flow::data::device%]" } }, "discovery_confirm": { @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "model": "Model", + "model": "[%key:common::generic::model%]", "transition": "Transition Time (ms)", "use_music_mode": "Enable Music Mode", "save_on_change": "Save Status On Change", @@ -44,7 +44,7 @@ "description": "Sets a operation mode.", "fields": { "mode": { - "name": "Mode", + "name": "[%key:common::config_flow::data::mode%]", "description": "Operation mode." } } @@ -73,7 +73,7 @@ }, "brightness": { "name": "Brightness", - "description": "The brightness value to set." + "description": "[%key:component::yeelight::services::set_color_scene::fields::brightness::description%]" } } }, @@ -87,7 +87,7 @@ }, "brightness": { "name": "Brightness", - "description": "The brightness value to set." + "description": "[%key:component::yeelight::services::set_color_scene::fields::brightness::description%]" } } }, @@ -119,7 +119,7 @@ }, "brightness": { "name": "Brightness", - "description": "The brightness value to set." + "description": "[%key:component::yeelight::services::set_color_scene::fields::brightness::description%]" } } }, @@ -129,15 +129,15 @@ "fields": { "count": { "name": "Count", - "description": "The number of times to run this flow (0 to run forever)." + "description": "[%key:component::yeelight::services::set_color_flow_scene::fields::count::description%]" }, "action": { "name": "Action", - "description": "The action to take after the flow stops." + "description": "[%key:component::yeelight::services::set_color_flow_scene::fields::action::description%]" }, "transitions": { - "name": "Transitions", - "description": "Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html." + "name": "[%key:component::yeelight::services::set_color_flow_scene::fields::transitions::name%]", + "description": "[%key:component::yeelight::services::set_color_flow_scene::fields::transitions::description%]" } } }, @@ -165,7 +165,7 @@ }, "action": { "options": { - "off": "Off", + "off": "[%key:common::state::off%]", "recover": "Recover", "stay": "Stay" } diff --git a/homeassistant/components/youtube/strings.json b/homeassistant/components/youtube/strings.json index 1ecc2bc4db84eb..7f369e9909bd91 100644 --- a/homeassistant/components/youtube/strings.json +++ b/homeassistant/components/youtube/strings.json @@ -27,9 +27,9 @@ "options": { "step": { "init": { - "description": "Select the channels you want to add.", + "description": "[%key:component::youtube::config::step::channels::description%]", "data": { - "channels": "YouTube channels" + "channels": "[%key:component::youtube::config::step::channels::data::channels%]" } } } diff --git a/homeassistant/components/zamg/strings.json b/homeassistant/components/zamg/strings.json index f0a607f2da7da8..a92e7aa605e2b0 100644 --- a/homeassistant/components/zamg/strings.json +++ b/homeassistant/components/zamg/strings.json @@ -16,7 +16,7 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "station_not_found": "Station ID not found at zamg" + "station_not_found": "[%key:component::zamg::config::error::station_not_found%]" } } } diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 50eadfc6667f83..1e44191a762e0f 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -19,7 +19,7 @@ "data": { "radio_type": "Radio Type" }, - "title": "Radio Type", + "title": "[%key:component::zha::config::step::manual_pick_radio_type::data::radio_type%]", "description": "Pick your Zigbee radio type" }, "manual_port_config": { @@ -94,7 +94,7 @@ } }, "intent_migrate": { - "title": "Migrate to a new radio", + "title": "[%key:component::zha::options::step::prompt_migrate_or_reconfigure::menu_options::intent_migrate%]", "description": "Before plugging in your new radio, your old radio needs to be reset. An automatic backup will be performed. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\n*Note: if you are migrating from a **ConBee/RaspBee**, make sure it is running firmware `0x26720700` or newer! Otherwise, some devices may not be controllable after migrating until they are power cycled.*\n\nDo you wish to continue?" }, "instruct_unplug": { @@ -367,8 +367,8 @@ "description": "Parameters to pass to the command." }, "manufacturer": { - "name": "Manufacturer", - "description": "Manufacturer code." + "name": "[%key:component::zha::services::set_zigbee_cluster_attribute::fields::manufacturer::name%]", + "description": "[%key:component::zha::services::set_zigbee_cluster_attribute::fields::manufacturer::description%]" } } }, @@ -381,24 +381,24 @@ "description": "Hexadecimal address of the group." }, "cluster_id": { - "name": "Cluster ID", + "name": "[%key:component::zha::services::set_zigbee_cluster_attribute::fields::cluster_id::name%]", "description": "ZCL cluster to send command to." }, "cluster_type": { "name": "Cluster type", - "description": "Type of the cluster." + "description": "[%key:component::zha::services::set_zigbee_cluster_attribute::fields::cluster_type::description%]" }, "command": { "name": "Command", - "description": "ID of the command to execute." + "description": "[%key:component::zha::services::issue_zigbee_cluster_command::fields::command::description%]" }, "args": { "name": "Args", - "description": "Arguments to pass to the command." + "description": "[%key:component::zha::services::issue_zigbee_cluster_command::fields::args::description%]" }, "manufacturer": { - "name": "Manufacturer", - "description": "Manufacturer code." + "name": "[%key:component::zha::services::set_zigbee_cluster_attribute::fields::manufacturer::name%]", + "description": "[%key:component::zha::services::set_zigbee_cluster_attribute::fields::manufacturer::description%]" } } }, @@ -408,11 +408,11 @@ "fields": { "ieee": { "name": "[%key:component::zha::services::permit::fields::ieee::name%]", - "description": "IEEE address for the device." + "description": "[%key:component::zha::services::set_zigbee_cluster_attribute::fields::ieee::description%]" }, "mode": { - "name": "Mode", - "description": "The Squawk Mode field is used as a 4-bit enumeration, and can have one of the values shown in Table 8-24 of the ZCL spec - Squawk Mode Field. The exact operation of each mode (how the WD \u201csquawks\u201d) is implementation specific." + "name": "[%key:common::config_flow::data::mode%]", + "description": "The Squawk Mode field is used as a 4-bit enumeration, and can have one of the values shown in Table 8-24 of the ZCL spec - Squawk Mode Field. The exact operation of each mode (how the WD “squawks”) is implementation specific." }, "strobe": { "name": "Strobe", @@ -430,15 +430,15 @@ "fields": { "ieee": { "name": "[%key:component::zha::services::permit::fields::ieee::name%]", - "description": "IEEE address for the device." + "description": "[%key:component::zha::services::set_zigbee_cluster_attribute::fields::ieee::description%]" }, "mode": { - "name": "Mode", + "name": "[%key:common::config_flow::data::mode%]", "description": "The Warning Mode field is used as an 4-bit enumeration, can have one of the values 0-6 defined below in table 8-20 of the ZCL spec. The exact behavior of the WD device in each mode is according to the relevant security standards." }, "strobe": { - "name": "Strobe", - "description": "The Strobe field is used as a 2-bit enumeration, and determines if the visual indication is required in addition to the audible siren, as indicated in Table 8-21 of the ZCL spec. \"0\" means no strobe, \"1\" means strobe. If the strobe field is \u201c1\u201d and the Warning Mode is \u201c0\u201d (\u201cStop\u201d) then only the strobe is activated." + "name": "[%key:component::zha::services::warning_device_squawk::fields::strobe::name%]", + "description": "The Strobe field is used as a 2-bit enumeration, and determines if the visual indication is required in addition to the audible siren, as indicated in Table 8-21 of the ZCL spec. \"0\" means no strobe, \"1\" means strobe. If the strobe field is “1” and the Warning Mode is “0” (“Stop”) then only the strobe is activated." }, "level": { "name": "Level", @@ -450,7 +450,7 @@ }, "duty_cycle": { "name": "Duty cycle", - "description": "Indicates the length of the flash cycle. This allows you to vary the flash duration for different alarm types (e.g., fire, police, burglar). The valid range is 0-100 in increments of 10. All other values must be rounded to the nearest valid value. Strobe calculates a duty cycle over a duration of one second. The ON state must precede the OFF state. For example, if Strobe Duty Cycle Field specifies \u201c40,\u201d, then the strobe flashes ON for 4/10ths of a second and then turns OFF for 6/10ths of a second." + "description": "Indicates the length of the flash cycle. This allows you to vary the flash duration for different alarm types (e.g., fire, police, burglar). The valid range is 0-100 in increments of 10. All other values must be rounded to the nearest valid value. Strobe calculates a duty cycle over a duration of one second. The ON state must precede the OFF state. For example, if Strobe Duty Cycle Field specifies “40,”, then the strobe flashes ON for 4/10ths of a second and then turns OFF for 6/10ths of a second." }, "intensity": { "name": "Intensity", diff --git a/homeassistant/components/zoneminder/strings.json b/homeassistant/components/zoneminder/strings.json index 1e2e41d274107e..34e8b8454720f6 100644 --- a/homeassistant/components/zoneminder/strings.json +++ b/homeassistant/components/zoneminder/strings.json @@ -5,7 +5,7 @@ "description": "Sets the ZoneMinder run state.", "fields": { "name": { - "name": "Name", + "name": "[%key:common::config_flow::data::name%]", "description": "The string name of the ZoneMinder run state to set as active." } } diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 37b4577e5dfc10..3b86cbdd5a4bf3 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -74,50 +74,50 @@ } }, "on_supervisor": { - "title": "Select connection method", - "description": "Do you want to use the Z-Wave JS Supervisor add-on?", + "title": "[%key:component::zwave_js::config::step::on_supervisor::title%]", + "description": "[%key:component::zwave_js::config::step::on_supervisor::description%]", "data": { - "use_addon": "Use the Z-Wave JS Supervisor add-on" + "use_addon": "[%key:component::zwave_js::config::step::on_supervisor::data::use_addon%]" } }, "install_addon": { - "title": "The Z-Wave JS add-on installation has started" + "title": "[%key:component::zwave_js::config::step::install_addon::title%]" }, "configure_addon": { - "title": "Enter the Z-Wave JS add-on configuration", - "description": "The add-on will generate security keys if those fields are left empty.", + "title": "[%key:component::zwave_js::config::step::configure_addon::title%]", + "description": "[%key:component::zwave_js::config::step::configure_addon::description%]", "data": { "usb_path": "[%key:common::config_flow::data::usb_path%]", - "s0_legacy_key": "S0 Key (Legacy)", - "s2_authenticated_key": "S2 Authenticated Key", - "s2_unauthenticated_key": "S2 Unauthenticated Key", - "s2_access_control_key": "S2 Access Control Key", + "s0_legacy_key": "[%key:component::zwave_js::config::step::configure_addon::data::s0_legacy_key%]", + "s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_authenticated_key%]", + "s2_unauthenticated_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_unauthenticated_key%]", + "s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_access_control_key%]", "log_level": "Log level", "emulate_hardware": "Emulate Hardware" } }, "start_addon": { - "title": "The Z-Wave JS add-on is starting." + "title": "[%key:component::zwave_js::config::step::start_addon::title%]" } }, "error": { - "invalid_ws_url": "Invalid websocket URL", + "invalid_ws_url": "[%key:component::zwave_js::config::error::invalid_ws_url%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "addon_info_failed": "Failed to get Z-Wave JS add-on info.", - "addon_install_failed": "Failed to install the Z-Wave JS add-on.", - "addon_set_config_failed": "Failed to set Z-Wave JS configuration.", - "addon_start_failed": "Failed to start the Z-Wave JS add-on.", - "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.", + "addon_info_failed": "[%key:component::zwave_js::config::abort::addon_info_failed%]", + "addon_install_failed": "[%key:component::zwave_js::config::abort::addon_install_failed%]", + "addon_set_config_failed": "[%key:component::zwave_js::config::abort::addon_set_config_failed%]", + "addon_start_failed": "[%key:component::zwave_js::config::abort::addon_start_failed%]", + "addon_get_discovery_info_failed": "[%key:component::zwave_js::config::abort::addon_get_discovery_info_failed%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "different_device": "The connected USB device is not the same as previously configured for this config entry. Please instead create a new config entry for the new device." }, "progress": { - "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.", - "start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds." + "install_addon": "[%key:component::zwave_js::config::progress::install_addon%]", + "start_addon": "[%key:component::zwave_js::config::progress::start_addon%]" } }, "device_automation": { diff --git a/homeassistant/components/zwave_me/strings.json b/homeassistant/components/zwave_me/strings.json index 63add194d08e66..0c5a1d30976f7d 100644 --- a/homeassistant/components/zwave_me/strings.json +++ b/homeassistant/components/zwave_me/strings.json @@ -14,7 +14,7 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "no_valid_uuid_set": "No valid UUID set" + "no_valid_uuid_set": "[%key:component::zwave_me::config::error::no_valid_uuid_set%]" } } } diff --git a/script/translations/deduplicate.py b/script/translations/deduplicate.py new file mode 100644 index 00000000000000..86812318218b80 --- /dev/null +++ b/script/translations/deduplicate.py @@ -0,0 +1,131 @@ +"""Deduplicate translations in strings.json.""" + + +import argparse +import json +from pathlib import Path + +from homeassistant.const import Platform + +from . import upload +from .develop import flatten_translations +from .util import get_base_arg_parser + + +def get_arguments() -> argparse.Namespace: + """Get parsed passed in arguments.""" + parser = get_base_arg_parser() + parser.add_argument( + "--limit-reference", + "--lr", + action="store_true", + help="Only allow references to same strings.json or common.", + ) + return parser.parse_args() + + +STRINGS_PATH = "homeassistant/components/{}/strings.json" +ENTITY_COMPONENT_PREFIX = tuple(f"component::{domain}::" for domain in Platform) + + +def run(): + """Clean translations.""" + args = get_arguments() + translations = upload.generate_upload_data() + flattened_translations = flatten_translations(translations) + flattened_translations = { + key: value + for key, value in flattened_translations.items() + # Skip existing references + if not value.startswith("[%key:") + } + + primary = {} + secondary = {} + + for key, value in flattened_translations.items(): + if key.startswith("common::"): + primary[value] = key + elif key.startswith(ENTITY_COMPONENT_PREFIX): + primary.setdefault(value, key) + else: + secondary.setdefault(value, key) + + merged = {**secondary, **primary} + + # Questionable translations are ones that are duplicate but are not referenced + # by the common strings.json or strings.json from an entity component. + questionable = set(secondary.values()) + suggest_new_common = set() + update_keys = {} + + for key, value in flattened_translations.items(): + if merged[value] == key or key.startswith("common::"): + continue + + key_integration = key.split("::")[1] + + key_to_reference = merged[value] + key_to_reference_integration = key_to_reference.split("::")[1] + is_common = key_to_reference.startswith("common::") + + # If we want to only add references to own integrations + # but not include entity integrations + if ( + args.limit_reference + and (key_integration != key_to_reference_integration and not is_common) + # Do not create self-references in entity integrations + or key_integration in Platform.__members__.values() + ): + continue + + if ( + # We don't want integrations to reference arbitrary other integrations + key_to_reference in questionable + # Allow reference own integration + and key_to_reference_integration != key_integration + ): + suggest_new_common.add(value) + continue + + update_keys[key] = f"[%key:{key_to_reference}%]" + + if suggest_new_common: + print("Suggested new common words:") + for key in sorted(suggest_new_common): + print(key) + + components = sorted({key.split("::")[1] for key in update_keys}) + + strings = {} + + for component in components: + comp_strings_path = Path(STRINGS_PATH.format(component)) + strings[component] = json.loads(comp_strings_path.read_text(encoding="utf-8")) + + for path, value in update_keys.items(): + parts = path.split("::") + parts.pop(0) + component = parts.pop(0) + to_write = strings[component] + while len(parts) > 1: + try: + to_write = to_write[parts.pop(0)] + except KeyError: + print(to_write) + raise + + to_write[parts.pop(0)] = value + + for component in components: + comp_strings_path = Path(STRINGS_PATH.format(component)) + comp_strings_path.write_text( + json.dumps( + strings[component], + indent=2, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + return 0 diff --git a/script/translations/develop.py b/script/translations/develop.py index a318c7c08bc97e..3bfaa279e93591 100644 --- a/script/translations/develop.py +++ b/script/translations/develop.py @@ -92,6 +92,7 @@ def substitute_reference(value, flattened_translations): def run_single(translations, flattened_translations, integration): """Run the script for a single integration.""" + print(f"Generating translations for {integration}") if integration not in translations["component"]: print("Integration has no strings.json") @@ -114,8 +115,6 @@ def run_single(translations, flattened_translations, integration): download.write_integration_translations() - print(f"Generating translations for {integration}") - def run(): """Run the script.""" diff --git a/script/translations/util.py b/script/translations/util.py index 9839fefd9d540e..0c8c8a2a30f784 100644 --- a/script/translations/util.py +++ b/script/translations/util.py @@ -13,7 +13,15 @@ def get_base_arg_parser() -> argparse.ArgumentParser: parser.add_argument( "action", type=str, - choices=["clean", "develop", "download", "frontend", "migrate", "upload"], + choices=[ + "clean", + "deduplicate", + "develop", + "download", + "frontend", + "migrate", + "upload", + ], ) parser.add_argument("--debug", action="store_true", help="Enable log output") return parser From 025ed3868df866593ea2b25d447a0121bb98f1ad Mon Sep 17 00:00:00 2001 From: Mads Nedergaard Date: Thu, 13 Jul 2023 17:57:31 +0200 Subject: [PATCH 0457/1009] Rename CO2Signal to Electricity Maps (#96252) * Changes names and links * Changes link to documentation * Updates generated integration name --- homeassistant/components/co2signal/const.py | 2 +- homeassistant/components/co2signal/manifest.json | 4 ++-- homeassistant/components/co2signal/sensor.py | 6 +++--- homeassistant/components/co2signal/strings.json | 2 +- homeassistant/generated/integrations.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/co2signal/const.py b/homeassistant/components/co2signal/const.py index a1264acc9ff97f..1e0cbfe0f11275 100644 --- a/homeassistant/components/co2signal/const.py +++ b/homeassistant/components/co2signal/const.py @@ -3,4 +3,4 @@ DOMAIN = "co2signal" CONF_COUNTRY_CODE = "country_code" -ATTRIBUTION = "Data provided by CO2signal" +ATTRIBUTION = "Data provided by Electricity Maps" diff --git a/homeassistant/components/co2signal/manifest.json b/homeassistant/components/co2signal/manifest.json index b4dc01d03aa6d0..0c5e6f4139b21e 100644 --- a/homeassistant/components/co2signal/manifest.json +++ b/homeassistant/components/co2signal/manifest.json @@ -1,9 +1,9 @@ { "domain": "co2signal", - "name": "CO2 Signal", + "name": "Electricity Maps", "codeowners": [], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/co2signal", + "documentation": "https://www.home-assistant.io/integrations/electricity_maps", "iot_class": "cloud_polling", "loggers": ["CO2Signal"], "requirements": ["CO2Signal==0.4.2"] diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 9f133c0b0ca565..ae22fb7b7efa17 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -75,11 +75,11 @@ def __init__( "country_code": coordinator.data["countryCode"], } self._attr_device_info = DeviceInfo( - configuration_url="https://www.electricitymap.org/", + configuration_url="https://www.electricitymaps.com/", entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, coordinator.entry_id)}, - manufacturer="Tmrow.com", - name="CO2 signal", + manufacturer="Electricity Maps", + name="Electricity Maps", ) self._attr_unique_id = ( f"{coordinator.entry_id}_{description.unique_id or description.key}" diff --git a/homeassistant/components/co2signal/strings.json b/homeassistant/components/co2signal/strings.json index 78274b0586cfdf..01c5673d4b1b11 100644 --- a/homeassistant/components/co2signal/strings.json +++ b/homeassistant/components/co2signal/strings.json @@ -6,7 +6,7 @@ "location": "Get data for", "api_key": "[%key:common::config_flow::data::access_token%]" }, - "description": "Visit https://co2signal.com/ to request a token." + "description": "Visit https://electricitymaps.com/free-tier to request a token." }, "coordinates": { "data": { diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 21f7acd59e39c0..4dcde6d883f6f8 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -846,7 +846,7 @@ "iot_class": "local_polling" }, "co2signal": { - "name": "CO2 Signal", + "name": "Electricity Maps", "integration_type": "hub", "config_flow": true, "iot_class": "cloud_polling" From fbbdebee47620b00ff3c200d2d7dea9d0c340ebd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 13 Jul 2023 18:14:31 +0200 Subject: [PATCH 0458/1009] Correct unifi device info (#96483) --- homeassistant/components/unifi/controller.py | 1 - tests/components/unifi/test_controller.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 60507d5a8c6ba1..6ac4e6227369ca 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -345,7 +345,6 @@ async def async_update_device_registry(self) -> None: device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, - configuration_url=self.api.url, connections={(CONNECTION_NETWORK_MAC, self.mac)}, default_manufacturer=ATTR_MANUFACTURER, default_model="UniFi Network", diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index e3efaef915b168..d0f387a3c6c05f 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -261,8 +261,7 @@ async def test_controller_mac( config_entry_id=config_entry.entry_id, connections={(CONNECTION_NETWORK_MAC, controller.mac)}, ) - - assert device_entry.configuration_url == controller.api.url + assert device_entry async def test_controller_not_accessible(hass: HomeAssistant) -> None: From 5b93017740a8153f5c1a4d4202b224f47326be90 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 13 Jul 2023 18:15:28 +0200 Subject: [PATCH 0459/1009] Correct huawei_lte device info (#96481) --- homeassistant/components/huawei_lte/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 95197dcbb49f52..3c101dff9cc67d 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -413,9 +413,8 @@ def _connect() -> Connection: device_info = DeviceInfo( configuration_url=router.url, connections=router.device_connections, - default_manufacturer=DEFAULT_MANUFACTURER, identifiers=router.device_identifiers, - manufacturer=entry.data.get(CONF_MANUFACTURER), + manufacturer=entry.data.get(CONF_MANUFACTURER, DEFAULT_MANUFACTURER), name=router.device_name, ) hw_version = None From 8440f14a08047a802e7188d62ad4f4149f79eff4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 13 Jul 2023 18:15:46 +0200 Subject: [PATCH 0460/1009] Correct dlna_dmr device info (#96480) --- .../components/dlna_dmr/media_player.py | 2 - .../components/dlna_dmr/test_media_player.py | 41 +++++++++++++++---- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index eddb2633beacfd..50877756d521fc 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -37,7 +37,6 @@ CONF_CALLBACK_URL_OVERRIDE, CONF_LISTEN_PORT, CONF_POLL_AVAILABILITY, - DOMAIN, LOGGER as _LOGGER, MEDIA_METADATA_DIDL, MEDIA_TYPE_MAP, @@ -381,7 +380,6 @@ def _update_device_registry(self, set_mac: bool = False) -> None: device_entry = dev_reg.async_get_or_create( config_entry_id=self.registry_entry.config_entry_id, connections=connections, - identifiers={(DOMAIN, self.unique_id)}, default_manufacturer=self._device.manufacturer, default_model=self._device.model_name, default_name=self._device.name, diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index e07e0b6cfcba44..f8413e8f6200cc 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -50,6 +50,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, + CONNECTION_UPNP, async_get as async_get_dr, ) from homeassistant.helpers.entity_component import async_update_entity @@ -347,7 +348,10 @@ async def test_setup_entry_mac_address( # Check the device registry connections for MAC address dev_reg = async_get_dr(hass) - device = dev_reg.async_get_device(identifiers={(DLNA_DOMAIN, MOCK_DEVICE_UDN)}) + device = dev_reg.async_get_device( + connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + identifiers=set(), + ) assert device is not None assert (CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) in device.connections @@ -364,7 +368,10 @@ async def test_setup_entry_no_mac_address( # Check the device registry connections does not include the MAC address dev_reg = async_get_dr(hass) - device = dev_reg.async_get_device(identifiers={(DLNA_DOMAIN, MOCK_DEVICE_UDN)}) + device = dev_reg.async_get_device( + connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + identifiers=set(), + ) assert device is not None assert (CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) not in device.connections @@ -427,7 +434,10 @@ async def test_available_device( """Test a DlnaDmrEntity with a connected DmrDevice.""" # Check hass device information is filled in dev_reg = async_get_dr(hass) - device = dev_reg.async_get_device(identifiers={(DLNA_DOMAIN, MOCK_DEVICE_UDN)}) + device = dev_reg.async_get_device( + connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + identifiers=set(), + ) assert device is not None # Device properties are set in dmr_device_mock before the entity gets constructed assert device.manufacturer == "device_manufacturer" @@ -1323,7 +1333,10 @@ async def test_unavailable_device( # Check hass device information has not been filled in yet dev_reg = async_get_dr(hass) - device = dev_reg.async_get_device(identifiers={(DLNA_DOMAIN, MOCK_DEVICE_UDN)}) + device = dev_reg.async_get_device( + connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + identifiers=set(), + ) assert device is None # Unload config entry to clean up @@ -1360,7 +1373,10 @@ async def test_become_available( # Check hass device information has not been filled in yet dev_reg = async_get_dr(hass) - device = dev_reg.async_get_device(identifiers={(DLNA_DOMAIN, MOCK_DEVICE_UDN)}) + device = dev_reg.async_get_device( + connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + identifiers=set(), + ) assert device is None # Mock device is now available. @@ -1399,7 +1415,10 @@ async def test_become_available( assert mock_state.state == MediaPlayerState.IDLE # Check hass device information is now filled in dev_reg = async_get_dr(hass) - device = dev_reg.async_get_device(identifiers={(DLNA_DOMAIN, MOCK_DEVICE_UDN)}) + device = dev_reg.async_get_device( + connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + identifiers=set(), + ) assert device is not None assert device.manufacturer == "device_manufacturer" assert device.model == "device_model_name" @@ -2231,7 +2250,10 @@ async def test_config_update_mac_address( # Check the device registry connections does not include the MAC address dev_reg = async_get_dr(hass) - device = dev_reg.async_get_device(identifiers={(DLNA_DOMAIN, MOCK_DEVICE_UDN)}) + device = dev_reg.async_get_device( + connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + identifiers=set(), + ) assert device is not None assert (CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) not in device.connections @@ -2248,6 +2270,9 @@ async def test_config_update_mac_address( await hass.async_block_till_done() # Device registry connections should now include the MAC address - device = dev_reg.async_get_device(identifiers={(DLNA_DOMAIN, MOCK_DEVICE_UDN)}) + device = dev_reg.async_get_device( + connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, + identifiers=set(), + ) assert device is not None assert (CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) in device.connections From 580fd92ef2743d0f7f388a88032f16a215910758 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 13 Jul 2023 18:17:13 +0200 Subject: [PATCH 0461/1009] Correct knx device info (#96482) --- homeassistant/components/knx/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/knx/device.py b/homeassistant/components/knx/device.py index 452de577ce0830..18e6197360a480 100644 --- a/homeassistant/components/knx/device.py +++ b/homeassistant/components/knx/device.py @@ -25,7 +25,7 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, xknx: XKNX) -> None: _device_id = (DOMAIN, f"_{entry.entry_id}_interface") self.device = self.device_registry.async_get_or_create( config_entry_id=entry.entry_id, - default_name="KNX Interface", + name="KNX Interface", identifiers={_device_id}, ) self.device_info = DeviceInfo(identifiers={_device_id}) From 5f4643605785bb67e5a9c3eb07a8ad9afd343646 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Jul 2023 06:43:50 -1000 Subject: [PATCH 0462/1009] Bump yalexs-ble to 2.2.0 (#96460) --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index ca4e799f16b0dd..31d0ff09467612 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==1.5.1", "yalexs-ble==2.1.18"] + "requirements": ["yalexs==1.5.1", "yalexs-ble==2.2.0"] } diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 4822b2d27041e4..67b4e1c929956b 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", "iot_class": "local_push", - "requirements": ["yalexs-ble==2.1.18"] + "requirements": ["yalexs-ble==2.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 26d4911dddaad0..103bcb889ef6b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2711,7 +2711,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.1.18 +yalexs-ble==2.2.0 # homeassistant.components.august yalexs==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 864938446e8d7a..114e164481c3f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1990,7 +1990,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.1.18 +yalexs-ble==2.2.0 # homeassistant.components.august yalexs==1.5.1 From 7539cf25beb712d35b778e534671684ac6b8ae3f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 13 Jul 2023 19:39:25 +0200 Subject: [PATCH 0463/1009] Don't require passing identifiers to DeviceRegistry.async_get_device (#96479) * Require keyword arguments to DeviceRegistry.async_get_device * Update tests * Update tests * Don't enforce keyword arguments --- homeassistant/components/airly/__init__.py | 2 +- homeassistant/components/bosch_shc/entity.py | 4 +- homeassistant/components/broadlink/device.py | 4 +- homeassistant/components/deconz/services.py | 3 +- .../components/device_tracker/config_entry.py | 2 +- homeassistant/components/dhcp/__init__.py | 2 +- homeassistant/components/gios/__init__.py | 2 +- homeassistant/components/hassio/__init__.py | 4 +- homeassistant/components/heos/__init__.py | 4 +- .../components/home_plus_control/__init__.py | 2 +- .../homekit_controller/config_flow.py | 2 +- homeassistant/components/hue/v2/device.py | 2 +- homeassistant/components/hue/v2/entity.py | 4 +- homeassistant/components/hue/v2/hue_event.py | 4 +- .../components/ibeacon/coordinator.py | 4 +- .../components/insteon/api/device.py | 4 +- homeassistant/components/kodi/media_player.py | 2 +- homeassistant/components/lcn/__init__.py | 2 +- homeassistant/components/lcn/helpers.py | 6 +- .../components/lutron_caseta/__init__.py | 2 +- homeassistant/components/matter/adapter.py | 2 +- homeassistant/components/nest/__init__.py | 4 +- homeassistant/components/nest/device_info.py | 4 +- homeassistant/components/nest/media_source.py | 2 +- .../components/netatmo/netatmo_entity_base.py | 2 +- homeassistant/components/notion/__init__.py | 8 +- homeassistant/components/onvif/config_flow.py | 2 +- .../components/overkiz/coordinator.py | 4 +- homeassistant/components/sabnzbd/__init__.py | 2 +- homeassistant/components/shelly/__init__.py | 2 - .../components/simplisafe/__init__.py | 2 +- homeassistant/components/tasmota/__init__.py | 4 +- .../components/tasmota/device_trigger.py | 3 +- homeassistant/components/tasmota/discovery.py | 2 +- homeassistant/components/tibber/sensor.py | 4 +- homeassistant/components/unifiprotect/data.py | 2 +- .../components/uptimerobot/__init__.py | 2 +- homeassistant/components/zwave_js/__init__.py | 16 +-- homeassistant/components/zwave_js/api.py | 2 +- .../components/zwave_js/triggers/event.py | 2 +- .../zwave_js/triggers/value_updated.py | 2 +- homeassistant/components/zwave_me/__init__.py | 2 +- homeassistant/helpers/device_registry.py | 17 ++-- .../components/assist_pipeline/test_select.py | 2 +- tests/components/broadlink/test_device.py | 4 +- tests/components/broadlink/test_remote.py | 6 +- tests/components/broadlink/test_sensors.py | 22 ++--- tests/components/broadlink/test_switch.py | 8 +- .../components/bthome/test_device_trigger.py | 6 +- tests/components/canary/test_sensor.py | 4 +- tests/components/daikin/test_init.py | 5 +- tests/components/dlink/test_init.py | 2 +- .../components/dremel_3d_printer/test_init.py | 4 +- tests/components/efergy/test_init.py | 2 +- tests/components/flux_led/test_light.py | 2 +- .../freedompro/test_binary_sensor.py | 2 +- tests/components/freedompro/test_climate.py | 2 +- tests/components/freedompro/test_cover.py | 2 +- tests/components/freedompro/test_fan.py | 2 +- tests/components/freedompro/test_lock.py | 2 +- .../fully_kiosk/test_diagnostics.py | 2 +- tests/components/goalzero/test_init.py | 2 +- tests/components/gogogate2/test_cover.py | 4 +- tests/components/google_mail/test_init.py | 2 +- tests/components/heos/test_media_player.py | 4 +- .../home_plus_control/test_switch.py | 2 +- tests/components/homekit/test_homekit.py | 6 +- tests/components/homekit_controller/common.py | 4 +- .../components/hue/test_device_trigger_v1.py | 6 +- .../components/hue/test_device_trigger_v2.py | 2 +- tests/components/hue/test_sensor_v1.py | 6 +- tests/components/hyperion/test_camera.py | 2 +- tests/components/hyperion/test_light.py | 2 +- tests/components/hyperion/test_switch.py | 2 +- tests/components/ibeacon/test_init.py | 3 +- tests/components/lcn/conftest.py | 2 +- tests/components/lcn/test_device_trigger.py | 6 +- tests/components/lidarr/test_init.py | 2 +- tests/components/lifx/test_light.py | 3 +- tests/components/matter/test_adapter.py | 18 +++- tests/components/mobile_app/test_webhook.py | 4 +- tests/components/motioneye/test_camera.py | 10 +- tests/components/motioneye/test_sensor.py | 2 +- tests/components/motioneye/test_switch.py | 2 +- tests/components/mqtt/test_common.py | 26 ++--- tests/components/mqtt/test_device_tracker.py | 4 +- tests/components/mqtt/test_device_trigger.py | 98 ++++++++++++------- tests/components/mqtt/test_diagnostics.py | 4 +- tests/components/mqtt/test_discovery.py | 20 ++-- tests/components/mqtt/test_init.py | 22 ++--- tests/components/mqtt/test_sensor.py | 2 +- tests/components/mqtt/test_tag.py | 90 +++++++++++------ tests/components/nest/test_device_trigger.py | 6 +- tests/components/nest/test_diagnostics.py | 2 +- tests/components/nest/test_media_source.py | 42 ++++---- .../components/netatmo/test_device_trigger.py | 6 +- .../components/purpleair/test_config_flow.py | 4 +- tests/components/radarr/test_init.py | 2 +- tests/components/rainbird/test_number.py | 2 +- tests/components/renault/__init__.py | 4 +- tests/components/renault/test_diagnostics.py | 4 +- tests/components/renault/test_services.py | 6 +- tests/components/rfxtrx/test_device_action.py | 16 ++- .../components/rfxtrx/test_device_trigger.py | 16 ++- .../risco/test_alarm_control_panel.py | 12 ++- tests/components/risco/test_binary_sensor.py | 16 ++- tests/components/sharkiq/test_vacuum.py | 2 +- .../smartthings/test_binary_sensor.py | 2 +- tests/components/smartthings/test_climate.py | 4 +- tests/components/smartthings/test_cover.py | 2 +- tests/components/smartthings/test_fan.py | 2 +- tests/components/smartthings/test_light.py | 2 +- tests/components/smartthings/test_lock.py | 2 +- tests/components/smartthings/test_sensor.py | 12 +-- tests/components/smartthings/test_switch.py | 2 +- tests/components/steam_online/test_init.py | 2 +- tests/components/steamist/test_init.py | 2 +- tests/components/tasmota/test_common.py | 8 +- .../components/tasmota/test_device_trigger.py | 40 ++++---- tests/components/tasmota/test_discovery.py | 52 +++++----- tests/components/tasmota/test_init.py | 12 +-- tests/components/twinkly/test_light.py | 2 +- tests/components/velbus/test_init.py | 8 +- tests/components/voip/test_devices.py | 12 ++- tests/components/webostv/test_media_player.py | 4 +- .../xiaomi_ble/test_device_trigger.py | 8 +- .../components/yolink/test_device_trigger.py | 2 +- tests/components/youtube/test_init.py | 2 +- tests/components/zha/test_device_action.py | 12 ++- tests/components/zha/test_device_trigger.py | 20 +++- tests/components/zha/test_diagnostics.py | 2 +- tests/components/zha/test_logbook.py | 8 +- tests/components/zwave_js/test_api.py | 4 +- .../components/zwave_js/test_device_action.py | 20 ++-- .../zwave_js/test_device_condition.py | 18 ++-- .../zwave_js/test_device_trigger.py | 54 +++++----- tests/components/zwave_js/test_diagnostics.py | 10 +- tests/components/zwave_js/test_init.py | 4 +- tests/components/zwave_js/test_services.py | 18 ++-- tests/components/zwave_js/test_trigger.py | 6 +- .../zwave_me/test_remove_stale_devices.py | 2 +- tests/helpers/test_device_registry.py | 42 ++++---- tests/helpers/test_entity_platform.py | 12 ++- 143 files changed, 654 insertions(+), 494 deletions(-) diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 85b8b22043a48c..f52bdca4b86084 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -90,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: str(longitude), ), ): - device_entry = device_registry.async_get_device({old_ids}) # type: ignore[arg-type] + device_entry = device_registry.async_get_device(identifiers={old_ids}) # type: ignore[arg-type] if device_entry and entry.entry_id in device_entry.config_entries: new_ids = (DOMAIN, f"{latitude}-{longitude}") device_registry.async_update_device( diff --git a/homeassistant/components/bosch_shc/entity.py b/homeassistant/components/bosch_shc/entity.py index de3e2f9d3eaef7..3cf92a8adcc75b 100644 --- a/homeassistant/components/bosch_shc/entity.py +++ b/homeassistant/components/bosch_shc/entity.py @@ -15,9 +15,7 @@ async def async_remove_devices( ) -> None: """Get item that is removed from session.""" dev_registry = get_dev_reg(hass) - device = dev_registry.async_get_device( - identifiers={(DOMAIN, entity.device_id)}, connections=set() - ) + device = dev_registry.async_get_device(identifiers={(DOMAIN, entity.device_id)}) if device is not None: dev_registry.async_update_device(device.id, remove_config_entry_id=entry_id) diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index 87d8cf398fb249..69e1161a65ce52 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -80,7 +80,9 @@ async def async_update(hass: HomeAssistant, entry: ConfigEntry) -> None: """ device_registry = dr.async_get(hass) assert entry.unique_id - device_entry = device_registry.async_get_device({(DOMAIN, entry.unique_id)}) + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, entry.unique_id)} + ) assert device_entry device_registry.async_update_device(device_entry.id, name=entry.title) await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 5cea4ca3b159d5..bcac6ac1e1dd35 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -168,14 +168,13 @@ async def async_remove_orphaned_entries_service(gateway: DeconzGateway) -> None: if gateway.api.config.mac: gateway_host = device_registry.async_get_device( connections={(CONNECTION_NETWORK_MAC, gateway.api.config.mac)}, - identifiers=set(), ) if gateway_host and gateway_host.id in devices_to_be_removed: devices_to_be_removed.remove(gateway_host.id) # Don't remove the Gateway service entry gateway_service = device_registry.async_get_device( - identifiers={(DOMAIN, gateway.api.config.bridge_id)}, connections=set() + identifiers={(DOMAIN, gateway.api.config.bridge_id)} ) if gateway_service and gateway_service.id in devices_to_be_removed: devices_to_be_removed.remove(gateway_service.id) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 286929c534588d..05edfbad91db1f 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -365,7 +365,7 @@ def find_device_entry(self) -> dr.DeviceEntry | None: assert self.mac_address is not None return dr.async_get(self.hass).async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, self.mac_address)} + connections={(dr.CONNECTION_NETWORK_MAC, self.mac_address)} ) async def async_internal_added_to_hass(self) -> None: diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 4a9f6c2b163dab..9f9ec48f347929 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -196,7 +196,7 @@ def async_process_client( dev_reg: DeviceRegistry = async_get(self.hass) if device := dev_reg.async_get_device( - identifiers=set(), connections={(CONNECTION_NETWORK_MAC, uppercase_mac)} + connections={(CONNECTION_NETWORK_MAC, uppercase_mac)} ): for entry_id in device.config_entries: if entry := self.hass.config_entries.async_get_entry(entry_id): diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index 213fabc911bf93..2b56a9f6cbb174 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # We used to use int in device_entry identifiers, convert this to str. device_registry = dr.async_get(hass) old_ids = (DOMAIN, station_id) - device_entry = device_registry.async_get_device({old_ids}) # type: ignore[arg-type] + device_entry = device_registry.async_get_device(identifiers={old_ids}) # type: ignore[arg-type] if device_entry and entry.entry_id in device_entry.config_entries: new_ids = (DOMAIN, str(station_id)) device_registry.async_update_device(device_entry.id, new_identifiers={new_ids}) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 8c7f86700e7a07..9227b7da617bbf 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -758,7 +758,7 @@ def async_remove_addons_from_dev_reg( ) -> None: """Remove addons from the device registry.""" for addon_slug in addons: - if dev := dev_reg.async_get_device({(DOMAIN, addon_slug)}): + if dev := dev_reg.async_get_device(identifiers={(DOMAIN, addon_slug)}): dev_reg.async_remove_device(dev.id) @@ -855,7 +855,7 @@ async def _async_update_data(self) -> dict[str, Any]: async_remove_addons_from_dev_reg(self.dev_reg, stale_addons) if not self.is_hass_os and ( - dev := self.dev_reg.async_get_device({(DOMAIN, "OS")}) + dev := self.dev_reg.async_get_device(identifiers={(DOMAIN, "OS")}) ): # Remove the OS device if it exists and the installation is not hassos self.dev_reg.async_remove_device(dev.id) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 7aff107d8c5c6c..c50b70245e3b56 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -220,7 +220,9 @@ def update_ids(self, mapped_ids: dict[int, int]): # mapped_ids contains the mapped IDs (new:old) for new_id, old_id in mapped_ids.items(): # update device registry - entry = self._device_registry.async_get_device({(DOMAIN, old_id)}) + entry = self._device_registry.async_get_device( + identifiers={(DOMAIN, old_id)} + ) new_identifiers = {(DOMAIN, new_id)} if entry: self._device_registry.async_update_device( diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index d58086e59ecd2c..007f8895bf042c 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -128,7 +128,7 @@ def _async_update_entities(): entity_uids_to_remove = uids - set(module_data) for uid in entity_uids_to_remove: uids.remove(uid) - device = device_registry.async_get_device({(DOMAIN, uid)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, uid)}) device_registry.async_remove_device(device.id) # Send out signal for new entity addition to Home Assistant diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index f450c38527a60b..988adbd87a7b86 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -203,7 +203,7 @@ async def _hkid_is_homekit(self, hkid: str) -> bool: """Determine if the device is a homekit bridge or accessory.""" dev_reg = dr.async_get(self.hass) device = dev_reg.async_get_device( - identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, hkid)} + connections={(dr.CONNECTION_NETWORK_MAC, hkid)} ) if device is None: diff --git a/homeassistant/components/hue/v2/device.py b/homeassistant/components/hue/v2/device.py index bc3ce49cb6b4a5..6fed4bc16d1b16 100644 --- a/homeassistant/components/hue/v2/device.py +++ b/homeassistant/components/hue/v2/device.py @@ -58,7 +58,7 @@ def add_device(hue_device: Device) -> dr.DeviceEntry: @callback def remove_device(hue_device_id: str) -> None: """Remove device from registry.""" - if device := dev_reg.async_get_device({(DOMAIN, hue_device_id)}): + if device := dev_reg.async_get_device(identifiers={(DOMAIN, hue_device_id)}): # note: removal of any underlying entities is handled by core dev_reg.async_remove_device(device.id) diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 5878f01889b8db..ef01b2e4693048 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -147,7 +147,9 @@ def _handle_event(self, event_type: EventType, resource: HueResource) -> None: # regular devices are removed automatically by the logic in device.py. if resource.type in (ResourceTypes.ROOM, ResourceTypes.ZONE): dev_reg = async_get_device_registry(self.hass) - if device := dev_reg.async_get_device({(DOMAIN, resource.id)}): + if device := dev_reg.async_get_device( + identifiers={(DOMAIN, resource.id)} + ): dev_reg.async_remove_device(device.id) # cleanup entities that are not strictly device-bound and have the bridge as parent if self.device is None: diff --git a/homeassistant/components/hue/v2/hue_event.py b/homeassistant/components/hue/v2/hue_event.py index e0296bcb4346da..b8521a80af70cc 100644 --- a/homeassistant/components/hue/v2/hue_event.py +++ b/homeassistant/components/hue/v2/hue_event.py @@ -44,7 +44,7 @@ def handle_button_event(evt_type: EventType, hue_resource: Button) -> None: return hue_device = btn_controller.get_device(hue_resource.id) - device = dev_reg.async_get_device({(DOMAIN, hue_device.id)}) + device = dev_reg.async_get_device(identifiers={(DOMAIN, hue_device.id)}) # Fire event data = { @@ -70,7 +70,7 @@ def handle_rotary_event(evt_type: EventType, hue_resource: RelativeRotary) -> No LOGGER.debug("Received relative_rotary event: %s", hue_resource) hue_device = btn_controller.get_device(hue_resource.id) - device = dev_reg.async_get_device({(DOMAIN, hue_device.id)}) + device = dev_reg.async_get_device(identifiers={(DOMAIN, hue_device.id)}) # Fire event data = { diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py index 2e9af4ad9e6e1f..537b4b8f860096 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -200,7 +200,9 @@ def _async_ignore_address(self, address: str) -> None: def _async_purge_untrackable_entities(self, unique_ids: set[str]) -> None: """Remove entities that are no longer trackable.""" for unique_id in unique_ids: - if device := self._dev_reg.async_get_device({(DOMAIN, unique_id)}): + if device := self._dev_reg.async_get_device( + identifiers={(DOMAIN, unique_id)} + ): self._dev_reg.async_remove_device(device.id) self._last_ibeacon_advertisement_by_unique_id.pop(unique_id, None) diff --git a/homeassistant/components/insteon/api/device.py b/homeassistant/components/insteon/api/device.py index bffda96545686b..d48d87fa3475e5 100644 --- a/homeassistant/components/insteon/api/device.py +++ b/homeassistant/components/insteon/api/device.py @@ -43,9 +43,7 @@ def get_insteon_device_from_ha_device(ha_device): async def async_device_name(dev_registry, address): """Get the Insteon device name from a device registry id.""" - ha_device = dev_registry.async_get_device( - identifiers={(DOMAIN, str(address))}, connections=set() - ) + ha_device = dev_registry.async_get_device(identifiers={(DOMAIN, str(address))}) if not ha_device: if device := devices[address]: return f"{device.description} ({device.model})" diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index af4e5700805124..4a7f30506b25cd 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -422,7 +422,7 @@ async def _on_ws_connected(self): version = (await self._kodi.get_application_properties(["version"]))["version"] sw_version = f"{version['major']}.{version['minor']}" dev_reg = dr.async_get(self.hass) - device = dev_reg.async_get_device({(DOMAIN, self.unique_id)}) + device = dev_reg.async_get_device(identifiers={(DOMAIN, self.unique_id)}) dev_reg.async_update_device(device.id, sw_version=sw_version) self._device_id = device.id diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 72b66bc5cf1929..7ef7eb736737c0 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -180,7 +180,7 @@ def async_host_input_received( logical_address.is_group, ) identifiers = {(DOMAIN, generate_unique_id(config_entry.entry_id, address))} - device = device_registry.async_get_device(identifiers, set()) + device = device_registry.async_get_device(identifiers=identifiers) if device is None: return diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 776ad116f4ae76..e190b25eded2b1 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -291,7 +291,7 @@ def purge_device_registry( # Find device that references the host. references_host = set() - host_device = device_registry.async_get_device({(DOMAIN, entry_id)}) + host_device = device_registry.async_get_device(identifiers={(DOMAIN, entry_id)}) if host_device is not None: references_host.add(host_device.id) @@ -299,7 +299,9 @@ def purge_device_registry( references_entry_data = set() for device_data in imported_entry_data[CONF_DEVICES]: device_unique_id = generate_unique_id(entry_id, device_data[CONF_ADDRESS]) - device = device_registry.async_get_device({(DOMAIN, device_unique_id)}) + device = device_registry.async_get_device( + identifiers={(DOMAIN, device_unique_id)} + ) if device is not None: references_entry_data.add(device.id) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 6d20f29905d243..da2c03745fad46 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -142,7 +142,7 @@ def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, Any] | None: return None sensor_id = unique_id.split("_")[1] new_unique_id = f"occupancygroup_{bridge_unique_id}_{sensor_id}" - if dev_entry := dev_reg.async_get_device({(DOMAIN, unique_id)}): + if dev_entry := dev_reg.async_get_device(identifiers={(DOMAIN, unique_id)}): dev_reg.async_update_device( dev_entry.id, new_identifiers={(DOMAIN, new_unique_id)} ) diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index 8e76706b7fdb8b..52b8e905b4b6ff 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -80,7 +80,7 @@ def endpoint_removed_callback(event: EventType, data: dict[str, int]) -> None: node.endpoints[data["endpoint_id"]], ) identifier = (DOMAIN, f"{ID_TYPE_DEVICE_ID}_{node_device_id}") - if device := device_registry.async_get_device({identifier}): + if device := device_registry.async_get_device(identifiers={identifier}): device_registry.async_remove_device(device.id) def node_removed_callback(event: EventType, node_id: int) -> None: diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 5f2a0b0bffd6c1..e85073061c27d8 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -152,7 +152,9 @@ async def async_handle_event(self, event_message: EventMessage) -> None: return _LOGGER.debug("Event Update %s", events.keys()) device_registry = dr.async_get(self._hass) - device_entry = device_registry.async_get_device({(DOMAIN, device_id)}) + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, device_id)} + ) if not device_entry: return for api_event_type, image_event in events.items(): diff --git a/homeassistant/components/nest/device_info.py b/homeassistant/components/nest/device_info.py index e269b76fcc4be5..891365655def66 100644 --- a/homeassistant/components/nest/device_info.py +++ b/homeassistant/components/nest/device_info.py @@ -100,6 +100,8 @@ def async_nest_devices_by_device_id(hass: HomeAssistant) -> Mapping[str, Device] device_registry = dr.async_get(hass) devices = {} for nest_device_id, device in async_nest_devices(hass).items(): - if device_entry := device_registry.async_get_device({(DOMAIN, nest_device_id)}): + if device_entry := device_registry.async_get_device( + identifiers={(DOMAIN, nest_device_id)} + ): devices[device_entry.id] = device return devices diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index d9478a99316ce9..ba2faaeaae5ce0 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -244,7 +244,7 @@ async def _get_devices(self) -> Mapping[str, str]: devices = {} for device in device_manager.devices.values(): if device_entry := device_registry.async_get_device( - {(DOMAIN, device.name)} + identifiers={(DOMAIN, device.name)} ): devices[device.name] = device_entry.id return devices diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index 12798c164f834e..ff6783ecaa3e5d 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -70,7 +70,7 @@ async def async_added_to_hass(self) -> None: await self.data_handler.unsubscribe(signal_name, None) registry = dr.async_get(self.hass) - if device := registry.async_get_device({(DOMAIN, self._id)}): + if device := registry.async_get_device(identifiers={(DOMAIN, self._id)}): self.hass.data[DOMAIN][DATA_DEVICE_IDS][self._id] = device.id self.async_update_callback() diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index ad228f08a4b178..258f14056ca6bb 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -339,9 +339,13 @@ def _async_update_bridge_id(self) -> None: self._bridge_id = sensor.bridge.id device_registry = dr.async_get(self.hass) - this_device = device_registry.async_get_device({(DOMAIN, sensor.hardware_id)}) + this_device = device_registry.async_get_device( + identifiers={(DOMAIN, sensor.hardware_id)} + ) bridge = self.coordinator.data.bridges[self._bridge_id] - bridge_device = device_registry.async_get_device({(DOMAIN, bridge.hardware_id)}) + bridge_device = device_registry.async_get_device( + identifiers={(DOMAIN, bridge.hardware_id)} + ) if not bridge_device or not this_device: return diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index c1df94f5f8383b..842fe4298cfc53 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -171,7 +171,7 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes registry = dr.async_get(self.hass) if not ( device := registry.async_get_device( - identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) ): return self.async_abort(reason="no_devices_found") diff --git a/homeassistant/components/overkiz/coordinator.py b/homeassistant/components/overkiz/coordinator.py index b27051b1492d2d..7c9cab5f181de4 100644 --- a/homeassistant/components/overkiz/coordinator.py +++ b/homeassistant/components/overkiz/coordinator.py @@ -178,7 +178,9 @@ async def on_device_removed( base_device_url = event.device_url.split("#")[0] registry = dr.async_get(coordinator.hass) - if registered_device := registry.async_get_device({(DOMAIN, base_device_url)}): + if registered_device := registry.async_get_device( + identifiers={(DOMAIN, base_device_url)} + ): registry.async_remove_device(registered_device.id) if event.device_url: diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index 2e345905d50284..babdbc573bd14b 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -127,7 +127,7 @@ def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) def update_device_identifiers(hass: HomeAssistant, entry: ConfigEntry): """Update device identifiers to new identifiers.""" device_registry = async_get(hass) - device_entry = device_registry.async_get_device({(DOMAIN, DOMAIN)}) + device_entry = device_registry.async_get_device(identifiers={(DOMAIN, DOMAIN)}) if device_entry and entry.entry_id in device_entry.config_entries: new_identifiers = {(DOMAIN, entry.entry_id)} _LOGGER.debug( diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 69959453a78403..8f08aab8d3072f 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -143,7 +143,6 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b device_entry = None if entry.unique_id is not None: device_entry = dev_reg.async_get_device( - identifiers=set(), connections={(CONNECTION_NETWORK_MAC, format_mac(entry.unique_id))}, ) # https://github.com/home-assistant/core/pull/48076 @@ -227,7 +226,6 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo device_entry = None if entry.unique_id is not None: device_entry = dev_reg.async_get_device( - identifiers=set(), connections={(CONNECTION_NETWORK_MAC, format_mac(entry.unique_id))}, ) # https://github.com/home-assistant/core/pull/48076 diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 17fc6f3cc4de4e..dec1b35d34678b 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -268,7 +268,7 @@ def _async_register_base_station( # Check for an old system ID format and remove it: if old_base_station := device_registry.async_get_device( - {(DOMAIN, system.system_id)} # type: ignore[arg-type] + identifiers={(DOMAIN, system.system_id)} # type: ignore[arg-type] ): # Update the new base station with any properties the user might have configured # on the old base station: diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index 2123ee74f1b656..7d4331f0d408eb 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -119,7 +119,9 @@ async def _remove_device( device_registry: DeviceRegistry, ) -> None: """Remove a discovered Tasmota device.""" - device = device_registry.async_get_device(set(), {(CONNECTION_NETWORK_MAC, mac)}) + device = device_registry.async_get_device( + connections={(CONNECTION_NETWORK_MAC, mac)} + ) if device is None or config_entry.entry_id not in device.config_entries: return diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py index 49caf30b010ac1..f01cdddb1db4b8 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -223,8 +223,7 @@ async def discovery_update(trigger_config: TasmotaTriggerConfig) -> None: device_registry = dr.async_get(hass) device = device_registry.async_get_device( - set(), - {(CONNECTION_NETWORK_MAC, tasmota_trigger.cfg.mac)}, + connections={(CONNECTION_NETWORK_MAC, tasmota_trigger.cfg.mac)}, ) if device is None: diff --git a/homeassistant/components/tasmota/discovery.py b/homeassistant/components/tasmota/discovery.py index b490b4c724c0cf..70cedd9dd3d242 100644 --- a/homeassistant/components/tasmota/discovery.py +++ b/homeassistant/components/tasmota/discovery.py @@ -302,7 +302,7 @@ async def async_sensors_discovered( device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) device = device_registry.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) if device is None: diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 242c2179a05cc8..996490282d5256 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -289,7 +289,9 @@ async def async_setup_entry( ) # migrate to new device ids - device_entry = device_registry.async_get_device({(TIBBER_DOMAIN, old_id)}) + device_entry = device_registry.async_get_device( + identifiers={(TIBBER_DOMAIN, old_id)} + ) if device_entry and entry.entry_id in device_entry.config_entries: device_registry.async_update_device( device_entry.id, new_identifiers={(TIBBER_DOMAIN, home.home_id)} diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 88c500f18fd006..3e4410fa41ad1e 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -178,7 +178,7 @@ def _async_add_device(self, device: ProtectAdoptableDeviceModel) -> None: def _async_remove_device(self, device: ProtectAdoptableDeviceModel) -> None: registry = dr.async_get(self._hass) device_entry = registry.async_get_device( - identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, device.mac)} + connections={(dr.CONNECTION_NETWORK_MAC, device.mac)} ) if device_entry: _LOGGER.debug("Device removed: %s", device.id) diff --git a/homeassistant/components/uptimerobot/__init__.py b/homeassistant/components/uptimerobot/__init__.py index 359e4c6831a9d0..3cb119837d72bd 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -100,7 +100,7 @@ async def _async_update_data(self) -> list[UptimeRobotMonitor]: if stale_monitors := current_monitors - new_monitors: for monitor_id in stale_monitors: if device := self._device_registry.async_get_device( - {(DOMAIN, monitor_id)} + identifiers={(DOMAIN, monitor_id)} ): self._device_registry.async_remove_device(device.id) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 8c1dd9b2197fa2..7ff351893b1bae 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -254,7 +254,7 @@ async def setup(self, driver: Driver) -> None: self.dev_reg, self.config_entry.entry_id ) known_devices = [ - self.dev_reg.async_get_device({get_device_id(driver, node)}) + self.dev_reg.async_get_device(identifiers={get_device_id(driver, node)}) for node in controller.nodes.values() ] @@ -401,7 +401,7 @@ def async_on_node_removed(self, event: dict) -> None: replaced: bool = event.get("replaced", False) # grab device in device registry attached to this node dev_id = get_device_id(self.driver_events.driver, node) - device = self.dev_reg.async_get_device({dev_id}) + device = self.dev_reg.async_get_device(identifiers={dev_id}) # We assert because we know the device exists assert device if replaced: @@ -424,7 +424,7 @@ def register_node_in_dev_reg(self, node: ZwaveNode) -> dr.DeviceEntry: driver = self.driver_events.driver device_id = get_device_id(driver, node) device_id_ext = get_device_id_ext(driver, node) - device = self.dev_reg.async_get_device({device_id}) + device = self.dev_reg.async_get_device(identifiers={device_id}) via_device_id = None controller = driver.controller # Get the controller node device ID if this node is not the controller @@ -610,7 +610,7 @@ async def async_on_value_added( ) if ( not value.node.ready - or not (device := self.dev_reg.async_get_device({device_id})) + or not (device := self.dev_reg.async_get_device(identifiers={device_id})) or value.value_id in self.controller_events.discovered_value_ids[device.id] ): return @@ -632,7 +632,7 @@ def async_on_value_notification(self, notification: ValueNotification) -> None: """Relay stateless value notification events from Z-Wave nodes to hass.""" driver = self.controller_events.driver_events.driver device = self.dev_reg.async_get_device( - {get_device_id(driver, notification.node)} + identifiers={get_device_id(driver, notification.node)} ) # We assert because we know the device exists assert device @@ -671,7 +671,7 @@ def async_on_notification(self, event: dict[str, Any]) -> None: "notification" ] device = self.dev_reg.async_get_device( - {get_device_id(driver, notification.node)} + identifiers={get_device_id(driver, notification.node)} ) # We assert because we know the device exists assert device @@ -741,7 +741,9 @@ def async_on_value_updated_fire_event( driver = self.controller_events.driver_events.driver disc_info = value_updates_disc_info[value.value_id] - device = self.dev_reg.async_get_device({get_device_id(driver, value.node)}) + device = self.dev_reg.async_get_device( + identifiers={get_device_id(driver, value.node)} + ) # We assert because we know the device exists assert device diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 867405530abbb7..5fc7da68e99c4f 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -2347,7 +2347,7 @@ def _convert_node_to_device_id(node: Node) -> str: """Convert a node to a device id.""" driver = node.client.driver assert driver - device = dev_reg.async_get_device({get_device_id(driver, node)}) + device = dev_reg.async_get_device(identifiers={get_device_id(driver, node)}) assert device return device.id diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index 33cb59d8505154..edc10d4a16e0e1 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -232,7 +232,7 @@ def _create_zwave_listeners() -> None: assert driver is not None # The node comes from the driver. drivers.add(driver) device_identifier = get_device_id(driver, node) - device = dev_reg.async_get_device({device_identifier}) + device = dev_reg.async_get_device(identifiers={device_identifier}) assert device # We need to store the device for the callback unsubs.append( diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index 52ecc0a7742298..c44a0c6336ae72 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -179,7 +179,7 @@ def _create_zwave_listeners() -> None: assert driver is not None # The node comes from the driver. drivers.add(driver) device_identifier = get_device_id(driver, node) - device = dev_reg.async_get_device({device_identifier}) + device = dev_reg.async_get_device(identifiers={device_identifier}) assert device value_id = get_value_id_str( node, command_class, property_, endpoint, property_key diff --git a/homeassistant/components/zwave_me/__init__.py b/homeassistant/components/zwave_me/__init__.py index 1740820d0ba52f..86cebe811809bc 100644 --- a/homeassistant/components/zwave_me/__init__.py +++ b/homeassistant/components/zwave_me/__init__.py @@ -96,7 +96,7 @@ def remove_stale_devices(self, registry: dr.DeviceRegistry): """Remove old-format devices in the registry.""" for device_id in self.device_ids: device = registry.async_get_device( - {(DOMAIN, f"{self.config.unique_id}-{device_id}")} + identifiers={(DOMAIN, f"{self.config.unique_id}-{device_id}")} ) if device is not None: registry.async_remove_device(device.id) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 7df01fc8fd2b1c..79b4eac68d55ca 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -272,13 +272,14 @@ def __delitem__(self, key: str) -> None: def get_entry( self, - identifiers: set[tuple[str, str]], + identifiers: set[tuple[str, str]] | None, connections: set[tuple[str, str]] | None, ) -> _EntryTypeT | None: """Get entry from identifiers or connections.""" - for identifier in identifiers: - if identifier in self._identifiers: - return self._identifiers[identifier] + if identifiers: + for identifier in identifiers: + if identifier in self._identifiers: + return self._identifiers[identifier] if not connections: return None for connection in _normalize_connections(connections): @@ -317,7 +318,7 @@ def async_get(self, device_id: str) -> DeviceEntry | None: @callback def async_get_device( self, - identifiers: set[tuple[str, str]], + identifiers: set[tuple[str, str]] | None = None, connections: set[tuple[str, str]] | None = None, ) -> DeviceEntry | None: """Check if device is registered.""" @@ -326,7 +327,7 @@ def async_get_device( def _async_get_deleted_device( self, identifiers: set[tuple[str, str]], - connections: set[tuple[str, str]] | None, + connections: set[tuple[str, str]], ) -> DeletedDeviceEntry | None: """Check if device is deleted.""" return self.deleted_devices.get_entry(identifiers, connections) @@ -365,7 +366,7 @@ def async_get_or_create( else: connections = _normalize_connections(connections) - device = self.async_get_device(identifiers, connections) + device = self.async_get_device(identifiers=identifiers, connections=connections) if device is None: deleted_device = self._async_get_deleted_device(identifiers, connections) @@ -388,7 +389,7 @@ def async_get_or_create( name = default_name if via_device is not None: - via = self.async_get_device({via_device}) + via = self.async_get_device(identifiers={via_device}) via_device_id: str | UndefinedType = via.id if via else UNDEFINED else: via_device_id = UNDEFINED diff --git a/tests/components/assist_pipeline/test_select.py b/tests/components/assist_pipeline/test_select.py index bb9c4d45a32abc..29e6f9a8f31d20 100644 --- a/tests/components/assist_pipeline/test_select.py +++ b/tests/components/assist_pipeline/test_select.py @@ -102,7 +102,7 @@ async def test_select_entity_registering_device( ) -> None: """Test entity registering as an assist device.""" dev_reg = dr.async_get(hass) - device = dev_reg.async_get_device({("test", "test")}) + device = dev_reg.async_get_device(identifiers={("test", "test")}) assert device is not None # Test device is registered diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py index bcbc0fc9cdee0d..b97911262ef7fb 100644 --- a/tests/components/broadlink/test_device.py +++ b/tests/components/broadlink/test_device.py @@ -260,7 +260,7 @@ async def test_device_setup_registry( assert len(device_registry.devices) == 1 device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) assert device_entry.identifiers == {(DOMAIN, device.mac)} assert device_entry.name == device.name @@ -349,7 +349,7 @@ async def test_device_update_listener( await hass.async_block_till_done() device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) assert device_entry.name == "New Name" for entry in er.async_entries_for_device(entity_registry, device_entry.id): diff --git a/tests/components/broadlink/test_remote.py b/tests/components/broadlink/test_remote.py index 00048e09577f76..5665f7529d5ea4 100644 --- a/tests/components/broadlink/test_remote.py +++ b/tests/components/broadlink/test_remote.py @@ -33,7 +33,7 @@ async def test_remote_setup_works( mock_setup = await device.setup_entry(hass) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) entries = er.async_entries_for_device(entity_registry, device_entry.id) remotes = [entry for entry in entries if entry.domain == Platform.REMOTE] @@ -58,7 +58,7 @@ async def test_remote_send_command( mock_setup = await device.setup_entry(hass) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) entries = er.async_entries_for_device(entity_registry, device_entry.id) remotes = [entry for entry in entries if entry.domain == Platform.REMOTE] @@ -87,7 +87,7 @@ async def test_remote_turn_off_turn_on( mock_setup = await device.setup_entry(hass) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) entries = er.async_entries_for_device(entity_registry, device_entry.id) remotes = [entry for entry in entries if entry.domain == Platform.REMOTE] diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py index f1802ce51aa3f3..e00350b7627c99 100644 --- a/tests/components/broadlink/test_sensors.py +++ b/tests/components/broadlink/test_sensors.py @@ -34,7 +34,7 @@ async def test_a1_sensor_setup( assert mock_api.check_sensors_raw.call_count == 1 device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) entries = er.async_entries_for_device(entity_registry, device_entry.id) sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] @@ -75,7 +75,7 @@ async def test_a1_sensor_update( mock_setup = await device.setup_entry(hass, mock_api=mock_api) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) entries = er.async_entries_for_device(entity_registry, device_entry.id) sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] @@ -121,7 +121,7 @@ async def test_rm_pro_sensor_setup( assert mock_api.check_sensors.call_count == 1 device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) entries = er.async_entries_for_device(entity_registry, device_entry.id) sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] @@ -150,7 +150,7 @@ async def test_rm_pro_sensor_update( mock_setup = await device.setup_entry(hass, mock_api=mock_api) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) entries = er.async_entries_for_device(entity_registry, device_entry.id) sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] @@ -186,7 +186,7 @@ async def test_rm_pro_filter_crazy_temperature( mock_setup = await device.setup_entry(hass, mock_api=mock_api) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) entries = er.async_entries_for_device(entity_registry, device_entry.id) sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] @@ -220,7 +220,7 @@ async def test_rm_mini3_no_sensor( assert mock_api.check_sensors.call_count <= 1 device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) entries = er.async_entries_for_device(entity_registry, device_entry.id) sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] @@ -241,7 +241,7 @@ async def test_rm4_pro_hts2_sensor_setup( assert mock_api.check_sensors.call_count == 1 device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) entries = er.async_entries_for_device(entity_registry, device_entry.id) sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] @@ -273,7 +273,7 @@ async def test_rm4_pro_hts2_sensor_update( mock_setup = await device.setup_entry(hass, mock_api=mock_api) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) entries = er.async_entries_for_device(entity_registry, device_entry.id) sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] @@ -310,7 +310,7 @@ async def test_rm4_pro_no_sensor( assert mock_api.check_sensors.call_count <= 1 device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) entries = er.async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == Platform.SENSOR} @@ -341,7 +341,7 @@ async def test_scb1e_sensor_setup( assert mock_api.get_state.call_count == 1 device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) entries = er.async_entries_for_device(entity_registry, device_entry.id) sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] @@ -392,7 +392,7 @@ async def test_scb1e_sensor_update( mock_setup = await device.setup_entry(hass, mock_api=mock_api) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) entries = er.async_entries_for_device(entity_registry, device_entry.id) sensors = [entry for entry in entries if entry.domain == Platform.SENSOR] diff --git a/tests/components/broadlink/test_switch.py b/tests/components/broadlink/test_switch.py index 35edfb977a9b8a..93bad2db29567e 100644 --- a/tests/components/broadlink/test_switch.py +++ b/tests/components/broadlink/test_switch.py @@ -22,7 +22,7 @@ async def test_switch_setup_works( mock_setup = await device.setup_entry(hass) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) entries = er.async_entries_for_device(entity_registry, device_entry.id) switches = [entry for entry in entries if entry.domain == Platform.SWITCH] @@ -46,7 +46,7 @@ async def test_switch_turn_off_turn_on( mock_setup = await device.setup_entry(hass) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) entries = er.async_entries_for_device(entity_registry, device_entry.id) switches = [entry for entry in entries if entry.domain == Platform.SWITCH] @@ -82,7 +82,7 @@ async def test_slots_switch_setup_works( mock_setup = await device.setup_entry(hass) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) entries = er.async_entries_for_device(entity_registry, device_entry.id) switches = [entry for entry in entries if entry.domain == Platform.SWITCH] @@ -107,7 +107,7 @@ async def test_slots_switch_turn_off_turn_on( mock_setup = await device.setup_entry(hass) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_setup.entry.unique_id)} + identifiers={(DOMAIN, mock_setup.entry.unique_id)} ) entries = er.async_entries_for_device(entity_registry, device_entry.id) switches = [entry for entry in entries if entry.domain == Platform.SWITCH] diff --git a/tests/components/bthome/test_device_trigger.py b/tests/components/bthome/test_device_trigger.py index 348894346bb29d..85169e80394b06 100644 --- a/tests/components/bthome/test_device_trigger.py +++ b/tests/components/bthome/test_device_trigger.py @@ -112,7 +112,7 @@ async def test_get_triggers_button(hass: HomeAssistant) -> None: assert len(events) == 1 dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device({get_device_id(mac)}) + device = dev_reg.async_get_device(identifiers={get_device_id(mac)}) assert device expected_trigger = { CONF_PLATFORM: "device", @@ -148,7 +148,7 @@ async def test_get_triggers_dimmer(hass: HomeAssistant) -> None: assert len(events) == 1 dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device({get_device_id(mac)}) + device = dev_reg.async_get_device(identifiers={get_device_id(mac)}) assert device expected_trigger = { CONF_PLATFORM: "device", @@ -243,7 +243,7 @@ async def test_if_fires_on_motion_detected(hass: HomeAssistant, calls) -> None: await hass.async_block_till_done() dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device({get_device_id(mac)}) + device = dev_reg.async_get_device(identifiers={get_device_id(mac)}) device_id = device.id assert await async_setup_component( diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index 81718fe277cb69..f8e262896916d8 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -88,7 +88,7 @@ async def test_sensors_pro( assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == data[2] assert state.state == data[1] - device = device_registry.async_get_device({(DOMAIN, "20")}) + device = device_registry.async_get_device(identifiers={(DOMAIN, "20")}) assert device assert device.manufacturer == MANUFACTURER assert device.name == "Dining Room" @@ -208,7 +208,7 @@ async def test_sensors_flex( assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == data[2] assert state.state == data[1] - device = device_registry.async_get_device({(DOMAIN, "20")}) + device = device_registry.async_get_device(identifiers={(DOMAIN, "20")}) assert device assert device.manufacturer == MANUFACTURER assert device.name == "Dining Room" diff --git a/tests/components/daikin/test_init.py b/tests/components/daikin/test_init.py index 8145a7a1e99b7a..a6a58b4fb39303 100644 --- a/tests/components/daikin/test_init.py +++ b/tests/components/daikin/test_init.py @@ -67,7 +67,7 @@ async def test_unique_id_migrate(hass: HomeAssistant, mock_daikin) -> None: assert config_entry.unique_id == HOST - assert device_registry.async_get_device({}, {(KEY_MAC, HOST)}).name is None + assert device_registry.async_get_device(connections={(KEY_MAC, HOST)}).name is None entity = entity_registry.async_get("climate.daikin_127_0_0_1") assert entity.unique_id == HOST @@ -86,7 +86,8 @@ async def test_unique_id_migrate(hass: HomeAssistant, mock_daikin) -> None: assert config_entry.unique_id == MAC assert ( - device_registry.async_get_device({}, {(KEY_MAC, MAC)}).name == "DaikinAP00000" + device_registry.async_get_device(connections={(KEY_MAC, MAC)}).name + == "DaikinAP00000" ) entity = entity_registry.async_get("climate.daikin_127_0_0_1") diff --git a/tests/components/dlink/test_init.py b/tests/components/dlink/test_init.py index c931fed78e2ab3..dbd4cef0139a7c 100644 --- a/tests/components/dlink/test_init.py +++ b/tests/components/dlink/test_init.py @@ -66,7 +66,7 @@ async def test_device_info( entry = hass.config_entries.async_entries(DOMAIN)[0] device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, entry.entry_id)}) assert device.connections == {("mac", "aa:bb:cc:dd:ee:ff")} assert device.identifiers == {(DOMAIN, entry.entry_id)} diff --git a/tests/components/dremel_3d_printer/test_init.py b/tests/components/dremel_3d_printer/test_init.py index a77c6159927b20..2740b638316f5f 100644 --- a/tests/components/dremel_3d_printer/test_init.py +++ b/tests/components/dremel_3d_printer/test_init.py @@ -80,7 +80,9 @@ async def test_device_info( await hass.config_entries.async_setup(config_entry.entry_id) assert await async_setup_component(hass, DOMAIN, {}) device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, config_entry.unique_id)}) + device = device_registry.async_get_device( + identifiers={(DOMAIN, config_entry.unique_id)} + ) assert device.manufacturer == "Dremel" assert device.model == "3D45" diff --git a/tests/components/efergy/test_init.py b/tests/components/efergy/test_init.py index 723fd0d6332e90..e82d661592350c 100644 --- a/tests/components/efergy/test_init.py +++ b/tests/components/efergy/test_init.py @@ -53,7 +53,7 @@ async def test_device_info( entry = await setup_platform(hass, aioclient_mock, SENSOR_DOMAIN) device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, entry.entry_id)}) assert device.configuration_url == "https://engage.efergy.com/user/login" assert device.connections == {("mac", "ff:ff:ff:ff:ff:ff")} diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 2216e4df737faa..171112c90975eb 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -182,7 +182,7 @@ async def test_light_device_registry( device_registry = dr.async_get(hass) device = device_registry.async_get_device( - identifiers={}, connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)} + connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)} ) assert device.sw_version == str(sw_version) assert device.model == model diff --git a/tests/components/freedompro/test_binary_sensor.py b/tests/components/freedompro/test_binary_sensor.py index 8a3605782a25a8..5efa5ca96f7edb 100644 --- a/tests/components/freedompro/test_binary_sensor.py +++ b/tests/components/freedompro/test_binary_sensor.py @@ -56,7 +56,7 @@ async def test_binary_sensor_get_state( registry = er.async_get(hass) registry_device = dr.async_get(hass) - device = registry_device.async_get_device({("freedompro", uid)}) + device = registry_device.async_get_device(identifiers={("freedompro", uid)}) assert device is not None assert device.identifiers == {("freedompro", uid)} assert device.manufacturer == "Freedompro" diff --git a/tests/components/freedompro/test_climate.py b/tests/components/freedompro/test_climate.py index ae7c39ed4baf0f..41a550b3c508d6 100644 --- a/tests/components/freedompro/test_climate.py +++ b/tests/components/freedompro/test_climate.py @@ -33,7 +33,7 @@ async def test_climate_get_state(hass: HomeAssistant, init_integration) -> None: entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - device = device_registry.async_get_device({("freedompro", uid)}) + device = device_registry.async_get_device(identifiers={("freedompro", uid)}) assert device is not None assert device.identifiers == {("freedompro", uid)} assert device.manufacturer == "Freedompro" diff --git a/tests/components/freedompro/test_cover.py b/tests/components/freedompro/test_cover.py index b29e6499fec3e6..af54b1c2793a2a 100644 --- a/tests/components/freedompro/test_cover.py +++ b/tests/components/freedompro/test_cover.py @@ -47,7 +47,7 @@ async def test_cover_get_state( registry = er.async_get(hass) registry_device = dr.async_get(hass) - device = registry_device.async_get_device({("freedompro", uid)}) + device = registry_device.async_get_device(identifiers={("freedompro", uid)}) assert device is not None assert device.identifiers == {("freedompro", uid)} assert device.manufacturer == "Freedompro" diff --git a/tests/components/freedompro/test_fan.py b/tests/components/freedompro/test_fan.py index 159b495e0f8e3f..b5acf3e496ac81 100644 --- a/tests/components/freedompro/test_fan.py +++ b/tests/components/freedompro/test_fan.py @@ -27,7 +27,7 @@ async def test_fan_get_state(hass: HomeAssistant, init_integration) -> None: registry = er.async_get(hass) registry_device = dr.async_get(hass) - device = registry_device.async_get_device({("freedompro", uid)}) + device = registry_device.async_get_device(identifiers={("freedompro", uid)}) assert device is not None assert device.identifiers == {("freedompro", uid)} assert device.manufacturer == "Freedompro" diff --git a/tests/components/freedompro/test_lock.py b/tests/components/freedompro/test_lock.py index ae208194d2a256..c9f75e6b59416a 100644 --- a/tests/components/freedompro/test_lock.py +++ b/tests/components/freedompro/test_lock.py @@ -26,7 +26,7 @@ async def test_lock_get_state(hass: HomeAssistant, init_integration) -> None: registry = er.async_get(hass) registry_device = dr.async_get(hass) - device = registry_device.async_get_device({("freedompro", uid)}) + device = registry_device.async_get_device(identifiers={("freedompro", uid)}) assert device is not None assert device.identifiers == {("freedompro", uid)} assert device.manufacturer == "Freedompro" diff --git a/tests/components/fully_kiosk/test_diagnostics.py b/tests/components/fully_kiosk/test_diagnostics.py index ebd4a028f8c059..b1b30bda669b94 100644 --- a/tests/components/fully_kiosk/test_diagnostics.py +++ b/tests/components/fully_kiosk/test_diagnostics.py @@ -24,7 +24,7 @@ async def test_diagnostics( """Test Fully Kiosk diagnostics.""" device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, "abcdef-123456")}) + device = device_registry.async_get_device(identifiers={(DOMAIN, "abcdef-123456")}) diagnostics = await get_diagnostics_for_device( hass, hass_client, init_integration, device diff --git a/tests/components/goalzero/test_init.py b/tests/components/goalzero/test_init.py index 2603f0bf93a52d..287af75c9cd8fb 100644 --- a/tests/components/goalzero/test_init.py +++ b/tests/components/goalzero/test_init.py @@ -72,7 +72,7 @@ async def test_device_info( entry = await async_init_integration(hass, aioclient_mock) device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, entry.entry_id)}) assert device.connections == {("mac", "12:34:56:78:90:12")} assert device.identifiers == {(DOMAIN, entry.entry_id)} diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index 576cf16044e685..00cc0057d7ca02 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -334,7 +334,7 @@ async def test_device_info_ismartgate( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - device = device_registry.async_get_device({(DOMAIN, "xyz")}) + device = device_registry.async_get_device(identifiers={(DOMAIN, "xyz")}) assert device assert device.manufacturer == MANUFACTURER assert device.name == "mycontroller" @@ -369,7 +369,7 @@ async def test_device_info_gogogate2( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - device = device_registry.async_get_device({(DOMAIN, "xyz")}) + device = device_registry.async_get_device(identifiers={(DOMAIN, "xyz")}) assert device assert device.manufacturer == MANUFACTURER assert device.name == "mycontroller" diff --git a/tests/components/google_mail/test_init.py b/tests/components/google_mail/test_init.py index 9580430621b589..a069ae0807b1b1 100644 --- a/tests/components/google_mail/test_init.py +++ b/tests/components/google_mail/test_init.py @@ -123,7 +123,7 @@ async def test_device_info( device_registry = dr.async_get(hass) entry = hass.config_entries.async_entries(DOMAIN)[0] - device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, entry.entry_id)}) assert device.entry_type is dr.DeviceEntryType.SERVICE assert device.identifiers == {(DOMAIN, entry.entry_id)} diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index c429c8b46a80ed..1784ba83446183 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -263,7 +263,7 @@ async def test_updates_from_players_changed_new_ids( event = asyncio.Event() # Assert device registry matches current id - assert device_registry.async_get_device({(DOMAIN, 1)}) + assert device_registry.async_get_device(identifiers={(DOMAIN, 1)}) # Assert entity registry matches current id assert ( entity_registry.async_get_entity_id(MEDIA_PLAYER_DOMAIN, DOMAIN, "1") @@ -284,7 +284,7 @@ async def set_signal(): # Assert device registry identifiers were updated assert len(device_registry.devices) == 2 - assert device_registry.async_get_device({(DOMAIN, 101)}) + assert device_registry.async_get_device(identifiers={(DOMAIN, 101)}) # Assert entity registry unique id was updated assert len(entity_registry.entities) == 2 assert ( diff --git a/tests/components/home_plus_control/test_switch.py b/tests/components/home_plus_control/test_switch.py index 9c7736e2b8eb32..ead1f83cb94ed6 100644 --- a/tests/components/home_plus_control/test_switch.py +++ b/tests/components/home_plus_control/test_switch.py @@ -55,7 +55,7 @@ def one_entity_state(hass, device_uid): entity_reg = er.async_get(hass) device_reg = dr.async_get(hass) - device_id = device_reg.async_get_device({(DOMAIN, device_uid)}).id + device_id = device_reg.async_get_device(identifiers={(DOMAIN, device_uid)}).id entity_entries = er.async_entries_for_device(entity_reg, device_id) assert len(entity_entries) == 1 diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 112c138a8438a4..109f4205901afa 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -744,7 +744,7 @@ async def test_homekit_start( assert device_registry.async_get(bridge_with_wrong_mac.id) is None device = device_registry.async_get_device( - {(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)} + identifiers={(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)} ) assert device formatted_mac = dr.format_mac(homekit.driver.state.mac) @@ -760,7 +760,7 @@ async def test_homekit_start( await homekit.async_start() device = device_registry.async_get_device( - {(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)} + identifiers={(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)} ) assert device formatted_mac = dr.format_mac(homekit.driver.state.mac) @@ -953,7 +953,7 @@ async def test_homekit_unpair( formatted_mac = dr.format_mac(state.mac) hk_bridge_dev = device_registry.async_get_device( - {}, {(dr.CONNECTION_NETWORK_MAC, formatted_mac)} + connections={(dr.CONNECTION_NETWORK_MAC, formatted_mac)} ) await hass.services.async_call( diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 19e1b738aedf67..0c27e0a3648eaf 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -325,9 +325,7 @@ async def _do_assertions(expected: DeviceTestInfo) -> dr.DeviceEntry: # we have detected broken serial numbers (and serial number is not used as an identifier). device = device_registry.async_get_device( - { - (IDENTIFIER_ACCESSORY_ID, expected.unique_id), - } + identifiers={(IDENTIFIER_ACCESSORY_ID, expected.unique_id)} ) logger.debug("Comparing device %r to %r", device, expected) diff --git a/tests/components/hue/test_device_trigger_v1.py b/tests/components/hue/test_device_trigger_v1.py index aea91c06e88d2c..3be150f02697a1 100644 --- a/tests/components/hue/test_device_trigger_v1.py +++ b/tests/components/hue/test_device_trigger_v1.py @@ -29,7 +29,7 @@ async def test_get_triggers( # Get triggers for specific tap switch hue_tap_device = device_reg.async_get_device( - {(hue.DOMAIN, "00:00:00:00:00:44:23:08")} + identifiers={(hue.DOMAIN, "00:00:00:00:00:44:23:08")} ) triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, hue_tap_device.id @@ -50,7 +50,7 @@ async def test_get_triggers( # Get triggers for specific dimmer switch hue_dimmer_device = device_reg.async_get_device( - {(hue.DOMAIN, "00:17:88:01:10:3e:3a:dc")} + identifiers={(hue.DOMAIN, "00:17:88:01:10:3e:3a:dc")} ) hue_bat_sensor = entity_registry.async_get( "sensor.hue_dimmer_switch_1_battery_level" @@ -95,7 +95,7 @@ async def test_if_fires_on_state_change( # Set an automation with a specific tap switch trigger hue_tap_device = device_reg.async_get_device( - {(hue.DOMAIN, "00:00:00:00:00:44:23:08")} + identifiers={(hue.DOMAIN, "00:00:00:00:00:44:23:08")} ) assert await async_setup_component( hass, diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index 26c323617d294c..ab400c53ee4278 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -60,7 +60,7 @@ async def test_get_triggers( # Get triggers for `Wall switch with 2 controls` hue_wall_switch_device = device_reg.async_get_device( - {(hue.DOMAIN, "3ff06175-29e8-44a8-8fe7-af591b0025da")} + identifiers={(hue.DOMAIN, "3ff06175-29e8-44a8-8fe7-af591b0025da")} ) hue_bat_sensor = entity_registry.async_get( "sensor.wall_switch_with_2_controls_battery" diff --git a/tests/components/hue/test_sensor_v1.py b/tests/components/hue/test_sensor_v1.py index f7f0818803663f..d5ac8406f24e30 100644 --- a/tests/components/hue/test_sensor_v1.py +++ b/tests/components/hue/test_sensor_v1.py @@ -460,7 +460,7 @@ async def test_hue_events(hass: HomeAssistant, mock_bridge_v1, device_reg) -> No assert len(events) == 0 hue_tap_device = device_reg.async_get_device( - {(hue.DOMAIN, "00:00:00:00:00:44:23:08")} + identifiers={(hue.DOMAIN, "00:00:00:00:00:44:23:08")} ) mock_bridge_v1.api.sensors["7"].last_event = {"type": "button"} @@ -492,7 +492,7 @@ async def test_hue_events(hass: HomeAssistant, mock_bridge_v1, device_reg) -> No } hue_dimmer_device = device_reg.async_get_device( - {(hue.DOMAIN, "00:17:88:01:10:3e:3a:dc")} + identifiers={(hue.DOMAIN, "00:17:88:01:10:3e:3a:dc")} ) new_sensor_response = dict(new_sensor_response) @@ -595,7 +595,7 @@ async def test_hue_events(hass: HomeAssistant, mock_bridge_v1, device_reg) -> No await hass.async_block_till_done() hue_aurora_device = device_reg.async_get_device( - {(hue.DOMAIN, "ff:ff:00:0f:e7:fd:bc:b7")} + identifiers={(hue.DOMAIN, "ff:ff:00:0f:e7:fd:bc:b7")} ) assert len(mock_bridge_v1.mock_requests) == 6 diff --git a/tests/components/hyperion/test_camera.py b/tests/components/hyperion/test_camera.py index f83ed9c7e78ee1..a6234f34593c9c 100644 --- a/tests/components/hyperion/test_camera.py +++ b/tests/components/hyperion/test_camera.py @@ -192,7 +192,7 @@ async def test_device_info(hass: HomeAssistant) -> None: device_id = get_hyperion_device_id(TEST_SYSINFO_ID, TEST_INSTANCE) device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, device_id)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, device_id)}) assert device assert device.config_entries == {TEST_CONFIG_ENTRY_ID} assert device.identifiers == {(DOMAIN, device_id)} diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index 667c73a20ac65c..6c4cc4e512e4cd 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -775,7 +775,7 @@ async def test_device_info(hass: HomeAssistant) -> None: device_id = get_hyperion_device_id(TEST_SYSINFO_ID, TEST_INSTANCE) device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, device_id)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, device_id)}) assert device assert device.config_entries == {TEST_CONFIG_ENTRY_ID} assert device.identifiers == {(DOMAIN, device_id)} diff --git a/tests/components/hyperion/test_switch.py b/tests/components/hyperion/test_switch.py index 49338c72c5d023..dcdd86f0902730 100644 --- a/tests/components/hyperion/test_switch.py +++ b/tests/components/hyperion/test_switch.py @@ -164,7 +164,7 @@ async def test_device_info(hass: HomeAssistant) -> None: device_identifer = get_hyperion_device_id(TEST_SYSINFO_ID, TEST_INSTANCE) device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, device_identifer)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, device_identifer)}) assert device assert device.config_entries == {TEST_CONFIG_ENTRY_ID} assert device.identifiers == {(DOMAIN, device_identifer)} diff --git a/tests/components/ibeacon/test_init.py b/tests/components/ibeacon/test_init.py index 2437c7c1351dee..2e3aafb4984ce6 100644 --- a/tests/components/ibeacon/test_init.py +++ b/tests/components/ibeacon/test_init.py @@ -49,13 +49,12 @@ async def test_device_remove_devices( device_registry = dr.async_get(hass) device_entry = device_registry.async_get_device( - { + identifiers={ ( DOMAIN, "426c7565-4368-6172-6d42-6561636f6e73_3838_4949_61DE521B-F0BF-9F44-64D4-75BBE1738105", ) }, - {}, ) assert ( await remove_device(await hass_ws_client(hass), device_entry.id, entry.entry_id) diff --git a/tests/components/lcn/conftest.py b/tests/components/lcn/conftest.py index cf52263e69d6b8..a6bcdf639509d4 100644 --- a/tests/components/lcn/conftest.py +++ b/tests/components/lcn/conftest.py @@ -128,6 +128,6 @@ def get_device(hass, entry, address): """Get LCN device for specified address.""" device_registry = dr.async_get(hass) identifiers = {(DOMAIN, generate_unique_id(entry.entry_id, address))} - device = device_registry.async_get_device(identifiers) + device = device_registry.async_get_device(identifiers=identifiers) assert device return device diff --git a/tests/components/lcn/test_device_trigger.py b/tests/components/lcn/test_device_trigger.py index 637aeec1b0b1e6..47287fbd1d2b90 100644 --- a/tests/components/lcn/test_device_trigger.py +++ b/tests/components/lcn/test_device_trigger.py @@ -55,10 +55,12 @@ async def test_get_triggers_non_module_device( not_included_types = ("transmitter", "transponder", "fingerprint", "send_keys") device_registry = dr.async_get(hass) - host_device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + host_device = device_registry.async_get_device( + identifiers={(DOMAIN, entry.entry_id)} + ) group_device = get_device(hass, entry, (0, 5, True)) resource_device = device_registry.async_get_device( - {(DOMAIN, f"{entry.entry_id}-m000007-output1")} + identifiers={(DOMAIN, f"{entry.entry_id}-m000007-output1")} ) for device in (host_device, group_device, resource_device): diff --git a/tests/components/lidarr/test_init.py b/tests/components/lidarr/test_init.py index 2a217bebd5f8dc..5d6961e57c3bf2 100644 --- a/tests/components/lidarr/test_init.py +++ b/tests/components/lidarr/test_init.py @@ -52,7 +52,7 @@ async def test_device_info( entry = hass.config_entries.async_entries(DOMAIN)[0] device_registry = dr.async_get(hass) await hass.async_block_till_done() - device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, entry.entry_id)}) assert device.configuration_url == "http://127.0.0.1:8668" assert device.identifiers == {(DOMAIN, entry.entry_id)} diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index f64af98c9b5c0b..70a5a89a3ae113 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -100,7 +100,7 @@ async def test_light_unique_id(hass: HomeAssistant) -> None: device_registry = dr.async_get(hass) device = device_registry.async_get_device( - identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, SERIAL)} + connections={(dr.CONNECTION_NETWORK_MAC, SERIAL)} ) assert device.identifiers == {(DOMAIN, SERIAL)} @@ -123,7 +123,6 @@ async def test_light_unique_id_new_firmware(hass: HomeAssistant) -> None: assert entity_registry.async_get(entity_id).unique_id == SERIAL device_registry = dr.async_get(hass) device = device_registry.async_get_device( - identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, ) assert device.identifiers == {(DOMAIN, SERIAL)} diff --git a/tests/components/matter/test_adapter.py b/tests/components/matter/test_adapter.py index 62ed847bf28f8b..8ed309f61df468 100644 --- a/tests/components/matter/test_adapter.py +++ b/tests/components/matter/test_adapter.py @@ -41,7 +41,9 @@ async def test_device_registry_single_node_device( dev_reg = dr.async_get(hass) entry = dev_reg.async_get_device( - {(DOMAIN, "deviceid_00000000000004D2-0000000000000001-MatterNodeDevice")} + identifiers={ + (DOMAIN, "deviceid_00000000000004D2-0000000000000001-MatterNodeDevice") + } ) assert entry is not None @@ -70,7 +72,9 @@ async def test_device_registry_single_node_device_alt( dev_reg = dr.async_get(hass) entry = dev_reg.async_get_device( - {(DOMAIN, "deviceid_00000000000004D2-0000000000000001-MatterNodeDevice")} + identifiers={ + (DOMAIN, "deviceid_00000000000004D2-0000000000000001-MatterNodeDevice") + } ) assert entry is not None @@ -96,7 +100,7 @@ async def test_device_registry_bridge( dev_reg = dr.async_get(hass) # Validate bridge - bridge_entry = dev_reg.async_get_device({(DOMAIN, "mock-hub-id")}) + bridge_entry = dev_reg.async_get_device(identifiers={(DOMAIN, "mock-hub-id")}) assert bridge_entry is not None assert bridge_entry.name == "My Mock Bridge" @@ -106,7 +110,9 @@ async def test_device_registry_bridge( assert bridge_entry.sw_version == "123.4.5" # Device 1 - device1_entry = dev_reg.async_get_device({(DOMAIN, "mock-id-kitchen-ceiling")}) + device1_entry = dev_reg.async_get_device( + identifiers={(DOMAIN, "mock-id-kitchen-ceiling")} + ) assert device1_entry is not None assert device1_entry.via_device_id == bridge_entry.id @@ -117,7 +123,9 @@ async def test_device_registry_bridge( assert device1_entry.sw_version == "67.8.9" # Device 2 - device2_entry = dev_reg.async_get_device({(DOMAIN, "mock-id-living-room-ceiling")}) + device2_entry = dev_reg.async_get_device( + identifiers={(DOMAIN, "mock-id-living-room-ceiling")} + ) assert device2_entry is not None assert device2_entry.via_device_id == bridge_entry.id diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index ce1dc19319aa78..4faf48e2118a62 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -857,7 +857,9 @@ async def test_webhook_handle_scan_tag( hass: HomeAssistant, create_registrations, webhook_client ) -> None: """Test that we can scan tags.""" - device = dr.async_get(hass).async_get_device({(DOMAIN, "mock-device-id")}) + device = dr.async_get(hass).async_get_device( + identifiers={(DOMAIN, "mock-device-id")} + ) assert device is not None events = async_capture_events(hass, EVENT_TAG_SCANNED) diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index 6972bee35d019b..5f5c5f7854eb13 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -159,15 +159,17 @@ async def test_setup_camera_new_data_camera_removed(hass: HomeAssistant) -> None await hass.async_block_till_done() assert hass.states.get(TEST_CAMERA_ENTITY_ID) - assert device_registry.async_get_device({TEST_CAMERA_DEVICE_IDENTIFIER}) + assert device_registry.async_get_device(identifiers={TEST_CAMERA_DEVICE_IDENTIFIER}) client.async_get_cameras = AsyncMock(return_value={KEY_CAMERAS: []}) async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() await hass.async_block_till_done() assert not hass.states.get(TEST_CAMERA_ENTITY_ID) - assert not device_registry.async_get_device({TEST_CAMERA_DEVICE_IDENTIFIER}) - assert not device_registry.async_get_device({(DOMAIN, old_device_id)}) + assert not device_registry.async_get_device( + identifiers={TEST_CAMERA_DEVICE_IDENTIFIER} + ) + assert not device_registry.async_get_device(identifiers={(DOMAIN, old_device_id)}) assert not entity_registry.async_get_entity_id( DOMAIN, "camera", old_entity_unique_id ) @@ -320,7 +322,7 @@ async def test_device_info(hass: HomeAssistant) -> None: device_identifier = get_motioneye_device_identifier(entry.entry_id, TEST_CAMERA_ID) device_registry = dr.async_get(hass) - device = device_registry.async_get_device({device_identifier}) + device = device_registry.async_get_device(identifiers={device_identifier}) assert device assert device.config_entries == {TEST_CONFIG_ENTRY_ID} assert device.identifiers == {device_identifier} diff --git a/tests/components/motioneye/test_sensor.py b/tests/components/motioneye/test_sensor.py index ea07834976b2b9..5494e69d9e9152 100644 --- a/tests/components/motioneye/test_sensor.py +++ b/tests/components/motioneye/test_sensor.py @@ -88,7 +88,7 @@ async def test_sensor_device_info(hass: HomeAssistant) -> None: ) device_registry = dr.async_get(hass) - device = device_registry.async_get_device({device_identifer}) + device = device_registry.async_get_device(identifiers={device_identifer}) assert device entity_registry = er.async_get(hass) diff --git a/tests/components/motioneye/test_switch.py b/tests/components/motioneye/test_switch.py index 03c39a4b5424e7..f0fe4f1faba95a 100644 --- a/tests/components/motioneye/test_switch.py +++ b/tests/components/motioneye/test_switch.py @@ -193,7 +193,7 @@ async def test_switch_device_info(hass: HomeAssistant) -> None: ) device_registry = dr.async_get(hass) - device = device_registry.async_get_device({device_identifer}) + device = device_registry.async_get_device(identifiers={device_identifer}) assert device entity_registry = er.async_get(hass) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index cd1cc7280c60ac..cfd714725c43e8 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -1001,7 +1001,7 @@ async def help_test_entity_device_info_with_identifier( async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}) + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None assert device.identifiers == {("mqtt", "helloworld")} assert device.manufacturer == "Whatever" @@ -1036,7 +1036,7 @@ async def help_test_entity_device_info_with_connection( await hass.async_block_till_done() device = registry.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} + connections={(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} ) assert device is not None assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} @@ -1069,14 +1069,14 @@ async def help_test_entity_device_info_remove( async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = dev_registry.async_get_device({("mqtt", "helloworld")}) + device = dev_registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None assert ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique") async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", "") await hass.async_block_till_done() - device = dev_registry.async_get_device({("mqtt", "helloworld")}) + device = dev_registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is None assert not ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique") @@ -1103,7 +1103,7 @@ async def help_test_entity_device_info_update( async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}) + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None assert device.name == "Beer" @@ -1112,7 +1112,7 @@ async def help_test_entity_device_info_update( async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}) + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None assert device.name == "Milk" @@ -1232,7 +1232,7 @@ async def help_test_entity_debug_info( async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}) + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None debug_info_data = debug_info.info_for_device(hass, device.id) @@ -1272,7 +1272,7 @@ async def help_test_entity_debug_info_max_messages( async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}) + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None debug_info_data = debug_info.info_for_device(hass, device.id) @@ -1352,7 +1352,7 @@ async def help_test_entity_debug_info_message( async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}) + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None debug_info_data = debug_info.info_for_device(hass, device.id) @@ -1443,7 +1443,7 @@ async def help_test_entity_debug_info_remove( async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}) + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None debug_info_data = debug_info.info_for_device(hass, device.id) @@ -1493,7 +1493,7 @@ async def help_test_entity_debug_info_update_entity_id( async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = device_registry.async_get_device({("mqtt", "helloworld")}) + device = device_registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None debug_info_data = debug_info.info_for_device(hass, device.id) @@ -1555,7 +1555,7 @@ async def help_test_entity_disabled_by_default( await hass.async_block_till_done() entity_id = ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique1") assert entity_id is not None and hass.states.get(entity_id) is None - assert dev_registry.async_get_device({("mqtt", "helloworld")}) + assert dev_registry.async_get_device(identifiers={("mqtt", "helloworld")}) # Discover an enabled entity, tied to the same device config["enabled_by_default"] = True @@ -1571,7 +1571,7 @@ async def help_test_entity_disabled_by_default( await hass.async_block_till_done() assert not ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique1") assert not ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique2") - assert not dev_registry.async_get_device({("mqtt", "helloworld")}) + assert not dev_registry.async_get_device(identifiers={("mqtt", "helloworld")}) async def help_test_entity_category( diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 3793902258d618..ddce53bfca0c90 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -249,7 +249,7 @@ async def test_cleanup_device_tracker( await hass.async_block_till_done() # Verify device and registry entries are created - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is not None entity_entry = entity_registry.async_get("device_tracker.mqtt_unique") assert entity_entry is not None @@ -273,7 +273,7 @@ async def test_cleanup_device_tracker( await hass.async_block_till_done() # Verify device and registry entries are cleared - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is None entity_entry = entity_registry.async_get("device_tracker.mqtt_unique") assert entity_entry is None diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 9f3b7565332c41..485c2774f7b4d9 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -64,7 +64,7 @@ async def test_get_triggers( async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data1) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) expected_triggers = [ { "platform": "device", @@ -98,7 +98,7 @@ async def test_get_unknown_triggers( async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data1) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -145,7 +145,7 @@ async def test_get_non_existing_triggers( async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data1) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id ) @@ -171,7 +171,7 @@ async def test_discover_bad_triggers( ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data0) await hass.async_block_till_done() - assert device_registry.async_get_device({("mqtt", "0AFFD2")}) is None + assert device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) is None # Test sending correct data data1 = ( @@ -185,7 +185,7 @@ async def test_discover_bad_triggers( async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data1) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) expected_triggers = [ { "platform": "device", @@ -235,7 +235,7 @@ async def test_update_remove_triggers( async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data1) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) expected_triggers1 = [ { "platform": "device", @@ -268,7 +268,7 @@ async def test_update_remove_triggers( async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", "") await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is None @@ -299,7 +299,7 @@ async def test_if_fires_on_mqtt_message( async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", data2) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -380,7 +380,7 @@ async def test_if_fires_on_mqtt_message_template( async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", data2) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -463,7 +463,7 @@ async def test_if_fires_on_mqtt_message_late_discover( ) async_fire_mqtt_message(hass, "homeassistant/sensor/bla0/config", data0) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -543,7 +543,7 @@ async def test_if_fires_on_mqtt_message_after_update( ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -615,7 +615,7 @@ async def test_no_resubscribe_same_topic( ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -663,7 +663,7 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -735,7 +735,7 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -801,7 +801,7 @@ async def test_attach_remove( ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) calls = [] @@ -864,7 +864,7 @@ async def test_attach_remove_late( ) async_fire_mqtt_message(hass, "homeassistant/sensor/bla0/config", data0) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) calls = [] @@ -930,7 +930,7 @@ async def test_attach_remove_late2( ) async_fire_mqtt_message(hass, "homeassistant/sensor/bla0/config", data0) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) calls = [] @@ -999,7 +999,7 @@ async def test_entity_device_info_with_connection( await hass.async_block_till_done() device = registry.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} + connections={(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} ) assert device is not None assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} @@ -1036,7 +1036,7 @@ async def test_entity_device_info_with_identifier( async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}) + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None assert device.identifiers == {("mqtt", "helloworld")} assert device.manufacturer == "Whatever" @@ -1072,7 +1072,7 @@ async def test_entity_device_info_update( async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}) + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None assert device.name == "Beer" @@ -1081,7 +1081,7 @@ async def test_entity_device_info_update( async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}) + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None assert device.name == "Milk" @@ -1110,7 +1110,9 @@ async def test_cleanup_trigger( await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is not None triggers = await async_get_device_automations( @@ -1134,7 +1136,9 @@ async def test_cleanup_trigger( await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is None # Verify retained discovery topic has been cleared @@ -1163,7 +1167,9 @@ async def test_cleanup_device( await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is not None triggers = await async_get_device_automations( @@ -1175,7 +1181,9 @@ async def test_cleanup_device( await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is None @@ -1210,7 +1218,9 @@ async def test_cleanup_device_several_triggers( await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is not None triggers = await async_get_device_automations( @@ -1224,7 +1234,9 @@ async def test_cleanup_device_several_triggers( await hass.async_block_till_done() # Verify device registry entry is not cleared - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is not None triggers = await async_get_device_automations( @@ -1237,7 +1249,9 @@ async def test_cleanup_device_several_triggers( await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is None @@ -1274,7 +1288,9 @@ async def test_cleanup_device_with_entity1( await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is not None triggers = await async_get_device_automations( @@ -1286,7 +1302,9 @@ async def test_cleanup_device_with_entity1( await hass.async_block_till_done() # Verify device registry entry is not cleared - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is not None triggers = await async_get_device_automations( @@ -1298,7 +1316,9 @@ async def test_cleanup_device_with_entity1( await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is None @@ -1335,7 +1355,9 @@ async def test_cleanup_device_with_entity2( await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is not None triggers = await async_get_device_automations( @@ -1347,7 +1369,9 @@ async def test_cleanup_device_with_entity2( await hass.async_block_till_done() # Verify device registry entry is not cleared - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is not None triggers = await async_get_device_automations( @@ -1359,7 +1383,9 @@ async def test_cleanup_device_with_entity2( await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is None @@ -1404,7 +1430,7 @@ async def test_trigger_debug_info( await hass.async_block_till_done() device = registry.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} + connections={(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} ) assert device is not None @@ -1457,7 +1483,7 @@ async def test_unload_entry( ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert await async_setup_component( hass, diff --git a/tests/components/mqtt/test_diagnostics.py b/tests/components/mqtt/test_diagnostics.py index 81a86f1c61f9f9..fb1033848743ca 100644 --- a/tests/components/mqtt/test_diagnostics.py +++ b/tests/components/mqtt/test_diagnostics.py @@ -75,7 +75,7 @@ async def test_entry_diagnostics( ) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) expected_debug_info = { "entities": [ @@ -190,7 +190,7 @@ async def test_redact_diagnostics( async_fire_mqtt_message(hass, "attributes-topic", location_data) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) expected_debug_info = { "entities": [ diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 14074ce113546a..62b87bdb791c04 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -727,7 +727,7 @@ async def test_cleanup_device( await hass.async_block_till_done() # Verify device and registry entries are created - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is not None entity_entry = entity_registry.async_get("sensor.mqtt_sensor") assert entity_entry is not None @@ -751,7 +751,7 @@ async def test_cleanup_device( await hass.async_block_till_done() # Verify device and registry entries are cleared - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is None entity_entry = entity_registry.async_get("sensor.mqtt_sensor") assert entity_entry is None @@ -786,7 +786,7 @@ async def test_cleanup_device_mqtt( await hass.async_block_till_done() # Verify device and registry entries are created - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is not None entity_entry = entity_registry.async_get("sensor.mqtt_sensor") assert entity_entry is not None @@ -799,7 +799,7 @@ async def test_cleanup_device_mqtt( await hass.async_block_till_done() # Verify device and registry entries are cleared - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is None entity_entry = entity_registry.async_get("sensor.mqtt_sensor") assert entity_entry is None @@ -866,7 +866,7 @@ async def test_cleanup_device_multiple_config_entries( # Verify device and registry entries are created device_entry = device_registry.async_get_device( - set(), {("mac", "12:34:56:AB:CD:EF")} + connections={("mac", "12:34:56:AB:CD:EF")} ) assert device_entry is not None assert device_entry.config_entries == { @@ -897,7 +897,7 @@ async def test_cleanup_device_multiple_config_entries( # Verify device is still there but entity is cleared device_entry = device_registry.async_get_device( - set(), {("mac", "12:34:56:AB:CD:EF")} + connections={("mac", "12:34:56:AB:CD:EF")} ) assert device_entry is not None entity_entry = entity_registry.async_get("sensor.mqtt_sensor") @@ -966,7 +966,7 @@ async def test_cleanup_device_multiple_config_entries_mqtt( # Verify device and registry entries are created device_entry = device_registry.async_get_device( - set(), {("mac", "12:34:56:AB:CD:EF")} + connections={("mac", "12:34:56:AB:CD:EF")} ) assert device_entry is not None assert device_entry.config_entries == { @@ -989,7 +989,7 @@ async def test_cleanup_device_multiple_config_entries_mqtt( # Verify device is still there but entity is cleared device_entry = device_registry.async_get_device( - set(), {("mac", "12:34:56:AB:CD:EF")} + connections={("mac", "12:34:56:AB:CD:EF")} ) assert device_entry is not None entity_entry = entity_registry.async_get("sensor.mqtt_sensor") @@ -1518,7 +1518,7 @@ async def test_clear_config_topic_disabled_entity( # Verify device is created device_entry = device_registry.async_get_device( - set(), {("mac", "12:34:56:AB:CD:EF")} + connections={("mac", "12:34:56:AB:CD:EF")} ) assert device_entry is not None @@ -1584,7 +1584,7 @@ async def test_clean_up_registry_monitoring( # Verify device is created device_entry = device_registry.async_get_device( - set(), {("mac", "12:34:56:AB:CD:EF")} + connections={("mac", "12:34:56:AB:CD:EF")} ) assert device_entry is not None diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 08aa53aec7a1b5..9432f23130161a 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -2604,7 +2604,7 @@ async def test_default_entry_setting_are_applied( async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is not None @@ -2757,7 +2757,7 @@ async def test_mqtt_ws_remove_discovered_device( await hass.async_block_till_done() # Verify device entry is created - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is not None client = await hass_ws_client(hass) @@ -2774,7 +2774,7 @@ async def test_mqtt_ws_remove_discovered_device( assert response["success"] # Verify device entry is cleared - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is None @@ -2809,7 +2809,7 @@ async def test_mqtt_ws_get_device_debug_info( await hass.async_block_till_done() # Verify device entry is created - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is not None client = await hass_ws_client(hass) @@ -2864,7 +2864,7 @@ async def test_mqtt_ws_get_device_debug_info_binary( await hass.async_block_till_done() # Verify device entry is created - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is not None small_png = ( @@ -2971,7 +2971,7 @@ async def test_debug_info_multiple_devices( for dev in devices: domain = dev["domain"] id = dev["config"]["device"]["identifiers"][0] - device = registry.async_get_device({("mqtt", id)}) + device = registry.async_get_device(identifiers={("mqtt", id)}) assert device is not None debug_info_data = debug_info.info_for_device(hass, device.id) @@ -3052,7 +3052,7 @@ async def test_debug_info_multiple_entities_triggers( await hass.async_block_till_done() device_id = config[0]["config"]["device"]["identifiers"][0] - device = registry.async_get_device({("mqtt", device_id)}) + device = registry.async_get_device(identifiers={("mqtt", device_id)}) assert device is not None debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"]) == 2 @@ -3132,7 +3132,7 @@ async def test_debug_info_wildcard( async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}) + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None debug_info_data = debug_info.info_for_device(hass, device.id) @@ -3180,7 +3180,7 @@ async def test_debug_info_filter_same( async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}) + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None debug_info_data = debug_info.info_for_device(hass, device.id) @@ -3241,7 +3241,7 @@ async def test_debug_info_same_topic( async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}) + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None debug_info_data = debug_info.info_for_device(hass, device.id) @@ -3294,7 +3294,7 @@ async def test_debug_info_qos_retain( async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}) + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None debug_info_data = debug_info.info_for_device(hass, device.id) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 8f2aa754bacf31..d5483cf3a748eb 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -1142,7 +1142,7 @@ async def test_entity_device_info_with_hub( async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}) + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None assert device.via_device_id == hub.id diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index c18e24d1a701b5..55eac636edb40b 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -75,13 +75,13 @@ async def test_discover_bad_tag( data0 = '{ "device":{"identifiers":["0AFFD2"]}, "topics": "foobar/tag_scanned" }' async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data0) await hass.async_block_till_done() - assert device_registry.async_get_device({("mqtt", "0AFFD2")}) is None + assert device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) is None # Test sending correct data async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) await hass.async_block_till_done() @@ -100,7 +100,7 @@ async def test_if_fires_on_mqtt_message_with_device( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) @@ -138,7 +138,7 @@ async def test_if_fires_on_mqtt_message_with_template( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN_JSON) @@ -180,7 +180,7 @@ async def test_if_fires_on_mqtt_message_after_update_with_device( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) @@ -275,7 +275,7 @@ async def test_if_fires_on_mqtt_message_after_update_with_template( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN_JSON) @@ -320,7 +320,7 @@ async def test_no_resubscribe_same_topic( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) await hass.async_block_till_done() - assert device_registry.async_get_device({("mqtt", "0AFFD2")}) + assert device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) call_count = mqtt_mock.async_subscribe.call_count async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -340,7 +340,7 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_with_device( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) @@ -417,7 +417,7 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) @@ -467,7 +467,7 @@ async def test_entity_device_info_with_connection( await hass.async_block_till_done() device = registry.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} + connections={(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} ) assert device is not None assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} @@ -501,7 +501,7 @@ async def test_entity_device_info_with_identifier( async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}) + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None assert device.identifiers == {("mqtt", "helloworld")} assert device.manufacturer == "Whatever" @@ -534,7 +534,7 @@ async def test_entity_device_info_update( async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}) + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None assert device.name == "Beer" @@ -543,7 +543,7 @@ async def test_entity_device_info_update( async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}) + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) assert device is not None assert device.name == "Milk" @@ -588,20 +588,24 @@ async def test_cleanup_tag( await hass.async_block_till_done() # Verify device registry entries are created - device_entry1 = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry1 = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry1 is not None assert device_entry1.config_entries == {config_entry.entry_id, mqtt_entry.entry_id} - device_entry2 = device_registry.async_get_device({("mqtt", "hejhopp")}) + device_entry2 = device_registry.async_get_device(identifiers={("mqtt", "hejhopp")}) assert device_entry2 is not None # Remove other config entry from the device device_registry.async_update_device( device_entry1.id, remove_config_entry_id=config_entry.entry_id ) - device_entry1 = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry1 = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry1 is not None assert device_entry1.config_entries == {mqtt_entry.entry_id} - device_entry2 = device_registry.async_get_device({("mqtt", "hejhopp")}) + device_entry2 = device_registry.async_get_device(identifiers={("mqtt", "hejhopp")}) assert device_entry2 is not None mqtt_mock.async_publish.assert_not_called() @@ -621,9 +625,11 @@ async def test_cleanup_tag( await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry1 = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry1 = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry1 is None - device_entry2 = device_registry.async_get_device({("mqtt", "hejhopp")}) + device_entry2 = device_registry.async_get_device(identifiers={("mqtt", "hejhopp")}) assert device_entry2 is not None # Verify retained discovery topic has been cleared @@ -649,14 +655,18 @@ async def test_cleanup_device( await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is not None async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", "") await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is None @@ -684,14 +694,18 @@ async def test_cleanup_device_several_tags( await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is not None async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", "") await hass.async_block_till_done() # Verify device registry entry is not cleared - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is not None # Fake tag scan. @@ -704,7 +718,9 @@ async def test_cleanup_device_several_tags( await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is None @@ -749,7 +765,9 @@ async def test_cleanup_device_with_entity_and_trigger_1( await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is not None triggers = await async_get_device_automations( @@ -761,7 +779,9 @@ async def test_cleanup_device_with_entity_and_trigger_1( await hass.async_block_till_done() # Verify device registry entry is not cleared - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is not None async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", "") @@ -771,7 +791,9 @@ async def test_cleanup_device_with_entity_and_trigger_1( await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is None @@ -816,7 +838,9 @@ async def test_cleanup_device_with_entity2( await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is not None triggers = await async_get_device_automations( @@ -831,14 +855,18 @@ async def test_cleanup_device_with_entity2( await hass.async_block_till_done() # Verify device registry entry is not cleared - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is not None async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", "") await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_registry.async_get_device({("mqtt", "helloworld")}) + device_entry = device_registry.async_get_device( + identifiers={("mqtt", "helloworld")} + ) assert device_entry is None @@ -899,7 +927,7 @@ async def test_unload_entry( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_registry.async_get_device({("mqtt", "0AFFD2")}) + device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) # Fake tag scan, should be processed async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py index f659568c674312..a35b10afa9c456 100644 --- a/tests/components/nest/test_device_trigger.py +++ b/tests/components/nest/test_device_trigger.py @@ -103,7 +103,7 @@ async def test_get_triggers( await setup_platform() device_registry = dr.async_get(hass) - device_entry = device_registry.async_get_device({("nest", DEVICE_ID)}) + device_entry = device_registry.async_get_device(identifiers={("nest", DEVICE_ID)}) expected_triggers = [ { @@ -198,7 +198,7 @@ async def test_triggers_for_invalid_device_id( await setup_platform() device_registry = dr.async_get(hass) - device_entry = device_registry.async_get_device({("nest", DEVICE_ID)}) + device_entry = device_registry.async_get_device(identifiers={("nest", DEVICE_ID)}) assert device_entry is not None # Create an additional device that does not exist. Fetching supported @@ -324,7 +324,7 @@ async def test_subscriber_automation( await setup_platform() device_registry = dr.async_get(hass) - device_entry = device_registry.async_get_device({("nest", DEVICE_ID)}) + device_entry = device_registry.async_get_device(identifiers={("nest", DEVICE_ID)}) assert await setup_automation(hass, device_entry.id, "camera_motion") diff --git a/tests/components/nest/test_diagnostics.py b/tests/components/nest/test_diagnostics.py index 408e4e0d96391d..191253a2a9a0bd 100644 --- a/tests/components/nest/test_diagnostics.py +++ b/tests/components/nest/test_diagnostics.py @@ -117,7 +117,7 @@ async def test_device_diagnostics( assert config_entry.state is ConfigEntryState.LOADED device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, NEST_DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, NEST_DEVICE_ID)}) assert device is not None assert ( diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 2b25694de6c28b..6c827e76163491 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -256,7 +256,7 @@ async def test_supported_device(hass: HomeAssistant, setup_platform) -> None: assert camera is not None device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME @@ -318,7 +318,7 @@ async def test_camera_event( assert camera is not None device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME @@ -448,7 +448,7 @@ async def test_event_order( assert camera is not None device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME @@ -493,7 +493,7 @@ async def test_multiple_image_events_in_session( assert camera is not None device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME @@ -607,7 +607,7 @@ async def test_multiple_clip_preview_events_in_session( assert camera is not None device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME @@ -695,7 +695,7 @@ async def test_browse_invalid_device_id( await setup_platform() device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME @@ -716,7 +716,7 @@ async def test_browse_invalid_event_id( await setup_platform() device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME @@ -739,7 +739,7 @@ async def test_resolve_missing_event_id( await setup_platform() device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME @@ -771,7 +771,7 @@ async def test_resolve_invalid_event_id( await setup_platform() device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME @@ -819,7 +819,7 @@ async def test_camera_event_clip_preview( assert camera is not None device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME @@ -916,7 +916,7 @@ async def test_event_media_render_invalid_event_id( """Test event media API called with an invalid device id.""" await setup_platform() device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME @@ -958,7 +958,7 @@ async def test_event_media_failure( assert camera is not None device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME @@ -998,7 +998,7 @@ async def test_media_permission_unauthorized( assert camera is not None device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME @@ -1034,9 +1034,9 @@ async def test_multiple_devices( await setup_platform() device_registry = dr.async_get(hass) - device1 = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device1 = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device1 - device2 = device_registry.async_get_device({(DOMAIN, device_id2)}) + device2 = device_registry.async_get_device(identifiers={(DOMAIN, device_id2)}) assert device2 # Very no events have been received yet @@ -1121,7 +1121,7 @@ async def test_media_store_persistence( await setup_platform() device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME @@ -1174,7 +1174,7 @@ async def test_media_store_persistence( await hass.async_block_till_done() device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME @@ -1233,7 +1233,7 @@ async def test_media_store_save_filesystem_error( assert camera is not None device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME @@ -1272,7 +1272,7 @@ async def test_media_store_load_filesystem_error( assert camera is not None device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME @@ -1322,7 +1322,7 @@ async def test_camera_event_media_eviction( await setup_platform() device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME @@ -1399,7 +1399,7 @@ async def test_camera_image_resize( await setup_platform() device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)}) assert device assert device.name == DEVICE_NAME diff --git a/tests/components/netatmo/test_device_trigger.py b/tests/components/netatmo/test_device_trigger.py index c7cbaf4d131f64..ebafd313ff45bf 100644 --- a/tests/components/netatmo/test_device_trigger.py +++ b/tests/components/netatmo/test_device_trigger.py @@ -161,7 +161,7 @@ async def test_if_fires_on_event( }, ) - device = device_registry.async_get_device(set(), {connection}) + device = device_registry.async_get_device(connections={connection}) assert device is not None # Fake that the entity is turning on. @@ -244,7 +244,7 @@ async def test_if_fires_on_event_legacy( }, ) - device = device_registry.async_get_device(set(), {connection}) + device = device_registry.async_get_device(connections={connection}) assert device is not None # Fake that the entity is turning on. @@ -328,7 +328,7 @@ async def test_if_fires_on_event_with_subtype( }, ) - device = device_registry.async_get_device(set(), {connection}) + device = device_registry.async_get_device(connections={connection}) assert device is not None # Fake that the entity is turning on. diff --git a/tests/components/purpleair/test_config_flow.py b/tests/components/purpleair/test_config_flow.py index 503ba23e052d36..b72ac7e3a79854 100644 --- a/tests/components/purpleair/test_config_flow.py +++ b/tests/components/purpleair/test_config_flow.py @@ -288,7 +288,9 @@ async def test_options_remove_sensor( assert result["step_id"] == "remove_sensor" device_registry = dr.async_get(hass) - device_entry = device_registry.async_get_device({(DOMAIN, str(TEST_SENSOR_INDEX1))}) + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, str(TEST_SENSOR_INDEX1))} + ) result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"sensor_device_id": device_entry.id}, diff --git a/tests/components/radarr/test_init.py b/tests/components/radarr/test_init.py index 0bd4c538cf69e0..6b602c8c4d1d15 100644 --- a/tests/components/radarr/test_init.py +++ b/tests/components/radarr/test_init.py @@ -51,7 +51,7 @@ async def test_device_info( entry = await setup_integration(hass, aioclient_mock) device_registry = dr.async_get(hass) await hass.async_block_till_done() - device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, entry.entry_id)}) assert device.configuration_url == "http://192.168.1.189:7887/test" assert device.identifiers == {(DOMAIN, entry.entry_id)} diff --git a/tests/components/rainbird/test_number.py b/tests/components/rainbird/test_number.py index 2ecdfcc537f072..1335a1595d3212 100644 --- a/tests/components/rainbird/test_number.py +++ b/tests/components/rainbird/test_number.py @@ -67,7 +67,7 @@ async def test_set_value( assert await setup_integration() device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, SERIAL_NUMBER)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, SERIAL_NUMBER)}) assert device assert device.name == "Rain Bird Controller" assert device.model == "ST8x-WiFi" diff --git a/tests/components/renault/__init__.py b/tests/components/renault/__init__.py index 8dd0a1bf15449a..8c47410ce40a46 100644 --- a/tests/components/renault/__init__.py +++ b/tests/components/renault/__init__.py @@ -37,7 +37,9 @@ def check_device_registry( ) -> None: """Ensure that the expected_device is correctly registered.""" assert len(device_registry.devices) == 1 - registry_entry = device_registry.async_get_device(expected_device[ATTR_IDENTIFIERS]) + registry_entry = device_registry.async_get_device( + identifiers=expected_device[ATTR_IDENTIFIERS] + ) assert registry_entry is not None assert registry_entry.identifiers == expected_device[ATTR_IDENTIFIERS] assert registry_entry.manufacturer == expected_device[ATTR_MANUFACTURER] diff --git a/tests/components/renault/test_diagnostics.py b/tests/components/renault/test_diagnostics.py index 31148d4551a5d7..76ea88b4b45828 100644 --- a/tests/components/renault/test_diagnostics.py +++ b/tests/components/renault/test_diagnostics.py @@ -197,7 +197,9 @@ async def test_device_diagnostics( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - device = device_registry.async_get_device({(DOMAIN, "VF1AAAAA555777999")}) + device = device_registry.async_get_device( + identifiers={(DOMAIN, "VF1AAAAA555777999")} + ) assert device is not None assert await get_diagnostics_for_device( diff --git a/tests/components/renault/test_services.py b/tests/components/renault/test_services.py index d0ba320c1c6489..58d51eca537155 100644 --- a/tests/components/renault/test_services.py +++ b/tests/components/renault/test_services.py @@ -55,7 +55,7 @@ def get_device_id(hass: HomeAssistant) -> str: """Get device_id.""" device_registry = dr.async_get(hass) identifiers = {(DOMAIN, "VF1AAAAA555777999")} - device = device_registry.async_get_device(identifiers) + device = device_registry.async_get_device(identifiers=identifiers) return device.id @@ -272,7 +272,9 @@ async def test_service_invalid_device_id2( model=extra_vehicle[ATTR_MODEL], sw_version=extra_vehicle[ATTR_SW_VERSION], ) - device_id = device_registry.async_get_device(extra_vehicle[ATTR_IDENTIFIERS]).id + device_id = device_registry.async_get_device( + identifiers=extra_vehicle[ATTR_IDENTIFIERS] + ).id data = {ATTR_VEHICLE: device_id} diff --git a/tests/components/rfxtrx/test_device_action.py b/tests/components/rfxtrx/test_device_action.py index d53ef6a7a02b8d..087a6840c5917d 100644 --- a/tests/components/rfxtrx/test_device_action.py +++ b/tests/components/rfxtrx/test_device_action.py @@ -86,7 +86,9 @@ async def test_get_actions( """Test we get the expected actions from a rfxtrx.""" await setup_entry(hass, {device.code: {}}) - device_entry = device_registry.async_get_device(device.device_identifiers, set()) + device_entry = device_registry.async_get_device( + identifiers=device.device_identifiers + ) assert device_entry # Add alternate identifiers, to make sure we can handle future formats @@ -94,7 +96,9 @@ async def test_get_actions( device_registry.async_update_device( device_entry.id, merge_identifiers={(identifiers[0], "_".join(identifiers[1:]))} ) - device_entry = device_registry.async_get_device(device.device_identifiers, set()) + device_entry = device_registry.async_get_device( + identifiers=device.device_identifiers + ) assert device_entry actions = await async_get_device_automations( @@ -142,7 +146,9 @@ async def test_action( await setup_entry(hass, {device.code: {}}) - device_entry = device_registry.async_get_device(device.device_identifiers, set()) + device_entry = device_registry.async_get_device( + identifiers=device.device_identifiers + ) assert device_entry assert await async_setup_component( @@ -181,8 +187,8 @@ async def test_invalid_action( await setup_entry(hass, {device.code: {}}) - device_identifers: Any = device.device_identifiers - device_entry = device_registry.async_get_device(device_identifers, set()) + device_identifiers: Any = device.device_identifiers + device_entry = device_registry.async_get_device(identifiers=device_identifiers) assert device_entry assert await async_setup_component( diff --git a/tests/components/rfxtrx/test_device_trigger.py b/tests/components/rfxtrx/test_device_trigger.py index 02e9ec87630897..a253810c4c82bc 100644 --- a/tests/components/rfxtrx/test_device_trigger.py +++ b/tests/components/rfxtrx/test_device_trigger.py @@ -87,7 +87,9 @@ async def test_get_triggers( """Test we get the expected triggers from a rfxtrx.""" await setup_entry(hass, {event.code: {}}) - device_entry = device_registry.async_get_device(event.device_identifiers, set()) + device_entry = device_registry.async_get_device( + identifiers=event.device_identifiers + ) assert device_entry # Add alternate identifiers, to make sure we can handle future formats @@ -95,7 +97,9 @@ async def test_get_triggers( device_registry.async_update_device( device_entry.id, merge_identifiers={(identifiers[0], "_".join(identifiers[1:]))} ) - device_entry = device_registry.async_get_device(event.device_identifiers, set()) + device_entry = device_registry.async_get_device( + identifiers=event.device_identifiers + ) assert device_entry expected_triggers = [ @@ -131,7 +135,9 @@ async def test_firing_event( await setup_entry(hass, {event.code: {"fire_event": True}}) - device_entry = device_registry.async_get_device(event.device_identifiers, set()) + device_entry = device_registry.async_get_device( + identifiers=event.device_identifiers + ) assert device_entry calls = async_mock_service(hass, "test", "automation") @@ -175,8 +181,8 @@ async def test_invalid_trigger( await setup_entry(hass, {event.code: {"fire_event": True}}) - device_identifers: Any = event.device_identifiers - device_entry = device_registry.async_get_device(device_identifers, set()) + device_identifiers: Any = event.device_identifiers + device_entry = device_registry.async_get_device(identifiers=device_identifiers) assert device_entry assert await async_setup_component( diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index 56756aa87fb38f..e49817469b4d2f 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -149,11 +149,11 @@ async def test_cloud_setup( assert registry.async_is_registered(SECOND_CLOUD_ENTITY_ID) registry = dr.async_get(hass) - device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_0")}) + device = registry.async_get_device(identifiers={(DOMAIN, TEST_SITE_UUID + "_0")}) assert device is not None assert device.manufacturer == "Risco" - device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_1")}) + device = registry.async_get_device(identifiers={(DOMAIN, TEST_SITE_UUID + "_1")}) assert device is not None assert device.manufacturer == "Risco" @@ -485,11 +485,15 @@ async def test_local_setup( assert registry.async_is_registered(SECOND_LOCAL_ENTITY_ID) registry = dr.async_get(hass) - device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_0_local")}) + device = registry.async_get_device( + identifiers={(DOMAIN, TEST_SITE_UUID + "_0_local")} + ) assert device is not None assert device.manufacturer == "Risco" - device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_1_local")}) + device = registry.async_get_device( + identifiers={(DOMAIN, TEST_SITE_UUID + "_1_local")} + ) assert device is not None assert device.manufacturer == "Risco" with patch("homeassistant.components.risco.RiscoLocal.disconnect") as mock_close: diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py index a223bcd8f74f2b..ee74dbbedc8a28 100644 --- a/tests/components/risco/test_binary_sensor.py +++ b/tests/components/risco/test_binary_sensor.py @@ -41,11 +41,15 @@ async def test_cloud_setup( assert registry.async_is_registered(SECOND_ENTITY_ID) registry = dr.async_get(hass) - device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_0")}) + device = registry.async_get_device( + identifiers={(DOMAIN, TEST_SITE_UUID + "_zone_0")} + ) assert device is not None assert device.manufacturer == "Risco" - device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_1")}) + device = registry.async_get_device( + identifiers={(DOMAIN, TEST_SITE_UUID + "_zone_1")} + ) assert device is not None assert device.manufacturer == "Risco" @@ -99,11 +103,15 @@ async def test_local_setup( assert registry.async_is_registered(SECOND_ALARMED_ENTITY_ID) registry = dr.async_get(hass) - device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_0_local")}) + device = registry.async_get_device( + identifiers={(DOMAIN, TEST_SITE_UUID + "_zone_0_local")} + ) assert device is not None assert device.manufacturer == "Risco" - device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_1_local")}) + device = registry.async_get_device( + identifiers={(DOMAIN, TEST_SITE_UUID + "_zone_1_local")} + ) assert device is not None assert device.manufacturer == "Risco" diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index 4a54b900be1894..34b49f5d581ce2 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -218,7 +218,7 @@ async def test_device_properties( ) -> None: """Test device properties.""" registry = dr.async_get(hass) - device = registry.async_get_device({(DOMAIN, "AC000Wxxxxxxxxx")}) + device = registry.async_get_device(identifiers={(DOMAIN, "AC000Wxxxxxxxxx")}) assert getattr(device, device_property) == target_value diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index b6f2159ae13edb..d6fe0bd40fc616 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -69,7 +69,7 @@ async def test_entity_and_device_attributes( entry = entity_registry.async_get("binary_sensor.motion_sensor_1_motion") assert entry assert entry.unique_id == f"{device.device_id}.{Attribute.motion}" - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + entry = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)}) assert entry assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index fe917504dcd478..ce875190efb146 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -580,7 +580,9 @@ async def test_entity_and_device_attributes(hass: HomeAssistant, thermostat) -> assert entry assert entry.unique_id == thermostat.device_id - entry = device_registry.async_get_device({(DOMAIN, thermostat.device_id)}) + entry = device_registry.async_get_device( + identifiers={(DOMAIN, thermostat.device_id)} + ) assert entry assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, thermostat.device_id)} diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index f8c21166fe1c02..bdf3cc901a7133 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -52,7 +52,7 @@ async def test_entity_and_device_attributes( assert entry assert entry.unique_id == device.device_id - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + entry = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)}) assert entry assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index aef9ce319e73a3..ccf4b50fa1bd16 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -66,7 +66,7 @@ async def test_entity_and_device_attributes( assert entry assert entry.unique_id == device.device_id - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + entry = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)}) assert entry assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index 0e01910c84a05d..d2d0a133859f35 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -128,7 +128,7 @@ async def test_entity_and_device_attributes( assert entry assert entry.unique_id == device.device_id - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + entry = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)}) assert entry assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 0d237cec132da0..58111087848a0c 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -42,7 +42,7 @@ async def test_entity_and_device_attributes( assert entry assert entry.unique_id == device.device_id - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + entry = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)}) assert entry assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index cc7b67145c1e0f..ab163360778d9d 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -110,7 +110,7 @@ async def test_entity_and_device_attributes( assert entry assert entry.unique_id == f"{device.device_id}.{Attribute.battery}" assert entry.entity_category is EntityCategory.DIAGNOSTIC - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + entry = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)}) assert entry assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} @@ -151,7 +151,7 @@ async def test_energy_sensors_for_switch_device( assert entry assert entry.unique_id == f"{device.device_id}.{Attribute.energy}" assert entry.entity_category is None - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + entry = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)}) assert entry assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} @@ -168,7 +168,7 @@ async def test_energy_sensors_for_switch_device( assert entry assert entry.unique_id == f"{device.device_id}.{Attribute.power}" assert entry.entity_category is None - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + entry = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)}) assert entry assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} @@ -213,7 +213,7 @@ async def test_power_consumption_sensor(hass: HomeAssistant, device_factory) -> entry = entity_registry.async_get("sensor.refrigerator_energy") assert entry assert entry.unique_id == f"{device.device_id}.energy_meter" - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + entry = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)}) assert entry assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} @@ -231,7 +231,7 @@ async def test_power_consumption_sensor(hass: HomeAssistant, device_factory) -> entry = entity_registry.async_get("sensor.refrigerator_power") assert entry assert entry.unique_id == f"{device.device_id}.power_meter" - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + entry = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)}) assert entry assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} @@ -263,7 +263,7 @@ async def test_power_consumption_sensor(hass: HomeAssistant, device_factory) -> entry = entity_registry.async_get("sensor.vacuum_energy") assert entry assert entry.unique_id == f"{device.device_id}.energy_meter" - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + entry = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)}) assert entry assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index f90395f0064197..437acb04f56dc6 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -41,7 +41,7 @@ async def test_entity_and_device_attributes( assert entry assert entry.unique_id == device.device_id - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) + entry = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)}) assert entry assert entry.configuration_url == "https://account.smartthings.com" assert entry.identifiers == {(DOMAIN, device.device_id)} diff --git a/tests/components/steam_online/test_init.py b/tests/components/steam_online/test_init.py index 435a5ac6f5afcd..e3f473e01c65ae 100644 --- a/tests/components/steam_online/test_init.py +++ b/tests/components/steam_online/test_init.py @@ -43,7 +43,7 @@ async def test_device_info(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) device_registry = dr.async_get(hass) await hass.async_block_till_done() - device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, entry.entry_id)}) assert device.configuration_url == "https://store.steampowered.com" assert device.entry_type == dr.DeviceEntryType.SERVICE diff --git a/tests/components/steamist/test_init.py b/tests/components/steamist/test_init.py index a40917cfc3c076..0a98f746c4c427 100644 --- a/tests/components/steamist/test_init.py +++ b/tests/components/steamist/test_init.py @@ -105,7 +105,7 @@ def found_devices(self): device_registry = dr.async_get(hass) device_entry = device_registry.async_get_device( - connections={(dr.CONNECTION_NETWORK_MAC, FORMATTED_MAC_ADDRESS)}, identifiers={} + connections={(dr.CONNECTION_NETWORK_MAC, FORMATTED_MAC_ADDRESS)} ) assert isinstance(device_entry, dr.DeviceEntry) assert device_entry.name == DEVICE_NAME diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index 4744d6c2ccff7d..703dd2a1893309 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -413,7 +413,7 @@ async def help_test_discovery_removal( # Verify device and entity registry entries are created device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, config1[CONF_MAC])} + connections={(dr.CONNECTION_NETWORK_MAC, config1[CONF_MAC])} ) assert device_entry is not None entity_entry = entity_reg.async_get(f"{domain}.{entity_id}") @@ -436,7 +436,7 @@ async def help_test_discovery_removal( # Verify entity registry entries are cleared device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, config2[CONF_MAC])} + connections={(dr.CONNECTION_NETWORK_MAC, config2[CONF_MAC])} ) assert device_entry is not None entity_entry = entity_reg.async_get(f"{domain}.{entity_id}") @@ -522,7 +522,7 @@ async def help_test_discovery_device_remove( await hass.async_block_till_done() device = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, config[CONF_MAC])} + connections={(dr.CONNECTION_NETWORK_MAC, config[CONF_MAC])} ) assert device is not None assert entity_reg.async_get_entity_id(domain, "tasmota", unique_id) @@ -531,7 +531,7 @@ async def help_test_discovery_device_remove( await hass.async_block_till_done() device = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, config[CONF_MAC])} + connections={(dr.CONNECTION_NETWORK_MAC, config[CONF_MAC])} ) assert device is None assert not entity_reg.async_get_entity_id(domain, "tasmota", unique_id) diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py index 880f4ed0e7585e..ffff4b1b8b01b2 100644 --- a/tests/components/tasmota/test_device_trigger.py +++ b/tests/components/tasmota/test_device_trigger.py @@ -49,7 +49,7 @@ async def test_get_triggers_btn( await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) expected_triggers = [ { @@ -93,7 +93,7 @@ async def test_get_triggers_swc( await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) expected_triggers = [ { @@ -129,7 +129,7 @@ async def test_get_unknown_triggers( await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert await async_setup_component( @@ -178,7 +178,7 @@ async def test_get_non_existing_triggers( await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id @@ -210,7 +210,7 @@ async def test_discover_bad_triggers( await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id @@ -246,7 +246,7 @@ def is_active(self): await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id @@ -299,7 +299,7 @@ async def test_update_remove_triggers( await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) expected_triggers1 = [ @@ -365,7 +365,7 @@ async def test_if_fires_on_mqtt_message_btn( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert await async_setup_component( @@ -437,7 +437,7 @@ async def test_if_fires_on_mqtt_message_swc( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert await async_setup_component( @@ -535,7 +535,7 @@ async def test_if_fires_on_mqtt_message_late_discover( await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert await async_setup_component( @@ -611,7 +611,7 @@ async def test_if_fires_on_mqtt_message_after_update( await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert await async_setup_component( @@ -692,7 +692,7 @@ async def test_no_resubscribe_same_topic( await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert await async_setup_component( @@ -740,7 +740,7 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert await async_setup_component( @@ -817,7 +817,7 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert await async_setup_component( @@ -876,7 +876,7 @@ async def test_attach_remove( await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) calls = [] @@ -939,7 +939,7 @@ async def test_attach_remove_late( await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) calls = [] @@ -1012,7 +1012,7 @@ async def test_attach_remove_late2( await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) calls = [] @@ -1066,7 +1066,7 @@ async def test_attach_remove_unknown1( await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) remove = await async_initialize_triggers( @@ -1119,7 +1119,7 @@ async def test_attach_unknown_remove_device_from_registry( await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) await async_initialize_triggers( @@ -1160,7 +1160,7 @@ async def test_attach_remove_config_entry( await hass.async_block_till_done() device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) calls = [] diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index 74014c9110220d..9a3f4f91ec754b 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -140,7 +140,7 @@ async def test_correct_config_discovery( # Verify device and registry entries are created device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None entity_entry = entity_reg.async_get("switch.test") @@ -174,7 +174,7 @@ async def test_device_discover( # Verify device and registry entries are created device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None assert device_entry.configuration_url == f"http://{config['ip']}/" @@ -205,7 +205,7 @@ async def test_device_discover_deprecated( # Verify device and registry entries are created device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None assert device_entry.manufacturer == "Tasmota" @@ -238,7 +238,7 @@ async def test_device_update( # Verify device entry is created device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None @@ -256,7 +256,7 @@ async def test_device_update( # Verify device entry is updated device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None assert device_entry.model == "Another model" @@ -285,7 +285,7 @@ async def test_device_remove( # Verify device entry is created device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None @@ -298,7 +298,7 @@ async def test_device_remove( # Verify device entry is removed device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is None @@ -334,7 +334,7 @@ async def test_device_remove_multiple_config_entries_1( # Verify device entry is created device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None assert device_entry.config_entries == {tasmota_entry.entry_id, mock_entry.entry_id} @@ -348,7 +348,7 @@ async def test_device_remove_multiple_config_entries_1( # Verify device entry is not removed device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None assert device_entry.config_entries == {mock_entry.entry_id} @@ -390,7 +390,7 @@ async def test_device_remove_multiple_config_entries_2( # Verify device entry is created device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None assert device_entry.config_entries == {tasmota_entry.entry_id, mock_entry.entry_id} @@ -404,7 +404,7 @@ async def test_device_remove_multiple_config_entries_2( # Verify device entry is not removed device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None assert device_entry.config_entries == {tasmota_entry.entry_id} @@ -440,7 +440,7 @@ async def test_device_remove_stale( # Verify device entry was created device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None @@ -449,7 +449,7 @@ async def test_device_remove_stale( # Verify device entry is removed device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is None @@ -475,7 +475,7 @@ async def test_device_rediscover( # Verify device entry is created device_entry1 = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry1 is not None @@ -488,7 +488,7 @@ async def test_device_rediscover( # Verify device entry is removed device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is None @@ -501,7 +501,7 @@ async def test_device_rediscover( # Verify device entry is created, and id is reused device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None assert device_entry1.id == device_entry.id @@ -602,7 +602,7 @@ async def test_same_topic( # Verify device registry entries are created for both devices for config in configs[0:2]: device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, config["mac"])} + connections={(dr.CONNECTION_NETWORK_MAC, config["mac"])} ) assert device_entry is not None assert device_entry.configuration_url == f"http://{config['ip']}/" @@ -613,11 +613,11 @@ async def test_same_topic( # Verify entities are created only for the first device device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, configs[0]["mac"])} + connections={(dr.CONNECTION_NETWORK_MAC, configs[0]["mac"])} ) assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 1 device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, configs[1]["mac"])} + connections={(dr.CONNECTION_NETWORK_MAC, configs[1]["mac"])} ) assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 0 @@ -637,7 +637,7 @@ async def test_same_topic( # Verify device registry entries was created device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, configs[2]["mac"])} + connections={(dr.CONNECTION_NETWORK_MAC, configs[2]["mac"])} ) assert device_entry is not None assert device_entry.configuration_url == f"http://{configs[2]['ip']}/" @@ -648,7 +648,7 @@ async def test_same_topic( # Verify no entities were created device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, configs[2]["mac"])} + connections={(dr.CONNECTION_NETWORK_MAC, configs[2]["mac"])} ) assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 0 @@ -667,7 +667,7 @@ async def test_same_topic( # Verify entities are created also for the third device device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, configs[2]["mac"])} + connections={(dr.CONNECTION_NETWORK_MAC, configs[2]["mac"])} ) assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 1 @@ -686,7 +686,7 @@ async def test_same_topic( # Verify entities are created also for the second device device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, configs[1]["mac"])} + connections={(dr.CONNECTION_NETWORK_MAC, configs[1]["mac"])} ) assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 1 @@ -716,7 +716,7 @@ async def test_topic_no_prefix( # Verify device registry entry is created device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, config["mac"])} + connections={(dr.CONNECTION_NETWORK_MAC, config["mac"])} ) assert device_entry is not None assert device_entry.configuration_url == f"http://{config['ip']}/" @@ -727,7 +727,7 @@ async def test_topic_no_prefix( # Verify entities are not created device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, config["mac"])} + connections={(dr.CONNECTION_NETWORK_MAC, config["mac"])} ) assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 0 @@ -747,7 +747,7 @@ async def test_topic_no_prefix( # Verify entities are created device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, config["mac"])} + connections={(dr.CONNECTION_NETWORK_MAC, config["mac"])} ) assert len(er.async_entries_for_device(entity_reg, device_entry.id, True)) == 1 diff --git a/tests/components/tasmota/test_init.py b/tests/components/tasmota/test_init.py index b19e8e5110303b..09467b893e08de 100644 --- a/tests/components/tasmota/test_init.py +++ b/tests/components/tasmota/test_init.py @@ -44,7 +44,7 @@ async def test_device_remove( # Verify device entry is created device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None @@ -53,7 +53,7 @@ async def test_device_remove( # Verify device entry is removed device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is None @@ -104,7 +104,7 @@ async def async_remove_config_entry_device(hass, config_entry, device_entry): # Verify device entry is removed device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is None @@ -135,7 +135,7 @@ async def test_device_remove_stale_tasmota_device( # Verify device entry is removed device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is None @@ -161,7 +161,7 @@ async def test_tasmota_ws_remove_discovered_device( # Verify device entry is created device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None @@ -180,6 +180,6 @@ async def test_tasmota_ws_remove_discovered_device( # Verify device entry is cleared device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is None diff --git a/tests/components/twinkly/test_light.py b/tests/components/twinkly/test_light.py index 278c2549b457e7..f66c82dc2eda34 100644 --- a/tests/components/twinkly/test_light.py +++ b/tests/components/twinkly/test_light.py @@ -342,7 +342,7 @@ async def _create_entries( entity_id = entity_registry.async_get_entity_id("light", TWINKLY_DOMAIN, client.id) entity_entry = entity_registry.async_get(entity_id) - device = device_registry.async_get_device({(TWINKLY_DOMAIN, client.id)}) + device = device_registry.async_get_device(identifiers={(TWINKLY_DOMAIN, client.id)}) assert entity_entry is not None assert device is not None diff --git a/tests/components/velbus/test_init.py b/tests/components/velbus/test_init.py index ce0a08f18ffe51..0a1a727abcf989 100644 --- a/tests/components/velbus/test_init.py +++ b/tests/components/velbus/test_init.py @@ -45,17 +45,17 @@ async def test_device_identifier_migration( sw_version="module_sw_version", ) assert device_registry.async_get_device( - original_identifiers # type: ignore[arg-type] + identifiers=original_identifiers # type: ignore[arg-type] ) - assert not device_registry.async_get_device(target_identifiers) + assert not device_registry.async_get_device(identifiers=target_identifiers) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert not device_registry.async_get_device( - original_identifiers # type: ignore[arg-type] + identifiers=original_identifiers # type: ignore[arg-type] ) - device_entry = device_registry.async_get_device(target_identifiers) + device_entry = device_registry.async_get_device(identifiers=target_identifiers) assert device_entry assert device_entry.name == "channel_name" assert device_entry.manufacturer == "Velleman" diff --git a/tests/components/voip/test_devices.py b/tests/components/voip/test_devices.py index c421a08ccf89ec..189dff498394a6 100644 --- a/tests/components/voip/test_devices.py +++ b/tests/components/voip/test_devices.py @@ -19,7 +19,9 @@ async def test_device_registry_info( voip_device = voip_devices.async_get_or_create(call_info) assert not voip_device.async_allow_call(hass) - device = device_registry.async_get_device({(DOMAIN, call_info.caller_ip)}) + device = device_registry.async_get_device( + identifiers={(DOMAIN, call_info.caller_ip)} + ) assert device is not None assert device.name == call_info.caller_ip assert device.manufacturer == "Grandstream" @@ -32,7 +34,9 @@ async def test_device_registry_info( assert not voip_device.async_allow_call(hass) - device = device_registry.async_get_device({(DOMAIN, call_info.caller_ip)}) + device = device_registry.async_get_device( + identifiers={(DOMAIN, call_info.caller_ip)} + ) assert device.sw_version == "2.0.0.0" @@ -47,7 +51,9 @@ async def test_device_registry_info_from_unknown_phone( voip_device = voip_devices.async_get_or_create(call_info) assert not voip_device.async_allow_call(hass) - device = device_registry.async_get_device({(DOMAIN, call_info.caller_ip)}) + device = device_registry.async_get_device( + identifiers={(DOMAIN, call_info.caller_ip)} + ) assert device.manufacturer is None assert device.model == "Unknown" assert device.sw_version is None diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index fec1bf7a04a852..c027b57acf83bd 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -279,7 +279,7 @@ async def test_device_info_startup_off( assert hass.states.get(ENTITY_ID).state == STATE_OFF - device = device_registry.async_get_device({(DOMAIN, entry.unique_id)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, entry.unique_id)}) assert device assert device.identifiers == {(DOMAIN, entry.unique_id)} @@ -326,7 +326,7 @@ async def test_entity_attributes( assert attrs[ATTR_MEDIA_TITLE] == "Channel Name 2" # Device Info - device = device_registry.async_get_device({(DOMAIN, entry.unique_id)}) + device = device_registry.async_get_device(identifiers={(DOMAIN, entry.unique_id)}) assert device assert device.identifiers == {(DOMAIN, entry.unique_id)} diff --git a/tests/components/xiaomi_ble/test_device_trigger.py b/tests/components/xiaomi_ble/test_device_trigger.py index 85454959cf44d4..eba850e61e9b1a 100644 --- a/tests/components/xiaomi_ble/test_device_trigger.py +++ b/tests/components/xiaomi_ble/test_device_trigger.py @@ -99,7 +99,7 @@ async def test_get_triggers( await hass.async_block_till_done() assert len(events) == 1 - device = device_registry.async_get_device({get_device_id(mac)}) + device = device_registry.async_get_device(identifiers={get_device_id(mac)}) assert device expected_trigger = { CONF_PLATFORM: "device", @@ -196,7 +196,7 @@ async def test_if_fires_on_motion_detected( # wait for the event await hass.async_block_till_done() - device = device_registry.async_get_device({get_device_id(mac)}) + device = device_registry.async_get_device(identifiers={get_device_id(mac)}) device_id = device.id assert await async_setup_component( @@ -256,7 +256,7 @@ async def test_automation_with_invalid_trigger_type( # wait for the event await hass.async_block_till_done() - device = device_registry.async_get_device({get_device_id(mac)}) + device = device_registry.async_get_device(identifiers={get_device_id(mac)}) device_id = device.id assert await async_setup_component( @@ -305,7 +305,7 @@ async def test_automation_with_invalid_trigger_event_property( # wait for the event await hass.async_block_till_done() - device = device_registry.async_get_device({get_device_id(mac)}) + device = device_registry.async_get_device(identifiers={get_device_id(mac)}) device_id = device.id assert await async_setup_component( diff --git a/tests/components/yolink/test_device_trigger.py b/tests/components/yolink/test_device_trigger.py index e0ef37c1b75bd6..0e258d0e1c7ba7 100644 --- a/tests/components/yolink/test_device_trigger.py +++ b/tests/components/yolink/test_device_trigger.py @@ -154,7 +154,7 @@ async def test_if_fires_on_event( }, ) - device = device_registry.async_get_device(set(), {connection}) + device = device_registry.async_get_device(connections={connection}) assert device is not None # Fake remote button long press. hass.bus.async_fire( diff --git a/tests/components/youtube/test_init.py b/tests/components/youtube/test_init.py index 02df1b0e32e2e2..bd3babdc383d3b 100644 --- a/tests/components/youtube/test_init.py +++ b/tests/components/youtube/test_init.py @@ -126,7 +126,7 @@ async def test_device_info( entry = hass.config_entries.async_entries(DOMAIN)[0] channel_id = entry.options[CONF_CHANNELS][0] device = device_registry.async_get_device( - {(DOMAIN, f"{entry.entry_id}_{channel_id}")} + identifiers={(DOMAIN, f"{entry.entry_id}_{channel_id}")} ) assert device.entry_type is dr.DeviceEntryType.SERVICE diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index d938512981fdcd..46cdff180e9b72 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -113,7 +113,9 @@ async def test_get_actions(hass: HomeAssistant, device_ias) -> None: ieee_address = str(device_ias[0].ieee) ha_device_registry = dr.async_get(hass) - reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}) + reg_device = ha_device_registry.async_get_device( + identifiers={(DOMAIN, ieee_address)} + ) ha_entity_registry = er.async_get(hass) siren_level_select = ha_entity_registry.async_get( "select.fakemanufacturer_fakemodel_default_siren_level" @@ -175,7 +177,7 @@ async def test_get_inovelli_actions(hass: HomeAssistant, device_inovelli) -> Non inovelli_ieee_address = str(device_inovelli[0].ieee) ha_device_registry = dr.async_get(hass) inovelli_reg_device = ha_device_registry.async_get_device( - {(DOMAIN, inovelli_ieee_address)} + identifiers={(DOMAIN, inovelli_ieee_address)} ) ha_entity_registry = er.async_get(hass) inovelli_button = ha_entity_registry.async_get("button.inovelli_vzm31_sn_identify") @@ -265,9 +267,11 @@ async def test_action(hass: HomeAssistant, device_ias, device_inovelli) -> None: inovelli_ieee_address = str(inovelli_zha_device.ieee) ha_device_registry = dr.async_get(hass) - reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}) + reg_device = ha_device_registry.async_get_device( + identifiers={(DOMAIN, ieee_address)} + ) inovelli_reg_device = ha_device_registry.async_get_device( - {(DOMAIN, inovelli_ieee_address)} + identifiers={(DOMAIN, inovelli_ieee_address)} ) cluster = inovelli_zigpy_device.endpoints[1].in_clusters[0xFC31] diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 85e012c5bfb24b..22f62cb977a871 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -105,7 +105,9 @@ async def test_triggers(hass: HomeAssistant, mock_devices) -> None: ieee_address = str(zha_device.ieee) ha_device_registry = dr.async_get(hass) - reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) + reg_device = ha_device_registry.async_get_device( + identifiers={("zha", ieee_address)} + ) triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, reg_device.id @@ -171,7 +173,9 @@ async def test_no_triggers(hass: HomeAssistant, mock_devices) -> None: ieee_address = str(zha_device.ieee) ha_device_registry = dr.async_get(hass) - reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) + reg_device = ha_device_registry.async_get_device( + identifiers={("zha", ieee_address)} + ) triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, reg_device.id @@ -203,7 +207,9 @@ async def test_if_fires_on_event(hass: HomeAssistant, mock_devices, calls) -> No ieee_address = str(zha_device.ieee) ha_device_registry = dr.async_get(hass) - reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) + reg_device = ha_device_registry.async_get_device( + identifiers={("zha", ieee_address)} + ) assert await async_setup_component( hass, @@ -312,7 +318,9 @@ async def test_exception_no_triggers( ieee_address = str(zha_device.ieee) ha_device_registry = dr.async_get(hass) - reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) + reg_device = ha_device_registry.async_get_device( + identifiers={("zha", ieee_address)} + ) await async_setup_component( hass, @@ -359,7 +367,9 @@ async def test_exception_bad_trigger( ieee_address = str(zha_device.ieee) ha_device_registry = dr.async_get(hass) - reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) + reg_device = ha_device_registry.async_get_device( + identifiers={("zha", ieee_address)} + ) await async_setup_component( hass, diff --git a/tests/components/zha/test_diagnostics.py b/tests/components/zha/test_diagnostics.py index 5ec555d88dfc93..0bb06ea723b2ae 100644 --- a/tests/components/zha/test_diagnostics.py +++ b/tests/components/zha/test_diagnostics.py @@ -94,7 +94,7 @@ async def test_diagnostics_for_device( zha_device: ZHADevice = await zha_device_joined(zigpy_device) dev_reg = async_get(hass) - device = dev_reg.async_get_device({("zha", str(zha_device.ieee))}) + device = dev_reg.async_get_device(identifiers={("zha", str(zha_device.ieee))}) assert device diagnostics_data = await get_diagnostics_for_device( hass, hass_client, config_entry, device diff --git a/tests/components/zha/test_logbook.py b/tests/components/zha/test_logbook.py index 3d20749baacfee..44495cf0e15f42 100644 --- a/tests/components/zha/test_logbook.py +++ b/tests/components/zha/test_logbook.py @@ -78,7 +78,9 @@ async def test_zha_logbook_event_device_with_triggers( ieee_address = str(zha_device.ieee) ha_device_registry = dr.async_get(hass) - reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) + reg_device = ha_device_registry.async_get_device( + identifiers={("zha", ieee_address)} + ) hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) @@ -154,7 +156,9 @@ async def test_zha_logbook_event_device_no_triggers( zigpy_device, zha_device = mock_devices ieee_address = str(zha_device.ieee) ha_device_registry = dr.async_get(hass) - reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) + reg_device = ha_device_registry.async_get_device( + identifiers={("zha", ieee_address)} + ) hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index c6a0f7a845d51e..ebdf21124357fe 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -94,7 +94,7 @@ def get_device(hass: HomeAssistant, node): """Get device ID for a node.""" dev_reg = dr.async_get(hass) device_id = get_device_id(node.client.driver, node) - return dev_reg.async_get_device({device_id}) + return dev_reg.async_get_device(identifiers={device_id}) async def test_no_driver( @@ -462,7 +462,7 @@ async def test_node_comments( ws_client = await hass_ws_client(hass) dev_reg = dr.async_get(hass) - device = dev_reg.async_get_device({(DOMAIN, "3245146787-35")}) + device = dev_reg.async_get_device(identifiers={(DOMAIN, "3245146787-35")}) assert device await ws_client.send_json( diff --git a/tests/components/zwave_js/test_device_action.py b/tests/components/zwave_js/test_device_action.py index ccb65c1d8fa216..b5d4149a5260d1 100644 --- a/tests/components/zwave_js/test_device_action.py +++ b/tests/components/zwave_js/test_device_action.py @@ -32,7 +32,7 @@ async def test_get_actions( node = lock_schlage_be469 driver = client.driver assert driver - device = device_registry.async_get_device({get_device_id(driver, node)}) + device = device_registry.async_get_device(identifiers={get_device_id(driver, node)}) assert device expected_actions = [ { @@ -94,7 +94,7 @@ async def test_get_actions( # Test that we don't return actions for a controller node device = device_registry.async_get_device( - {get_device_id(driver, client.driver.controller.nodes[1])} + identifiers={get_device_id(driver, client.driver.controller.nodes[1])} ) assert device assert ( @@ -114,7 +114,7 @@ async def test_get_actions_meter( node = aeon_smart_switch_6 driver = client.driver assert driver - device = device_registry.async_get_device({get_device_id(driver, node)}) + device = device_registry.async_get_device(identifiers={get_device_id(driver, node)}) assert device actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device.id @@ -135,7 +135,7 @@ async def test_actions( driver = client.driver assert driver device_id = get_device_id(driver, node) - device = device_registry.async_get_device({device_id}) + device = device_registry.async_get_device(identifiers={device_id}) assert device assert await async_setup_component( @@ -285,7 +285,7 @@ async def test_actions_multiple_calls( driver = client.driver assert driver device_id = get_device_id(driver, node) - device = device_registry.async_get_device({device_id}) + device = device_registry.async_get_device(identifiers={device_id}) assert device assert await async_setup_component( @@ -332,7 +332,7 @@ async def test_lock_actions( driver = client.driver assert driver device_id = get_device_id(driver, node) - device = device_registry.async_get_device({device_id}) + device = device_registry.async_get_device(identifiers={device_id}) assert device assert await async_setup_component( @@ -403,7 +403,7 @@ async def test_reset_meter_action( driver = client.driver assert driver device_id = get_device_id(driver, node) - device = device_registry.async_get_device({device_id}) + device = device_registry.async_get_device(identifiers={device_id}) assert device assert await async_setup_component( @@ -448,7 +448,7 @@ async def test_get_action_capabilities( ) -> None: """Test we get the expected action capabilities.""" device = device_registry.async_get_device( - {get_device_id(client.driver, climate_radio_thermostat_ct100_plus)} + identifiers={get_device_id(client.driver, climate_radio_thermostat_ct100_plus)} ) assert device @@ -668,7 +668,7 @@ async def test_get_action_capabilities_meter_triggers( node = aeon_smart_switch_6 driver = client.driver assert driver - device = device_registry.async_get_device({get_device_id(driver, node)}) + device = device_registry.async_get_device(identifiers={get_device_id(driver, node)}) assert device capabilities = await device_action.async_get_action_capabilities( hass, @@ -724,7 +724,7 @@ async def test_unavailable_entity_actions( node = lock_schlage_be469 driver = client.driver assert driver - device = device_registry.async_get_device({get_device_id(driver, node)}) + device = device_registry.async_get_device(identifiers={get_device_id(driver, node)}) assert device actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device.id diff --git a/tests/components/zwave_js/test_device_condition.py b/tests/components/zwave_js/test_device_condition.py index 11213d9c37543c..f7aacec36ac5d3 100644 --- a/tests/components/zwave_js/test_device_condition.py +++ b/tests/components/zwave_js/test_device_condition.py @@ -42,7 +42,7 @@ async def test_get_conditions( ) -> None: """Test we get the expected onditions from a zwave_js.""" device = device_registry.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device config_value = list(lock_schlage_be469.get_configuration_values().values())[0] @@ -82,7 +82,7 @@ async def test_get_conditions( # Test that we don't return actions for a controller node device = device_registry.async_get_device( - {get_device_id(client.driver, client.driver.controller.nodes[1])} + identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} ) assert device assert ( @@ -103,7 +103,7 @@ async def test_node_status_state( ) -> None: """Test for node_status conditions.""" device = device_registry.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -268,7 +268,7 @@ async def test_config_parameter_state( ) -> None: """Test for config_parameter conditions.""" device = device_registry.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -388,7 +388,7 @@ async def test_value_state( ) -> None: """Test for value conditions.""" device = device_registry.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -439,7 +439,7 @@ async def test_get_condition_capabilities_node_status( ) -> None: """Test we don't get capabilities from a node_status condition.""" device = device_registry.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -479,7 +479,7 @@ async def test_get_condition_capabilities_value( ) -> None: """Test we get the expected capabilities from a value condition.""" device = device_registry.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -532,7 +532,7 @@ async def test_get_condition_capabilities_config_parameter( """Test we get the expected capabilities from a config_parameter condition.""" node = climate_radio_thermostat_ct100_plus device = device_registry.async_get_device( - {get_device_id(client.driver, climate_radio_thermostat_ct100_plus)} + identifiers={get_device_id(client.driver, climate_radio_thermostat_ct100_plus)} ) assert device @@ -617,7 +617,7 @@ async def test_failure_scenarios( ) -> None: """Test failure scenarios.""" device = device_registry.async_get_device( - {get_device_id(client.driver, hank_binary_switch)} + identifiers={get_device_id(client.driver, hank_binary_switch)} ) assert device diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py index 8209564579c33f..fd091b2bfe78a7 100644 --- a/tests/components/zwave_js/test_device_trigger.py +++ b/tests/components/zwave_js/test_device_trigger.py @@ -41,7 +41,7 @@ async def test_no_controller_triggers(hass: HomeAssistant, client, integration) """Test that we do not get triggers for the controller.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, client.driver.controller.nodes[1])} + identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} ) assert device assert ( @@ -58,7 +58,7 @@ async def test_get_notification_notification_triggers( """Test we get the expected triggers from a zwave_js device with the Notification CC.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device expected_trigger = { @@ -82,7 +82,7 @@ async def test_if_notification_notification_fires( node: Node = lock_schlage_be469 dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -178,7 +178,7 @@ async def test_get_trigger_capabilities_notification_notification( """Test we get the expected capabilities from a notification.notification trigger.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device capabilities = await device_trigger.async_get_trigger_capabilities( @@ -212,7 +212,7 @@ async def test_if_entry_control_notification_fires( node: Node = lock_schlage_be469 dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -307,7 +307,7 @@ async def test_get_trigger_capabilities_entry_control_notification( """Test we get the expected capabilities from a notification.entry_control trigger.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device capabilities = await device_trigger.async_get_trigger_capabilities( @@ -338,7 +338,7 @@ async def test_get_node_status_triggers( """Test we get the expected triggers from a device with node status sensor enabled.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device ent_reg = async_get_ent_reg(hass) @@ -370,7 +370,7 @@ async def test_if_node_status_change_fires( node: Node = lock_schlage_be469 dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device ent_reg = async_get_ent_reg(hass) @@ -448,7 +448,7 @@ async def test_get_trigger_capabilities_node_status( """Test we get the expected capabilities from a node_status trigger.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device ent_reg = async_get_ent_reg(hass) @@ -506,7 +506,7 @@ async def test_get_basic_value_notification_triggers( """Test we get the expected triggers from a zwave_js device with the Basic CC.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, ge_in_wall_dimmer_switch)} + identifiers={get_device_id(client.driver, ge_in_wall_dimmer_switch)} ) assert device expected_trigger = { @@ -534,7 +534,7 @@ async def test_if_basic_value_notification_fires( node: Node = ge_in_wall_dimmer_switch dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, ge_in_wall_dimmer_switch)} + identifiers={get_device_id(client.driver, ge_in_wall_dimmer_switch)} ) assert device @@ -645,7 +645,7 @@ async def test_get_trigger_capabilities_basic_value_notification( """Test we get the expected capabilities from a value_notification.basic trigger.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, ge_in_wall_dimmer_switch)} + identifiers={get_device_id(client.driver, ge_in_wall_dimmer_switch)} ) assert device capabilities = await device_trigger.async_get_trigger_capabilities( @@ -683,7 +683,7 @@ async def test_get_central_scene_value_notification_triggers( """Test we get the expected triggers from a zwave_js device with the Central Scene CC.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, wallmote_central_scene)} + identifiers={get_device_id(client.driver, wallmote_central_scene)} ) assert device expected_trigger = { @@ -711,7 +711,7 @@ async def test_if_central_scene_value_notification_fires( node: Node = wallmote_central_scene dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, wallmote_central_scene)} + identifiers={get_device_id(client.driver, wallmote_central_scene)} ) assert device @@ -828,7 +828,7 @@ async def test_get_trigger_capabilities_central_scene_value_notification( """Test we get the expected capabilities from a value_notification.central_scene trigger.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, wallmote_central_scene)} + identifiers={get_device_id(client.driver, wallmote_central_scene)} ) assert device capabilities = await device_trigger.async_get_trigger_capabilities( @@ -865,7 +865,7 @@ async def test_get_scene_activation_value_notification_triggers( """Test we get the expected triggers from a zwave_js device with the SceneActivation CC.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, hank_binary_switch)} + identifiers={get_device_id(client.driver, hank_binary_switch)} ) assert device expected_trigger = { @@ -893,7 +893,7 @@ async def test_if_scene_activation_value_notification_fires( node: Node = hank_binary_switch dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, hank_binary_switch)} + identifiers={get_device_id(client.driver, hank_binary_switch)} ) assert device @@ -1004,7 +1004,7 @@ async def test_get_trigger_capabilities_scene_activation_value_notification( """Test we get the expected capabilities from a value_notification.scene_activation trigger.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, hank_binary_switch)} + identifiers={get_device_id(client.driver, hank_binary_switch)} ) assert device capabilities = await device_trigger.async_get_trigger_capabilities( @@ -1042,7 +1042,7 @@ async def test_get_value_updated_value_triggers( """Test we get the zwave_js.value_updated.value trigger from a zwave_js device.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device expected_trigger = { @@ -1065,7 +1065,7 @@ async def test_if_value_updated_value_fires( node: Node = lock_schlage_be469 dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -1157,7 +1157,7 @@ async def test_value_updated_value_no_driver( node: Node = lock_schlage_be469 dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device driver = client.driver @@ -1227,7 +1227,7 @@ async def test_get_trigger_capabilities_value_updated_value( """Test we get the expected capabilities from a zwave_js.value_updated.value trigger.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device capabilities = await device_trigger.async_get_trigger_capabilities( @@ -1278,7 +1278,7 @@ async def test_get_value_updated_config_parameter_triggers( """Test we get the zwave_js.value_updated.config_parameter trigger from a zwave_js device.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device expected_trigger = { @@ -1306,7 +1306,7 @@ async def test_if_value_updated_config_parameter_fires( node: Node = lock_schlage_be469 dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -1376,7 +1376,7 @@ async def test_get_trigger_capabilities_value_updated_config_parameter_range( """Test we get the expected capabilities from a range zwave_js.value_updated.config_parameter trigger.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device capabilities = await device_trigger.async_get_trigger_capabilities( @@ -1421,7 +1421,7 @@ async def test_get_trigger_capabilities_value_updated_config_parameter_enumerate """Test we get the expected capabilities from an enumerated zwave_js.value_updated.config_parameter trigger.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device capabilities = await device_trigger.async_get_trigger_capabilities( @@ -1477,7 +1477,7 @@ async def test_failure_scenarios( dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, hank_binary_switch)} + identifiers={get_device_id(client.driver, hank_binary_switch)} ) assert device diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index aa5ec77b7981c0..4454e38e0d8c85 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -58,7 +58,9 @@ async def test_device_diagnostics( ) -> None: """Test the device level diagnostics data dump.""" dev_reg = dr.async_get(hass) - device = dev_reg.async_get_device({get_device_id(client.driver, multisensor_6)}) + device = dev_reg.async_get_device( + identifiers={get_device_id(client.driver, multisensor_6)} + ) assert device # Create mock config entry for fake entity @@ -151,7 +153,9 @@ async def test_device_diagnostics_missing_primary_value( ) -> None: """Test that device diagnostics handles an entity with a missing primary value.""" dev_reg = dr.async_get(hass) - device = dev_reg.async_get_device({get_device_id(client.driver, multisensor_6)}) + device = dev_reg.async_get_device( + identifiers={get_device_id(client.driver, multisensor_6)} + ) assert device entity_id = "sensor.multisensor_6_air_temperature" @@ -240,7 +244,7 @@ def _find_ultraviolet_val(data: dict) -> dict: client.driver.controller.emit("node added", {"node": node}) await hass.async_block_till_done() dev_reg = dr.async_get(hass) - device = dev_reg.async_get_device({get_device_id(client.driver, node)}) + device = dev_reg.async_get_device(identifiers={get_device_id(client.driver, node)}) assert device diagnostics_data = await get_diagnostics_for_device( diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index a33ee75661ca8b..3ec1f113b3e086 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -976,7 +976,9 @@ async def test_removed_device( assert len(device_entries) == 2 entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id) assert len(entity_entries) == 60 - assert dev_reg.async_get_device({get_device_id(driver, old_node)}) is None + assert ( + dev_reg.async_get_device(identifiers={get_device_id(driver, old_node)}) is None + ) async def test_suggested_area(hass: HomeAssistant, client, eaton_rf9640_dimmer) -> None: diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index a8671edbe64e2d..54638358fe7d1c 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -410,7 +410,9 @@ async def test_bulk_set_config_parameters( ) -> None: """Test the bulk_set_partial_config_parameters service.""" dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device({get_device_id(client.driver, multisensor_6)}) + device = dev_reg.async_get_device( + identifiers={get_device_id(client.driver, multisensor_6)} + ) assert device # Test setting config parameter by property and property_key await hass.services.async_call( @@ -757,7 +759,7 @@ async def test_set_value( """Test set_value service.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, climate_danfoss_lc_13)} + identifiers={get_device_id(client.driver, climate_danfoss_lc_13)} ) assert device @@ -1103,11 +1105,11 @@ async def test_multicast_set_value( # Test using area ID dev_reg = async_get_dev_reg(hass) device_eurotronic = dev_reg.async_get_device( - {get_device_id(client.driver, climate_eurotronic_spirit_z)} + identifiers={get_device_id(client.driver, climate_eurotronic_spirit_z)} ) assert device_eurotronic device_danfoss = dev_reg.async_get_device( - {get_device_id(client.driver, climate_danfoss_lc_13)} + identifiers={get_device_id(client.driver, climate_danfoss_lc_13)} ) assert device_danfoss area_reg = async_get_area_reg(hass) @@ -1416,7 +1418,7 @@ async def test_ping( """Test ping service.""" dev_reg = async_get_dev_reg(hass) device_radio_thermostat = dev_reg.async_get_device( - { + identifiers={ get_device_id( client.driver, climate_radio_thermostat_ct100_plus_different_endpoints ) @@ -1424,7 +1426,7 @@ async def test_ping( ) assert device_radio_thermostat device_danfoss = dev_reg.async_get_device( - {get_device_id(client.driver, climate_danfoss_lc_13)} + identifiers={get_device_id(client.driver, climate_danfoss_lc_13)} ) assert device_danfoss @@ -1566,7 +1568,7 @@ async def test_invoke_cc_api( """Test invoke_cc_api service.""" dev_reg = async_get_dev_reg(hass) device_radio_thermostat = dev_reg.async_get_device( - { + identifiers={ get_device_id( client.driver, climate_radio_thermostat_ct100_plus_different_endpoints ) @@ -1574,7 +1576,7 @@ async def test_invoke_cc_api( ) assert device_radio_thermostat device_danfoss = dev_reg.async_get_device( - {get_device_id(client.driver, climate_danfoss_lc_13)} + identifiers={get_device_id(client.driver, climate_danfoss_lc_13)} ) assert device_danfoss diff --git a/tests/components/zwave_js/test_trigger.py b/tests/components/zwave_js/test_trigger.py index eae9d6f5416fd0..501ad13cbaa4db 100644 --- a/tests/components/zwave_js/test_trigger.py +++ b/tests/components/zwave_js/test_trigger.py @@ -35,7 +35,7 @@ async def test_zwave_js_value_updated( node: Node = lock_schlage_be469 dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -459,7 +459,7 @@ async def test_zwave_js_event( node: Node = lock_schlage_be469 dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device @@ -1013,7 +1013,7 @@ async def test_zwave_js_trigger_config_entry_unloaded( """Test zwave_js triggers bypass dynamic validation when needed.""" dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} + identifiers={get_device_id(client.driver, lock_schlage_be469)} ) assert device diff --git a/tests/components/zwave_me/test_remove_stale_devices.py b/tests/components/zwave_me/test_remove_stale_devices.py index dca28929b3ba7d..d5496255addb16 100644 --- a/tests/components/zwave_me/test_remove_stale_devices.py +++ b/tests/components/zwave_me/test_remove_stale_devices.py @@ -60,7 +60,7 @@ async def test_remove_stale_devices( assert ( bool( device_registry.async_get_device( - { + identifiers={ ( "zwave_me", f"{config_entry.unique_id}-{identifier}", diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index e183bd4c38047f..7df5859f502853 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -530,8 +530,10 @@ async def test_removing_config_entries( assert entry2.config_entries == {"123", "456"} device_registry.async_clear_config_entry("123") - entry = device_registry.async_get_device({("bridgeid", "0123")}) - entry3_removed = device_registry.async_get_device({("bridgeid", "4567")}) + entry = device_registry.async_get_device(identifiers={("bridgeid", "0123")}) + entry3_removed = device_registry.async_get_device( + identifiers={("bridgeid", "4567")} + ) assert entry.config_entries == {"456"} assert entry3_removed is None @@ -664,7 +666,7 @@ async def test_removing_area_id(device_registry: dr.DeviceRegistry) -> None: entry_w_area = device_registry.async_update_device(entry.id, area_id="12345A") device_registry.async_clear_area_id("12345A") - entry_wo_area = device_registry.async_get_device({("bridgeid", "0123")}) + entry_wo_area = device_registry.async_get_device(identifiers={("bridgeid", "0123")}) assert not entry_wo_area.area_id assert entry_w_area != entry_wo_area @@ -692,7 +694,7 @@ async def test_specifying_via_device_create(device_registry: dr.DeviceRegistry) assert light.via_device_id == via.id device_registry.async_remove_device(via.id) - light = device_registry.async_get_device({("hue", "456")}) + light = device_registry.async_get_device(identifiers={("hue", "456")}) assert light.via_device_id is None @@ -821,9 +823,9 @@ async def test_loading_saving_data( assert list(device_registry.devices) == list(registry2.devices) assert list(device_registry.deleted_devices) == list(registry2.deleted_devices) - new_via = registry2.async_get_device({("hue", "0123")}) - new_light = registry2.async_get_device({("hue", "456")}) - new_light4 = registry2.async_get_device({("hue", "abc")}) + new_via = registry2.async_get_device(identifiers={("hue", "0123")}) + new_light = registry2.async_get_device(identifiers={("hue", "456")}) + new_light4 = registry2.async_get_device(identifiers={("hue", "abc")}) assert orig_via == new_via assert orig_light == new_light @@ -839,7 +841,7 @@ async def test_loading_saving_data( assert old.entry_type is new.entry_type # Ensure a save/load cycle does not keep suggested area - new_kitchen_light = registry2.async_get_device({("hue", "999")}) + new_kitchen_light = registry2.async_get_device(identifiers={("hue", "999")}) assert orig_kitchen_light.suggested_area == "Kitchen" orig_kitchen_light_witout_suggested_area = device_registry.async_update_device( @@ -951,15 +953,19 @@ async def test_update( via_device_id="98765B", ) - assert device_registry.async_get_device({("hue", "456")}) is None - assert device_registry.async_get_device({("bla", "123")}) is None + assert device_registry.async_get_device(identifiers={("hue", "456")}) is None + assert device_registry.async_get_device(identifiers={("bla", "123")}) is None - assert device_registry.async_get_device({("hue", "654")}) == updated_entry - assert device_registry.async_get_device({("bla", "321")}) == updated_entry + assert ( + device_registry.async_get_device(identifiers={("hue", "654")}) == updated_entry + ) + assert ( + device_registry.async_get_device(identifiers={("bla", "321")}) == updated_entry + ) assert ( device_registry.async_get_device( - {}, {(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")} + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")} ) == updated_entry ) @@ -1032,7 +1038,7 @@ async def test_update_remove_config_entries( assert updated_entry.config_entries == {"456"} assert removed_entry is None - removed_entry = device_registry.async_get_device({("bridgeid", "4567")}) + removed_entry = device_registry.async_get_device(identifiers={("bridgeid", "4567")}) assert removed_entry is None @@ -1137,10 +1143,10 @@ async def test_cleanup_device_registry( dr.async_cleanup(hass, device_registry, ent_reg) - assert device_registry.async_get_device({("hue", "d1")}) is not None - assert device_registry.async_get_device({("hue", "d2")}) is not None - assert device_registry.async_get_device({("hue", "d3")}) is not None - assert device_registry.async_get_device({("something", "d4")}) is None + assert device_registry.async_get_device(identifiers={("hue", "d1")}) is not None + assert device_registry.async_get_device(identifiers={("hue", "d2")}) is not None + assert device_registry.async_get_device(identifiers={("hue", "d3")}) is not None + assert device_registry.async_get_device(identifiers={("something", "d4")}) is None async def test_cleanup_device_registry_removes_expired_orphaned_devices( diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 9673e1dc73a7fb..e07c3cb475392d 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1108,7 +1108,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): assert len(hass.states.async_entity_ids()) == 2 - device = registry.async_get_device({("hue", "1234")}) + device = registry.async_get_device(identifiers={("hue", "1234")}) assert device is not None assert device.identifiers == {("hue", "1234")} assert device.configuration_url == "http://192.168.0.100/config" @@ -1162,7 +1162,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): assert await entity_platform.async_setup_entry(config_entry) await hass.async_block_till_done() - device2 = registry.async_get_device(set(), {(dr.CONNECTION_NETWORK_MAC, "abcd")}) + device2 = registry.async_get_device( + connections={(dr.CONNECTION_NETWORK_MAC, "abcd")} + ) assert device2 is not None assert device.id == device2.id assert device2.manufacturer == "test-manufacturer" @@ -1209,7 +1211,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): assert len(hass.states.async_entity_ids()) == 1 - device = registry.async_get_device({("mqtt", "1234")}) + device = registry.async_get_device(identifiers={("mqtt", "1234")}) assert device is not None assert device.identifiers == {("mqtt", "1234")} assert device.configuration_url == "homeassistant://config/mqtt" @@ -1256,7 +1258,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): assert len(hass.states.async_entity_ids()) == 1 - device = registry.async_get_device({("mqtt", "1234")}) + device = registry.async_get_device(identifiers={("mqtt", "1234")}) assert device is not None assert device.identifiers == {("mqtt", "1234")} assert device.configuration_url is None @@ -1836,7 +1838,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): await hass.async_block_till_done() dev_reg = dr.async_get(hass) - device = dev_reg.async_get_device(set(), {(dr.CONNECTION_NETWORK_MAC, "1234")}) + device = dev_reg.async_get_device(connections={(dr.CONNECTION_NETWORK_MAC, "1234")}) assert device is not None assert device.name == expected_device_name From 3b80deb2b74d642e805f6b1abaa8c4be9253439e 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 0464/1009] 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 34eee94411470d..b43dabbba80858 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 da9624de2facf3770621676c68614876cb589d77 Mon Sep 17 00:00:00 2001 From: Oliver <10700296+ol-iver@users.noreply.github.com> Date: Thu, 13 Jul 2023 21:30:18 +0200 Subject: [PATCH 0465/1009] Update denonavr to `0.11.3` (#96467) --- homeassistant/components/denonavr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 660e4c770b0cf5..b3c36ed39d2b84 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/denonavr", "iot_class": "local_push", "loggers": ["denonavr"], - "requirements": ["denonavr==0.11.2"], + "requirements": ["denonavr==0.11.3"], "ssdp": [ { "manufacturer": "Denon", diff --git a/requirements_all.txt b/requirements_all.txt index 103bcb889ef6b7..76ab11172bb09c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -649,7 +649,7 @@ deluge-client==1.7.1 demetriek==0.4.0 # homeassistant.components.denonavr -denonavr==0.11.2 +denonavr==0.11.3 # homeassistant.components.devolo_home_control devolo-home-control-api==0.18.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 114e164481c3f5..37388bfca95429 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -526,7 +526,7 @@ deluge-client==1.7.1 demetriek==0.4.0 # homeassistant.components.denonavr -denonavr==0.11.2 +denonavr==0.11.3 # homeassistant.components.devolo_home_control devolo-home-control-api==0.18.2 From 1865cd0805f9ddac19c692af283429b2902d64f6 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 13 Jul 2023 15:30:55 -0400 Subject: [PATCH 0466/1009] Bump unifiprotect to 4.10.5 (#96486) --- homeassistant/components/unifiprotect/camera.py | 2 +- homeassistant/components/unifiprotect/data.py | 2 +- homeassistant/components/unifiprotect/entity.py | 4 ++-- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index a4da77fe50b37d..019ff7b786363c 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -115,7 +115,7 @@ async def async_setup_entry( async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: if not isinstance(device, UFPCamera): - return + return # type: ignore[unreachable] entities = _async_camera_entities(data, ufp_device=device) async_add_entities(entities) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 3e4410fa41ad1e..73d05f1be1d77f 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -227,7 +227,7 @@ def _async_process_ws_message(self, message: WSSubscriptionMessage) -> None: self._async_update_device(obj, message.changed_data) # trigger updates for camera that the event references - elif isinstance(obj, Event): + elif isinstance(obj, Event): # type: ignore[unreachable] if obj.type in SMART_EVENTS: if obj.camera is not None: if obj.end is None: diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 15bd17554ad30f..79ee483dd8d4f8 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -272,7 +272,7 @@ class ProtectNVREntity(ProtectDeviceEntity): """Base class for unifi protect entities.""" # separate subclass on purpose - device: NVR # type: ignore[assignment] + device: NVR def __init__( self, @@ -281,7 +281,7 @@ def __init__( description: EntityDescription | None = None, ) -> None: """Initialize the entity.""" - super().__init__(entry, device, description) # type: ignore[arg-type] + super().__init__(entry, device, description) @callback def _async_set_device_info(self) -> None: diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index cfa90664f36c02..95353e84754935 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -41,7 +41,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==4.10.3", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.10.5", "unifi-discovery==1.1.7"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 76ab11172bb09c..59a058352f2442 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2186,7 +2186,7 @@ pytrafikverket==0.3.3 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.10.3 +pyunifiprotect==4.10.5 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37388bfca95429..d3163bc5fd28a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1603,7 +1603,7 @@ pytrafikverket==0.3.3 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.10.3 +pyunifiprotect==4.10.5 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From bfd4446d2e72da2c7c124d501045bd4668284bcf 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 0467/1009] 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 4f3fcbf9c87601..479c84d238cfb4 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 59a058352f2442..a1d3f619fd7858 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2602,7 +2602,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 d3163bc5fd28a0..b166ed3ae481a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1902,7 +1902,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 c86b60bdf7ccb739c93951d4fbcabc4e3359824d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Jul 2023 11:42:11 -1000 Subject: [PATCH 0468/1009] Bump bluetooth-data-tools to 1.6.0 (#96461) --- 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 bed677ebd306f0..f4c690dcffc556 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.16.0", "bluetooth-auto-recovery==1.2.1", - "bluetooth-data-tools==1.3.0", + "bluetooth-data-tools==1.6.0", "dbus-fast==1.86.0" ] } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 764b12cedc2438..4f208ed011541f 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "aioesphomeapi==15.1.6", - "bluetooth-data-tools==1.3.0", + "bluetooth-data-tools==1.6.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 6eaf2885d89306..a161c3ecde1915 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.3.0", "ld2410-ble==0.1.1"] + "requirements": ["bluetooth-data-tools==1.6.0", "ld2410-ble==0.1.1"] } diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index cdc270f2e99371..51acbe8c7d9e5b 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.3.0", "led-ble==1.0.0"] + "requirements": ["bluetooth-data-tools==1.6.0", "led-ble==1.0.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8ae3ba06985ba3..6b3e1d8506df63 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.16.0 bluetooth-auto-recovery==1.2.1 -bluetooth-data-tools==1.3.0 +bluetooth-data-tools==1.6.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index a1d3f619fd7858..8742fd97c778f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -534,7 +534,7 @@ bluetooth-auto-recovery==1.2.1 # homeassistant.components.esphome # homeassistant.components.ld2410_ble # homeassistant.components.led_ble -bluetooth-data-tools==1.3.0 +bluetooth-data-tools==1.6.0 # homeassistant.components.bond bond-async==0.1.23 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b166ed3ae481a2..f9237251ebaf79 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -448,7 +448,7 @@ bluetooth-auto-recovery==1.2.1 # homeassistant.components.esphome # homeassistant.components.ld2410_ble # homeassistant.components.led_ble -bluetooth-data-tools==1.3.0 +bluetooth-data-tools==1.6.0 # homeassistant.components.bond bond-async==0.1.23 From d2991d3f5e52e075b7064c7993a12cf98b2ed902 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Jul 2023 13:20:24 -1000 Subject: [PATCH 0469/1009] Bump bond-async to 0.2.1 (#96504) --- homeassistant/components/bond/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index fc91f8eb72efa8..08e4fb007b7ee6 100644 --- a/homeassistant/components/bond/manifest.json +++ b/homeassistant/components/bond/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_push", "loggers": ["bond_async"], "quality_scale": "platinum", - "requirements": ["bond-async==0.1.23"], + "requirements": ["bond-async==0.2.1"], "zeroconf": ["_bond._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 8742fd97c778f3..aa80f7676ca52c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -537,7 +537,7 @@ bluetooth-auto-recovery==1.2.1 bluetooth-data-tools==1.6.0 # homeassistant.components.bond -bond-async==0.1.23 +bond-async==0.2.1 # homeassistant.components.bosch_shc boschshcpy==0.2.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f9237251ebaf79..0a7fe75fe45a46 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -451,7 +451,7 @@ bluetooth-auto-recovery==1.2.1 bluetooth-data-tools==1.6.0 # homeassistant.components.bond -bond-async==0.1.23 +bond-async==0.2.1 # homeassistant.components.bosch_shc boschshcpy==0.2.57 From 09237e4eff0d39ba7d1b7d8b228a6908c2317f0f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Jul 2023 13:38:15 -1000 Subject: [PATCH 0470/1009] Remove unused code in ESPHome (#96503) --- homeassistant/components/esphome/domain_data.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/esphome/domain_data.py b/homeassistant/components/esphome/domain_data.py index 2fc32129d1f998..aacda1083985f1 100644 --- a/homeassistant/components/esphome/domain_data.py +++ b/homeassistant/components/esphome/domain_data.py @@ -78,10 +78,6 @@ def pop_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: """Pop the runtime entry data instance associated with this config entry.""" return self._entry_datas.pop(entry.entry_id) - def is_entry_loaded(self, entry: ConfigEntry) -> bool: - """Check whether the given entry is loaded.""" - return entry.entry_id in self._entry_datas - def get_or_create_store( self, hass: HomeAssistant, entry: ConfigEntry ) -> ESPHomeStorage: From bbc420bc9013403fb97dee800a3b8811ef1d7750 Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 13 Jul 2023 17:52:26 -0700 Subject: [PATCH 0471/1009] Bump opower to 0.0.14 (#96506) --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 3b48e96a35194c..e7ebb7b546b0ba 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["recorder"], "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", - "requirements": ["opower==0.0.12"] + "requirements": ["opower==0.0.14"] } diff --git a/requirements_all.txt b/requirements_all.txt index aa80f7676ca52c..7497521708ae69 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1363,7 +1363,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.12 +opower==0.0.14 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a7fe75fe45a46..681c17952e4106 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1032,7 +1032,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.12 +opower==0.0.14 # homeassistant.components.oralb oralb-ble==0.17.6 From c44c7bba840951509d9726588b76b009d64cdd0c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Jul 2023 16:45:45 -1000 Subject: [PATCH 0472/1009] Simplify ESPHome bluetooth disconnected during operation wrapper (#96459) --- .../components/esphome/bluetooth/client.py | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index d452ab8764aef3..00b9883f26182f 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -62,29 +62,32 @@ def verify_connected(func: _WrapFuncType) -> _WrapFuncType: async def _async_wrap_bluetooth_connected_operation( self: ESPHomeClient, *args: Any, **kwargs: Any ) -> Any: - disconnected_event = ( - self._disconnected_event # pylint: disable=protected-access + loop = self._loop # pylint: disable=protected-access + disconnected_futures = ( + self._disconnected_futures # pylint: disable=protected-access ) - if not disconnected_event: - raise BleakError("Not connected") - action_task = asyncio.create_task(func(self, *args, **kwargs)) - disconnect_task = asyncio.create_task(disconnected_event.wait()) - await asyncio.wait( - (action_task, disconnect_task), - return_when=asyncio.FIRST_COMPLETED, - ) - if disconnect_task.done(): - action_task.cancel() - with contextlib.suppress(asyncio.CancelledError): - await action_task + disconnected_future = loop.create_future() + disconnected_futures.add(disconnected_future) + + task = asyncio.current_task(loop) + + def _on_disconnected(fut: asyncio.Future[None]) -> None: + if task and not task.done(): + task.cancel() + disconnected_future.add_done_callback(_on_disconnected) + try: + return await func(self, *args, **kwargs) + except asyncio.CancelledError as ex: + source_name = self._source_name # pylint: disable=protected-access + ble_device = self._ble_device # pylint: disable=protected-access raise BleakError( - f"{self._source_name}: " # pylint: disable=protected-access - f"{self._ble_device.name} - " # pylint: disable=protected-access - f" {self._ble_device.address}: " # pylint: disable=protected-access + f"{source_name}: {ble_device.name} - {ble_device.address}: " "Disconnected during operation" - ) - return action_task.result() + ) from ex + finally: + disconnected_futures.discard(disconnected_future) + disconnected_future.remove_done_callback(_on_disconnected) return cast(_WrapFuncType, _async_wrap_bluetooth_connected_operation) @@ -152,7 +155,8 @@ def __init__( self._notify_cancels: dict[ int, tuple[Callable[[], Coroutine[Any, Any, None]], Callable[[], None]] ] = {} - self._disconnected_event: asyncio.Event | None = None + self._loop = asyncio.get_running_loop() + self._disconnected_futures: set[asyncio.Future[None]] = set() device_info = self.entry_data.device_info assert device_info is not None self._device_info = device_info @@ -192,9 +196,10 @@ def _async_disconnected_cleanup(self) -> None: for _, notify_abort in self._notify_cancels.values(): notify_abort() self._notify_cancels.clear() - if self._disconnected_event: - self._disconnected_event.set() - self._disconnected_event = None + for future in self._disconnected_futures: + if not future.done(): + future.set_result(None) + self._disconnected_futures.clear() self._unsubscribe_connection_state() def _async_ble_device_disconnected(self) -> None: @@ -359,7 +364,6 @@ def _on_bluetooth_connection_state( await self.disconnect() raise - self._disconnected_event = asyncio.Event() return True @api_error_as_bleak_error From 0e8c85c5fc3d9c39930acd5b7fa69347688cc0ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Jul 2023 16:46:09 -1000 Subject: [PATCH 0473/1009] Only lookup supported_features once in media_player capability_attributes (#96510) --- homeassistant/components/media_player/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 0f827d60736974..f9c68e2b1f01d7 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -1001,13 +1001,14 @@ def media_image_local(self) -> str | None: def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" data: dict[str, Any] = {} + supported_features = self.supported_features - if self.supported_features & MediaPlayerEntityFeature.SELECT_SOURCE and ( + if supported_features & MediaPlayerEntityFeature.SELECT_SOURCE and ( source_list := self.source_list ): data[ATTR_INPUT_SOURCE_LIST] = source_list - if self.supported_features & MediaPlayerEntityFeature.SELECT_SOUND_MODE and ( + if supported_features & MediaPlayerEntityFeature.SELECT_SOUND_MODE and ( sound_mode_list := self.sound_mode_list ): data[ATTR_SOUND_MODE_LIST] = sound_mode_list From 3e429ae081a6d559bb397dcf99c4674431f79629 Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Fri, 14 Jul 2023 08:50:36 +0200 Subject: [PATCH 0474/1009] Add Ezviz last motion picture image entity (#94421) * Initial commit * Update camera.py * ignore type mismatch on append. * Use new image entity. * coveragerc update * Remove all changes to camera in this pull. * Fix docstring. * remove old "last_alarm_pic" sensor * Update image entity * bypass for content check error * Fix last updated not sting object * Remove redundant url change check. * Remove debug string * Check url change on coordinator data update. * Add translation key for name. * simplify update check * Rebase EzvizLastMotion ImageEntity * Change logging to debug. --- .coveragerc | 1 + homeassistant/components/ezviz/__init__.py | 1 + homeassistant/components/ezviz/image.py | 88 +++++++++++++++++++++ homeassistant/components/ezviz/sensor.py | 1 - homeassistant/components/ezviz/strings.json | 5 ++ 5 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/ezviz/image.py diff --git a/.coveragerc b/.coveragerc index 6b2870e84881af..52350a498d91d6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -317,6 +317,7 @@ omit = homeassistant/components/ezviz/__init__.py homeassistant/components/ezviz/binary_sensor.py homeassistant/components/ezviz/camera.py + homeassistant/components/ezviz/image.py homeassistant/components/ezviz/light.py homeassistant/components/ezviz/coordinator.py homeassistant/components/ezviz/number.py diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index 9aeba56360e8d8..4355d3d2595cb3 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -35,6 +35,7 @@ ATTR_TYPE_CLOUD: [ Platform.BINARY_SENSOR, Platform.CAMERA, + Platform.IMAGE, Platform.LIGHT, Platform.NUMBER, Platform.SELECT, diff --git a/homeassistant/components/ezviz/image.py b/homeassistant/components/ezviz/image.py new file mode 100644 index 00000000000000..a5dbdea1d6f8ea --- /dev/null +++ b/homeassistant/components/ezviz/image.py @@ -0,0 +1,88 @@ +"""Support EZVIZ last motion image.""" +from __future__ import annotations + +import logging + +from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription +from homeassistant.config_entries import ( + ConfigEntry, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, +) +from homeassistant.util import dt as dt_util + +from .const import ( + DATA_COORDINATOR, + DOMAIN, +) +from .coordinator import EzvizDataUpdateCoordinator +from .entity import EzvizEntity + +_LOGGER = logging.getLogger(__name__) +GET_IMAGE_TIMEOUT = 10 + +IMAGE_TYPE = ImageEntityDescription( + key="last_motion_image", + translation_key="last_motion_image", +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up EZVIZ image entities based on a config entry.""" + + coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR + ] + + async_add_entities( + EzvizLastMotion(hass, coordinator, camera) for camera in coordinator.data + ) + + +class EzvizLastMotion(EzvizEntity, ImageEntity): + """Return Last Motion Image from Ezviz Camera.""" + + _attr_has_entity_name = True + + def __init__( + self, hass: HomeAssistant, coordinator: EzvizDataUpdateCoordinator, serial: str + ) -> None: + """Initialize a image entity.""" + super().__init__(coordinator, serial) + ImageEntity.__init__(self, hass) + self._attr_unique_id = f"{serial}_{IMAGE_TYPE.key}" + self.entity_description = IMAGE_TYPE + self._attr_image_url = self.data["last_alarm_pic"] + self._attr_image_last_updated = dt_util.parse_datetime( + str(self.data["last_alarm_time"]) + ) + + async def _async_load_image_from_url(self, url: str) -> Image | None: + """Load an image by url.""" + if response := await self._fetch_url(url): + return Image( + content=response.content, + content_type="image/jpeg", # Actually returns binary/octet-stream + ) + return None + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + if ( + self.data["last_alarm_pic"] + and self.data["last_alarm_pic"] != self._attr_image_url + ): + _LOGGER.debug("Image url changed to %s", self.data["last_alarm_pic"]) + + self._attr_image_url = self.data["last_alarm_pic"] + self._cached_image = None + self._attr_image_last_updated = dt_util.parse_datetime( + str(self.data["last_alarm_time"]) + ) + + super()._handle_coordinator_update() diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py index 075fe6bd6d1b7d..1a8bfba21fb7a8 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -33,7 +33,6 @@ key="Seconds_Last_Trigger", entity_registry_enabled_default=False, ), - "last_alarm_pic": SensorEntityDescription(key="last_alarm_pic"), "supported_channels": SensorEntityDescription(key="supported_channels"), "local_ip": SensorEntityDescription(key="local_ip"), "wan_ip": SensorEntityDescription(key="wan_ip"), diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index aec1f892b1fbc2..1b244182059df3 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -93,6 +93,11 @@ "silent": "Silent" } } + }, + "image": { + "last_motion_image": { + "name": "Last motion image" + } } }, "services": { From 7a1f0a0b7410590108cb9bd04f80b5cc8a075a86 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Jul 2023 22:37:59 -1000 Subject: [PATCH 0475/1009] Remove unneeded str() in StrEnum backport (#96509) --- homeassistant/backports/enum.py | 4 +++- homeassistant/components/demo/climate.py | 4 ++-- homeassistant/components/isy994/climate.py | 10 ++++++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/backports/enum.py b/homeassistant/backports/enum.py index 35562877bab34b..178859ecbe77dd 100644 --- a/homeassistant/backports/enum.py +++ b/homeassistant/backports/enum.py @@ -10,6 +10,8 @@ class StrEnum(str, Enum): """Partial backport of Python 3.11's StrEnum for our basic use cases.""" + value: str + def __new__(cls, value: str, *args: Any, **kwargs: Any) -> Self: """Create a new StrEnum instance.""" if not isinstance(value, str): @@ -18,7 +20,7 @@ def __new__(cls, value: str, *args: Any, **kwargs: Any) -> Self: def __str__(self) -> str: """Return self.value.""" - return str(self.value) + return self.value @staticmethod def _generate_next_value_( diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index 407860526ae797..bfc2cd1a2e71c3 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -64,7 +64,7 @@ async def async_setup_entry( aux=False, target_temp_high=None, target_temp_low=None, - hvac_modes=[cls.value for cls in HVACMode if cls != HVACMode.HEAT_COOL], + hvac_modes=[cls for cls in HVACMode if cls != HVACMode.HEAT_COOL], ), DemoClimate( unique_id="climate_3", @@ -83,7 +83,7 @@ async def async_setup_entry( aux=None, target_temp_high=24, target_temp_low=21, - hvac_modes=[cls.value for cls in HVACMode if cls != HVACMode.HEAT], + hvac_modes=[cls for cls in HVACMode if cls != HVACMode.HEAT], ), ] ) diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 83fea57a9fa8a7..8fc90efaabcd98 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -37,6 +37,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.enum import try_parse_enum from .const import ( _LOGGER, @@ -131,7 +132,10 @@ def hvac_mode(self) -> HVACMode: if self._node.protocol == PROTO_INSTEON else UOM_HVAC_MODE_GENERIC ) - return UOM_TO_STATES[uom].get(hvac_mode.value, HVACMode.OFF) + return ( + try_parse_enum(HVACMode, UOM_TO_STATES[uom].get(hvac_mode.value)) + or HVACMode.OFF + ) @property def hvac_action(self) -> HVACAction | None: @@ -139,7 +143,9 @@ def hvac_action(self) -> HVACAction | None: hvac_action = self._node.aux_properties.get(PROP_HEAT_COOL_STATE) if not hvac_action: return None - return UOM_TO_STATES[UOM_HVAC_ACTIONS].get(hvac_action.value) + return try_parse_enum( + HVACAction, UOM_TO_STATES[UOM_HVAC_ACTIONS].get(hvac_action.value) + ) @property def current_temperature(self) -> float | None: From e44c74f9eb783e1ccea4598236b702496ca41453 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Jul 2023 11:52:24 +0200 Subject: [PATCH 0476/1009] Bump actions/setup-python from 4.6.1 to 4.7.0 (#96526) --- .github/workflows/builder.yml | 6 +++--- .github/workflows/ci.yaml | 24 ++++++++++++------------ .github/workflows/translations.yml | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 2ee32ca9dbc6e8..47e3e765b72789 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -29,7 +29,7 @@ jobs: fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -59,7 +59,7 @@ jobs: uses: actions/checkout@v3.5.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -124,7 +124,7 @@ jobs: - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.0 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 368a3eb6e98c1b..89618685873036 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -209,7 +209,7 @@ jobs: uses: actions/checkout@v3.5.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -253,7 +253,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.5.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -299,7 +299,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.5.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -348,7 +348,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.5.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -443,7 +443,7 @@ jobs: uses: actions/checkout@v3.5.3 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -511,7 +511,7 @@ jobs: uses: actions/checkout@v3.5.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -543,7 +543,7 @@ jobs: uses: actions/checkout@v3.5.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -576,7 +576,7 @@ jobs: uses: actions/checkout@v3.5.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -620,7 +620,7 @@ jobs: uses: actions/checkout@v3.5.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -702,7 +702,7 @@ jobs: uses: actions/checkout@v3.5.3 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -827,7 +827,7 @@ jobs: uses: actions/checkout@v3.5.3 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -934,7 +934,7 @@ jobs: uses: actions/checkout@v3.5.3 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.0 with: python-version: ${{ matrix.python-version }} check-latest: true diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index dd1f3d061a99f7..1d77ac8f13006f 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v3.5.3 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.0 with: python-version: ${{ env.DEFAULT_PYTHON }} From 3b32dcb6133cdb767b9b677bfe0839e1a137d131 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 14 Jul 2023 12:28:19 +0200 Subject: [PATCH 0477/1009] Revert translation reference for Tuya motion_sensitivity (#96536) --- homeassistant/components/tuya/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index ccb7d878a4989f..e7896f5da8623d 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -61,9 +61,9 @@ }, "motion_sensitivity": { "state": { - "0": "[%key:component::tuya::entity::select::decibel_sensitivity::state::0%]", + "0": "Low sensitivity", "1": "Medium sensitivity", - "2": "[%key:component::tuya::entity::select::decibel_sensitivity::state::1%]" + "2": "High sensitivity" } }, "record_mode": { From 614f3c6a15713aa762269f7c628d4420987cbd4c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 14 Jul 2023 14:55:17 +0200 Subject: [PATCH 0478/1009] Move device info validation to device registry (#96465) * Move device info validation to device registry * Don't move DeviceInfo * Fix type annotation * Don't block adding device for unknown config entry * Fix test * Remove use of locals() * Improve error message --- homeassistant/exceptions.py | 15 --- homeassistant/helpers/device_registry.py | 145 +++++++++++++++++++++-- homeassistant/helpers/entity_platform.py | 99 ++-------------- tests/helpers/test_device_registry.py | 24 ++-- tests/helpers/test_entity.py | 1 + tests/helpers/test_entity_platform.py | 1 + 6 files changed, 159 insertions(+), 126 deletions(-) diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index bfc96eabfdfbce..2946c8c3743481 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -191,21 +191,6 @@ def __init__(self, value: str, property_name: str, max_length: int) -> None: self.max_length = max_length -class RequiredParameterMissing(HomeAssistantError): - """Raised when a required parameter is missing from a function call.""" - - def __init__(self, parameter_names: list[str]) -> None: - """Initialize error.""" - super().__init__( - self, - ( - "Call must include at least one of the following parameters: " - f"{', '.join(parameter_names)}" - ), - ) - self.parameter_names = parameter_names - - class DependencyError(HomeAssistantError): """Raised when dependencies cannot be setup.""" diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 79b4eac68d55ca..a59313ed8863cc 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -6,13 +6,14 @@ import logging import time from typing import TYPE_CHECKING, Any, TypeVar, cast +from urllib.parse import urlparse import attr from homeassistant.backports.enum import StrEnum from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError, RequiredParameterMissing +from homeassistant.exceptions import HomeAssistantError from homeassistant.util.json import format_unserializable_data import homeassistant.util.uuid as uuid_util @@ -26,6 +27,7 @@ from homeassistant.config_entries import ConfigEntry from . import entity_registry + from .entity import DeviceInfo _LOGGER = logging.getLogger(__name__) @@ -60,6 +62,39 @@ class DeviceEntryDisabler(StrEnum): DISABLED_INTEGRATION = DeviceEntryDisabler.INTEGRATION.value DISABLED_USER = DeviceEntryDisabler.USER.value +DEVICE_INFO_TYPES = { + # Device info is categorized by finding the first device info type which has all + # the keys of the device info. The link device info type must be kept first + # to make it preferred over primary. + "link": { + "connections", + "identifiers", + }, + "primary": { + "configuration_url", + "connections", + "entry_type", + "hw_version", + "identifiers", + "manufacturer", + "model", + "name", + "suggested_area", + "sw_version", + "via_device", + }, + "secondary": { + "connections", + "default_manufacturer", + "default_model", + "default_name", + # Used by Fritz + "via_device", + }, +} + +DEVICE_INFO_KEYS = set.union(*(itm for itm in DEVICE_INFO_TYPES.values())) + class DeviceEntryType(StrEnum): """Device entry type.""" @@ -67,6 +102,66 @@ class DeviceEntryType(StrEnum): SERVICE = "service" +class DeviceInfoError(HomeAssistantError): + """Raised when device info is invalid.""" + + def __init__(self, domain: str, device_info: DeviceInfo, message: str) -> None: + """Initialize error.""" + super().__init__( + f"Invalid device info {device_info} for '{domain}' config entry: {message}", + ) + self.device_info = device_info + self.domain = domain + + +def _validate_device_info( + config_entry: ConfigEntry | None, + device_info: DeviceInfo, +) -> str: + """Process a device info.""" + keys = set(device_info) + + # If no keys or not enough info to match up, abort + if not device_info.get("connections") and not device_info.get("identifiers"): + raise DeviceInfoError( + config_entry.domain if config_entry else "unknown", + device_info, + "device info must include at least one of identifiers or connections", + ) + + device_info_type: str | None = None + + # Find the first device info type which has all keys in the device info + for possible_type, allowed_keys in DEVICE_INFO_TYPES.items(): + if keys <= allowed_keys: + device_info_type = possible_type + break + + if device_info_type is None: + raise DeviceInfoError( + config_entry.domain if config_entry else "unknown", + device_info, + ( + "device info needs to either describe a device, " + "link to existing device or provide extra information." + ), + ) + + if (config_url := device_info.get("configuration_url")) is not None: + if type(config_url) is not str or urlparse(config_url).scheme not in [ + "http", + "https", + "homeassistant", + ]: + raise DeviceInfoError( + config_entry.domain if config_entry else "unknown", + device_info, + f"invalid configuration_url '{config_url}'", + ) + + return device_info_type + + @attr.s(slots=True, frozen=True) class DeviceEntry: """Device Registry Entry.""" @@ -338,7 +433,7 @@ def async_get_or_create( *, config_entry_id: str, configuration_url: str | None | UndefinedType = UNDEFINED, - connections: set[tuple[str, str]] | None = None, + connections: set[tuple[str, str]] | None | UndefinedType = UNDEFINED, default_manufacturer: str | None | UndefinedType = UNDEFINED, default_model: str | None | UndefinedType = UNDEFINED, default_name: str | None | UndefinedType = UNDEFINED, @@ -346,22 +441,47 @@ def async_get_or_create( disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED, entry_type: DeviceEntryType | None | UndefinedType = UNDEFINED, hw_version: str | None | UndefinedType = UNDEFINED, - identifiers: set[tuple[str, str]] | None = None, + identifiers: set[tuple[str, str]] | None | UndefinedType = UNDEFINED, manufacturer: str | None | UndefinedType = UNDEFINED, model: str | None | UndefinedType = UNDEFINED, name: str | None | UndefinedType = UNDEFINED, suggested_area: str | None | UndefinedType = UNDEFINED, sw_version: str | None | UndefinedType = UNDEFINED, - via_device: tuple[str, str] | None = None, + via_device: tuple[str, str] | None | UndefinedType = UNDEFINED, ) -> DeviceEntry: """Get device. Create if it doesn't exist.""" - if not identifiers and not connections: - raise RequiredParameterMissing(["identifiers", "connections"]) - if identifiers is None: + # Reconstruct a DeviceInfo dict from the arguments. + # When we upgrade to Python 3.12, we can change this method to instead + # accept kwargs typed as a DeviceInfo dict (PEP 692) + device_info: DeviceInfo = {} + for key, val in ( + ("configuration_url", configuration_url), + ("connections", connections), + ("default_manufacturer", default_manufacturer), + ("default_model", default_model), + ("default_name", default_name), + ("entry_type", entry_type), + ("hw_version", hw_version), + ("identifiers", identifiers), + ("manufacturer", manufacturer), + ("model", model), + ("name", name), + ("suggested_area", suggested_area), + ("sw_version", sw_version), + ("via_device", via_device), + ): + if val is UNDEFINED: + continue + device_info[key] = val # type: ignore[literal-required] + + config_entry = self.hass.config_entries.async_get_entry(config_entry_id) + device_info_type = _validate_device_info(config_entry, device_info) + + if identifiers is None or identifiers is UNDEFINED: identifiers = set() - if connections is None: + if connections is None or connections is UNDEFINED: connections = set() else: connections = _normalize_connections(connections) @@ -378,6 +498,13 @@ def async_get_or_create( config_entry_id, connections, identifiers ) self.devices[device.id] = device + # If creating a new device, default to the config entry name + if ( + device_info_type == "primary" + and (not name or name is UNDEFINED) + and config_entry + ): + name = config_entry.title if default_manufacturer is not UNDEFINED and device.manufacturer is None: manufacturer = default_manufacturer @@ -388,7 +515,7 @@ def async_get_or_create( if default_name is not UNDEFINED and device.name is None: name = default_name - if via_device is not None: + if via_device is not None and via_device is not UNDEFINED: via = self.async_get_device(identifiers={via_device}) via_device_id: str | UndefinedType = via.id if via else UNDEFINED else: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index f97e509f486943..067d6430c9f8c6 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -7,7 +7,6 @@ from datetime import datetime, timedelta from logging import Logger, getLogger from typing import TYPE_CHECKING, Any, Protocol -from urllib.parse import urlparse import voluptuous as vol @@ -48,7 +47,7 @@ from .typing import UNDEFINED, ConfigType, DiscoveryInfoType if TYPE_CHECKING: - from .entity import DeviceInfo, Entity + from .entity import Entity SLOW_SETUP_WARNING = 10 @@ -60,37 +59,6 @@ DATA_ENTITY_PLATFORM = "entity_platform" PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds -DEVICE_INFO_TYPES = { - # Device info is categorized by finding the first device info type which has all - # the keys of the device info. The link device info type must be kept first - # to make it preferred over primary. - "link": { - "connections", - "identifiers", - }, - "primary": { - "configuration_url", - "connections", - "entry_type", - "hw_version", - "identifiers", - "manufacturer", - "model", - "name", - "suggested_area", - "sw_version", - "via_device", - }, - "secondary": { - "connections", - "default_manufacturer", - "default_model", - "default_name", - # Used by Fritz - "via_device", - }, -} - _LOGGER = getLogger(__name__) @@ -646,7 +614,14 @@ async def _async_add_entity( # noqa: C901 return if self.config_entry and (device_info := entity.device_info): - device = self._async_process_device_info(device_info) + try: + device = dev_reg.async_get(self.hass).async_get_or_create( + config_entry_id=self.config_entry.entry_id, + **device_info, + ) + except dev_reg.DeviceInfoError as exc: + self.logger.error("Ignoring invalid device info: %s", str(exc)) + device = None else: device = None @@ -773,62 +748,6 @@ def remove_entity_cb() -> None: await entity.add_to_platform_finish() - @callback - def _async_process_device_info( - self, device_info: DeviceInfo - ) -> dev_reg.DeviceEntry | None: - """Process a device info.""" - keys = set(device_info) - - # If no keys or not enough info to match up, abort - if len(keys & {"connections", "identifiers"}) == 0: - self.logger.error( - "Ignoring device info without identifiers or connections: %s", - device_info, - ) - return None - - device_info_type: str | None = None - - # Find the first device info type which has all keys in the device info - for possible_type, allowed_keys in DEVICE_INFO_TYPES.items(): - if keys <= allowed_keys: - device_info_type = possible_type - break - - if device_info_type is None: - self.logger.error( - "Device info for %s needs to either describe a device, " - "link to existing device or provide extra information.", - device_info, - ) - return None - - if (config_url := device_info.get("configuration_url")) is not None: - if type(config_url) is not str or urlparse(config_url).scheme not in [ - "http", - "https", - "homeassistant", - ]: - self.logger.error( - "Ignoring device info with invalid configuration_url '%s'", - config_url, - ) - return None - - assert self.config_entry is not None - - if device_info_type == "primary" and not device_info.get("name"): - device_info = { - **device_info, # type: ignore[misc] - "name": self.config_entry.title, - } - - return dev_reg.async_get(self.hass).async_get_or_create( - config_entry_id=self.config_entry.entry_id, - **device_info, - ) - async def async_reset(self) -> None: """Remove all entities and reset data. diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 7df5859f502853..3e59b08cfa8fff 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -8,7 +8,7 @@ from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, HomeAssistant, callback -from homeassistant.exceptions import RequiredParameterMissing +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( area_registry as ar, device_registry as dr, @@ -118,7 +118,7 @@ async def test_requirement_for_identifier_or_connection( assert entry assert entry2 - with pytest.raises(RequiredParameterMissing) as exc_info: + with pytest.raises(HomeAssistantError): device_registry.async_get_or_create( config_entry_id="1234", connections=set(), @@ -127,8 +127,6 @@ async def test_requirement_for_identifier_or_connection( model="model", ) - assert exc_info.value.parameter_names == ["identifiers", "connections"] - async def test_multiple_config_entries(device_registry: dr.DeviceRegistry) -> None: """Make sure we do not get duplicate entries.""" @@ -1462,7 +1460,8 @@ async def test_get_or_create_empty_then_set_default_values( ) -> None: """Test creating an entry, then setting default name, model, manufacturer.""" entry = device_registry.async_get_or_create( - identifiers={("bridgeid", "0123")}, config_entry_id="1234" + config_entry_id="1234", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) assert entry.name is None assert entry.model is None @@ -1470,7 +1469,7 @@ async def test_get_or_create_empty_then_set_default_values( entry = device_registry.async_get_or_create( config_entry_id="1234", - identifiers={("bridgeid", "0123")}, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, default_name="default name 1", default_model="default model 1", default_manufacturer="default manufacturer 1", @@ -1481,7 +1480,7 @@ async def test_get_or_create_empty_then_set_default_values( entry = device_registry.async_get_or_create( config_entry_id="1234", - identifiers={("bridgeid", "0123")}, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, default_name="default name 2", default_model="default model 2", default_manufacturer="default manufacturer 2", @@ -1496,7 +1495,8 @@ async def test_get_or_create_empty_then_update( ) -> None: """Test creating an entry, then setting name, model, manufacturer.""" entry = device_registry.async_get_or_create( - identifiers={("bridgeid", "0123")}, config_entry_id="1234" + config_entry_id="1234", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) assert entry.name is None assert entry.model is None @@ -1504,7 +1504,7 @@ async def test_get_or_create_empty_then_update( entry = device_registry.async_get_or_create( config_entry_id="1234", - identifiers={("bridgeid", "0123")}, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, name="name 1", model="model 1", manufacturer="manufacturer 1", @@ -1515,7 +1515,7 @@ async def test_get_or_create_empty_then_update( entry = device_registry.async_get_or_create( config_entry_id="1234", - identifiers={("bridgeid", "0123")}, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, default_name="default name 1", default_model="default model 1", default_manufacturer="default manufacturer 1", @@ -1531,7 +1531,7 @@ async def test_get_or_create_sets_default_values( """Test creating an entry, then setting default name, model, manufacturer.""" entry = device_registry.async_get_or_create( config_entry_id="1234", - identifiers={("bridgeid", "0123")}, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, default_name="default name 1", default_model="default model 1", default_manufacturer="default manufacturer 1", @@ -1542,7 +1542,7 @@ async def test_get_or_create_sets_default_values( entry = device_registry.async_get_or_create( config_entry_id="1234", - identifiers={("bridgeid", "0123")}, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, default_name="default name 2", default_model="default model 2", default_manufacturer="default manufacturer 2", diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 7de6f70e793698..0d9ee76ac62e94 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -972,6 +972,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(entry_id="super-mock-id") + config_entry.add_to_hass(hass) entity_platform = MockEntityPlatform( hass, platform_name=config_entry.domain, platform=platform ) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index e07c3cb475392d..1f7e579ea95310 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1830,6 +1830,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(title=config_entry_title, entry_id="super-mock-id") + config_entry.add_to_hass(hass) entity_platform = MockEntityPlatform( hass, platform_name=config_entry.domain, platform=platform ) From afdded58eec01031f657a86865d61e0f767bfc54 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 14 Jul 2023 07:56:27 -0500 Subject: [PATCH 0479/1009] Wyoming Piper 1.1 (#96490) * Add voice/speaker options to Piper TTS * Use description if available * Fix tests * Clean up if --- homeassistant/components/wyoming/__init__.py | 7 +++- homeassistant/components/wyoming/const.py | 3 ++ .../components/wyoming/manifest.json | 2 +- homeassistant/components/wyoming/tts.py | 26 +++++++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wyoming/__init__.py | 15 ++++++++- .../wyoming/snapshots/test_tts.ambr | 15 +++++++++ tests/components/wyoming/test_tts.py | 33 +++++++++++++++++-- 9 files changed, 92 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/wyoming/__init__.py b/homeassistant/components/wyoming/__init__.py index 8676365212accc..33064d21097554 100644 --- a/homeassistant/components/wyoming/__init__.py +++ b/homeassistant/components/wyoming/__init__.py @@ -7,11 +7,16 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import DOMAIN +from .const import ATTR_SPEAKER, DOMAIN from .data import WyomingService _LOGGER = logging.getLogger(__name__) +__all__ = [ + "ATTR_SPEAKER", + "DOMAIN", +] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load Wyoming.""" diff --git a/homeassistant/components/wyoming/const.py b/homeassistant/components/wyoming/const.py index 26443cc11eba2e..fd73a6bd047439 100644 --- a/homeassistant/components/wyoming/const.py +++ b/homeassistant/components/wyoming/const.py @@ -5,3 +5,6 @@ SAMPLE_RATE = 16000 SAMPLE_WIDTH = 2 SAMPLE_CHANNELS = 1 + +# For multi-speaker voices, this is the name of the selected speaker. +ATTR_SPEAKER = "speaker" diff --git a/homeassistant/components/wyoming/manifest.json b/homeassistant/components/wyoming/manifest.json index 9ad8092bb8c445..7fbf3542e13dbc 100644 --- a/homeassistant/components/wyoming/manifest.json +++ b/homeassistant/components/wyoming/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wyoming", "iot_class": "local_push", - "requirements": ["wyoming==0.0.1"] + "requirements": ["wyoming==1.0.0"] } diff --git a/homeassistant/components/wyoming/tts.py b/homeassistant/components/wyoming/tts.py index 0fc7bf5e6c4cd0..6510fd8c761243 100644 --- a/homeassistant/components/wyoming/tts.py +++ b/homeassistant/components/wyoming/tts.py @@ -6,14 +6,14 @@ from wyoming.audio import AudioChunk, AudioChunkConverter, AudioStop from wyoming.client import AsyncTcpClient -from wyoming.tts import Synthesize +from wyoming.tts import Synthesize, SynthesizeVoice from homeassistant.components import tts from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import ATTR_SPEAKER, DOMAIN from .data import WyomingService from .error import WyomingError @@ -57,10 +57,16 @@ def __init__( self._voices[language].append( tts.Voice( voice_id=voice.name, - name=voice.name, + name=voice.description or voice.name, ) ) + # Sort voices by name + for language in self._voices: + self._voices[language] = sorted( + self._voices[language], key=lambda v: v.name + ) + self._supported_languages: list[str] = list(voice_languages) self._attr_name = self._tts_service.name @@ -82,7 +88,7 @@ def supported_languages(self): @property def supported_options(self): """Return list of supported options like voice, emotion.""" - return [tts.ATTR_AUDIO_OUTPUT, tts.ATTR_VOICE] + return [tts.ATTR_AUDIO_OUTPUT, tts.ATTR_VOICE, ATTR_SPEAKER] @property def default_options(self): @@ -95,10 +101,18 @@ def async_get_supported_voices(self, language: str) -> list[tts.Voice] | None: return self._voices.get(language) async def async_get_tts_audio(self, message, language, options): - """Load TTS from UNIX socket.""" + """Load TTS from TCP socket.""" + voice_name: str | None = options.get(tts.ATTR_VOICE) + voice_speaker: str | None = options.get(ATTR_SPEAKER) + try: async with AsyncTcpClient(self.service.host, self.service.port) as client: - await client.write_event(Synthesize(message).event()) + voice: SynthesizeVoice | None = None + if voice_name is not None: + voice = SynthesizeVoice(name=voice_name, speaker=voice_speaker) + + synthesize = Synthesize(text=message, voice=voice) + await client.write_event(synthesize.event()) with io.BytesIO() as wav_io: wav_writer: wave.Wave_write | None = None diff --git a/requirements_all.txt b/requirements_all.txt index 7497521708ae69..74c409bc8c1023 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2681,7 +2681,7 @@ wled==0.16.0 wolf-smartset==0.1.11 # homeassistant.components.wyoming -wyoming==0.0.1 +wyoming==1.0.0 # homeassistant.components.xbox xbox-webapi==2.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 681c17952e4106..24f5987227f739 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1963,7 +1963,7 @@ wled==0.16.0 wolf-smartset==0.1.11 # homeassistant.components.wyoming -wyoming==0.0.1 +wyoming==1.0.0 # homeassistant.components.xbox xbox-webapi==2.0.11 diff --git a/tests/components/wyoming/__init__.py b/tests/components/wyoming/__init__.py index d48b908f26b7e6..3d12d41ce5e273 100644 --- a/tests/components/wyoming/__init__.py +++ b/tests/components/wyoming/__init__.py @@ -1,16 +1,26 @@ """Tests for the Wyoming integration.""" -from wyoming.info import AsrModel, AsrProgram, Attribution, Info, TtsProgram, TtsVoice +from wyoming.info import ( + AsrModel, + AsrProgram, + Attribution, + Info, + TtsProgram, + TtsVoice, + TtsVoiceSpeaker, +) TEST_ATTR = Attribution(name="Test", url="http://www.test.com") STT_INFO = Info( asr=[ AsrProgram( name="Test ASR", + description="Test ASR", installed=True, attribution=TEST_ATTR, models=[ AsrModel( name="Test Model", + description="Test Model", installed=True, attribution=TEST_ATTR, languages=["en-US"], @@ -23,14 +33,17 @@ tts=[ TtsProgram( name="Test TTS", + description="Test TTS", installed=True, attribution=TEST_ATTR, voices=[ TtsVoice( name="Test Voice", + description="Test Voice", installed=True, attribution=TEST_ATTR, languages=["en-US"], + speakers=[TtsVoiceSpeaker(name="Test Speaker")], ) ], ) diff --git a/tests/components/wyoming/snapshots/test_tts.ambr b/tests/components/wyoming/snapshots/test_tts.ambr index eb0b33c3276fca..1cb5a6cb874ac8 100644 --- a/tests/components/wyoming/snapshots/test_tts.ambr +++ b/tests/components/wyoming/snapshots/test_tts.ambr @@ -21,3 +21,18 @@ }), ]) # --- +# name: test_voice_speaker + list([ + dict({ + 'data': dict({ + 'text': 'Hello world', + 'voice': dict({ + 'name': 'voice1', + 'speaker': 'speaker1', + }), + }), + 'payload': None, + 'type': 'synthesize', + }), + ]) +# --- diff --git a/tests/components/wyoming/test_tts.py b/tests/components/wyoming/test_tts.py index 8767660ca08795..51a684bc4fd13f 100644 --- a/tests/components/wyoming/test_tts.py +++ b/tests/components/wyoming/test_tts.py @@ -8,7 +8,7 @@ import pytest from wyoming.audio import AudioChunk, AudioStop -from homeassistant.components import tts +from homeassistant.components import tts, wyoming from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_component import DATA_INSTANCES @@ -31,7 +31,11 @@ async def test_support(hass: HomeAssistant, init_wyoming_tts) -> None: assert entity is not None assert entity.supported_languages == ["en-US"] - assert entity.supported_options == [tts.ATTR_AUDIO_OUTPUT, tts.ATTR_VOICE] + assert entity.supported_options == [ + tts.ATTR_AUDIO_OUTPUT, + tts.ATTR_VOICE, + wyoming.ATTR_SPEAKER, + ] voices = entity.async_get_supported_voices("en-US") assert len(voices) == 1 assert voices[0].name == "Test Voice" @@ -137,3 +141,28 @@ async def test_get_tts_audio_audio_oserror( hass, "Hello world", "tts.test_tts", hass.config.language ), ) + + +async def test_voice_speaker(hass: HomeAssistant, init_wyoming_tts, snapshot) -> None: + """Test using a different voice and speaker.""" + audio = bytes(100) + audio_events = [ + AudioChunk(audio=audio, rate=16000, width=2, channels=1).event(), + AudioStop().event(), + ] + + with patch( + "homeassistant.components.wyoming.tts.AsyncTcpClient", + MockAsyncTcpClient(audio_events), + ) as mock_client: + await tts.async_get_media_source_audio( + hass, + tts.generate_media_source_id( + hass, + "Hello world", + "tts.test_tts", + "en-US", + options={tts.ATTR_VOICE: "voice1", wyoming.ATTR_SPEAKER: "speaker1"}, + ), + ) + assert mock_client.written == snapshot From 357af58c8128d75df513d3815c4e96e09e581ec1 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Fri, 14 Jul 2023 15:04:23 +0200 Subject: [PATCH 0480/1009] 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 e635b1f7021ca7..54b65c17e606ba 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 74c409bc8c1023..b9bea4e24e20ea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ denonavr==0.11.3 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 24f5987227f739..a11459908c01df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -532,7 +532,7 @@ denonavr==0.11.3 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 1b7632a673b6b1bd59fc93ccbf81c3b532fd7305 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 14 Jul 2023 15:04:48 +0200 Subject: [PATCH 0481/1009] 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 972db00e476496..3166c05db19de1 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 80011b47915398..4100a270e0a9c4 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 1e704c4abe0b5c19109ce0fff05a5252dbd91dbd Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Fri, 14 Jul 2023 19:27:41 +0200 Subject: [PATCH 0482/1009] Address Ezviz select entity late review (#96525) * Ezviz Select Entity * Update IR description --- homeassistant/components/ezviz/select.py | 4 ++-- homeassistant/components/ezviz/strings.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ezviz/select.py b/homeassistant/components/ezviz/select.py index 0f6a52ef578502..ef1dd78539248b 100644 --- a/homeassistant/components/ezviz/select.py +++ b/homeassistant/components/ezviz/select.py @@ -53,14 +53,14 @@ async def async_setup_entry( ] async_add_entities( - EzvizSensor(coordinator, camera) + EzvizSelect(coordinator, camera) for camera in coordinator.data for switch in coordinator.data[camera]["switches"] if switch == SELECT_TYPE.supported_switch ) -class EzvizSensor(EzvizEntity, SelectEntity): +class EzvizSelect(EzvizEntity, SelectEntity): """Representation of a EZVIZ select entity.""" _attr_has_entity_name = True diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index 1b244182059df3..f3e76c6748035e 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -77,7 +77,7 @@ "step": { "confirm": { "title": "[%key:component::ezviz::issues::service_deprecation_alarm_sound_level::title%]", - "description": "Ezviz Alarm sound level service is deprecated and will be removed in Home Assistant 2024.2.\nTo set the Alarm sound level, you can instead use the `select.select_option` service targetting the Warning sound entity.\n\nPlease remove the use of this service from your automations and scripts and select **submit** to close this issue." + "description": "Ezviz Alarm sound level service is deprecated and will be removed.\nTo set the Alarm sound level, you can instead use the `select.select_option` service targetting the Warning sound entity.\n\nPlease remove the use of this service from your automations and scripts and select **submit** to close this issue." } } } From bbc3d0d2875c334b4f3dc965943eaf8ecde6aa68 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 14 Jul 2023 21:24:41 +0200 Subject: [PATCH 0483/1009] Improve Mullvad typing (#96545) --- homeassistant/components/mullvad/__init__.py | 4 +- .../components/mullvad/binary_sensor.py | 38 +++++++++++-------- .../components/mullvad/config_flow.py | 3 +- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py index 7934220cf91572..b8551682f1f92d 100644 --- a/homeassistant/components/mullvad/__init__.py +++ b/homeassistant/components/mullvad/__init__.py @@ -8,7 +8,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import update_coordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN @@ -23,7 +23,7 @@ async def async_get_mullvad_api_data(): api = await hass.async_add_executor_job(MullvadAPI) return api.data - coordinator = update_coordinator.DataUpdateCoordinator( + coordinator = DataUpdateCoordinator( hass, logging.getLogger(__name__), name=DOMAIN, diff --git a/homeassistant/components/mullvad/binary_sensor.py b/homeassistant/components/mullvad/binary_sensor.py index f39f05dd430ede..2ccf754bbbd0ba 100644 --- a/homeassistant/components/mullvad/binary_sensor.py +++ b/homeassistant/components/mullvad/binary_sensor.py @@ -2,21 +2,24 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_DEVICE_CLASS, CONF_ID, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import DOMAIN BINARY_SENSORS = ( - { - CONF_ID: "mullvad_exit_ip", - CONF_NAME: "Mullvad Exit IP", - CONF_DEVICE_CLASS: BinarySensorDeviceClass.CONNECTIVITY, - }, + BinarySensorEntityDescription( + key="mullvad_exit_ip", + name="Mullvad Exit IP", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + ), ) @@ -29,23 +32,26 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN] async_add_entities( - MullvadBinarySensor(coordinator, sensor, config_entry) - for sensor in BINARY_SENSORS + MullvadBinarySensor(coordinator, entity_description, config_entry) + for entity_description in BINARY_SENSORS ) class MullvadBinarySensor(CoordinatorEntity, BinarySensorEntity): """Represents a Mullvad binary sensor.""" - def __init__(self, coordinator, sensor, config_entry): + def __init__( + self, + coordinator: DataUpdateCoordinator, + entity_description: BinarySensorEntityDescription, + config_entry: ConfigEntry, + ) -> None: """Initialize the Mullvad binary sensor.""" super().__init__(coordinator) - self._sensor = sensor - self._attr_device_class = sensor[CONF_DEVICE_CLASS] - self._attr_name = sensor[CONF_NAME] - self._attr_unique_id = f"{config_entry.entry_id}_{sensor[CONF_ID]}" + self.entity_description = entity_description + self._attr_unique_id = f"{config_entry.entry_id}_{entity_description.key}" @property - def is_on(self): + def is_on(self) -> bool: """Return the state for this binary sensor.""" - return self.coordinator.data[self._sensor[CONF_ID]] + return self.coordinator.data[self.entity_description.key] diff --git a/homeassistant/components/mullvad/config_flow.py b/homeassistant/components/mullvad/config_flow.py index 5b6ef78133f205..ad045dbb54c38e 100644 --- a/homeassistant/components/mullvad/config_flow.py +++ b/homeassistant/components/mullvad/config_flow.py @@ -2,6 +2,7 @@ from mullvad_api import MullvadAPI, MullvadAPIError from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -11,7 +12,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> FlowResult: """Handle the initial step.""" self._async_abort_entries_match() From 72458b66725c9f05a8c7e4e338bc8f89b5c41fb6 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 14 Jul 2023 21:26:35 +0200 Subject: [PATCH 0484/1009] Add feature to turn off using IMAP-Push on an IMAP server (#96436) * Add feature to enforce polling an IMAP server * Add test * Remove not needed string tweak * Rename enforce_polling to enable_push * Push enabled by default --- homeassistant/components/imap/__init__.py | 5 +- homeassistant/components/imap/config_flow.py | 2 + homeassistant/components/imap/const.py | 1 + homeassistant/components/imap/strings.json | 3 +- tests/components/imap/test_config_flow.py | 12 ++-- tests/components/imap/test_init.py | 76 ++++++++++++++++++-- 6 files changed, 85 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/imap/__init__.py b/homeassistant/components/imap/__init__.py index 04069d42d7d2ab..3914e0c52c1a19 100644 --- a/homeassistant/components/imap/__init__.py +++ b/homeassistant/components/imap/__init__.py @@ -14,7 +14,7 @@ ConfigEntryNotReady, ) -from .const import DOMAIN +from .const import CONF_ENABLE_PUSH, DOMAIN from .coordinator import ( ImapPollingDataUpdateCoordinator, ImapPushDataUpdateCoordinator, @@ -39,7 +39,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator_class: type[ ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator ] - if imap_client.has_capability("IDLE"): + enable_push: bool = entry.data.get(CONF_ENABLE_PUSH, True) + if enable_push and imap_client.has_capability("IDLE"): coordinator_class = ImapPushDataUpdateCoordinator else: coordinator_class = ImapPollingDataUpdateCoordinator diff --git a/homeassistant/components/imap/config_flow.py b/homeassistant/components/imap/config_flow.py index 00be545fb67ddb..4c4a2e2a35c5ef 100644 --- a/homeassistant/components/imap/config_flow.py +++ b/homeassistant/components/imap/config_flow.py @@ -33,6 +33,7 @@ from .const import ( CONF_CHARSET, CONF_CUSTOM_EVENT_DATA_TEMPLATE, + CONF_ENABLE_PUSH, CONF_FOLDER, CONF_MAX_MESSAGE_SIZE, CONF_SEARCH, @@ -87,6 +88,7 @@ cv.positive_int, vol.Range(min=DEFAULT_MAX_MESSAGE_SIZE, max=MAX_MESSAGE_SIZE_LIMIT), ), + vol.Optional(CONF_ENABLE_PUSH, default=True): BOOLEAN_SELECTOR, } diff --git a/homeassistant/components/imap/const.py b/homeassistant/components/imap/const.py index 2e36dd41e16856..fd3da28971e7fb 100644 --- a/homeassistant/components/imap/const.py +++ b/homeassistant/components/imap/const.py @@ -11,6 +11,7 @@ CONF_MAX_MESSAGE_SIZE = "max_message_size" CONF_CUSTOM_EVENT_DATA_TEMPLATE: Final = "custom_event_data_template" CONF_SSL_CIPHER_LIST: Final = "ssl_cipher_list" +CONF_ENABLE_PUSH: Final = "enable_push" DEFAULT_PORT: Final = 993 diff --git a/homeassistant/components/imap/strings.json b/homeassistant/components/imap/strings.json index 6fad88959310a2..62579d61f5a94b 100644 --- a/homeassistant/components/imap/strings.json +++ b/homeassistant/components/imap/strings.json @@ -42,7 +42,8 @@ "folder": "[%key:component::imap::config::step::user::data::folder%]", "search": "[%key:component::imap::config::step::user::data::search%]", "custom_event_data_template": "Template to create custom event data", - "max_message_size": "Max message size (2048 < size < 30000)" + "max_message_size": "Max message size (2048 < size < 30000)", + "enable_push": "Enable Push-IMAP if the server supports it. Turn off if Push-IMAP updates are unreliable" } } }, diff --git a/tests/components/imap/test_config_flow.py b/tests/components/imap/test_config_flow.py index fb4347b08a7a27..efb505cda774db 100644 --- a/tests/components/imap/test_config_flow.py +++ b/tests/components/imap/test_config_flow.py @@ -401,9 +401,9 @@ async def test_key_options_in_options_form(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("advanced_options", "assert_result"), [ - ({"max_message_size": "8192"}, data_entry_flow.FlowResultType.CREATE_ENTRY), - ({"max_message_size": "1024"}, data_entry_flow.FlowResultType.FORM), - ({"max_message_size": "65536"}, data_entry_flow.FlowResultType.FORM), + ({"max_message_size": 8192}, data_entry_flow.FlowResultType.CREATE_ENTRY), + ({"max_message_size": 1024}, data_entry_flow.FlowResultType.FORM), + ({"max_message_size": 65536}, data_entry_flow.FlowResultType.FORM), ( {"custom_event_data_template": "{{ subject }}"}, data_entry_flow.FlowResultType.CREATE_ENTRY, @@ -412,6 +412,8 @@ async def test_key_options_in_options_form(hass: HomeAssistant) -> None: {"custom_event_data_template": "{{ invalid_syntax"}, data_entry_flow.FlowResultType.FORM, ), + ({"enable_push": True}, data_entry_flow.FlowResultType.CREATE_ENTRY), + ({"enable_push": False}, data_entry_flow.FlowResultType.CREATE_ENTRY), ], ids=[ "valid_message_size", @@ -419,6 +421,8 @@ async def test_key_options_in_options_form(hass: HomeAssistant) -> None: "invalid_message_size_high", "valid_template", "invalid_template", + "enable_push_true", + "enable_push_false", ], ) async def test_advanced_options_form( @@ -459,7 +463,7 @@ async def test_advanced_options_form( else: # Check if entry was updated for key, value in new_config.items(): - assert str(entry.data[key]) == value + assert entry.data[key] == value except vol.MultipleInvalid: # Check if form was expected with these options assert assert_result == data_entry_flow.FlowResultType.FORM diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index ff94942361431b..31b42b50781043 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -2,7 +2,7 @@ import asyncio from datetime import datetime, timedelta, timezone from typing import Any -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, call, patch from aioimaplib import AUTH, NONAUTH, SELECTED, AioImapException, Response import pytest @@ -36,13 +36,17 @@ @pytest.mark.parametrize( - ("cipher_list", "verify_ssl"), + ("cipher_list", "verify_ssl", "enable_push"), [ - (None, None), - ("python_default", True), - ("python_default", False), - ("modern", True), - ("intermediate", True), + (None, None, None), + ("python_default", True, None), + ("python_default", False, None), + ("modern", True, None), + ("intermediate", True, None), + (None, None, False), + (None, None, True), + ("python_default", True, False), + ("python_default", False, True), ], ) @pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) @@ -51,6 +55,7 @@ async def test_entry_startup_and_unload( mock_imap_protocol: MagicMock, cipher_list: str | None, verify_ssl: bool | None, + enable_push: bool | None, ) -> None: """Test imap entry startup and unload with push and polling coordinator and alternate ciphers.""" config = MOCK_CONFIG.copy() @@ -58,6 +63,8 @@ async def test_entry_startup_and_unload( config["ssl_cipher_list"] = cipher_list if verify_ssl is not None: config["verify_ssl"] = verify_ssl + if enable_push is not None: + config["enable_push"] = enable_push config_entry = MockConfigEntry(domain=DOMAIN, data=config) config_entry.add_to_hass(hass) @@ -618,3 +625,58 @@ async def test_custom_template( assert data["text"] assert data["custom"] == result assert error in caplog.text if error is not None else True + + +@pytest.mark.parametrize( + ("imap_search", "imap_fetch"), + [(TEST_SEARCH_RESPONSE, TEST_FETCH_RESPONSE_TEXT_PLAIN)], +) +@pytest.mark.parametrize( + ("imap_has_capability", "enable_push", "should_poll"), + [ + (True, False, True), + (False, False, True), + (True, True, False), + (False, True, True), + ], + ids=["enforce_poll", "poll", "auto_push", "auto_poll"], +) +async def test_enforce_polling( + hass: HomeAssistant, + mock_imap_protocol: MagicMock, + enable_push: bool, + should_poll: True, +) -> None: + """Test enforce polling.""" + event_called = async_capture_events(hass, "imap_content") + config = MOCK_CONFIG.copy() + config["enable_push"] = enable_push + + config_entry = MockConfigEntry(domain=DOMAIN, data=config) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + # Make sure we have had one update (when polling) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + state = hass.states.get("sensor.imap_email_email_com") + # we should have received one message + assert state is not None + assert state.state == "1" + assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT + + # we should have received one event + assert len(event_called) == 1 + data: dict[str, Any] = event_called[0].data + assert data["server"] == "imap.server.com" + assert data["username"] == "email@email.com" + assert data["search"] == "UnSeen UnDeleted" + assert data["folder"] == "INBOX" + assert data["sender"] == "john.doe@example.com" + assert data["subject"] == "Test subject" + assert data["text"] + + if should_poll: + mock_imap_protocol.wait_server_push.assert_not_called() + else: + mock_imap_protocol.assert_has_calls([call.wait_server_push]) From b77de2abafc61d65511022c4f58c0ed1a2c0d821 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jul 2023 12:14:32 -1000 Subject: [PATCH 0485/1009] 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 4e3d052e6ef06a..6be1822f90fe38 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -63,7 +63,10 @@ def _on_static_info_update(self, static_info: EntityInfo) -> None: 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 3185a5eb53618c..2e658389e03c46 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -76,7 +76,10 @@ def _on_static_info_update(self, static_info: EntityInfo) -> None: 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 cf3ee4876a832e..dc90d1c10987d5 100644 --- a/tests/components/esphome/test_number.py +++ b/tests/components/esphome/test_number.py @@ -15,7 +15,7 @@ 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_mynumber") 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 9a1863c3c9072e..6c034e674ee783 100644 --- a/tests/components/esphome/test_sensor.py +++ b/tests/components/esphome/test_sensor.py @@ -13,7 +13,7 @@ ) 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_mysensor") 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 81ce6e4797d9f266b5a43eb98f7da6b3c08b1672 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 15 Jul 2023 00:36:26 +0200 Subject: [PATCH 0486/1009] Add entity translations to Sonos (#96167) * Add entity translations to Sonos * Add entity translations to Sonos * Add entity translations to Sonos * Add entity translations to Sonos --- .../components/sonos/binary_sensor.py | 3 +- homeassistant/components/sonos/number.py | 2 +- homeassistant/components/sonos/sensor.py | 3 +- homeassistant/components/sonos/strings.json | 64 +++++++++++++++++++ homeassistant/components/sonos/switch.py | 16 +---- tests/components/sonos/test_sensor.py | 10 +-- 6 files changed, 74 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index 6025af19a60bfb..4a41e572c1ac26 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -58,7 +58,6 @@ class SonosPowerEntity(SonosEntity, BinarySensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING - _attr_name = "Power" def __init__(self, speaker: SonosSpeaker) -> None: """Initialize the power entity binary sensor.""" @@ -92,7 +91,7 @@ class SonosMicrophoneSensorEntity(SonosEntity, BinarySensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_icon = "mdi:microphone" - _attr_name = "Microphone" + _attr_translation_key = "microphone" def __init__(self, speaker: SonosSpeaker) -> None: """Initialize the microphone binary sensor entity.""" diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index 8a9b8e9af704f0..375ed58035b2ab 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -110,7 +110,7 @@ def __init__( """Initialize the level entity.""" super().__init__(speaker) self._attr_unique_id = f"{self.soco.uid}-{level_type}" - self._attr_name = level_type.replace("_", " ").capitalize() + self._attr_translation_key = level_type self.level_type = level_type self._attr_native_min_value, self._attr_native_max_value = valid_range diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index dab70466c8524c..ca3cc89d750c77 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -79,7 +79,6 @@ class SonosBatteryEntity(SonosEntity, SensorEntity): _attr_device_class = SensorDeviceClass.BATTERY _attr_entity_category = EntityCategory.DIAGNOSTIC - _attr_name = "Battery" _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, speaker: SonosSpeaker) -> None: @@ -107,7 +106,7 @@ class SonosAudioInputFormatSensorEntity(SonosPollingEntity, SensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_icon = "mdi:import" - _attr_name = "Audio input format" + _attr_translation_key = "audio_input_format" _attr_should_poll = True def __init__(self, speaker: SonosSpeaker, audio_format: str) -> None: diff --git a/homeassistant/components/sonos/strings.json b/homeassistant/components/sonos/strings.json index 7ce1d727b17f98..fb10167f1d0b1e 100644 --- a/homeassistant/components/sonos/strings.json +++ b/homeassistant/components/sonos/strings.json @@ -17,6 +17,70 @@ "description": "Falling back to polling, functionality may be limited.\n\nSonos device at {device_ip} cannot reach Home Assistant at {listener_address}.\n\nSee our [documentation]({sub_fail_url}) for more information on how to solve this issue." } }, + "entity": { + "binary_sensor": { + "microphone": { + "name": "Microphone" + } + }, + "number": { + "audio_delay": { + "name": "Audio delay" + }, + "bass": { + "name": "Bass" + }, + "balance": { + "name": "Balance" + }, + "treble": { + "name": "Treble" + }, + "sub_gain": { + "name": "Sub gain" + }, + "surround_level": { + "name": "Surround level" + }, + "music_surround_level": { + "name": "Music surround level" + } + }, + "sensor": { + "audio_input_format": { + "name": "Audio input format" + } + }, + "switch": { + "cross_fade": { + "name": "Crossfade" + }, + "loudness": { + "name": "Loudness" + }, + "surround_mode": { + "name": "Surround music full volume" + }, + "night_mode": { + "name": "Night sound" + }, + "dialog_level": { + "name": "Speech enhancement" + }, + "status_light": { + "name": "Status light" + }, + "sub_enabled": { + "name": "Subwoofer enabled" + }, + "surround_enabled": { + "name": "Surround enabled" + }, + "buttons_enabled": { + "name": "Touch controls" + } + } + }, "services": { "snapshot": { "name": "Snapshot", diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 1201ed964903df..c551d4a00d332a 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -67,18 +67,6 @@ ATTR_STATUS_LIGHT, ) -FRIENDLY_NAMES = { - ATTR_CROSSFADE: "Crossfade", - ATTR_LOUDNESS: "Loudness", - ATTR_MUSIC_PLAYBACK_FULL_VOLUME: "Surround music full volume", - ATTR_NIGHT_SOUND: "Night sound", - ATTR_SPEECH_ENHANCEMENT: "Speech enhancement", - ATTR_STATUS_LIGHT: "Status light", - ATTR_SUB_ENABLED: "Subwoofer enabled", - ATTR_SURROUND_ENABLED: "Surround enabled", - ATTR_TOUCH_CONTROLS: "Touch controls", -} - FEATURE_ICONS = { ATTR_LOUDNESS: "mdi:bullhorn-variant", ATTR_MUSIC_PLAYBACK_FULL_VOLUME: "mdi:music-note-plus", @@ -140,7 +128,7 @@ async def _async_create_switches(speaker: SonosSpeaker) -> None: ) _LOGGER.debug( "Creating %s switch on %s", - FRIENDLY_NAMES[feature_type], + feature_type, speaker.zone_name, ) entities.append(SonosSwitchEntity(feature_type, speaker)) @@ -163,7 +151,7 @@ def __init__(self, feature_type: str, speaker: SonosSpeaker) -> None: self.feature_type = feature_type self.needs_coordinator = feature_type in COORDINATOR_FEATURES self._attr_entity_category = EntityCategory.CONFIG - self._attr_name = FRIENDLY_NAMES[feature_type] + self._attr_translation_key = feature_type self._attr_unique_id = f"{speaker.soco.uid}-{feature_type}" self._attr_icon = FEATURE_ICONS.get(feature_type) diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 2d7a9322aeb6b4..40b0c2d21c6e5e 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -28,7 +28,7 @@ async def test_entity_registry_unsupported( assert "media_player.zone_a" in entity_registry.entities assert "sensor.zone_a_battery" not in entity_registry.entities - assert "binary_sensor.zone_a_power" not in entity_registry.entities + assert "binary_sensor.zone_a_charging" not in entity_registry.entities async def test_entity_registry_supported( @@ -37,7 +37,7 @@ async def test_entity_registry_supported( """Test sonos device with battery registered in the device registry.""" assert "media_player.zone_a" in entity_registry.entities assert "sensor.zone_a_battery" in entity_registry.entities - assert "binary_sensor.zone_a_power" in entity_registry.entities + assert "binary_sensor.zone_a_charging" in entity_registry.entities async def test_battery_attributes( @@ -49,7 +49,7 @@ async def test_battery_attributes( assert battery_state.state == "100" assert battery_state.attributes.get("unit_of_measurement") == "%" - power = entity_registry.entities["binary_sensor.zone_a_power"] + power = entity_registry.entities["binary_sensor.zone_a_charging"] power_state = hass.states.get(power.entity_id) assert power_state.state == STATE_ON assert ( @@ -73,7 +73,7 @@ async def test_battery_on_s1( sub_callback = subscription.callback assert "sensor.zone_a_battery" not in entity_registry.entities - assert "binary_sensor.zone_a_power" not in entity_registry.entities + assert "binary_sensor.zone_a_charging" not in entity_registry.entities # Update the speaker with a callback event sub_callback(device_properties_event) @@ -83,7 +83,7 @@ async def test_battery_on_s1( battery_state = hass.states.get(battery.entity_id) assert battery_state.state == "100" - power = entity_registry.entities["binary_sensor.zone_a_power"] + power = entity_registry.entities["binary_sensor.zone_a_charging"] power_state = hass.states.get(power.entity_id) assert power_state.state == STATE_OFF assert power_state.attributes.get(ATTR_BATTERY_POWER_SOURCE) == "BATTERY" From 9775832d530d048b4f3654b9361c64b37fc658d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jul 2023 13:37:16 -1000 Subject: [PATCH 0487/1009] Remove unreachable code in the ESPHome fan platform (#96458) --- homeassistant/components/esphome/fan.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index c6be200e2b21ad..27a259f4441b2c 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -119,9 +119,6 @@ def is_on(self) -> bool | None: @esphome_state_property def percentage(self) -> int | None: """Return the current speed percentage.""" - if not self._static_info.supports_speed: - return None - if not self._supports_speed_levels: return ordered_list_item_to_percentage( ORDERED_NAMED_FAN_SPEEDS, self._state.speed # type: ignore[misc] From c95e2c074cefda03b53d63ffef2ab4c5582ab8ef Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sat, 15 Jul 2023 02:18:34 +0200 Subject: [PATCH 0488/1009] Add missing type hints for AndroidTV (#96554) * Add missing type hints for AndroidTV * Suggested change --- .../components/androidtv/media_player.py | 54 +++++++++++-------- tests/components/androidtv/patchers.py | 8 +-- .../components/androidtv/test_media_player.py | 10 ++-- 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index f4fbe4a498f403..bd800ea04dd964 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -9,6 +9,7 @@ from androidtv.constants import APPS, KEYS from androidtv.exceptions import LockNotAcquiredException +from androidtv.setup_async import AndroidTVAsync, FireTVAsync import voluptuous as vol from homeassistant.components import persistent_notification @@ -88,13 +89,15 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Android Debug Bridge entity.""" - aftv = hass.data[DOMAIN][entry.entry_id][ANDROID_DEV] + aftv: AndroidTVAsync | FireTVAsync = hass.data[DOMAIN][entry.entry_id][ANDROID_DEV] device_class = aftv.DEVICE_CLASS device_type = ( PREFIX_ANDROIDTV if device_class == DEVICE_ANDROIDTV else PREFIX_FIRETV ) # CONF_NAME may be present in entry.data for configuration imported from YAML - device_name = entry.data.get(CONF_NAME) or f"{device_type} {entry.data[CONF_HOST]}" + device_name: str = entry.data.get( + CONF_NAME, f"{device_type} {entry.data[CONF_HOST]}" + ) device_args = [ aftv, @@ -171,8 +174,11 @@ async def _adb_exception_catcher( except LockNotAcquiredException: # If the ADB lock could not be acquired, skip this command _LOGGER.info( - "ADB command not executed because the connection is currently" - " in use" + ( + "ADB command %s not executed because the connection is" + " currently in use" + ), + func.__name__, ) return None except self.exceptions as err: @@ -207,13 +213,13 @@ class ADBDevice(MediaPlayerEntity): def __init__( self, - aftv, - name, - dev_type, - unique_id, - entry_id, - entry_data, - ): + aftv: AndroidTVAsync | FireTVAsync, + name: str, + dev_type: str, + unique_id: str, + entry_id: str, + entry_data: dict[str, Any], + ) -> None: """Initialize the Android / Fire TV device.""" self.aftv = aftv self._attr_name = name @@ -235,13 +241,13 @@ def __init__( if mac := get_androidtv_mac(info): self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, mac)} - self._app_id_to_name = {} - self._app_name_to_id = {} + self._app_id_to_name: dict[str, str] = {} + self._app_name_to_id: dict[str, str] = {} self._get_sources = DEFAULT_GET_SOURCES self._exclude_unnamed_apps = DEFAULT_EXCLUDE_UNNAMED_APPS self._screencap = DEFAULT_SCREENCAP - self.turn_on_command = None - self.turn_off_command = None + self.turn_on_command: str | None = None + self.turn_off_command: str | None = None # ADB exceptions to catch if not aftv.adb_server_ip: @@ -260,7 +266,7 @@ def __init__( # The number of consecutive failed connect attempts self._failed_connect_count = 0 - def _process_config(self): + def _process_config(self) -> None: """Load the config options.""" _LOGGER.debug("Loading configuration options") options = self._entry_data[ANDROID_DEV_OPT] @@ -303,7 +309,7 @@ def media_image_hash(self) -> str | None: return f"{datetime.now().timestamp()}" if self._screencap else None @adb_decorator() - async def _adb_screencap(self): + async def _adb_screencap(self) -> bytes | None: """Take a screen capture from the device.""" return await self.aftv.adb_screencap() @@ -382,7 +388,7 @@ async def async_select_source(self, source: str) -> None: await self.aftv.stop_app(self._app_name_to_id.get(source_, source_)) @adb_decorator() - async def adb_command(self, command): + async def adb_command(self, command: str) -> None: """Send an ADB command to an Android / Fire TV device.""" if key := KEYS.get(command): await self.aftv.adb_shell(f"input keyevent {key}") @@ -407,7 +413,7 @@ async def adb_command(self, command): return @adb_decorator() - async def learn_sendevent(self): + async def learn_sendevent(self) -> None: """Translate a key press on a remote to ADB 'sendevent' commands.""" output = await self.aftv.learn_sendevent() if output: @@ -426,7 +432,7 @@ async def learn_sendevent(self): _LOGGER.info("%s", msg) @adb_decorator() - async def service_download(self, device_path, local_path): + async def service_download(self, device_path: str, local_path: str) -> None: """Download a file from your Android / Fire TV device to your Home Assistant instance.""" if not self.hass.config.is_allowed_path(local_path): _LOGGER.warning("'%s' is not secure to load data from!", local_path) @@ -435,7 +441,7 @@ async def service_download(self, device_path, local_path): await self.aftv.adb_pull(local_path, device_path) @adb_decorator() - async def service_upload(self, device_path, local_path): + async def service_upload(self, device_path: str, local_path: str) -> None: """Upload a file from your Home Assistant instance to an Android / Fire TV device.""" if not self.hass.config.is_allowed_path(local_path): _LOGGER.warning("'%s' is not secure to load data from!", local_path) @@ -460,6 +466,7 @@ class AndroidTVDevice(ADBDevice): | MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_STEP ) + aftv: AndroidTVAsync @adb_decorator(override_available=True) async def async_update(self) -> None: @@ -492,7 +499,7 @@ async def async_update(self) -> None: if self._attr_state is None: self._attr_available = False - if running_apps: + if running_apps and self._attr_app_id: self._attr_source = self._attr_app_name = self._app_id_to_name.get( self._attr_app_id, self._attr_app_id ) @@ -549,6 +556,7 @@ class FireTVDevice(ADBDevice): | MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.STOP ) + aftv: FireTVAsync @adb_decorator(override_available=True) async def async_update(self) -> None: @@ -578,7 +586,7 @@ async def async_update(self) -> None: if self._attr_state is None: self._attr_available = False - if running_apps: + if running_apps and self._attr_app_id: self._attr_source = self._app_id_to_name.get( self._attr_app_id, self._attr_app_id ) diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 5ebd95ccacd1c0..f0fca5aae90362 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -23,7 +23,7 @@ class AdbDeviceTcpAsyncFake: """A fake of the `adb_shell.adb_device_async.AdbDeviceTcpAsync` class.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: """Initialize a fake `adb_shell.adb_device_async.AdbDeviceTcpAsync` instance.""" self.available = False @@ -43,7 +43,7 @@ async def shell(self, cmd, *args, **kwargs): class ClientAsyncFakeSuccess: """A fake of the `ClientAsync` class when the connection and shell commands succeed.""" - def __init__(self, host=ADB_SERVER_HOST, port=DEFAULT_ADB_SERVER_PORT): + def __init__(self, host=ADB_SERVER_HOST, port=DEFAULT_ADB_SERVER_PORT) -> None: """Initialize a `ClientAsyncFakeSuccess` instance.""" self._devices = [] @@ -57,7 +57,7 @@ async def device(self, serial): class ClientAsyncFakeFail: """A fake of the `ClientAsync` class when the connection and shell commands fail.""" - def __init__(self, host=ADB_SERVER_HOST, port=DEFAULT_ADB_SERVER_PORT): + def __init__(self, host=ADB_SERVER_HOST, port=DEFAULT_ADB_SERVER_PORT) -> None: """Initialize a `ClientAsyncFakeFail` instance.""" self._devices = [] @@ -70,7 +70,7 @@ async def device(self, serial): class DeviceAsyncFake: """A fake of the `DeviceAsync` class.""" - def __init__(self, host): + def __init__(self, host) -> None: """Initialize a `DeviceAsyncFake` instance.""" self.host = host diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 59c7ce751ace37..c7083626e15bac 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -197,7 +197,7 @@ def keygen_fixture() -> None: yield -def _setup(config): +def _setup(config) -> tuple[str, str, MockConfigEntry]: """Perform common setup tasks for the tests.""" patch_key = config[ADB_PATCH_KEY] entity_id = f"{MP_DOMAIN}.{slugify(config[TEST_ENTITY_NAME])}" @@ -453,8 +453,8 @@ async def test_exclude_sources( async def _test_select_source( - hass, config, conf_apps, source, expected_arg, method_patch -): + hass: HomeAssistant, config, conf_apps, source, expected_arg, method_patch +) -> None: """Test that the methods for launching and stopping apps are called correctly when selecting a source.""" patch_key, entity_id, config_entry = _setup(config) config_entry.add_to_hass(hass) @@ -947,13 +947,13 @@ async def test_get_image_disabled(hass: HomeAssistant) -> None: async def _test_service( - hass, + hass: HomeAssistant, entity_id, ha_service_name, androidtv_method, additional_service_data=None, return_value=None, -): +) -> None: """Test generic Android media player entity service.""" service_data = {ATTR_ENTITY_ID: entity_id} if additional_service_data: From 1c814b0ee36fce197b39b1a1ced23cb0515fdc30 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jul 2023 14:28:29 -1000 Subject: [PATCH 0489/1009] 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 e448fe066c48d6..4bc9bb248356f0 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -42,11 +42,12 @@ 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 @@ def __init__(self, hass: HomeAssistant) -> None: 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 a80b9f48798527..ed5241a42ad26e 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -742,6 +742,8 @@ async def _async_start(self): 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 7da8e0295efb7d616ea921a057b5d68a82dc97cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jul 2023 14:49:20 -1000 Subject: [PATCH 0490/1009] 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 e92e80a9a68aaa..d03073dcfd377f 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 b9bea4e24e20ea..1b99449ff9d1da 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 a11459908c01df..f8736dc37eaf63 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1014,7 +1014,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 38630f7898e7c133964d3cfb4487732e4a663215 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 14 Jul 2023 15:23:00 -1000 Subject: [PATCH 0491/1009] 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 a524d8ea519825..358cbbf5c834d0 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -387,8 +387,12 @@ async def async_start_events(self): "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 a27e126c861f403c4438a177c3e1fd234237432e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 15 Jul 2023 03:31:56 +0200 Subject: [PATCH 0492/1009] Migrate AppleTV to use has entity name (#96563) * Migrate AppleTV to use has entity name * Add comma --- homeassistant/components/apple_tv/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 440ca5e6c9f182..c1d35c94b4f440 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -88,14 +88,18 @@ class AppleTVEntity(Entity): """Device that sends commands to an Apple TV.""" _attr_should_poll = False + _attr_has_entity_name = True + _attr_name = None def __init__(self, name, identifier, manager): """Initialize device.""" self.atv = None self.manager = manager - self._attr_name = name self._attr_unique_id = identifier - self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, identifier)}) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, identifier)}, + name=name, + ) async def async_added_to_hass(self): """Handle when an entity is about to be added to Home Assistant.""" From 62c5194bc8928eb8591ad07b61a28f4d0c05b0e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Jul 2023 00:09:25 -1000 Subject: [PATCH 0493/1009] Avoid compressing binary images on ingress (#96581) --- homeassistant/components/hassio/http.py | 10 +++- homeassistant/components/hassio/ingress.py | 8 ++- tests/components/hassio/test_ingress.py | 62 ++++++++++++++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 34e1d89b8b4ea1..0e18a009323c2b 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -177,7 +177,8 @@ async def _handle(self, request: web.Request, path: str) -> web.StreamResponse: ) response.content_type = client.content_type - response.enable_compression() + if should_compress(response.content_type): + response.enable_compression() await response.prepare(request) async for data in client.content.iter_chunked(8192): await response.write(data) @@ -213,3 +214,10 @@ def _get_timeout(path: str) -> ClientTimeout: if NO_TIMEOUT.match(path): return ClientTimeout(connect=10, total=None) return ClientTimeout(connect=10, total=300) + + +def should_compress(content_type: str) -> bool: + """Return if we should compress a response.""" + if content_type.startswith("image/"): + return "svg" in content_type + return not content_type.startswith(("video/", "audio/", "font/")) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 2a9d9b7397824e..4a612de7f87984 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -20,6 +20,7 @@ from homeassistant.helpers.typing import UNDEFINED from .const import X_HASS_SOURCE, X_INGRESS_PATH +from .http import should_compress _LOGGER = logging.getLogger(__name__) @@ -182,7 +183,9 @@ async def _handle_request( content_type=result.content_type, body=body, ) - if content_length_int > MIN_COMPRESSED_SIZE: + if content_length_int > MIN_COMPRESSED_SIZE and should_compress( + simple_response.content_type + ): simple_response.enable_compression() await simple_response.prepare(request) return simple_response @@ -192,7 +195,8 @@ async def _handle_request( response.content_type = result.content_type try: - response.enable_compression() + if should_compress(response.content_type): + response.enable_compression() await response.prepare(request) async for data in result.content.iter_chunked(8192): await response.write(data) diff --git a/tests/components/hassio/test_ingress.py b/tests/components/hassio/test_ingress.py index 6df946ad2cfafb..3eda10b15142fe 100644 --- a/tests/components/hassio/test_ingress.py +++ b/tests/components/hassio/test_ingress.py @@ -396,6 +396,68 @@ async def test_ingress_request_get_compressed( assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO] +@pytest.mark.parametrize( + "content_type", + [ + "image/png", + "image/jpeg", + "font/woff2", + "video/mp4", + ], +) +async def test_ingress_request_not_compressed( + hassio_noauth_client, content_type: str, aioclient_mock: AiohttpClientMocker +) -> None: + """Test ingress does not compress images.""" + body = b"this_is_long_enough_to_be_compressed" * 100 + aioclient_mock.get( + "http://127.0.0.1/ingress/core/x.any", + data=body, + headers={"Content-Length": len(body), "Content-Type": content_type}, + ) + + resp = await hassio_noauth_client.get( + "/api/hassio_ingress/core/x.any", + headers={"X-Test-Header": "beer", "Accept-Encoding": "gzip, deflate"}, + ) + + # Check we got right response + assert resp.status == HTTPStatus.OK + assert resp.headers["Content-Type"] == content_type + assert "Content-Encoding" not in resp.headers + + +@pytest.mark.parametrize( + "content_type", + [ + "image/svg+xml", + "text/html", + "application/javascript", + "text/plain", + ], +) +async def test_ingress_request_compressed( + hassio_noauth_client, content_type: str, aioclient_mock: AiohttpClientMocker +) -> None: + """Test ingress compresses text.""" + body = b"this_is_long_enough_to_be_compressed" * 100 + aioclient_mock.get( + "http://127.0.0.1/ingress/core/x.any", + data=body, + headers={"Content-Length": len(body), "Content-Type": content_type}, + ) + + resp = await hassio_noauth_client.get( + "/api/hassio_ingress/core/x.any", + headers={"X-Test-Header": "beer", "Accept-Encoding": "gzip, deflate"}, + ) + + # Check we got right response + assert resp.status == HTTPStatus.OK + assert resp.headers["Content-Type"] == content_type + assert resp.headers["Content-Encoding"] == "deflate" + + @pytest.mark.parametrize( "build_type", [ From d35e5db9843624f560e0bf4934568bca90478d6d Mon Sep 17 00:00:00 2001 From: Aaron Collins Date: Sun, 16 Jul 2023 00:17:02 +1200 Subject: [PATCH 0494/1009] 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 b0097f607d5809..3ef9c0aba62afb 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 cccf7bba9bdfe0de6672e81ed4f4154ff9dc00ab Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 15 Jul 2023 09:02:59 -0700 Subject: [PATCH 0495/1009] 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 a44cfb3ce138ff..f1f1aed044bf73 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 1b99449ff9d1da..416ae7f3a4964c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1944,7 +1944,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 f8736dc37eaf63..ee4c49ac94586e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1445,7 +1445,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 d65119bbb35144b8af2a2c68ade4b5b538759f8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Jul 2023 06:38:42 -1000 Subject: [PATCH 0496/1009] Avoid writing state in homekit_controller for unrelated aid/iids (#96583) --- .../components/homekit_controller/connection.py | 2 +- .../components/homekit_controller/entity.py | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index b937e7f2e0b0a7..314db187b6a3ee 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -768,7 +768,7 @@ def process_new_events( self.entity_map.process_changes(new_values_dict) - async_dispatcher_send(self.hass, self.signal_state_updated) + async_dispatcher_send(self.hass, self.signal_state_updated, new_values_dict) async def get_characteristics(self, *args: Any, **kwargs: Any) -> dict[str, Any]: """Read latest state from homekit accessory.""" diff --git a/homeassistant/components/homekit_controller/entity.py b/homeassistant/components/homekit_controller/entity.py index 6171e9406a0b14..5a687020eb65ff 100644 --- a/homeassistant/components/homekit_controller/entity.py +++ b/homeassistant/components/homekit_controller/entity.py @@ -11,6 +11,7 @@ ) from aiohomekit.model.services import Service, ServicesTypes +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.typing import ConfigType @@ -30,6 +31,7 @@ def __init__(self, accessory: HKDevice, devinfo: ConfigType) -> None: self._aid = devinfo["aid"] self._iid = devinfo["iid"] self._char_name: str | None = None + self.all_characteristics: set[tuple[int, int]] = set() self.setup() super().__init__() @@ -51,13 +53,23 @@ def service(self) -> Service: """Return a Service model that this entity is attached to.""" return self.accessory.services.iid(self._iid) + @callback + def _async_state_changed( + self, new_values_dict: dict[tuple[int, int], dict[str, Any]] | None = None + ) -> None: + """Handle when characteristics change value.""" + if new_values_dict is None or self.all_characteristics.intersection( + new_values_dict + ): + self.async_write_ha_state() + async def async_added_to_hass(self) -> None: """Entity added to hass.""" self.async_on_remove( async_dispatcher_connect( self.hass, self._accessory.signal_state_updated, - self.async_write_ha_state, + self._async_state_changed, ) ) @@ -105,6 +117,9 @@ def setup(self) -> None: for char in service.characteristics.filter(char_types=char_types): self._setup_characteristic(char) + self.all_characteristics.update(self.pollable_characteristics) + self.all_characteristics.update(self.watchable_characteristics) + def _setup_characteristic(self, char: Characteristic) -> None: """Configure an entity based on a HomeKit characteristics metadata.""" # Build up a list of (aid, iid) tuples to poll on update() From 3b309cad9925285598ef41467d52da58f01a9f3e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 15 Jul 2023 19:09:37 +0200 Subject: [PATCH 0497/1009] Migrate Heos to has entity name (#96595) --- homeassistant/components/heos/media_player.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 3b6f5bcdd2fcc7..c111a23bf0606b 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -114,6 +114,8 @@ class HeosMediaPlayer(MediaPlayerEntity): _attr_media_content_type = MediaType.MUSIC _attr_should_poll = False + _attr_has_entity_name = True + _attr_name = None def __init__(self, player): """Initialize.""" @@ -392,11 +394,6 @@ def media_title(self) -> str: """Title of current playing media.""" return self._player.now_playing_media.song - @property - def name(self) -> str: - """Return the name of the device.""" - return self._player.name - @property def shuffle(self) -> bool: """Boolean if shuffle is enabled.""" From edcae7581258d1324c1bb4f54cd6e502313891c6 Mon Sep 17 00:00:00 2001 From: Dennis Date: Sat, 15 Jul 2023 20:58:40 +0200 Subject: [PATCH 0498/1009] Add UV Index and UV Health Concern sensors to tomorrow.io (#96534) --- homeassistant/components/tomorrowio/__init__.py | 4 ++++ homeassistant/components/tomorrowio/const.py | 2 ++ homeassistant/components/tomorrowio/sensor.py | 17 +++++++++++++++++ .../components/tomorrowio/strings.json | 9 +++++++++ homeassistant/components/tomorrowio/weather.py | 1 + tests/components/tomorrowio/fixtures/v4.json | 4 +++- tests/components/tomorrowio/test_sensor.py | 9 +++++++++ 7 files changed, 45 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tomorrowio/__init__.py b/homeassistant/components/tomorrowio/__init__.py index b8a1ba64423875..ce5ec4191c5abf 100644 --- a/homeassistant/components/tomorrowio/__init__.py +++ b/homeassistant/components/tomorrowio/__init__.py @@ -72,6 +72,8 @@ TMRW_ATTR_TEMPERATURE, TMRW_ATTR_TEMPERATURE_HIGH, TMRW_ATTR_TEMPERATURE_LOW, + TMRW_ATTR_UV_HEALTH_CONCERN, + TMRW_ATTR_UV_INDEX, TMRW_ATTR_VISIBILITY, TMRW_ATTR_WIND_DIRECTION, TMRW_ATTR_WIND_GUST, @@ -291,6 +293,8 @@ async def _async_update_data(self) -> dict[str, Any]: TMRW_ATTR_PRESSURE_SURFACE_LEVEL, TMRW_ATTR_SOLAR_GHI, TMRW_ATTR_SULPHUR_DIOXIDE, + TMRW_ATTR_UV_INDEX, + TMRW_ATTR_UV_HEALTH_CONCERN, TMRW_ATTR_WIND_GUST, ], [ diff --git a/homeassistant/components/tomorrowio/const.py b/homeassistant/components/tomorrowio/const.py index 4b1e2487da8db7..51d8d5f31ccd21 100644 --- a/homeassistant/components/tomorrowio/const.py +++ b/homeassistant/components/tomorrowio/const.py @@ -115,3 +115,5 @@ TMRW_ATTR_SOLAR_GHI = "solarGHI" TMRW_ATTR_CLOUD_BASE = "cloudBase" TMRW_ATTR_CLOUD_CEILING = "cloudCeiling" +TMRW_ATTR_UV_INDEX = "uvIndex" +TMRW_ATTR_UV_HEALTH_CONCERN = "uvHealthConcern" diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py index 046dc79f2c6d7e..6f75679f124dd3 100644 --- a/homeassistant/components/tomorrowio/sensor.py +++ b/homeassistant/components/tomorrowio/sensor.py @@ -11,6 +11,7 @@ PollenIndex, PrecipitationType, PrimaryPollutantType, + UVDescription, ) from homeassistant.components.sensor import ( @@ -64,6 +65,8 @@ TMRW_ATTR_PRESSURE_SURFACE_LEVEL, TMRW_ATTR_SOLAR_GHI, TMRW_ATTR_SULPHUR_DIOXIDE, + TMRW_ATTR_UV_HEALTH_CONCERN, + TMRW_ATTR_UV_INDEX, TMRW_ATTR_WIND_GUST, ) @@ -309,6 +312,20 @@ def convert_ppb_to_ugm3(molecular_weight: int | float) -> Callable[[float], floa name="Fire Index", icon="mdi:fire", ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_UV_INDEX, + name="UV Index", + icon="mdi:sun-wireless", + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_UV_HEALTH_CONCERN, + name="UV Radiation Health Concern", + value_map=UVDescription, + device_class=SensorDeviceClass.ENUM, + options=["high", "low", "moderate", "very_high", "extreme"], + translation_key="uv_index", + icon="mdi:sun-wireless", + ), ) diff --git a/homeassistant/components/tomorrowio/strings.json b/homeassistant/components/tomorrowio/strings.json index 1057477b0acedb..c795dbfdbafd10 100644 --- a/homeassistant/components/tomorrowio/strings.json +++ b/homeassistant/components/tomorrowio/strings.json @@ -61,6 +61,15 @@ "freezing_rain": "Freezing Rain", "ice_pellets": "Ice Pellets" } + }, + "uv_index": { + "state": { + "low": "Low", + "moderate": "Moderate", + "high": "High", + "very_high": "Very high", + "extreme": "Extreme" + } } } } diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py index d92ac401f92b61..86b84ec3ca62e6 100644 --- a/homeassistant/components/tomorrowio/weather.py +++ b/homeassistant/components/tomorrowio/weather.py @@ -224,6 +224,7 @@ def forecast(self): temp = values.get(TMRW_ATTR_TEMPERATURE_HIGH) temp_low = None + wind_direction = values.get(TMRW_ATTR_WIND_DIRECTION) wind_speed = values.get(TMRW_ATTR_WIND_SPEED) diff --git a/tests/components/tomorrowio/fixtures/v4.json b/tests/components/tomorrowio/fixtures/v4.json index ed5fb0982a023c..0ca4f348956104 100644 --- a/tests/components/tomorrowio/fixtures/v4.json +++ b/tests/components/tomorrowio/fixtures/v4.json @@ -31,7 +31,9 @@ "pressureSurfaceLevel": 29.47, "solarGHI": 0, "cloudBase": 0.74, - "cloudCeiling": 0.74 + "cloudCeiling": 0.74, + "uvIndex": 3, + "uvHealthConcern": 1 }, "forecasts": { "nowcast": [ diff --git a/tests/components/tomorrowio/test_sensor.py b/tests/components/tomorrowio/test_sensor.py index 487b3a4adb8dd7..77335769383e98 100644 --- a/tests/components/tomorrowio/test_sensor.py +++ b/tests/components/tomorrowio/test_sensor.py @@ -60,6 +60,9 @@ CLOUD_CEILING = "cloud_ceiling" WIND_GUST = "wind_gust" PRECIPITATION_TYPE = "precipitation_type" +UV_INDEX = "uv_index" +UV_HEALTH_CONCERN = "uv_radiation_health_concern" + V3_FIELDS = [ O3, @@ -91,6 +94,8 @@ CLOUD_CEILING, WIND_GUST, PRECIPITATION_TYPE, + UV_INDEX, + UV_HEALTH_CONCERN, ] @@ -171,6 +176,8 @@ async def test_v4_sensor(hass: HomeAssistant) -> None: check_sensor_state(hass, CLOUD_CEILING, "0.74") check_sensor_state(hass, WIND_GUST, "12.64") check_sensor_state(hass, PRECIPITATION_TYPE, "rain") + check_sensor_state(hass, UV_INDEX, "3") + check_sensor_state(hass, UV_HEALTH_CONCERN, "moderate") async def test_v4_sensor_imperial(hass: HomeAssistant) -> None: @@ -202,6 +209,8 @@ async def test_v4_sensor_imperial(hass: HomeAssistant) -> None: check_sensor_state(hass, CLOUD_CEILING, "0.46") check_sensor_state(hass, WIND_GUST, "28.27") check_sensor_state(hass, PRECIPITATION_TYPE, "rain") + check_sensor_state(hass, UV_INDEX, "3") + check_sensor_state(hass, UV_HEALTH_CONCERN, "moderate") async def test_entity_description() -> None: From e91e32f071cac01da65ff5675b0753188d1a850f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 15 Jul 2023 14:11:14 -0700 Subject: [PATCH 0499/1009] 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 f1f1aed044bf73..986e89783d7f10 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 416ae7f3a4964c..96ff74ee91a562 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1944,7 +1944,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 ee4c49ac94586e..59e8e4b66e423e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1445,7 +1445,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 2f5c480f7fb07ce1c152ef21931388a74c9b4bb6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 16 Jul 2023 00:28:34 +0200 Subject: [PATCH 0500/1009] Update pip constraint to allow pip 23.2 (#96614) --- .github/workflows/ci.yaml | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 89618685873036..8fd01ada0e9d81 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -492,7 +492,7 @@ jobs: python -m venv venv . venv/bin/activate python --version - pip install --cache-dir=$PIP_CACHE -U "pip>=21.3.1,<23.2" setuptools wheel + pip install --cache-dir=$PIP_CACHE -U "pip>=21.3.1,<23.3" setuptools wheel pip install --cache-dir=$PIP_CACHE -r requirements_all.txt pip install --cache-dir=$PIP_CACHE -r requirements_test.txt pip install -e . --config-settings editable_mode=compat diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6b3e1d8506df63..31dcba97924d34 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ mutagen==1.46.0 orjson==3.9.2 paho-mqtt==1.6.1 Pillow==10.0.0 -pip>=21.3.1,<23.2 +pip>=21.3.1,<23.3 psutil-home-assistant==0.0.1 PyJWT==2.7.0 PyNaCl==1.5.0 diff --git a/pyproject.toml b/pyproject.toml index f746797277308a..317a68d36bc8cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ dependencies = [ # pyOpenSSL 23.2.0 is required to work with cryptography 41+ "pyOpenSSL==23.2.0", "orjson==3.9.2", - "pip>=21.3.1,<23.2", + "pip>=21.3.1,<23.3", "python-slugify==4.0.1", "PyYAML==6.0", "requests==2.31.0", diff --git a/requirements.txt b/requirements.txt index 210bd8a0bfc348..cb78783559b287 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ PyJWT==2.7.0 cryptography==41.0.1 pyOpenSSL==23.2.0 orjson==3.9.2 -pip>=21.3.1,<23.2 +pip>=21.3.1,<23.3 python-slugify==4.0.1 PyYAML==6.0 requests==2.31.0 From 30e05ab85e4cf9849bbdabfcc803dd2c22936f92 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Jul 2023 12:31:35 -1000 Subject: [PATCH 0501/1009] Bump aioesphomeapi to 15.1.7 (#96615) --- 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 4f208ed011541f..e5448fb395d765 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.1.6", + "aioesphomeapi==15.1.7", "bluetooth-data-tools==1.6.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 96ff74ee91a562..47f1514f13d034 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==15.1.6 +aioesphomeapi==15.1.7 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 59e8e4b66e423e..0cb2d4a6a7d1c3 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.1.6 +aioesphomeapi==15.1.7 # homeassistant.components.flo aioflo==2021.11.0 From 5d3039f21e33b38dff8beba1e9bb6cae6342ff46 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 16 Jul 2023 00:36:13 +0200 Subject: [PATCH 0502/1009] Use device class naming for Switchbot (#96187) --- homeassistant/components/switchbot/binary_sensor.py | 1 - homeassistant/components/switchbot/sensor.py | 3 --- homeassistant/components/switchbot/strings.json | 12 ------------ 3 files changed, 16 deletions(-) diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index 237a2d9766825d..7169f01b38fae4 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -41,7 +41,6 @@ ), "is_light": BinarySensorEntityDescription( key="is_light", - translation_key="light", device_class=BinarySensorDeviceClass.LIGHT, ), "door_open": BinarySensorEntityDescription( diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index e9e434bc51ca76..a408bcb58bc3be 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -46,7 +46,6 @@ ), "battery": SensorEntityDescription( key="battery", - translation_key="battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, @@ -60,7 +59,6 @@ ), "humidity": SensorEntityDescription( key="humidity", - translation_key="humidity", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.HUMIDITY, @@ -74,7 +72,6 @@ ), "power": SensorEntityDescription( key="power", - translation_key="power", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index c00f2fe79e4e79..8eab1ec6f1a34e 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -67,9 +67,6 @@ "door_timeout": { "name": "Timeout" }, - "light": { - "name": "[%key:component::binary_sensor::entity_component::light::name%]" - }, "door_unclosed_alarm": { "name": "Unclosed alarm" }, @@ -87,17 +84,8 @@ "wifi_signal": { "name": "Wi-Fi signal" }, - "battery": { - "name": "[%key:component::sensor::entity_component::battery::name%]" - }, "light_level": { "name": "Light level" - }, - "humidity": { - "name": "[%key:component::sensor::entity_component::humidity::name%]" - }, - "power": { - "name": "[%key:component::sensor::entity_component::power::name%]" } }, "cover": { From b53df429facc0e68d6201b8abbca7d54f8e1cd40 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 16 Jul 2023 03:03:29 +0200 Subject: [PATCH 0503/1009] Add entity translations for Mazda (#95729) * Add entity translations for Mazda * Use references --- .../components/mazda/binary_sensor.py | 14 +-- homeassistant/components/mazda/button.py | 10 +- homeassistant/components/mazda/climate.py | 2 +- .../components/mazda/device_tracker.py | 2 +- homeassistant/components/mazda/lock.py | 2 +- homeassistant/components/mazda/sensor.py | 18 ++-- homeassistant/components/mazda/strings.json | 91 +++++++++++++++++++ homeassistant/components/mazda/switch.py | 2 +- 8 files changed, 116 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/mazda/binary_sensor.py b/homeassistant/components/mazda/binary_sensor.py index c2727654525702..36c3ba274633ad 100644 --- a/homeassistant/components/mazda/binary_sensor.py +++ b/homeassistant/components/mazda/binary_sensor.py @@ -46,49 +46,49 @@ def _plugged_in_supported(data): BINARY_SENSOR_ENTITIES = [ MazdaBinarySensorEntityDescription( key="driver_door", - name="Driver door", + translation_key="driver_door", icon="mdi:car-door", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["driverDoorOpen"], ), MazdaBinarySensorEntityDescription( key="passenger_door", - name="Passenger door", + translation_key="passenger_door", icon="mdi:car-door", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["passengerDoorOpen"], ), MazdaBinarySensorEntityDescription( key="rear_left_door", - name="Rear left door", + translation_key="rear_left_door", icon="mdi:car-door", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["rearLeftDoorOpen"], ), MazdaBinarySensorEntityDescription( key="rear_right_door", - name="Rear right door", + translation_key="rear_right_door", icon="mdi:car-door", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["rearRightDoorOpen"], ), MazdaBinarySensorEntityDescription( key="trunk", - name="Trunk", + translation_key="trunk", icon="mdi:car-back", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["trunkOpen"], ), MazdaBinarySensorEntityDescription( key="hood", - name="Hood", + translation_key="hood", icon="mdi:car", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["hoodOpen"], ), MazdaBinarySensorEntityDescription( key="ev_plugged_in", - name="Plugged in", + translation_key="ev_plugged_in", device_class=BinarySensorDeviceClass.PLUG, is_supported=_plugged_in_supported, value_fn=lambda data: data["evStatus"]["chargeInfo"]["pluggedIn"], diff --git a/homeassistant/components/mazda/button.py b/homeassistant/components/mazda/button.py index 1b1e51db0358f6..ced1094981fda8 100644 --- a/homeassistant/components/mazda/button.py +++ b/homeassistant/components/mazda/button.py @@ -76,31 +76,31 @@ class MazdaButtonEntityDescription(ButtonEntityDescription): BUTTON_ENTITIES = [ MazdaButtonEntityDescription( key="start_engine", - name="Start engine", + translation_key="start_engine", icon="mdi:engine", is_supported=lambda data: not data["isElectric"], ), MazdaButtonEntityDescription( key="stop_engine", - name="Stop engine", + translation_key="stop_engine", icon="mdi:engine-off", is_supported=lambda data: not data["isElectric"], ), MazdaButtonEntityDescription( key="turn_on_hazard_lights", - name="Turn on hazard lights", + translation_key="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", + translation_key="turn_off_hazard_lights", icon="mdi:hazard-lights", is_supported=lambda data: not data["isElectric"], ), MazdaButtonEntityDescription( key="refresh_vehicle_status", - name="Refresh status", + translation_key="refresh_vehicle_status", icon="mdi:refresh", async_press=handle_refresh_vehicle_status, is_supported=lambda data: data["isElectric"], diff --git a/homeassistant/components/mazda/climate.py b/homeassistant/components/mazda/climate.py index 02c4e7ce923ac8..43dc4b4151db9e 100644 --- a/homeassistant/components/mazda/climate.py +++ b/homeassistant/components/mazda/climate.py @@ -66,7 +66,7 @@ async def async_setup_entry( class MazdaClimateEntity(MazdaEntity, ClimateEntity): """Class for a Mazda climate entity.""" - _attr_name = "Climate" + _attr_translation_key = "climate" _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) diff --git a/homeassistant/components/mazda/device_tracker.py b/homeassistant/components/mazda/device_tracker.py index 67702ba5455214..2af191f97bc7ef 100644 --- a/homeassistant/components/mazda/device_tracker.py +++ b/homeassistant/components/mazda/device_tracker.py @@ -28,7 +28,7 @@ async def async_setup_entry( class MazdaDeviceTracker(MazdaEntity, TrackerEntity): """Class for the device tracker.""" - _attr_name = "Device tracker" + _attr_translation_key = "device_tracker" _attr_icon = "mdi:car" _attr_force_update = False diff --git a/homeassistant/components/mazda/lock.py b/homeassistant/components/mazda/lock.py index 1f42c5dce48b09..d095ac81955954 100644 --- a/homeassistant/components/mazda/lock.py +++ b/homeassistant/components/mazda/lock.py @@ -32,7 +32,7 @@ async def async_setup_entry( class MazdaLock(MazdaEntity, LockEntity): """Class for the lock.""" - _attr_name = "Lock" + _attr_translation_key = "lock" def __init__(self, client, coordinator, index) -> None: """Initialize Mazda lock.""" diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py index 5815f93102995b..f50533e339aeba 100644 --- a/homeassistant/components/mazda/sensor.py +++ b/homeassistant/components/mazda/sensor.py @@ -135,7 +135,7 @@ def _ev_remaining_range_value(data): SENSOR_ENTITIES = [ MazdaSensorEntityDescription( key="fuel_remaining_percentage", - name="Fuel remaining percentage", + translation_key="fuel_remaining_percentage", icon="mdi:gas-station", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -144,7 +144,7 @@ def _ev_remaining_range_value(data): ), MazdaSensorEntityDescription( key="fuel_distance_remaining", - name="Fuel distance remaining", + translation_key="fuel_distance_remaining", icon="mdi:gas-station", device_class=SensorDeviceClass.DISTANCE, native_unit_of_measurement=UnitOfLength.KILOMETERS, @@ -154,7 +154,7 @@ def _ev_remaining_range_value(data): ), MazdaSensorEntityDescription( key="odometer", - name="Odometer", + translation_key="odometer", icon="mdi:speedometer", device_class=SensorDeviceClass.DISTANCE, native_unit_of_measurement=UnitOfLength.KILOMETERS, @@ -164,7 +164,7 @@ def _ev_remaining_range_value(data): ), MazdaSensorEntityDescription( key="front_left_tire_pressure", - name="Front left tire pressure", + translation_key="front_left_tire_pressure", icon="mdi:car-tire-alert", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=UnitOfPressure.PSI, @@ -174,7 +174,7 @@ def _ev_remaining_range_value(data): ), MazdaSensorEntityDescription( key="front_right_tire_pressure", - name="Front right tire pressure", + translation_key="front_right_tire_pressure", icon="mdi:car-tire-alert", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=UnitOfPressure.PSI, @@ -184,7 +184,7 @@ def _ev_remaining_range_value(data): ), MazdaSensorEntityDescription( key="rear_left_tire_pressure", - name="Rear left tire pressure", + translation_key="rear_left_tire_pressure", icon="mdi:car-tire-alert", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=UnitOfPressure.PSI, @@ -194,7 +194,7 @@ def _ev_remaining_range_value(data): ), MazdaSensorEntityDescription( key="rear_right_tire_pressure", - name="Rear right tire pressure", + translation_key="rear_right_tire_pressure", icon="mdi:car-tire-alert", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=UnitOfPressure.PSI, @@ -204,7 +204,7 @@ def _ev_remaining_range_value(data): ), MazdaSensorEntityDescription( key="ev_charge_level", - name="Charge level", + translation_key="ev_charge_level", device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -213,7 +213,7 @@ def _ev_remaining_range_value(data): ), MazdaSensorEntityDescription( key="ev_remaining_range", - name="Remaining range", + translation_key="ev_remaining_range", icon="mdi:ev-station", device_class=SensorDeviceClass.DISTANCE, native_unit_of_measurement=UnitOfLength.KILOMETERS, diff --git a/homeassistant/components/mazda/strings.json b/homeassistant/components/mazda/strings.json index a714d1af00f798..6c1214f76c6b17 100644 --- a/homeassistant/components/mazda/strings.json +++ b/homeassistant/components/mazda/strings.json @@ -21,6 +21,97 @@ } } }, + "entity": { + "binary_sensor": { + "driver_door": { + "name": "Driver door" + }, + "passenger_door": { + "name": "Passenger door" + }, + "rear_left_door": { + "name": "Rear left door" + }, + "rear_right_door": { + "name": "Rear right door" + }, + "trunk": { + "name": "Trunk" + }, + "hood": { + "name": "Hood" + }, + "ev_plugged_in": { + "name": "Plugged in" + } + }, + "button": { + "start_engine": { + "name": "Start engine" + }, + "stop_engine": { + "name": "Stop engine" + }, + "turn_on_hazard_lights": { + "name": "Turn on hazard lights" + }, + "turn_off_hazard_lights": { + "name": "Turn off hazard lights" + }, + "refresh_vehicle_status": { + "name": "Refresh status" + } + }, + "climate": { + "climate": { + "name": "[%key:component::climate::title%]" + } + }, + "device_tracker": { + "device_tracker": { + "name": "[%key:component::device_tracker::title%]" + } + }, + "lock": { + "lock": { + "name": "[%key:component::lock::title%]" + } + }, + "sensor": { + "fuel_remaining_percentage": { + "name": "Fuel remaining percentage" + }, + "fuel_distance_remaining": { + "name": "Fuel distance remaining" + }, + "odometer": { + "name": "Odometer" + }, + "front_left_tire_pressure": { + "name": "Front left tire pressure" + }, + "front_right_tire_pressure": { + "name": "Front right tire pressure" + }, + "rear_left_tire_pressure": { + "name": "Rear left tire pressure" + }, + "rear_right_tire_pressure": { + "name": "Rear right tire pressure" + }, + "ev_charge_level": { + "name": "Charge level" + }, + "ev_remaining_range": { + "name": "Remaining range" + } + }, + "switch": { + "charging": { + "name": "Charging" + } + } + }, "services": { "send_poi": { "name": "Send POI", diff --git a/homeassistant/components/mazda/switch.py b/homeassistant/components/mazda/switch.py index 7097237bc5da87..327d371769bdfa 100644 --- a/homeassistant/components/mazda/switch.py +++ b/homeassistant/components/mazda/switch.py @@ -32,7 +32,7 @@ async def async_setup_entry( class MazdaChargingSwitch(MazdaEntity, SwitchEntity): """Class for the charging switch.""" - _attr_name = "Charging" + _attr_translation_key = "charging" _attr_icon = "mdi:ev-station" def __init__( From 63115a906d7c8ede02be7ee9c30ac95d0c5ff7b8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 16 Jul 2023 03:03:47 +0200 Subject: [PATCH 0504/1009] Migrate evil genius labs to has entity name (#96570) --- homeassistant/components/evil_genius_labs/__init__.py | 2 ++ homeassistant/components/evil_genius_labs/light.py | 6 +----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/evil_genius_labs/__init__.py b/homeassistant/components/evil_genius_labs/__init__.py index d70837153942fd..839d546588cedb 100644 --- a/homeassistant/components/evil_genius_labs/__init__.py +++ b/homeassistant/components/evil_genius_labs/__init__.py @@ -103,6 +103,8 @@ async def _async_update_data(self) -> dict: class EvilGeniusEntity(CoordinatorEntity[EvilGeniusUpdateCoordinator]): """Base entity for Evil Genius.""" + _attr_has_entity_name = True + @property def device_info(self) -> DeviceInfo: """Return device info.""" diff --git a/homeassistant/components/evil_genius_labs/light.py b/homeassistant/components/evil_genius_labs/light.py index 41fbcfa9b489c5..a915619b1b8db9 100644 --- a/homeassistant/components/evil_genius_labs/light.py +++ b/homeassistant/components/evil_genius_labs/light.py @@ -32,6 +32,7 @@ async def async_setup_entry( class EvilGeniusLight(EvilGeniusEntity, LightEntity): """Evil Genius Labs light.""" + _attr_name = None _attr_supported_features = LightEntityFeature.EFFECT _attr_supported_color_modes = {ColorMode.RGB} _attr_color_mode = ColorMode.RGB @@ -47,11 +48,6 @@ def __init__(self, coordinator: EvilGeniusUpdateCoordinator) -> None: ] self._attr_effect_list.insert(0, HA_NO_EFFECT) - @property - def name(self) -> str: - """Return name.""" - return cast(str, self.coordinator.data["name"]["value"]) - @property def is_on(self) -> bool: """Return if light is on.""" From 4d3e24465cebde19a3983859a34327a8b258cc5f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Jul 2023 21:47:09 -1000 Subject: [PATCH 0505/1009] Bump bthome-ble to 3.0.0 (#96616) --- homeassistant/components/bthome/config_flow.py | 2 +- homeassistant/components/bthome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bthome/test_config_flow.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bthome/config_flow.py b/homeassistant/components/bthome/config_flow.py index 6514f2c5396eda..a728efdf05aa05 100644 --- a/homeassistant/components/bthome/config_flow.py +++ b/homeassistant/components/bthome/config_flow.py @@ -80,7 +80,7 @@ async def async_step_get_encryption_key( if len(bindkey) != 32: errors["bindkey"] = "expected_32_characters" else: - self._discovered_device.bindkey = bytes.fromhex(bindkey) + self._discovered_device.set_bindkey(bytes.fromhex(bindkey)) # If we got this far we already know supported will # return true so we don't bother checking that again diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index b38c1d3829b367..418c7b8e3e3392 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.1"] + "requirements": ["bthome-ble==3.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 47f1514f13d034..962e3fb3cdc9b4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -562,7 +562,7 @@ brunt==1.2.0 bt-proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==2.12.1 +bthome-ble==3.0.0 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0cb2d4a6a7d1c3..7c86965e902609 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.1 +bthome-ble==3.0.0 # homeassistant.components.buienradar buienradar==1.0.5 diff --git a/tests/components/bthome/test_config_flow.py b/tests/components/bthome/test_config_flow.py index ad5d5b45cbbbd0..ee983148fd402b 100644 --- a/tests/components/bthome/test_config_flow.py +++ b/tests/components/bthome/test_config_flow.py @@ -175,7 +175,7 @@ async def test_async_step_user_no_devices_found_2(hass: HomeAssistant) -> None: This variant tests with a non-BTHome device known to us. """ with patch( - "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + "homeassistant.components.bthome.config_flow.async_discovered_service_info", return_value=[NOT_BTHOME_SERVICE_INFO], ): result = await hass.config_entries.flow.async_init( From cd0e9839a06aa1920dbc1c587503151155e0df0a Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 16 Jul 2023 13:31:23 +0200 Subject: [PATCH 0506/1009] Correct unit types in gardean bluetooth (#96683) --- homeassistant/components/gardena_bluetooth/number.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/gardena_bluetooth/number.py b/homeassistant/components/gardena_bluetooth/number.py index 367e2f727bc8fc..50cc209e26868c 100644 --- a/homeassistant/components/gardena_bluetooth/number.py +++ b/homeassistant/components/gardena_bluetooth/number.py @@ -16,7 +16,7 @@ NumberMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory +from homeassistant.const import EntityCategory, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -37,7 +37,7 @@ class GardenaBluetoothNumberEntityDescription(NumberEntityDescription): GardenaBluetoothNumberEntityDescription( key=Valve.manual_watering_time.uuid, translation_key="manual_watering_time", - native_unit_of_measurement="s", + native_unit_of_measurement=UnitOfTime.SECONDS, mode=NumberMode.BOX, native_min_value=0.0, native_max_value=24 * 60 * 60, @@ -48,7 +48,7 @@ class GardenaBluetoothNumberEntityDescription(NumberEntityDescription): GardenaBluetoothNumberEntityDescription( key=Valve.remaining_open_time.uuid, translation_key="remaining_open_time", - native_unit_of_measurement="s", + native_unit_of_measurement=UnitOfTime.SECONDS, native_min_value=0.0, native_max_value=24 * 60 * 60, native_step=60.0, @@ -58,7 +58,7 @@ class GardenaBluetoothNumberEntityDescription(NumberEntityDescription): GardenaBluetoothNumberEntityDescription( key=DeviceConfiguration.rain_pause.uuid, translation_key="rain_pause", - native_unit_of_measurement="d", + native_unit_of_measurement=UnitOfTime.DAYS, mode=NumberMode.BOX, native_min_value=0.0, native_max_value=127.0, @@ -69,7 +69,7 @@ class GardenaBluetoothNumberEntityDescription(NumberEntityDescription): GardenaBluetoothNumberEntityDescription( key=DeviceConfiguration.season_pause.uuid, translation_key="season_pause", - native_unit_of_measurement="d", + native_unit_of_measurement=UnitOfTime.DAYS, mode=NumberMode.BOX, native_min_value=0.0, native_max_value=365.0, From 7ec506907cd53d25069ee2edccc1e03d9b235d7f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jul 2023 05:10:07 -1000 Subject: [PATCH 0507/1009] Ensure async_get_system_info does not fail if supervisor is unavailable (#96492) * Ensure async_get_system_info does not fail if supervisor is unavailable fixes #96470 * fix i/o in the event loop * fix tests * handle some more failure cases * more I/O here * coverage * coverage * Update homeassistant/helpers/system_info.py Co-authored-by: Paulus Schoutsen * remove supervisor detection fallback * Update tests/helpers/test_system_info.py --------- Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/system_info.py | 35 +++++++++--- tests/helpers/test_system_info.py | 79 +++++++++++++++++++++++++--- 2 files changed, 100 insertions(+), 14 deletions(-) diff --git a/homeassistant/helpers/system_info.py b/homeassistant/helpers/system_info.py index a551c6e3b9e9ad..8af04c11c1844a 100644 --- a/homeassistant/helpers/system_info.py +++ b/homeassistant/helpers/system_info.py @@ -1,7 +1,9 @@ """Helper to gather system info.""" from __future__ import annotations +from functools import cache from getpass import getuser +import logging import os import platform from typing import Any @@ -9,17 +11,32 @@ from homeassistant.const import __version__ as current_version from homeassistant.core import HomeAssistant from homeassistant.loader import bind_hass -from homeassistant.util.package import is_virtual_env +from homeassistant.util.package import is_docker_env, is_virtual_env + +_LOGGER = logging.getLogger(__name__) + + +@cache +def is_official_image() -> bool: + """Return True if Home Assistant is running in an official container.""" + return os.path.isfile("/OFFICIAL_IMAGE") + + +# Cache the result of getuser() because it can call getpwuid() which +# can do blocking I/O to look up the username in /etc/passwd. +cached_get_user = cache(getuser) @bind_hass async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]: """Return info about the system.""" + is_hassio = hass.components.hassio.is_hassio() + info_object = { "installation_type": "Unknown", "version": current_version, "dev": "dev" in current_version, - "hassio": hass.components.hassio.is_hassio(), + "hassio": is_hassio, "virtualenv": is_virtual_env(), "python_version": platform.python_version(), "docker": False, @@ -30,18 +47,18 @@ async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]: } try: - info_object["user"] = getuser() + info_object["user"] = cached_get_user() except KeyError: info_object["user"] = None if platform.system() == "Darwin": info_object["os_version"] = platform.mac_ver()[0] elif platform.system() == "Linux": - info_object["docker"] = os.path.isfile("/.dockerenv") + info_object["docker"] = is_docker_env() # Determine installation type on current data if info_object["docker"]: - if info_object["user"] == "root" and os.path.isfile("/OFFICIAL_IMAGE"): + if info_object["user"] == "root" and is_official_image(): info_object["installation_type"] = "Home Assistant Container" else: info_object["installation_type"] = "Unsupported Third Party Container" @@ -50,10 +67,12 @@ async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]: info_object["installation_type"] = "Home Assistant Core" # Enrich with Supervisor information - if hass.components.hassio.is_hassio(): - info = hass.components.hassio.get_info() - host = hass.components.hassio.get_host_info() + if is_hassio: + if not (info := hass.components.hassio.get_info()): + _LOGGER.warning("No Home Assistant Supervisor info available") + info = {} + host = hass.components.hassio.get_host_info() or {} info_object["supervisor"] = info.get("supervisor") info_object["host_os"] = host.get("operating_system") info_object["docker_version"] = info.get("docker") diff --git a/tests/helpers/test_system_info.py b/tests/helpers/test_system_info.py index ba43386b821ce2..ebb0cc35c20bfa 100644 --- a/tests/helpers/test_system_info.py +++ b/tests/helpers/test_system_info.py @@ -1,10 +1,23 @@ """Tests for the system info helper.""" import json +import os from unittest.mock import patch +import pytest + from homeassistant.const import __version__ as current_version from homeassistant.core import HomeAssistant -from homeassistant.helpers.system_info import async_get_system_info +from homeassistant.helpers.system_info import async_get_system_info, is_official_image + + +async def test_is_official_image() -> None: + """Test is_official_image.""" + is_official_image.cache_clear() + with patch("homeassistant.helpers.system_info.os.path.isfile", return_value=True): + assert is_official_image() is True + is_official_image.cache_clear() + with patch("homeassistant.helpers.system_info.os.path.isfile", return_value=False): + assert is_official_image() is False async def test_get_system_info(hass: HomeAssistant) -> None: @@ -16,23 +29,77 @@ async def test_get_system_info(hass: HomeAssistant) -> None: assert json.dumps(info) is not None +async def test_get_system_info_supervisor_not_available( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test the get system info when supervisor is not available.""" + hass.config.components.add("hassio") + with patch("platform.system", return_value="Linux"), patch( + "homeassistant.helpers.system_info.is_docker_env", return_value=True + ), patch( + "homeassistant.helpers.system_info.is_official_image", return_value=True + ), patch( + "homeassistant.components.hassio.is_hassio", return_value=True + ), patch( + "homeassistant.components.hassio.get_info", return_value=None + ), patch( + "homeassistant.helpers.system_info.cached_get_user", return_value="root" + ): + info = await async_get_system_info(hass) + assert isinstance(info, dict) + assert info["version"] == current_version + assert info["user"] is not None + assert json.dumps(info) is not None + assert info["installation_type"] == "Home Assistant Supervised" + assert "No Home Assistant Supervisor info available" in caplog.text + + +async def test_get_system_info_supervisor_not_loaded(hass: HomeAssistant) -> None: + """Test the get system info when supervisor is not loaded.""" + with patch("platform.system", return_value="Linux"), patch( + "homeassistant.helpers.system_info.is_docker_env", return_value=True + ), patch( + "homeassistant.helpers.system_info.is_official_image", return_value=True + ), patch( + "homeassistant.components.hassio.get_info", return_value=None + ), patch.dict( + os.environ, {"SUPERVISOR": "127.0.0.1"} + ): + info = await async_get_system_info(hass) + assert isinstance(info, dict) + assert info["version"] == current_version + assert info["user"] is not None + assert json.dumps(info) is not None + assert info["installation_type"] == "Unsupported Third Party Container" + + async def test_container_installationtype(hass: HomeAssistant) -> None: """Test container installation type.""" with patch("platform.system", return_value="Linux"), patch( - "os.path.isfile", return_value=True - ), patch("homeassistant.helpers.system_info.getuser", return_value="root"): + "homeassistant.helpers.system_info.is_docker_env", return_value=True + ), patch( + "homeassistant.helpers.system_info.is_official_image", return_value=True + ), patch( + "homeassistant.helpers.system_info.cached_get_user", return_value="root" + ): info = await async_get_system_info(hass) assert info["installation_type"] == "Home Assistant Container" with patch("platform.system", return_value="Linux"), patch( - "os.path.isfile", side_effect=lambda file: file == "/.dockerenv" - ), patch("homeassistant.helpers.system_info.getuser", return_value="user"): + "homeassistant.helpers.system_info.is_docker_env", return_value=True + ), patch( + "homeassistant.helpers.system_info.is_official_image", return_value=False + ), patch( + "homeassistant.helpers.system_info.cached_get_user", return_value="user" + ): info = await async_get_system_info(hass) assert info["installation_type"] == "Unsupported Third Party Container" async def test_getuser_keyerror(hass: HomeAssistant) -> None: """Test getuser keyerror.""" - with patch("homeassistant.helpers.system_info.getuser", side_effect=KeyError): + with patch( + "homeassistant.helpers.system_info.cached_get_user", side_effect=KeyError + ): info = await async_get_system_info(hass) assert info["user"] is None From 28540b0cb250543cfc9934df96e446396b8fb21a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 16 Jul 2023 17:39:27 +0200 Subject: [PATCH 0508/1009] Migrate google assistant to has entity name (#96593) * Migrate google assistant to has entity name * Fix tests * Add device name * Update homeassistant/components/google_assistant/button.py Co-authored-by: Paulus Schoutsen --------- Co-authored-by: Paulus Schoutsen --- homeassistant/components/google_assistant/button.py | 9 +++++++-- homeassistant/components/google_assistant/strings.json | 7 +++++++ tests/components/google_assistant/test_button.py | 6 +++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google_assistant/button.py b/homeassistant/components/google_assistant/button.py index 415531214c5bdb..47681308b53b77 100644 --- a/homeassistant/components/google_assistant/button.py +++ b/homeassistant/components/google_assistant/button.py @@ -34,14 +34,19 @@ async def async_setup_entry( class SyncButton(ButtonEntity): """Representation of a synchronization button.""" + _attr_has_entity_name = True + _attr_translation_key = "sync_devices" + def __init__(self, project_id: str, google_config: GoogleConfig) -> None: """Initialize button.""" super().__init__() self._google_config = google_config self._attr_entity_category = EntityCategory.DIAGNOSTIC self._attr_unique_id = f"{project_id}_sync" - self._attr_name = "Synchronize Devices" - self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, project_id)}) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, project_id)}, + name="Google Assistant", + ) async def async_press(self) -> None: """Press the button.""" diff --git a/homeassistant/components/google_assistant/strings.json b/homeassistant/components/google_assistant/strings.json index cb01a0febf5282..8ef77f8d8c3258 100644 --- a/homeassistant/components/google_assistant/strings.json +++ b/homeassistant/components/google_assistant/strings.json @@ -1,4 +1,11 @@ { + "entity": { + "button": { + "sync_devices": { + "name": "Synchronize devices" + } + } + }, "services": { "request_sync": { "name": "Request sync", diff --git a/tests/components/google_assistant/test_button.py b/tests/components/google_assistant/test_button.py index d16d406999ebba..d3c5665b9457f1 100644 --- a/tests/components/google_assistant/test_button.py +++ b/tests/components/google_assistant/test_button.py @@ -24,7 +24,7 @@ async def test_sync_button(hass: HomeAssistant, hass_owner_user: MockUser) -> No await hass.async_block_till_done() - state = hass.states.get("button.synchronize_devices") + state = hass.states.get("button.google_assistant_synchronize_devices") assert state config_entry = hass.config_entries.async_entries("google_assistant")[0] @@ -36,7 +36,7 @@ async def test_sync_button(hass: HomeAssistant, hass_owner_user: MockUser) -> No await hass.services.async_call( "button", "press", - {"entity_id": "button.synchronize_devices"}, + {"entity_id": "button.google_assistant_synchronize_devices"}, blocking=True, context=context, ) @@ -48,7 +48,7 @@ async def test_sync_button(hass: HomeAssistant, hass_owner_user: MockUser) -> No await hass.services.async_call( "button", "press", - {"entity_id": "button.synchronize_devices"}, + {"entity_id": "button.google_assistant_synchronize_devices"}, blocking=True, context=context, ) From cde1903e8b55c3e29de151e517763d25ff5db61d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jul 2023 06:22:36 -1000 Subject: [PATCH 0509/1009] Avoid multiple options and current_option lookups in select entites (#96630) --- homeassistant/components/select/__init__.py | 24 ++++++++++++--------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/select/__init__.py b/homeassistant/components/select/__init__.py index af390a005a7b09..a8034588ed1f39 100644 --- a/homeassistant/components/select/__init__.py +++ b/homeassistant/components/select/__init__.py @@ -144,9 +144,10 @@ def capability_attributes(self) -> dict[str, Any]: @final def state(self) -> str | None: """Return the entity state.""" - if self.current_option is None or self.current_option not in self.options: + current_option = self.current_option + if current_option is None or current_option not in self.options: return None - return self.current_option + return current_option @property def options(self) -> list[str]: @@ -209,21 +210,24 @@ async def async_previous(self, cycle: bool) -> None: async def _async_offset_index(self, offset: int, cycle: bool) -> None: """Offset current index.""" current_index = 0 - if self.current_option is not None and self.current_option in self.options: - current_index = self.options.index(self.current_option) + current_option = self.current_option + options = self.options + if current_option is not None and current_option in self.options: + current_index = self.options.index(current_option) new_index = current_index + offset if cycle: - new_index = new_index % len(self.options) + new_index = new_index % len(options) elif new_index < 0: new_index = 0 - elif new_index >= len(self.options): - new_index = len(self.options) - 1 + elif new_index >= len(options): + new_index = len(options) - 1 - await self.async_select_option(self.options[new_index]) + await self.async_select_option(options[new_index]) @final async def _async_select_index(self, idx: int) -> None: """Select new option by index.""" - new_index = idx % len(self.options) - await self.async_select_option(self.options[new_index]) + options = self.options + new_index = idx % len(options) + await self.async_select_option(options[new_index]) From f2556df7dba2d67c5549c2094c34b0217d22aa1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jul 2023 06:24:27 -1000 Subject: [PATCH 0510/1009] Reduce unifiprotect update overhead (#96626) --- .../components/unifiprotect/binary_sensor.py | 13 +++--- .../components/unifiprotect/button.py | 3 +- .../components/unifiprotect/camera.py | 37 +++++++++------- .../components/unifiprotect/entity.py | 35 ++++++--------- .../components/unifiprotect/light.py | 5 ++- homeassistant/components/unifiprotect/lock.py | 11 ++--- .../components/unifiprotect/media_player.py | 13 +++--- .../components/unifiprotect/models.py | 17 +------ .../components/unifiprotect/select.py | 8 ++-- .../components/unifiprotect/sensor.py | 44 ++++--------------- 10 files changed, 74 insertions(+), 112 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index fe4399c4c6d37a..668fe479e1f15d 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -556,12 +556,13 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity): @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) - - self._attr_is_on = self.entity_description.get_ufp_value(self.device) + entity_description = self.entity_description + updated_device = self.device + self._attr_is_on = entity_description.get_ufp_value(updated_device) # UP Sense can be any of the 3 contact sensor device classes - if self.entity_description.key == _KEY_DOOR and isinstance(self.device, Sensor): - self.entity_description.device_class = MOUNT_DEVICE_CLASS_MAP.get( - self.device.mount_type, BinarySensorDeviceClass.DOOR + if entity_description.key == _KEY_DOOR and isinstance(updated_device, Sensor): + entity_description.device_class = MOUNT_DEVICE_CLASS_MAP.get( + updated_device.mount_type, BinarySensorDeviceClass.DOOR ) @@ -615,7 +616,7 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity): @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) - is_on = self.entity_description.get_is_on(device) + is_on = self.entity_description.get_is_on(self._event) self._attr_is_on: bool | None = is_on if not is_on: self._event = None diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 8c620402e7758c..3306743b7072af 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -183,7 +183,8 @@ def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) if self.entity_description.key == KEY_ADOPT: - self._attr_available = self.device.can_adopt and self.device.can_create( + device = self.device + self._attr_available = device.can_adopt and device.can_create( self.data.api.bootstrap.auth_user ) diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index 019ff7b786363c..481d51ec529eaf 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -151,23 +151,25 @@ def __init__( self._disable_stream = disable_stream self._last_image: bytes | None = None super().__init__(data, camera) + device = self.device if self._secure: - self._attr_unique_id = f"{self.device.mac}_{self.channel.id}" - self._attr_name = f"{self.device.display_name} {self.channel.name}" + self._attr_unique_id = f"{device.mac}_{channel.id}" + self._attr_name = f"{device.display_name} {channel.name}" else: - self._attr_unique_id = f"{self.device.mac}_{self.channel.id}_insecure" - self._attr_name = f"{self.device.display_name} {self.channel.name} Insecure" + self._attr_unique_id = f"{device.mac}_{channel.id}_insecure" + self._attr_name = f"{device.display_name} {channel.name} Insecure" # only the default (first) channel is enabled by default self._attr_entity_registry_enabled_default = is_default and secure @callback def _async_set_stream_source(self) -> None: disable_stream = self._disable_stream - if not self.channel.is_rtsp_enabled: + channel = self.channel + + if not channel.is_rtsp_enabled: disable_stream = False - channel = self.channel rtsp_url = channel.rtsps_url if self._secure else channel.rtsp_url # _async_set_stream_source called by __init__ @@ -182,27 +184,30 @@ def _async_set_stream_source(self) -> None: @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) - self.channel = self.device.channels[self.channel.id] - motion_enabled = self.device.recording_settings.enable_motion_detection + updated_device = self.device + channel = updated_device.channels[self.channel.id] + self.channel = channel + motion_enabled = updated_device.recording_settings.enable_motion_detection self._attr_motion_detection_enabled = ( motion_enabled if motion_enabled is not None else True ) self._attr_is_recording = ( - self.device.state == StateType.CONNECTED and self.device.is_recording + updated_device.state == StateType.CONNECTED and updated_device.is_recording ) is_connected = ( - self.data.last_update_success and self.device.state == StateType.CONNECTED + self.data.last_update_success + and updated_device.state == StateType.CONNECTED ) # some cameras have detachable lens that could cause the camera to be offline - self._attr_available = is_connected and self.device.is_video_ready + self._attr_available = is_connected and updated_device.is_video_ready self._async_set_stream_source() self._attr_extra_state_attributes = { - ATTR_WIDTH: self.channel.width, - ATTR_HEIGHT: self.channel.height, - ATTR_FPS: self.channel.fps, - ATTR_BITRATE: self.channel.bitrate, - ATTR_CHANNEL_ID: self.channel.id, + ATTR_WIDTH: channel.width, + ATTR_HEIGHT: channel.height, + ATTR_FPS: channel.fps, + ATTR_BITRATE: channel.bitrate, + ATTR_CHANNEL_ID: channel.id, } async def async_camera_image( diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 79ee483dd8d4f8..fa85a0629cbe8d 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -297,10 +297,12 @@ def _async_set_device_info(self) -> None: @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: - if self.data.last_update_success: - self.device = self.data.api.bootstrap.nvr + data = self.data + last_update_success = data.last_update_success + if last_update_success: + self.device = data.api.bootstrap.nvr - self._attr_available = self.data.last_update_success + self._attr_available = last_update_success class EventEntityMixin(ProtectDeviceEntity): @@ -317,24 +319,15 @@ def __init__( super().__init__(*args, **kwarg) self._event: Event | None = None - @callback - def _async_event_extra_attrs(self) -> dict[str, Any]: - attrs: dict[str, Any] = {} - - if self._event is None: - return attrs - - attrs[ATTR_EVENT_ID] = self._event.id - attrs[ATTR_EVENT_SCORE] = self._event.score - return attrs - @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + event = self.entity_description.get_event_obj(device) + if event is not None: + self._attr_extra_state_attributes = { + ATTR_EVENT_ID: event.id, + ATTR_EVENT_SCORE: event.score, + } + else: + self._attr_extra_state_attributes = {} + self._event = event super()._async_update_device_from_protect(device) - self._event = self.entity_description.get_event_obj(device) - - attrs = self.extra_state_attributes or {} - self._attr_extra_state_attributes = { - **attrs, - **self._async_event_extra_attrs(), - } diff --git a/homeassistant/components/unifiprotect/light.py b/homeassistant/components/unifiprotect/light.py index 500b4b4703ea4c..38ce73828c279d 100644 --- a/homeassistant/components/unifiprotect/light.py +++ b/homeassistant/components/unifiprotect/light.py @@ -73,9 +73,10 @@ class ProtectLight(ProtectDeviceEntity, LightEntity): @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) - self._attr_is_on = self.device.is_light_on + updated_device = self.device + self._attr_is_on = updated_device.is_light_on self._attr_brightness = unifi_brightness_to_hass( - self.device.light_device_settings.led_level + updated_device.light_device_settings.led_level ) async def async_turn_on(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/unifiprotect/lock.py b/homeassistant/components/unifiprotect/lock.py index 4fa9ebf400143f..791a5e958eaf40 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -73,18 +73,19 @@ def __init__( @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) + lock_status = self.device.lock_status self._attr_is_locked = False self._attr_is_locking = False self._attr_is_unlocking = False self._attr_is_jammed = False - if self.device.lock_status == LockStatusType.CLOSED: + if lock_status == LockStatusType.CLOSED: self._attr_is_locked = True - elif self.device.lock_status == LockStatusType.CLOSING: + elif lock_status == LockStatusType.CLOSING: self._attr_is_locking = True - elif self.device.lock_status == LockStatusType.OPENING: + elif lock_status == LockStatusType.OPENING: self._attr_is_unlocking = True - elif self.device.lock_status in ( + elif lock_status in ( LockStatusType.FAILED_WHILE_CLOSING, LockStatusType.FAILED_WHILE_OPENING, LockStatusType.JAMMED_WHILE_CLOSING, @@ -92,7 +93,7 @@ def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: ): self._attr_is_jammed = True # lock is not fully initialized yet - elif self.device.lock_status != LockStatusType.OPEN: + elif lock_status != LockStatusType.OPEN: self._attr_available = False async def async_unlock(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index 4704c42762e560..c3f4e58e2471e8 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -98,21 +98,22 @@ def __init__( @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) - self._attr_volume_level = float(self.device.speaker_settings.volume / 100) + updated_device = self.device + self._attr_volume_level = float(updated_device.speaker_settings.volume / 100) if ( - self.device.talkback_stream is not None - and self.device.talkback_stream.is_running + updated_device.talkback_stream is not None + and updated_device.talkback_stream.is_running ): self._attr_state = MediaPlayerState.PLAYING else: self._attr_state = MediaPlayerState.IDLE is_connected = self.data.last_update_success and ( - self.device.state == StateType.CONNECTED - or (not self.device.is_adopted_by_us and self.device.can_adopt) + updated_device.state == StateType.CONNECTED + or (not updated_device.is_adopted_by_us and updated_device.can_adopt) ) - self._attr_available = is_connected and self.device.feature_flags.has_speaker + self._attr_available = is_connected and updated_device.feature_flags.has_speaker async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index 8c68823162810c..375784d0323e69 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -3,7 +3,6 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass -from datetime import timedelta from enum import Enum import logging from typing import Any, Generic, TypeVar, cast @@ -77,10 +76,8 @@ def get_event_obj(self, obj: T) -> Event | None: return cast(Event, get_nested_attr(obj, self.ufp_event_obj)) return None - def get_is_on(self, obj: T) -> bool: + def get_is_on(self, event: Event | None) -> bool: """Return value if event is active.""" - - event = self.get_event_obj(obj) if event is None: return False @@ -88,17 +85,7 @@ def get_is_on(self, obj: T) -> bool: value = now > event.start if value and event.end is not None and now > event.end: value = False - # only log if the recent ended recently - if event.end + timedelta(seconds=10) < now: - _LOGGER.debug( - "%s (%s): end ended at %s", - self.name, - obj.mac, - event.end.isoformat(), - ) - - if value: - _LOGGER.debug("%s (%s): value is on", self.name, obj.mac) + return value diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 753563023f4f47..26a03fb7967ced 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -356,15 +356,15 @@ def __init__( @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) - + entity_description = self.entity_description # entities with categories are not exposed for voice # and safe to update dynamically if ( - self.entity_description.entity_category is not None - and self.entity_description.ufp_options_fn is not None + entity_description.entity_category is not None + and entity_description.ufp_options_fn is not None ): _LOGGER.debug( - "Updating dynamic select options for %s", self.entity_description.name + "Updating dynamic select options for %s", entity_description.name ) self._async_set_options() diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index dec6f10a57f1da..d842b13b0151d3 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -710,15 +710,6 @@ class ProtectDeviceSensor(ProtectDeviceEntity, SensorEntity): entity_description: ProtectSensorEntityDescription - def __init__( - self, - data: ProtectData, - device: ProtectAdoptableDeviceModel, - description: ProtectSensorEntityDescription, - ) -> None: - """Initialize an UniFi Protect sensor.""" - super().__init__(data, device, description) - @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) @@ -730,15 +721,6 @@ class ProtectNVRSensor(ProtectNVREntity, SensorEntity): entity_description: ProtectSensorEntityDescription - def __init__( - self, - data: ProtectData, - device: NVR, - description: ProtectSensorEntityDescription, - ) -> None: - """Initialize an UniFi Protect sensor.""" - super().__init__(data, device, description) - @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) @@ -750,32 +732,22 @@ class ProtectEventSensor(EventEntityMixin, SensorEntity): entity_description: ProtectSensorEventEntityDescription - def __init__( - self, - data: ProtectData, - device: ProtectAdoptableDeviceModel, - description: ProtectSensorEventEntityDescription, - ) -> None: - """Initialize an UniFi Protect sensor.""" - super().__init__(data, device, description) - @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: # do not call ProtectDeviceSensor method since we want event to get value here EventEntityMixin._async_update_device_from_protect(self, device) - is_on = self.entity_description.get_is_on(device) + event = self._event + entity_description = self.entity_description + is_on = entity_description.get_is_on(event) is_license_plate = ( - self.entity_description.ufp_event_obj == "last_license_plate_detect_event" + entity_description.ufp_event_obj == "last_license_plate_detect_event" ) if ( not is_on - or self._event is None + or event is None or ( is_license_plate - and ( - self._event.metadata is None - or self._event.metadata.license_plate is None - ) + and (event.metadata is None or event.metadata.license_plate is None) ) ): self._attr_native_value = OBJECT_TYPE_NONE @@ -785,6 +757,6 @@ def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: if is_license_plate: # type verified above - self._attr_native_value = self._event.metadata.license_plate.name # type: ignore[union-attr] + self._attr_native_value = event.metadata.license_plate.name # type: ignore[union-attr] else: - self._attr_native_value = self._event.smart_detect_types[0].value + self._attr_native_value = event.smart_detect_types[0].value From 79c6b773da45c0d70ffc2c333b20bc54d4cf4153 Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Sun, 16 Jul 2023 21:19:04 +0200 Subject: [PATCH 0511/1009] IMAP service strings: Fix typo (#96711) Fix typo --- homeassistant/components/imap/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imap/strings.json b/homeassistant/components/imap/strings.json index 62579d61f5a94b..c332e3e8edbdd4 100644 --- a/homeassistant/components/imap/strings.json +++ b/homeassistant/components/imap/strings.json @@ -28,7 +28,7 @@ "invalid_charset": "The specified charset is not supported", "invalid_folder": "The selected folder is invalid", "invalid_search": "The selected search is invalid", - "ssl_error": "An SSL error occurred. Change SSL cipher list and try again" + "ssl_error": "An SSL error occurred. Change SSL cipher list and try again." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", @@ -43,12 +43,12 @@ "search": "[%key:component::imap::config::step::user::data::search%]", "custom_event_data_template": "Template to create custom event data", "max_message_size": "Max message size (2048 < size < 30000)", - "enable_push": "Enable Push-IMAP if the server supports it. Turn off if Push-IMAP updates are unreliable" + "enable_push": "Enable Push-IMAP if the server supports it. Turn off if Push-IMAP updates are unreliable." } } }, "error": { - "already_configured": "An entry with these folder and search options already exists", + "already_configured": "An entry with these folder and search options already exists.", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_charset": "[%key:component::imap::config::error::invalid_charset%]", From c34194d8e0c78404bb6743db3cfa796d15b0d53d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 16 Jul 2023 22:34:40 +0200 Subject: [PATCH 0512/1009] Use device class naming for BraviaTV (#96564) --- homeassistant/components/braviatv/button.py | 1 - homeassistant/components/braviatv/strings.json | 3 --- 2 files changed, 4 deletions(-) diff --git a/homeassistant/components/braviatv/button.py b/homeassistant/components/braviatv/button.py index b382d97a2aeed3..1f6c9961c5176b 100644 --- a/homeassistant/components/braviatv/button.py +++ b/homeassistant/components/braviatv/button.py @@ -36,7 +36,6 @@ class BraviaTVButtonDescription( BUTTONS: tuple[BraviaTVButtonDescription, ...] = ( BraviaTVButtonDescription( key="reboot", - translation_key="restart", device_class=ButtonDeviceClass.RESTART, entity_category=EntityCategory.CONFIG, press_action=lambda coordinator: coordinator.async_reboot_device(), diff --git a/homeassistant/components/braviatv/strings.json b/homeassistant/components/braviatv/strings.json index 30ad296554cd59..8f8e728cb9d049 100644 --- a/homeassistant/components/braviatv/strings.json +++ b/homeassistant/components/braviatv/strings.json @@ -47,9 +47,6 @@ }, "entity": { "button": { - "restart": { - "name": "[%key:component::button::entity_component::restart::name%]" - }, "terminate_apps": { "name": "Terminate apps" } From 4523105deeec44a5c3d3f15b0d68c1eadf39e4fe Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 16 Jul 2023 22:37:12 +0200 Subject: [PATCH 0513/1009] Migrate DuneHD to has entity name (#96568) --- homeassistant/components/dunehd/media_player.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py index a184b91c05eb6b..367eb6cb296f20 100644 --- a/homeassistant/components/dunehd/media_player.py +++ b/homeassistant/components/dunehd/media_player.py @@ -35,7 +35,7 @@ async def async_setup_entry( """Add Dune HD entities from a config_entry.""" unique_id = entry.entry_id - player: str = hass.data[DOMAIN][entry.entry_id] + player: DuneHDPlayer = hass.data[DOMAIN][entry.entry_id] async_add_entities([DuneHDPlayerEntity(player, DEFAULT_NAME, unique_id)], True) @@ -43,6 +43,9 @@ async def async_setup_entry( class DuneHDPlayerEntity(MediaPlayerEntity): """Implementation of the Dune HD player.""" + _attr_has_entity_name = True + _attr_name = None + def __init__(self, player: DuneHDPlayer, name: str, unique_id: str) -> None: """Initialize entity to control Dune HD.""" self._player = player @@ -70,11 +73,6 @@ def state(self) -> MediaPlayerState: state = MediaPlayerState.ON return state - @property - def name(self) -> str: - """Return the name of the device.""" - return self._name - @property def available(self) -> bool: """Return True if entity is available.""" @@ -91,7 +89,7 @@ def device_info(self) -> DeviceInfo: return DeviceInfo( identifiers={(DOMAIN, self._unique_id)}, manufacturer=ATTR_MANUFACTURER, - name=DEFAULT_NAME, + name=self._name, ) @property From 1e9a5e48c32ec055c4aa18f98128cb2d0f7baa55 Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Sun, 16 Jul 2023 23:02:37 +0200 Subject: [PATCH 0514/1009] Remove redundant phrase (#96716) --- homeassistant/components/counter/strings.json | 2 +- homeassistant/components/ezviz/strings.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/counter/strings.json b/homeassistant/components/counter/strings.json index 0446b2447872e0..53c87349836e0d 100644 --- a/homeassistant/components/counter/strings.json +++ b/homeassistant/components/counter/strings.json @@ -33,7 +33,7 @@ "step": { "confirm": { "title": "[%key:component::counter::issues::deprecated_configure_service::title%]", - "description": "The counter service `counter.configure` is being removed and use of it has been detected. If you want to change the current value of a counter, use the new `counter.set_value` service instead.\n\nPlease remove the use of this service from your automations and scripts and select **submit** to close this issue." + "description": "The counter service `counter.configure` is being removed and use of it has been detected. If you want to change the current value of a counter, use the new `counter.set_value` service instead.\n\nPlease remove this service from your automations and scripts and select **submit** to close this issue." } } } diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index f3e76c6748035e..94a73fc16cd257 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -66,7 +66,7 @@ "step": { "confirm": { "title": "[%key:component::ezviz::issues::service_depreciation_detection_sensibility::title%]", - "description": "The Ezviz Detection sensitivity service is deprecated and will be removed in Home Assistant 2023.12.\nTo set the sensitivity, you can instead use the `number.set_value` service targetting the Detection sensitivity entity.\n\nPlease remove the use of this service from your automations and scripts and select **submit** to close this issue." + "description": "The Ezviz Detection sensitivity service is deprecated and will be removed in Home Assistant 2023.12.\nTo set the sensitivity, you can instead use the `number.set_value` service targetting the Detection sensitivity entity.\n\nPlease remove this service from your automations and scripts and select **submit** to close this issue." } } } @@ -77,7 +77,7 @@ "step": { "confirm": { "title": "[%key:component::ezviz::issues::service_deprecation_alarm_sound_level::title%]", - "description": "Ezviz Alarm sound level service is deprecated and will be removed.\nTo set the Alarm sound level, you can instead use the `select.select_option` service targetting the Warning sound entity.\n\nPlease remove the use of this service from your automations and scripts and select **submit** to close this issue." + "description": "Ezviz Alarm sound level service is deprecated and will be removed.\nTo set the Alarm sound level, you can instead use the `select.select_option` service targetting the Warning sound entity.\n\nPlease remove this service from your automations and scripts and select **submit** to close this issue." } } } From 194d4e4f66d6307b6b158b1effd408d0ed6c44e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jul 2023 12:11:35 -1000 Subject: [PATCH 0515/1009] Guard type checking assertions in unifiprotect (#96721) --- homeassistant/components/unifiprotect/entity.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index fa85a0629cbe8d..a8a4c78465dc10 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -3,7 +3,7 @@ from collections.abc import Callable, Sequence import logging -from typing import Any +from typing import TYPE_CHECKING, Any from pyunifiprotect.data import ( NVR, @@ -57,7 +57,8 @@ def _async_device_entities( else data.get_by_types({model_type}, ignore_unadopted=False) ) for device in devices: - assert isinstance(device, (Camera, Light, Sensor, Viewer, Doorlock, Chime)) + if TYPE_CHECKING: + assert isinstance(device, (Camera, Light, Sensor, Viewer, Doorlock, Chime)) if not device.is_adopted_by_us: for description in unadopted_descs: entities.append( @@ -237,7 +238,8 @@ def _async_set_device_info(self) -> None: @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: """Update Entity object from Protect device.""" - assert isinstance(device, ProtectAdoptableDeviceModel) + if TYPE_CHECKING: + assert isinstance(device, ProtectAdoptableDeviceModel) if last_update_success := self.data.last_update_success: self.device = device From 33d2dd37977fc1152820ee2b82ffd6ace11a8b8c Mon Sep 17 00:00:00 2001 From: Robert Hafner Date: Sun, 16 Jul 2023 17:44:03 -0500 Subject: [PATCH 0516/1009] Airvisual Pro Outside Station Support (#96618) * Airvisual Pro Outside Station Support * pr feedback * formatting, language * Update homeassistant/components/airvisual_pro/strings.json Co-authored-by: Joost Lekkerkerker * fix assertion on airvisual test --------- Co-authored-by: Joost Lekkerkerker --- .../components/airvisual_pro/__init__.py | 4 ++ .../components/airvisual_pro/sensor.py | 38 ++++++++++++++----- .../components/airvisual_pro/strings.json | 7 ++++ .../airvisual_pro/test_diagnostics.py | 1 + 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/airvisual_pro/__init__.py b/homeassistant/components/airvisual_pro/__init__.py index b146651b6e6cbf..5bbbb0e895ddae 100644 --- a/homeassistant/components/airvisual_pro/__init__.py +++ b/homeassistant/components/airvisual_pro/__init__.py @@ -60,6 +60,10 @@ async def async_get_data() -> dict[str, Any]: """Get data from the device.""" try: data = await node.async_get_latest_measurements() + data["history"] = {} + if data["settings"].get("follow_mode") == "device": + history = await node.async_get_history(include_trends=False) + data["history"] = history.get("measurements", [])[-1] except InvalidAuthenticationError as err: raise ConfigEntryAuthFailed("Invalid Samba password") from err except NodeConnectionError as err: diff --git a/homeassistant/components/airvisual_pro/sensor.py b/homeassistant/components/airvisual_pro/sensor.py index 5f64e38c4a3c0a..69fbd1a128a092 100644 --- a/homeassistant/components/airvisual_pro/sensor.py +++ b/homeassistant/components/airvisual_pro/sensor.py @@ -30,7 +30,9 @@ class AirVisualProMeasurementKeyMixin: """Define an entity description mixin to include a measurement key.""" - value_fn: Callable[[dict[str, Any], dict[str, Any], dict[str, Any]], float | int] + value_fn: Callable[ + [dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any]], float | int + ] @dataclass @@ -45,29 +47,42 @@ class AirVisualProMeasurementDescription( key="air_quality_index", device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda settings, status, measurements: measurements[ + value_fn=lambda settings, status, measurements, history: measurements[ async_get_aqi_locale(settings) ], ), + AirVisualProMeasurementDescription( + key="outdoor_air_quality_index", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda settings, status, measurements, history: int( + history.get( + f'Outdoor {"AQI(US)" if settings["is_aqi_usa"] else "AQI(CN)"}', -1 + ) + ), + translation_key="outdoor_air_quality_index", + ), AirVisualProMeasurementDescription( key="battery_level", device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, - value_fn=lambda settings, status, measurements: status["battery"], + value_fn=lambda settings, status, measurements, history: status["battery"], ), AirVisualProMeasurementDescription( key="carbon_dioxide", device_class=SensorDeviceClass.CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda settings, status, measurements: measurements["co2"], + value_fn=lambda settings, status, measurements, history: measurements["co2"], ), AirVisualProMeasurementDescription( key="humidity", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, - value_fn=lambda settings, status, measurements: measurements["humidity"], + value_fn=lambda settings, status, measurements, history: measurements[ + "humidity" + ], ), AirVisualProMeasurementDescription( key="particulate_matter_0_1", @@ -75,7 +90,7 @@ class AirVisualProMeasurementDescription( device_class=SensorDeviceClass.PM1, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda settings, status, measurements: measurements["pm0_1"], + value_fn=lambda settings, status, measurements, history: measurements["pm0_1"], ), AirVisualProMeasurementDescription( key="particulate_matter_1_0", @@ -83,28 +98,30 @@ class AirVisualProMeasurementDescription( device_class=SensorDeviceClass.PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda settings, status, measurements: measurements["pm1_0"], + value_fn=lambda settings, status, measurements, history: measurements["pm1_0"], ), AirVisualProMeasurementDescription( key="particulate_matter_2_5", device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda settings, status, measurements: measurements["pm2_5"], + value_fn=lambda settings, status, measurements, history: measurements["pm2_5"], ), AirVisualProMeasurementDescription( key="temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda settings, status, measurements: measurements["temperature_C"], + value_fn=lambda settings, status, measurements, history: measurements[ + "temperature_C" + ], ), AirVisualProMeasurementDescription( key="voc", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda settings, status, measurements: measurements["voc"], + value_fn=lambda settings, status, measurements, history: measurements["voc"], ), ) @@ -143,4 +160,5 @@ def native_value(self) -> float | int: self.coordinator.data["settings"], self.coordinator.data["status"], self.coordinator.data["measurements"], + self.coordinator.data["history"], ) diff --git a/homeassistant/components/airvisual_pro/strings.json b/homeassistant/components/airvisual_pro/strings.json index f06f120885eed4..04801c8fa0e395 100644 --- a/homeassistant/components/airvisual_pro/strings.json +++ b/homeassistant/components/airvisual_pro/strings.json @@ -24,5 +24,12 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "entity": { + "sensor": { + "outdoor_air_quality_index": { + "name": "Outdoor air quality index" + } + } } } diff --git a/tests/components/airvisual_pro/test_diagnostics.py b/tests/components/airvisual_pro/test_diagnostics.py index 7f953946b69527..5141782e574420 100644 --- a/tests/components/airvisual_pro/test_diagnostics.py +++ b/tests/components/airvisual_pro/test_diagnostics.py @@ -33,6 +33,7 @@ async def test_entry_diagnostics( "time": "16:00:44", "timestamp": "1665072044", }, + "history": {}, "measurements": { "co2": "472", "humidity": "57", From d553a749a018121456c6963c8409412a4c6b3853 Mon Sep 17 00:00:00 2001 From: Renier Moorcroft <66512715+RenierM26@users.noreply.github.com> Date: Mon, 17 Jul 2023 08:30:17 +0200 Subject: [PATCH 0517/1009] Ezviz image entity cleanup (#96548) * Update image.py * Inheratance format --- homeassistant/components/ezviz/image.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/ezviz/image.py b/homeassistant/components/ezviz/image.py index a5dbdea1d6f8ea..9bc65f12355f40 100644 --- a/homeassistant/components/ezviz/image.py +++ b/homeassistant/components/ezviz/image.py @@ -21,7 +21,6 @@ from .entity import EzvizEntity _LOGGER = logging.getLogger(__name__) -GET_IMAGE_TIMEOUT = 10 IMAGE_TYPE = ImageEntityDescription( key="last_motion_image", @@ -52,7 +51,7 @@ def __init__( self, hass: HomeAssistant, coordinator: EzvizDataUpdateCoordinator, serial: str ) -> None: """Initialize a image entity.""" - super().__init__(coordinator, serial) + EzvizEntity.__init__(self, coordinator, serial) ImageEntity.__init__(self, hass) self._attr_unique_id = f"{serial}_{IMAGE_TYPE.key}" self.entity_description = IMAGE_TYPE From d242eaa37524b8a4f07884b39c3dec6fd8f097c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jul 2023 20:41:39 -1000 Subject: [PATCH 0518/1009] Remove the ability to defer websocket message construction (#96734) This was added in #71364 but all use cases of it were refactored away so it can now be removed --- homeassistant/components/websocket_api/auth.py | 2 +- homeassistant/components/websocket_api/connection.py | 2 +- homeassistant/components/websocket_api/http.py | 11 +++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py index d0831f2e90eee1..9f8e8bfb6f826d 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -57,7 +57,7 @@ def __init__( self, logger: WebSocketAdapter, hass: HomeAssistant, - send_message: Callable[[str | dict[str, Any] | Callable[[], str]], None], + send_message: Callable[[str | dict[str, Any]], None], cancel_ws: CALLBACK_TYPE, request: Request, ) -> None: diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index a554001970b5ac..f598906661c86e 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -51,7 +51,7 @@ def __init__( self, logger: WebSocketAdapter, hass: HomeAssistant, - send_message: Callable[[str | dict[str, Any] | Callable[[], str]], None], + send_message: Callable[[str | dict[str, Any]], None], user: User, refresh_token: RefreshToken, ) -> None: diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 728405b5d969e5..fcaa13ff8dec18 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -95,7 +95,7 @@ def __init__(self, hass: HomeAssistant, request: web.Request) -> None: # to where messages are queued. This allows the implementation # to use a deque and an asyncio.Future to avoid the overhead of # an asyncio.Queue. - self._message_queue: deque[str | Callable[[], str] | None] = deque() + self._message_queue: deque[str | None] = deque() self._ready_future: asyncio.Future[None] | None = None def __repr__(self) -> str: @@ -136,12 +136,11 @@ async def _writer(self) -> None: messages_remaining = len(message_queue) # A None message is used to signal the end of the connection - if (process := message_queue.popleft()) is None: + if (message := message_queue.popleft()) is None: return debug_enabled = is_enabled_for(logging_debug) messages_remaining -= 1 - message = process if isinstance(process, str) else process() if ( not messages_remaining @@ -156,9 +155,9 @@ async def _writer(self) -> None: messages: list[str] = [message] while messages_remaining: # A None message is used to signal the end of the connection - if (process := message_queue.popleft()) is None: + if (message := message_queue.popleft()) is None: return - messages.append(process if isinstance(process, str) else process()) + messages.append(message) messages_remaining -= 1 joined_messages = ",".join(messages) @@ -184,7 +183,7 @@ def _cancel_peak_checker(self) -> None: self._peak_checker_unsub = None @callback - def _send_message(self, message: str | dict[str, Any] | Callable[[], str]) -> None: + def _send_message(self, message: str | dict[str, Any]) -> None: """Send a message to the client. Closes connection if the client is not reading the messages. From 51a7df162ce5b4f2c5455c262593cca3e6b42d5a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jul 2023 20:42:46 -1000 Subject: [PATCH 0519/1009] Avoid regenerating the mobile app schema every time a webhook is called (#96733) Avoid regnerating the mobile app schema every time a webhook is called --- .../components/mobile_app/webhook.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 90e244aaf06af9..62417b0873a9a2 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -144,6 +144,16 @@ ), ) +SENSOR_SCHEMA_FULL = vol.Schema( + { + vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, + vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): vol.Any(None, cv.icon), + vol.Required(ATTR_SENSOR_STATE): vol.Any(None, bool, int, float, str), + vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), + vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, + } +) + def validate_schema(schema): """Decorate a webhook function with a schema.""" @@ -636,18 +646,6 @@ async def webhook_update_sensor_states( hass: HomeAssistant, config_entry: ConfigEntry, data: list[dict[str, Any]] ) -> Response: """Handle an update sensor states webhook.""" - sensor_schema_full = vol.Schema( - { - vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, - vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): vol.Any( - None, cv.icon - ), - vol.Required(ATTR_SENSOR_STATE): vol.Any(None, bool, int, float, str), - vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), - vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, - } - ) - device_name: str = config_entry.data[ATTR_DEVICE_NAME] resp: dict[str, Any] = {} entity_registry = er.async_get(hass) @@ -677,7 +675,7 @@ async def webhook_update_sensor_states( continue try: - sensor = sensor_schema_full(sensor) + sensor = SENSOR_SCHEMA_FULL(sensor) except vol.Invalid as err: err_msg = vol.humanize.humanize_error(sensor, err) _LOGGER.error( From 260e00ffb4c0229afba42096e99a2ce0de087bbd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jul 2023 20:50:06 -1000 Subject: [PATCH 0520/1009] Check the registry entry in sensor unit_of_measurement instead of unique_id (#96731) The unit_of_measurement check was checking to see if the entity has a unique_id instead of a registry entry. Its much cheaper to check for the registry_entry than the unique id since some entity have to construct it every time its read --- homeassistant/components/sensor/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 2477e849666008..e8303c12c10dc4 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -453,7 +453,7 @@ def unit_of_measurement(self) -> str | None: return self._sensor_option_unit_of_measurement # Second priority, for non registered entities: unit suggested by integration - if not self.unique_id and ( + if not self.registry_entry and ( suggested_unit_of_measurement := self.suggested_unit_of_measurement ): return suggested_unit_of_measurement From 085eebc903f842dd76000c8673fead77feaf14c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jul 2023 20:58:12 -1000 Subject: [PATCH 0521/1009] Make async_set_state in ConfigEntry a protected method (#96727) I added this in #77803 but I never designed it to be called externally. External usage may break at any time because the class is not designed for this. I should have made it protected in the original PR but I did not think it would get called externally (my mistake) --- homeassistant/components/imap/coordinator.py | 2 +- homeassistant/config_entries.py | 32 +++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index bf7f173e647659..b13a861fa79289 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -373,7 +373,7 @@ async def _async_wait_push_loop(self) -> None: except InvalidFolder as ex: _LOGGER.warning("Selected mailbox folder is invalid") await self._cleanup() - self.config_entry.async_set_state( + self.config_entry._async_set_state( # pylint: disable=protected-access self.hass, ConfigEntryState.SETUP_ERROR, "Selected mailbox folder is invalid.", diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 9e27f6efb3e995..825064e541097f 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -338,7 +338,7 @@ async def async_setup( # Only store setup result as state if it was not forwarded. if self.domain == integration.domain: - self.async_set_state(hass, ConfigEntryState.SETUP_IN_PROGRESS, None) + self._async_set_state(hass, ConfigEntryState.SETUP_IN_PROGRESS, None) if self.supports_unload is None: self.supports_unload = await support_entry_unload(hass, self.domain) @@ -357,7 +357,9 @@ async def async_setup( err, ) if self.domain == integration.domain: - self.async_set_state(hass, ConfigEntryState.SETUP_ERROR, "Import error") + self._async_set_state( + hass, ConfigEntryState.SETUP_ERROR, "Import error" + ) return if self.domain == integration.domain: @@ -373,12 +375,14 @@ async def async_setup( self.domain, err, ) - self.async_set_state(hass, ConfigEntryState.SETUP_ERROR, "Import error") + self._async_set_state( + hass, ConfigEntryState.SETUP_ERROR, "Import error" + ) return # Perform migration if not await self.async_migrate(hass): - self.async_set_state(hass, ConfigEntryState.MIGRATION_ERROR, None) + self._async_set_state(hass, ConfigEntryState.MIGRATION_ERROR, None) return error_reason = None @@ -418,7 +422,7 @@ async def async_setup( self.async_start_reauth(hass) result = False except ConfigEntryNotReady as ex: - self.async_set_state(hass, ConfigEntryState.SETUP_RETRY, str(ex) or None) + self._async_set_state(hass, ConfigEntryState.SETUP_RETRY, str(ex) or None) wait_time = 2 ** min(tries, 4) * 5 + ( randint(RANDOM_MICROSECOND_MIN, RANDOM_MICROSECOND_MAX) / 1000000 ) @@ -479,9 +483,9 @@ async def setup_again(*_: Any) -> None: return if result: - self.async_set_state(hass, ConfigEntryState.LOADED, None) + self._async_set_state(hass, ConfigEntryState.LOADED, None) else: - self.async_set_state(hass, ConfigEntryState.SETUP_ERROR, error_reason) + self._async_set_state(hass, ConfigEntryState.SETUP_ERROR, error_reason) async def async_shutdown(self) -> None: """Call when Home Assistant is stopping.""" @@ -502,7 +506,7 @@ async def async_unload( Returns if unload is possible and was successful. """ if self.source == SOURCE_IGNORE: - self.async_set_state(hass, ConfigEntryState.NOT_LOADED, None) + self._async_set_state(hass, ConfigEntryState.NOT_LOADED, None) return True if self.state == ConfigEntryState.NOT_LOADED: @@ -516,7 +520,7 @@ async def async_unload( # that was uninstalled, or an integration # that has been renamed without removing the config # entry. - self.async_set_state(hass, ConfigEntryState.NOT_LOADED, None) + self._async_set_state(hass, ConfigEntryState.NOT_LOADED, None) return True component = integration.get_component() @@ -527,14 +531,14 @@ async def async_unload( if self.state is not ConfigEntryState.LOADED: self.async_cancel_retry_setup() - self.async_set_state(hass, ConfigEntryState.NOT_LOADED, None) + self._async_set_state(hass, ConfigEntryState.NOT_LOADED, None) return True supports_unload = hasattr(component, "async_unload_entry") if not supports_unload: if integration.domain == self.domain: - self.async_set_state( + self._async_set_state( hass, ConfigEntryState.FAILED_UNLOAD, "Unload not supported" ) return False @@ -546,7 +550,7 @@ async def async_unload( # Only adjust state if we unloaded the component if result and integration.domain == self.domain: - self.async_set_state(hass, ConfigEntryState.NOT_LOADED, None) + self._async_set_state(hass, ConfigEntryState.NOT_LOADED, None) await self._async_process_on_unload(hass) @@ -556,7 +560,7 @@ async def async_unload( "Error unloading entry %s for %s", self.title, integration.domain ) if integration.domain == self.domain: - self.async_set_state( + self._async_set_state( hass, ConfigEntryState.FAILED_UNLOAD, str(ex) or "Unknown error" ) return False @@ -588,7 +592,7 @@ async def async_remove(self, hass: HomeAssistant) -> None: ) @callback - def async_set_state( + def _async_set_state( self, hass: HomeAssistant, state: ConfigEntryState, reason: str | None ) -> None: """Set the state of the config entry.""" From 79bcca2853827adbdd36972c2f7a945bb1dd4e76 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 17 Jul 2023 07:02:42 +0000 Subject: [PATCH 0522/1009] Add wellness sensors to Tractive integration (#96719) * Add sleep sensors * Add minutes rest sensor * Add calories sensor * Add state_class to entity descriptions --- homeassistant/components/tractive/__init__.py | 19 ++++++ homeassistant/components/tractive/const.py | 7 +- homeassistant/components/tractive/sensor.py | 68 ++++++++++++++++++- .../components/tractive/strings.json | 12 ++++ 4 files changed, 102 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index 96fc718c67d3ae..351b39f61e76ab 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -24,10 +24,14 @@ from .const import ( ATTR_BUZZER, + ATTR_CALORIES, ATTR_DAILY_GOAL, ATTR_LED, ATTR_LIVE_TRACKING, ATTR_MINUTES_ACTIVE, + ATTR_MINUTES_DAY_SLEEP, + ATTR_MINUTES_NIGHT_SLEEP, + ATTR_MINUTES_REST, ATTR_TRACKER_STATE, CLIENT, CLIENT_ID, @@ -38,6 +42,7 @@ TRACKER_ACTIVITY_STATUS_UPDATED, TRACKER_HARDWARE_STATUS_UPDATED, TRACKER_POSITION_UPDATED, + TRACKER_WELLNESS_STATUS_UPDATED, ) PLATFORMS = [ @@ -202,6 +207,9 @@ async def _listen(self) -> None: if event["message"] == "activity_update": self._send_activity_update(event) continue + if event["message"] == "wellness_overview": + self._send_wellness_update(event) + continue if ( "hardware" in event and self._last_hw_time != event["hardware"]["time"] @@ -264,6 +272,17 @@ def _send_activity_update(self, event: dict[str, Any]) -> None: TRACKER_ACTIVITY_STATUS_UPDATED, event["pet_id"], payload ) + def _send_wellness_update(self, event: dict[str, Any]) -> None: + payload = { + ATTR_CALORIES: event["activity"]["calories"], + ATTR_MINUTES_DAY_SLEEP: event["sleep"]["minutes_day_sleep"], + ATTR_MINUTES_NIGHT_SLEEP: event["sleep"]["minutes_night_sleep"], + ATTR_MINUTES_REST: event["activity"]["minutes_rest"], + } + self._dispatch_tracker_event( + TRACKER_WELLNESS_STATUS_UPDATED, event["pet_id"], payload + ) + def _send_position_update(self, event: dict[str, Any]) -> None: payload = { "latitude": event["position"]["latlong"][0], diff --git a/homeassistant/components/tractive/const.py b/homeassistant/components/tractive/const.py index a87e22c505d353..81936ae5d80ff0 100644 --- a/homeassistant/components/tractive/const.py +++ b/homeassistant/components/tractive/const.py @@ -6,11 +6,15 @@ RECONNECT_INTERVAL = timedelta(seconds=10) -ATTR_DAILY_GOAL = "daily_goal" ATTR_BUZZER = "buzzer" +ATTR_CALORIES = "calories" +ATTR_DAILY_GOAL = "daily_goal" ATTR_LED = "led" ATTR_LIVE_TRACKING = "live_tracking" ATTR_MINUTES_ACTIVE = "minutes_active" +ATTR_MINUTES_DAY_SLEEP = "minutes_day_sleep" +ATTR_MINUTES_NIGHT_SLEEP = "minutes_night_sleep" +ATTR_MINUTES_REST = "minutes_rest" ATTR_TRACKER_STATE = "tracker_state" # This client ID was issued by Tractive specifically for Home Assistant. @@ -23,5 +27,6 @@ TRACKER_HARDWARE_STATUS_UPDATED = f"{DOMAIN}_tracker_hardware_status_updated" TRACKER_POSITION_UPDATED = f"{DOMAIN}_tracker_position_updated" TRACKER_ACTIVITY_STATUS_UPDATED = f"{DOMAIN}_tracker_activity_updated" +TRACKER_WELLNESS_STATUS_UPDATED = f"{DOMAIN}_tracker_wellness_updated" SERVER_UNAVAILABLE = f"{DOMAIN}_server_unavailable" diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index 24439b489c8a73..8f56d1a2e9c52d 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -8,6 +8,7 @@ SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -22,8 +23,12 @@ from . import Trackables from .const import ( + ATTR_CALORIES, ATTR_DAILY_GOAL, ATTR_MINUTES_ACTIVE, + ATTR_MINUTES_DAY_SLEEP, + ATTR_MINUTES_NIGHT_SLEEP, + ATTR_MINUTES_REST, ATTR_TRACKER_STATE, CLIENT, DOMAIN, @@ -31,6 +36,7 @@ TRACKABLES, TRACKER_ACTIVITY_STATUS_UPDATED, TRACKER_HARDWARE_STATUS_UPDATED, + TRACKER_WELLNESS_STATUS_UPDATED, ) from .entity import TractiveEntity @@ -107,8 +113,8 @@ class TractiveActivitySensor(TractiveSensor): """Tractive active sensor.""" @callback - def handle_activity_status_update(self, event: dict[str, Any]) -> None: - """Handle activity status update.""" + def handle_status_update(self, event: dict[str, Any]) -> None: + """Handle status update.""" self._attr_native_value = event[self.entity_description.key] self._attr_available = True self.async_write_ha_state() @@ -120,7 +126,30 @@ async def async_added_to_hass(self) -> None: async_dispatcher_connect( self.hass, f"{TRACKER_ACTIVITY_STATUS_UPDATED}-{self._trackable['_id']}", - self.handle_activity_status_update, + self.handle_status_update, + ) + ) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SERVER_UNAVAILABLE}-{self._user_id}", + self.handle_server_unavailable, + ) + ) + + +class TractiveWellnessSensor(TractiveActivitySensor): + """Tractive wellness sensor.""" + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{TRACKER_WELLNESS_STATUS_UPDATED}-{self._trackable['_id']}", + self.handle_status_update, ) ) @@ -155,6 +184,23 @@ async def async_added_to_hass(self) -> None: icon="mdi:clock-time-eight-outline", native_unit_of_measurement=UnitOfTime.MINUTES, entity_class=TractiveActivitySensor, + state_class=SensorStateClass.TOTAL, + ), + TractiveSensorEntityDescription( + key=ATTR_MINUTES_REST, + translation_key="minutes_rest", + icon="mdi:clock-time-eight-outline", + native_unit_of_measurement=UnitOfTime.MINUTES, + entity_class=TractiveWellnessSensor, + state_class=SensorStateClass.TOTAL, + ), + TractiveSensorEntityDescription( + key=ATTR_CALORIES, + translation_key="calories", + icon="mdi:fire", + native_unit_of_measurement="kcal", + entity_class=TractiveWellnessSensor, + state_class=SensorStateClass.TOTAL, ), TractiveSensorEntityDescription( key=ATTR_DAILY_GOAL, @@ -163,6 +209,22 @@ async def async_added_to_hass(self) -> None: native_unit_of_measurement=UnitOfTime.MINUTES, entity_class=TractiveActivitySensor, ), + TractiveSensorEntityDescription( + key=ATTR_MINUTES_DAY_SLEEP, + translation_key="minutes_day_sleep", + icon="mdi:sleep", + native_unit_of_measurement=UnitOfTime.MINUTES, + entity_class=TractiveWellnessSensor, + state_class=SensorStateClass.TOTAL, + ), + TractiveSensorEntityDescription( + key=ATTR_MINUTES_NIGHT_SLEEP, + translation_key="minutes_night_sleep", + icon="mdi:sleep", + native_unit_of_measurement=UnitOfTime.MINUTES, + entity_class=TractiveWellnessSensor, + state_class=SensorStateClass.TOTAL, + ), ) diff --git a/homeassistant/components/tractive/strings.json b/homeassistant/components/tractive/strings.json index d5aee51ed613ae..44b0a497881072 100644 --- a/homeassistant/components/tractive/strings.json +++ b/homeassistant/components/tractive/strings.json @@ -30,12 +30,24 @@ } }, "sensor": { + "calories": { + "name": "Calories burned" + }, "daily_goal": { "name": "Daily goal" }, "minutes_active": { "name": "Minutes active" }, + "minutes_day_sleep": { + "name": "Day sleep" + }, + "minutes_night_sleep": { + "name": "Night sleep" + }, + "minutes_rest": { + "name": "Minutes rest" + }, "tracker_battery_level": { "name": "Tracker battery" }, From 3a043655b9f6540d87c1aaeef76df1b3378b6273 Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Mon, 17 Jul 2023 09:03:25 +0200 Subject: [PATCH 0523/1009] Vacuum services strings: rename 'base' to 'dock' for consistency (#96715) --- homeassistant/components/vacuum/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index 9822c2fa8216a5..73e50af5caae74 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -65,7 +65,7 @@ "description": "Pauses the cleaning task." }, "return_to_base": { - "name": "Return to base", + "name": "Return to dock", "description": "Tells the vacuum cleaner to return to its dock." }, "clean_spot": { @@ -74,7 +74,7 @@ }, "send_command": { "name": "Send command", - "description": "Sends a raw command to the vacuum cleaner.", + "description": "Sends a command to the vacuum cleaner.", "fields": { "command": { "name": "Command", From f809b7284be577386c2f292007168a470285bcba Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 17 Jul 2023 07:04:43 +0000 Subject: [PATCH 0524/1009] Create Tractive battery charging sensor if `charging_state` is not `None` (#96713) Check if charging_state is available --- homeassistant/components/tractive/binary_sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/tractive/binary_sensor.py b/homeassistant/components/tractive/binary_sensor.py index cb4abc9b385a0d..d7968f15bf8580 100644 --- a/homeassistant/components/tractive/binary_sensor.py +++ b/homeassistant/components/tractive/binary_sensor.py @@ -24,8 +24,6 @@ ) from .entity import TractiveEntity -TRACKERS_WITH_BUILTIN_BATTERY = ("TRNJA4", "TRAXL1") - class TractiveBinarySensor(TractiveEntity, BinarySensorEntity): """Tractive sensor.""" @@ -90,7 +88,7 @@ async def async_setup_entry( entities = [ TractiveBinarySensor(client.user_id, item, SENSOR_TYPE) for item in trackables - if item.tracker_details["model_number"] in TRACKERS_WITH_BUILTIN_BATTERY + if item.tracker_details.get("charging_state") is not None ] async_add_entities(entities) From 9496b651a8e1260e6905d951d027eb5db5f3b945 Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Mon, 17 Jul 2023 09:08:27 +0200 Subject: [PATCH 0525/1009] Small tweaks to ZHA service strings (#96709) --- homeassistant/components/zha/strings.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 1e44191a762e0f..9731fb0c2d1c7c 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -425,8 +425,8 @@ } }, "warning_device_warn": { - "name": "Warning device warn", - "description": "This service starts the WD operation. The WD alerts the surrounding area by audible (siren) and visual (strobe) signals.", + "name": "Warning device starts alert", + "description": "This service starts the operation of the warning device. The warning device alerts the surrounding area by audible (siren) and visual (strobe) signals.", "fields": { "ieee": { "name": "[%key:component::zha::services::permit::fields::ieee::name%]", @@ -434,11 +434,11 @@ }, "mode": { "name": "[%key:common::config_flow::data::mode%]", - "description": "The Warning Mode field is used as an 4-bit enumeration, can have one of the values 0-6 defined below in table 8-20 of the ZCL spec. The exact behavior of the WD device in each mode is according to the relevant security standards." + "description": "The Warning Mode field is used as a 4-bit enumeration, can have one of the values 0-6 defined below in table 8-20 of the ZCL spec. The exact behavior of the warning device in each mode is according to the relevant security standards." }, "strobe": { "name": "[%key:component::zha::services::warning_device_squawk::fields::strobe::name%]", - "description": "The Strobe field is used as a 2-bit enumeration, and determines if the visual indication is required in addition to the audible siren, as indicated in Table 8-21 of the ZCL spec. \"0\" means no strobe, \"1\" means strobe. If the strobe field is “1” and the Warning Mode is “0” (“Stop”) then only the strobe is activated." + "description": "The Strobe field is used as a 2-bit enumeration, and determines if the visual indication is required in addition to the audible siren, as indicated in Table 8-21 of the ZCL spec. \"0\" means no strobe, \"1\" means strobe. If the strobe field is “1” and the Warning Mode is “0” (“Stop”), then only the strobe is activated." }, "level": { "name": "Level", @@ -446,11 +446,11 @@ }, "duration": { "name": "Duration", - "description": "Requested duration of warning, in seconds (16 bit). If both Strobe and Warning Mode are \"0\" this field SHALL be ignored." + "description": "Requested duration of warning, in seconds (16 bit). If both Strobe and Warning Mode are \"0\" this field is ignored." }, "duty_cycle": { "name": "Duty cycle", - "description": "Indicates the length of the flash cycle. This allows you to vary the flash duration for different alarm types (e.g., fire, police, burglar). The valid range is 0-100 in increments of 10. All other values must be rounded to the nearest valid value. Strobe calculates a duty cycle over a duration of one second. The ON state must precede the OFF state. For example, if Strobe Duty Cycle Field specifies “40,”, then the strobe flashes ON for 4/10ths of a second and then turns OFF for 6/10ths of a second." + "description": "Indicates the length of the flash cycle. This allows you to vary the flash duration for different alarm types (e.g., fire, police, burglar). The valid range is 0-100 in increments of 10. All other values must be rounded to the nearest valid value. Strobe calculates a duty cycle over a duration of one second. The ON state must precede the OFF state. For example, if the Strobe Duty Cycle field specifies “40,”, then the strobe flashes ON for 4/10ths of a second and then turns OFF for 6/10ths of a second." }, "intensity": { "name": "Intensity", From 13140830a013b641bc7a655feceb86a167836397 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 09:09:53 +0200 Subject: [PATCH 0526/1009] Migrate Monoprice to has entity name (#96704) --- homeassistant/components/monoprice/media_player.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 52e33da54ed012..5a61e306991a21 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -125,6 +125,8 @@ class MonopriceZone(MediaPlayerEntity): | MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.SELECT_SOURCE ) + _attr_has_entity_name = True + _attr_name = None def __init__(self, monoprice, sources, namespace, zone_id): """Initialize new zone.""" @@ -137,12 +139,11 @@ def __init__(self, monoprice, sources, namespace, zone_id): self._attr_source_list = sources[2] self._zone_id = zone_id self._attr_unique_id = f"{namespace}_{self._zone_id}" - self._attr_name = f"Zone {self._zone_id}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._attr_unique_id)}, manufacturer="Monoprice", model="6-Zone Amplifier", - name=self.name, + name=f"Zone {self._zone_id}", ) self._snapshot = None From 13ac8d00f9be3325001ed88f351ff75d746089d2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 09:11:02 +0200 Subject: [PATCH 0527/1009] Migrate Laundrify to has entity name (#96703) --- homeassistant/components/laundrify/binary_sensor.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/laundrify/binary_sensor.py b/homeassistant/components/laundrify/binary_sensor.py index fb9bac987bb09c..3e865bd4c0cae1 100644 --- a/homeassistant/components/laundrify/binary_sensor.py +++ b/homeassistant/components/laundrify/binary_sensor.py @@ -42,6 +42,8 @@ class LaundrifyPowerPlug( _attr_device_class = BinarySensorDeviceClass.RUNNING _attr_icon = "mdi:washing-machine" _attr_unique_id: str + _attr_has_entity_name = True + _attr_name = None def __init__( self, coordinator: LaundrifyUpdateCoordinator, device: LaundrifyDevice @@ -56,7 +58,7 @@ def device_info(self) -> DeviceInfo: """Configure the Device of this Entity.""" return DeviceInfo( identifiers={(DOMAIN, self._device["_id"])}, - name=self.name, + name=self._device["name"], manufacturer=MANUFACTURER, model=MODEL, sw_version=self._device["firmwareVersion"], @@ -70,11 +72,6 @@ def available(self) -> bool: and self.coordinator.last_update_success ) - @property - def name(self) -> str: - """Name of the entity.""" - return self._device["name"] - @property def is_on(self) -> bool: """Return entity state.""" From 088d04fe0f6ada834779e00c730f917ceb64cc04 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 17 Jul 2023 09:11:23 +0200 Subject: [PATCH 0528/1009] Add sensor to gardena (#96691) --- .coveragerc | 1 + .../components/gardena_bluetooth/__init__.py | 2 +- .../gardena_bluetooth/coordinator.py | 2 +- .../components/gardena_bluetooth/sensor.py | 125 ++++++++++++++++++ .../components/gardena_bluetooth/strings.json | 8 ++ 5 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/gardena_bluetooth/sensor.py diff --git a/.coveragerc b/.coveragerc index 52350a498d91d6..d160efb776c597 100644 --- a/.coveragerc +++ b/.coveragerc @@ -410,6 +410,7 @@ omit = homeassistant/components/gardena_bluetooth/const.py homeassistant/components/gardena_bluetooth/coordinator.py homeassistant/components/gardena_bluetooth/number.py + homeassistant/components/gardena_bluetooth/sensor.py homeassistant/components/gardena_bluetooth/switch.py homeassistant/components/gc100/* homeassistant/components/geniushub/* diff --git a/homeassistant/components/gardena_bluetooth/__init__.py b/homeassistant/components/gardena_bluetooth/__init__.py index 2954a5fe377029..98869019d29588 100644 --- a/homeassistant/components/gardena_bluetooth/__init__.py +++ b/homeassistant/components/gardena_bluetooth/__init__.py @@ -20,7 +20,7 @@ from .const import DOMAIN from .coordinator import Coordinator, DeviceUnavailable -PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SWITCH] +PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH] LOGGER = logging.getLogger(__name__) TIMEOUT = 20.0 DISCONNECT_DELAY = 5 diff --git a/homeassistant/components/gardena_bluetooth/coordinator.py b/homeassistant/components/gardena_bluetooth/coordinator.py index fa7639dece0177..997c78d0f00556 100644 --- a/homeassistant/components/gardena_bluetooth/coordinator.py +++ b/homeassistant/components/gardena_bluetooth/coordinator.py @@ -80,7 +80,7 @@ async def _async_update_data(self) -> dict[str, bytes]: ) from exception return data - def read_cached( + def get_cached( self, char: Characteristic[CharacteristicType] ) -> CharacteristicType | None: """Read cached characteristic.""" diff --git a/homeassistant/components/gardena_bluetooth/sensor.py b/homeassistant/components/gardena_bluetooth/sensor.py new file mode 100644 index 00000000000000..0c8558419e2535 --- /dev/null +++ b/homeassistant/components/gardena_bluetooth/sensor.py @@ -0,0 +1,125 @@ +"""Support for switch entities.""" +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import datetime, timedelta, timezone + +from gardena_bluetooth.const import Battery, Valve +from gardena_bluetooth.parse import Characteristic + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.util.dt as dt_util + +from .const import DOMAIN +from .coordinator import Coordinator, GardenaBluetoothEntity + + +@dataclass +class GardenaBluetoothSensorEntityDescription(SensorEntityDescription): + """Description of entity.""" + + char: Characteristic = field(default_factory=lambda: Characteristic("")) + + +DESCRIPTIONS = ( + GardenaBluetoothSensorEntityDescription( + key=Valve.activation_reason.uuid, + translation_key="activation_reason", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + char=Valve.activation_reason, + ), + GardenaBluetoothSensorEntityDescription( + key=Battery.battery_level.uuid, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + char=Battery.battery_level, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Gardena Bluetooth sensor based on a config entry.""" + coordinator: Coordinator = hass.data[DOMAIN][entry.entry_id] + entities: list[GardenaBluetoothEntity] = [ + GardenaBluetoothSensor(coordinator, description) + for description in DESCRIPTIONS + if description.key in coordinator.characteristics + ] + entities.append(GardenaBluetoothRemainSensor(coordinator)) + async_add_entities(entities) + + +class GardenaBluetoothSensor(GardenaBluetoothEntity, SensorEntity): + """Representation of a sensor.""" + + entity_description: GardenaBluetoothSensorEntityDescription + + def __init__( + self, + coordinator: Coordinator, + description: GardenaBluetoothSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator, {description.key}) + self._attr_native_value = None + self._attr_unique_id = f"{coordinator.address}-{description.key}" + self.entity_description = description + + def _handle_coordinator_update(self) -> None: + value = self.coordinator.get_cached(self.entity_description.char) + if isinstance(value, datetime): + value = value.replace( + tzinfo=dt_util.get_time_zone(self.hass.config.time_zone) + ) + self._attr_native_value = value + super()._handle_coordinator_update() + + +class GardenaBluetoothRemainSensor(GardenaBluetoothEntity, SensorEntity): + """Representation of a sensor.""" + + _attr_device_class = SensorDeviceClass.TIMESTAMP + _attr_native_value: datetime | None = None + _attr_translation_key = "remaining_open_timestamp" + + def __init__( + self, + coordinator: Coordinator, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator, {Valve.remaining_open_time.uuid}) + self._attr_unique_id = f"{coordinator.address}-remaining_open_timestamp" + + def _handle_coordinator_update(self) -> None: + value = self.coordinator.get_cached(Valve.remaining_open_time) + if not value: + self._attr_native_value = None + super()._handle_coordinator_update() + return + + time = datetime.now(timezone.utc) + timedelta(seconds=value) + if not self._attr_native_value: + self._attr_native_value = time + super()._handle_coordinator_update() + return + + error = time - self._attr_native_value + if abs(error.total_seconds()) > 10: + self._attr_native_value = time + super()._handle_coordinator_update() + return diff --git a/homeassistant/components/gardena_bluetooth/strings.json b/homeassistant/components/gardena_bluetooth/strings.json index 0a9677b1f92e88..3548412e04f19b 100644 --- a/homeassistant/components/gardena_bluetooth/strings.json +++ b/homeassistant/components/gardena_bluetooth/strings.json @@ -36,6 +36,14 @@ "name": "Season pause" } }, + "sensor": { + "activation_reason": { + "name": "Activation reason" + }, + "remaining_open_timestamp": { + "name": "Valve closing" + } + }, "switch": { "state": { "name": "[%key:common::state::open%]" From f0fb09c2be401202be754327bebb582a9b52ad34 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 09:12:07 +0200 Subject: [PATCH 0529/1009] Migrate Kulersky to has entity name (#96702) --- homeassistant/components/kulersky/light.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index c6763e6d9f677e..91f19dbdd08c4e 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -62,7 +62,10 @@ async def discover(*args): class KulerskyLight(LightEntity): - """Representation of an Kuler Sky Light.""" + """Representation of a Kuler Sky Light.""" + + _attr_has_entity_name = True + _attr_name = None def __init__(self, light: pykulersky.Light) -> None: """Initialize a Kuler Sky light.""" @@ -88,11 +91,6 @@ async def async_will_remove_from_hass(self, *args) -> None: "Exception disconnected from %s", self._light.address, exc_info=True ) - @property - def name(self): - """Return the display name of this light.""" - return self._light.name - @property def unique_id(self): """Return the ID of this light.""" @@ -104,7 +102,7 @@ def device_info(self) -> DeviceInfo: return DeviceInfo( identifiers={(DOMAIN, self.unique_id)}, manufacturer="Brightech", - name=self.name, + name=self._light.name, ) @property From bd22cfc1d0b6dd75e6b67654adc7f42d536095ad Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 09:14:02 +0200 Subject: [PATCH 0530/1009] Use device class naming in keenteic ndms2 (#96701) --- homeassistant/components/keenetic_ndms2/binary_sensor.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/keenetic_ndms2/binary_sensor.py b/homeassistant/components/keenetic_ndms2/binary_sensor.py index fa9b1fd48ddc55..f39c92519e432c 100644 --- a/homeassistant/components/keenetic_ndms2/binary_sensor.py +++ b/homeassistant/components/keenetic_ndms2/binary_sensor.py @@ -28,16 +28,12 @@ class RouterOnlineBinarySensor(BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY _attr_should_poll = False + _attr_has_entity_name = True def __init__(self, router: KeeneticRouter) -> None: """Initialize the APCUPSd binary device.""" self._router = router - @property - def name(self): - """Return the name of the online status sensor.""" - return f"{self._router.name} Online" - @property def unique_id(self) -> str: """Return a unique identifier for this device.""" From e5ca20b4d06666f163b9632d675cb5ebd6af27fe Mon Sep 17 00:00:00 2001 From: Blastoise186 <40033667+blastoise186@users.noreply.github.com> Date: Mon, 17 Jul 2023 08:15:33 +0100 Subject: [PATCH 0531/1009] Bump Cryptography from 41.0.1 to 41.0.2 (#96699) Bump cryptography from 41.0.1 to 41.0.2 Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.1 to 41.0.2. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/41.0.1...41.0.2) --- updated-dependencies: - dependency-name: cryptography dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- 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 31dcba97924d34..fca6e2fcb25467 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ bluetooth-auto-recovery==1.2.1 bluetooth-data-tools==1.6.0 certifi>=2021.5.30 ciso8601==2.3.0 -cryptography==41.0.1 +cryptography==41.0.2 dbus-fast==1.86.0 fnv-hash-fast==0.3.1 ha-av==10.1.0 diff --git a/pyproject.toml b/pyproject.toml index 317a68d36bc8cb..5ed5ad532249a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ dependencies = [ "lru-dict==1.2.0", "PyJWT==2.7.0", # PyJWT has loose dependency. We want the latest one. - "cryptography==41.0.1", + "cryptography==41.0.2", # pyOpenSSL 23.2.0 is required to work with cryptography 41+ "pyOpenSSL==23.2.0", "orjson==3.9.2", diff --git a/requirements.txt b/requirements.txt index cb78783559b287..8de97cb6156b88 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ ifaddr==0.2.0 Jinja2==3.1.2 lru-dict==1.2.0 PyJWT==2.7.0 -cryptography==41.0.1 +cryptography==41.0.2 pyOpenSSL==23.2.0 orjson==3.9.2 pip>=21.3.1,<23.3 From 9f71482f8ce25ac8722304a5c7f62b7c75c969f8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 09:16:23 +0200 Subject: [PATCH 0532/1009] Migrate iAlarm to has entity name (#96700) --- homeassistant/components/ialarm/alarm_control_panel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py index 6a4e3d191eb73b..1981a56e21120d 100644 --- a/homeassistant/components/ialarm/alarm_control_panel.py +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -30,7 +30,8 @@ class IAlarmPanel( ): """Representation of an iAlarm device.""" - _attr_name = "iAlarm" + _attr_has_entity_name = True + _attr_name = None _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY From a8e92bfcb643a8ded79959777a5a0b1bb279c086 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 09:22:07 +0200 Subject: [PATCH 0533/1009] Fix typo for PM 1 (#96473) --- homeassistant/components/number/const.py | 2 +- homeassistant/components/sensor/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index b0542aa588a8d0..1a2580cfc613e4 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -210,7 +210,7 @@ class NumberDeviceClass(StrEnum): """ PM1 = "pm1" - """Particulate matter <= 0.1 μm. + """Particulate matter <= 1 μm. Unit of measurement: `µg/m³` """ diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 17155912e48218..fe01058fda7e0b 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -248,7 +248,7 @@ class SensorDeviceClass(StrEnum): """ PM1 = "pm1" - """Particulate matter <= 0.1 μm. + """Particulate matter <= 1 μm. Unit of measurement: `µg/m³` """ From 2f8b88e6ef767c3ca27f4d2e96837c4d65c59666 Mon Sep 17 00:00:00 2001 From: mattmccormack Date: Mon, 17 Jul 2023 17:25:01 +1000 Subject: [PATCH 0534/1009] Add string "Quiet" to fan mode in climate component (#96584) --- homeassistant/components/esphome/climate.py | 1 + homeassistant/components/esphome/strings.json | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 34043da012e2bc..a9b184cc936015 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -140,6 +140,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti """A climate implementation for ESPHome.""" _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_translation_key = "climate" @callback def _on_static_info_update(self, static_info: EntityInfo) -> None: diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index 2bbbb229949aec..e38e8e1a2c4f30 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -76,6 +76,17 @@ "relaxed": "[%key:component::assist_pipeline::entity::select::vad_sensitivity::state::relaxed%]" } } + }, + "climate": { + "climate": { + "state_attributes": { + "fan_mode": { + "state": { + "quiet": "Quiet" + } + } + } + } } }, "issues": { From 657fdb075ad4d79ad21c442495f0145d6e672f3c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 17 Jul 2023 03:25:47 -0400 Subject: [PATCH 0535/1009] Bump pytomorrowio to 0.3.6 (#96628) --- homeassistant/components/tomorrowio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tomorrowio/manifest.json b/homeassistant/components/tomorrowio/manifest.json index 325a852c6d809c..95e164f12767be 100644 --- a/homeassistant/components/tomorrowio/manifest.json +++ b/homeassistant/components/tomorrowio/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["pytomorrowio"], - "requirements": ["pytomorrowio==0.3.5"] + "requirements": ["pytomorrowio==0.3.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 962e3fb3cdc9b4..1e6d17c93a78cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2166,7 +2166,7 @@ pythonegardia==1.0.52 pytile==2023.04.0 # homeassistant.components.tomorrowio -pytomorrowio==0.3.5 +pytomorrowio==0.3.6 # homeassistant.components.touchline pytouchline==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c86965e902609..289d847cd1dc0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1586,7 +1586,7 @@ python-telegram-bot==13.1 pytile==2023.04.0 # homeassistant.components.tomorrowio -pytomorrowio==0.3.5 +pytomorrowio==0.3.6 # homeassistant.components.traccar pytraccar==1.0.0 From c76fac0633749e8f463f9b6c22a4d47bba5ba1e7 Mon Sep 17 00:00:00 2001 From: Maximilian <43999966+DeerMaximum@users.noreply.github.com> Date: Mon, 17 Jul 2023 07:27:01 +0000 Subject: [PATCH 0536/1009] Bump pynina to 0.3.1 (#96693) --- homeassistant/components/nina/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nina/fixtures/sample_warning_details.json | 3 +-- tests/components/nina/test_binary_sensor.py | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nina/manifest.json b/homeassistant/components/nina/manifest.json index 98a088620eac2b..0185c727f67df3 100644 --- a/homeassistant/components/nina/manifest.json +++ b/homeassistant/components/nina/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/nina", "iot_class": "cloud_polling", "loggers": ["pynina"], - "requirements": ["PyNINA==0.3.0"] + "requirements": ["PyNINA==0.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1e6d17c93a78cc..a7582d464f7077 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -79,7 +79,7 @@ PyMetno==0.10.0 PyMicroBot==0.0.9 # homeassistant.components.nina -PyNINA==0.3.0 +PyNINA==0.3.1 # homeassistant.components.mobile_app # homeassistant.components.owntracks diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 289d847cd1dc0b..f8875c60adb2ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -69,7 +69,7 @@ PyMetno==0.10.0 PyMicroBot==0.0.9 # homeassistant.components.nina -PyNINA==0.3.0 +PyNINA==0.3.1 # homeassistant.components.mobile_app # homeassistant.components.owntracks diff --git a/tests/components/nina/fixtures/sample_warning_details.json b/tests/components/nina/fixtures/sample_warning_details.json index 612885e9aba58e..48a2e6964c7cfc 100644 --- a/tests/components/nina/fixtures/sample_warning_details.json +++ b/tests/components/nina/fixtures/sample_warning_details.json @@ -30,7 +30,7 @@ ], "headline": "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen", "description": "Die Zahl der mit dem Corona-Virus infizierten Menschen steigt gegenwärtig stark an. Es wächst daher die Gefahr einer weiteren Verbreitung der Infektion und - je nach Einzelfall - auch von schweren Erkrankungen.", - "instruction": "Waschen Sie sich regelmäßig und gründlich die Hände.
- Beachten Sie die AHA + A + L - Regeln:
Abstand halten - 1,5 m Mindestabstand beachten, Körperkontakt vermeiden!
Hygiene - regelmäßiges Händewaschen, Husten- und Nieshygiene beachten!
Alltagsmaske (Mund-Nase-Bedeckung) tragen!
App - installieren und nutzen Sie die Corona-Warn-App!
Lüften: Sorgen Sie für eine regelmäßige und gründliche Lüftung von Räumen - auch und gerade in der kommenden kalten Jahreszeit!
- Bitte folgen Sie den behördlichen Anordnungen.
- Husten und niesen Sie in ein Taschentuch oder in die Armbeuge.
- Bleiben Sie bei Erkältungssymptomen nach Möglichkeit zu Hause. Kontaktieren Sie Ihre Hausarztpraxis per Telefon oder wenden sich an die Telefonnummer 116117 des Ärztlichen Bereitschaftsdienstes und besprechen Sie das weitere Vorgehen. Gehen Sie nicht unaufgefordert in eine Arztpraxis oder ins Krankenhaus.
- Seien Sie kritisch: Informieren Sie sich nur aus gesicherten Quellen.", + "instruction": "Waschen sich regelmäßig und gründlich die Hände.", "contact": "Weitere Informationen und Empfehlungen finden Sie im Corona-Informations-Bereich der Warn-App NINA. Beachten Sie auch die Internetseiten der örtlichen Gesundheitsbehörde (Stadt- bzw. Kreisverwaltung) Ihres Aufenthaltsortes", "parameter": [ { @@ -125,7 +125,6 @@ "senderName": "Deutscher Wetterdienst", "headline": "Ausfall Notruf 112", "description": "Es treten Sturmböen mit Geschwindigkeiten zwischen 70 km/h (20m/s, 38kn, Bft 8) und 85 km/h (24m/s, 47kn, Bft 9) aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit schweren Sturmböen bis 90 km/h (25m/s, 48kn, Bft 10) gerechnet werden.", - "instruction": "ACHTUNG! Hinweis auf mögliche Gefahren: Es können zum Beispiel einzelne Äste herabstürzen. Achten Sie besonders auf herabfallende Gegenstände.", "web": "https://www.wettergefahren.de", "contact": "Deutscher Wetterdienst", "parameter": [ diff --git a/tests/components/nina/test_binary_sensor.py b/tests/components/nina/test_binary_sensor.py index 6238496ed09670..c6fd5bdd8301fc 100644 --- a/tests/components/nina/test_binary_sensor.py +++ b/tests/components/nina/test_binary_sensor.py @@ -182,7 +182,7 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert state_w1.attributes.get(ATTR_SEVERITY) == "Minor" assert ( state_w1.attributes.get(ATTR_RECOMMENDED_ACTIONS) - == "Es besteht keine Gefahr." + == "Waschen sich regelmäßig und gründlich die Hände." ) assert state_w1.attributes.get(ATTR_ID) == "mow.DE-BW-S-SE018-20211102-18-001" assert state_w1.attributes.get(ATTR_SENT) == "2021-11-02T20:07:16+01:00" From 3a06659120aa629e0db290ec9e83e2ad129baaf3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jul 2023 21:33:13 -1000 Subject: [PATCH 0537/1009] Speed up single entity/response service calls (#96729) * Significantly speed up single entity/response service calls Since the majority of service calls are single entity, we can avoid creating tasks in this case. Since the multi-entity service calls always check the result and raise, we can switch the asyncio.wait to asyncio.gather * Significantly speed up single entity/response service calls Since the majority of service calls are single entity, we can avoid creating tasks in this case. Since the multi-entity service calls always check the result and raise, we can switch the asyncio.wait to asyncio.gather * revert * cannot be inside pytest.raises * one more * Update homeassistant/helpers/service.py --- homeassistant/helpers/service.py | 25 +++++++++++++++---- .../devolo_home_network/test_button.py | 3 ++- .../devolo_home_network/test_switch.py | 3 +++ .../homeassistant/triggers/test_time.py | 4 +++ tests/components/rflink/test_light.py | 2 ++ tests/components/shelly/test_climate.py | 1 + tests/components/shelly/test_number.py | 1 + tests/components/shelly/test_switch.py | 2 ++ tests/components/shelly/test_update.py | 2 ++ .../totalconnect/test_alarm_control_panel.py | 24 +++++++++--------- tests/components/vizio/test_media_player.py | 1 + 11 files changed, 50 insertions(+), 18 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 16a79b3ae12b60..5470a94896da57 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -741,6 +741,8 @@ async def entity_service_call( # noqa: C901 Calls all platforms simultaneously. """ entity_perms: None | (Callable[[str, str], bool]) = None + return_response = call.return_response + if call.context.user_id: user = await hass.auth.async_get_user(call.context.user_id) if user is None: @@ -851,13 +853,27 @@ async def entity_service_call( # noqa: C901 entities.append(entity) if not entities: - if call.return_response: + if return_response: raise HomeAssistantError( "Service call requested response data but did not match any entities" ) return None - if call.return_response and len(entities) != 1: + if len(entities) == 1: + # Single entity case avoids creating tasks and allows returning + # ServiceResponse + entity = entities[0] + response_data = await _handle_entity_call( + hass, entity, func, data, call.context + ) + if entity.should_poll: + # Context expires if the turn on commands took a long time. + # Set context again so it's there when we update + entity.async_set_context(call.context) + await entity.async_update_ha_state(True) + return response_data if return_response else None + + if return_response: raise HomeAssistantError( "Service call requested response data but matched more than one entity" ) @@ -874,9 +890,8 @@ async def entity_service_call( # noqa: C901 ) assert not pending - response_data: ServiceResponse | None for task in done: - response_data = task.result() # pop exception if have + task.result() # pop exception if have tasks: list[asyncio.Task[None]] = [] @@ -895,7 +910,7 @@ async def entity_service_call( # noqa: C901 for future in done: future.result() # pop exception if have - return response_data if call.return_response else None + return None async def _handle_entity_call( diff --git a/tests/components/devolo_home_network/test_button.py b/tests/components/devolo_home_network/test_button.py index 69252a7c508099..c5681e4a278fb3 100644 --- a/tests/components/devolo_home_network/test_button.py +++ b/tests/components/devolo_home_network/test_button.py @@ -228,7 +228,8 @@ async def test_auth_failed(hass: HomeAssistant, mock_device: MockDevice) -> None {ATTR_ENTITY_ID: state_key}, blocking=True, ) - await hass.async_block_till_done() + + await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 diff --git a/tests/components/devolo_home_network/test_switch.py b/tests/components/devolo_home_network/test_switch.py index 8b84a0a9344517..00c06a6acc1db2 100644 --- a/tests/components/devolo_home_network/test_switch.py +++ b/tests/components/devolo_home_network/test_switch.py @@ -307,6 +307,9 @@ async def test_auth_failed( await hass.services.async_call( PLATFORM, SERVICE_TURN_ON, {"entity_id": state_key}, blocking=True ) + + await hass.async_block_till_done() + flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 diff --git a/tests/components/homeassistant/triggers/test_time.py b/tests/components/homeassistant/triggers/test_time.py index 0a41df17c8d8a9..b4554f1a4e6ad8 100644 --- a/tests/components/homeassistant/triggers/test_time.py +++ b/tests/components/homeassistant/triggers/test_time.py @@ -102,6 +102,7 @@ async def test_if_fires_using_at_input_datetime( }, blocking=True, ) + await hass.async_block_till_done() time_that_will_not_match_right_away = trigger_dt - timedelta(minutes=1) @@ -148,6 +149,7 @@ async def test_if_fires_using_at_input_datetime( }, blocking=True, ) + await hass.async_block_till_done() async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) await hass.async_block_till_done() @@ -556,6 +558,7 @@ async def test_datetime_in_past_on_load(hass: HomeAssistant, calls) -> None: }, blocking=True, ) + await hass.async_block_till_done() assert await async_setup_component( hass, @@ -587,6 +590,7 @@ async def test_datetime_in_past_on_load(hass: HomeAssistant, calls) -> None: }, blocking=True, ) + await hass.async_block_till_done() async_fire_time_changed(hass, future + timedelta(seconds=1)) await hass.async_block_till_done() diff --git a/tests/components/rflink/test_light.py b/tests/components/rflink/test_light.py index 27dca72fd96409..34b918cd3edb87 100644 --- a/tests/components/rflink/test_light.py +++ b/tests/components/rflink/test_light.py @@ -285,11 +285,13 @@ async def test_signal_repetitions_cancelling(hass: HomeAssistant, monkeypatch) - await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: f"{DOMAIN}.test"} ) + # Get background service time to start running await asyncio.sleep(0) await hass.services.async_call( DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}, blocking=True ) + await hass.async_block_till_done() assert [call[0][1] for call in protocol.send_command_ack.call_args_list] == [ "off", diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index 6c0ac74296a0df..505d1d463e8829 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -427,6 +427,7 @@ async def test_block_set_mode_auth_error( {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, blocking=True, ) + await hass.async_block_till_done() assert entry.state == ConfigEntryState.LOADED diff --git a/tests/components/shelly/test_number.py b/tests/components/shelly/test_number.py index 403d2f2993d4ed..a072c7638a1f64 100644 --- a/tests/components/shelly/test_number.py +++ b/tests/components/shelly/test_number.py @@ -186,6 +186,7 @@ async def test_block_set_value_auth_error( {ATTR_ENTITY_ID: "number.test_name_valve_position", ATTR_VALUE: 30}, blocking=True, ) + await hass.async_block_till_done() assert entry.state == ConfigEntryState.LOADED diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index 7892d98c45a067..7a709e0cc2e6f0 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -82,6 +82,7 @@ async def test_block_set_state_auth_error( {ATTR_ENTITY_ID: "switch.test_name_channel_1"}, blocking=True, ) + await hass.async_block_till_done() assert entry.state == ConfigEntryState.LOADED @@ -211,6 +212,7 @@ async def test_rpc_auth_error( {ATTR_ENTITY_ID: "switch.test_switch_0"}, blocking=True, ) + await hass.async_block_till_done() assert entry.state == ConfigEntryState.LOADED diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index 89d78dd8fa121c..ed5dd81339e376 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -203,6 +203,7 @@ async def test_block_update_auth_error( {ATTR_ENTITY_ID: "update.test_name_firmware_update"}, blocking=True, ) + await hass.async_block_till_done() assert entry.state == ConfigEntryState.LOADED @@ -541,6 +542,7 @@ async def test_rpc_update_auth_error( blocking=True, ) + await hass.async_block_till_done() assert entry.state == ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index 6161b793610208..be1a05947cc72d 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -129,7 +129,7 @@ async def test_arm_home_failure(hass: HomeAssistant) -> None: await hass.services.async_call( ALARM_DOMAIN, SERVICE_ALARM_ARM_HOME, DATA, blocking=True ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert f"{err.value}" == "TotalConnect failed to arm home test." assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 2 @@ -139,7 +139,7 @@ async def test_arm_home_failure(hass: HomeAssistant) -> None: await hass.services.async_call( ALARM_DOMAIN, SERVICE_ALARM_ARM_HOME, DATA, blocking=True ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert f"{err.value}" == "TotalConnect usercode is invalid. Did not arm home" assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED # should have started a re-auth flow @@ -183,7 +183,7 @@ async def test_arm_home_instant_failure(hass: HomeAssistant) -> None: await hass.services.async_call( DOMAIN, SERVICE_ALARM_ARM_HOME_INSTANT, DATA, blocking=True ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert f"{err.value}" == "TotalConnect failed to arm home instant test." assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 2 @@ -193,7 +193,7 @@ async def test_arm_home_instant_failure(hass: HomeAssistant) -> None: await hass.services.async_call( DOMAIN, SERVICE_ALARM_ARM_HOME_INSTANT, DATA, blocking=True ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert ( f"{err.value}" == "TotalConnect usercode is invalid. Did not arm home instant" @@ -240,7 +240,7 @@ async def test_arm_away_instant_failure(hass: HomeAssistant) -> None: await hass.services.async_call( DOMAIN, SERVICE_ALARM_ARM_AWAY_INSTANT, DATA, blocking=True ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert f"{err.value}" == "TotalConnect failed to arm away instant test." assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 2 @@ -250,7 +250,7 @@ async def test_arm_away_instant_failure(hass: HomeAssistant) -> None: await hass.services.async_call( DOMAIN, SERVICE_ALARM_ARM_AWAY_INSTANT, DATA, blocking=True ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert ( f"{err.value}" == "TotalConnect usercode is invalid. Did not arm away instant" @@ -296,7 +296,7 @@ async def test_arm_away_failure(hass: HomeAssistant) -> None: await hass.services.async_call( ALARM_DOMAIN, SERVICE_ALARM_ARM_AWAY, DATA, blocking=True ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert f"{err.value}" == "TotalConnect failed to arm away test." assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 2 @@ -306,7 +306,7 @@ async def test_arm_away_failure(hass: HomeAssistant) -> None: await hass.services.async_call( ALARM_DOMAIN, SERVICE_ALARM_ARM_AWAY, DATA, blocking=True ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert f"{err.value}" == "TotalConnect usercode is invalid. Did not arm away" assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED # should have started a re-auth flow @@ -353,7 +353,7 @@ async def test_disarm_failure(hass: HomeAssistant) -> None: await hass.services.async_call( ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert f"{err.value}" == "TotalConnect failed to disarm test." assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY assert mock_request.call_count == 2 @@ -363,7 +363,7 @@ async def test_disarm_failure(hass: HomeAssistant) -> None: await hass.services.async_call( ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert f"{err.value}" == "TotalConnect usercode is invalid. Did not disarm" assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY # should have started a re-auth flow @@ -406,7 +406,7 @@ async def test_arm_night_failure(hass: HomeAssistant) -> None: await hass.services.async_call( ALARM_DOMAIN, SERVICE_ALARM_ARM_NIGHT, DATA, blocking=True ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert f"{err.value}" == "TotalConnect failed to arm night test." assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 2 @@ -416,7 +416,7 @@ async def test_arm_night_failure(hass: HomeAssistant) -> None: await hass.services.async_call( ALARM_DOMAIN, SERVICE_ALARM_ARM_NIGHT, DATA, blocking=True ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert f"{err.value}" == "TotalConnect usercode is invalid. Did not arm night" assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED # should have started a re-auth flow diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 86733d83f15268..660de3ff6b6b4e 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -456,6 +456,7 @@ async def test_options_update( options=new_options, ) assert config_entry.options == updated_options + await hass.async_block_till_done() await _test_service( hass, MP_DOMAIN, "vol_up", SERVICE_VOLUME_UP, None, num=VOLUME_STEP ) From 65ebb6a74f4a276e557b17898720f6eac5741bf9 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 17 Jul 2023 09:44:47 +0200 Subject: [PATCH 0538/1009] 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 b13a861fa79289..c3cd21e6b2dda0 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -13,7 +13,7 @@ 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 @@ BACKOFF_TIME = 10 EVENT_IMAP = "imap_content" +MAX_ERRORS = 3 MAX_EVENT_DATA_BYTES = 32168 @@ -174,6 +175,7 @@ def __init__( ) -> 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 @@ def __init__( 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 @@ async def _async_update_data(self) -> int | None: 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 @@ async def async_start(self) -> None: 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( # pylint: disable=protected-access - 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 @@ async def _async_wait_push_loop(self) -> None: 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 @@ async def _async_wait_push_loop(self) -> None: 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 @@ async def _async_wait_push_loop(self) -> None: ) 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 31b42b50781043..b9512da0278a1a 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -235,6 +235,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 e29b6408f6581c5ccd133a94fc00a08c6455aec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn?= <72104362+weitzelb@users.noreply.github.com> Date: Mon, 17 Jul 2023 10:16:28 +0200 Subject: [PATCH 0539/1009] Periodically re-scan for Fronius inverters that were offline while setup (#96538) --- homeassistant/components/fronius/__init__.py | 86 ++++++++++++++++--- homeassistant/components/fronius/const.py | 1 + homeassistant/components/fronius/sensor.py | 2 + tests/components/fronius/__init__.py | 2 +- .../fixtures/igplus_v2/GetAPIVersion.json | 5 ++ .../fixtures/igplus_v2/GetInverterInfo.json | 24 ++++++ .../igplus_v2/GetInverterInfo_night.json | 14 +++ .../GetInverterRealtimeData_Device_1.json | 64 ++++++++++++++ ...etInverterRealtimeData_Device_1_night.json | 14 +++ .../fixtures/igplus_v2/GetLoggerInfo.json | 29 +++++++ .../igplus_v2/GetMeterRealtimeData.json | 17 ++++ .../igplus_v2/GetOhmPilotRealtimeData.json | 17 ++++ .../igplus_v2/GetPowerFlowRealtimeData.json | 38 ++++++++ .../GetPowerFlowRealtimeData_night.json | 32 +++++++ .../igplus_v2/GetStorageRealtimeData.json | 1 + .../fixtures/symo/GetInverterInfo_night.json | 24 ++++++ tests/components/fronius/test_init.py | 85 +++++++++++++++++- 17 files changed, 439 insertions(+), 16 deletions(-) create mode 100644 tests/components/fronius/fixtures/igplus_v2/GetAPIVersion.json create mode 100644 tests/components/fronius/fixtures/igplus_v2/GetInverterInfo.json create mode 100644 tests/components/fronius/fixtures/igplus_v2/GetInverterInfo_night.json create mode 100644 tests/components/fronius/fixtures/igplus_v2/GetInverterRealtimeData_Device_1.json create mode 100644 tests/components/fronius/fixtures/igplus_v2/GetInverterRealtimeData_Device_1_night.json create mode 100644 tests/components/fronius/fixtures/igplus_v2/GetLoggerInfo.json create mode 100644 tests/components/fronius/fixtures/igplus_v2/GetMeterRealtimeData.json create mode 100644 tests/components/fronius/fixtures/igplus_v2/GetOhmPilotRealtimeData.json create mode 100644 tests/components/fronius/fixtures/igplus_v2/GetPowerFlowRealtimeData.json create mode 100644 tests/components/fronius/fixtures/igplus_v2/GetPowerFlowRealtimeData_night.json create mode 100644 tests/components/fronius/fixtures/igplus_v2/GetStorageRealtimeData.json create mode 100644 tests/components/fronius/fixtures/symo/GetInverterInfo_night.json diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index c4d764f4c711bd..f8dcb4f4a9cb71 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -3,20 +3,28 @@ import asyncio from collections.abc import Callable +from datetime import datetime, timedelta import logging from typing import Final, TypeVar from pyfronius import Fronius, FroniusError -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ATTR_MODEL, ATTR_SW_VERSION, CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_time_interval -from .const import DOMAIN, SOLAR_NET_ID_SYSTEM, FroniusDeviceInfo +from .const import ( + DOMAIN, + SOLAR_NET_ID_SYSTEM, + SOLAR_NET_RESCAN_TIMER, + FroniusDeviceInfo, +) from .coordinator import ( FroniusCoordinatorBase, FroniusInverterUpdateCoordinator, @@ -26,6 +34,7 @@ FroniusPowerFlowUpdateCoordinator, FroniusStorageUpdateCoordinator, ) +from .sensor import InverterSensor _LOGGER: Final = logging.getLogger(__name__) PLATFORMS: Final = [Platform.SENSOR] @@ -67,6 +76,7 @@ def __init__( self.cleanup_callbacks: list[Callable[[], None]] = [] self.config_entry = entry self.coordinator_lock = asyncio.Lock() + self.sensor_async_add_entities: AddEntitiesCallback | None = None self.fronius = fronius self.host: str = entry.data[CONF_HOST] # entry.unique_id is either logger uid or first inverter uid if no logger available @@ -95,17 +105,7 @@ async def init_devices(self) -> None: # _create_solar_net_device uses data from self.logger_coordinator when available self.system_device_info = await self._create_solar_net_device() - _inverter_infos = await self._get_inverter_infos() - for inverter_info in _inverter_infos: - coordinator = FroniusInverterUpdateCoordinator( - hass=self.hass, - solar_net=self, - logger=_LOGGER, - name=f"{DOMAIN}_inverter_{inverter_info.solar_net_id}_{self.host}", - inverter_info=inverter_info, - ) - await coordinator.async_config_entry_first_refresh() - self.inverter_coordinators.append(coordinator) + await self._init_devices_inverter() self.meter_coordinator = await self._init_optional_coordinator( FroniusMeterUpdateCoordinator( @@ -143,6 +143,15 @@ async def init_devices(self) -> None: ) ) + # Setup periodic re-scan + self.cleanup_callbacks.append( + async_track_time_interval( + self.hass, + self._init_devices_inverter, + timedelta(minutes=SOLAR_NET_RESCAN_TIMER), + ) + ) + async def _create_solar_net_device(self) -> DeviceInfo: """Create a device for the Fronius SolarNet system.""" solar_net_device: DeviceInfo = DeviceInfo( @@ -168,14 +177,57 @@ async def _create_solar_net_device(self) -> DeviceInfo: ) return solar_net_device + async def _init_devices_inverter(self, _now: datetime | None = None) -> None: + """Get available inverters and set up coordinators for new found devices.""" + _inverter_infos = await self._get_inverter_infos() + + _LOGGER.debug("Processing inverters for: %s", _inverter_infos) + for _inverter_info in _inverter_infos: + _inverter_name = ( + f"{DOMAIN}_inverter_{_inverter_info.solar_net_id}_{self.host}" + ) + + # Add found inverter only not already existing + if _inverter_info.solar_net_id in [ + inv.inverter_info.solar_net_id for inv in self.inverter_coordinators + ]: + continue + + _coordinator = FroniusInverterUpdateCoordinator( + hass=self.hass, + solar_net=self, + logger=_LOGGER, + name=_inverter_name, + inverter_info=_inverter_info, + ) + await _coordinator.async_config_entry_first_refresh() + self.inverter_coordinators.append(_coordinator) + + # Only for re-scans. Initial setup adds entities through sensor.async_setup_entry + if self.sensor_async_add_entities is not None: + _coordinator.add_entities_for_seen_keys( + self.sensor_async_add_entities, InverterSensor + ) + + _LOGGER.debug( + "New inverter added (UID: %s)", + _inverter_info.unique_id, + ) + async def _get_inverter_infos(self) -> list[FroniusDeviceInfo]: """Get information about the inverters in the SolarNet system.""" + inverter_infos: list[FroniusDeviceInfo] = [] + try: _inverter_info = await self.fronius.inverter_info() except FroniusError as err: + if self.config_entry.state == ConfigEntryState.LOADED: + # During a re-scan we will attempt again as per schedule. + _LOGGER.debug("Re-scan failed for %s", self.host) + return inverter_infos + raise ConfigEntryNotReady from err - inverter_infos: list[FroniusDeviceInfo] = [] for inverter in _inverter_info["inverters"]: solar_net_id = inverter["device_id"]["value"] unique_id = inverter["unique_id"]["value"] @@ -195,6 +247,12 @@ async def _get_inverter_infos(self) -> list[FroniusDeviceInfo]: unique_id=unique_id, ) ) + _LOGGER.debug( + "Inverter found at %s (Device ID: %s, UID: %s)", + self.host, + solar_net_id, + unique_id, + ) return inverter_infos @staticmethod diff --git a/homeassistant/components/fronius/const.py b/homeassistant/components/fronius/const.py index de3e0cc9563513..042773472c5b0f 100644 --- a/homeassistant/components/fronius/const.py +++ b/homeassistant/components/fronius/const.py @@ -8,6 +8,7 @@ SolarNetId = str SOLAR_NET_ID_POWER_FLOW: SolarNetId = "power_flow" SOLAR_NET_ID_SYSTEM: SolarNetId = "system" +SOLAR_NET_RESCAN_TIMER: Final = 60 class FroniusConfigEntryData(TypedDict): diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 4e706db032fcf9..d701d0d1860a22 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -53,6 +53,8 @@ async def async_setup_entry( ) -> None: """Set up Fronius sensor entities based on a config entry.""" solar_net: FroniusSolarNet = hass.data[DOMAIN][config_entry.entry_id] + solar_net.sensor_async_add_entities = async_add_entities + for inverter_coordinator in solar_net.inverter_coordinators: inverter_coordinator.add_entities_for_seen_keys( async_add_entities, InverterSensor diff --git a/tests/components/fronius/__init__.py b/tests/components/fronius/__init__.py index bd70604398dbd0..4d11291508b927 100644 --- a/tests/components/fronius/__init__.py +++ b/tests/components/fronius/__init__.py @@ -59,7 +59,7 @@ def mock_responses( ) aioclient_mock.get( f"{host}/solar_api/v1/GetInverterInfo.cgi", - text=load_fixture(f"{fixture_set}/GetInverterInfo.json", "fronius"), + text=load_fixture(f"{fixture_set}/GetInverterInfo{_night}.json", "fronius"), ) aioclient_mock.get( f"{host}/solar_api/v1/GetLoggerInfo.cgi", diff --git a/tests/components/fronius/fixtures/igplus_v2/GetAPIVersion.json b/tests/components/fronius/fixtures/igplus_v2/GetAPIVersion.json new file mode 100644 index 00000000000000..28b2077691cb9f --- /dev/null +++ b/tests/components/fronius/fixtures/igplus_v2/GetAPIVersion.json @@ -0,0 +1,5 @@ +{ + "APIVersion": 1, + "BaseURL": "/solar_api/v1/", + "CompatibilityRange": "1.5-18" +} diff --git a/tests/components/fronius/fixtures/igplus_v2/GetInverterInfo.json b/tests/components/fronius/fixtures/igplus_v2/GetInverterInfo.json new file mode 100644 index 00000000000000..844fcff89e41dc --- /dev/null +++ b/tests/components/fronius/fixtures/igplus_v2/GetInverterInfo.json @@ -0,0 +1,24 @@ +{ + "Body": { + "Data": { + "1": { + "CustomName": "IG Plus 70 V-2", + "DT": 174, + "ErrorCode": 0, + "PVPower": 6500, + "Show": 1, + "StatusCode": 7, + "UniqueID": "203200" + } + } + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2023-07-14T17:19:20+02:00" + } +} diff --git a/tests/components/fronius/fixtures/igplus_v2/GetInverterInfo_night.json b/tests/components/fronius/fixtures/igplus_v2/GetInverterInfo_night.json new file mode 100644 index 00000000000000..e65784e797132f --- /dev/null +++ b/tests/components/fronius/fixtures/igplus_v2/GetInverterInfo_night.json @@ -0,0 +1,14 @@ +{ + "Body": { + "Data": {} + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2023-06-27T21:48:52+02:00" + } +} diff --git a/tests/components/fronius/fixtures/igplus_v2/GetInverterRealtimeData_Device_1.json b/tests/components/fronius/fixtures/igplus_v2/GetInverterRealtimeData_Device_1.json new file mode 100644 index 00000000000000..150ea901a0c628 --- /dev/null +++ b/tests/components/fronius/fixtures/igplus_v2/GetInverterRealtimeData_Device_1.json @@ -0,0 +1,64 @@ +{ + "Body": { + "Data": { + "DAY_ENERGY": { + "Unit": "Wh", + "Value": 42000 + }, + "DeviceStatus": { + "ErrorCode": 0, + "LEDColor": 2, + "LEDState": 0, + "MgmtTimerRemainingTime": -1, + "StateToReset": false, + "StatusCode": 7 + }, + "FAC": { + "Unit": "Hz", + "Value": 49.960000000000001 + }, + "IAC": { + "Unit": "A", + "Value": 9.0299999999999994 + }, + "IDC": { + "Unit": "A", + "Value": 6.46 + }, + "PAC": { + "Unit": "W", + "Value": 2096 + }, + "TOTAL_ENERGY": { + "Unit": "Wh", + "Value": 81809000 + }, + "UAC": { + "Unit": "V", + "Value": 232 + }, + "UDC": { + "Unit": "V", + "Value": 345 + }, + "YEAR_ENERGY": { + "Unit": "Wh", + "Value": 4927000 + } + } + }, + "Head": { + "RequestArguments": { + "DataCollection": "CommonInverterData", + "DeviceClass": "Inverter", + "DeviceId": "1", + "Scope": "Device" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2023-07-14T17:21:42+02:00" + } +} diff --git a/tests/components/fronius/fixtures/igplus_v2/GetInverterRealtimeData_Device_1_night.json b/tests/components/fronius/fixtures/igplus_v2/GetInverterRealtimeData_Device_1_night.json new file mode 100644 index 00000000000000..e65784e797132f --- /dev/null +++ b/tests/components/fronius/fixtures/igplus_v2/GetInverterRealtimeData_Device_1_night.json @@ -0,0 +1,14 @@ +{ + "Body": { + "Data": {} + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2023-06-27T21:48:52+02:00" + } +} diff --git a/tests/components/fronius/fixtures/igplus_v2/GetLoggerInfo.json b/tests/components/fronius/fixtures/igplus_v2/GetLoggerInfo.json new file mode 100644 index 00000000000000..0ebeb823deff28 --- /dev/null +++ b/tests/components/fronius/fixtures/igplus_v2/GetLoggerInfo.json @@ -0,0 +1,29 @@ +{ + "Body": { + "LoggerInfo": { + "CO2Factor": 0.52999997138977051, + "CO2Unit": "kg", + "CashCurrency": "EUR", + "CashFactor": 0.07700000643730164, + "DefaultLanguage": "en", + "DeliveryFactor": 0.25, + "HWVersion": "2.4D", + "PlatformID": "wilma", + "ProductID": "fronius-datamanager-card", + "SWVersion": "3.26.1-3", + "TimezoneLocation": "Berlin", + "TimezoneName": "CEST", + "UTCOffset": 7200, + "UniqueID": "123.4567890" + } + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2023-07-14T17:23:22+02:00" + } +} diff --git a/tests/components/fronius/fixtures/igplus_v2/GetMeterRealtimeData.json b/tests/components/fronius/fixtures/igplus_v2/GetMeterRealtimeData.json new file mode 100644 index 00000000000000..30de1a1fa98c53 --- /dev/null +++ b/tests/components/fronius/fixtures/igplus_v2/GetMeterRealtimeData.json @@ -0,0 +1,17 @@ +{ + "Body": { + "Data": {} + }, + "Head": { + "RequestArguments": { + "DeviceClass": "Meter", + "Scope": "System" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2023-07-14T17:28:05+02:00" + } +} diff --git a/tests/components/fronius/fixtures/igplus_v2/GetOhmPilotRealtimeData.json b/tests/components/fronius/fixtures/igplus_v2/GetOhmPilotRealtimeData.json new file mode 100644 index 00000000000000..e77b751db3b037 --- /dev/null +++ b/tests/components/fronius/fixtures/igplus_v2/GetOhmPilotRealtimeData.json @@ -0,0 +1,17 @@ +{ + "Body": { + "Data": {} + }, + "Head": { + "RequestArguments": { + "DeviceClass": "OhmPilot", + "Scope": "System" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2023-07-14T17:29:16+02:00" + } +} diff --git a/tests/components/fronius/fixtures/igplus_v2/GetPowerFlowRealtimeData.json b/tests/components/fronius/fixtures/igplus_v2/GetPowerFlowRealtimeData.json new file mode 100644 index 00000000000000..a8ae2fc6d861fd --- /dev/null +++ b/tests/components/fronius/fixtures/igplus_v2/GetPowerFlowRealtimeData.json @@ -0,0 +1,38 @@ +{ + "Body": { + "Data": { + "Inverters": { + "1": { + "DT": 174, + "E_Day": 43000, + "E_Total": 1230000, + "E_Year": 12345, + "P": 2241 + } + }, + "Site": { + "E_Day": 43000, + "E_Total": 1230000, + "E_Year": 12345, + "Meter_Location": "unknown", + "Mode": "produce-only", + "P_Akku": null, + "P_Grid": null, + "P_Load": null, + "P_PV": 2241, + "rel_Autonomy": null, + "rel_SelfConsumption": null + }, + "Version": "12" + } + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2023-07-14T17:29:55+02:00" + } +} diff --git a/tests/components/fronius/fixtures/igplus_v2/GetPowerFlowRealtimeData_night.json b/tests/components/fronius/fixtures/igplus_v2/GetPowerFlowRealtimeData_night.json new file mode 100644 index 00000000000000..1da28803195646 --- /dev/null +++ b/tests/components/fronius/fixtures/igplus_v2/GetPowerFlowRealtimeData_night.json @@ -0,0 +1,32 @@ +{ + "Body": { + "Data": { + "Inverters": {}, + "Site": { + "E_Day": null, + "E_Total": null, + "E_Year": null, + "Meter_Location": "unknown", + "Mode": "produce-only", + "P_Akku": null, + "P_Grid": null, + "P_Load": null, + "P_PV": null, + "rel_Autonomy": null, + "rel_SelfConsumption": null + }, + "Version": "12" + } + }, + "Head": { + "RequestArguments": { + "humanreadable": "false" + }, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2023-07-13T22:04:44+02:00" + } +} diff --git a/tests/components/fronius/fixtures/igplus_v2/GetStorageRealtimeData.json b/tests/components/fronius/fixtures/igplus_v2/GetStorageRealtimeData.json new file mode 100644 index 00000000000000..0967ef424bce67 --- /dev/null +++ b/tests/components/fronius/fixtures/igplus_v2/GetStorageRealtimeData.json @@ -0,0 +1 @@ +{} diff --git a/tests/components/fronius/fixtures/symo/GetInverterInfo_night.json b/tests/components/fronius/fixtures/symo/GetInverterInfo_night.json new file mode 100644 index 00000000000000..5b2676c3a3f1b7 --- /dev/null +++ b/tests/components/fronius/fixtures/symo/GetInverterInfo_night.json @@ -0,0 +1,24 @@ +{ + "Body": { + "Data": { + "1": { + "CustomName": "Symo 20", + "DT": 121, + "ErrorCode": 0, + "PVPower": 23100, + "Show": 1, + "StatusCode": 7, + "UniqueID": "1234567" + } + } + }, + "Head": { + "RequestArguments": {}, + "Status": { + "Code": 0, + "Reason": "", + "UserMessage": "" + }, + "Timestamp": "2021-10-07T13:41:00+02:00" + } +} diff --git a/tests/components/fronius/test_init.py b/tests/components/fronius/test_init.py index 0e8b405da44d44..d46c60c3cb398b 100644 --- a/tests/components/fronius/test_init.py +++ b/tests/components/fronius/test_init.py @@ -1,14 +1,18 @@ """Test the Fronius integration.""" +from datetime import timedelta from unittest.mock import patch from pyfronius import FroniusError -from homeassistant.components.fronius.const import DOMAIN +from homeassistant.components.fronius.const import DOMAIN, SOLAR_NET_RESCAN_TIMER from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.util import dt as dt_util from . import mock_responses, setup_fronius_integration +from tests.common import async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker @@ -53,3 +57,82 @@ async def test_inverter_error( ): config_entry = await setup_fronius_integration(hass) assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_inverter_night_rescan( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test dynamic adding of an inverter discovered automatically after a Home Assistant reboot during the night.""" + mock_responses(aioclient_mock, fixture_set="igplus_v2", night=True) + config_entry = await setup_fronius_integration(hass, is_logger=True) + assert config_entry.state is ConfigEntryState.LOADED + + # Only expect logger during the night + fronius_entries = hass.config_entries.async_entries(DOMAIN) + assert len(fronius_entries) == 1 + + # Switch to daytime + mock_responses(aioclient_mock, fixture_set="igplus_v2", night=False) + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(minutes=SOLAR_NET_RESCAN_TIMER) + ) + await hass.async_block_till_done() + + # We expect our inverter to be present now + device_registry = dr.async_get(hass) + inverter_1 = device_registry.async_get_device(identifiers={(DOMAIN, "203200")}) + assert inverter_1.manufacturer == "Fronius" + + # After another re-scan we still only expect this inverter + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(minutes=SOLAR_NET_RESCAN_TIMER * 2) + ) + await hass.async_block_till_done() + inverter_1 = device_registry.async_get_device(identifiers={(DOMAIN, "203200")}) + assert inverter_1.manufacturer == "Fronius" + + +async def test_inverter_rescan_interruption( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test interruption of re-scan during runtime to process further.""" + mock_responses(aioclient_mock, fixture_set="igplus_v2", night=True) + config_entry = await setup_fronius_integration(hass, is_logger=True) + assert config_entry.state is ConfigEntryState.LOADED + device_registry = dr.async_get(hass) + # Expect 1 devices during the night, logger + assert ( + len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + == 1 + ) + + with patch( + "pyfronius.Fronius.inverter_info", + side_effect=FroniusError, + ): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(minutes=SOLAR_NET_RESCAN_TIMER) + ) + await hass.async_block_till_done() + + # No increase of devices expected because of a FroniusError + assert ( + len( + dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + ) + == 1 + ) + + # Next re-scan will pick up the new inverter. Expect 2 devices now. + mock_responses(aioclient_mock, fixture_set="igplus_v2", night=False) + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(minutes=SOLAR_NET_RESCAN_TIMER * 2) + ) + await hass.async_block_till_done() + + assert ( + len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + == 2 + ) From 4a6247f922a41f3145a30b1e071aa92f1dcdd1f1 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 17 Jul 2023 10:20:57 +0200 Subject: [PATCH 0540/1009] Update pygtfs to 0.1.9 (#96682) --- homeassistant/components/gtfs/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/gtfs/manifest.json b/homeassistant/components/gtfs/manifest.json index e7f7e617df9b99..73a5998ea92be2 100644 --- a/homeassistant/components/gtfs/manifest.json +++ b/homeassistant/components/gtfs/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/gtfs", "iot_class": "local_polling", "loggers": ["pygtfs"], - "requirements": ["pygtfs==0.1.7"] + "requirements": ["pygtfs==0.1.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index a7582d464f7077..81d11692c20f21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1702,7 +1702,7 @@ pyfttt==0.3 pygatt[GATTTOOL]==4.0.5 # homeassistant.components.gtfs -pygtfs==0.1.7 +pygtfs==0.1.9 # homeassistant.components.hvv_departures pygti==0.9.4 From 56bc708b28d93ddc354593d5c02af3655febde6b Mon Sep 17 00:00:00 2001 From: b-uwe <61052367+b-uwe@users.noreply.github.com> Date: Mon, 17 Jul 2023 11:49:42 +0200 Subject: [PATCH 0541/1009] Remove the virtual integration for ultraloq (#96355) --- homeassistant/brands/u_tec.json | 2 +- homeassistant/components/ultraloq/__init__.py | 1 - homeassistant/components/ultraloq/manifest.json | 6 ------ homeassistant/generated/integrations.json | 13 +++---------- 4 files changed, 4 insertions(+), 18 deletions(-) delete mode 100644 homeassistant/components/ultraloq/__init__.py delete mode 100644 homeassistant/components/ultraloq/manifest.json diff --git a/homeassistant/brands/u_tec.json b/homeassistant/brands/u_tec.json index f0c2cf8a6915fb..2ce4be9a7d9972 100644 --- a/homeassistant/brands/u_tec.json +++ b/homeassistant/brands/u_tec.json @@ -1,5 +1,5 @@ { "domain": "u_tec", "name": "U-tec", - "integrations": ["ultraloq"] + "iot_standards": ["zwave"] } diff --git a/homeassistant/components/ultraloq/__init__.py b/homeassistant/components/ultraloq/__init__.py deleted file mode 100644 index b650c59a5de278..00000000000000 --- a/homeassistant/components/ultraloq/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Virtual integration: Ultraloq.""" diff --git a/homeassistant/components/ultraloq/manifest.json b/homeassistant/components/ultraloq/manifest.json deleted file mode 100644 index 4775ba6caa3f5c..00000000000000 --- a/homeassistant/components/ultraloq/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "domain": "ultraloq", - "name": "Ultraloq", - "integration_type": "virtual", - "iot_standards": ["zwave"] -} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 4dcde6d883f6f8..9fa12a84383aea 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5914,16 +5914,9 @@ }, "u_tec": { "name": "U-tec", - "integrations": { - "ultraloq": { - "integration_type": "virtual", - "config_flow": false, - "iot_standards": [ - "zwave" - ], - "name": "Ultraloq" - } - } + "iot_standards": [ + "zwave" + ] }, "ubiquiti": { "name": "Ubiquiti", From 5a951c390b5c2b6554c5ca0018d609e9336fb741 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 12:07:43 +0200 Subject: [PATCH 0542/1009] Add entity translations to mutesync (#96741) --- .../components/mutesync/binary_sensor.py | 16 +++++++--------- homeassistant/components/mutesync/strings.json | 10 ++++++++++ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mutesync/binary_sensor.py b/homeassistant/components/mutesync/binary_sensor.py index d225400c7cc84f..3c9d92094f79f8 100644 --- a/homeassistant/components/mutesync/binary_sensor.py +++ b/homeassistant/components/mutesync/binary_sensor.py @@ -9,10 +9,10 @@ from .const import DOMAIN -SENSORS = { - "in_meeting": "In Meeting", - "muted": "Muted", -} +SENSORS = ( + "in_meeting", + "muted", +) async def async_setup_entry( @@ -30,15 +30,13 @@ async def async_setup_entry( class MuteStatus(update_coordinator.CoordinatorEntity, BinarySensorEntity): """Mütesync binary sensors.""" + _attr_has_entity_name = True + def __init__(self, coordinator, sensor_type): """Initialize our sensor.""" super().__init__(coordinator) self._sensor_type = sensor_type - - @property - def name(self): - """Return the name of the sensor.""" - return SENSORS[self._sensor_type] + self._attr_translation_key = sensor_type @property def unique_id(self): diff --git a/homeassistant/components/mutesync/strings.json b/homeassistant/components/mutesync/strings.json index 9b18620acf88f7..2a3cca666ee504 100644 --- a/homeassistant/components/mutesync/strings.json +++ b/homeassistant/components/mutesync/strings.json @@ -12,5 +12,15 @@ "invalid_auth": "Enable authentication in mütesync Preferences > Authentication", "unknown": "[%key:common::config_flow::error::unknown%]" } + }, + "entity": { + "binary_sensor": { + "in_meeting": { + "name": "In meeting" + }, + "muted": { + "name": "Muted" + } + } } } From dc8267b05a5c81c484b10d0ab11daf88cbdfdfe4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 12:09:43 +0200 Subject: [PATCH 0543/1009] Migrate NuHeat to has entity name (#96742) --- homeassistant/components/nuheat/climate.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index aa69215321598e..b2bc66b60c03f7 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -75,6 +75,8 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity): _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) + _attr_has_entity_name = True + _attr_name = None def __init__(self, coordinator, thermostat, temperature_unit): """Initialize the thermostat.""" @@ -84,11 +86,6 @@ def __init__(self, coordinator, thermostat, temperature_unit): self._schedule_mode = None self._target_temperature = None - @property - def name(self): - """Return the name of the thermostat.""" - return self._thermostat.room - @property def temperature_unit(self) -> str: """Return the unit of measurement.""" From 57361a738e26de2b83cd5f8478c140ae92b1b6dc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 12:58:51 +0200 Subject: [PATCH 0544/1009] 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 d3920d3f0e429f..1d074bba9c2d3a 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 73bbfc7a2d7e85229da8a22c719e8a8d064dcd49 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 13:05:58 +0200 Subject: [PATCH 0545/1009] Add base entity to philips js (#96756) * Create superclass for philips js * Move device info creation to coordinator --- .../components/philips_js/__init__.py | 14 +++++++++++ homeassistant/components/philips_js/entity.py | 20 +++++++++++++++ homeassistant/components/philips_js/light.py | 18 ++----------- .../components/philips_js/media_player.py | 17 ++----------- homeassistant/components/philips_js/remote.py | 16 ++---------- homeassistant/components/philips_js/switch.py | 25 +++---------------- 6 files changed, 44 insertions(+), 66 deletions(-) create mode 100644 homeassistant/components/philips_js/entity.py diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 55ac33d198f074..8ecc8a0e8c429b 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, DOMAIN @@ -101,6 +102,19 @@ def __init__( ), ) + @property + def device_info(self) -> DeviceInfo: + """Return device info.""" + return DeviceInfo( + identifiers={ + (DOMAIN, self.unique_id), + }, + manufacturer="Philips", + model=self.system.get("model"), + name=self.system["name"], + sw_version=self.system.get("softwareversion"), + ) + @property def system(self) -> SystemType: """Return the system descriptor.""" diff --git a/homeassistant/components/philips_js/entity.py b/homeassistant/components/philips_js/entity.py new file mode 100644 index 00000000000000..c2645974f490a0 --- /dev/null +++ b/homeassistant/components/philips_js/entity.py @@ -0,0 +1,20 @@ +"""Base Philips js entity.""" +from __future__ import annotations + +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import PhilipsTVDataUpdateCoordinator + + +class PhilipsJsEntity(CoordinatorEntity[PhilipsTVDataUpdateCoordinator]): + """Base Philips js entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: PhilipsTVDataUpdateCoordinator, + ) -> None: + """Initialize light.""" + super().__init__(coordinator) + self._attr_device_info = coordinator.device_info diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index 8df88ff923af3d..9795b2303f177d 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -18,13 +18,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.color import color_hsv_to_RGB, color_RGB_to_hsv from . import PhilipsTVDataUpdateCoordinator from .const import DOMAIN +from .entity import PhilipsJsEntity EFFECT_PARTITION = ": " EFFECT_MODE = "Mode" @@ -134,13 +133,9 @@ def _average_pixels(data): return 0.0, 0.0, 0.0 -class PhilipsTVLightEntity( - CoordinatorEntity[PhilipsTVDataUpdateCoordinator], LightEntity -): +class PhilipsTVLightEntity(PhilipsJsEntity, LightEntity): """Representation of a Philips TV exposing the JointSpace API.""" - _attr_has_entity_name = True - def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, @@ -158,15 +153,6 @@ def __init__( self._attr_name = "Ambilight" self._attr_unique_id = coordinator.unique_id self._attr_icon = "mdi:television-ambient-light" - self._attr_device_info = DeviceInfo( - identifiers={ - (DOMAIN, self._attr_unique_id), - }, - manufacturer="Philips", - model=coordinator.system.get("model"), - name=coordinator.system["name"], - sw_version=coordinator.system.get("softwareversion"), - ) self._update_from_coordinator() diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index bdd55bb2dad9d5..6ee70b03d54b49 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -18,13 +18,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.trigger import PluggableAction -from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import LOGGER as _LOGGER, PhilipsTVDataUpdateCoordinator from .const import DOMAIN +from .entity import PhilipsJsEntity from .helpers import async_get_turn_on_trigger SUPPORT_PHILIPS_JS = ( @@ -63,13 +62,10 @@ async def async_setup_entry( ) -class PhilipsTVMediaPlayer( - CoordinatorEntity[PhilipsTVDataUpdateCoordinator], MediaPlayerEntity -): +class PhilipsTVMediaPlayer(PhilipsJsEntity, MediaPlayerEntity): """Representation of a Philips TV exposing the JointSpace API.""" _attr_device_class = MediaPlayerDeviceClass.TV - _attr_has_entity_name = True _attr_name = None def __init__( @@ -80,15 +76,6 @@ def __init__( self._tv = coordinator.api self._sources: dict[str, str] = {} self._attr_unique_id = coordinator.unique_id - self._attr_device_info = DeviceInfo( - identifiers={ - (DOMAIN, coordinator.unique_id), - }, - manufacturer="Philips", - model=coordinator.system.get("model"), - sw_version=coordinator.system.get("softwareversion"), - name=coordinator.system["name"], - ) self._attr_state = MediaPlayerState.OFF self._turn_on = PluggableAction(self.async_write_ha_state) diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index 0aa61979eb2852..55e84e88599831 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -13,13 +13,12 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.trigger import PluggableAction -from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import LOGGER, PhilipsTVDataUpdateCoordinator from .const import DOMAIN +from .entity import PhilipsJsEntity from .helpers import async_get_turn_on_trigger @@ -33,11 +32,9 @@ async def async_setup_entry( async_add_entities([PhilipsTVRemote(coordinator)]) -class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteEntity): +class PhilipsTVRemote(PhilipsJsEntity, RemoteEntity): """Device that sends commands.""" - _attr_has_entity_name = True - def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, @@ -47,15 +44,6 @@ def __init__( self._tv = coordinator.api self._attr_name = "Remote" self._attr_unique_id = coordinator.unique_id - self._attr_device_info = DeviceInfo( - identifiers={ - (DOMAIN, coordinator.unique_id), - }, - manufacturer="Philips", - model=coordinator.system.get("model"), - name=coordinator.system["name"], - sw_version=coordinator.system.get("softwareversion"), - ) self._turn_on = PluggableAction(self.async_write_ha_state) async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/philips_js/switch.py b/homeassistant/components/philips_js/switch.py index b66fd3296d9655..a950e606566c6f 100644 --- a/homeassistant/components/philips_js/switch.py +++ b/homeassistant/components/philips_js/switch.py @@ -6,12 +6,11 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import PhilipsTVDataUpdateCoordinator from .const import DOMAIN +from .entity import PhilipsJsEntity HUE_POWER_OFF = "Off" HUE_POWER_ON = "On" @@ -33,13 +32,9 @@ async def async_setup_entry( async_add_entities([PhilipsTVAmbilightHueSwitch(coordinator)]) -class PhilipsTVScreenSwitch( - CoordinatorEntity[PhilipsTVDataUpdateCoordinator], SwitchEntity -): +class PhilipsTVScreenSwitch(PhilipsJsEntity, SwitchEntity): """A Philips TV screen state switch.""" - _attr_has_entity_name = True - def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, @@ -51,11 +46,6 @@ def __init__( self._attr_name = "Screen state" self._attr_icon = "mdi:television-shimmer" self._attr_unique_id = f"{coordinator.unique_id}_screenstate" - self._attr_device_info = DeviceInfo( - identifiers={ - (DOMAIN, coordinator.unique_id), - } - ) @property def available(self) -> bool: @@ -80,9 +70,7 @@ async def async_turn_off(self, **kwargs: Any) -> None: await self.coordinator.api.setScreenState("Off") -class PhilipsTVAmbilightHueSwitch( - CoordinatorEntity[PhilipsTVDataUpdateCoordinator], SwitchEntity -): +class PhilipsTVAmbilightHueSwitch(PhilipsJsEntity, SwitchEntity): """A Philips TV Ambi+Hue switch.""" def __init__( @@ -93,14 +81,9 @@ def __init__( super().__init__(coordinator) - self._attr_name = f"{coordinator.system['name']} Ambilight+Hue" + self._attr_name = "Ambilight+Hue" self._attr_icon = "mdi:television-ambient-light" self._attr_unique_id = f"{coordinator.unique_id}_ambi_hue" - self._attr_device_info = DeviceInfo( - identifiers={ - (DOMAIN, coordinator.unique_id), - } - ) @property def available(self) -> bool: From b0dd05a411eb027cd5feb9268bc8a211a5c1aba8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 13:12:50 +0200 Subject: [PATCH 0546/1009] Add entity translations to philips js (#96747) * Add entity translations to philips js * Remove name --- homeassistant/components/philips_js/light.py | 3 ++- homeassistant/components/philips_js/remote.py | 3 ++- .../components/philips_js/strings.json | 20 +++++++++++++++++++ homeassistant/components/philips_js/switch.py | 6 ++++-- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index 9795b2303f177d..75f43039de8103 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -136,6 +136,8 @@ def _average_pixels(data): class PhilipsTVLightEntity(PhilipsJsEntity, LightEntity): """Representation of a Philips TV exposing the JointSpace API.""" + _attr_translation_key = "ambilight" + def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, @@ -150,7 +152,6 @@ def __init__( self._attr_supported_color_modes = {ColorMode.HS, ColorMode.ONOFF} self._attr_supported_features = LightEntityFeature.EFFECT - self._attr_name = "Ambilight" self._attr_unique_id = coordinator.unique_id self._attr_icon = "mdi:television-ambient-light" diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index 55e84e88599831..c5b240898097c6 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -35,6 +35,8 @@ async def async_setup_entry( class PhilipsTVRemote(PhilipsJsEntity, RemoteEntity): """Device that sends commands.""" + _attr_translation_key = "remote" + def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, @@ -42,7 +44,6 @@ def __init__( """Initialize the Philips TV.""" super().__init__(coordinator) self._tv = coordinator.api - self._attr_name = "Remote" self._attr_unique_id = coordinator.unique_id self._turn_on = PluggableAction(self.async_write_ha_state) diff --git a/homeassistant/components/philips_js/strings.json b/homeassistant/components/philips_js/strings.json index 302e1b9accf7d3..a260d42feda39e 100644 --- a/homeassistant/components/philips_js/strings.json +++ b/homeassistant/components/philips_js/strings.json @@ -39,5 +39,25 @@ "trigger_type": { "turn_on": "Device is requested to turn on" } + }, + "entity": { + "light": { + "ambilight": { + "name": "Ambilight" + } + }, + "remote": { + "remote": { + "name": "[%key:component::remote::title%]" + } + }, + "switch": { + "screen_state": { + "name": "Screen state" + }, + "ambilight_hue": { + "name": "Ambilight + Hue" + } + } } } diff --git a/homeassistant/components/philips_js/switch.py b/homeassistant/components/philips_js/switch.py index a950e606566c6f..29cfa10a230f68 100644 --- a/homeassistant/components/philips_js/switch.py +++ b/homeassistant/components/philips_js/switch.py @@ -35,6 +35,8 @@ async def async_setup_entry( class PhilipsTVScreenSwitch(PhilipsJsEntity, SwitchEntity): """A Philips TV screen state switch.""" + _attr_translation_key = "screen_state" + def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, @@ -43,7 +45,6 @@ def __init__( super().__init__(coordinator) - self._attr_name = "Screen state" self._attr_icon = "mdi:television-shimmer" self._attr_unique_id = f"{coordinator.unique_id}_screenstate" @@ -73,6 +74,8 @@ async def async_turn_off(self, **kwargs: Any) -> None: class PhilipsTVAmbilightHueSwitch(PhilipsJsEntity, SwitchEntity): """A Philips TV Ambi+Hue switch.""" + _attr_translation_key = "ambilight_hue" + def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, @@ -81,7 +84,6 @@ def __init__( super().__init__(coordinator) - self._attr_name = "Ambilight+Hue" self._attr_icon = "mdi:television-ambient-light" self._attr_unique_id = f"{coordinator.unique_id}_ambi_hue" From e76254a50fe0d546300ce34e5ae45df3e4cfea1f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 14:42:58 +0200 Subject: [PATCH 0547/1009] Migrate Plum Lightpad to has entity name (#96744) --- homeassistant/components/plum_lightpad/light.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py index 9f26200e9aed12..ac0dd0c919c7a9 100644 --- a/homeassistant/components/plum_lightpad/light.py +++ b/homeassistant/components/plum_lightpad/light.py @@ -66,6 +66,8 @@ class PlumLight(LightEntity): """Representation of a Plum Lightpad dimmer.""" _attr_should_poll = False + _attr_has_entity_name = True + _attr_name = None def __init__(self, load): """Initialize the light.""" @@ -86,11 +88,6 @@ def unique_id(self): """Combine logical load ID with .light to guarantee it is unique.""" return f"{self._load.llid}.light" - @property - def name(self): - """Return the name of the switch if any.""" - return self._load.name - @property def device_info(self) -> DeviceInfo: """Return the device info.""" @@ -98,7 +95,7 @@ def device_info(self) -> DeviceInfo: identifiers={(DOMAIN, self.unique_id)}, manufacturer="Plum", model="Dimmer", - name=self.name, + name=self._load.name, ) @property From 98e166f795690cc60133572873d89ad6ecc30a98 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 14:49:35 +0200 Subject: [PATCH 0548/1009] Fix device name for OwnTracks (#96759) --- homeassistant/components/owntracks/device_tracker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index a1fc632c2fd13f..18185619f6b2a2 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -121,7 +121,10 @@ def source_type(self) -> SourceType: @property def device_info(self) -> DeviceInfo: """Return the device info.""" - return DeviceInfo(identifiers={(OT_DOMAIN, self._dev_id)}, name=self.name) + device_info = DeviceInfo(identifiers={(OT_DOMAIN, self._dev_id)}) + if "host_name" in self._data: + device_info["name"] = self._data["host_name"] + return device_info async def async_added_to_hass(self) -> None: """Call when entity about to be added to Home Assistant.""" From 34f1b2b71d208a3f040442cd450f8c03c5c04f35 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 14:54:26 +0200 Subject: [PATCH 0549/1009] Add entity translations to radiotherm (#96745) --- homeassistant/components/radiotherm/climate.py | 2 +- homeassistant/components/radiotherm/entity.py | 2 ++ homeassistant/components/radiotherm/strings.json | 7 +++++++ homeassistant/components/radiotherm/switch.py | 3 ++- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index 2c71eac019339d..f5ea14e8f4e7fd 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -105,11 +105,11 @@ class RadioThermostat(RadioThermostatEntity, ClimateEntity): _attr_hvac_modes = OPERATION_LIST _attr_temperature_unit = UnitOfTemperature.FAHRENHEIT _attr_precision = PRECISION_HALVES + _attr_name = None def __init__(self, coordinator: RadioThermUpdateCoordinator) -> None: """Initialize the thermostat.""" super().__init__(coordinator) - self._attr_name = self.init_data.name self._attr_unique_id = self.init_data.mac self._attr_fan_modes = CT30_FAN_OPERATION_LIST self._attr_supported_features = ( diff --git a/homeassistant/components/radiotherm/entity.py b/homeassistant/components/radiotherm/entity.py index 203d17a5dc2155..7eb14548adab9d 100644 --- a/homeassistant/components/radiotherm/entity.py +++ b/homeassistant/components/radiotherm/entity.py @@ -14,6 +14,8 @@ class RadioThermostatEntity(CoordinatorEntity[RadioThermUpdateCoordinator]): """Base class for radiotherm entities.""" + _attr_has_entity_name = True + def __init__(self, coordinator: RadioThermUpdateCoordinator) -> None: """Initialize the entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/radiotherm/strings.json b/homeassistant/components/radiotherm/strings.json index 21f53d72bfa5ee..693811f59abd90 100644 --- a/homeassistant/components/radiotherm/strings.json +++ b/homeassistant/components/radiotherm/strings.json @@ -27,5 +27,12 @@ } } } + }, + "entity": { + "switch": { + "hold": { + "name": "Hold" + } + } } } diff --git a/homeassistant/components/radiotherm/switch.py b/homeassistant/components/radiotherm/switch.py index 2cf0602a3fa46f..3b71baffec6bcb 100644 --- a/homeassistant/components/radiotherm/switch.py +++ b/homeassistant/components/radiotherm/switch.py @@ -28,10 +28,11 @@ async def async_setup_entry( class RadioThermHoldSwitch(RadioThermostatEntity, SwitchEntity): """Provides radiotherm hold switch support.""" + _attr_translation_key = "hold" + def __init__(self, coordinator: RadioThermUpdateCoordinator) -> None: """Initialize the hold mode switch.""" super().__init__(coordinator) - self._attr_name = f"{coordinator.init_data.name} Hold" self._attr_unique_id = f"{coordinator.init_data.mac}_hold" @property From 8937884e33866938764cdc6b0d126531d95641ae Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 14:54:38 +0200 Subject: [PATCH 0550/1009] Add entity translations to MotionEye (#96740) * Add entity translations to MotionEye * Fix name * Explicit device name --- .../components/motioneye/__init__.py | 2 ++ homeassistant/components/motioneye/camera.py | 6 +---- homeassistant/components/motioneye/sensor.py | 10 +++---- .../components/motioneye/strings.json | 27 +++++++++++++++++++ homeassistant/components/motioneye/switch.py | 19 +++++-------- 5 files changed, 39 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index c7aa8edc6c979d..b936497cfc67ae 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -536,6 +536,8 @@ def get_media_url( class MotionEyeEntity(CoordinatorEntity): """Base class for motionEye entities.""" + _attr_has_entity_name = True + def __init__( self, config_entry_id: str, diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py index 189296039aa7f5..683308e081cedb 100644 --- a/homeassistant/components/motioneye/camera.py +++ b/homeassistant/components/motioneye/camera.py @@ -12,7 +12,6 @@ DEFAULT_SURVEILLANCE_USERNAME, KEY_ACTION_SNAPSHOT, KEY_MOTION_DETECTION, - KEY_NAME, KEY_STREAMING_AUTH_MODE, KEY_TEXT_OVERLAY_CAMERA_NAME, KEY_TEXT_OVERLAY_CUSTOM_TEXT, @@ -144,8 +143,6 @@ def camera_add(camera: dict[str, Any]) -> None: class MotionEyeMjpegCamera(MotionEyeEntity, MjpegCamera): """motionEye mjpeg camera.""" - _name: str - def __init__( self, config_entry_id: str, @@ -203,7 +200,7 @@ def _get_mjpeg_camera_properties_for_camera( streaming_url = self._client.get_camera_stream_url(camera) return { - CONF_NAME: camera[KEY_NAME], + CONF_NAME: None, CONF_USERNAME: self._surveillance_username if auth is not None else None, CONF_PASSWORD: self._surveillance_password if auth is not None else "", CONF_MJPEG_URL: streaming_url or "", @@ -218,7 +215,6 @@ def _set_mjpeg_camera_state_for_camera(self, camera: dict[str, Any]) -> None: # Sets the state of the underlying (inherited) MjpegCamera based on the updated # MotionEye camera dictionary. properties = self._get_mjpeg_camera_properties_for_camera(camera) - self._name = properties[CONF_NAME] self._username = properties[CONF_USERNAME] self._password = properties[CONF_PASSWORD] self._mjpeg_url = properties[CONF_MJPEG_URL] diff --git a/homeassistant/components/motioneye/sensor.py b/homeassistant/components/motioneye/sensor.py index c8b7679149c734..4d0abb84d46c99 100644 --- a/homeassistant/components/motioneye/sensor.py +++ b/homeassistant/components/motioneye/sensor.py @@ -6,7 +6,7 @@ from typing import Any from motioneye_client.client import MotionEyeClient -from motioneye_client.const import KEY_ACTIONS, KEY_NAME +from motioneye_client.const import KEY_ACTIONS from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry @@ -48,6 +48,8 @@ def camera_add(camera: dict[str, Any]) -> None: class MotionEyeActionSensor(MotionEyeEntity, SensorEntity): """motionEye action sensor camera.""" + _attr_translation_key = "actions" + def __init__( self, config_entry_id: str, @@ -69,12 +71,6 @@ def __init__( ), ) - @property - def name(self) -> str: - """Return the name of the sensor.""" - camera_prepend = f"{self._camera[KEY_NAME]} " if self._camera else "" - return f"{camera_prepend}Actions" - @property def native_value(self) -> StateType: """Return the value reported by the sensor.""" diff --git a/homeassistant/components/motioneye/strings.json b/homeassistant/components/motioneye/strings.json index fdf73cd8cf8ee7..ea7901617cbeab 100644 --- a/homeassistant/components/motioneye/strings.json +++ b/homeassistant/components/motioneye/strings.json @@ -37,6 +37,33 @@ } } }, + "entity": { + "sensor": { + "actions": { + "name": "Actions" + } + }, + "switch": { + "motion_detection": { + "name": "Motion detection" + }, + "text_overlay": { + "name": "Text overlay" + }, + "video_streaming": { + "name": "Video streaming" + }, + "still_images": { + "name": "Still images" + }, + "movies": { + "name": "Movies" + }, + "upload_enabled": { + "name": "Upload enabled" + } + } + }, "services": { "set_text_overlay": { "name": "Set text overlay", diff --git a/homeassistant/components/motioneye/switch.py b/homeassistant/components/motioneye/switch.py index 3be1c20981dcd4..069c5edaad7846 100644 --- a/homeassistant/components/motioneye/switch.py +++ b/homeassistant/components/motioneye/switch.py @@ -8,7 +8,6 @@ from motioneye_client.const import ( KEY_MOTION_DETECTION, KEY_MOVIES, - KEY_NAME, KEY_STILL_IMAGES, KEY_TEXT_OVERLAY, KEY_UPLOAD_ENABLED, @@ -28,37 +27,37 @@ MOTIONEYE_SWITCHES = [ SwitchEntityDescription( key=KEY_MOTION_DETECTION, - name="Motion Detection", + translation_key="motion_detection", entity_registry_enabled_default=True, entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=KEY_TEXT_OVERLAY, - name="Text Overlay", + translation_key="text_overlay", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=KEY_VIDEO_STREAMING, - name="Video Streaming", + translation_key="video_streaming", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=KEY_STILL_IMAGES, - name="Still Images", + translation_key="still_images", entity_registry_enabled_default=True, entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=KEY_MOVIES, - name="Movies", + translation_key="movies", entity_registry_enabled_default=True, entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=KEY_UPLOAD_ENABLED, - name="Upload Enabled", + translation_key="upload_enabled", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, ), @@ -114,12 +113,6 @@ def __init__( entity_description, ) - @property - def name(self) -> str: - """Return the name of the switch.""" - camera_prepend = f"{self._camera[KEY_NAME]} " if self._camera else "" - return f"{camera_prepend}{self.entity_description.name}" - @property def is_on(self) -> bool: """Return true if the switch is on.""" From 005e45edcc3de1400808e7c889f4e0646960d310 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 14:55:34 +0200 Subject: [PATCH 0551/1009] Migrate OwnTracks to has entity name (#96743) * Migrate OwnTracks to has entity name * Fix test * Fix tests --- homeassistant/components/owntracks/device_tracker.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index 18185619f6b2a2..ba0beb40cf8e37 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -59,6 +59,9 @@ def _receive_data(dev_id, **data): class OwnTracksEntity(TrackerEntity, RestoreEntity): """Represent a tracked device.""" + _attr_has_entity_name = True + _attr_name = None + def __init__(self, dev_id, data=None): """Set up OwnTracks entity.""" self._dev_id = dev_id @@ -108,11 +111,6 @@ def location_name(self): """Return a location name for the current location of the device.""" return self._data.get("location_name") - @property - def name(self): - """Return the name of the device.""" - return self._data.get("host_name") - @property def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" From 7ccb06ed2247e7c3ba753f2300fe92f8d12016e2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 17:26:13 +0200 Subject: [PATCH 0552/1009] Add entity translations to Twentemilieu (#96762) --- .../components/twentemilieu/sensor.py | 12 ++++++------ .../components/twentemilieu/strings.json | 19 +++++++++++++++++++ .../twentemilieu/snapshots/test_sensor.ambr | 10 +++++----- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index ab0a60c44ca1c8..fba10a269f7cce 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, WASTE_TYPE_TO_DESCRIPTION +from .const import DOMAIN from .entity import TwenteMilieuEntity @@ -38,36 +38,36 @@ class TwenteMilieuSensorDescription( SENSORS: tuple[TwenteMilieuSensorDescription, ...] = ( TwenteMilieuSensorDescription( key="tree", + translation_key="christmas_tree_pickup", waste_type=WasteType.TREE, - name=WASTE_TYPE_TO_DESCRIPTION[WasteType.TREE], icon="mdi:pine-tree", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Non-recyclable", + translation_key="non_recyclable_waste_pickup", waste_type=WasteType.NON_RECYCLABLE, - name=WASTE_TYPE_TO_DESCRIPTION[WasteType.NON_RECYCLABLE], icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Organic", + translation_key="organic_waste_pickup", waste_type=WasteType.ORGANIC, - name=WASTE_TYPE_TO_DESCRIPTION[WasteType.ORGANIC], icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Paper", + translation_key="paper_waste_pickup", waste_type=WasteType.PAPER, - name=WASTE_TYPE_TO_DESCRIPTION[WasteType.PAPER], icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Plastic", + translation_key="packages_waste_pickup", waste_type=WasteType.PACKAGES, - name=WASTE_TYPE_TO_DESCRIPTION[WasteType.PACKAGES], icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), diff --git a/homeassistant/components/twentemilieu/strings.json b/homeassistant/components/twentemilieu/strings.json index d9b59b2d02c376..7797167ea0b822 100644 --- a/homeassistant/components/twentemilieu/strings.json +++ b/homeassistant/components/twentemilieu/strings.json @@ -17,5 +17,24 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" } + }, + "entity": { + "sensor": { + "non_recyclable_waste_pickup": { + "name": "Non-recyclable waste pickup" + }, + "organic_waste_pickup": { + "name": "Organic waste pickup" + }, + "packages_waste_pickup": { + "name": "Packages waste pickup" + }, + "paper_waste_pickup": { + "name": "Paper waste pickup" + }, + "christmas_tree_pickup": { + "name": "Christmas tree pickup" + } + } } } diff --git a/tests/components/twentemilieu/snapshots/test_sensor.ambr b/tests/components/twentemilieu/snapshots/test_sensor.ambr index 46b21ebab3294f..367da49c7f63e8 100644 --- a/tests/components/twentemilieu/snapshots/test_sensor.ambr +++ b/tests/components/twentemilieu/snapshots/test_sensor.ambr @@ -38,7 +38,7 @@ 'original_name': 'Christmas tree pickup', 'platform': 'twentemilieu', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'christmas_tree_pickup', 'unique_id': 'twentemilieu_12345_tree', 'unit_of_measurement': None, }) @@ -109,7 +109,7 @@ 'original_name': 'Non-recyclable waste pickup', 'platform': 'twentemilieu', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'non_recyclable_waste_pickup', 'unique_id': 'twentemilieu_12345_Non-recyclable', 'unit_of_measurement': None, }) @@ -180,7 +180,7 @@ 'original_name': 'Organic waste pickup', 'platform': 'twentemilieu', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'organic_waste_pickup', 'unique_id': 'twentemilieu_12345_Organic', 'unit_of_measurement': None, }) @@ -251,7 +251,7 @@ 'original_name': 'Packages waste pickup', 'platform': 'twentemilieu', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'packages_waste_pickup', 'unique_id': 'twentemilieu_12345_Plastic', 'unit_of_measurement': None, }) @@ -322,7 +322,7 @@ 'original_name': 'Paper waste pickup', 'platform': 'twentemilieu', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'paper_waste_pickup', 'unique_id': 'twentemilieu_12345_Paper', 'unit_of_measurement': None, }) From 70c88a125c9937fb50ef832af04814a90d9ca11e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Jul 2023 05:47:36 -1000 Subject: [PATCH 0553/1009] Reduce attribute lookups in update state_attributes (#96511) --- homeassistant/components/update/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index e0244034865d48..f788ad21098833 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -375,25 +375,25 @@ def state_attributes(self) -> dict[str, Any] | None: else: in_progress = self.__in_progress + installed_version = self.installed_version + latest_version = self.latest_version + skipped_version = self.__skipped_version # Clear skipped version in case it matches the current installed # version or the latest version diverged. - if ( - self.installed_version is not None - and self.__skipped_version == self.installed_version - ) or ( - self.latest_version is not None - and self.__skipped_version != self.latest_version + if (installed_version is not None and skipped_version == installed_version) or ( + latest_version is not None and skipped_version != latest_version ): + skipped_version = None self.__skipped_version = None return { ATTR_AUTO_UPDATE: self.auto_update, - ATTR_INSTALLED_VERSION: self.installed_version, + ATTR_INSTALLED_VERSION: installed_version, ATTR_IN_PROGRESS: in_progress, - ATTR_LATEST_VERSION: self.latest_version, + ATTR_LATEST_VERSION: latest_version, ATTR_RELEASE_SUMMARY: release_summary, ATTR_RELEASE_URL: self.release_url, - ATTR_SKIPPED_VERSION: self.__skipped_version, + ATTR_SKIPPED_VERSION: skipped_version, ATTR_TITLE: self.title, } From 560e0cc7e0c759adba3b7ad16fbb72e67794e67d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 17:47:47 +0200 Subject: [PATCH 0554/1009] Migrate VLC Telnet to has entity naming (#96774) * Migrate VLC Telnet to has entity naming * Remove unused variable --- homeassistant/components/vlc_telnet/media_player.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 80b9d75303bef2..14728c05e53075 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -70,6 +70,8 @@ async def wrapper(self: _VlcDeviceT, *args: _P.args, **kwargs: _P.kwargs) -> Non class VlcDevice(MediaPlayerEntity): """Representation of a vlc player.""" + _attr_has_entity_name = True + _attr_name = None _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.CLEAR_PLAYLIST @@ -91,7 +93,6 @@ def __init__( ) -> None: """Initialize the vlc device.""" self._config_entry = config_entry - self._name = name self._volume: float | None = None self._muted: bool | None = None self._media_position_updated_at: datetime | None = None @@ -183,11 +184,6 @@ async def async_update(self) -> None: if self._media_title and (pos := self._media_title.find("?authSig=")) != -1: self._media_title = self._media_title[:pos] - @property - def name(self) -> str: - """Return the name of the device.""" - return self._name - @property def available(self) -> bool: """Return True if entity is available.""" From e99b6b2a0328b361f494c1d0c188161406e9e840 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 17 Jul 2023 17:52:53 +0200 Subject: [PATCH 0555/1009] Migrate VeSync to has entity name (#96772) * Migrate VeSync to has entity name * Fix tests --- homeassistant/components/vesync/common.py | 11 +- homeassistant/components/vesync/fan.py | 1 + homeassistant/components/vesync/light.py | 2 + homeassistant/components/vesync/sensor.py | 18 ++- homeassistant/components/vesync/strings.json | 28 +++++ homeassistant/components/vesync/switch.py | 2 + .../vesync/snapshots/test_diagnostics.ambr | 10 +- .../components/vesync/snapshots/test_fan.ambr | 16 +-- .../vesync/snapshots/test_light.ambr | 12 +- .../vesync/snapshots/test_sensor.ambr | 112 +++++++++--------- .../vesync/snapshots/test_switch.ambr | 8 +- 11 files changed, 123 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index f0684b6b01dfa7..8e6ad545bd0a27 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -51,11 +51,12 @@ async def async_process_devices(hass, manager): class VeSyncBaseEntity(Entity): """Base class for VeSync Entity Representations.""" + _attr_has_entity_name = True + def __init__(self, device: VeSyncBaseDevice) -> None: """Initialize the VeSync device.""" self.device = device self._attr_unique_id = self.base_unique_id - self._attr_name = self.base_name @property def base_unique_id(self): @@ -67,12 +68,6 @@ def base_unique_id(self): return f"{self.device.cid}{str(self.device.sub_device_no)}" return self.device.cid - @property - def base_name(self) -> str: - """Return the name of the device.""" - # Same story here as `base_unique_id` above - return self.device.device_name - @property def available(self) -> bool: """Return True if device is available.""" @@ -83,7 +78,7 @@ def device_info(self) -> DeviceInfo: """Return device information.""" return DeviceInfo( identifiers={(DOMAIN, self.base_unique_id)}, - name=self.base_name, + name=self.device.device_name, model=self.device.device_type, manufacturer="VeSync", sw_version=self.device.current_firm_version, diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index f89224aaba8db6..a3bf027c28ff05 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -87,6 +87,7 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): """Representation of a VeSync fan.""" _attr_supported_features = FanEntityFeature.SET_SPEED + _attr_name = None def __init__(self, fan): """Initialize the VeSync fan device.""" diff --git a/homeassistant/components/vesync/light.py b/homeassistant/components/vesync/light.py index 9129d060cdcf6f..e6cc979e8082ef 100644 --- a/homeassistant/components/vesync/light.py +++ b/homeassistant/components/vesync/light.py @@ -66,6 +66,8 @@ def _setup_entities(devices, async_add_entities): class VeSyncBaseLight(VeSyncDevice, LightEntity): """Base class for VeSync Light Devices Representations.""" + _attr_name = None + @property def brightness(self) -> int: """Get light brightness.""" diff --git a/homeassistant/components/vesync/sensor.py b/homeassistant/components/vesync/sensor.py index bc0db04dd47ecc..f3612c2d011974 100644 --- a/homeassistant/components/vesync/sensor.py +++ b/homeassistant/components/vesync/sensor.py @@ -79,7 +79,7 @@ def ha_dev_type(device): SENSORS: tuple[VeSyncSensorEntityDescription, ...] = ( VeSyncSensorEntityDescription( key="filter-life", - name="Filter Life", + translation_key="filter_life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -88,13 +88,12 @@ def ha_dev_type(device): ), VeSyncSensorEntityDescription( key="air-quality", - name="Air Quality", + translation_key="air_quality", value_fn=lambda device: device.details["air_quality"], exists_fn=lambda device: sku_supported(device, AIR_QUALITY_SUPPORTED), ), VeSyncSensorEntityDescription( key="pm25", - name="PM2.5", device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, @@ -103,7 +102,7 @@ def ha_dev_type(device): ), VeSyncSensorEntityDescription( key="power", - name="current power", + translation_key="current_power", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, @@ -113,7 +112,7 @@ def ha_dev_type(device): ), VeSyncSensorEntityDescription( key="energy", - name="energy use today", + translation_key="energy_today", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, @@ -123,7 +122,7 @@ def ha_dev_type(device): ), VeSyncSensorEntityDescription( key="energy-weekly", - name="energy use weekly", + translation_key="energy_week", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, @@ -133,7 +132,7 @@ def ha_dev_type(device): ), VeSyncSensorEntityDescription( key="energy-monthly", - name="energy use monthly", + translation_key="energy_month", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, @@ -143,7 +142,7 @@ def ha_dev_type(device): ), VeSyncSensorEntityDescription( key="energy-yearly", - name="energy use yearly", + translation_key="energy_year", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, @@ -153,7 +152,7 @@ def ha_dev_type(device): ), VeSyncSensorEntityDescription( key="voltage", - name="current voltage", + translation_key="current_voltage", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, @@ -207,7 +206,6 @@ def __init__( """Initialize the VeSync outlet device.""" super().__init__(device) self.entity_description = description - self._attr_name = f"{super().name} {description.name}" self._attr_unique_id = f"{super().unique_id}-{description.key}" @property diff --git a/homeassistant/components/vesync/strings.json b/homeassistant/components/vesync/strings.json index 08cbdf943f67b9..9a54062a2b518e 100644 --- a/homeassistant/components/vesync/strings.json +++ b/homeassistant/components/vesync/strings.json @@ -16,6 +16,34 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, + "entity": { + "sensor": { + "filter_life": { + "name": "Filter life" + }, + "air_quality": { + "name": "Air quality" + }, + "current_power": { + "name": "Current power" + }, + "energy_today": { + "name": "Energy use today" + }, + "energy_week": { + "name": "Energy use weekly" + }, + "energy_month": { + "name": "Energy use monthly" + }, + "energy_year": { + "name": "Energy use yearly" + }, + "current_voltage": { + "name": "Current voltage" + } + } + }, "services": { "update_devices": { "name": "Update devices", diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index 93cb5c67a5d2f8..e6101b2ba5176f 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -54,6 +54,8 @@ def _setup_entities(devices, async_add_entities): class VeSyncBaseSwitch(VeSyncDevice, SwitchEntity): """Base class for VeSync switch Device Representations.""" + _attr_name = None + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self.device.turn_on() diff --git a/tests/components/vesync/snapshots/test_diagnostics.ambr b/tests/components/vesync/snapshots/test_diagnostics.ambr index 2c12f9bc5f6e43..b9426f5ba1eefa 100644 --- a/tests/components/vesync/snapshots/test_diagnostics.ambr +++ b/tests/components/vesync/snapshots/test_diagnostics.ambr @@ -192,7 +192,7 @@ 'name': None, 'original_device_class': None, 'original_icon': None, - 'original_name': 'Fan', + 'original_name': None, 'state': dict({ 'attributes': dict({ 'friendly_name': 'Fan', @@ -220,10 +220,10 @@ 'name': None, 'original_device_class': None, 'original_icon': None, - 'original_name': 'Fan Air Quality', + 'original_name': 'Air quality', 'state': dict({ 'attributes': dict({ - 'friendly_name': 'Fan Air Quality', + 'friendly_name': 'Fan Air quality', }), 'entity_id': 'sensor.fan_air_quality', 'last_changed': str, @@ -243,10 +243,10 @@ 'name': None, 'original_device_class': None, 'original_icon': None, - 'original_name': 'Fan Filter Life', + 'original_name': 'Filter life', 'state': dict({ 'attributes': dict({ - 'friendly_name': 'Fan Filter Life', + 'friendly_name': 'Fan Filter life', 'state_class': 'measurement', 'unit_of_measurement': '%', }), diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index 82a31b5fc14aff..428f066e6cc4db 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -47,7 +47,7 @@ 'domain': 'fan', 'entity_category': None, 'entity_id': 'fan.air_purifier_131s', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -56,7 +56,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Air Purifier 131s', + 'original_name': None, 'platform': 'vesync', 'supported_features': , 'translation_key': None, @@ -129,7 +129,7 @@ 'domain': 'fan', 'entity_category': None, 'entity_id': 'fan.air_purifier_200s', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -138,7 +138,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Air Purifier 200s', + 'original_name': None, 'platform': 'vesync', 'supported_features': , 'translation_key': None, @@ -218,7 +218,7 @@ 'domain': 'fan', 'entity_category': None, 'entity_id': 'fan.air_purifier_400s', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -227,7 +227,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Air Purifier 400s', + 'original_name': None, 'platform': 'vesync', 'supported_features': , 'translation_key': None, @@ -308,7 +308,7 @@ 'domain': 'fan', 'entity_category': None, 'entity_id': 'fan.air_purifier_600s', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -317,7 +317,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Air Purifier 600s', + 'original_name': None, 'platform': 'vesync', 'supported_features': , 'translation_key': None, diff --git a/tests/components/vesync/snapshots/test_light.ambr b/tests/components/vesync/snapshots/test_light.ambr index 1f7b0aa9bafddf..67940603d41c33 100644 --- a/tests/components/vesync/snapshots/test_light.ambr +++ b/tests/components/vesync/snapshots/test_light.ambr @@ -178,7 +178,7 @@ 'domain': 'light', 'entity_category': None, 'entity_id': 'light.dimmable_light', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -187,7 +187,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Dimmable Light', + 'original_name': None, 'platform': 'vesync', 'supported_features': 0, 'translation_key': None, @@ -259,7 +259,7 @@ 'domain': 'light', 'entity_category': None, 'entity_id': 'light.dimmer_switch', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -268,7 +268,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Dimmer Switch', + 'original_name': None, 'platform': 'vesync', 'supported_features': 0, 'translation_key': None, @@ -395,7 +395,7 @@ 'domain': 'light', 'entity_category': None, 'entity_id': 'light.temperature_light', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -404,7 +404,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Temperature Light', + 'original_name': None, 'platform': 'vesync', 'supported_features': 0, 'translation_key': None, diff --git a/tests/components/vesync/snapshots/test_sensor.ambr b/tests/components/vesync/snapshots/test_sensor.ambr index 040e41747a27c6..1fc897226997f1 100644 --- a/tests/components/vesync/snapshots/test_sensor.ambr +++ b/tests/components/vesync/snapshots/test_sensor.ambr @@ -44,7 +44,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.air_purifier_131s_filter_life', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -53,10 +53,10 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Air Purifier 131s Filter Life', + 'original_name': 'Filter life', 'platform': 'vesync', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'filter_life', 'unique_id': 'air-purifier-filter-life', 'unit_of_measurement': '%', }), @@ -72,7 +72,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.air_purifier_131s_air_quality', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -81,10 +81,10 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Air Purifier 131s Air Quality', + 'original_name': 'Air quality', 'platform': 'vesync', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'air_quality', 'unique_id': 'air-purifier-air-quality', 'unit_of_measurement': None, }), @@ -93,7 +93,7 @@ # name: test_sensor_state[Air Purifier 131s][sensor.air_purifier_131s_air_quality] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Air Purifier 131s Air Quality', + 'friendly_name': 'Air Purifier 131s Air quality', }), 'context': , 'entity_id': 'sensor.air_purifier_131s_air_quality', @@ -105,7 +105,7 @@ # name: test_sensor_state[Air Purifier 131s][sensor.air_purifier_131s_filter_life] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Air Purifier 131s Filter Life', + 'friendly_name': 'Air Purifier 131s Filter life', 'state_class': , 'unit_of_measurement': '%', }), @@ -161,7 +161,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.air_purifier_200s_filter_life', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -170,10 +170,10 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Air Purifier 200s Filter Life', + 'original_name': 'Filter life', 'platform': 'vesync', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'filter_life', 'unique_id': 'asd_sdfKIHG7IJHGwJGJ7GJ_ag5h3G55-filter-life', 'unit_of_measurement': '%', }), @@ -182,7 +182,7 @@ # name: test_sensor_state[Air Purifier 200s][sensor.air_purifier_200s_filter_life] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Air Purifier 200s Filter Life', + 'friendly_name': 'Air Purifier 200s Filter life', 'state_class': , 'unit_of_measurement': '%', }), @@ -238,7 +238,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.air_purifier_400s_filter_life', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -247,10 +247,10 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Air Purifier 400s Filter Life', + 'original_name': 'Filter life', 'platform': 'vesync', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'filter_life', 'unique_id': '400s-purifier-filter-life', 'unit_of_measurement': '%', }), @@ -266,7 +266,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.air_purifier_400s_air_quality', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -275,10 +275,10 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Air Purifier 400s Air Quality', + 'original_name': 'Air quality', 'platform': 'vesync', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'air_quality', 'unique_id': '400s-purifier-air-quality', 'unit_of_measurement': None, }), @@ -296,7 +296,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.air_purifier_400s_pm2_5', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -305,7 +305,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Air Purifier 400s PM2.5', + 'original_name': 'PM2.5', 'platform': 'vesync', 'supported_features': 0, 'translation_key': None, @@ -317,7 +317,7 @@ # name: test_sensor_state[Air Purifier 400s][sensor.air_purifier_400s_air_quality] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Air Purifier 400s Air Quality', + 'friendly_name': 'Air Purifier 400s Air quality', }), 'context': , 'entity_id': 'sensor.air_purifier_400s_air_quality', @@ -329,7 +329,7 @@ # name: test_sensor_state[Air Purifier 400s][sensor.air_purifier_400s_filter_life] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Air Purifier 400s Filter Life', + 'friendly_name': 'Air Purifier 400s Filter life', 'state_class': , 'unit_of_measurement': '%', }), @@ -400,7 +400,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.air_purifier_600s_filter_life', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -409,10 +409,10 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Air Purifier 600s Filter Life', + 'original_name': 'Filter life', 'platform': 'vesync', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'filter_life', 'unique_id': '600s-purifier-filter-life', 'unit_of_measurement': '%', }), @@ -428,7 +428,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.air_purifier_600s_air_quality', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -437,10 +437,10 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Air Purifier 600s Air Quality', + 'original_name': 'Air quality', 'platform': 'vesync', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'air_quality', 'unique_id': '600s-purifier-air-quality', 'unit_of_measurement': None, }), @@ -458,7 +458,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.air_purifier_600s_pm2_5', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -467,7 +467,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Air Purifier 600s PM2.5', + 'original_name': 'PM2.5', 'platform': 'vesync', 'supported_features': 0, 'translation_key': None, @@ -479,7 +479,7 @@ # name: test_sensor_state[Air Purifier 600s][sensor.air_purifier_600s_air_quality] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Air Purifier 600s Air Quality', + 'friendly_name': 'Air Purifier 600s Air quality', }), 'context': , 'entity_id': 'sensor.air_purifier_600s_air_quality', @@ -491,7 +491,7 @@ # name: test_sensor_state[Air Purifier 600s][sensor.air_purifier_600s_filter_life] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Air Purifier 600s Filter Life', + 'friendly_name': 'Air Purifier 600s Filter life', 'state_class': , 'unit_of_measurement': '%', }), @@ -644,7 +644,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.outlet_current_power', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -653,10 +653,10 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Outlet current power', + 'original_name': 'Current power', 'platform': 'vesync', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'current_power', 'unique_id': 'outlet-power', 'unit_of_measurement': , }), @@ -674,7 +674,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.outlet_energy_use_today', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -683,10 +683,10 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Outlet energy use today', + 'original_name': 'Energy use today', 'platform': 'vesync', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'energy_today', 'unique_id': 'outlet-energy', 'unit_of_measurement': , }), @@ -704,7 +704,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.outlet_energy_use_weekly', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -713,10 +713,10 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Outlet energy use weekly', + 'original_name': 'Energy use weekly', 'platform': 'vesync', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'energy_week', 'unique_id': 'outlet-energy-weekly', 'unit_of_measurement': , }), @@ -734,7 +734,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.outlet_energy_use_monthly', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -743,10 +743,10 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Outlet energy use monthly', + 'original_name': 'Energy use monthly', 'platform': 'vesync', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'energy_month', 'unique_id': 'outlet-energy-monthly', 'unit_of_measurement': , }), @@ -764,7 +764,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.outlet_energy_use_yearly', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -773,10 +773,10 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Outlet energy use yearly', + 'original_name': 'Energy use yearly', 'platform': 'vesync', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'energy_year', 'unique_id': 'outlet-energy-yearly', 'unit_of_measurement': , }), @@ -794,7 +794,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.outlet_current_voltage', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -803,10 +803,10 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Outlet current voltage', + 'original_name': 'Current voltage', 'platform': 'vesync', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'current_voltage', 'unique_id': 'outlet-voltage', 'unit_of_measurement': , }), @@ -816,7 +816,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', - 'friendly_name': 'Outlet current power', + 'friendly_name': 'Outlet Current power', 'state_class': , 'unit_of_measurement': , }), @@ -831,7 +831,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', - 'friendly_name': 'Outlet current voltage', + 'friendly_name': 'Outlet Current voltage', 'state_class': , 'unit_of_measurement': , }), @@ -846,7 +846,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Outlet energy use monthly', + 'friendly_name': 'Outlet Energy use monthly', 'state_class': , 'unit_of_measurement': , }), @@ -861,7 +861,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Outlet energy use today', + 'friendly_name': 'Outlet Energy use today', 'state_class': , 'unit_of_measurement': , }), @@ -876,7 +876,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Outlet energy use weekly', + 'friendly_name': 'Outlet Energy use weekly', 'state_class': , 'unit_of_measurement': , }), @@ -891,7 +891,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Outlet energy use yearly', + 'friendly_name': 'Outlet Energy use yearly', 'state_class': , 'unit_of_measurement': , }), diff --git a/tests/components/vesync/snapshots/test_switch.ambr b/tests/components/vesync/snapshots/test_switch.ambr index 77f4011a532916..cfe9d66a2ed9c8 100644 --- a/tests/components/vesync/snapshots/test_switch.ambr +++ b/tests/components/vesync/snapshots/test_switch.ambr @@ -256,7 +256,7 @@ 'domain': 'switch', 'entity_category': None, 'entity_id': 'switch.outlet', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -265,7 +265,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Outlet', + 'original_name': None, 'platform': 'vesync', 'supported_features': 0, 'translation_key': None, @@ -362,7 +362,7 @@ 'domain': 'switch', 'entity_category': None, 'entity_id': 'switch.wall_switch', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -371,7 +371,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Wall Switch', + 'original_name': None, 'platform': 'vesync', 'supported_features': 0, 'translation_key': None, From a4d4eb3871d689aa53cf26a5caae2b538a64ac7a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 17 Jul 2023 17:56:39 +0200 Subject: [PATCH 0556/1009] Remove support for mqtt climate option CONF_POWER_STATE_TOPIC and template (#96771) Remove support CONF_POWER_STATE_TOPIC and template --- homeassistant/components/mqtt/climate.py | 25 ++++++++++-------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 40ec754aa44076..676e5b50f49440 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -109,10 +109,8 @@ CONF_HUMIDITY_MAX = "max_humidity" CONF_HUMIDITY_MIN = "min_humidity" -# CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE -# are deprecated, support for CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE -# was already removed or never added support was deprecated with release 2023.2 -# and will be removed with release 2023.8 +# Support for CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE +# was removed in HA Core 2023.8 CONF_POWER_STATE_TEMPLATE = "power_state_template" CONF_POWER_STATE_TOPIC = "power_state_topic" @@ -352,12 +350,10 @@ def valid_humidity_state_configuration(config: ConfigType) -> ConfigType: ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) PLATFORM_SCHEMA_MODERN = vol.All( - # CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE - # are deprecated, support for CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE - # was already removed or never added support was deprecated with release 2023.2 - # and will be removed with release 2023.8 - cv.deprecated(CONF_POWER_STATE_TEMPLATE), - cv.deprecated(CONF_POWER_STATE_TOPIC), + # Support for CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE + # was removed in HA Core 2023.8 + cv.removed(CONF_POWER_STATE_TEMPLATE), + cv.removed(CONF_POWER_STATE_TOPIC), _PLATFORM_SCHEMA_BASE, valid_preset_mode_configuration, valid_humidity_range_configuration, @@ -368,11 +364,10 @@ def valid_humidity_state_configuration(config: ConfigType) -> ConfigType: DISCOVERY_SCHEMA = vol.All( _DISCOVERY_SCHEMA_BASE, - # CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE are deprecated, - # support for CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE was already removed or never added - # support was deprecated with release 2023.2 and will be removed with release 2023.8 - cv.deprecated(CONF_POWER_STATE_TEMPLATE), - cv.deprecated(CONF_POWER_STATE_TOPIC), + # Support for CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE + # was removed in HA Core 2023.8 + cv.removed(CONF_POWER_STATE_TEMPLATE), + cv.removed(CONF_POWER_STATE_TOPIC), valid_preset_mode_configuration, valid_humidity_range_configuration, valid_humidity_state_configuration, From aa87f0ad546d95028cfa62ab57a69f260fb11189 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Jul 2023 06:32:38 -1000 Subject: [PATCH 0557/1009] Switch homekit_controller to use subscriber lookups (#96739) --- .../homekit_controller/connection.py | 37 ++++++++++++++++--- .../components/homekit_controller/entity.py | 18 +-------- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 314db187b6a3ee..6ef5917a0fb1c4 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -22,11 +22,10 @@ from homeassistant.components.thread.dataset_store import async_get_preferred_dataset from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_VIA_DEVICE, EVENT_HOMEASSISTANT_STARTED -from homeassistant.core import CoreState, Event, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval @@ -116,8 +115,6 @@ def __init__( self.available = False - self.signal_state_updated = "_".join((DOMAIN, self.unique_id, "state_updated")) - self.pollable_characteristics: list[tuple[int, int]] = [] # Never allow concurrent polling of the same accessory or bridge @@ -138,6 +135,9 @@ def __init__( function=self.async_update, ) + self._all_subscribers: set[CALLBACK_TYPE] = set() + self._subscriptions: dict[tuple[int, int], set[CALLBACK_TYPE]] = {} + @property def entity_map(self) -> Accessories: """Return the accessories from the pairing.""" @@ -182,7 +182,8 @@ def async_set_available_state(self, available: bool) -> None: if self.available == available: return self.available = available - async_dispatcher_send(self.hass, self.signal_state_updated) + for callback_ in self._all_subscribers: + callback_() async def _async_populate_ble_accessory_state(self, event: Event) -> None: """Populate the BLE accessory state without blocking startup. @@ -768,7 +769,31 @@ def process_new_events( self.entity_map.process_changes(new_values_dict) - async_dispatcher_send(self.hass, self.signal_state_updated, new_values_dict) + to_callback: set[CALLBACK_TYPE] = set() + for aid_iid in new_values_dict: + if callbacks := self._subscriptions.get(aid_iid): + to_callback.update(callbacks) + + for callback_ in to_callback: + callback_() + + @callback + def async_subscribe( + self, characteristics: Iterable[tuple[int, int]], callback_: CALLBACK_TYPE + ) -> CALLBACK_TYPE: + """Add characteristics to the watch list.""" + self._all_subscribers.add(callback_) + for aid_iid in characteristics: + self._subscriptions.setdefault(aid_iid, set()).add(callback_) + + def _unsub(): + self._all_subscribers.remove(callback_) + for aid_iid in characteristics: + self._subscriptions[aid_iid].remove(callback_) + if not self._subscriptions[aid_iid]: + del self._subscriptions[aid_iid] + + return _unsub async def get_characteristics(self, *args: Any, **kwargs: Any) -> dict[str, Any]: """Read latest state from homekit accessory.""" diff --git a/homeassistant/components/homekit_controller/entity.py b/homeassistant/components/homekit_controller/entity.py index 5a687020eb65ff..f6aadfac7ac90d 100644 --- a/homeassistant/components/homekit_controller/entity.py +++ b/homeassistant/components/homekit_controller/entity.py @@ -11,8 +11,6 @@ ) from aiohomekit.model.services import Service, ServicesTypes -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.typing import ConfigType @@ -53,23 +51,11 @@ def service(self) -> Service: """Return a Service model that this entity is attached to.""" return self.accessory.services.iid(self._iid) - @callback - def _async_state_changed( - self, new_values_dict: dict[tuple[int, int], dict[str, Any]] | None = None - ) -> None: - """Handle when characteristics change value.""" - if new_values_dict is None or self.all_characteristics.intersection( - new_values_dict - ): - self.async_write_ha_state() - async def async_added_to_hass(self) -> None: """Entity added to hass.""" self.async_on_remove( - async_dispatcher_connect( - self.hass, - self._accessory.signal_state_updated, - self._async_state_changed, + self._accessory.async_subscribe( + self.all_characteristics, self._async_write_ha_state ) ) From 31dfa5561a65ecc55fbf93d97b789d63638bd1d2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 17 Jul 2023 19:07:24 +0000 Subject: [PATCH 0558/1009] Add external power sensor for Shelly Plus HT (#96768) * Add external power sensor for Plus HT * Tests --- homeassistant/components/shelly/binary_sensor.py | 8 ++++++++ tests/components/shelly/conftest.py | 1 + tests/components/shelly/test_binary_sensor.py | 5 +++++ 3 files changed, 14 insertions(+) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 1474906cacb1fe..a5889cd11a75c8 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -164,6 +164,14 @@ class RestBinarySensorDescription(RestEntityDescription, BinarySensorEntityDescr entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), + "external_power": RpcBinarySensorDescription( + key="devicepower:0", + sub_key="external", + name="External power", + value=lambda status, _: status["present"], + device_class=BinarySensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + ), "overtemp": RpcBinarySensorDescription( key="switch", sub_key="errors", diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 2a80233aeb9939..96e888d7509713 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -189,6 +189,7 @@ def mock_light_set_state( "current_pos": 50, "apower": 85.3, }, + "devicepower:0": {"external": {"present": True}}, "temperature:0": {"tC": 22.9}, "illuminance:0": {"lux": 345}, "sys": { diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index 207b73bf44b84d..c067f5dffc9175 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -218,6 +218,11 @@ async def test_rpc_sleeping_binary_sensor( assert hass.states.get(entity_id).state == STATE_ON + # test external power sensor + state = hass.states.get("binary_sensor.test_name_external_power") + assert state + assert state.state == STATE_ON + async def test_rpc_restored_sleeping_binary_sensor( hass: HomeAssistant, mock_rpc_device, device_reg, monkeypatch From 36cb3f72781f4d4441fe760c3b105d4ed0edda39 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 17 Jul 2023 21:12:24 +0200 Subject: [PATCH 0559/1009] Protect entities for availability in gardena bluetooth (#96776) Protect entities for availability --- homeassistant/components/gardena_bluetooth/number.py | 3 ++- homeassistant/components/gardena_bluetooth/sensor.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/gardena_bluetooth/number.py b/homeassistant/components/gardena_bluetooth/number.py index 50cc209e26868c..5f7f870302b990 100644 --- a/homeassistant/components/gardena_bluetooth/number.py +++ b/homeassistant/components/gardena_bluetooth/number.py @@ -90,7 +90,8 @@ async def async_setup_entry( for description in DESCRIPTIONS if description.key in coordinator.characteristics ] - entities.append(GardenaBluetoothRemainingOpenSetNumber(coordinator)) + if Valve.remaining_open_time.uuid in coordinator.characteristics: + entities.append(GardenaBluetoothRemainingOpenSetNumber(coordinator)) async_add_entities(entities) diff --git a/homeassistant/components/gardena_bluetooth/sensor.py b/homeassistant/components/gardena_bluetooth/sensor.py index 0c8558419e2535..d7cf914b9df7a6 100644 --- a/homeassistant/components/gardena_bluetooth/sensor.py +++ b/homeassistant/components/gardena_bluetooth/sensor.py @@ -60,7 +60,8 @@ async def async_setup_entry( for description in DESCRIPTIONS if description.key in coordinator.characteristics ] - entities.append(GardenaBluetoothRemainSensor(coordinator)) + if Valve.remaining_open_time.uuid in coordinator.characteristics: + entities.append(GardenaBluetoothRemainSensor(coordinator)) async_add_entities(entities) From d80b7d0145c15d2dd593a84d98c4f6704b82fdb8 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 17 Jul 2023 21:12:41 +0200 Subject: [PATCH 0560/1009] Add base class to gardena bluetooth entities (#96775) Add helper base class for gardena entities --- .../gardena_bluetooth/coordinator.py | 14 +++++++++++- .../components/gardena_bluetooth/number.py | 22 +++++++------------ .../components/gardena_bluetooth/sensor.py | 19 +++++----------- .../components/gardena_bluetooth/switch.py | 5 +---- 4 files changed, 28 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/gardena_bluetooth/coordinator.py b/homeassistant/components/gardena_bluetooth/coordinator.py index 997c78d0f00556..9f5dc3223b5c5b 100644 --- a/homeassistant/components/gardena_bluetooth/coordinator.py +++ b/homeassistant/components/gardena_bluetooth/coordinator.py @@ -15,7 +15,7 @@ from homeassistant.components import bluetooth from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -119,3 +119,15 @@ def available(self) -> bool: return super().available and bluetooth.async_address_present( self.hass, self.coordinator.address, True ) + + +class GardenaBluetoothDescriptorEntity(GardenaBluetoothEntity): + """Coordinator entity for entities with entity description.""" + + def __init__( + self, coordinator: Coordinator, description: EntityDescription + ) -> None: + """Initialize description entity.""" + super().__init__(coordinator, {description.key}) + self._attr_unique_id = f"{coordinator.address}-{description.key}" + self.entity_description = description diff --git a/homeassistant/components/gardena_bluetooth/number.py b/homeassistant/components/gardena_bluetooth/number.py index 5f7f870302b990..ec7ae513a3ed2e 100644 --- a/homeassistant/components/gardena_bluetooth/number.py +++ b/homeassistant/components/gardena_bluetooth/number.py @@ -21,7 +21,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .coordinator import Coordinator, GardenaBluetoothEntity +from .coordinator import ( + Coordinator, + GardenaBluetoothDescriptorEntity, + GardenaBluetoothEntity, +) @dataclass @@ -95,24 +99,14 @@ async def async_setup_entry( async_add_entities(entities) -class GardenaBluetoothNumber(GardenaBluetoothEntity, NumberEntity): +class GardenaBluetoothNumber(GardenaBluetoothDescriptorEntity, NumberEntity): """Representation of a number.""" entity_description: GardenaBluetoothNumberEntityDescription - def __init__( - self, - coordinator: Coordinator, - description: GardenaBluetoothNumberEntityDescription, - ) -> None: - """Initialize the number entity.""" - super().__init__(coordinator, {description.key}) - self._attr_unique_id = f"{coordinator.address}-{description.key}" - self.entity_description = description - def _handle_coordinator_update(self) -> None: - if data := self.coordinator.data.get(self.entity_description.char.uuid): - self._attr_native_value = float(self.entity_description.char.decode(data)) + if data := self.coordinator.get_cached(self.entity_description.char): + self._attr_native_value = float(data) else: self._attr_native_value = None super()._handle_coordinator_update() diff --git a/homeassistant/components/gardena_bluetooth/sensor.py b/homeassistant/components/gardena_bluetooth/sensor.py index d7cf914b9df7a6..eaa44d9d4fb67e 100644 --- a/homeassistant/components/gardena_bluetooth/sensor.py +++ b/homeassistant/components/gardena_bluetooth/sensor.py @@ -20,7 +20,11 @@ import homeassistant.util.dt as dt_util from .const import DOMAIN -from .coordinator import Coordinator, GardenaBluetoothEntity +from .coordinator import ( + Coordinator, + GardenaBluetoothDescriptorEntity, + GardenaBluetoothEntity, +) @dataclass @@ -65,22 +69,11 @@ async def async_setup_entry( async_add_entities(entities) -class GardenaBluetoothSensor(GardenaBluetoothEntity, SensorEntity): +class GardenaBluetoothSensor(GardenaBluetoothDescriptorEntity, SensorEntity): """Representation of a sensor.""" entity_description: GardenaBluetoothSensorEntityDescription - def __init__( - self, - coordinator: Coordinator, - description: GardenaBluetoothSensorEntityDescription, - ) -> None: - """Initialize the sensor.""" - super().__init__(coordinator, {description.key}) - self._attr_native_value = None - self._attr_unique_id = f"{coordinator.address}-{description.key}" - self.entity_description = description - def _handle_coordinator_update(self) -> None: value = self.coordinator.get_cached(self.entity_description.char) if isinstance(value, datetime): diff --git a/homeassistant/components/gardena_bluetooth/switch.py b/homeassistant/components/gardena_bluetooth/switch.py index e3fcc8978c7683..adb23c74c1d1a3 100644 --- a/homeassistant/components/gardena_bluetooth/switch.py +++ b/homeassistant/components/gardena_bluetooth/switch.py @@ -51,10 +51,7 @@ def __init__( self._attr_is_on = None def _handle_coordinator_update(self) -> None: - if data := self.coordinator.data.get(Valve.state.uuid): - self._attr_is_on = Valve.state.decode(data) - else: - self._attr_is_on = None + self._attr_is_on = self.coordinator.get_cached(Valve.state) super()._handle_coordinator_update() async def async_turn_on(self, **kwargs: Any) -> None: From d02bf837a6cdb5e44e029bca39035212e2958d24 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 17 Jul 2023 21:13:13 +0200 Subject: [PATCH 0561/1009] Add some basic tests for gardena (#96777) --- .../components/gardena_bluetooth/__init__.py | 26 ++++++ .../components/gardena_bluetooth/conftest.py | 61 ++++++++++++- .../snapshots/test_init.ambr | 28 ++++++ .../snapshots/test_number.ambr | 86 +++++++++++++++++++ .../snapshots/test_sensor.ambr | 57 ++++++++++++ .../components/gardena_bluetooth/test_init.py | 58 +++++++++++++ .../gardena_bluetooth/test_number.py | 60 +++++++++++++ .../gardena_bluetooth/test_sensor.py | 52 +++++++++++ 8 files changed, 426 insertions(+), 2 deletions(-) create mode 100644 tests/components/gardena_bluetooth/snapshots/test_init.ambr create mode 100644 tests/components/gardena_bluetooth/snapshots/test_number.ambr create mode 100644 tests/components/gardena_bluetooth/snapshots/test_sensor.ambr create mode 100644 tests/components/gardena_bluetooth/test_init.py create mode 100644 tests/components/gardena_bluetooth/test_number.py create mode 100644 tests/components/gardena_bluetooth/test_sensor.py diff --git a/tests/components/gardena_bluetooth/__init__.py b/tests/components/gardena_bluetooth/__init__.py index 6a064409e9e2f3..a5ea94088fd522 100644 --- a/tests/components/gardena_bluetooth/__init__.py +++ b/tests/components/gardena_bluetooth/__init__.py @@ -1,7 +1,18 @@ """Tests for the Gardena Bluetooth integration.""" +from unittest.mock import patch + +from homeassistant.components.gardena_bluetooth.const import DOMAIN +from homeassistant.components.gardena_bluetooth.coordinator import Coordinator +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from tests.common import MockConfigEntry +from tests.components.bluetooth import ( + inject_bluetooth_service_info, +) + WATER_TIMER_SERVICE_INFO = BluetoothServiceInfo( name="Timer", address="00000000-0000-0000-0000-000000000001", @@ -59,3 +70,18 @@ service_uuids=["98bd0001-0b0e-421a-84e5-ddbf75dc6de4"], source="local", ) + + +async def setup_entry( + hass: HomeAssistant, mock_entry: MockConfigEntry, platforms: list[Platform] +) -> Coordinator: + """Make sure the device is available.""" + + inject_bluetooth_service_info(hass, WATER_TIMER_SERVICE_INFO) + + with patch("homeassistant.components.gardena_bluetooth.PLATFORMS", platforms): + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + return hass.data[DOMAIN][mock_entry.entry_id] diff --git a/tests/components/gardena_bluetooth/conftest.py b/tests/components/gardena_bluetooth/conftest.py index f09a274742f617..a4d7170e945bfe 100644 --- a/tests/components/gardena_bluetooth/conftest.py +++ b/tests/components/gardena_bluetooth/conftest.py @@ -1,10 +1,30 @@ """Common fixtures for the Gardena Bluetooth tests.""" from collections.abc import Generator +from typing import Any from unittest.mock import AsyncMock, Mock, patch +from freezegun import freeze_time from gardena_bluetooth.client import Client +from gardena_bluetooth.const import DeviceInformation +from gardena_bluetooth.exceptions import CharacteristicNotFound +from gardena_bluetooth.parse import Characteristic import pytest +from homeassistant.components.gardena_bluetooth.const import DOMAIN +from homeassistant.const import CONF_ADDRESS + +from . import WATER_TIMER_SERVICE_INFO + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_entry(): + """Create hass config fixture.""" + return MockConfigEntry( + domain=DOMAIN, data={CONF_ADDRESS: WATER_TIMER_SERVICE_INFO.address} + ) + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock, None, None]: @@ -16,15 +36,52 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: yield mock_setup_entry +@pytest.fixture +def mock_read_char_raw(): + """Mock data on device.""" + return { + DeviceInformation.firmware_version.uuid: b"1.2.3", + DeviceInformation.model_number.uuid: b"Mock Model", + } + + @pytest.fixture(autouse=True) -def mock_client(enable_bluetooth): +def mock_client(enable_bluetooth: None, mock_read_char_raw: dict[str, Any]) -> None: """Auto mock bluetooth.""" client = Mock(spec_set=Client) - client.get_all_characteristics_uuid.return_value = set() + + SENTINEL = object() + + def _read_char(char: Characteristic, default: Any = SENTINEL): + try: + return char.decode(mock_read_char_raw[char.uuid]) + except KeyError: + if default is SENTINEL: + raise CharacteristicNotFound from KeyError + return default + + def _read_char_raw(uuid: str, default: Any = SENTINEL): + try: + return mock_read_char_raw[uuid] + except KeyError: + if default is SENTINEL: + raise CharacteristicNotFound from KeyError + return default + + def _all_char(): + return set(mock_read_char_raw.keys()) + + client.read_char.side_effect = _read_char + client.read_char_raw.side_effect = _read_char_raw + client.get_all_characteristics_uuid.side_effect = _all_char with patch( "homeassistant.components.gardena_bluetooth.config_flow.Client", return_value=client, + ), patch( + "homeassistant.components.gardena_bluetooth.Client", return_value=client + ), freeze_time( + "2023-01-01", tz_offset=1 ): yield client diff --git a/tests/components/gardena_bluetooth/snapshots/test_init.ambr b/tests/components/gardena_bluetooth/snapshots/test_init.ambr new file mode 100644 index 00000000000000..a3ecff80a4638a --- /dev/null +++ b/tests/components/gardena_bluetooth/snapshots/test_init.ambr @@ -0,0 +1,28 @@ +# serializer version: 1 +# name: test_setup + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'gardena_bluetooth', + '00000000-0000-0000-0000-000000000001', + ), + }), + 'is_new': False, + 'manufacturer': None, + 'model': 'Mock Model', + 'name': 'Mock Title', + 'name_by_user': None, + 'suggested_area': None, + 'sw_version': '1.2.3', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/gardena_bluetooth/snapshots/test_number.ambr b/tests/components/gardena_bluetooth/snapshots/test_number.ambr new file mode 100644 index 00000000000000..a12cce060194d9 --- /dev/null +++ b/tests/components/gardena_bluetooth/snapshots/test_number.ambr @@ -0,0 +1,86 @@ +# serializer version: 1 +# name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-number.mock_title_remaining_open_time] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Remaining open time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_remaining_open_time', + 'last_changed': , + 'last_updated': , + 'state': '100.0', + }) +# --- +# name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-number.mock_title_remaining_open_time].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Remaining open time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_remaining_open_time', + 'last_changed': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw2-number.mock_title_open_for] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Open for', + 'max': 1440, + 'min': 0.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'number.mock_title_open_for', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[98bd0f14-0b0e-421a-84e5-ddbf75dc6de4-raw0-number.mock_title_manual_watering_time] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Manual watering time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_manual_watering_time', + 'last_changed': , + 'last_updated': , + 'state': '100.0', + }) +# --- +# name: test_setup[98bd0f14-0b0e-421a-84e5-ddbf75dc6de4-raw0-number.mock_title_manual_watering_time].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Manual watering time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_manual_watering_time', + 'last_changed': , + 'last_updated': , + 'state': '10.0', + }) +# --- diff --git a/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr b/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr new file mode 100644 index 00000000000000..883f377c3a59af --- /dev/null +++ b/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr @@ -0,0 +1,57 @@ +# serializer version: 1 +# name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-sensor.mock_title_valve_closing] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Mock Title Valve closing', + }), + 'context': , + 'entity_id': 'sensor.mock_title_valve_closing', + 'last_changed': , + 'last_updated': , + 'state': '2023-01-01T01:01:40+00:00', + }) +# --- +# name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-sensor.mock_title_valve_closing].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Mock Title Valve closing', + }), + 'context': , + 'entity_id': 'sensor.mock_title_valve_closing', + 'last_changed': , + 'last_updated': , + 'state': '2023-01-01T01:00:10+00:00', + }) +# --- +# name: test_setup[98bd2a19-0b0e-421a-84e5-ddbf75dc6de4-raw0-sensor.mock_title_battery] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Mock Title Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.mock_title_battery', + 'last_changed': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_setup[98bd2a19-0b0e-421a-84e5-ddbf75dc6de4-raw0-sensor.mock_title_battery].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Mock Title Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.mock_title_battery', + 'last_changed': , + 'last_updated': , + 'state': '10', + }) +# --- diff --git a/tests/components/gardena_bluetooth/test_init.py b/tests/components/gardena_bluetooth/test_init.py new file mode 100644 index 00000000000000..3ad7e6dce619f1 --- /dev/null +++ b/tests/components/gardena_bluetooth/test_init.py @@ -0,0 +1,58 @@ +"""Test the Gardena Bluetooth setup.""" + +from datetime import timedelta +from unittest.mock import Mock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.gardena_bluetooth import DeviceUnavailable +from homeassistant.components.gardena_bluetooth.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.util import utcnow + +from . import WATER_TIMER_SERVICE_INFO + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_setup( + hass: HomeAssistant, + mock_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test setup creates expected devices.""" + + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert mock_entry.state is ConfigEntryState.LOADED + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device( + identifiers={(DOMAIN, WATER_TIMER_SERVICE_INFO.address)} + ) + assert device == snapshot + + +async def test_setup_retry( + hass: HomeAssistant, mock_entry: MockConfigEntry, mock_client: Mock +) -> None: + """Test setup creates expected devices.""" + + original_read_char = mock_client.read_char.side_effect + mock_client.read_char.side_effect = DeviceUnavailable + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert mock_entry.state is ConfigEntryState.SETUP_RETRY + + mock_client.read_char.side_effect = original_read_char + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + + assert mock_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/gardena_bluetooth/test_number.py b/tests/components/gardena_bluetooth/test_number.py new file mode 100644 index 00000000000000..f1955905cce4eb --- /dev/null +++ b/tests/components/gardena_bluetooth/test_number.py @@ -0,0 +1,60 @@ +"""Test Gardena Bluetooth sensor.""" + + +from gardena_bluetooth.const import Valve +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from . import setup_entry + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("uuid", "raw", "entity_id"), + [ + ( + Valve.manual_watering_time.uuid, + [ + Valve.manual_watering_time.encode(100), + Valve.manual_watering_time.encode(10), + ], + "number.mock_title_manual_watering_time", + ), + ( + Valve.remaining_open_time.uuid, + [ + Valve.remaining_open_time.encode(100), + Valve.remaining_open_time.encode(10), + ], + "number.mock_title_remaining_open_time", + ), + ( + Valve.remaining_open_time.uuid, + [Valve.remaining_open_time.encode(100)], + "number.mock_title_open_for", + ), + ], +) +async def test_setup( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_read_char_raw: dict[str, bytes], + uuid: str, + raw: list[bytes], + entity_id: str, +) -> None: + """Test setup creates expected entities.""" + + mock_read_char_raw[uuid] = raw[0] + coordinator = await setup_entry(hass, mock_entry, [Platform.NUMBER]) + assert hass.states.get(entity_id) == snapshot + + for char_raw in raw[1:]: + mock_read_char_raw[uuid] = char_raw + await coordinator.async_refresh() + assert hass.states.get(entity_id) == snapshot diff --git a/tests/components/gardena_bluetooth/test_sensor.py b/tests/components/gardena_bluetooth/test_sensor.py new file mode 100644 index 00000000000000..d7cdc205f50c58 --- /dev/null +++ b/tests/components/gardena_bluetooth/test_sensor.py @@ -0,0 +1,52 @@ +"""Test Gardena Bluetooth sensor.""" + + +from gardena_bluetooth.const import Battery, Valve +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from . import setup_entry + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("uuid", "raw", "entity_id"), + [ + ( + Battery.battery_level.uuid, + [Battery.battery_level.encode(100), Battery.battery_level.encode(10)], + "sensor.mock_title_battery", + ), + ( + Valve.remaining_open_time.uuid, + [ + Valve.remaining_open_time.encode(100), + Valve.remaining_open_time.encode(10), + ], + "sensor.mock_title_valve_closing", + ), + ], +) +async def test_setup( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_read_char_raw: dict[str, bytes], + uuid: str, + raw: list[bytes], + entity_id: str, +) -> None: + """Test setup creates expected entities.""" + + mock_read_char_raw[uuid] = raw[0] + coordinator = await setup_entry(hass, mock_entry, [Platform.SENSOR]) + assert hass.states.get(entity_id) == snapshot + + for char_raw in raw[1:]: + mock_read_char_raw[uuid] = char_raw + await coordinator.async_refresh() + assert hass.states.get(entity_id) == snapshot From 1e3fdcc4d196821dfba24c9b3df3f0827ea87fdd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 17 Jul 2023 21:22:50 +0200 Subject: [PATCH 0562/1009] 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 67c8412102d6a5..a3fe046409be44 100644 --- a/homeassistant/components/otbr/config_flow.py +++ b/homeassistant/components/otbr/config_flow.py @@ -130,6 +130,11 @@ async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResu 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 b6cb0df78cdc47..da25edde04548b 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -23,6 +23,12 @@ 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 8559af8232a19f9c078930186d60c441ac93383a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 17 Jul 2023 21:23:20 +0200 Subject: [PATCH 0563/1009] Remove extra otbr config entries (#96785) --- homeassistant/components/otbr/__init__.py | 3 +++ tests/components/otbr/__init__.py | 1 + tests/components/otbr/test_init.py | 31 +++++++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/homeassistant/components/otbr/__init__.py b/homeassistant/components/otbr/__init__.py index 8f8810b5f33b17..8685282acecced 100644 --- a/homeassistant/components/otbr/__init__.py +++ b/homeassistant/components/otbr/__init__.py @@ -24,6 +24,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Open Thread Border Router component.""" websocket_api.async_setup(hass) + if len(config_entries := hass.config_entries.async_entries(DOMAIN)): + for config_entry in config_entries[1:]: + await hass.config_entries.async_remove(config_entry.entry_id) return True diff --git a/tests/components/otbr/__init__.py b/tests/components/otbr/__init__.py index 1f103884db2825..e641f67dfaf645 100644 --- a/tests/components/otbr/__init__.py +++ b/tests/components/otbr/__init__.py @@ -1,6 +1,7 @@ """Tests for the Open Thread Border Router integration.""" BASE_URL = "http://core-silabs-multiprotocol:8081" CONFIG_ENTRY_DATA = {"url": "http://core-silabs-multiprotocol:8081"} +CONFIG_ENTRY_DATA_2 = {"url": "http://core-silabs-multiprotocol_2:8081"} DATASET_CH15 = bytes.fromhex( "0E080000000000010000000300000F35060004001FFFE00208F642646DA209B1D00708FDF57B5A" diff --git a/tests/components/otbr/test_init.py b/tests/components/otbr/test_init.py index 990c015244fc01..4ec99818b28bba 100644 --- a/tests/components/otbr/test_init.py +++ b/tests/components/otbr/test_init.py @@ -11,10 +11,12 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component from . import ( BASE_URL, CONFIG_ENTRY_DATA, + CONFIG_ENTRY_DATA_2, DATASET_CH15, DATASET_CH16, DATASET_INSECURE_NW_KEY, @@ -280,3 +282,32 @@ async def test_get_active_dataset_tlvs_invalid( aioclient_mock.get(f"{BASE_URL}/node/dataset/active", text="unexpected") with pytest.raises(HomeAssistantError): assert await otbr.async_get_active_dataset_tlvs(hass) + + +async def test_remove_extra_entries( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we remove additional config entries.""" + + config_entry1 = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=otbr.DOMAIN, + options={}, + title="Open Thread Border Router", + ) + config_entry2 = MockConfigEntry( + data=CONFIG_ENTRY_DATA_2, + domain=otbr.DOMAIN, + options={}, + title="Open Thread Border Router", + ) + config_entry1.add_to_hass(hass) + config_entry2.add_to_hass(hass) + assert len(hass.config_entries.async_entries(otbr.DOMAIN)) == 2 + with patch( + "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 + ), patch( + "homeassistant.components.otbr.util.compute_pskc" + ): # Patch to speed up tests + assert await async_setup_component(hass, otbr.DOMAIN, {}) + assert len(hass.config_entries.async_entries(otbr.DOMAIN)) == 1 From 863b36c0c30010c3ea79afc32bfaf46bb6687922 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 17 Jul 2023 21:26:15 +0200 Subject: [PATCH 0564/1009] Include addon name in otbr config entry title (#96786) --- homeassistant/components/otbr/config_flow.py | 4 ++-- tests/components/otbr/test_config_flow.py | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py index a3fe046409be44..f1219aaebf0ab4 100644 --- a/homeassistant/components/otbr/config_flow.py +++ b/homeassistant/components/otbr/config_flow.py @@ -51,10 +51,10 @@ async def _title(hass: HomeAssistant, discovery_info: HassioServiceInfo) -> str: device = addon_info.get("options", {}).get("device") if _is_yellow(hass) and device == "/dev/TTYAMA1": - return "Home Assistant Yellow" + return f"Home Assistant Yellow ({discovery_info.name})" if device and "SkyConnect" in device: - return "Home Assistant SkyConnect" + return f"Home Assistant SkyConnect ({discovery_info.name})" return discovery_info.name diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index da25edde04548b..d67a9c0ff0abda 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 @@ -307,7 +307,7 @@ async def test_hassio_discovery_flow_sky_connect( } assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Home Assistant SkyConnect" + assert result["title"] == "Home Assistant SkyConnect (Silicon Labs Multiprotocol)" assert result["data"] == expected_data assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -315,7 +315,9 @@ async def test_hassio_discovery_flow_sky_connect( 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.title == "Home Assistant SkyConnect (Silicon Labs Multiprotocol)" + ) assert config_entry.unique_id == HASSIO_DATA.uuid From 49a27bb9a75bd74effa131252329230a98c241d6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 17 Jul 2023 22:12:59 +0200 Subject: [PATCH 0565/1009] Fix otbr test (#96788) --- tests/components/otbr/test_config_flow.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index d67a9c0ff0abda..b7d42b661c90ac 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -380,7 +380,9 @@ async def _addon_info(hass, slug): } assert results[0]["type"] == FlowResultType.CREATE_ENTRY - assert results[0]["title"] == "Home Assistant SkyConnect" + assert ( + results[0]["title"] == "Home Assistant SkyConnect (Silicon Labs Multiprotocol)" + ) assert results[0]["data"] == expected_data assert results[0]["options"] == {} assert results[1]["type"] == FlowResultType.ABORT @@ -391,7 +393,9 @@ async def _addon_info(hass, slug): 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.title == "Home Assistant SkyConnect (Silicon Labs Multiprotocol)" + ) assert config_entry.unique_id == HASSIO_DATA.uuid From c79fa87a7f772528f73d7606bc9914c296bcae2b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 17 Jul 2023 22:21:52 +0200 Subject: [PATCH 0566/1009] Fix check for HA Yellow radio in otbr config flow (#96789) --- homeassistant/components/otbr/config_flow.py | 2 +- tests/components/otbr/test_config_flow.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py index f1219aaebf0ab4..35772c00a896b6 100644 --- a/homeassistant/components/otbr/config_flow.py +++ b/homeassistant/components/otbr/config_flow.py @@ -50,7 +50,7 @@ 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": + if _is_yellow(hass) and device == "/dev/ttyAMA1": return f"Home Assistant Yellow ({discovery_info.name})" if device and "SkyConnect" in device: diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index b7d42b661c90ac..deb8672b961b3d 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 8cccfcc9465aaa0e904650a71543f8cff03f8cb6 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 17 Jul 2023 15:58:05 -0500 Subject: [PATCH 0567/1009] Bump wyoming to 1.1 (#96778) --- homeassistant/components/wyoming/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wyoming/manifest.json b/homeassistant/components/wyoming/manifest.json index 7fbf3542e13dbc..810092094d15af 100644 --- a/homeassistant/components/wyoming/manifest.json +++ b/homeassistant/components/wyoming/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wyoming", "iot_class": "local_push", - "requirements": ["wyoming==1.0.0"] + "requirements": ["wyoming==1.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 81d11692c20f21..bd2903f501c1fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2681,7 +2681,7 @@ wled==0.16.0 wolf-smartset==0.1.11 # homeassistant.components.wyoming -wyoming==1.0.0 +wyoming==1.1.0 # homeassistant.components.xbox xbox-webapi==2.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8875c60adb2ee..f91c9616339e29 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1963,7 +1963,7 @@ wled==0.16.0 wolf-smartset==0.1.11 # homeassistant.components.wyoming -wyoming==1.0.0 +wyoming==1.1.0 # homeassistant.components.xbox xbox-webapi==2.0.11 From 564e618d0c9c34aef85052959886f80a5770df24 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 18 Jul 2023 00:37:02 +0200 Subject: [PATCH 0568/1009] Drop upper constraint for pip (#96738) --- .github/workflows/ci.yaml | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8fd01ada0e9d81..da7d73c272df79 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -492,7 +492,7 @@ jobs: python -m venv venv . venv/bin/activate python --version - pip install --cache-dir=$PIP_CACHE -U "pip>=21.3.1,<23.3" setuptools wheel + pip install --cache-dir=$PIP_CACHE -U "pip>=21.3.1" setuptools wheel pip install --cache-dir=$PIP_CACHE -r requirements_all.txt pip install --cache-dir=$PIP_CACHE -r requirements_test.txt pip install -e . --config-settings editable_mode=compat diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fca6e2fcb25467..03c472d106a6c5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ mutagen==1.46.0 orjson==3.9.2 paho-mqtt==1.6.1 Pillow==10.0.0 -pip>=21.3.1,<23.3 +pip>=21.3.1 psutil-home-assistant==0.0.1 PyJWT==2.7.0 PyNaCl==1.5.0 diff --git a/pyproject.toml b/pyproject.toml index 5ed5ad532249a9..6f1f3d9a351683 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ dependencies = [ # pyOpenSSL 23.2.0 is required to work with cryptography 41+ "pyOpenSSL==23.2.0", "orjson==3.9.2", - "pip>=21.3.1,<23.3", + "pip>=21.3.1", "python-slugify==4.0.1", "PyYAML==6.0", "requests==2.31.0", diff --git a/requirements.txt b/requirements.txt index 8de97cb6156b88..8f55e68cd3cb81 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ PyJWT==2.7.0 cryptography==41.0.2 pyOpenSSL==23.2.0 orjson==3.9.2 -pip>=21.3.1,<23.3 +pip>=21.3.1 python-slugify==4.0.1 PyYAML==6.0 requests==2.31.0 From 44aa531a5177ffeed58d704d7710336a4c078ee4 Mon Sep 17 00:00:00 2001 From: Mike Keesey Date: Mon, 17 Jul 2023 17:12:15 -0600 Subject: [PATCH 0569/1009] Alexa temperature adjustment handle multiple setpoint (#95821) * Alexa temperature adjustment handle multiple setpoint In "auto" mode with many thermostats, the thermostats expose both an upper and lower setpoint representing a range of temperatures. When a temperature delta is sent from Alexa (e.g. "lower by 2 degrees), we need to handle the case where the temperature property is not set, but instead the upper and lower setpoint properties are set. In this case, we adjust those properties via service call instead of the singular value. * Updating tests to fix coverage --- homeassistant/components/alexa/handlers.py | 58 +++++++++--- tests/components/alexa/test_smart_home.py | 101 ++++++++++++++++++++- 2 files changed, 145 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index eb23b09627e694..c1b99b017e536e 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -857,14 +857,55 @@ async def async_api_adjust_target_temp( temp_delta = temperature_from_object( hass, directive.payload["targetSetpointDelta"], interval=True ) - target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta - if target_temp < min_temp or target_temp > max_temp: - raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp) + response = directive.response() - data = {ATTR_ENTITY_ID: entity.entity_id, ATTR_TEMPERATURE: target_temp} + current_target_temp_high = entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH) + current_target_temp_low = entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW) + if current_target_temp_high and current_target_temp_low: + target_temp_high = float(current_target_temp_high) + temp_delta + if target_temp_high < min_temp or target_temp_high > max_temp: + raise AlexaTempRangeError(hass, target_temp_high, min_temp, max_temp) + + target_temp_low = float(current_target_temp_low) + temp_delta + if target_temp_low < min_temp or target_temp_low > max_temp: + raise AlexaTempRangeError(hass, target_temp_low, min_temp, max_temp) + + data = { + ATTR_ENTITY_ID: entity.entity_id, + climate.ATTR_TARGET_TEMP_HIGH: target_temp_high, + climate.ATTR_TARGET_TEMP_LOW: target_temp_low, + } + + response.add_context_property( + { + "name": "upperSetpoint", + "namespace": "Alexa.ThermostatController", + "value": {"value": target_temp_high, "scale": API_TEMP_UNITS[unit]}, + } + ) + response.add_context_property( + { + "name": "lowerSetpoint", + "namespace": "Alexa.ThermostatController", + "value": {"value": target_temp_low, "scale": API_TEMP_UNITS[unit]}, + } + ) + else: + target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta + + if target_temp < min_temp or target_temp > max_temp: + raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp) + + data = {ATTR_ENTITY_ID: entity.entity_id, ATTR_TEMPERATURE: target_temp} + response.add_context_property( + { + "name": "targetSetpoint", + "namespace": "Alexa.ThermostatController", + "value": {"value": target_temp, "scale": API_TEMP_UNITS[unit]}, + } + ) - response = directive.response() await hass.services.async_call( entity.domain, climate.SERVICE_SET_TEMPERATURE, @@ -872,13 +913,6 @@ async def async_api_adjust_target_temp( blocking=False, context=context, ) - response.add_context_property( - { - "name": "targetSetpoint", - "namespace": "Alexa.ThermostatController", - "value": {"value": target_temp, "scale": API_TEMP_UNITS[unit]}, - } - ) return response diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index a1f77a9b49b3d0..a2dcdedd4706e6 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -2176,8 +2176,8 @@ async def test_thermostat(hass: HomeAssistant) -> None: "cool", { "temperature": 70.0, - "target_temp_high": 80.0, - "target_temp_low": 60.0, + "target_temp_high": None, + "target_temp_low": None, "current_temperature": 75.0, "friendly_name": "Test Thermostat", "supported_features": 1 | 2 | 4 | 128, @@ -2439,6 +2439,103 @@ async def test_thermostat(hass: HomeAssistant) -> None: assert call.data["preset_mode"] == "eco" +async def test_thermostat_dual(hass: HomeAssistant) -> None: + """Test thermostat discovery with auto mode, with upper and lower target temperatures.""" + hass.config.units = US_CUSTOMARY_SYSTEM + device = ( + "climate.test_thermostat", + "auto", + { + "temperature": None, + "target_temp_high": 80.0, + "target_temp_low": 60.0, + "current_temperature": 75.0, + "friendly_name": "Test Thermostat", + "supported_features": 1 | 2 | 4 | 128, + "hvac_modes": ["off", "heat", "cool", "auto", "dry", "fan_only"], + "preset_mode": None, + "preset_modes": ["eco"], + "min_temp": 50, + "max_temp": 90, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "climate#test_thermostat" + assert appliance["displayCategories"][0] == "THERMOSTAT" + assert appliance["friendlyName"] == "Test Thermostat" + + assert_endpoint_capabilities( + appliance, + "Alexa.PowerController", + "Alexa.ThermostatController", + "Alexa.TemperatureSensor", + "Alexa.EndpointHealth", + "Alexa", + ) + + properties = await reported_properties(hass, "climate#test_thermostat") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "AUTO") + properties.assert_equal( + "Alexa.ThermostatController", + "upperSetpoint", + {"value": 80.0, "scale": "FAHRENHEIT"}, + ) + properties.assert_equal( + "Alexa.ThermostatController", + "lowerSetpoint", + {"value": 60.0, "scale": "FAHRENHEIT"}, + ) + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 75.0, "scale": "FAHRENHEIT"} + ) + + # Adjust temperature when in auto mode + call, msg = await assert_request_calls_service( + "Alexa.ThermostatController", + "AdjustTargetTemperature", + "climate#test_thermostat", + "climate.set_temperature", + hass, + payload={"targetSetpointDelta": {"value": -5.0, "scale": "KELVIN"}}, + ) + assert call.data["target_temp_high"] == 71.0 + assert call.data["target_temp_low"] == 51.0 + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal( + "Alexa.ThermostatController", + "upperSetpoint", + {"value": 71.0, "scale": "FAHRENHEIT"}, + ) + properties.assert_equal( + "Alexa.ThermostatController", + "lowerSetpoint", + {"value": 51.0, "scale": "FAHRENHEIT"}, + ) + + # Fails if the upper setpoint goes too high + msg = await assert_request_fails( + "Alexa.ThermostatController", + "AdjustTargetTemperature", + "climate#test_thermostat", + "climate.set_temperature", + hass, + payload={"targetSetpointDelta": {"value": 6.0, "scale": "CELSIUS"}}, + ) + assert msg["event"]["payload"]["type"] == "TEMPERATURE_VALUE_OUT_OF_RANGE" + + # Fails if the lower setpoint goes too low + msg = await assert_request_fails( + "Alexa.ThermostatController", + "AdjustTargetTemperature", + "climate#test_thermostat", + "climate.set_temperature", + hass, + payload={"targetSetpointDelta": {"value": -6.0, "scale": "CELSIUS"}}, + ) + assert msg["event"]["payload"]["type"] == "TEMPERATURE_VALUE_OUT_OF_RANGE" + + async def test_exclude_filters(hass: HomeAssistant) -> None: """Test exclusion filters.""" request = get_new_request("Alexa.Discovery", "Discover") From 771b5e34b7442b577c545db07409b9c797cd1daa Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 17 Jul 2023 16:42:31 -0700 Subject: [PATCH 0570/1009] Bump androidtvremote2 to 0.0.12 (#96796) Bump androidtvremote2==0.0.12 --- homeassistant/components/androidtv_remote/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv_remote/manifest.json b/homeassistant/components/androidtv_remote/manifest.json index c728ea0a682735..3feddacd4e5e4d 100644 --- a/homeassistant/components/androidtv_remote/manifest.json +++ b/homeassistant/components/androidtv_remote/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_push", "loggers": ["androidtvremote2"], "quality_scale": "platinum", - "requirements": ["androidtvremote2==0.0.9"], + "requirements": ["androidtvremote2==0.0.12"], "zeroconf": ["_androidtvremote2._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index bd2903f501c1fc..6f1e2bcba66fd0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -396,7 +396,7 @@ amcrest==1.9.7 androidtv[async]==0.0.70 # homeassistant.components.androidtv_remote -androidtvremote2==0.0.9 +androidtvremote2==0.0.12 # homeassistant.components.anel_pwrctrl anel-pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f91c9616339e29..f4bb633b7484f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -365,7 +365,7 @@ amberelectric==1.0.4 androidtv[async]==0.0.70 # homeassistant.components.androidtv_remote -androidtvremote2==0.0.9 +androidtvremote2==0.0.12 # homeassistant.components.anova anova-wifi==0.10.0 From eb60dc65ec0df7ce1e4166b6fc1af750472d783a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Jul 2023 15:35:37 -1000 Subject: [PATCH 0571/1009] Bump aioesphomeapi to 15.1.9 (#96791) --- 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 e5448fb395d765..a8665d76656f4c 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.1.7", + "aioesphomeapi==15.1.9", "bluetooth-data-tools==1.6.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 6f1e2bcba66fd0..16cb430d090dc1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==15.1.7 +aioesphomeapi==15.1.9 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4bb633b7484f2..30513062d102f9 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.1.7 +aioesphomeapi==15.1.9 # homeassistant.components.flo aioflo==2021.11.0 From a9f75228579084f6a8c47882802ef0c22ba0edbe Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 18 Jul 2023 07:22:48 +0200 Subject: [PATCH 0572/1009] Correct tests for gardena (#96806) --- tests/components/gardena_bluetooth/test_init.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/components/gardena_bluetooth/test_init.py b/tests/components/gardena_bluetooth/test_init.py index 3ad7e6dce619f1..b09d2177c22dab 100644 --- a/tests/components/gardena_bluetooth/test_init.py +++ b/tests/components/gardena_bluetooth/test_init.py @@ -3,6 +3,7 @@ from datetime import timedelta from unittest.mock import Mock +from gardena_bluetooth.const import Battery from syrupy.assertion import SnapshotAssertion from homeassistant.components.gardena_bluetooth import DeviceUnavailable @@ -20,10 +21,13 @@ async def test_setup( hass: HomeAssistant, mock_entry: MockConfigEntry, + mock_read_char_raw: dict[str, bytes], snapshot: SnapshotAssertion, ) -> None: """Test setup creates expected devices.""" + mock_read_char_raw[Battery.battery_level.uuid] = Battery.battery_level.encode(100) + mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() From ca2863a1b928ba904f8342614aa5edf1bfcc5e20 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Jul 2023 20:29:27 -1000 Subject: [PATCH 0573/1009] Bump aiohomekit to 2.6.8 (#96805) --- 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 2a9e2225e9f3be..82d91863fadf3c 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.7"], + "requirements": ["aiohomekit==2.6.8"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 16cb430d090dc1..19ef9194307278 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -249,7 +249,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==2.6.7 +aiohomekit==2.6.8 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30513062d102f9..655a165cf39d38 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.7 +aiohomekit==2.6.8 # homeassistant.components.emulated_hue # homeassistant.components.http From 4bf23fac6f50eeebd127b979212a7ea607e0f212 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 0574/1009] 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 03c472d106a6c5..927f0db3f01b2d 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.7.1 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 6f1f3d9a351683..4e608c36b97966 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ dependencies = [ "orjson==3.9.2", "pip>=21.3.1", "python-slugify==4.0.1", - "PyYAML==6.0", + "PyYAML==6.0.1", "requests==2.31.0", "typing-extensions>=4.7.0,<5.0", "ulid-transform==0.7.2", diff --git a/requirements.txt b/requirements.txt index 8f55e68cd3cb81..84fa6a0cbb457a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,7 @@ pyOpenSSL==23.2.0 orjson==3.9.2 pip>=21.3.1 python-slugify==4.0.1 -PyYAML==6.0 +PyYAML==6.0.1 requests==2.31.0 typing-extensions>=4.7.0,<5.0 ulid-transform==0.7.2 From 9e67bccb89de3ef3627ddf892cb422e8cbcf1295 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 18 Jul 2023 08:51:06 +0200 Subject: [PATCH 0575/1009] Replace EventType annotations with Event (#96426) --- homeassistant/components/dsmr/sensor.py | 4 ++-- homeassistant/components/group/media_player.py | 6 +++--- homeassistant/components/min_max/sensor.py | 3 +-- homeassistant/components/shelly/logbook.py | 7 +++---- homeassistant/components/shelly/utils.py | 5 ++--- homeassistant/helpers/typing.py | 2 +- 6 files changed, 12 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index e6d1d035e3b6e8..12ad3350e444c5 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -35,7 +35,7 @@ from homeassistant.core import CoreState, Event, HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import EventType, StateType +from homeassistant.helpers.typing import StateType from homeassistant.util import Throttle from .const import ( @@ -457,7 +457,7 @@ async def connect_and_reconnect() -> None: if transport: # Register listener to close transport on HA shutdown @callback - def close_transport(_event: EventType) -> None: + def close_transport(_event: Event) -> None: """Close the transport on HA shutdown.""" if not transport: # noqa: B023 return diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index b271e57cb8a0a8..ad375630beae5a 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -44,11 +44,11 @@ STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType KEY_ANNOUNCE = "announce" KEY_CLEAR_PLAYLIST = "clear_playlist" @@ -130,7 +130,7 @@ def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> Non } @callback - def async_on_state_change(self, event: EventType) -> None: + def async_on_state_change(self, event: Event) -> None: """Update supported features and state when a new state is received.""" self.async_set_context(event.context) self.async_update_supported_features( diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index d0064d075112ff..d1ea9695322973 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -30,7 +30,6 @@ from homeassistant.helpers.typing import ( ConfigType, DiscoveryInfoType, - EventType, StateType, ) @@ -287,7 +286,7 @@ def extra_state_attributes(self) -> dict[str, Any] | None: @callback def _async_min_max_sensor_state_listener( - self, event: EventType, update_state: bool = True + self, event: Event, update_state: bool = True ) -> None: """Handle the sensor state changes.""" new_state: State | None = event.data.get("new_state") diff --git a/homeassistant/components/shelly/logbook.py b/homeassistant/components/shelly/logbook.py index 57df5d1ab0a81f..d55ffe0fd28fdd 100644 --- a/homeassistant/components/shelly/logbook.py +++ b/homeassistant/components/shelly/logbook.py @@ -5,8 +5,7 @@ from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME from homeassistant.const import ATTR_DEVICE_ID -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.typing import EventType +from homeassistant.core import Event, HomeAssistant, callback from .const import ( ATTR_CHANNEL, @@ -27,12 +26,12 @@ @callback def async_describe_events( hass: HomeAssistant, - async_describe_event: Callable[[str, str, Callable[[EventType], dict]], None], + async_describe_event: Callable[[str, str, Callable[[Event], dict]], None], ) -> None: """Describe logbook events.""" @callback - def async_describe_shelly_click_event(event: EventType) -> dict[str, str]: + def async_describe_shelly_click_event(event: Event) -> dict[str, str]: """Describe shelly.click logbook event (block device).""" device_id = event.data[ATTR_DEVICE_ID] click_type = event.data[ATTR_CLICK_TYPE] diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 03df3da346bdb8..a66b77ed94b61d 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -12,7 +12,7 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import singleton from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, @@ -20,7 +20,6 @@ format_mac, ) from homeassistant.helpers.entity_registry import async_get as er_async_get -from homeassistant.helpers.typing import EventType from homeassistant.util.dt import utcnow from .const import ( @@ -211,7 +210,7 @@ async def get_coap_context(hass: HomeAssistant) -> COAP: await context.initialize(port) @callback - def shutdown_listener(ev: EventType) -> None: + def shutdown_listener(ev: Event) -> None: context.close() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_listener) diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 326d2f982594a7..5a76fd262a818f 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -9,7 +9,6 @@ ConfigType = dict[str, Any] ContextType = homeassistant.core.Context DiscoveryInfoType = dict[str, Any] -EventType = homeassistant.core.Event ServiceDataType = dict[str, Any] StateType = str | int | float | None TemplateVarsType = Mapping[str, Any] | None @@ -33,4 +32,5 @@ class UndefinedType(Enum): # that may rely on them. # In due time they will be removed. HomeAssistantType = homeassistant.core.HomeAssistant +EventType = homeassistant.core.Event ServiceCallType = homeassistant.core.ServiceCall From 2c949d56dcf5ee2919abf75ce845fbf3d32fba5a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 08:56:15 +0200 Subject: [PATCH 0576/1009] Migrate Traccar to has entity naming (#96760) --- homeassistant/components/traccar/device_tracker.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index a22b8a993f1b9c..ad31f20e3cfd43 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -365,8 +365,11 @@ async def import_events(self): class TraccarEntity(TrackerEntity, RestoreEntity): """Represent a tracked device.""" + _attr_has_entity_name = True + _attr_name = None + def __init__(self, device, latitude, longitude, battery, accuracy, attributes): - """Set up Geofency entity.""" + """Set up Traccar entity.""" self._accuracy = accuracy self._attributes = attributes self._name = device @@ -401,11 +404,6 @@ def location_accuracy(self): """Return the gps accuracy of the device.""" return self._accuracy - @property - def name(self): - """Return the name of the device.""" - return self._name - @property def unique_id(self): """Return the unique ID.""" @@ -468,7 +466,7 @@ def _async_receive_data( self, device, latitude, longitude, battery, accuracy, attributes ): """Mark the device as seen.""" - if device != self.name: + if device != self._name: return self._latitude = latitude From 878429fdecddf2f407225dd4e055ad11110070f5 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 18 Jul 2023 09:00:25 +0200 Subject: [PATCH 0577/1009] Add binary sensor for valve connectivity for gardena bluetooth (#96810) * Add binary_sensor to gardena * Add tests for binary_sensor --- .coveragerc | 1 + .../components/gardena_bluetooth/__init__.py | 7 +- .../gardena_bluetooth/binary_sensor.py | 64 +++++++++++++++++++ .../components/gardena_bluetooth/strings.json | 5 ++ .../snapshots/test_binary_sensor.ambr | 27 ++++++++ .../gardena_bluetooth/test_binary_sensor.py | 44 +++++++++++++ 6 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/gardena_bluetooth/binary_sensor.py create mode 100644 tests/components/gardena_bluetooth/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/gardena_bluetooth/test_binary_sensor.py diff --git a/.coveragerc b/.coveragerc index d160efb776c597..6cf3f66d8af3a7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -407,6 +407,7 @@ omit = homeassistant/components/garages_amsterdam/binary_sensor.py homeassistant/components/garages_amsterdam/sensor.py homeassistant/components/gardena_bluetooth/__init__.py + homeassistant/components/gardena_bluetooth/binary_sensor.py homeassistant/components/gardena_bluetooth/const.py homeassistant/components/gardena_bluetooth/coordinator.py homeassistant/components/gardena_bluetooth/number.py diff --git a/homeassistant/components/gardena_bluetooth/__init__.py b/homeassistant/components/gardena_bluetooth/__init__.py index 98869019d29588..c779d30b0fc3fe 100644 --- a/homeassistant/components/gardena_bluetooth/__init__.py +++ b/homeassistant/components/gardena_bluetooth/__init__.py @@ -20,7 +20,12 @@ from .const import DOMAIN from .coordinator import Coordinator, DeviceUnavailable -PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH] +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.NUMBER, + Platform.SENSOR, + Platform.SWITCH, +] LOGGER = logging.getLogger(__name__) TIMEOUT = 20.0 DISCONNECT_DELAY = 5 diff --git a/homeassistant/components/gardena_bluetooth/binary_sensor.py b/homeassistant/components/gardena_bluetooth/binary_sensor.py new file mode 100644 index 00000000000000..0285f7bdf822f3 --- /dev/null +++ b/homeassistant/components/gardena_bluetooth/binary_sensor.py @@ -0,0 +1,64 @@ +"""Support for binary_sensor entities.""" +from __future__ import annotations + +from dataclasses import dataclass, field + +from gardena_bluetooth.const import Valve +from gardena_bluetooth.parse import CharacteristicBool + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import Coordinator, GardenaBluetoothDescriptorEntity + + +@dataclass +class GardenaBluetoothBinarySensorEntityDescription(BinarySensorEntityDescription): + """Description of entity.""" + + char: CharacteristicBool = field(default_factory=lambda: CharacteristicBool("")) + + +DESCRIPTIONS = ( + GardenaBluetoothBinarySensorEntityDescription( + key=Valve.connected_state.uuid, + translation_key="valve_connected_state", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + char=Valve.connected_state, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up binary sensor based on a config entry.""" + coordinator: Coordinator = hass.data[DOMAIN][entry.entry_id] + entities = [ + GardenaBluetoothBinarySensor(coordinator, description) + for description in DESCRIPTIONS + if description.key in coordinator.characteristics + ] + async_add_entities(entities) + + +class GardenaBluetoothBinarySensor( + GardenaBluetoothDescriptorEntity, BinarySensorEntity +): + """Representation of a binary sensor.""" + + entity_description: GardenaBluetoothBinarySensorEntityDescription + + def _handle_coordinator_update(self) -> None: + char = self.entity_description.char + self._attr_is_on = self.coordinator.get_cached(char) + super()._handle_coordinator_update() diff --git a/homeassistant/components/gardena_bluetooth/strings.json b/homeassistant/components/gardena_bluetooth/strings.json index 3548412e04f19b..5a3f77eafa4fd2 100644 --- a/homeassistant/components/gardena_bluetooth/strings.json +++ b/homeassistant/components/gardena_bluetooth/strings.json @@ -19,6 +19,11 @@ } }, "entity": { + "binary_sensor": { + "valve_connected_state": { + "name": "Valve connection" + } + }, "number": { "remaining_open_time": { "name": "Remaining open time" diff --git a/tests/components/gardena_bluetooth/snapshots/test_binary_sensor.ambr b/tests/components/gardena_bluetooth/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000000..8a2600dcbb12fb --- /dev/null +++ b/tests/components/gardena_bluetooth/snapshots/test_binary_sensor.ambr @@ -0,0 +1,27 @@ +# serializer version: 1 +# name: test_setup[98bd0f12-0b0e-421a-84e5-ddbf75dc6de4-raw0-binary_sensor.mock_title_valve_connection] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Mock Title Valve connection', + }), + 'context': , + 'entity_id': 'binary_sensor.mock_title_valve_connection', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_setup[98bd0f12-0b0e-421a-84e5-ddbf75dc6de4-raw0-binary_sensor.mock_title_valve_connection].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Mock Title Valve connection', + }), + 'context': , + 'entity_id': 'binary_sensor.mock_title_valve_connection', + 'last_changed': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/gardena_bluetooth/test_binary_sensor.py b/tests/components/gardena_bluetooth/test_binary_sensor.py new file mode 100644 index 00000000000000..cda24f871e8952 --- /dev/null +++ b/tests/components/gardena_bluetooth/test_binary_sensor.py @@ -0,0 +1,44 @@ +"""Test Gardena Bluetooth binary sensor.""" + + +from gardena_bluetooth.const import Valve +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from . import setup_entry + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("uuid", "raw", "entity_id"), + [ + ( + Valve.connected_state.uuid, + [b"\x01", b"\x00"], + "binary_sensor.mock_title_valve_connection", + ), + ], +) +async def test_setup( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_read_char_raw: dict[str, bytes], + uuid: str, + raw: list[bytes], + entity_id: str, +) -> None: + """Test setup creates expected entities.""" + + mock_read_char_raw[uuid] = raw[0] + coordinator = await setup_entry(hass, mock_entry, [Platform.BINARY_SENSOR]) + assert hass.states.get(entity_id) == snapshot + + for char_raw in raw[1:]: + mock_read_char_raw[uuid] = char_raw + await coordinator.async_refresh() + assert hass.states.get(entity_id) == snapshot From c154c2b0601e32c54b603c1c25c2f03b8093674d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 09:17:28 +0200 Subject: [PATCH 0578/1009] Add entity translations to Transmission (#96761) --- .../components/transmission/sensor.py | 53 +++++++++++++++---- .../components/transmission/strings.json | 22 ++++++++ 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 3cee556044f122..833c1910d4ec83 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -38,21 +38,53 @@ async def async_setup_entry( name = config_entry.data[CONF_NAME] dev = [ - TransmissionSpeedSensor(tm_client, name, "Down speed", "download"), - TransmissionSpeedSensor(tm_client, name, "Up speed", "upload"), - TransmissionStatusSensor(tm_client, name, "Status", "status"), + TransmissionSpeedSensor( + tm_client, + name, + "download_speed", + "download", + ), + TransmissionSpeedSensor( + tm_client, + name, + "upload_speed", + "upload", + ), + TransmissionStatusSensor( + tm_client, + name, + "transmission_status", + "status", + ), + TransmissionTorrentsSensor( + tm_client, + name, + "active_torrents", + "active_torrents", + ), TransmissionTorrentsSensor( - tm_client, name, "Active torrents", "active_torrents" + tm_client, + name, + "paused_torrents", + "paused_torrents", ), TransmissionTorrentsSensor( - tm_client, name, "Paused torrents", "paused_torrents" + tm_client, + name, + "total_torrents", + "total_torrents", ), - TransmissionTorrentsSensor(tm_client, name, "Total torrents", "total_torrents"), TransmissionTorrentsSensor( - tm_client, name, "Completed torrents", "completed_torrents" + tm_client, + name, + "completed_torrents", + "completed_torrents", ), TransmissionTorrentsSensor( - tm_client, name, "Started torrents", "started_torrents" + tm_client, + name, + "started_torrents", + "started_torrents", ), ] @@ -65,10 +97,10 @@ class TransmissionSensor(SensorEntity): _attr_has_entity_name = True _attr_should_poll = False - def __init__(self, tm_client, client_name, sensor_name, key): + def __init__(self, tm_client, client_name, sensor_translation_key, key): """Initialize the sensor.""" self._tm_client: TransmissionClient = tm_client - self._attr_name = sensor_name + self._attr_translation_key = sensor_translation_key self._key = key self._state = None self._attr_unique_id = f"{tm_client.config_entry.entry_id}-{key}" @@ -128,7 +160,6 @@ class TransmissionStatusSensor(TransmissionSensor): _attr_device_class = SensorDeviceClass.ENUM _attr_options = [STATE_IDLE, STATE_UP_DOWN, STATE_SEEDING, STATE_DOWNLOADING] - _attr_translation_key = "transmission_status" def update(self) -> None: """Get the latest data from Transmission and updates the state.""" diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index 97741bd65bb0bc..aaab4d2e2d7e91 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -43,13 +43,35 @@ }, "entity": { "sensor": { + "download_speed": { + "name": "Download speed" + }, + "upload_speed": { + "name": "Upload speed" + }, "transmission_status": { + "name": "Status", "state": { "idle": "[%key:common::state::idle%]", "up_down": "Up/Down", "seeding": "Seeding", "downloading": "Downloading" } + }, + "active_torrents": { + "name": "Active torrents" + }, + "paused_torrents": { + "name": "Paused torrents" + }, + "total_torrents": { + "name": "Total torrents" + }, + "completed_torrents": { + "name": "Completed torrents" + }, + "started_torrents": { + "name": "Started torrents" } } }, From c5b20ca91b957128a66fa666c6170a282ccbed80 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Jul 2023 21:29:42 -1000 Subject: [PATCH 0579/1009] Bump yalexs-ble to 2.2.1 (#96808) --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 31d0ff09467612..b8d77d5d82a318 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==1.5.1", "yalexs-ble==2.2.0"] + "requirements": ["yalexs==1.5.1", "yalexs-ble==2.2.1"] } diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 67b4e1c929956b..8cac3fb81f7ed8 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", "iot_class": "local_push", - "requirements": ["yalexs-ble==2.2.0"] + "requirements": ["yalexs-ble==2.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 19ef9194307278..cdd695d4d74a6b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2711,7 +2711,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.2.0 +yalexs-ble==2.2.1 # homeassistant.components.august yalexs==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 655a165cf39d38..75b8fc80d06b0d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1990,7 +1990,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.2.0 +yalexs-ble==2.2.1 # homeassistant.components.august yalexs==1.5.1 From 57352578ff79f27edd6627406dd0c7fb6d0d2d25 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 18 Jul 2023 09:36:40 +0200 Subject: [PATCH 0580/1009] Use entity registry id in zwave_js device actions (#96407) --- .../components/device_automation/__init__.py | 7 +- .../components/zwave_js/device_action.py | 24 ++-- .../components/zwave_js/test_device_action.py | 120 +++++++++++++++--- 3 files changed, 124 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index af2fd61081cb80..d7641c34316dc1 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -349,9 +349,10 @@ def async_validate_entity_schema( config = schema(config) registry = er.async_get(hass) - config[CONF_ENTITY_ID] = er.async_resolve_entity_id( - registry, config[CONF_ENTITY_ID] - ) + if CONF_ENTITY_ID in config: + config[CONF_ENTITY_ID] = er.async_resolve_entity_id( + registry, config[CONF_ENTITY_ID] + ) return config diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index 18a3ccef7d8a89..04db33fdff6596 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -12,6 +12,7 @@ from zwave_js_server.model.value import get_value_id_str from zwave_js_server.util.command_class.meter import get_meter_type +from homeassistant.components.device_automation import async_validate_entity_schema from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( @@ -70,7 +71,7 @@ CLEAR_LOCK_USERCODE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): SERVICE_CLEAR_LOCK_USERCODE, - vol.Required(CONF_ENTITY_ID): cv.entity_domain(LOCK_DOMAIN), + vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid, vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), } ) @@ -84,7 +85,7 @@ REFRESH_VALUE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): SERVICE_REFRESH_VALUE, - vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid, vol.Optional(ATTR_REFRESH_ALL_VALUES, default=False): cv.boolean, } ) @@ -92,7 +93,7 @@ RESET_METER_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): SERVICE_RESET_METER, - vol.Required(CONF_ENTITY_ID): cv.entity_domain(SENSOR_DOMAIN), + vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid, vol.Optional(ATTR_METER_TYPE): vol.Coerce(int), vol.Optional(ATTR_VALUE): vol.Coerce(int), } @@ -112,7 +113,7 @@ SET_LOCK_USERCODE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): SERVICE_SET_LOCK_USERCODE, - vol.Required(CONF_ENTITY_ID): cv.entity_domain(LOCK_DOMAIN), + vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid, vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), vol.Required(ATTR_USERCODE): cv.string, } @@ -130,7 +131,7 @@ } ) -ACTION_SCHEMA = vol.Any( +_ACTION_SCHEMA = vol.Any( CLEAR_LOCK_USERCODE_SCHEMA, PING_SCHEMA, REFRESH_VALUE_SCHEMA, @@ -141,6 +142,13 @@ ) +async def async_validate_action_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + return async_validate_entity_schema(hass, config, _ACTION_SCHEMA) + + async def async_get_actions( hass: HomeAssistant, device_id: str ) -> list[dict[str, Any]]: @@ -192,7 +200,7 @@ async def async_get_actions( or state.state == STATE_UNAVAILABLE ): continue - entity_action = {**base_action, CONF_ENTITY_ID: entry.entity_id} + entity_action = {**base_action, CONF_ENTITY_ID: entry.id} actions.append({**entity_action, CONF_TYPE: SERVICE_REFRESH_VALUE}) if entry.domain == LOCK_DOMAIN: actions.extend( @@ -213,9 +221,7 @@ async def async_get_actions( # action for it if CC_SPECIFIC_METER_TYPE in value.metadata.cc_specific: endpoint_idx = value.endpoint or 0 - meter_endpoints[endpoint_idx].setdefault( - CONF_ENTITY_ID, entry.entity_id - ) + meter_endpoints[endpoint_idx].setdefault(CONF_ENTITY_ID, entry.id) meter_endpoints[endpoint_idx].setdefault(ATTR_METER_TYPE, set()).add( get_meter_type(value) ) diff --git a/tests/components/zwave_js/test_device_action.py b/tests/components/zwave_js/test_device_action.py index b5d4149a5260d1..ce2b916b7a1690 100644 --- a/tests/components/zwave_js/test_device_action.py +++ b/tests/components/zwave_js/test_device_action.py @@ -15,7 +15,11 @@ from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, +) from homeassistant.setup import async_setup_component from tests.common import async_get_device_automations @@ -27,6 +31,7 @@ async def test_get_actions( lock_schlage_be469: Node, integration: ConfigEntry, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test we get the expected actions from a zwave_js node.""" node = lock_schlage_be469 @@ -34,33 +39,39 @@ async def test_get_actions( assert driver device = device_registry.async_get_device(identifiers={get_device_id(driver, node)}) assert device + binary_sensor = entity_registry.async_get( + "binary_sensor.touchscreen_deadbolt_low_battery_level" + ) + assert binary_sensor + lock = entity_registry.async_get("lock.touchscreen_deadbolt") + assert lock expected_actions = [ { "domain": DOMAIN, "type": "clear_lock_usercode", "device_id": device.id, - "entity_id": "lock.touchscreen_deadbolt", + "entity_id": lock.id, "metadata": {"secondary": False}, }, { "domain": DOMAIN, "type": "set_lock_usercode", "device_id": device.id, - "entity_id": "lock.touchscreen_deadbolt", + "entity_id": lock.id, "metadata": {"secondary": False}, }, { "domain": DOMAIN, "type": "refresh_value", "device_id": device.id, - "entity_id": "binary_sensor.touchscreen_deadbolt_low_battery_level", + "entity_id": binary_sensor.id, "metadata": {"secondary": True}, }, { "domain": DOMAIN, "type": "refresh_value", "device_id": device.id, - "entity_id": "lock.touchscreen_deadbolt", + "entity_id": lock.id, "metadata": {"secondary": False}, }, { @@ -129,6 +140,7 @@ async def test_actions( climate_radio_thermostat_ct100_plus: Node, integration: ConfigEntry, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test actions.""" node = climate_radio_thermostat_ct100_plus @@ -138,6 +150,9 @@ async def test_actions( device = device_registry.async_get_device(identifiers={device_id}) assert device + climate = entity_registry.async_get("climate.z_wave_thermostat") + assert climate + assert await async_setup_component( hass, automation.DOMAIN, @@ -152,7 +167,7 @@ async def test_actions( "domain": DOMAIN, "type": "refresh_value", "device_id": device.id, - "entity_id": "climate.z_wave_thermostat", + "entity_id": climate.id, }, }, { @@ -273,20 +288,81 @@ async def test_actions( assert args[2] == 1 +async def test_actions_legacy( + hass: HomeAssistant, + client: Client, + climate_radio_thermostat_ct100_plus: Node, + integration: ConfigEntry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test actions.""" + node = climate_radio_thermostat_ct100_plus + driver = client.driver + assert driver + device_id = get_device_id(driver, node) + device = device_registry.async_get_device(identifiers={device_id}) + assert device + + climate = entity_registry.async_get("climate.z_wave_thermostat") + assert climate + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_refresh_value", + }, + "action": { + "domain": DOMAIN, + "type": "refresh_value", + "device_id": device.id, + "entity_id": climate.entity_id, + }, + }, + ] + }, + ) + + with patch("zwave_js_server.model.node.Node.async_poll_value") as mock_call: + hass.bus.async_fire("test_event_refresh_value") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 1 + assert args[0].value_id == "13-64-1-mode" + + # Call action a second time to confirm that it works (this was previously a bug) + with patch("zwave_js_server.model.node.Node.async_poll_value") as mock_call: + hass.bus.async_fire("test_event_refresh_value") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 1 + assert args[0].value_id == "13-64-1-mode" + + async def test_actions_multiple_calls( hass: HomeAssistant, client: Client, climate_radio_thermostat_ct100_plus: Node, integration: ConfigEntry, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test actions can be called multiple times and still work.""" node = climate_radio_thermostat_ct100_plus driver = client.driver assert driver device_id = get_device_id(driver, node) - device = device_registry.async_get_device(identifiers={device_id}) + device = device_registry.async_get_device({device_id}) assert device + climate = entity_registry.async_get("climate.z_wave_thermostat") + assert climate assert await async_setup_component( hass, @@ -302,7 +378,7 @@ async def test_actions_multiple_calls( "domain": DOMAIN, "type": "refresh_value", "device_id": device.id, - "entity_id": "climate.z_wave_thermostat", + "entity_id": climate.id, }, }, ] @@ -326,6 +402,7 @@ async def test_lock_actions( lock_schlage_be469: Node, integration: ConfigEntry, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test actions for locks.""" node = lock_schlage_be469 @@ -334,6 +411,8 @@ async def test_lock_actions( device_id = get_device_id(driver, node) device = device_registry.async_get_device(identifiers={device_id}) assert device + lock = entity_registry.async_get("lock.touchscreen_deadbolt") + assert lock assert await async_setup_component( hass, @@ -349,7 +428,7 @@ async def test_lock_actions( "domain": DOMAIN, "type": "clear_lock_usercode", "device_id": device.id, - "entity_id": "lock.touchscreen_deadbolt", + "entity_id": lock.id, "code_slot": 1, }, }, @@ -362,7 +441,7 @@ async def test_lock_actions( "domain": DOMAIN, "type": "set_lock_usercode", "device_id": device.id, - "entity_id": "lock.touchscreen_deadbolt", + "entity_id": lock.id, "code_slot": 1, "usercode": "1234", }, @@ -397,6 +476,7 @@ async def test_reset_meter_action( aeon_smart_switch_6: Node, integration: ConfigEntry, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test reset_meter action.""" node = aeon_smart_switch_6 @@ -405,6 +485,8 @@ async def test_reset_meter_action( device_id = get_device_id(driver, node) device = device_registry.async_get_device(identifiers={device_id}) assert device + sensor = entity_registry.async_get("sensor.smart_switch_6_electric_consumed_kwh") + assert sensor assert await async_setup_component( hass, @@ -420,7 +502,7 @@ async def test_reset_meter_action( "domain": DOMAIN, "type": "reset_meter", "device_id": device.id, - "entity_id": "sensor.smart_switch_6_electric_consumed_kwh", + "entity_id": sensor.id, }, }, ] @@ -615,9 +697,12 @@ async def test_get_action_capabilities_lock_triggers( lock_schlage_be469: Node, integration: ConfigEntry, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test we get the expected action capabilities for lock triggers.""" device = dr.async_entries_for_config_entry(device_registry, integration.entry_id)[0] + lock = entity_registry.async_get("lock.touchscreen_deadbolt") + assert lock # Test clear_lock_usercode capabilities = await device_action.async_get_action_capabilities( @@ -626,7 +711,7 @@ async def test_get_action_capabilities_lock_triggers( "platform": "device", "domain": DOMAIN, "device_id": device.id, - "entity_id": "lock.touchscreen_deadbolt", + "entity_id": lock.id, "type": "clear_lock_usercode", }, ) @@ -643,7 +728,7 @@ async def test_get_action_capabilities_lock_triggers( "platform": "device", "domain": DOMAIN, "device_id": device.id, - "entity_id": "lock.touchscreen_deadbolt", + "entity_id": lock.id, "type": "set_lock_usercode", }, ) @@ -663,6 +748,7 @@ async def test_get_action_capabilities_meter_triggers( aeon_smart_switch_6: Node, integration: ConfigEntry, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test we get the expected action capabilities for meter triggers.""" node = aeon_smart_switch_6 @@ -676,7 +762,7 @@ async def test_get_action_capabilities_meter_triggers( "platform": "device", "domain": DOMAIN, "device_id": device.id, - "entity_id": "sensor.meter", + "entity_id": "123456789", # The entity is not checked "type": "reset_meter", }, ) @@ -716,9 +802,10 @@ async def test_unavailable_entity_actions( lock_schlage_be469: Node, integration: ConfigEntry, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test unavailable entities are not included in actions list.""" - entity_id_unavailable = "binary_sensor.touchscreen_deadbolt_home_security_intrusion" + entity_id_unavailable = "binary_sensor.touchscreen_deadbolt_low_battery_level" hass.states.async_set(entity_id_unavailable, STATE_UNAVAILABLE, force_update=True) await hass.async_block_till_done() node = lock_schlage_be469 @@ -726,9 +813,12 @@ async def test_unavailable_entity_actions( assert driver device = device_registry.async_get_device(identifiers={get_device_id(driver, node)}) assert device + binary_sensor = entity_registry.async_get(entity_id_unavailable) + assert binary_sensor actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device.id ) assert not any( action.get("entity_id") == entity_id_unavailable for action in actions ) + assert not any(action.get("entity_id") == binary_sensor.id for action in actions) From 7d4016d7bf09b968b396be5894b1ab72edbb3b34 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 09:37:38 +0200 Subject: [PATCH 0581/1009] Migrate gpslogger to has entity name (#96594) --- .../components/gpslogger/device_tracker.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 317f2619beff2f..4cce9290a68204 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -66,8 +66,11 @@ def _receive_data(device, gps, battery, accuracy, attrs): class GPSLoggerEntity(TrackerEntity, RestoreEntity): """Represent a tracked device.""" + _attr_has_entity_name = True + _attr_name = None + def __init__(self, device, location, battery, accuracy, attributes): - """Set up Geofency entity.""" + """Set up GPSLogger entity.""" self._accuracy = accuracy self._attributes = attributes self._name = device @@ -101,11 +104,6 @@ def location_accuracy(self): """Return the gps accuracy of the device.""" return self._accuracy - @property - def name(self): - """Return the name of the device.""" - return self._name - @property def unique_id(self): """Return the unique ID.""" @@ -114,7 +112,10 @@ def unique_id(self): @property def device_info(self) -> DeviceInfo: """Return the device info.""" - return DeviceInfo(identifiers={(GPL_DOMAIN, self._unique_id)}, name=self._name) + return DeviceInfo( + identifiers={(GPL_DOMAIN, self._unique_id)}, + name=self._name, + ) @property def source_type(self) -> SourceType: @@ -165,7 +166,7 @@ async def async_will_remove_from_hass(self) -> None: @callback def _async_receive_data(self, device, location, battery, accuracy, attributes): """Mark the device as seen.""" - if device != self.name: + if device != self._name: return self._location = location From fca40be5dfc6fd21378c8fe5dc6faee5942c5c7a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Jul 2023 21:41:37 -1000 Subject: [PATCH 0582/1009] Small cleanups to expand_entity_ids (#96585) --- homeassistant/components/group/__init__.py | 38 +++++++++------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 9480fa3ce1712b..4bdabdf9c96287 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -10,7 +10,6 @@ import voluptuous as vol -from homeassistant import core as ha from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -82,6 +81,8 @@ REG_KEY = f"{DOMAIN}_registry" +ENTITY_PREFIX = f"{DOMAIN}." + _LOGGER = logging.getLogger(__name__) current_domain: ContextVar[str] = ContextVar("current_domain") @@ -180,28 +181,19 @@ def expand_entity_ids(hass: HomeAssistant, entity_ids: Iterable[Any]) -> list[st continue entity_id = entity_id.lower() - - try: - # If entity_id points at a group, expand it - domain, _ = ha.split_entity_id(entity_id) - - if domain == DOMAIN: - child_entities = get_entity_ids(hass, entity_id) - if entity_id in child_entities: - child_entities = list(child_entities) - child_entities.remove(entity_id) - found_ids.extend( - ent_id - for ent_id in expand_entity_ids(hass, child_entities) - if ent_id not in found_ids - ) - - elif entity_id not in found_ids: - found_ids.append(entity_id) - - except AttributeError: - # Raised by split_entity_id if entity_id is not a string - pass + # If entity_id points at a group, expand it + if entity_id.startswith(ENTITY_PREFIX): + child_entities = get_entity_ids(hass, entity_id) + if entity_id in child_entities: + child_entities = list(child_entities) + child_entities.remove(entity_id) + found_ids.extend( + ent_id + for ent_id in expand_entity_ids(hass, child_entities) + if ent_id not in found_ids + ) + elif entity_id not in found_ids: + found_ids.append(entity_id) return found_ids From 4dd7611c832a33c7b339442a294a7cedf9efcb89 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 09:42:07 +0200 Subject: [PATCH 0583/1009] Make Version integration title translatable (#96586) --- homeassistant/components/version/strings.json | 1 + homeassistant/generated/integrations.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/version/strings.json b/homeassistant/components/version/strings.json index 299ab753cb9e82..36da7072626120 100644 --- a/homeassistant/components/version/strings.json +++ b/homeassistant/components/version/strings.json @@ -1,4 +1,5 @@ { + "title": "Version", "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 9fa12a84383aea..1da6a6be9dae57 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6076,7 +6076,6 @@ "iot_class": "local_polling" }, "version": { - "name": "Version", "integration_type": "hub", "config_flow": true, "iot_class": "local_push" @@ -6692,6 +6691,7 @@ "tod", "uptime", "utility_meter", + "version", "waze_travel_time", "workday", "zodiac" From bc6a41fb9465523998b2e152dd7f3d4275f2cc9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Jul 2023 21:42:48 -1000 Subject: [PATCH 0584/1009] Remove deprecated state.get_changed_since (#96579) --- homeassistant/helpers/state.py | 50 +-------------------------------- tests/helpers/test_state.py | 51 +--------------------------------- 2 files changed, 2 insertions(+), 99 deletions(-) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 21d060f4ba77a3..dae63b4ead1991 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -4,9 +4,8 @@ import asyncio from collections import defaultdict from collections.abc import Iterable -import datetime as dt import logging -from types import ModuleType, TracebackType +from types import ModuleType from typing import Any from homeassistant.components.sun import STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON @@ -23,57 +22,10 @@ ) from homeassistant.core import Context, HomeAssistant, State from homeassistant.loader import IntegrationNotFound, async_get_integration, bind_hass -import homeassistant.util.dt as dt_util - -from .frame import report _LOGGER = logging.getLogger(__name__) -class AsyncTrackStates: - """Record the time when the with-block is entered. - - Add all states that have changed since the start time to the return list - when with-block is exited. - - Must be run within the event loop. - - Deprecated. Remove after June 2021. - Warning added via `get_changed_since`. - """ - - def __init__(self, hass: HomeAssistant) -> None: - """Initialize a TrackStates block.""" - self.hass = hass - self.states: list[State] = [] - - # pylint: disable=attribute-defined-outside-init - def __enter__(self) -> list[State]: - """Record time from which to track changes.""" - self.now = dt_util.utcnow() - return self.states - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Add changes states to changes list.""" - self.states.extend(get_changed_since(self.hass.states.async_all(), self.now)) - - -def get_changed_since( - states: Iterable[State], utc_point_in_time: dt.datetime -) -> list[State]: - """Return list of states that have been changed since utc_point_in_time. - - Deprecated. Remove after June 2021. - """ - report("uses deprecated `get_changed_since`") - return [state for state in states if state.last_updated >= utc_point_in_time] - - @bind_hass async def async_reproduce_state( hass: HomeAssistant, diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index 1919586daa30aa..255fba0e7e718a 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -1,9 +1,7 @@ """Test state helpers.""" import asyncio -from datetime import timedelta -from unittest.mock import Mock, patch +from unittest.mock import patch -from freezegun import freeze_time import pytest from homeassistant.components.sun import STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON @@ -21,34 +19,10 @@ ) from homeassistant.core import HomeAssistant, State from homeassistant.helpers import state -from homeassistant.util import dt as dt_util from tests.common import async_mock_service -async def test_async_track_states( - hass: HomeAssistant, mock_integration_frame: Mock -) -> None: - """Test AsyncTrackStates context manager.""" - point1 = dt_util.utcnow() - point2 = point1 + timedelta(seconds=5) - point3 = point2 + timedelta(seconds=5) - - with freeze_time(point2) as freezer, state.AsyncTrackStates(hass) as states: - freezer.move_to(point1) - hass.states.async_set("light.test", "on") - - freezer.move_to(point2) - hass.states.async_set("light.test2", "on") - state2 = hass.states.get("light.test2") - - freezer.move_to(point3) - hass.states.async_set("light.test3", "on") - state3 = hass.states.get("light.test3") - - assert [state2, state3] == sorted(states, key=lambda state: state.entity_id) - - async def test_call_to_component(hass: HomeAssistant) -> None: """Test calls to components state reproduction functions.""" with patch( @@ -82,29 +56,6 @@ async def test_call_to_component(hass: HomeAssistant) -> None: ) -async def test_get_changed_since( - hass: HomeAssistant, mock_integration_frame: Mock -) -> None: - """Test get_changed_since.""" - point1 = dt_util.utcnow() - point2 = point1 + timedelta(seconds=5) - point3 = point2 + timedelta(seconds=5) - - with freeze_time(point1) as freezer: - hass.states.async_set("light.test", "on") - state1 = hass.states.get("light.test") - - freezer.move_to(point2) - hass.states.async_set("light.test2", "on") - state2 = hass.states.get("light.test2") - - freezer.move_to(point3) - hass.states.async_set("light.test3", "on") - state3 = hass.states.get("light.test3") - - assert [state2, state3] == state.get_changed_since([state1, state2, state3], point2) - - async def test_reproduce_with_no_entity(hass: HomeAssistant) -> None: """Test reproduce_state with no entity.""" calls = async_mock_service(hass, "light", SERVICE_TURN_ON) From 8d048c4cfa229a1accd9ab2c090aac06daca0991 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 09:43:29 +0200 Subject: [PATCH 0585/1009] Migrate geofency to has entity name (#96592) --- .../components/geofency/device_tracker.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index 892116121a0ea1..66cbbcbd67e9f7 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -50,6 +50,9 @@ def _receive_data(device, gps, location_name, attributes): class GeofencyEntity(TrackerEntity, RestoreEntity): """Represent a tracked device.""" + _attr_has_entity_name = True + _attr_name = None + def __init__(self, device, gps=None, location_name=None, attributes=None): """Set up Geofency entity.""" self._attributes = attributes or {} @@ -79,11 +82,6 @@ def location_name(self): """Return a location name for the current location of the device.""" return self._location_name - @property - def name(self): - """Return the name of the device.""" - return self._name - @property def unique_id(self): """Return the unique ID.""" @@ -92,7 +90,10 @@ def unique_id(self): @property def device_info(self) -> DeviceInfo: """Return the device info.""" - return DeviceInfo(identifiers={(GF_DOMAIN, self._unique_id)}, name=self._name) + return DeviceInfo( + identifiers={(GF_DOMAIN, self._unique_id)}, + name=self._name, + ) @property def source_type(self) -> SourceType: @@ -125,7 +126,7 @@ async def async_will_remove_from_hass(self) -> None: @callback def _async_receive_data(self, device, gps, location_name, attributes): """Mark the device as seen.""" - if device != self.name: + if device != self._name: return self._attributes.update(attributes) From 9b29cbd71ca486bf8fc253cbc52e6efb45055e39 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 09:44:47 +0200 Subject: [PATCH 0586/1009] Migrate Home plus control to has entity name (#96596) --- homeassistant/components/home_plus_control/switch.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/home_plus_control/switch.py b/homeassistant/components/home_plus_control/switch.py index 6e92fac3b72c94..99766ebfec9b15 100644 --- a/homeassistant/components/home_plus_control/switch.py +++ b/homeassistant/components/home_plus_control/switch.py @@ -66,17 +66,15 @@ class HomeControlSwitchEntity(CoordinatorEntity, SwitchEntity): consumption methods and state attributes. """ + _attr_has_entity_name = True + _attr_name = None + def __init__(self, coordinator, idx): """Pass coordinator to CoordinatorEntity.""" super().__init__(coordinator) self.idx = idx self.module = self.coordinator.data[self.idx] - @property - def name(self): - """Name of the device.""" - return self.module.name - @property def unique_id(self): """ID (unique) of the device.""" @@ -92,7 +90,7 @@ def device_info(self) -> DeviceInfo: }, manufacturer="Legrand", model=HW_TYPE.get(self.module.hw_type), - name=self.name, + name=self.module.name, sw_version=self.module.fw, ) From 43842e243d96d68c593a7247a1d6a456d787584c Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Tue, 18 Jul 2023 09:54:07 +0200 Subject: [PATCH 0587/1009] Rename 'life' to 'lifetime' in Tuya (#96813) --- homeassistant/components/tuya/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index afa40f27afd9b4..96866b7cd67183 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -854,25 +854,25 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), TuyaSensorEntityDescription( key=DPCode.DUSTER_CLOTH, - name="Duster cloth life", + name="Duster cloth lifetime", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.EDGE_BRUSH, - name="Side brush life", + name="Side brush lifetime", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.FILTER_LIFE, - name="Filter life", + name="Filter lifetime", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.ROLL_BRUSH, - name="Rolling brush life", + name="Rolling brush lifetime", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), From 5cea0bb3deb18fbaad10c715cfec18a9d4f36caa Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 09:54:50 +0200 Subject: [PATCH 0588/1009] Migrate Soundtouch to has entity name (#96754) --- homeassistant/components/soundtouch/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 9cd94330812f72..f8670074c5ca2e 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -73,6 +73,8 @@ class SoundTouchMediaPlayer(MediaPlayerEntity): | MediaPlayerEntityFeature.BROWSE_MEDIA ) _attr_device_class = MediaPlayerDeviceClass.SPEAKER + _attr_has_entity_name = True + _attr_name = None def __init__(self, device: SoundTouchDevice) -> None: """Create SoundTouch media player entity.""" @@ -80,7 +82,6 @@ def __init__(self, device: SoundTouchDevice) -> None: self._device = device self._attr_unique_id = self._device.config.device_id - self._attr_name = self._device.config.name self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._device.config.device_id)}, connections={ From 2bbce7ad229be049018032c78e0688c8bdccb4e9 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 09:55:26 +0200 Subject: [PATCH 0589/1009] Migrate Senz to has entity name (#96752) --- homeassistant/components/senz/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/senz/climate.py b/homeassistant/components/senz/climate.py index f52a94d2c9a83c..0c49368001d798 100644 --- a/homeassistant/components/senz/climate.py +++ b/homeassistant/components/senz/climate.py @@ -43,6 +43,8 @@ class SENZClimate(CoordinatorEntity, ClimateEntity): _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE _attr_max_temp = 35 _attr_min_temp = 5 + _attr_has_entity_name = True + _attr_name = None def __init__( self, @@ -52,7 +54,6 @@ def __init__( """Init SENZ climate.""" super().__init__(coordinator) self._thermostat = thermostat - self._attr_name = thermostat.name self._attr_unique_id = thermostat.serial_number self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, thermostat.serial_number)}, From 69bcba7ef5c442132a351add2283f010186bb3e0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 09:56:11 +0200 Subject: [PATCH 0590/1009] Migrate frontier silicon to has entity name (#96571) --- homeassistant/components/frontier_silicon/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 04b689ae917ccd..62df3a12c2bd7c 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -46,6 +46,8 @@ class AFSAPIDevice(MediaPlayerEntity): """Representation of a Frontier Silicon device on the network.""" _attr_media_content_type: str = MediaType.CHANNEL + _attr_has_entity_name = True + _attr_name = None _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE @@ -73,7 +75,6 @@ def __init__(self, name: str | None, afsapi: AFSAPI) -> None: identifiers={(DOMAIN, afsapi.webfsapi_endpoint)}, name=name, ) - self._attr_name = name self._max_volume: int | None = None From 1097bde71b2e368fbed3d33bf5e2fde1888d02db Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 09:56:57 +0200 Subject: [PATCH 0591/1009] Migrate AndroidTV to has entity name (#96572) --- homeassistant/components/androidtv/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index bd800ea04dd964..8f5f3bdfe56bc2 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -210,6 +210,8 @@ class ADBDevice(MediaPlayerEntity): """Representation of an Android or Fire TV device.""" _attr_device_class = MediaPlayerDeviceClass.TV + _attr_has_entity_name = True + _attr_name = None def __init__( self, @@ -222,7 +224,6 @@ def __init__( ) -> None: """Initialize the Android / Fire TV device.""" self.aftv = aftv - self._attr_name = name self._attr_unique_id = unique_id self._entry_id = entry_id self._entry_data = entry_data From 65db77dd8ad1acd8f1b70f08e8fa6fd85c74b566 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 09:58:42 +0200 Subject: [PATCH 0592/1009] Migrate Dynalite to has entity name (#96569) --- homeassistant/components/dynalite/dynalitebase.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/dynalite/dynalitebase.py b/homeassistant/components/dynalite/dynalitebase.py index 3ebf04ab219adb..85c672e0f64d79 100644 --- a/homeassistant/components/dynalite/dynalitebase.py +++ b/homeassistant/components/dynalite/dynalitebase.py @@ -41,17 +41,15 @@ def async_add_entities_platform(devices): class DynaliteBase(RestoreEntity, ABC): """Base class for the Dynalite entities.""" + _attr_has_entity_name = True + _attr_name = None + def __init__(self, device: Any, bridge: DynaliteBridge) -> None: """Initialize the base class.""" self._device = device self._bridge = bridge self._unsub_dispatchers: list[Callable[[], None]] = [] - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._device.name - @property def unique_id(self) -> str: """Return the unique ID of the entity.""" @@ -68,7 +66,7 @@ def device_info(self) -> DeviceInfo: return DeviceInfo( identifiers={(DOMAIN, self._device.unique_id)}, manufacturer="Dynalite", - name=self.name, + name=self._device.name, ) async def async_added_to_hass(self) -> None: From 5d096a657f35676e28457ba89363d09bbec316ff Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 09:59:32 +0200 Subject: [PATCH 0593/1009] Migrate Brunt to has entity name (#96565) --- homeassistant/components/brunt/cover.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index 3fb328ab7fbc5c..9f916e5751ffe8 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -60,6 +60,10 @@ class BruntDevice( Contains the common logic for all Brunt devices. """ + _attr_has_entity_name = True + _attr_name = None + _attr_device_class = CoverDeviceClass.BLIND + _attr_attribution = ATTRIBUTION _attr_supported_features = ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE @@ -83,12 +87,9 @@ def __init__( self._remove_update_listener = None - self._attr_name = self._thing.name - self._attr_device_class = CoverDeviceClass.BLIND - self._attr_attribution = ATTRIBUTION self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._attr_unique_id)}, # type: ignore[arg-type] - name=self._attr_name, + name=self._thing.name, via_device=(DOMAIN, self._entry_id), manufacturer="Brunt", sw_version=self._thing.fw_version, From aa13082ce0dd9fb29e631c9280449428822d8000 Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Tue, 18 Jul 2023 10:13:33 +0200 Subject: [PATCH 0594/1009] Rename 'life' to 'lifetime' in Xiaomi Miio (#96817) String review: rename 'life' to 'lifetime' - The term life, such as in 'filter life' can be ambiguous. - Renamed to 'lifetime', as quite a few integrations use the term 'lifetime' to express this concept - Improves consistency and should be easier to understand. --- homeassistant/components/xiaomi_miio/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index b28f06eb97d6e5..86c7905848a558 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -293,7 +293,7 @@ class XiaomiMiioSensorDescription(SensorEntityDescription): ), ATTR_FILTER_LIFE_REMAINING: XiaomiMiioSensorDescription( key=ATTR_FILTER_LIFE_REMAINING, - name="Filter life remaining", + name="Filter lifetime remaining", native_unit_of_measurement=PERCENTAGE, icon="mdi:air-filter", state_class=SensorStateClass.MEASUREMENT, @@ -311,7 +311,7 @@ class XiaomiMiioSensorDescription(SensorEntityDescription): ), ATTR_FILTER_LEFT_TIME: XiaomiMiioSensorDescription( key=ATTR_FILTER_LEFT_TIME, - name="Filter time left", + name="Filter lifetime left", native_unit_of_measurement=UnitOfTime.DAYS, icon="mdi:clock-outline", device_class=SensorDeviceClass.DURATION, @@ -320,7 +320,7 @@ class XiaomiMiioSensorDescription(SensorEntityDescription): ), ATTR_DUST_FILTER_LIFE_REMAINING: XiaomiMiioSensorDescription( key=ATTR_DUST_FILTER_LIFE_REMAINING, - name="Dust filter life remaining", + name="Dust filter lifetime remaining", native_unit_of_measurement=PERCENTAGE, icon="mdi:air-filter", state_class=SensorStateClass.MEASUREMENT, @@ -329,7 +329,7 @@ class XiaomiMiioSensorDescription(SensorEntityDescription): ), ATTR_DUST_FILTER_LIFE_REMAINING_DAYS: XiaomiMiioSensorDescription( key=ATTR_DUST_FILTER_LIFE_REMAINING_DAYS, - name="Dust filter life remaining days", + name="Dust filter lifetime remaining days", native_unit_of_measurement=UnitOfTime.DAYS, icon="mdi:clock-outline", device_class=SensorDeviceClass.DURATION, @@ -338,7 +338,7 @@ class XiaomiMiioSensorDescription(SensorEntityDescription): ), ATTR_UPPER_FILTER_LIFE_REMAINING: XiaomiMiioSensorDescription( key=ATTR_UPPER_FILTER_LIFE_REMAINING, - name="Upper filter life remaining", + name="Upper filter lifetime remaining", native_unit_of_measurement=PERCENTAGE, icon="mdi:air-filter", state_class=SensorStateClass.MEASUREMENT, @@ -347,7 +347,7 @@ class XiaomiMiioSensorDescription(SensorEntityDescription): ), ATTR_UPPER_FILTER_LIFE_REMAINING_DAYS: XiaomiMiioSensorDescription( key=ATTR_UPPER_FILTER_LIFE_REMAINING_DAYS, - name="Upper filter life remaining days", + name="Upper filter lifetime remaining days", native_unit_of_measurement=UnitOfTime.DAYS, icon="mdi:clock-outline", device_class=SensorDeviceClass.DURATION, From 0134ee9305f975dcf2961b2137ea634548e07928 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 18 Jul 2023 10:50:34 +0200 Subject: [PATCH 0595/1009] Fix incorrect leagacy code tweak for MQTT (#96812) Cleanup mqtt_data_updated_config --- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/mqtt/mixins.py | 7 +------ homeassistant/components/mqtt/models.py | 1 - 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 3fb6c8d2c48547..de5093d18177ee 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -336,7 +336,7 @@ async def _reload_config(call: ServiceCall) -> None: """Reload the platforms.""" # Fetch updated manual configured items and validate config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {} - mqtt_data.updated_config = config_yaml.get(DOMAIN, {}) + mqtt_data.config = config_yaml.get(DOMAIN, {}) # Reload the modern yaml platforms mqtt_platforms = async_get_platforms(hass, DOMAIN) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 34b61d89c48c87..e1e5f3d61bb5a0 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -307,12 +307,7 @@ async def async_discover(discovery_payload: MQTTDiscoveryPayload) -> None: async def _async_setup_entities() -> None: """Set up MQTT items from configuration.yaml.""" mqtt_data = get_mqtt_data(hass) - if mqtt_data.updated_config: - # The platform has been reloaded - config_yaml = mqtt_data.updated_config - else: - config_yaml = mqtt_data.config or {} - if not config_yaml: + if not (config_yaml := mqtt_data.config): return if domain not in config_yaml: return diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index aeae184dc89dec..9f0a178ce8750d 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -313,4 +313,3 @@ class MqttData: state_write_requests: EntityTopicState = field(default_factory=EntityTopicState) subscriptions_to_restore: list[Subscription] = field(default_factory=list) tags: dict[str, dict[str, MQTTTagScanner]] = field(default_factory=dict) - updated_config: ConfigType = field(default_factory=dict) From d361caf6c45675d1eed966e93bbe6b6f87bc6728 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 11:04:24 +0200 Subject: [PATCH 0596/1009] Add entity translations to Yalexs BLE (#96827) --- homeassistant/components/yalexs_ble/binary_sensor.py | 1 - homeassistant/components/yalexs_ble/entity.py | 1 + homeassistant/components/yalexs_ble/lock.py | 1 - homeassistant/components/yalexs_ble/sensor.py | 4 +--- homeassistant/components/yalexs_ble/strings.json | 7 +++++++ 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/yalexs_ble/binary_sensor.py b/homeassistant/components/yalexs_ble/binary_sensor.py index 32421f67fbb5a1..8213baf33aa5f7 100644 --- a/homeassistant/components/yalexs_ble/binary_sensor.py +++ b/homeassistant/components/yalexs_ble/binary_sensor.py @@ -32,7 +32,6 @@ class YaleXSBLEDoorSensor(YALEXSBLEEntity, BinarySensorEntity): """Yale XS BLE binary sensor.""" _attr_device_class = BinarySensorDeviceClass.DOOR - _attr_has_entity_name = True @callback def _async_update_state( diff --git a/homeassistant/components/yalexs_ble/entity.py b/homeassistant/components/yalexs_ble/entity.py index 18f1e28ece69a5..51f30b8a861d47 100644 --- a/homeassistant/components/yalexs_ble/entity.py +++ b/homeassistant/components/yalexs_ble/entity.py @@ -15,6 +15,7 @@ class YALEXSBLEEntity(Entity): """Base class for yale xs ble entities.""" + _attr_has_entity_name = True _attr_should_poll = False def __init__(self, data: YaleXSBLEData) -> None: diff --git a/homeassistant/components/yalexs_ble/lock.py b/homeassistant/components/yalexs_ble/lock.py index 0ecf0e7b697482..d457784a038b4b 100644 --- a/homeassistant/components/yalexs_ble/lock.py +++ b/homeassistant/components/yalexs_ble/lock.py @@ -28,7 +28,6 @@ async def async_setup_entry( class YaleXSBLELock(YALEXSBLEEntity, LockEntity): """A yale xs ble lock.""" - _attr_has_entity_name = True _attr_name = None @callback diff --git a/homeassistant/components/yalexs_ble/sensor.py b/homeassistant/components/yalexs_ble/sensor.py index 6304b791edd66a..9d702ff52eb642 100644 --- a/homeassistant/components/yalexs_ble/sensor.py +++ b/homeassistant/components/yalexs_ble/sensor.py @@ -44,7 +44,6 @@ class YaleXSBLESensorEntityDescription( SENSORS: tuple[YaleXSBLESensorEntityDescription, ...] = ( YaleXSBLESensorEntityDescription( key="", # No key for the original RSSI sensor unique id - name="Signal strength", device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, @@ -55,7 +54,6 @@ class YaleXSBLESensorEntityDescription( ), YaleXSBLESensorEntityDescription( key="battery_level", - name="Battery level", device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, @@ -67,7 +65,7 @@ class YaleXSBLESensorEntityDescription( ), YaleXSBLESensorEntityDescription( key="battery_voltage", - name="Battery Voltage", + translation_key="battery_voltage", device_class=SensorDeviceClass.VOLTAGE, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/yalexs_ble/strings.json b/homeassistant/components/yalexs_ble/strings.json index bd96e07f6bac4f..c79830be3a9bf3 100644 --- a/homeassistant/components/yalexs_ble/strings.json +++ b/homeassistant/components/yalexs_ble/strings.json @@ -45,5 +45,12 @@ } } } + }, + "entity": { + "sensor": { + "battery_voltage": { + "name": "Battery voltage" + } + } } } From 772fb463b56c6d51605c7bc9b35076f9614d5dae Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 11:07:26 +0200 Subject: [PATCH 0597/1009] Migrate Wilight to has entity name (#96825) Migrate Wilight to has entity naming --- homeassistant/components/wilight/__init__.py | 2 +- homeassistant/components/wilight/cover.py | 2 ++ homeassistant/components/wilight/fan.py | 1 + homeassistant/components/wilight/light.py | 3 +++ homeassistant/components/wilight/strings.json | 10 ++++++++++ homeassistant/components/wilight/switch.py | 10 ++-------- 6 files changed, 19 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index 326265b8b3fb8a..58ba237ae6891f 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -56,6 +56,7 @@ class WiLightDevice(Entity): """ _attr_should_poll = False + _attr_has_entity_name = True def __init__(self, api_device: PyWiLightDevice, index: str, item_name: str) -> None: """Initialize the device.""" @@ -65,7 +66,6 @@ def __init__(self, api_device: PyWiLightDevice, index: str, item_name: str) -> N self._index = index self._status: dict[str, Any] = {} - self._attr_name = item_name self._attr_unique_id = f"{self._device_id}_{index}" self._attr_device_info = DeviceInfo( name=item_name, diff --git a/homeassistant/components/wilight/cover.py b/homeassistant/components/wilight/cover.py index cd0a3cc21acdbc..aa50b79f139dbf 100644 --- a/homeassistant/components/wilight/cover.py +++ b/homeassistant/components/wilight/cover.py @@ -57,6 +57,8 @@ def hass_to_wilight_position(value: int) -> int: class WiLightCover(WiLightDevice, CoverEntity): """Representation of a WiLights cover.""" + _attr_name = None + @property def current_cover_position(self) -> int | None: """Return current position of cover. diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py index 3d0c6d0ff39d26..ba9a108f63629c 100644 --- a/homeassistant/components/wilight/fan.py +++ b/homeassistant/components/wilight/fan.py @@ -54,6 +54,7 @@ async def async_setup_entry( class WiLightFan(WiLightDevice, FanEntity): """Representation of a WiLights fan.""" + _attr_name = None _attr_icon = "mdi:fan" _attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS) _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION diff --git a/homeassistant/components/wilight/light.py b/homeassistant/components/wilight/light.py index 2509dc50737a71..b17eac36f09e34 100644 --- a/homeassistant/components/wilight/light.py +++ b/homeassistant/components/wilight/light.py @@ -53,6 +53,7 @@ async def async_setup_entry( class WiLightLightOnOff(WiLightDevice, LightEntity): """Representation of a WiLights light on-off.""" + _attr_name = None _attr_color_mode = ColorMode.ONOFF _attr_supported_color_modes = {ColorMode.ONOFF} @@ -73,6 +74,7 @@ async def async_turn_off(self, **kwargs: Any) -> None: class WiLightLightDimmer(WiLightDevice, LightEntity): """Representation of a WiLights light dimmer.""" + _attr_name = None _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} @@ -124,6 +126,7 @@ def hass_to_wilight_saturation(value: float) -> int: class WiLightLightColor(WiLightDevice, LightEntity): """Representation of a WiLights light rgb.""" + _attr_name = None _attr_color_mode = ColorMode.HS _attr_supported_color_modes = {ColorMode.HS} diff --git a/homeassistant/components/wilight/strings.json b/homeassistant/components/wilight/strings.json index a287104e7adb5c..ccba52d99e0c5c 100644 --- a/homeassistant/components/wilight/strings.json +++ b/homeassistant/components/wilight/strings.json @@ -12,6 +12,16 @@ "not_wilight_device": "This Device is not WiLight" } }, + "entity": { + "switch": { + "watering": { + "name": "Watering" + }, + "pause": { + "name": "Pause" + } + } + }, "services": { "set_watering_time": { "name": "Set watering time", diff --git a/homeassistant/components/wilight/switch.py b/homeassistant/components/wilight/switch.py index f2d74cce359948..101162302ae559 100644 --- a/homeassistant/components/wilight/switch.py +++ b/homeassistant/components/wilight/switch.py @@ -148,10 +148,7 @@ def hass_to_wilight_pause_time(value: int) -> int: class WiLightValveSwitch(WiLightDevice, SwitchEntity): """Representation of a WiLights Valve switch.""" - @property - def name(self) -> str: - """Return the name of the switch.""" - return f"{self._attr_name} {DESC_WATERING}" + _attr_translation_key = "watering" @property def is_on(self) -> bool: @@ -272,10 +269,7 @@ async def async_set_trigger(self, trigger_index: int, trigger: str) -> None: class WiLightValvePauseSwitch(WiLightDevice, SwitchEntity): """Representation of a WiLights Valve Pause switch.""" - @property - def name(self) -> str: - """Return the name of the switch.""" - return f"{self._attr_name} {DESC_PAUSE}" + _attr_translation_key = "pause" @property def is_on(self) -> bool: From a69b5a8d3b8a8bfb5c579d4d57714e49beed7ed0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 11:15:41 +0200 Subject: [PATCH 0598/1009] Add support for restricted playback devices in Spotify (#96794) * Add support for restricted devices * Add support for restricted devices --- homeassistant/components/spotify/media_player.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 0145d6f0906082..de2cced08b5032 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -120,9 +120,6 @@ def __init__( self._attr_unique_id = user_id - if self.data.current_user["product"] == "premium": - self._attr_supported_features = SUPPORT_SPOTIFY - self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, user_id)}, manufacturer="Spotify AB", @@ -137,6 +134,16 @@ def __init__( ) self._currently_playing: dict | None = {} self._playlist: dict | None = None + self._restricted_device: bool = False + + @property + def supported_features(self) -> MediaPlayerEntityFeature: + """Return the supported features.""" + if self._restricted_device: + return MediaPlayerEntityFeature.SELECT_SOURCE + if self.data.current_user["product"] == "premium": + return SUPPORT_SPOTIFY + return MediaPlayerEntityFeature(0) @property def state(self) -> MediaPlayerState: @@ -398,6 +405,9 @@ def update(self) -> None: self._playlist = None if context["type"] == MediaType.PLAYLIST: self._playlist = self.data.client.playlist(current["context"]["uri"]) + device = self._currently_playing.get("device") + if device is not None: + self._restricted_device = device["is_restricted"] async def async_browse_media( self, From 1a9e27cdafcb6e9d4dbcba44715d74cce70177df Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 18 Jul 2023 11:35:44 +0200 Subject: [PATCH 0599/1009] Allow integrations to register custom config panels (#96245) --- homeassistant/components/frontend/__init__.py | 9 +++++ .../components/panel_custom/__init__.py | 3 ++ tests/components/hassio/test_init.py | 1 + tests/components/panel_custom/test_init.py | 36 ++++++++++++++++++- tests/components/panel_iframe/test_init.py | 4 +++ 5 files changed, 52 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8c04e59196833e..59315e9f5763c1 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -222,6 +222,9 @@ class Panel: # If the panel should only be visible to admins require_admin = False + # If the panel is a configuration panel for a integration + config_panel_domain: str | None = None + def __init__( self, component_name: str, @@ -230,6 +233,7 @@ def __init__( frontend_url_path: str | None, config: dict[str, Any] | None, require_admin: bool, + config_panel_domain: str | None, ) -> None: """Initialize a built-in panel.""" self.component_name = component_name @@ -238,6 +242,7 @@ def __init__( self.frontend_url_path = frontend_url_path or component_name self.config = config self.require_admin = require_admin + self.config_panel_domain = config_panel_domain @callback def to_response(self) -> PanelRespons: @@ -249,6 +254,7 @@ def to_response(self) -> PanelRespons: "config": self.config, "url_path": self.frontend_url_path, "require_admin": self.require_admin, + "config_panel_domain": self.config_panel_domain, } @@ -264,6 +270,7 @@ def async_register_built_in_panel( require_admin: bool = False, *, update: bool = False, + config_panel_domain: str | None = None, ) -> None: """Register a built-in panel.""" panel = Panel( @@ -273,6 +280,7 @@ def async_register_built_in_panel( frontend_url_path, config, require_admin, + config_panel_domain, ) panels = hass.data.setdefault(DATA_PANELS, {}) @@ -720,3 +728,4 @@ class PanelRespons(TypedDict): config: dict[str, Any] | None url_path: str | None require_admin: bool + config_panel_domain: str | None diff --git a/homeassistant/components/panel_custom/__init__.py b/homeassistant/components/panel_custom/__init__.py index 493a738c1ea6a6..4f084d5900acd0 100644 --- a/homeassistant/components/panel_custom/__init__.py +++ b/homeassistant/components/panel_custom/__init__.py @@ -92,6 +92,8 @@ async def async_register_panel( config: ConfigType | None = None, # If your panel should only be shown to admin users require_admin: bool = False, + # If your panel is used to configure an integration, needs the domain of the integration + config_panel_domain: str | None = None, ) -> None: """Register a new custom panel.""" if js_url is None and module_url is None: @@ -127,6 +129,7 @@ async def async_register_panel( frontend_url_path=frontend_url_path, config=config, require_admin=require_admin, + config_panel_domain=config_panel_domain, ) diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 0dff261d864092..b394d439654954 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -265,6 +265,7 @@ async def test_setup_api_panel( "title": None, "url_path": "hassio", "require_admin": True, + "config_panel_domain": None, "config": { "_panel_custom": { "embed_iframe": True, diff --git a/tests/components/panel_custom/test_init.py b/tests/components/panel_custom/test_init.py index 81365273986558..d84b4c812c7db2 100644 --- a/tests/components/panel_custom/test_init.py +++ b/tests/components/panel_custom/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import Mock, patch from homeassistant import setup -from homeassistant.components import frontend +from homeassistant.components import frontend, panel_custom from homeassistant.core import HomeAssistant @@ -155,3 +155,37 @@ async def test_url_path_conflict(hass: HomeAssistant) -> None: ] }, ) + + +async def test_register_config_panel(hass: HomeAssistant) -> None: + """Test setting up a custom config panel for an integration.""" + result = await setup.async_setup_component(hass, "panel_custom", {}) + assert result + + # Register a custom panel + await panel_custom.async_register_panel( + hass=hass, + frontend_url_path="config_panel", + webcomponent_name="custom-frontend", + module_url="custom-frontend", + embed_iframe=True, + require_admin=True, + config_panel_domain="test", + ) + + panels = hass.data.get(frontend.DATA_PANELS, []) + assert panels + assert "config_panel" in panels + + panel = panels["config_panel"] + + assert panel.config == { + "_panel_custom": { + "module_url": "custom-frontend", + "name": "custom-frontend", + "embed_iframe": True, + "trust_external": False, + }, + } + assert panel.frontend_url_path == "config_panel" + assert panel.config_panel_domain == "test" diff --git a/tests/components/panel_iframe/test_init.py b/tests/components/panel_iframe/test_init.py index 79bc7e37ee3747..bd8950163a997a 100644 --- a/tests/components/panel_iframe/test_init.py +++ b/tests/components/panel_iframe/test_init.py @@ -54,6 +54,7 @@ async def test_correct_config(hass: HomeAssistant) -> None: assert panels.get("router").to_response() == { "component_name": "iframe", "config": {"url": "http://192.168.1.1"}, + "config_panel_domain": None, "icon": "mdi:network-wireless", "title": "Router", "url_path": "router", @@ -63,6 +64,7 @@ async def test_correct_config(hass: HomeAssistant) -> None: assert panels.get("weather").to_response() == { "component_name": "iframe", "config": {"url": "https://www.wunderground.com/us/ca/san-diego"}, + "config_panel_domain": None, "icon": "mdi:weather", "title": "Weather", "url_path": "weather", @@ -72,6 +74,7 @@ async def test_correct_config(hass: HomeAssistant) -> None: assert panels.get("api").to_response() == { "component_name": "iframe", "config": {"url": "/api"}, + "config_panel_domain": None, "icon": "mdi:weather", "title": "Api", "url_path": "api", @@ -81,6 +84,7 @@ async def test_correct_config(hass: HomeAssistant) -> None: assert panels.get("ftp").to_response() == { "component_name": "iframe", "config": {"url": "ftp://some/ftp"}, + "config_panel_domain": None, "icon": "mdi:weather", "title": "FTP", "url_path": "ftp", From 8b5bdf9e2fd352c570251a44817fdb3e673688d7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 12:09:22 +0200 Subject: [PATCH 0600/1009] Add entity translations to Whirlpool (#96823) --- homeassistant/components/whirlpool/climate.py | 1 + homeassistant/components/whirlpool/sensor.py | 8 +++----- homeassistant/components/whirlpool/strings.json | 5 +++++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py index 2b658387ef52d6..d1c5d6cf8f82a2 100644 --- a/homeassistant/components/whirlpool/climate.py +++ b/homeassistant/components/whirlpool/climate.py @@ -93,6 +93,7 @@ class AirConEntity(ClimateEntity): _attr_fan_modes = SUPPORTED_FAN_MODES _attr_has_entity_name = True + _attr_name = None _attr_hvac_modes = SUPPORTED_HVAC_MODES _attr_max_temp = SUPPORTED_MAX_TEMP _attr_min_temp = SUPPORTED_MIN_TEMP diff --git a/homeassistant/components/whirlpool/sensor.py b/homeassistant/components/whirlpool/sensor.py index de415035c76886..37b16530b0d902 100644 --- a/homeassistant/components/whirlpool/sensor.py +++ b/homeassistant/components/whirlpool/sensor.py @@ -105,7 +105,6 @@ class WhirlpoolSensorEntityDescription( SENSORS: tuple[WhirlpoolSensorEntityDescription, ...] = ( WhirlpoolSensorEntityDescription( key="state", - name="State", translation_key="whirlpool_machine", device_class=SensorDeviceClass.ENUM, options=( @@ -117,7 +116,6 @@ class WhirlpoolSensorEntityDescription( ), WhirlpoolSensorEntityDescription( key="DispenseLevel", - name="Detergent Level", translation_key="whirlpool_tank", entity_registry_enabled_default=False, device_class=SensorDeviceClass.ENUM, @@ -131,7 +129,7 @@ class WhirlpoolSensorEntityDescription( SENSOR_TIMER: tuple[SensorEntityDescription] = ( SensorEntityDescription( key="timeremaining", - name="End Time", + translation_key="end_time", device_class=SensorDeviceClass.TIMESTAMP, ), ) @@ -183,6 +181,7 @@ class WasherDryerClass(SensorEntity): """A class for the whirlpool/maytag washer account.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__( self, @@ -205,7 +204,6 @@ def __init__( name=name.capitalize(), manufacturer="Whirlpool", ) - self._attr_has_entity_name = True self._attr_unique_id = f"{said}-{description.key}" async def async_added_to_hass(self) -> None: @@ -231,6 +229,7 @@ class WasherDryerTimeClass(RestoreSensor): """A timestamp class for the whirlpool/maytag washer account.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__( self, @@ -254,7 +253,6 @@ def __init__( name=name.capitalize(), manufacturer="Whirlpool", ) - self._attr_has_entity_name = True self._attr_unique_id = f"{said}-{description.key}" async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/whirlpool/strings.json b/homeassistant/components/whirlpool/strings.json index 94dc9aa219fe2a..a24e42304d041c 100644 --- a/homeassistant/components/whirlpool/strings.json +++ b/homeassistant/components/whirlpool/strings.json @@ -21,6 +21,7 @@ "entity": { "sensor": { "whirlpool_machine": { + "name": "State", "state": { "standby": "[%key:common::state::standby%]", "setting": "Setting", @@ -51,6 +52,7 @@ } }, "whirlpool_tank": { + "name": "Detergent level", "state": { "unknown": "Unknown", "empty": "Empty", @@ -59,6 +61,9 @@ "100": "100%", "active": "[%key:common::state::active%]" } + }, + "end_time": { + "name": "End time" } } } From 4ceba01ab73f96a5d980f3cc032e74776c8fafa7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 18 Jul 2023 12:10:40 +0200 Subject: [PATCH 0601/1009] Prevent creating scripts which override script services (#96828) --- homeassistant/components/script/config.py | 23 ++++++++++++++- tests/components/config/test_script.py | 34 +++++++++++++++++++++++ tests/components/script/test_init.py | 9 ++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/script/config.py b/homeassistant/components/script/config.py index 10c7f08484b8cf..c11bb37294fe39 100644 --- a/homeassistant/components/script/config.py +++ b/homeassistant/components/script/config.py @@ -23,6 +23,10 @@ CONF_SELECTOR, CONF_SEQUENCE, CONF_VARIABLES, + SERVICE_RELOAD, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -57,6 +61,23 @@ extra=vol.ALLOW_EXTRA, ) +_INVALID_OBJECT_IDS = { + SERVICE_RELOAD, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_TOGGLE, +} + +_SCRIPT_OBJECT_ID_SCHEMA = vol.All( + cv.slug, + vol.NotIn( + _INVALID_OBJECT_IDS, + ( + "A script's object_id must not be one of " + f"{', '.join(sorted(_INVALID_OBJECT_IDS))}" + ), + ), +) SCRIPT_ENTITY_SCHEMA = make_script_schema( { @@ -170,7 +191,7 @@ def _minimal_config() -> ScriptConfig: script_name = f"Script with alias '{config[CONF_ALIAS]}'" try: - cv.slug(object_id) + _SCRIPT_OBJECT_ID_SCHEMA(object_id) except vol.Invalid as err: _log_invalid_script(err, script_name, "has invalid object id", object_id) raise diff --git a/tests/components/config/test_script.py b/tests/components/config/test_script.py index 34f807e3cc5960..86ea2cf9e7f51e 100644 --- a/tests/components/config/test_script.py +++ b/tests/components/config/test_script.py @@ -86,6 +86,40 @@ async def test_update_script_config( assert new_data["moon"] == {"alias": "Moon updated", "sequence": []} +@pytest.mark.parametrize("script_config", ({},)) +async def test_invalid_object_id( + hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store +) -> None: + """Test creating a script with an invalid object_id.""" + with patch.object(config, "SECTIONS", ["script"]): + await async_setup_component(hass, "config", {}) + + assert sorted(hass.states.async_entity_ids("script")) == [] + + client = await hass_client() + + hass_config_store["scripts.yaml"] = {} + + resp = await client.post( + "/api/config/script/config/turn_on", + data=json.dumps({"alias": "Turn on", "sequence": []}), + ) + await hass.async_block_till_done() + assert sorted(hass.states.async_entity_ids("script")) == [] + + assert resp.status == HTTPStatus.BAD_REQUEST + result = await resp.json() + assert result == { + "message": ( + "Message malformed: A script's object_id must not be one of " + "reload, toggle, turn_off, turn_on" + ) + } + + new_data = hass_config_store["scripts.yaml"] + assert new_data == {} + + @pytest.mark.parametrize("script_config", ({},)) @pytest.mark.parametrize( ("updated_config", "validation_error"), diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index cc41b6c404cd24..cddefc8d3dcc65 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -196,6 +196,15 @@ async def test_setup_with_invalid_configs( "has invalid object id", "invalid slug Bad Script", ), + ( + "turn_on", + {}, + "has invalid object id", + ( + "A script's object_id must not be one of " + "reload, toggle, turn_off, turn_on. Got 'turn_on'" + ), + ), ), ) async def test_bad_config_validation_critical( From b9f92b526bc64824017c3937c1fde49521c595d7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 18 Jul 2023 12:17:31 +0200 Subject: [PATCH 0602/1009] Add prefix support to text selector (#96830) --- homeassistant/helpers/selector.py | 2 ++ tests/helpers/test_selector.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index c996fcaf524f6e..61ac81b0bcab8c 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -1137,6 +1137,7 @@ class TextSelectorConfig(TypedDict, total=False): """Class to represent a text selector config.""" multiline: bool + prefix: str suffix: str type: TextSelectorType autocomplete: str @@ -1169,6 +1170,7 @@ class TextSelector(Selector[TextSelectorConfig]): CONFIG_SCHEMA = vol.Schema( { vol.Optional("multiline", default=False): bool, + vol.Optional("prefix"): str, vol.Optional("suffix"): str, # The "type" controls the input field in the browser, the resulting # data can be any string so we don't validate it. diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 09cf79116a0af2..10dc825372f268 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -581,6 +581,7 @@ def test_object_selector_schema(schema, valid_selections, invalid_selections) -> ({}, ("abc123",), (None,)), ({"multiline": True}, (), ()), ({"multiline": False, "type": "email"}, (), ()), + ({"prefix": "before", "suffix": "after"}, (), ()), ), ) def test_text_selector_schema(schema, valid_selections, invalid_selections) -> None: From 5f0e5b7e0c2f8205f4e2c43c3a277d7f39c4299f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 12:17:41 +0200 Subject: [PATCH 0603/1009] Migrate Volumio to has entity naming (#96822) --- homeassistant/components/volumio/media_player.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 1d586198c5a21c..880d02cfeae7b1 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -49,6 +49,8 @@ async def async_setup_entry( class Volumio(MediaPlayerEntity): """Volumio Player Object.""" + _attr_has_entity_name = True + _attr_name = None _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE @@ -89,11 +91,6 @@ def unique_id(self): """Return the unique id for the entity.""" return self._uid - @property - def name(self): - """Return the name of the entity.""" - return self._name - @property def device_info(self) -> DeviceInfo: """Return device info for this device.""" @@ -101,7 +98,7 @@ def device_info(self) -> DeviceInfo: identifiers={(DOMAIN, self.unique_id)}, manufacturer="Volumio", model=self._info["hardware"], - name=self.name, + name=self._name, sw_version=self._info["systemversion"], ) From faa67a40c453e7fbef9e3e8ff8af7ab1b974a697 Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Tue, 18 Jul 2023 12:24:02 +0200 Subject: [PATCH 0604/1009] =?UTF-8?q?Rename=20'life'=20to=20'lifetime'=20i?= =?UTF-8?q?n=20tr=C3=A5dfri=20(#96818)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit String review: rename 'life' to 'lifetime' - The term life, such as in 'filter life' can be ambiguous. - Renamed to 'lifetime', as quite a few integrations use the term 'lifetime' to express this concept - Improves consistency and should be easier to understand. --- homeassistant/components/tradfri/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 1b3839ce2d7d56..3eb4d72effd252 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -65,7 +65,7 @@ def _get_air_quality(device: Device) -> int | None: def _get_filter_time_left(device: Device) -> int: - """Fetch the filter's remaining life (in hours).""" + """Fetch the filter's remaining lifetime (in hours).""" assert device.air_purifier_control is not None return round( cast( From c253549e6883fc8c62aeb5b77d1c415d844f5629 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 12:38:17 +0200 Subject: [PATCH 0605/1009] Migrate Songpal to has entity name (#96753) --- homeassistant/components/songpal/media_player.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 0d41aec699b717..bc5e15ba989026 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -100,6 +100,8 @@ class SongpalEntity(MediaPlayerEntity): | MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF ) + _attr_has_entity_name = True + _attr_name = None def __init__(self, name, device): """Init.""" @@ -197,11 +199,6 @@ async def handle_stop(event): self.hass.loop.create_task(self._dev.listen_notifications()) - @property - def name(self): - """Return name of the device.""" - return self._name - @property def unique_id(self): """Return a unique ID.""" @@ -220,7 +217,7 @@ def device_info(self) -> DeviceInfo: identifiers={(DOMAIN, self.unique_id)}, manufacturer="Sony Corporation", model=self._model, - name=self.name, + name=self._name, sw_version=self._sysinfo.version, ) From 9a2a920fd4a6007b7041f1ecfa352e67feb20cb3 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 0606/1009] 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 36d0a67fded4fd..71952431b5ae3b 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==10.0.0" ] diff --git a/requirements_all.txt b/requirements_all.txt index cdd695d4d74a6b..b1989ae25a0068 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1603,7 +1603,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 2a18d0a764eaa5eaf02180081b5809e06fc77f12 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 18 Jul 2023 13:37:17 +0200 Subject: [PATCH 0607/1009] Do not include stack trace when shell_command service times out (#96833) --- homeassistant/components/shell_command/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 0cc979a321f80b..36c3a5dbda5021 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -86,7 +86,7 @@ async def async_service_handler(service: ServiceCall) -> None: async with async_timeout.timeout(COMMAND_TIMEOUT): stdout_data, stderr_data = await process.communicate() except asyncio.TimeoutError: - _LOGGER.exception( + _LOGGER.error( "Timed out running command: `%s`, after: %ss", cmd, COMMAND_TIMEOUT ) if process: From 5c54fa1ce1e36388b82367b9163094b2f3110924 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 18 Jul 2023 13:37:27 +0200 Subject: [PATCH 0608/1009] Fix shell_command timeout test (#96834) * Fix shell_command timeout test * Improve test --- tests/components/shell_command/test_init.py | 36 ++++++++++++++------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index 96ccaa47d849e8..fe685398c5d4fe 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -1,9 +1,10 @@ """The tests for the Shell command component.""" from __future__ import annotations +import asyncio import os import tempfile -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest @@ -160,24 +161,37 @@ async def test_stderr_captured(mock_output, hass: HomeAssistant) -> None: assert test_phrase.encode() + b"\n" == mock_output.call_args_list[0][0][-1] -@pytest.mark.skip(reason="disabled to check if it fixes flaky CI") -async def test_do_no_run_forever( +async def test_do_not_run_forever( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test subprocesses terminate after the timeout.""" - with patch.object(shell_command, "COMMAND_TIMEOUT", 0.001): - assert await async_setup_component( - hass, - shell_command.DOMAIN, - {shell_command.DOMAIN: {"test_service": "sleep 10000"}}, - ) - await hass.async_block_till_done() + async def block(): + event = asyncio.Event() + await event.wait() + return (None, None) + + mock_process = Mock() + mock_process.communicate = block + mock_process.kill = Mock() + mock_create_subprocess_shell = AsyncMock(return_value=mock_process) + + assert await async_setup_component( + hass, + shell_command.DOMAIN, + {shell_command.DOMAIN: {"test_service": "mock_sleep 10000"}}, + ) + await hass.async_block_till_done() + with patch.object(shell_command, "COMMAND_TIMEOUT", 0.001), patch( + "homeassistant.components.shell_command.asyncio.create_subprocess_shell", + side_effect=mock_create_subprocess_shell, + ): await hass.services.async_call( shell_command.DOMAIN, "test_service", blocking=True ) await hass.async_block_till_done() + mock_process.kill.assert_called_once() assert "Timed out" in caplog.text - assert "sleep 10000" in caplog.text + assert "mock_sleep 10000" in caplog.text From d46a72e5abf00ac7f4831564b251d0aaa1cd34be Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 13:39:40 +0200 Subject: [PATCH 0609/1009] Migrate Zerproc to has entity naming (#96837) --- homeassistant/components/zerproc/light.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zerproc/light.py b/homeassistant/components/zerproc/light.py index 5a32ca23332684..41ecb751b869c9 100644 --- a/homeassistant/components/zerproc/light.py +++ b/homeassistant/components/zerproc/light.py @@ -82,6 +82,8 @@ class ZerprocLight(LightEntity): _attr_color_mode = ColorMode.HS _attr_icon = "mdi:string-lights" _attr_supported_color_modes = {ColorMode.HS} + _attr_has_entity_name = True + _attr_name = None def __init__(self, light) -> None: """Initialize a Zerproc light.""" @@ -106,11 +108,6 @@ async def async_will_remove_from_hass(self) -> None: "Exception disconnecting from %s", self._light.address, exc_info=True ) - @property - def name(self): - """Return the display name of this light.""" - return self._light.name - @property def unique_id(self): """Return the ID of this light.""" @@ -122,7 +119,7 @@ def device_info(self) -> DeviceInfo: return DeviceInfo( identifiers={(DOMAIN, self.unique_id)}, manufacturer="Zerproc", - name=self.name, + name=self._light.name, ) async def async_turn_on(self, **kwargs: Any) -> None: From 8a9f117bdcd7424969783707538e2197f5060e3d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 13:40:06 +0200 Subject: [PATCH 0610/1009] Add entity translations to zeversolar (#96838) * Add entity translations to zeversolar * Remove current power --- homeassistant/components/zeversolar/sensor.py | 3 +-- homeassistant/components/zeversolar/strings.json | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zeversolar/sensor.py b/homeassistant/components/zeversolar/sensor.py index 2243edc48e4387..ee9aa5531c83d1 100644 --- a/homeassistant/components/zeversolar/sensor.py +++ b/homeassistant/components/zeversolar/sensor.py @@ -39,7 +39,6 @@ class ZeversolarEntityDescription( SENSOR_TYPES = ( ZeversolarEntityDescription( key="pac", - name="Current power", icon="mdi:solar-power-variant", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, @@ -49,7 +48,7 @@ class ZeversolarEntityDescription( ), ZeversolarEntityDescription( key="energy_today", - name="Energy today", + translation_key="energy_today", icon="mdi:home-battery", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, diff --git a/homeassistant/components/zeversolar/strings.json b/homeassistant/components/zeversolar/strings.json index a4f52dc6aa36bd..0e2e23f244c1f8 100644 --- a/homeassistant/components/zeversolar/strings.json +++ b/homeassistant/components/zeversolar/strings.json @@ -16,5 +16,12 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "sensor": { + "energy_today": { + "name": "Energy today" + } + } } } From 8dc5f737895c01c431445b3333dcdd5cf435a949 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 13:58:42 +0200 Subject: [PATCH 0611/1009] Migrate Yolink to has entity name (#96839) * Migrate Yolink to has entity name * Add sensor --- .../components/yolink/binary_sensor.py | 10 ---- homeassistant/components/yolink/climate.py | 3 +- homeassistant/components/yolink/cover.py | 3 +- homeassistant/components/yolink/entity.py | 2 + homeassistant/components/yolink/light.py | 1 - homeassistant/components/yolink/lock.py | 3 +- homeassistant/components/yolink/sensor.py | 16 ++---- homeassistant/components/yolink/siren.py | 6 +-- homeassistant/components/yolink/strings.json | 51 +++++++++++++++++++ homeassistant/components/yolink/switch.py | 19 +++---- 10 files changed, 73 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/yolink/binary_sensor.py b/homeassistant/components/yolink/binary_sensor.py index 5b9dacb9db7e11..38ea7d4653753a 100644 --- a/homeassistant/components/yolink/binary_sensor.py +++ b/homeassistant/components/yolink/binary_sensor.py @@ -51,42 +51,35 @@ class YoLinkBinarySensorEntityDescription(BinarySensorEntityDescription): key="door_state", icon="mdi:door", device_class=BinarySensorDeviceClass.DOOR, - name="State", value=lambda value: value == "open" if value is not None else None, exists_fn=lambda device: device.device_type == ATTR_DEVICE_DOOR_SENSOR, ), YoLinkBinarySensorEntityDescription( key="motion_state", device_class=BinarySensorDeviceClass.MOTION, - name="Motion", value=lambda value: value == "alert" if value is not None else None, exists_fn=lambda device: device.device_type == ATTR_DEVICE_MOTION_SENSOR, ), YoLinkBinarySensorEntityDescription( key="leak_state", - name="Leak", - icon="mdi:water", device_class=BinarySensorDeviceClass.MOISTURE, value=lambda value: value == "alert" if value is not None else None, exists_fn=lambda device: device.device_type == ATTR_DEVICE_LEAK_SENSOR, ), YoLinkBinarySensorEntityDescription( key="vibration_state", - name="Vibration", device_class=BinarySensorDeviceClass.VIBRATION, value=lambda value: value == "alert" if value is not None else None, exists_fn=lambda device: device.device_type == ATTR_DEVICE_VIBRATION_SENSOR, ), YoLinkBinarySensorEntityDescription( key="co_detected", - name="Co Detected", device_class=BinarySensorDeviceClass.CO, value=lambda state: state.get("gasAlarm"), exists_fn=lambda device: device.device_type == ATTR_DEVICE_CO_SMOKE_SENSOR, ), YoLinkBinarySensorEntityDescription( key="smoke_detected", - name="Smoke Detected", device_class=BinarySensorDeviceClass.SMOKE, value=lambda state: state.get("smokeAlarm"), exists_fn=lambda device: device.device_type == ATTR_DEVICE_CO_SMOKE_SENSOR, @@ -135,9 +128,6 @@ def __init__( self._attr_unique_id = ( f"{coordinator.device.device_id} {self.entity_description.key}" ) - self._attr_name = ( - f"{coordinator.device.device_name} ({self.entity_description.name})" - ) @callback def update_entity_state(self, state: dict[str, Any]) -> None: diff --git a/homeassistant/components/yolink/climate.py b/homeassistant/components/yolink/climate.py index e9d11fb77d008e..6e4495ee0b941f 100644 --- a/homeassistant/components/yolink/climate.py +++ b/homeassistant/components/yolink/climate.py @@ -61,6 +61,8 @@ async def async_setup_entry( class YoLinkClimateEntity(YoLinkEntity, ClimateEntity): """YoLink Climate Entity.""" + _attr_name = None + def __init__( self, config_entry: ConfigEntry, @@ -69,7 +71,6 @@ def __init__( """Init YoLink Thermostat.""" super().__init__(config_entry, coordinator) self._attr_unique_id = f"{coordinator.device.device_id}_climate" - self._attr_name = f"{coordinator.device.device_name} (Thermostat)" self._attr_temperature_unit = UnitOfTemperature.CELSIUS self._attr_fan_modes = [FAN_ON, FAN_AUTO] self._attr_min_temp = -10 diff --git a/homeassistant/components/yolink/cover.py b/homeassistant/components/yolink/cover.py index 1b22f76f177846..0d1f1e590b468c 100644 --- a/homeassistant/components/yolink/cover.py +++ b/homeassistant/components/yolink/cover.py @@ -38,6 +38,8 @@ async def async_setup_entry( class YoLinkCoverEntity(YoLinkEntity, CoverEntity): """YoLink Cover Entity.""" + _attr_name = None + def __init__( self, config_entry: ConfigEntry, @@ -46,7 +48,6 @@ def __init__( """Init YoLink garage door entity.""" super().__init__(config_entry, coordinator) self._attr_unique_id = f"{coordinator.device.device_id}_door_state" - self._attr_name = f"{coordinator.device.device_name} (State)" self._attr_device_class = CoverDeviceClass.GARAGE self._attr_supported_features = ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE diff --git a/homeassistant/components/yolink/entity.py b/homeassistant/components/yolink/entity.py index 76ef1ecd5340aa..09da5545d57454 100644 --- a/homeassistant/components/yolink/entity.py +++ b/homeassistant/components/yolink/entity.py @@ -19,6 +19,8 @@ class YoLinkEntity(CoordinatorEntity[YoLinkCoordinator]): """YoLink Device Basic Entity.""" + _attr_has_entity_name = True + def __init__( self, config_entry: ConfigEntry, diff --git a/homeassistant/components/yolink/light.py b/homeassistant/components/yolink/light.py index a7f52e801b2c3c..248a42df60ca12 100644 --- a/homeassistant/components/yolink/light.py +++ b/homeassistant/components/yolink/light.py @@ -35,7 +35,6 @@ class YoLinkDimmerEntity(YoLinkEntity, LightEntity): """YoLink Dimmer Entity.""" _attr_color_mode = ColorMode.BRIGHTNESS - _attr_has_entity_name = True _attr_name = None _attr_supported_color_modes: set[ColorMode] = {ColorMode.BRIGHTNESS} diff --git a/homeassistant/components/yolink/lock.py b/homeassistant/components/yolink/lock.py index 7565c66867a7cd..3b0f68c175c073 100644 --- a/homeassistant/components/yolink/lock.py +++ b/homeassistant/components/yolink/lock.py @@ -34,6 +34,8 @@ async def async_setup_entry( class YoLinkLockEntity(YoLinkEntity, LockEntity): """YoLink Lock Entity.""" + _attr_name = None + def __init__( self, config_entry: ConfigEntry, @@ -42,7 +44,6 @@ def __init__( """Init YoLink Lock.""" super().__init__(config_entry, coordinator) self._attr_unique_id = f"{coordinator.device.device_id}_lock_state" - self._attr_name = f"{coordinator.device.device_name}(LockState)" @callback def update_entity_state(self, state: dict[str, Any]) -> None: diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 75c4949859cb45..149bdc0adf89df 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -126,7 +126,6 @@ def cvt_volume(val: int | None) -> str | None: key="battery", device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, - name="Battery", state_class=SensorStateClass.MEASUREMENT, value=cvt_battery, exists_fn=lambda device: device.device_type in BATTERY_POWER_SENSOR, @@ -135,7 +134,6 @@ def cvt_volume(val: int | None) -> str | None: key="humidity", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, - name="Humidity", state_class=SensorStateClass.MEASUREMENT, exists_fn=lambda device: device.device_type in [ATTR_DEVICE_TH_SENSOR], ), @@ -143,7 +141,6 @@ def cvt_volume(val: int | None) -> str | None: key="temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, - name="Temperature", state_class=SensorStateClass.MEASUREMENT, exists_fn=lambda device: device.device_type in [ATTR_DEVICE_TH_SENSOR], ), @@ -152,7 +149,6 @@ def cvt_volume(val: int | None) -> str | None: key="devTemperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, - name="Temperature", state_class=SensorStateClass.MEASUREMENT, exists_fn=lambda device: device.device_type in MCU_DEV_TEMPERATURE_SENSOR, should_update_entity=lambda value: value is not None, @@ -161,7 +157,6 @@ def cvt_volume(val: int | None) -> str | None: key="loraInfo", device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - name="Signal", value=lambda value: value["signal"] if value is not None else None, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -170,16 +165,16 @@ def cvt_volume(val: int | None) -> str | None: ), YoLinkSensorEntityDescription( key="state", + translation_key="power_failure_alarm", device_class=SensorDeviceClass.ENUM, - name="Power failure alarm", icon="mdi:flash", options=["normal", "alert", "off"], exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM, ), YoLinkSensorEntityDescription( key="mute", + translation_key="power_failure_alarm_mute", device_class=SensorDeviceClass.ENUM, - name="Power failure alarm mute", icon="mdi:volume-mute", options=["muted", "unmuted"], exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM, @@ -187,8 +182,8 @@ def cvt_volume(val: int | None) -> str | None: ), YoLinkSensorEntityDescription( key="sound", + translation_key="power_failure_alarm_volume", device_class=SensorDeviceClass.ENUM, - name="Power failure alarm volume", icon="mdi:volume-high", options=["low", "medium", "high"], exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM, @@ -196,8 +191,8 @@ def cvt_volume(val: int | None) -> str | None: ), YoLinkSensorEntityDescription( key="beep", + translation_key="power_failure_alarm_beep", device_class=SensorDeviceClass.ENUM, - name="Power failure alarm beep", icon="mdi:bullhorn", options=["enabled", "disabled"], exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM, @@ -249,9 +244,6 @@ def __init__( self._attr_unique_id = ( f"{coordinator.device.device_id} {self.entity_description.key}" ) - self._attr_name = ( - f"{coordinator.device.device_name} ({self.entity_description.name})" - ) @callback def update_entity_state(self, state: dict) -> None: diff --git a/homeassistant/components/yolink/siren.py b/homeassistant/components/yolink/siren.py index ad51b912193718..81c2b46a840cec 100644 --- a/homeassistant/components/yolink/siren.py +++ b/homeassistant/components/yolink/siren.py @@ -34,7 +34,6 @@ class YoLinkSirenEntityDescription(SirenEntityDescription): DEVICE_TYPES: tuple[YoLinkSirenEntityDescription, ...] = ( YoLinkSirenEntityDescription( key="state", - name="State", value=lambda value: value == "alert" if value is not None else None, exists_fn=lambda device: device.device_type in [ATTR_DEVICE_SIREN], ), @@ -70,6 +69,8 @@ async def async_setup_entry( class YoLinkSirenEntity(YoLinkEntity, SirenEntity): """YoLink Siren Entity.""" + _attr_name = None + entity_description: YoLinkSirenEntityDescription def __init__( @@ -84,9 +85,6 @@ def __init__( self._attr_unique_id = ( f"{coordinator.device.device_id} {self.entity_description.key}" ) - self._attr_name = ( - f"{coordinator.device.device_name} ({self.entity_description.name})" - ) self._attr_supported_features = ( SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF ) diff --git a/homeassistant/components/yolink/strings.json b/homeassistant/components/yolink/strings.json index de16e1a6e3923a..b1cd8d87a7542b 100644 --- a/homeassistant/components/yolink/strings.json +++ b/homeassistant/components/yolink/strings.json @@ -33,5 +33,56 @@ "button_4_short_press": "Button_4 (short press)", "button_4_long_press": "Button_4 (long press)" } + }, + "entity": { + "switch": { + "usb_ports": { + "name": "USB ports" + }, + "plug_1": { + "name": "Plug 1" + }, + "plug_2": { + "name": "Plug 2" + }, + "plug_3": { + "name": "Plug 3" + }, + "plug_4": { + "name": "Plug 4" + } + }, + "sensor": { + "power_failure_alarm": { + "name": "Power failure alarm", + "state": { + "normal": "Normal", + "alert": "Alert", + "off": "[%key:common::state::off%]" + } + }, + "power_failure_alarm_mute": { + "name": "Power failure alarm mute", + "state": { + "muted": "Muted", + "unmuted": "Unmuted" + } + }, + "power_failure_alarm_volume": { + "name": "Power failure alarm volume", + "state": { + "low": "Low", + "medium": "Medium", + "high": "High" + } + }, + "power_failure_alarm_beep": { + "name": "Power failure alarm beep", + "state": { + "enabled": "[%key:common::state::enabled%]", + "disabled": "[%key:common::state::disabled%]" + } + } + } } } diff --git a/homeassistant/components/yolink/switch.py b/homeassistant/components/yolink/switch.py index 773477e6c3f17c..415c1e9584de41 100644 --- a/homeassistant/components/yolink/switch.py +++ b/homeassistant/components/yolink/switch.py @@ -40,52 +40,52 @@ class YoLinkSwitchEntityDescription(SwitchEntityDescription): YoLinkSwitchEntityDescription( key="outlet_state", device_class=SwitchDeviceClass.OUTLET, - name="State", + name=None, exists_fn=lambda device: device.device_type == ATTR_DEVICE_OUTLET, ), YoLinkSwitchEntityDescription( key="manipulator_state", - name="State", + name=None, icon="mdi:pipe", exists_fn=lambda device: device.device_type == ATTR_DEVICE_MANIPULATOR, ), YoLinkSwitchEntityDescription( key="switch_state", - name="State", + name=None, device_class=SwitchDeviceClass.SWITCH, exists_fn=lambda device: device.device_type == ATTR_DEVICE_SWITCH, ), YoLinkSwitchEntityDescription( key="multi_outlet_usb_ports", - name="UsbPorts", + translation_key="usb_ports", device_class=SwitchDeviceClass.OUTLET, exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET, plug_index=0, ), YoLinkSwitchEntityDescription( key="multi_outlet_plug_1", - name="Plug1", + translation_key="plug_1", device_class=SwitchDeviceClass.OUTLET, exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET, plug_index=1, ), YoLinkSwitchEntityDescription( key="multi_outlet_plug_2", - name="Plug2", + translation_key="plug_2", device_class=SwitchDeviceClass.OUTLET, exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET, plug_index=2, ), YoLinkSwitchEntityDescription( key="multi_outlet_plug_3", - name="Plug3", + translation_key="plug_3", device_class=SwitchDeviceClass.OUTLET, exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET, plug_index=3, ), YoLinkSwitchEntityDescription( key="multi_outlet_plug_4", - name="Plug4", + translation_key="plug_4", device_class=SwitchDeviceClass.OUTLET, exists_fn=lambda device: device.device_type == ATTR_DEVICE_MULTI_OUTLET, plug_index=4, @@ -141,9 +141,6 @@ def __init__( self._attr_unique_id = ( f"{coordinator.device.device_id} {self.entity_description.key}" ) - self._attr_name = ( - f"{coordinator.device.device_name} ({self.entity_description.name})" - ) def _get_state( self, state_value: str | list[str] | None, plug_index: int | None From 1ace9ab82e76985bbd158714d7685d74c389c788 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 14:08:18 +0200 Subject: [PATCH 0612/1009] Make Spotify accept user playlist uris (#96820) * Make Spotify accept user platlist uris * Fix feedback * Fix feedback --- .../components/spotify/media_player.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index de2cced08b5032..3c2f9ef729cadf 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -398,13 +398,24 @@ def update(self) -> None: ) self._currently_playing = current or {} - context = self._currently_playing.get("context") + context = self._currently_playing.get("context", {}) + + # For some users in some cases, the uri is formed like + # "spotify:user:{name}:playlist:{id}" and spotipy wants + # the type to be playlist. + uri = context.get("uri") + if uri is not None: + parts = uri.split(":") + if len(parts) == 5 and parts[1] == "user" and parts[3] == "playlist": + uri = ":".join([parts[0], parts[3], parts[4]]) + if context is not None and ( - self._playlist is None or self._playlist["uri"] != context["uri"] + self._playlist is None or self._playlist["uri"] != uri ): self._playlist = None if context["type"] == MediaType.PLAYLIST: - self._playlist = self.data.client.playlist(current["context"]["uri"]) + self._playlist = self.data.client.playlist(uri) + device = self._currently_playing.get("device") if device is not None: self._restricted_device = device["is_restricted"] From 4ae69787a2f20649b5dec3124251ae057b4264e4 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 0613/1009] 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 d2d0dba6773722..5d7e29c131215c 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 @@ def __init__(self, device): """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 @@ async def async_set_cover_position(self, **kwargs: Any) -> None: 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 @@ async def async_update(self) -> None: 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 @@ def is_closed(self) -> bool | None: @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 29eb681dc4d081..89e5071051c287 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 b1989ae25a0068..9bf2a461a12591 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2006,10 +2006,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 75b8fc80d06b0d..1487828b24673c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1492,10 +1492,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 bdf3cc901a7133..bf781c71c4e832 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -113,8 +113,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", @@ -140,6 +142,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 f9a0877bb9887e638c6940bc6aa10a48b2f317c8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 14:20:30 +0200 Subject: [PATCH 0614/1009] Change device classes for Airvisual Pro (#96474) Change device classes --- homeassistant/components/airvisual_pro/sensor.py | 6 ++---- homeassistant/components/airvisual_pro/strings.json | 3 +++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airvisual_pro/sensor.py b/homeassistant/components/airvisual_pro/sensor.py index 69fbd1a128a092..188647b73384ea 100644 --- a/homeassistant/components/airvisual_pro/sensor.py +++ b/homeassistant/components/airvisual_pro/sensor.py @@ -86,16 +86,14 @@ class AirVisualProMeasurementDescription( ), AirVisualProMeasurementDescription( key="particulate_matter_0_1", - name="PM 0.1", - device_class=SensorDeviceClass.PM1, + translation_key="pm01", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda settings, status, measurements, history: measurements["pm0_1"], ), AirVisualProMeasurementDescription( key="particulate_matter_1_0", - name="PM 1.0", - device_class=SensorDeviceClass.PM10, + device_class=SensorDeviceClass.PM1, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda settings, status, measurements, history: measurements["pm1_0"], diff --git a/homeassistant/components/airvisual_pro/strings.json b/homeassistant/components/airvisual_pro/strings.json index 04801c8fa0e395..b5c68371fdf2cf 100644 --- a/homeassistant/components/airvisual_pro/strings.json +++ b/homeassistant/components/airvisual_pro/strings.json @@ -27,6 +27,9 @@ }, "entity": { "sensor": { + "pm01": { + "name": "PM0.1" + }, "outdoor_air_quality_index": { "name": "Outdoor air quality index" } From 7c22225cd19ddd4e84d71781794876e6715a854d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 18 Jul 2023 14:29:45 +0200 Subject: [PATCH 0615/1009] Allow ADR 0007 compliant schema for mqtt (#94305) * Enforce listed entities in MQTT yaml config * Add tests for setup with listed items * Fix test * Remove validator add comment * Update homeassistant/components/mqtt/__init__.py Co-authored-by: Erik Montnemery --------- Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 67 +++++++++++++------ .../components/mqtt/config_integration.py | 2 +- homeassistant/components/mqtt/mixins.py | 11 ++- homeassistant/components/mqtt/models.py | 2 +- .../mqtt/test_alarm_control_panel.py | 6 +- tests/components/mqtt/test_binary_sensor.py | 6 +- tests/components/mqtt/test_button.py | 6 +- tests/components/mqtt/test_camera.py | 6 +- tests/components/mqtt/test_climate.py | 6 +- tests/components/mqtt/test_cover.py | 6 +- tests/components/mqtt/test_fan.py | 6 +- tests/components/mqtt/test_humidifier.py | 6 +- tests/components/mqtt/test_init.py | 4 +- tests/components/mqtt/test_legacy_vacuum.py | 6 +- tests/components/mqtt/test_light.py | 6 +- tests/components/mqtt/test_light_json.py | 6 +- tests/components/mqtt/test_light_template.py | 6 +- tests/components/mqtt/test_lock.py | 6 +- tests/components/mqtt/test_number.py | 6 +- tests/components/mqtt/test_scene.py | 6 +- tests/components/mqtt/test_select.py | 6 +- tests/components/mqtt/test_sensor.py | 6 +- tests/components/mqtt/test_siren.py | 6 +- tests/components/mqtt/test_state_vacuum.py | 6 +- tests/components/mqtt/test_switch.py | 6 +- tests/components/mqtt/test_text.py | 6 +- tests/components/mqtt/test_update.py | 6 +- tests/components/mqtt/test_water_heater.py | 6 +- 28 files changed, 176 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index de5093d18177ee..405eb86e6ec255 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -5,7 +5,7 @@ from collections.abc import Callable from datetime import datetime import logging -from typing import Any, cast +from typing import Any, TypeVar, cast import jinja2 import voluptuous as vol @@ -42,7 +42,7 @@ publish, subscribe, ) -from .config_integration import PLATFORM_CONFIG_SCHEMA_BASE +from .config_integration import CONFIG_SCHEMA_BASE from .const import ( # noqa: F401 ATTR_PAYLOAD, ATTR_QOS, @@ -130,25 +130,54 @@ CONF_WILL_MESSAGE, ] +_T = TypeVar("_T") + +REMOVED_OPTIONS = vol.All( + cv.removed(CONF_BIRTH_MESSAGE), # Removed in HA Core 2023.4 + cv.removed(CONF_BROKER), # Removed in HA Core 2023.4 + cv.removed(CONF_CERTIFICATE), # Removed in HA Core 2023.4 + cv.removed(CONF_CLIENT_ID), # Removed in HA Core 2023.4 + cv.removed(CONF_CLIENT_CERT), # Removed in HA Core 2023.4 + cv.removed(CONF_CLIENT_KEY), # Removed in HA Core 2023.4 + cv.removed(CONF_DISCOVERY), # Removed in HA Core 2022.3 + cv.removed(CONF_DISCOVERY_PREFIX), # Removed in HA Core 2023.4 + cv.removed(CONF_KEEPALIVE), # Removed in HA Core 2023.4 + cv.removed(CONF_PASSWORD), # Removed in HA Core 2023.4 + cv.removed(CONF_PORT), # Removed in HA Core 2023.4 + cv.removed(CONF_PROTOCOL), # Removed in HA Core 2023.4 + cv.removed(CONF_TLS_INSECURE), # Removed in HA Core 2023.4 + cv.removed(CONF_USERNAME), # Removed in HA Core 2023.4 + cv.removed(CONF_WILL_MESSAGE), # Removed in HA Core 2023.4 +) + +# We accept 2 schemes for configuring manual MQTT items +# +# Preferred style: +# +# mqtt: +# - {domain}: +# name: "" +# ... +# - {domain}: +# name: "" +# ... +# ``` +# +# Legacy supported style: +# +# mqtt: +# {domain}: +# - name: "" +# ... +# - name: "" +# ... CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - cv.removed(CONF_BIRTH_MESSAGE), # Removed in HA Core 2023.4 - cv.removed(CONF_BROKER), # Removed in HA Core 2023.4 - cv.removed(CONF_CERTIFICATE), # Removed in HA Core 2023.4 - cv.removed(CONF_CLIENT_ID), # Removed in HA Core 2023.4 - cv.removed(CONF_CLIENT_CERT), # Removed in HA Core 2023.4 - cv.removed(CONF_CLIENT_KEY), # Removed in HA Core 2023.4 - cv.removed(CONF_DISCOVERY), # Removed in HA Core 2022.3 - cv.removed(CONF_DISCOVERY_PREFIX), # Removed in HA Core 2023.4 - cv.removed(CONF_KEEPALIVE), # Removed in HA Core 2023.4 - cv.removed(CONF_PASSWORD), # Removed in HA Core 2023.4 - cv.removed(CONF_PORT), # Removed in HA Core 2023.4 - cv.removed(CONF_PROTOCOL), # Removed in HA Core 2023.4 - cv.removed(CONF_TLS_INSECURE), # Removed in HA Core 2023.4 - cv.removed(CONF_USERNAME), # Removed in HA Core 2023.4 - cv.removed(CONF_WILL_MESSAGE), # Removed in HA Core 2023.4 - PLATFORM_CONFIG_SCHEMA_BASE, + cv.ensure_list, + cv.remove_falsy, + [REMOVED_OPTIONS], + [CONFIG_SCHEMA_BASE], ) }, extra=vol.ALLOW_EXTRA, @@ -190,7 +219,7 @@ async def _setup_client() -> tuple[MqttData, dict[str, Any]]: # Fetch configuration conf = dict(entry.data) hass_config = await conf_util.async_hass_config_yaml(hass) - mqtt_yaml = PLATFORM_CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {})) + mqtt_yaml = CONFIG_SCHEMA(hass_config).get(DOMAIN, []) await async_create_certificate_temp_files(hass, conf) client = MQTT(hass, entry, conf) if DOMAIN in hass.data: diff --git a/homeassistant/components/mqtt/config_integration.py b/homeassistant/components/mqtt/config_integration.py index ba2e0427ba78ec..ef2c771218ada7 100644 --- a/homeassistant/components/mqtt/config_integration.py +++ b/homeassistant/components/mqtt/config_integration.py @@ -52,7 +52,7 @@ DEFAULT_TLS_PROTOCOL = "auto" -PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( +CONFIG_SCHEMA_BASE = vol.Schema( { Platform.ALARM_CONTROL_PANEL.value: vol.All( cv.ensure_list, diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index e1e5f3d61bb5a0..314800f33f2d64 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -309,9 +309,16 @@ async def _async_setup_entities() -> None: mqtt_data = get_mqtt_data(hass) if not (config_yaml := mqtt_data.config): return - if domain not in config_yaml: + setups: list[Coroutine[Any, Any, None]] = [ + async_setup(config) + for config_item in config_yaml + for config_domain, configs in config_item.items() + for config in configs + if config_domain == domain + ] + if not setups: return - await asyncio.gather(*[async_setup(config) for config in config_yaml[domain]]) + await asyncio.gather(*setups) # discover manual configured MQTT items mqtt_data.reload_handlers[domain] = _async_setup_entities diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 9f0a178ce8750d..fb11400a312e9b 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -289,7 +289,7 @@ class MqttData: """Keep the MQTT entry data.""" client: MQTT - config: ConfigType + config: list[ConfigType] debug_info_entities: dict[str, EntityDebugInfo] = field(default_factory=dict) debug_info_triggers: dict[tuple[str, str], TriggerDebugInfo] = field( default_factory=dict diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index ee32b7131c4a1a..d1b1d6b68b38a0 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -1096,7 +1096,11 @@ async def test_reloadable( await help_test_reloadable(hass, mqtt_client_mock, domain, config) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 921f46703c2f27..d32754625f4238 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -1203,7 +1203,11 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( assert state.state == STATE_UNAVAILABLE -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index e99182323c8189..fa16ef77817805 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -545,7 +545,11 @@ async def test_reloadable( await help_test_reloadable(hass, mqtt_client_mock, domain, config) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 8bb21f5eb51f7c..5552457c2131cf 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -439,7 +439,11 @@ async def test_reloadable( await help_test_reloadable(hass, mqtt_client_mock, domain, config) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 4a6d1bf64d4f5d..e717c04b317426 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -2484,7 +2484,11 @@ async def test_reloadable( await help_test_reloadable(hass, mqtt_client_mock, domain, config) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index c388ded6587a90..2eec5f8374b941 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -3642,7 +3642,11 @@ async def test_encoding_subscribable_topics( ) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index c4181a3f885b22..803a0d747666b7 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -2220,7 +2220,11 @@ async def test_reloadable( await help_test_reloadable(hass, mqtt_client_mock, domain, config) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 1c386b28703bca..0cc4d93684107f 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -1545,7 +1545,11 @@ async def test_reloadable( await help_test_reloadable(hass, mqtt_client_mock, domain, config) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 9432f23130161a..3395dc0825f233 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -2106,8 +2106,8 @@ async def test_setup_manual_mqtt_with_invalid_config( with pytest.raises(AssertionError): await mqtt_mock_entry() assert ( - "Invalid config for [mqtt]: required key not provided @ data['mqtt']['light'][0]['command_topic']." - " Got None. (See ?, line ?)" in caplog.text + "Invalid config for [mqtt]: required key not provided @ data['mqtt'][0]['light'][0]['command_topic']. " + "Got None. (See ?, line ?)" in caplog.text ) diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 6b1a74f256dcca..85e3bdd12b99d0 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -1087,7 +1087,11 @@ async def test_encoding_subscribable_topics( ) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 59d5090b7118da..08def9a923ee6c 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -3440,7 +3440,11 @@ async def test_sending_mqtt_xy_command_with_template( assert state.attributes["xy_color"] == (0.151, 0.343) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 5a7bedd91e65ad..7ff4ccbab8565c 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -2441,7 +2441,11 @@ async def test_encoding_subscribable_topics( ) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 4727caca2ccb04..0583a1176b698c 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -1354,7 +1354,11 @@ async def test_encoding_subscribable_topics( ) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 2b77a573bad8c2..bf7e1529a4e299 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -1006,7 +1006,11 @@ async def test_encoding_subscribable_topics( ) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index f882209139ce52..96d9cdcef64178 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -1097,7 +1097,11 @@ async def test_encoding_subscribable_topics( ) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 4da60d44bb758d..dfea7b3f915d3f 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -251,7 +251,11 @@ async def test_reloadable( await help_test_reloadable(hass, mqtt_client_mock, domain, config) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index 583e65bc61c996..f1903fa4c3c1e4 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -762,7 +762,11 @@ async def test_encoding_subscribable_topics( ) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index d5483cf3a748eb..d6ab692af52742 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -1385,7 +1385,11 @@ async def test_encoding_subscribable_topics( ) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 76a9cb9c6f6455..7c448eba85ee1a 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -1067,7 +1067,11 @@ async def test_encoding_subscribable_topics( ) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index b22fb96aa13706..a24884941fc11f 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -809,7 +809,11 @@ async def test_encoding_subscribable_topics( ) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index b06cfa34442564..4471cc7dc11faa 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -738,7 +738,11 @@ async def test_encoding_subscribable_topics( ) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_text.py b/tests/components/mqtt/test_text.py index b96b82277b0bd6..9e068a078249fe 100644 --- a/tests/components/mqtt/test_text.py +++ b/tests/components/mqtt/test_text.py @@ -738,7 +738,11 @@ async def test_encoding_subscribable_topics( ) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_update.py b/tests/components/mqtt/test_update.py index 8e2cdaf8eaa250..9c881352f8c2bd 100644 --- a/tests/components/mqtt/test_update.py +++ b/tests/components/mqtt/test_update.py @@ -696,7 +696,11 @@ async def test_entity_id_update_discovery_update( ) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_water_heater.py b/tests/components/mqtt/test_water_heater.py index 942a2ec87d4e9f..c4f798e05ec2d1 100644 --- a/tests/components/mqtt/test_water_heater.py +++ b/tests/components/mqtt/test_water_heater.py @@ -1087,7 +1087,11 @@ async def test_reloadable( await help_test_reloadable(hass, mqtt_client_mock, domain, config) -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) async def test_setup_manual_entity_from_yaml( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: From 0bdfb95d1d765bd218d43adbb4c9c68482fb5db2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 15:05:55 +0200 Subject: [PATCH 0616/1009] Add entity translations to Whois (#96824) * Add entity translations to Whois * Fix tests --- homeassistant/components/whois/sensor.py | 18 +++++------ homeassistant/components/whois/strings.json | 31 +++++++++++++++++++ .../whois/snapshots/test_sensor.ambr | 20 ++++++------ 3 files changed, 50 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index c3b115de60ac56..6333139e5400e3 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -64,7 +64,7 @@ def _ensure_timezone(timestamp: datetime | None) -> datetime | None: SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( WhoisSensorEntityDescription( key="admin", - name="Admin", + translation_key="admin", icon="mdi:account-star", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -72,35 +72,35 @@ def _ensure_timezone(timestamp: datetime | None) -> datetime | None: ), WhoisSensorEntityDescription( key="creation_date", - name="Created", + translation_key="creation_date", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda domain: _ensure_timezone(domain.creation_date), ), WhoisSensorEntityDescription( key="days_until_expiration", - name="Days until expiration", + translation_key="days_until_expiration", icon="mdi:calendar-clock", native_unit_of_measurement=UnitOfTime.DAYS, value_fn=_days_until_expiration, ), WhoisSensorEntityDescription( key="expiration_date", - name="Expires", + translation_key="expiration_date", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda domain: _ensure_timezone(domain.expiration_date), ), WhoisSensorEntityDescription( key="last_updated", - name="Last updated", + translation_key="last_updated", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda domain: _ensure_timezone(domain.last_updated), ), WhoisSensorEntityDescription( key="owner", - name="Owner", + translation_key="owner", icon="mdi:account", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -108,7 +108,7 @@ def _ensure_timezone(timestamp: datetime | None) -> datetime | None: ), WhoisSensorEntityDescription( key="registrant", - name="Registrant", + translation_key="registrant", icon="mdi:account-edit", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -116,7 +116,7 @@ def _ensure_timezone(timestamp: datetime | None) -> datetime | None: ), WhoisSensorEntityDescription( key="registrar", - name="Registrar", + translation_key="registrar", icon="mdi:store", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -124,7 +124,7 @@ def _ensure_timezone(timestamp: datetime | None) -> datetime | None: ), WhoisSensorEntityDescription( key="reseller", - name="Reseller", + translation_key="reseller", icon="mdi:store", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, diff --git a/homeassistant/components/whois/strings.json b/homeassistant/components/whois/strings.json index 553293962cde49..c28c079784d98a 100644 --- a/homeassistant/components/whois/strings.json +++ b/homeassistant/components/whois/strings.json @@ -16,5 +16,36 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + }, + "entity": { + "sensor": { + "admin": { + "name": "Admin" + }, + "creation_date": { + "name": "Created" + }, + "days_until_expiration": { + "name": "Days until expiration" + }, + "expiration_date": { + "name": "Expires" + }, + "last_updated": { + "name": "Last updated" + }, + "owner": { + "name": "Owner" + }, + "registrant": { + "name": "Registrant" + }, + "registrar": { + "name": "Registrar" + }, + "reseller": { + "name": "Reseller" + } + } } } diff --git a/tests/components/whois/snapshots/test_sensor.ambr b/tests/components/whois/snapshots/test_sensor.ambr index d0bcff20b0ed86..464af13c7c80f1 100644 --- a/tests/components/whois/snapshots/test_sensor.ambr +++ b/tests/components/whois/snapshots/test_sensor.ambr @@ -37,7 +37,7 @@ 'original_name': 'Admin', 'platform': 'whois', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'admin', 'unique_id': 'home-assistant.io_admin', 'unit_of_measurement': None, }) @@ -107,7 +107,7 @@ 'original_name': 'Created', 'platform': 'whois', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'creation_date', 'unique_id': 'home-assistant.io_creation_date', 'unit_of_measurement': None, }) @@ -182,7 +182,7 @@ 'original_name': 'Days until expiration', 'platform': 'whois', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'days_until_expiration', 'unique_id': 'home-assistant.io_days_until_expiration', 'unit_of_measurement': , }) @@ -252,7 +252,7 @@ 'original_name': 'Expires', 'platform': 'whois', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'expiration_date', 'unique_id': 'home-assistant.io_expiration_date', 'unit_of_measurement': None, }) @@ -322,7 +322,7 @@ 'original_name': 'Last updated', 'platform': 'whois', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'last_updated', 'unique_id': 'home-assistant.io_last_updated', 'unit_of_measurement': None, }) @@ -392,7 +392,7 @@ 'original_name': 'Owner', 'platform': 'whois', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'owner', 'unique_id': 'home-assistant.io_owner', 'unit_of_measurement': None, }) @@ -462,7 +462,7 @@ 'original_name': 'Registrant', 'platform': 'whois', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'registrant', 'unique_id': 'home-assistant.io_registrant', 'unit_of_measurement': None, }) @@ -532,7 +532,7 @@ 'original_name': 'Registrar', 'platform': 'whois', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'registrar', 'unique_id': 'home-assistant.io_registrar', 'unit_of_measurement': None, }) @@ -602,7 +602,7 @@ 'original_name': 'Reseller', 'platform': 'whois', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'reseller', 'unique_id': 'home-assistant.io_reseller', 'unit_of_measurement': None, }) @@ -672,7 +672,7 @@ 'original_name': 'Last updated', 'platform': 'whois', 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'last_updated', 'unique_id': 'home-assistant.io_last_updated', 'unit_of_measurement': None, }) From 67eeed67036248ed4c9fb7dbe4bcb7e90c1dfc11 Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Tue, 18 Jul 2023 15:11:14 +0200 Subject: [PATCH 0617/1009] Rename homekit "Filter Life" sensor to "Filter lifetime" (#96821) * String review: rename 'life' to 'lifetime' - The term life, such as in 'filter life' can be ambiguous. - Renamed to 'lifetime', as quite a few integrations use the term 'lifetime' to express this concept - Improves consistency and should be easier to understand. * HomeKit: adapt test case to reflect string change * Fix test case failure caused by string rename: first step --- homeassistant/components/homekit_controller/sensor.py | 2 +- .../homekit_controller/specific_devices/test_airversa_ap2.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 4d6ad7148d262c..d7230de0832815 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -333,7 +333,7 @@ def thread_status_to_str(char: Characteristic) -> str: ), CharacteristicsTypes.FILTER_LIFE_LEVEL: HomeKitSensorEntityDescription( key=CharacteristicsTypes.FILTER_LIFE_LEVEL, - name="Filter Life", + name="Filter lifetime", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), diff --git a/tests/components/homekit_controller/specific_devices/test_airversa_ap2.py b/tests/components/homekit_controller/specific_devices/test_airversa_ap2.py index d1c9398bb24588..0091fc098ded5a 100644 --- a/tests/components/homekit_controller/specific_devices/test_airversa_ap2.py +++ b/tests/components/homekit_controller/specific_devices/test_airversa_ap2.py @@ -60,8 +60,8 @@ async def test_airversa_ap2_setup(hass: HomeAssistant) -> None: capabilities={"state_class": SensorStateClass.MEASUREMENT}, ), EntityTestInfo( - entity_id="sensor.airversa_ap2_1808_filter_life", - friendly_name="Airversa AP2 1808 Filter Life", + entity_id="sensor.airversa_ap2_1808_filter_lifetime", + friendly_name="Airversa AP2 1808 Filter lifetime", unique_id="00:00:00:00:00:00_1_32896_32900", state="100.0", capabilities={"state_class": SensorStateClass.MEASUREMENT}, From 9a8fe0490749e440577c0211b2e460c72baf0904 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 18 Jul 2023 23:12:43 +1000 Subject: [PATCH 0618/1009] Resolve bugs with Transport NSW (#96692) * 2023.7.16 - Fix bug with values defaulting to "n/a" in stead of None * 2023.7.16 - Set device class and state classes on entities * 2023.7.16 - Set StateClass and DeviceClass directly on the entitiy * 2023.7.16 - Fix black and ruff issues * 2023.7.17 - Update logic catering for the 'n/a' response on an API failure - Add testcase * - Fix bug in formatting * 2023.7.17 - Refacotr to consider the "n/a" response returned from the Python lib on an error or faliure - Remove setting of StateClass and DeviceClass as requested - Add "n/a" test case * 2023.7.17 - Remove unused imports * 2023.7.18 - Apply review requested changes * - Additional review change resolved --- .../components/transport_nsw/sensor.py | 30 ++++++++++------- tests/components/transport_nsw/test_sensor.py | 33 +++++++++++++++++++ 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index 83fa590429f7c5..0a740ec4347a0f 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -6,7 +6,10 @@ from TransportNSW import TransportNSW import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ATTR_MODE, CONF_API_KEY, CONF_NAME, UnitOfTime from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -121,6 +124,11 @@ def update(self) -> None: self._icon = ICONS[self._times[ATTR_MODE]] +def _get_value(value): + """Replace the API response 'n/a' value with None.""" + return None if (value is None or value == "n/a") else value + + class PublicTransportData: """The Class for handling the data retrieval.""" @@ -132,10 +140,10 @@ def __init__(self, stop_id, route, destination, api_key): self._api_key = api_key self.info = { ATTR_ROUTE: self._route, - ATTR_DUE_IN: "n/a", - ATTR_DELAY: "n/a", - ATTR_REAL_TIME: "n/a", - ATTR_DESTINATION: "n/a", + ATTR_DUE_IN: None, + ATTR_DELAY: None, + ATTR_REAL_TIME: None, + ATTR_DESTINATION: None, ATTR_MODE: None, } self.tnsw = TransportNSW() @@ -146,10 +154,10 @@ def update(self): self._stop_id, self._route, self._destination, self._api_key ) self.info = { - ATTR_ROUTE: _data["route"], - ATTR_DUE_IN: _data["due"], - ATTR_DELAY: _data["delay"], - ATTR_REAL_TIME: _data["real_time"], - ATTR_DESTINATION: _data["destination"], - ATTR_MODE: _data["mode"], + ATTR_ROUTE: _get_value(_data["route"]), + ATTR_DUE_IN: _get_value(_data["due"]), + ATTR_DELAY: _get_value(_data["delay"]), + ATTR_REAL_TIME: _get_value(_data["real_time"]), + ATTR_DESTINATION: _get_value(_data["destination"]), + ATTR_MODE: _get_value(_data["mode"]), } diff --git a/tests/components/transport_nsw/test_sensor.py b/tests/components/transport_nsw/test_sensor.py index 181c5fdd1e4952..f9ead2a3054ca8 100644 --- a/tests/components/transport_nsw/test_sensor.py +++ b/tests/components/transport_nsw/test_sensor.py @@ -42,3 +42,36 @@ async def test_transportnsw_config(mocked_get_departures, hass: HomeAssistant) - assert state.attributes["real_time"] == "y" assert state.attributes["destination"] == "Palm Beach" assert state.attributes["mode"] == "Bus" + + +def get_departuresMock_notFound(_stop_id, route, destination, api_key): + """Mock TransportNSW departures loading.""" + data = { + "stop_id": "n/a", + "route": "n/a", + "due": "n/a", + "delay": "n/a", + "real_time": "n/a", + "destination": "n/a", + "mode": "n/a", + } + return data + + +@patch( + "TransportNSW.TransportNSW.get_departures", side_effect=get_departuresMock_notFound +) +async def test_transportnsw_config_not_found( + mocked_get_departures_not_found, hass: HomeAssistant +) -> None: + """Test minimal TransportNSW configuration.""" + assert await async_setup_component(hass, "sensor", VALID_CONFIG) + await hass.async_block_till_done() + state = hass.states.get("sensor.next_bus") + assert state.state == "unknown" + assert state.attributes["stop_id"] == "209516" + assert state.attributes["route"] is None + assert state.attributes["delay"] is None + assert state.attributes["real_time"] is None + assert state.attributes["destination"] is None + assert state.attributes["mode"] is None From 6bd4ace3c3b4ab05e1fdb85c27d55ac09c39d950 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jul 2023 03:39:26 -1000 Subject: [PATCH 0619/1009] Fix ESPHome bluetooth client cancellation when the operation is cancelled externally (#96804) --- .../components/esphome/bluetooth/client.py | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 00b9883f26182f..f7c9da48883d96 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -4,6 +4,7 @@ import asyncio from collections.abc import Callable, Coroutine import contextlib +from functools import partial import logging from typing import Any, TypeVar, cast import uuid @@ -56,38 +57,40 @@ def mac_to_int(address: str) -> int: return int(address.replace(":", ""), 16) +def _on_disconnected(task: asyncio.Task[Any], _: asyncio.Future[None]) -> None: + if task and not task.done(): + task.cancel() + + def verify_connected(func: _WrapFuncType) -> _WrapFuncType: """Define a wrapper throw BleakError if not connected.""" async def _async_wrap_bluetooth_connected_operation( self: ESPHomeClient, *args: Any, **kwargs: Any ) -> Any: - loop = self._loop # pylint: disable=protected-access - disconnected_futures = ( - self._disconnected_futures # pylint: disable=protected-access - ) + # pylint: disable=protected-access + loop = self._loop + disconnected_futures = self._disconnected_futures disconnected_future = loop.create_future() + disconnect_handler = partial(_on_disconnected, asyncio.current_task(loop)) + disconnected_future.add_done_callback(disconnect_handler) disconnected_futures.add(disconnected_future) - - task = asyncio.current_task(loop) - - def _on_disconnected(fut: asyncio.Future[None]) -> None: - if task and not task.done(): - task.cancel() - - disconnected_future.add_done_callback(_on_disconnected) try: return await func(self, *args, **kwargs) except asyncio.CancelledError as ex: - source_name = self._source_name # pylint: disable=protected-access - ble_device = self._ble_device # pylint: disable=protected-access + if not disconnected_future.done(): + # If the disconnected future is not done, the task was cancelled + # externally and we need to raise cancelled error to avoid + # blocking the cancellation. + raise + ble_device = self._ble_device raise BleakError( - f"{source_name}: {ble_device.name} - {ble_device.address}: " + f"{self._source_name }: {ble_device.name} - {ble_device.address}: " "Disconnected during operation" ) from ex finally: disconnected_futures.discard(disconnected_future) - disconnected_future.remove_done_callback(_on_disconnected) + disconnected_future.remove_done_callback(disconnect_handler) return cast(_WrapFuncType, _async_wrap_bluetooth_connected_operation) From d8c989f7326cc65bd6ae8eccf4ab0c28b2a2e844 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 18 Jul 2023 17:36:35 +0200 Subject: [PATCH 0620/1009] Make default theme selectable for set theme service (#96849) --- homeassistant/components/frontend/services.yaml | 1 + homeassistant/helpers/selector.py | 6 +++++- tests/helpers/test_selector.py | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/services.yaml b/homeassistant/components/frontend/services.yaml index 2a562ab348ad2c..0cc88baf32f811 100644 --- a/homeassistant/components/frontend/services.yaml +++ b/homeassistant/components/frontend/services.yaml @@ -11,6 +11,7 @@ set_theme: example: "default" selector: theme: + include_default: true mode: name: Mode description: The mode the theme is for. diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 61ac81b0bcab8c..c7087918cf0aaf 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -1201,7 +1201,11 @@ class ThemeSelector(Selector[ThemeSelectorConfig]): selector_type = "theme" - CONFIG_SCHEMA = vol.Schema({}) + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("include_default", default=False): cv.boolean, + } + ) def __init__(self, config: ThemeSelectorConfig | None = None) -> None: """Instantiate a selector.""" diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 10dc825372f268..c1d5f76ea7872b 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -747,6 +747,11 @@ def test_icon_selector_schema(schema, valid_selections, invalid_selections) -> N ("abc",), (None,), ), + ( + {"include_default": True}, + ("abc",), + (None,), + ), ), ) def test_theme_selector_schema(schema, valid_selections, invalid_selections) -> None: From 1422a4f8c6f4eaada2195c6bd875c6fa5f497253 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 17:41:33 +0200 Subject: [PATCH 0621/1009] Clean up entity descriptions in Tuya (#96847) --- homeassistant/components/tuya/binary_sensor.py | 2 +- homeassistant/components/tuya/number.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 25b2df4147822f..06cb7958242af2 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -53,7 +53,7 @@ class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription): key=DPCode.GAS_SENSOR_STATE, name="Gas", icon="mdi:gas-cylinder", - device_class=BinarySensorDeviceClass.SAFETY, + device_class=BinarySensorDeviceClass.GAS, on_value="alarm", ), TuyaBinarySensorEntityDescription( diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index f4f827980bbd9b..4430172e9a72f3 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -9,7 +9,7 @@ NumberEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory, UnitOfTime +from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -264,14 +264,16 @@ "szjqr": ( NumberEntityDescription( key=DPCode.ARM_DOWN_PERCENT, - name="Move down %", + name="Move down", icon="mdi:arrow-down-bold", + native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.ARM_UP_PERCENT, - name="Move up %", + name="Move up", icon="mdi:arrow-up-bold", + native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( From c989e56d3cffd911e150cdfff3d3ae3658f469e5 Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Tue, 18 Jul 2023 17:50:02 +0200 Subject: [PATCH 0622/1009] Rename life to lifetime: wemo (#96845) --- homeassistant/components/wemo/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wemo/strings.json b/homeassistant/components/wemo/strings.json index 66fa656ebfe6b4..9b112d9a3886e8 100644 --- a/homeassistant/components/wemo/strings.json +++ b/homeassistant/components/wemo/strings.json @@ -41,8 +41,8 @@ } }, "reset_filter_life": { - "name": "Reset filter life", - "description": "Resets the WeMo Humidifier's filter life to 100%." + "name": "Reset filter lifetime", + "description": "Resets the WeMo Humidifier's filter lifetime to 100%." } } } From 4e9ce235e8cfbbfe47c5b4b862df25b533d7251d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 18 Jul 2023 17:50:31 +0200 Subject: [PATCH 0623/1009] Update construct to 2.10.68 (#96843) --- homeassistant/components/eq3btsmart/manifest.json | 2 +- homeassistant/components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index 4d82881e173872..8a976b25c7a5c6 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/eq3btsmart", "iot_class": "local_polling", "loggers": ["bleak", "eq3bt"], - "requirements": ["construct==2.10.56", "python-eq3bt==0.2"] + "requirements": ["construct==2.10.68", "python-eq3bt==0.2"] } diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index d1d703f9875e0f..abda8703e02eca 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", "iot_class": "local_polling", "loggers": ["micloud", "miio"], - "requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.12"], + "requirements": ["construct==2.10.68", "micloud==0.5", "python-miio==0.5.12"], "zeroconf": ["_miio._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 9bf2a461a12591..eaa70a0fd623d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -605,7 +605,7 @@ connect-box==0.2.8 # homeassistant.components.eq3btsmart # homeassistant.components.xiaomi_miio -construct==2.10.56 +construct==2.10.68 # homeassistant.components.utility_meter croniter==1.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1487828b24673c..bd6ceda67a91ac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -488,7 +488,7 @@ colorthief==0.2.1 # homeassistant.components.eq3btsmart # homeassistant.components.xiaomi_miio -construct==2.10.56 +construct==2.10.68 # homeassistant.components.utility_meter croniter==1.0.6 From 701c8a376835963ef0513c22c99f4fd448bb5178 Mon Sep 17 00:00:00 2001 From: Teesit E Date: Tue, 18 Jul 2023 22:51:18 +0700 Subject: [PATCH 0624/1009] Add Tuya Soil sensor (#96819) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/sensor.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 96866b7cd67183..9483443a19c51c 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -1017,6 +1017,22 @@ class TuyaSensorEntityDescription(SensorEntityDescription): state_class=SensorStateClass.MEASUREMENT, ), ), + # Soil sensor (Plant monitor) + "zwjcy": ( + TuyaSensorEntityDescription( + key=DPCode.TEMP_CURRENT, + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.HUMIDITY, + name="Humidity", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + *BATTERY_SENSORS, + ), } # Socket (duplicate of `kg`) From da5455c454eada12c8d887a40be8319fb07c5858 Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Tue, 18 Jul 2023 17:52:40 +0200 Subject: [PATCH 0625/1009] Rename 'life' to 'lifetime' in Brother (#96815) --- homeassistant/components/brother/strings.json | 20 ++++++------ tests/components/brother/test_sensor.py | 32 +++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/brother/strings.json b/homeassistant/components/brother/strings.json index 641b1dbadf3319..e24c941c5142b9 100644 --- a/homeassistant/components/brother/strings.json +++ b/homeassistant/components/brother/strings.json @@ -44,7 +44,7 @@ "name": "Duplex unit page counter" }, "drum_remaining_life": { - "name": "Drum remaining life" + "name": "Drum remaining lifetime" }, "drum_remaining_pages": { "name": "Drum remaining pages" @@ -53,7 +53,7 @@ "name": "Drum page counter" }, "black_drum_remaining_life": { - "name": "Black drum remaining life" + "name": "Black drum remaining lifetime" }, "black_drum_remaining_pages": { "name": "Black drum remaining pages" @@ -62,7 +62,7 @@ "name": "Black drum page counter" }, "cyan_drum_remaining_life": { - "name": "Cyan drum remaining life" + "name": "Cyan drum remaining lifetime" }, "cyan_drum_remaining_pages": { "name": "Cyan drum remaining pages" @@ -71,7 +71,7 @@ "name": "Cyan drum page counter" }, "magenta_drum_remaining_life": { - "name": "Magenta drum remaining life" + "name": "Magenta drum remaining lifetime" }, "magenta_drum_remaining_pages": { "name": "Magenta drum remaining pages" @@ -80,7 +80,7 @@ "name": "Magenta drum page counter" }, "yellow_drum_remaining_life": { - "name": "Yellow drum remaining life" + "name": "Yellow drum remaining lifetime" }, "yellow_drum_remaining_pages": { "name": "Yellow drum remaining pages" @@ -89,19 +89,19 @@ "name": "Yellow drum page counter" }, "belt_unit_remaining_life": { - "name": "Belt unit remaining life" + "name": "Belt unit remaining lifetime" }, "fuser_remaining_life": { - "name": "Fuser remaining life" + "name": "Fuser remaining lifetime" }, "laser_remaining_life": { - "name": "Laser remaining life" + "name": "Laser remaining lifetime" }, "pf_kit_1_remaining_life": { - "name": "PF Kit 1 remaining life" + "name": "PF Kit 1 remaining lifetime" }, "pf_kit_mp_remaining_life": { - "name": "PF Kit MP remaining life" + "name": "PF Kit MP remaining lifetime" }, "black_toner_remaining": { "name": "Black toner remaining" diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index e05fce9df3c37c..42bcb9847f10c5 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -110,14 +110,14 @@ async def test_sensors(hass: HomeAssistant) -> None: assert entry assert entry.unique_id == "0123456789_yellow_toner_remaining" - state = hass.states.get("sensor.hl_l2340dw_drum_remaining_life") + state = hass.states.get("sensor.hl_l2340dw_drum_remaining_lifetime") assert state assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - entry = registry.async_get("sensor.hl_l2340dw_drum_remaining_life") + entry = registry.async_get("sensor.hl_l2340dw_drum_remaining_lifetime") assert entry assert entry.unique_id == "0123456789_drum_remaining_life" @@ -143,14 +143,14 @@ async def test_sensors(hass: HomeAssistant) -> None: assert entry assert entry.unique_id == "0123456789_drum_counter" - state = hass.states.get("sensor.hl_l2340dw_black_drum_remaining_life") + state = hass.states.get("sensor.hl_l2340dw_black_drum_remaining_lifetime") assert state assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - entry = registry.async_get("sensor.hl_l2340dw_black_drum_remaining_life") + entry = registry.async_get("sensor.hl_l2340dw_black_drum_remaining_lifetime") assert entry assert entry.unique_id == "0123456789_black_drum_remaining_life" @@ -176,14 +176,14 @@ async def test_sensors(hass: HomeAssistant) -> None: assert entry assert entry.unique_id == "0123456789_black_drum_counter" - state = hass.states.get("sensor.hl_l2340dw_cyan_drum_remaining_life") + state = hass.states.get("sensor.hl_l2340dw_cyan_drum_remaining_lifetime") assert state assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - entry = registry.async_get("sensor.hl_l2340dw_cyan_drum_remaining_life") + entry = registry.async_get("sensor.hl_l2340dw_cyan_drum_remaining_lifetime") assert entry assert entry.unique_id == "0123456789_cyan_drum_remaining_life" @@ -209,14 +209,14 @@ async def test_sensors(hass: HomeAssistant) -> None: assert entry assert entry.unique_id == "0123456789_cyan_drum_counter" - state = hass.states.get("sensor.hl_l2340dw_magenta_drum_remaining_life") + state = hass.states.get("sensor.hl_l2340dw_magenta_drum_remaining_lifetime") assert state assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - entry = registry.async_get("sensor.hl_l2340dw_magenta_drum_remaining_life") + entry = registry.async_get("sensor.hl_l2340dw_magenta_drum_remaining_lifetime") assert entry assert entry.unique_id == "0123456789_magenta_drum_remaining_life" @@ -242,14 +242,14 @@ async def test_sensors(hass: HomeAssistant) -> None: assert entry assert entry.unique_id == "0123456789_magenta_drum_counter" - state = hass.states.get("sensor.hl_l2340dw_yellow_drum_remaining_life") + state = hass.states.get("sensor.hl_l2340dw_yellow_drum_remaining_lifetime") assert state assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - entry = registry.async_get("sensor.hl_l2340dw_yellow_drum_remaining_life") + entry = registry.async_get("sensor.hl_l2340dw_yellow_drum_remaining_lifetime") assert entry assert entry.unique_id == "0123456789_yellow_drum_remaining_life" @@ -275,36 +275,36 @@ async def test_sensors(hass: HomeAssistant) -> None: assert entry assert entry.unique_id == "0123456789_yellow_drum_counter" - state = hass.states.get("sensor.hl_l2340dw_fuser_remaining_life") + state = hass.states.get("sensor.hl_l2340dw_fuser_remaining_lifetime") assert state assert state.attributes.get(ATTR_ICON) == "mdi:water-outline" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "97" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - entry = registry.async_get("sensor.hl_l2340dw_fuser_remaining_life") + entry = registry.async_get("sensor.hl_l2340dw_fuser_remaining_lifetime") assert entry assert entry.unique_id == "0123456789_fuser_remaining_life" - state = hass.states.get("sensor.hl_l2340dw_belt_unit_remaining_life") + state = hass.states.get("sensor.hl_l2340dw_belt_unit_remaining_lifetime") assert state assert state.attributes.get(ATTR_ICON) == "mdi:current-ac" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "97" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - entry = registry.async_get("sensor.hl_l2340dw_belt_unit_remaining_life") + entry = registry.async_get("sensor.hl_l2340dw_belt_unit_remaining_lifetime") assert entry assert entry.unique_id == "0123456789_belt_unit_remaining_life" - state = hass.states.get("sensor.hl_l2340dw_pf_kit_1_remaining_life") + state = hass.states.get("sensor.hl_l2340dw_pf_kit_1_remaining_lifetime") assert state assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "98" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - entry = registry.async_get("sensor.hl_l2340dw_pf_kit_1_remaining_life") + entry = registry.async_get("sensor.hl_l2340dw_pf_kit_1_remaining_lifetime") assert entry assert entry.unique_id == "0123456789_pf_kit_1_remaining_life" From cb1f365482b1e8b69d42e30b84f8cfb28411d0c3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 18:07:32 +0200 Subject: [PATCH 0626/1009] Add entity translations to NextCloud (#96544) Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- homeassistant/components/nextcloud/entity.py | 5 +- .../components/nextcloud/strings.json | 147 ++++++++++++++++++ 2 files changed, 149 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nextcloud/entity.py b/homeassistant/components/nextcloud/entity.py index ed5882cfe74908..4308e5738599bc 100644 --- a/homeassistant/components/nextcloud/entity.py +++ b/homeassistant/components/nextcloud/entity.py @@ -1,9 +1,8 @@ """Base entity for the Nextcloud integration.""" - - from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import slugify from .const import DOMAIN from .coordinator import NextcloudDataUpdateCoordinator @@ -21,7 +20,7 @@ def __init__( """Initialize the Nextcloud sensor.""" super().__init__(coordinator) self.item = item - self._attr_name = item + self._attr_translation_key = slugify(item) self._attr_unique_id = f"{coordinator.url}#{item}" self._attr_device_info = DeviceInfo( name="Nextcloud", diff --git a/homeassistant/components/nextcloud/strings.json b/homeassistant/components/nextcloud/strings.json index bcb530ffd734b3..6c70421bf93e49 100644 --- a/homeassistant/components/nextcloud/strings.json +++ b/homeassistant/components/nextcloud/strings.json @@ -28,5 +28,152 @@ "connection_error": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" } + }, + "entity": { + "binary_sensor": { + "nextcloud_system_enable_avatars": { + "name": "Avatars enabled" + }, + "nextcloud_system_enable_previews": { + "name": "Previews enabled" + }, + "nextcloud_system_filelocking_enabled": { + "name": "Filelocking enabled" + }, + "nextcloud_system_debug": { + "name": "Debug enabled" + } + }, + "sensor": { + "nextcloud_system_version": { + "name": "System version" + }, + "nextcloud_system_theme": { + "name": "System theme" + }, + "nextcloud_system_memcache_local": { + "name": "System memcache local" + }, + "nextcloud_system_memcache_distributed": { + "name": "System memcache distributed" + }, + "nextcloud_system_memcache_locking": { + "name": "System memcache locking" + }, + "nextcloud_system_freespace": { + "name": "Free space" + }, + "nextcloud_system_cpuload": { + "name": "CPU Load" + }, + "nextcloud_system_mem_total": { + "name": "Total memory" + }, + "nextcloud_system_mem_free": { + "name": "Free memory" + }, + "nextcloud_system_swap_total": { + "name": "Total swap memory" + }, + "nextcloud_system_swap_free": { + "name": "Free swap memory" + }, + "nextcloud_system_apps_num_installed": { + "name": "Apps installed" + }, + "nextcloud_system_apps_num_updates_available": { + "name": "Updates available" + }, + "nextcloud_system_apps_app_updates_calendar": { + "name": "Calendar updates" + }, + "nextcloud_system_apps_app_updates_contacts": { + "name": "Contact updates" + }, + "nextcloud_system_apps_app_updates_tasks": { + "name": "Task updates" + }, + "nextcloud_system_apps_app_updates_twofactor_totp": { + "name": "Two factor authentication updates" + }, + "nextcloud_storage_num_users": { + "name": "Amount of user" + }, + "nextcloud_storage_num_files": { + "name": "Amount of files" + }, + "nextcloud_storage_num_storages": { + "name": "Amount of storages" + }, + "nextcloud_storage_num_storages_local": { + "name": "Amount of local storages" + }, + "nextcloud_storage_num_storages_home": { + "name": "Amount of storages at home" + }, + "nextcloud_storage_num_storages_other": { + "name": "Amount of other storages" + }, + "nextcloud_shares_num_shares": { + "name": "Amount of shares" + }, + "nextcloud_shares_num_shares_user": { + "name": "Amount of user shares" + }, + "nextcloud_shares_num_shares_groups": { + "name": "Amount of group shares" + }, + "nextcloud_shares_num_shares_link": { + "name": "Amount of link shares" + }, + "nextcloud_shares_num_shares_mail": { + "name": "Amount of mail shares" + }, + "nextcloud_shares_num_shares_room": { + "name": "Amount of room shares" + }, + "nextcloud_shares_num_shares_link_no_password": { + "name": "Amount of passwordless link shares" + }, + "nextcloud_shares_num_fed_shares_sent": { + "name": "Amount of shares sent" + }, + "nextcloud_shares_num_fed_shares_received": { + "name": "Amount of shares received" + }, + "nextcloud_shares_permissions_3_1": { + "name": "Permissions 3.1" + }, + "nextcloud_server_webserver": { + "name": "Webserver" + }, + "nextcloud_server_php_version": { + "name": "PHP version" + }, + "nextcloud_server_php_memory_limit": { + "name": "PHP memory limit" + }, + "nextcloud_server_php_max_execution_time": { + "name": "PHP max execution time" + }, + "nextcloud_server_php_upload_max_filesize": { + "name": "PHP upload maximum filesize" + }, + "nextcloud_database_type": { + "name": "Database type" + }, + "nextcloud_database_version": { + "name": "Database version" + }, + "nextcloud_activeusers_last5minutes": { + "name": "Amount of active users last 5 minutes" + }, + "nextcloud_activeusers_last1hour": { + "name": "Amount of active users last hour" + }, + "nextcloud_activeusers_last24hours": { + "name": "Amount of active users last day" + } + } } } From 0ca4da559238ae12d4b26a922451bc2c4864ea93 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 18:51:02 +0200 Subject: [PATCH 0627/1009] Use device class for DLink (#96567) --- homeassistant/components/dlink/switch.py | 3 ++- tests/components/dlink/test_switch.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index d06372bb28b340..0814945bc071b6 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -17,7 +17,6 @@ SWITCH_TYPE = SwitchEntityDescription( key="switch", - name="Switch", ) @@ -34,6 +33,8 @@ async def async_setup_entry( class SmartPlugSwitch(DLinkEntity, SwitchEntity): """Representation of a D-Link Smart Plug switch.""" + _attr_name = None + @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the device.""" diff --git a/tests/components/dlink/test_switch.py b/tests/components/dlink/test_switch.py index 24316006b5e9c9..845e8dfe85a16a 100644 --- a/tests/components/dlink/test_switch.py +++ b/tests/components/dlink/test_switch.py @@ -28,7 +28,7 @@ async def test_switch_state(hass: HomeAssistant, mocked_plug: AsyncMock) -> None await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - entity_id = "switch.mock_title_switch" + entity_id = "switch.mock_title" state = hass.states.get(entity_id) assert state.state == STATE_OFF assert state.attributes["total_consumption"] == 1040.0 @@ -62,7 +62,7 @@ async def test_switch_no_value( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("switch.mock_title_switch") + state = hass.states.get("switch.mock_title") assert state.state == STATE_OFF assert state.attributes["total_consumption"] is None assert state.attributes["temperature"] is None From ac06905b1c96587c57728086db50c665ffc9912c Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Tue, 18 Jul 2023 20:36:47 +0200 Subject: [PATCH 0628/1009] Rename life to lifetime in vesync (#96844) --- homeassistant/components/vesync/strings.json | 2 +- .../vesync/snapshots/test_diagnostics.ambr | 8 ++-- .../vesync/snapshots/test_sensor.ambr | 40 +++++++++---------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/vesync/strings.json b/homeassistant/components/vesync/strings.json index 9a54062a2b518e..5ff0aa58722f71 100644 --- a/homeassistant/components/vesync/strings.json +++ b/homeassistant/components/vesync/strings.json @@ -19,7 +19,7 @@ "entity": { "sensor": { "filter_life": { - "name": "Filter life" + "name": "Filter lifetime" }, "air_quality": { "name": "Air quality" diff --git a/tests/components/vesync/snapshots/test_diagnostics.ambr b/tests/components/vesync/snapshots/test_diagnostics.ambr index b9426f5ba1eefa..c463db179eb667 100644 --- a/tests/components/vesync/snapshots/test_diagnostics.ambr +++ b/tests/components/vesync/snapshots/test_diagnostics.ambr @@ -238,19 +238,19 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': 'diagnostic', - 'entity_id': 'sensor.fan_filter_life', + 'entity_id': 'sensor.fan_filter_lifetime', 'icon': None, 'name': None, 'original_device_class': None, 'original_icon': None, - 'original_name': 'Filter life', + 'original_name': 'Filter lifetime', 'state': dict({ 'attributes': dict({ - 'friendly_name': 'Fan Filter life', + 'friendly_name': 'Fan Filter lifetime', 'state_class': 'measurement', 'unit_of_measurement': '%', }), - 'entity_id': 'sensor.fan_filter_life', + 'entity_id': 'sensor.fan_filter_lifetime', 'last_changed': str, 'last_updated': str, 'state': 'unavailable', diff --git a/tests/components/vesync/snapshots/test_sensor.ambr b/tests/components/vesync/snapshots/test_sensor.ambr index 1fc897226997f1..06198bca145078 100644 --- a/tests/components/vesync/snapshots/test_sensor.ambr +++ b/tests/components/vesync/snapshots/test_sensor.ambr @@ -43,7 +43,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.air_purifier_131s_filter_life', + 'entity_id': 'sensor.air_purifier_131s_filter_lifetime', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -53,7 +53,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Filter life', + 'original_name': 'Filter lifetime', 'platform': 'vesync', 'supported_features': 0, 'translation_key': 'filter_life', @@ -102,15 +102,15 @@ 'state': 'unavailable', }) # --- -# name: test_sensor_state[Air Purifier 131s][sensor.air_purifier_131s_filter_life] +# name: test_sensor_state[Air Purifier 131s][sensor.air_purifier_131s_filter_lifetime] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Air Purifier 131s Filter life', + 'friendly_name': 'Air Purifier 131s Filter lifetime', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.air_purifier_131s_filter_life', + 'entity_id': 'sensor.air_purifier_131s_filter_lifetime', 'last_changed': , 'last_updated': , 'state': 'unavailable', @@ -160,7 +160,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.air_purifier_200s_filter_life', + 'entity_id': 'sensor.air_purifier_200s_filter_lifetime', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -170,7 +170,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Filter life', + 'original_name': 'Filter lifetime', 'platform': 'vesync', 'supported_features': 0, 'translation_key': 'filter_life', @@ -179,15 +179,15 @@ }), ]) # --- -# name: test_sensor_state[Air Purifier 200s][sensor.air_purifier_200s_filter_life] +# name: test_sensor_state[Air Purifier 200s][sensor.air_purifier_200s_filter_lifetime] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Air Purifier 200s Filter life', + 'friendly_name': 'Air Purifier 200s Filter lifetime', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.air_purifier_200s_filter_life', + 'entity_id': 'sensor.air_purifier_200s_filter_lifetime', 'last_changed': , 'last_updated': , 'state': '99', @@ -237,7 +237,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.air_purifier_400s_filter_life', + 'entity_id': 'sensor.air_purifier_400s_filter_lifetime', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -247,7 +247,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Filter life', + 'original_name': 'Filter lifetime', 'platform': 'vesync', 'supported_features': 0, 'translation_key': 'filter_life', @@ -326,15 +326,15 @@ 'state': '5', }) # --- -# name: test_sensor_state[Air Purifier 400s][sensor.air_purifier_400s_filter_life] +# name: test_sensor_state[Air Purifier 400s][sensor.air_purifier_400s_filter_lifetime] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Air Purifier 400s Filter life', + 'friendly_name': 'Air Purifier 400s Filter lifetime', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.air_purifier_400s_filter_life', + 'entity_id': 'sensor.air_purifier_400s_filter_lifetime', 'last_changed': , 'last_updated': , 'state': '99', @@ -399,7 +399,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.air_purifier_600s_filter_life', + 'entity_id': 'sensor.air_purifier_600s_filter_lifetime', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -409,7 +409,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Filter life', + 'original_name': 'Filter lifetime', 'platform': 'vesync', 'supported_features': 0, 'translation_key': 'filter_life', @@ -488,15 +488,15 @@ 'state': '5', }) # --- -# name: test_sensor_state[Air Purifier 600s][sensor.air_purifier_600s_filter_life] +# name: test_sensor_state[Air Purifier 600s][sensor.air_purifier_600s_filter_lifetime] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Air Purifier 600s Filter life', + 'friendly_name': 'Air Purifier 600s Filter lifetime', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.air_purifier_600s_filter_life', + 'entity_id': 'sensor.air_purifier_600s_filter_lifetime', 'last_changed': , 'last_updated': , 'state': '99', From 6afa49a441de6ce4fc8871fdf9406d853bd2035b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 20:39:37 +0200 Subject: [PATCH 0629/1009] Migrate Crownstone to has entity name (#96566) --- homeassistant/components/crownstone/devices.py | 1 + homeassistant/components/crownstone/light.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/crownstone/devices.py b/homeassistant/components/crownstone/devices.py index 427f88a1fb97f1..83aaac95393249 100644 --- a/homeassistant/components/crownstone/devices.py +++ b/homeassistant/components/crownstone/devices.py @@ -12,6 +12,7 @@ class CrownstoneBaseEntity(Entity): """Base entity class for Crownstone devices.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__(self, device: Crownstone) -> None: """Initialize the device.""" diff --git a/homeassistant/components/crownstone/light.py b/homeassistant/components/crownstone/light.py index c9cbeff90d5a0f..a140de59017494 100644 --- a/homeassistant/components/crownstone/light.py +++ b/homeassistant/components/crownstone/light.py @@ -71,6 +71,7 @@ class CrownstoneEntity(CrownstoneBaseEntity, LightEntity): """ _attr_icon = "mdi:power-socket-de" + _attr_name = None def __init__( self, crownstone_data: Crownstone, usb: CrownstoneUart | None = None @@ -79,7 +80,6 @@ def __init__( super().__init__(crownstone_data) self.usb = usb # Entity class attributes - self._attr_name = str(self.device.name) self._attr_unique_id = f"{self.cloud_id}-{CROWNSTONE_SUFFIX}" @property From 344f349371d593652fef0fcdfc47932be7b3501b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 20:41:14 +0200 Subject: [PATCH 0630/1009] Migrate Agent DVR to has entity name (#96562) --- homeassistant/components/agent_dvr/alarm_control_panel.py | 4 +++- homeassistant/components/agent_dvr/camera.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/agent_dvr/alarm_control_panel.py b/homeassistant/components/agent_dvr/alarm_control_panel.py index 632b2e29d5742c..dc8038862c6ac4 100644 --- a/homeassistant/components/agent_dvr/alarm_control_panel.py +++ b/homeassistant/components/agent_dvr/alarm_control_panel.py @@ -47,14 +47,16 @@ class AgentBaseStation(AlarmControlPanelEntity): | AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_NIGHT ) + _attr_has_entity_name = True + _attr_name = None def __init__(self, client): """Initialize the alarm control panel.""" self._client = client - self._attr_name = f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}" self._attr_unique_id = f"{client.unique}_CP" self._attr_device_info = DeviceInfo( identifiers={(AGENT_DOMAIN, client.unique)}, + name=f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}", manufacturer="Agent", model=CONST_ALARM_CONTROL_PANEL_NAME, sw_version=client.version, diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index e485940034fcbc..d49a1ac387e6e3 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -72,12 +72,13 @@ class AgentCamera(MjpegCamera): _attr_attribution = ATTRIBUTION _attr_should_poll = True # Cameras default to False _attr_supported_features = CameraEntityFeature.ON_OFF + _attr_has_entity_name = True + _attr_name = None def __init__(self, device): """Initialize as a subclass of MjpegCamera.""" self.device = device self._removed = False - self._attr_name = f"{device.client.name} {device.name}" self._attr_unique_id = f"{device._client.unique}_{device.typeID}_{device.id}" super().__init__( name=device.name, @@ -88,7 +89,7 @@ def __init__(self, device): identifiers={(AGENT_DOMAIN, self.unique_id)}, manufacturer="Agent", model="Camera", - name=self.name, + name=f"{device.client.name} {device.name}", sw_version=device.client.version, ) From 499c7491af9abf3f4dcbb908ba1e73965f17bc92 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 18 Jul 2023 20:48:15 +0200 Subject: [PATCH 0631/1009] Plugwise prepare native_value_fn and companions for number (#93416) Co-authored-by: Franck Nijhof Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> Co-authored-by: Bouwe Co-authored-by: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> --- homeassistant/components/plugwise/number.py | 58 +++++++++------------ 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index 5a3e394b11967a..25667ea16c6931 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass -from plugwise import Smile +from plugwise import ActuatorData, Smile from homeassistant.components.number import ( NumberDeviceClass, @@ -24,13 +24,13 @@ @dataclass class PlugwiseEntityDescriptionMixin: - """Mixin values for Plugwse entities.""" + """Mixin values for Plugwise entities.""" command: Callable[[Smile, str, float], Awaitable[None]] - native_max_value_key: str - native_min_value_key: str - native_step_key: str - native_value_key: str + native_max_value_fn: Callable[[ActuatorData], float] + native_min_value_fn: Callable[[ActuatorData], float] + native_step_fn: Callable[[ActuatorData], float] + native_value_fn: Callable[[ActuatorData], float] @dataclass @@ -47,11 +47,11 @@ class PlugwiseNumberEntityDescription( command=lambda api, number, value: api.set_number_setpoint(number, value), device_class=NumberDeviceClass.TEMPERATURE, entity_category=EntityCategory.CONFIG, - native_max_value_key="upper_bound", - native_min_value_key="lower_bound", - native_step_key="resolution", native_unit_of_measurement=UnitOfTemperature.CELSIUS, - native_value_key="setpoint", + native_max_value_fn=lambda data: data["upper_bound"], + native_min_value_fn=lambda data: data["lower_bound"], + native_step_fn=lambda data: data["resolution"], + native_value_fn=lambda data: data["setpoint"], ), ) @@ -70,7 +70,7 @@ async def async_setup_entry( entities: list[PlugwiseNumberEntity] = [] for device_id, device in coordinator.data.devices.items(): for description in NUMBER_TYPES: - if description.key in device and "setpoint" in device[description.key]: + if (actuator := device.get(description.key)) and "setpoint" in actuator: entities.append( PlugwiseNumberEntity(coordinator, device_id, description) ) @@ -91,40 +91,30 @@ def __init__( ) -> None: """Initiate Plugwise Number.""" super().__init__(coordinator, device_id) + self.actuator = self.device[description.key] self.entity_description = description self._attr_unique_id = f"{device_id}-{description.key}" self._attr_mode = NumberMode.BOX @property - def native_step(self) -> float: - """Return the setpoint step value.""" - return max( - self.device[self.entity_description.key][ - self.entity_description.native_step_key - ], - 1, - ) - - @property - def native_value(self) -> float: - """Return the present setpoint value.""" - return self.device[self.entity_description.key][ - self.entity_description.native_value_key - ] + def native_max_value(self) -> float: + """Return the setpoint max. value.""" + return self.entity_description.native_max_value_fn(self.actuator) @property def native_min_value(self) -> float: """Return the setpoint min. value.""" - return self.device[self.entity_description.key][ - self.entity_description.native_min_value_key - ] + return self.entity_description.native_min_value_fn(self.actuator) @property - def native_max_value(self) -> float: - """Return the setpoint max. value.""" - return self.device[self.entity_description.key][ - self.entity_description.native_max_value_key - ] + def native_step(self) -> float: + """Return the setpoint step value.""" + return max(self.entity_description.native_step_fn(self.actuator), 1) + + @property + def native_value(self) -> float: + """Return the present setpoint value.""" + return self.entity_description.native_value_fn(self.actuator) async def async_set_native_value(self, value: float) -> None: """Change to the new setpoint value.""" From 0ff8371953e29a1584fc5a7ab9db540f1c8a5d04 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 20:52:43 +0200 Subject: [PATCH 0632/1009] Migrate Ambiclimate to use has entity name (#96561) --- homeassistant/components/ambiclimate/climate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index 516ed319d0152e..cf8b40916f3eb8 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -154,17 +154,18 @@ class AmbiclimateEntity(ClimateEntity): _attr_target_temperature_step = 1 _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] + _attr_has_entity_name = True + _attr_name = None def __init__(self, heater, store): """Initialize the thermostat.""" self._heater = heater self._store = store self._attr_unique_id = heater.device_id - self._attr_name = heater.name self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.unique_id)}, manufacturer="Ambiclimate", - name=self.name, + name=heater.name, ) async def async_set_temperature(self, **kwargs: Any) -> None: From 1ceb536dfb9e5e3122c7e0a3d82f420b6a4ec4a2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 20:53:37 +0200 Subject: [PATCH 0633/1009] Migrate MyStrom to has entity name (#96540) --- homeassistant/components/mystrom/light.py | 3 ++- homeassistant/components/mystrom/switch.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mystrom/light.py b/homeassistant/components/mystrom/light.py index 8c4998fa45e2c6..d32a64dc1e6337 100644 --- a/homeassistant/components/mystrom/light.py +++ b/homeassistant/components/mystrom/light.py @@ -83,6 +83,8 @@ async def async_setup_platform( class MyStromLight(LightEntity): """Representation of the myStrom WiFi bulb.""" + _attr_has_entity_name = True + _attr_name = None _attr_color_mode = ColorMode.HS _attr_supported_color_modes = {ColorMode.HS} _attr_supported_features = LightEntityFeature.EFFECT | LightEntityFeature.FLASH @@ -91,7 +93,6 @@ class MyStromLight(LightEntity): def __init__(self, bulb, name, mac): """Initialize the light.""" self._bulb = bulb - self._attr_name = name self._attr_available = False self._attr_unique_id = mac self._attr_hs_color = 0, 0 diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index 7180862758cb23..54c1dc9ad5aa30 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -70,10 +70,12 @@ async def async_setup_platform( class MyStromSwitch(SwitchEntity): """Representation of a myStrom switch/plug.""" + _attr_has_entity_name = True + _attr_name = None + def __init__(self, plug, name): """Initialize the myStrom switch/plug.""" self.plug = plug - self._attr_name = name self._attr_unique_id = self.plug.mac self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.plug.mac)}, From 8675bc6554bf0248c3cd88ab42417c0ddbb83d6b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 20:56:50 +0200 Subject: [PATCH 0634/1009] Migrate Tradfri to has entity name (#96248) --- homeassistant/components/tradfri/base_class.py | 3 ++- homeassistant/components/tradfri/cover.py | 2 ++ homeassistant/components/tradfri/fan.py | 1 + homeassistant/components/tradfri/light.py | 1 + homeassistant/components/tradfri/sensor.py | 8 ++------ homeassistant/components/tradfri/strings.json | 10 ++++++++++ homeassistant/components/tradfri/switch.py | 2 ++ tests/components/tradfri/test_sensor.py | 4 ++-- 8 files changed, 22 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index 5a84ad5719c7e5..c7154c19f158c4 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -37,6 +37,8 @@ async def wrapper(command: Command | list[Command]) -> None: class TradfriBaseEntity(CoordinatorEntity[TradfriDeviceDataUpdateCoordinator]): """Base Tradfri device.""" + _attr_has_entity_name = True + def __init__( self, device_coordinator: TradfriDeviceDataUpdateCoordinator, @@ -52,7 +54,6 @@ def __init__( self._device_id = self._device.id self._api = handle_error(api) - self._attr_name = self._device.name self._attr_unique_id = f"{self._gateway_id}-{self._device.id}" diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py index 976a48906fc3b5..c51918b4a4f320 100644 --- a/homeassistant/components/tradfri/cover.py +++ b/homeassistant/components/tradfri/cover.py @@ -40,6 +40,8 @@ async def async_setup_entry( class TradfriCover(TradfriBaseEntity, CoverEntity): """The platform class required by Home Assistant.""" + _attr_name = None + def __init__( self, device_coordinator: TradfriDeviceDataUpdateCoordinator, diff --git a/homeassistant/components/tradfri/fan.py b/homeassistant/components/tradfri/fan.py index d6bb91a49791cc..a26dfa1d9a099c 100644 --- a/homeassistant/components/tradfri/fan.py +++ b/homeassistant/components/tradfri/fan.py @@ -54,6 +54,7 @@ async def async_setup_entry( class TradfriAirPurifierFan(TradfriBaseEntity, FanEntity): """The platform class required by Home Assistant.""" + _attr_name = None _attr_supported_features = FanEntityFeature.PRESET_MODE | FanEntityFeature.SET_SPEED def __init__( diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 32160c6a130a96..df35301b3736c0 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -49,6 +49,7 @@ async def async_setup_entry( class TradfriLight(TradfriBaseEntity, LightEntity): """The platform class required by Home Assistant.""" + _attr_name = None _attr_supported_features = LightEntityFeature.TRANSITION def __init__( diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 3eb4d72effd252..383eec8a8fbb51 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -24,7 +24,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import UNDEFINED from .base_class import TradfriBaseEntity from .const import ( @@ -89,7 +88,7 @@ def _get_filter_time_left(device: Device) -> int: SENSOR_DESCRIPTIONS_FAN: tuple[TradfriSensorEntityDescription, ...] = ( TradfriSensorEntityDescription( key="aqi", - name="air quality", + translation_key="aqi", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, icon="mdi:air-filter", @@ -97,7 +96,7 @@ def _get_filter_time_left(device: Device) -> int: ), TradfriSensorEntityDescription( key="filter_life_remaining", - name="filter time left", + translation_key="filter_life_remaining", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTime.HOURS, icon="mdi:clock-outline", @@ -203,9 +202,6 @@ def __init__( self._attr_unique_id = f"{self._attr_unique_id}-{description.key}" - if description.name is not UNDEFINED: - self._attr_name = f"{self._attr_name}: {description.name}" - self._refresh() # Set initial state def _refresh(self) -> None: diff --git a/homeassistant/components/tradfri/strings.json b/homeassistant/components/tradfri/strings.json index 34d7e89929af8b..0a9a86bd23a209 100644 --- a/homeassistant/components/tradfri/strings.json +++ b/homeassistant/components/tradfri/strings.json @@ -20,5 +20,15 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" } + }, + "entity": { + "sensor": { + "aqi": { + "name": "Air quality" + }, + "filter_life_remaining": { + "name": "Filter time left" + } + } } } diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index e0e2467ca4bf37..2f6f19961579a4 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -40,6 +40,8 @@ async def async_setup_entry( class TradfriSwitch(TradfriBaseEntity, SwitchEntity): """The platform class required by Home Assistant.""" + _attr_name = None + def __init__( self, device_coordinator: TradfriDeviceDataUpdateCoordinator, diff --git a/tests/components/tradfri/test_sensor.py b/tests/components/tradfri/test_sensor.py index 23391c8e875868..d301638ec5dc37 100644 --- a/tests/components/tradfri/test_sensor.py +++ b/tests/components/tradfri/test_sensor.py @@ -61,7 +61,7 @@ async def test_battery_sensor( remote_control: Device, ) -> None: """Test that a battery sensor is correctly added.""" - entity_id = "sensor.test" + entity_id = "sensor.test_battery" device = remote_control mock_gateway.mock_devices.append(device) await setup_integration(hass) @@ -92,7 +92,7 @@ async def test_cover_battery_sensor( blind: Blind, ) -> None: """Test that a battery sensor is correctly added for a cover (blind).""" - entity_id = "sensor.test" + entity_id = "sensor.test_battery" device = blind.device mock_gateway.mock_devices.append(device) await setup_integration(hass) From c94c7fae1b10a7f7759182fd17b6922d0a28651c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 20:57:41 +0200 Subject: [PATCH 0635/1009] Add device info to ISS (#96469) Co-authored-by: Franck Nijhof --- homeassistant/components/iss/config_flow.py | 4 +--- homeassistant/components/iss/const.py | 2 ++ homeassistant/components/iss/sensor.py | 23 +++++++++++++++------ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/iss/config_flow.py b/homeassistant/components/iss/config_flow.py index 2beffc7c8944f3..f8ebd9db723fad 100644 --- a/homeassistant/components/iss/config_flow.py +++ b/homeassistant/components/iss/config_flow.py @@ -7,9 +7,7 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from .const import DOMAIN - -DEFAULT_NAME = "ISS" +from .const import DEFAULT_NAME, DOMAIN class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/iss/const.py b/homeassistant/components/iss/const.py index 3d240041b67f83..c3bdcf6fa327e6 100644 --- a/homeassistant/components/iss/const.py +++ b/homeassistant/components/iss/const.py @@ -1,3 +1,5 @@ """Constants for iss.""" DOMAIN = "iss" + +DEFAULT_NAME = "ISS" diff --git a/homeassistant/components/iss/sensor.py b/homeassistant/components/iss/sensor.py index fac23dfd9fabc7..32516ee99c9111 100644 --- a/homeassistant/components/iss/sensor.py +++ b/homeassistant/components/iss/sensor.py @@ -8,6 +8,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -15,7 +17,7 @@ ) from . import IssData -from .const import DOMAIN +from .const import DEFAULT_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -28,23 +30,32 @@ async def async_setup_entry( """Set up the sensor platform.""" coordinator: DataUpdateCoordinator[IssData] = hass.data[DOMAIN] - name = entry.title show_on_map = entry.options.get(CONF_SHOW_ON_MAP, False) - async_add_entities([IssSensor(coordinator, name, show_on_map)]) + async_add_entities([IssSensor(coordinator, entry, show_on_map)]) class IssSensor(CoordinatorEntity[DataUpdateCoordinator[IssData]], SensorEntity): """Implementation of the ISS sensor.""" + _attr_has_entity_name = True + _attr_name = None + def __init__( - self, coordinator: DataUpdateCoordinator[IssData], name: str, show: bool + self, + coordinator: DataUpdateCoordinator[IssData], + entry: ConfigEntry, + show: bool, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._state = None - self._attr_name = name + self._attr_unique_id = f"{entry.entry_id}_people" self._show_on_map = show + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry.entry_id)}, + name=DEFAULT_NAME, + entry_type=DeviceEntryType.SERVICE, + ) @property def native_value(self) -> int: From e29598ecaa581e789987d855781fd9f6daef2baf Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 21:07:45 +0200 Subject: [PATCH 0636/1009] Add entity translations to Vallox (#96495) --- homeassistant/components/vallox/__init__.py | 2 + .../components/vallox/binary_sensor.py | 3 +- homeassistant/components/vallox/fan.py | 1 - homeassistant/components/vallox/number.py | 7 +- homeassistant/components/vallox/sensor.py | 29 ++++----- homeassistant/components/vallox/strings.json | 64 +++++++++++++++++++ homeassistant/components/vallox/switch.py | 3 +- 7 files changed, 84 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index 6f8d00eb48c2e7..473b9fa07d1229 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -304,6 +304,8 @@ async def async_handle(self, call: ServiceCall) -> None: class ValloxEntity(CoordinatorEntity[ValloxDataUpdateCoordinator]): """Representation of a Vallox entity.""" + _attr_has_entity_name = True + def __init__(self, name: str, coordinator: ValloxDataUpdateCoordinator) -> None: """Initialize a Vallox entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/vallox/binary_sensor.py b/homeassistant/components/vallox/binary_sensor.py index 2d40c43836d070..05085c24424572 100644 --- a/homeassistant/components/vallox/binary_sensor.py +++ b/homeassistant/components/vallox/binary_sensor.py @@ -21,7 +21,6 @@ class ValloxBinarySensorEntity(ValloxEntity, BinarySensorEntity): entity_description: ValloxBinarySensorEntityDescription _attr_entity_category = EntityCategory.DIAGNOSTIC - _attr_has_entity_name = True def __init__( self, @@ -59,7 +58,7 @@ class ValloxBinarySensorEntityDescription( BINARY_SENSOR_ENTITIES: tuple[ValloxBinarySensorEntityDescription, ...] = ( ValloxBinarySensorEntityDescription( key="post_heater", - name="Post heater", + translation_key="post_heater", icon="mdi:radiator", metric_key="A_CYC_IO_HEATER", ), diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index b43dabbba80858..2f420096c74800 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -83,7 +83,6 @@ async def async_setup_entry( 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 diff --git a/homeassistant/components/vallox/number.py b/homeassistant/components/vallox/number.py index 36145f85bc756a..ce43ca9c3fb8b7 100644 --- a/homeassistant/components/vallox/number.py +++ b/homeassistant/components/vallox/number.py @@ -23,7 +23,6 @@ class ValloxNumberEntity(ValloxEntity, NumberEntity): """Representation of a Vallox number entity.""" entity_description: ValloxNumberEntityDescription - _attr_has_entity_name = True _attr_entity_category = EntityCategory.CONFIG def __init__( @@ -76,7 +75,7 @@ class ValloxNumberEntityDescription(NumberEntityDescription, ValloxMetricMixin): NUMBER_ENTITIES: tuple[ValloxNumberEntityDescription, ...] = ( ValloxNumberEntityDescription( key="supply_air_target_home", - name="Supply air temperature (Home)", + translation_key="supply_air_target_home", metric_key="A_CYC_HOME_AIR_TEMP_TARGET", device_class=NumberDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -87,7 +86,7 @@ class ValloxNumberEntityDescription(NumberEntityDescription, ValloxMetricMixin): ), ValloxNumberEntityDescription( key="supply_air_target_away", - name="Supply air temperature (Away)", + translation_key="supply_air_target_away", metric_key="A_CYC_AWAY_AIR_TEMP_TARGET", device_class=NumberDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -98,7 +97,7 @@ class ValloxNumberEntityDescription(NumberEntityDescription, ValloxMetricMixin): ), ValloxNumberEntityDescription( key="supply_air_target_boost", - name="Supply air temperature (Boost)", + translation_key="supply_air_target_boost", metric_key="A_CYC_BOOST_AIR_TEMP_TARGET", device_class=NumberDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index a4f6563798dac9..ee0e1e432041a4 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -38,7 +38,6 @@ class ValloxSensorEntity(ValloxEntity, SensorEntity): entity_description: ValloxSensorEntityDescription _attr_entity_category = EntityCategory.DIAGNOSTIC - _attr_has_entity_name = True def __init__( self, @@ -138,13 +137,13 @@ class ValloxSensorEntityDescription(SensorEntityDescription): SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( ValloxSensorEntityDescription( key="current_profile", - name="Current profile", + translation_key="current_profile", icon="mdi:gauge", entity_type=ValloxProfileSensor, ), ValloxSensorEntityDescription( key="fan_speed", - name="Fan speed", + translation_key="fan_speed", metric_key="A_CYC_FAN_SPEED", icon="mdi:fan", state_class=SensorStateClass.MEASUREMENT, @@ -153,7 +152,7 @@ class ValloxSensorEntityDescription(SensorEntityDescription): ), ValloxSensorEntityDescription( key="extract_fan_speed", - name="Extract fan speed", + translation_key="extract_fan_speed", metric_key="A_CYC_EXTR_FAN_SPEED", icon="mdi:fan", state_class=SensorStateClass.MEASUREMENT, @@ -163,7 +162,7 @@ class ValloxSensorEntityDescription(SensorEntityDescription): ), ValloxSensorEntityDescription( key="supply_fan_speed", - name="Supply fan speed", + translation_key="supply_fan_speed", metric_key="A_CYC_SUPP_FAN_SPEED", icon="mdi:fan", state_class=SensorStateClass.MEASUREMENT, @@ -173,20 +172,20 @@ class ValloxSensorEntityDescription(SensorEntityDescription): ), ValloxSensorEntityDescription( key="remaining_time_for_filter", - name="Remaining time for filter", + translation_key="remaining_time_for_filter", device_class=SensorDeviceClass.TIMESTAMP, entity_type=ValloxFilterRemainingSensor, ), ValloxSensorEntityDescription( key="cell_state", - name="Cell state", + translation_key="cell_state", icon="mdi:swap-horizontal-bold", metric_key="A_CYC_CELL_STATE", entity_type=ValloxCellStateSensor, ), ValloxSensorEntityDescription( key="extract_air", - name="Extract air", + translation_key="extract_air", metric_key="A_CYC_TEMP_EXTRACT_AIR", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -194,7 +193,7 @@ class ValloxSensorEntityDescription(SensorEntityDescription): ), ValloxSensorEntityDescription( key="exhaust_air", - name="Exhaust air", + translation_key="exhaust_air", metric_key="A_CYC_TEMP_EXHAUST_AIR", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -202,7 +201,7 @@ class ValloxSensorEntityDescription(SensorEntityDescription): ), ValloxSensorEntityDescription( key="outdoor_air", - name="Outdoor air", + translation_key="outdoor_air", metric_key="A_CYC_TEMP_OUTDOOR_AIR", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -210,7 +209,7 @@ class ValloxSensorEntityDescription(SensorEntityDescription): ), ValloxSensorEntityDescription( key="supply_air", - name="Supply air", + translation_key="supply_air", metric_key="A_CYC_TEMP_SUPPLY_AIR", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -218,7 +217,7 @@ class ValloxSensorEntityDescription(SensorEntityDescription): ), ValloxSensorEntityDescription( key="supply_cell_air", - name="Supply cell air", + translation_key="supply_cell_air", metric_key="A_CYC_TEMP_SUPPLY_CELL_AIR", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -226,7 +225,7 @@ class ValloxSensorEntityDescription(SensorEntityDescription): ), ValloxSensorEntityDescription( key="optional_air", - name="Optional air", + translation_key="optional_air", metric_key="A_CYC_TEMP_OPTIONAL", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -235,7 +234,6 @@ class ValloxSensorEntityDescription(SensorEntityDescription): ), ValloxSensorEntityDescription( key="humidity", - name="Humidity", metric_key="A_CYC_RH_VALUE", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, @@ -243,7 +241,7 @@ class ValloxSensorEntityDescription(SensorEntityDescription): ), ValloxSensorEntityDescription( key="efficiency", - name="Efficiency", + translation_key="efficiency", metric_key="A_CYC_EXTRACT_EFFICIENCY", icon="mdi:gauge", state_class=SensorStateClass.MEASUREMENT, @@ -253,7 +251,6 @@ class ValloxSensorEntityDescription(SensorEntityDescription): ), ValloxSensorEntityDescription( key="co2", - name="CO2", metric_key="A_CYC_CO2_VALUE", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/vallox/strings.json b/homeassistant/components/vallox/strings.json index 42efaeb053871b..acc6a31f158181 100644 --- a/homeassistant/components/vallox/strings.json +++ b/homeassistant/components/vallox/strings.json @@ -19,6 +19,70 @@ "unknown": "[%key:common::config_flow::error::unknown%]" } }, + "entity": { + "binary_sensor": { + "post_heater": { + "name": "Post heater" + } + }, + "number": { + "supply_air_target_home": { + "name": "Supply air temperature (Home)" + }, + "supply_air_target_away": { + "name": "Supply air temperature (Away)" + }, + "supply_air_target_boost": { + "name": "Supply air temperature (Boost)" + } + }, + "sensor": { + "current_profile": { + "name": "Current profile" + }, + "fan_speed": { + "name": "Fan speed" + }, + "extract_fan_speed": { + "name": "Extract fan speed" + }, + "supply_fan_speed": { + "name": "Supply fan speed" + }, + "remaining_time_for_filter": { + "name": "Remaining time for filter" + }, + "cell_state": { + "name": "Cell state" + }, + "extract_air": { + "name": "Extract air" + }, + "exhaust_air": { + "name": "Exhaust air" + }, + "outdoor_air": { + "name": "Outdoor air" + }, + "supply_air": { + "name": "Supply air" + }, + "supply_cell_air": { + "name": "Supply cell air" + }, + "optional_air": { + "name": "Optional air" + }, + "efficiency": { + "name": "Efficiency" + } + }, + "switch": { + "bypass_locked": { + "name": "Bypass locked" + } + } + }, "services": { "set_profile_fan_speed_home": { "name": "Set profile fan speed home", diff --git a/homeassistant/components/vallox/switch.py b/homeassistant/components/vallox/switch.py index 7e8cb4e39c58ed..194659d40cd4b1 100644 --- a/homeassistant/components/vallox/switch.py +++ b/homeassistant/components/vallox/switch.py @@ -21,7 +21,6 @@ class ValloxSwitchEntity(ValloxEntity, SwitchEntity): entity_description: ValloxSwitchEntityDescription _attr_entity_category = EntityCategory.CONFIG - _attr_has_entity_name = True def __init__( self, @@ -79,7 +78,7 @@ class ValloxSwitchEntityDescription(SwitchEntityDescription, ValloxMetricKeyMixi SWITCH_ENTITIES: tuple[ValloxSwitchEntityDescription, ...] = ( ValloxSwitchEntityDescription( key="bypass_locked", - name="Bypass locked", + translation_key="bypass_locked", icon="mdi:arrow-horizontal-lock", metric_key="A_CYC_BYPASS_LOCKED", ), From 36b4b5b887e38cd9fad1a5252f7d17da40b56b33 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 18 Jul 2023 21:18:41 +0200 Subject: [PATCH 0637/1009] Remove duplicated available property from Shelly coordinator entities (#96859) remove duplicated available property --- homeassistant/components/shelly/climate.py | 2 +- homeassistant/components/shelly/entity.py | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 4cc5cacbde39a0..2027cf73d25747 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -210,7 +210,7 @@ def available(self) -> bool: """Device availability.""" if self.device_block is not None: return not cast(bool, self.device_block.valveError) - return self.coordinator.last_update_success + return super().available @property def hvac_mode(self) -> HVACMode: diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 9f95ea5ac2117b..548428c444c794 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -332,11 +332,6 @@ def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None: ) self._attr_unique_id = f"{coordinator.mac}-{block.description}" - @property - def available(self) -> bool: - """Available.""" - return self.coordinator.last_update_success - async def async_added_to_hass(self) -> None: """When entity is added to HASS.""" self.async_on_remove(self.coordinator.async_add_listener(self._update_callback)) @@ -375,11 +370,6 @@ def __init__(self, coordinator: ShellyRpcCoordinator, key: str) -> None: self._attr_unique_id = f"{coordinator.mac}-{key}" self._attr_name = get_rpc_entity_name(coordinator.device, key) - @property - def available(self) -> bool: - """Available.""" - return self.coordinator.last_update_success - @property def status(self) -> dict: """Device status by entity key.""" From 46675560d29883fa13a897234ba5f893d48a2c2a Mon Sep 17 00:00:00 2001 From: Simon Smith Date: Tue, 18 Jul 2023 20:18:58 +0100 Subject: [PATCH 0638/1009] Fix smoke alarm detection in tuya (#96475) --- homeassistant/components/tuya/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 06cb7958242af2..a392a338aba6c7 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -318,7 +318,7 @@ class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription): TuyaBinarySensorEntityDescription( key=DPCode.SMOKE_SENSOR_STATE, device_class=BinarySensorDeviceClass.SMOKE, - on_value="1", + on_value={"1", "alarm"}, ), TAMPER_BINARY_SENSOR, ), From fa59b7f8ac89f0aa5caf5eedfea08ed453a87299 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 21:32:38 +0200 Subject: [PATCH 0639/1009] Add entity translations to Forecast Solar (#96476) --- .../components/forecast_solar/sensor.py | 22 +++++------ .../components/forecast_solar/strings.json | 37 +++++++++++++++++++ 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/forecast_solar/sensor.py b/homeassistant/components/forecast_solar/sensor.py index 2858bff098ea04..1b511f03eda1d1 100644 --- a/homeassistant/components/forecast_solar/sensor.py +++ b/homeassistant/components/forecast_solar/sensor.py @@ -38,7 +38,7 @@ class ForecastSolarSensorEntityDescription(SensorEntityDescription): SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( ForecastSolarSensorEntityDescription( key="energy_production_today", - name="Estimated energy production - today", + translation_key="energy_production_today", state=lambda estimate: estimate.energy_production_today, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, @@ -47,7 +47,7 @@ class ForecastSolarSensorEntityDescription(SensorEntityDescription): ), ForecastSolarSensorEntityDescription( key="energy_production_today_remaining", - name="Estimated energy production - remaining today", + translation_key="energy_production_today_remaining", state=lambda estimate: estimate.energy_production_today_remaining, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, @@ -56,7 +56,7 @@ class ForecastSolarSensorEntityDescription(SensorEntityDescription): ), ForecastSolarSensorEntityDescription( key="energy_production_tomorrow", - name="Estimated energy production - tomorrow", + translation_key="energy_production_tomorrow", state=lambda estimate: estimate.energy_production_tomorrow, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, @@ -65,17 +65,17 @@ class ForecastSolarSensorEntityDescription(SensorEntityDescription): ), ForecastSolarSensorEntityDescription( key="power_highest_peak_time_today", - name="Highest power peak time - today", + translation_key="power_highest_peak_time_today", device_class=SensorDeviceClass.TIMESTAMP, ), ForecastSolarSensorEntityDescription( key="power_highest_peak_time_tomorrow", - name="Highest power peak time - tomorrow", + translation_key="power_highest_peak_time_tomorrow", device_class=SensorDeviceClass.TIMESTAMP, ), ForecastSolarSensorEntityDescription( key="power_production_now", - name="Estimated power production - now", + translation_key="power_production_now", device_class=SensorDeviceClass.POWER, state=lambda estimate: estimate.power_production_now, state_class=SensorStateClass.MEASUREMENT, @@ -83,37 +83,37 @@ class ForecastSolarSensorEntityDescription(SensorEntityDescription): ), ForecastSolarSensorEntityDescription( key="power_production_next_hour", + translation_key="power_production_next_hour", state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=1) ), - name="Estimated power production - next hour", device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfPower.WATT, ), ForecastSolarSensorEntityDescription( key="power_production_next_12hours", + translation_key="power_production_next_12hours", state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=12) ), - name="Estimated power production - next 12 hours", device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfPower.WATT, ), ForecastSolarSensorEntityDescription( key="power_production_next_24hours", + translation_key="power_production_next_24hours", state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=24) ), - name="Estimated power production - next 24 hours", device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfPower.WATT, ), ForecastSolarSensorEntityDescription( key="energy_current_hour", - name="Estimated energy production - this hour", + translation_key="energy_current_hour", state=lambda estimate: estimate.energy_current_hour, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, @@ -122,8 +122,8 @@ class ForecastSolarSensorEntityDescription(SensorEntityDescription): ), ForecastSolarSensorEntityDescription( key="energy_next_hour", + translation_key="energy_next_hour", state=lambda estimate: estimate.sum_energy_production(1), - name="Estimated energy production - next hour", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, diff --git a/homeassistant/components/forecast_solar/strings.json b/homeassistant/components/forecast_solar/strings.json index 7e8c32017ce0ce..43e6fca4ada6c8 100644 --- a/homeassistant/components/forecast_solar/strings.json +++ b/homeassistant/components/forecast_solar/strings.json @@ -31,5 +31,42 @@ } } } + }, + "entity": { + "sensor": { + "energy_production_today": { + "name": "Estimated energy production - today" + }, + "energy_production_today_remaining": { + "name": "Estimated energy production - remaining today" + }, + "energy_production_tomorrow": { + "name": "Estimated energy production - tomorrow" + }, + "power_highest_peak_time_today": { + "name": "Highest power peak time - today" + }, + "power_highest_peak_time_tomorrow": { + "name": "Highest power peak time - tomorrow" + }, + "power_production_now": { + "name": "Estimated power production - now" + }, + "power_production_next_hour": { + "name": "Estimated power production - next hour" + }, + "power_production_next_12hours": { + "name": "Estimated power production - next 12 hours" + }, + "power_production_next_24hours": { + "name": "Estimated power production - next 24 hours" + }, + "energy_current_hour": { + "name": "Estimated energy production - this hour" + }, + "energy_next_hour": { + "name": "Estimated energy production - next hour" + } + } } } From 89ed630af94744f9b0734aed6bdb1110a1378217 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 21:38:29 +0200 Subject: [PATCH 0640/1009] Clean up Kraken const file (#95544) --- homeassistant/components/kraken/const.py | 113 --------------------- homeassistant/components/kraken/sensor.py | 118 +++++++++++++++++++++- 2 files changed, 115 insertions(+), 116 deletions(-) diff --git a/homeassistant/components/kraken/const.py b/homeassistant/components/kraken/const.py index 816fb35fadb530..8a5f7fa828f049 100644 --- a/homeassistant/components/kraken/const.py +++ b/homeassistant/components/kraken/const.py @@ -1,13 +1,8 @@ """Constants for the kraken integration.""" from __future__ import annotations -from collections.abc import Callable -from dataclasses import dataclass from typing import TypedDict -from homeassistant.components.sensor import SensorEntityDescription -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - class KrakenResponseEntry(TypedDict): """Dict describing a single response entry.""" @@ -33,111 +28,3 @@ class KrakenResponseEntry(TypedDict): CONF_TRACKED_ASSET_PAIRS = "tracked_asset_pairs" DOMAIN = "kraken" - - -@dataclass -class KrakenRequiredKeysMixin: - """Mixin for required keys.""" - - value_fn: Callable[[DataUpdateCoordinator[KrakenResponse], str], float | int] - - -@dataclass -class KrakenSensorEntityDescription(SensorEntityDescription, KrakenRequiredKeysMixin): - """Describes Kraken sensor entity.""" - - -SENSOR_TYPES: tuple[KrakenSensorEntityDescription, ...] = ( - KrakenSensorEntityDescription( - key="ask", - name="Ask", - value_fn=lambda x, y: x.data[y]["ask"][0], - ), - KrakenSensorEntityDescription( - key="ask_volume", - name="Ask Volume", - value_fn=lambda x, y: x.data[y]["ask"][1], - entity_registry_enabled_default=False, - ), - KrakenSensorEntityDescription( - key="bid", - name="Bid", - value_fn=lambda x, y: x.data[y]["bid"][0], - ), - KrakenSensorEntityDescription( - key="bid_volume", - name="Bid Volume", - value_fn=lambda x, y: x.data[y]["bid"][1], - entity_registry_enabled_default=False, - ), - KrakenSensorEntityDescription( - key="volume_today", - name="Volume Today", - value_fn=lambda x, y: x.data[y]["volume"][0], - entity_registry_enabled_default=False, - ), - KrakenSensorEntityDescription( - key="volume_last_24h", - name="Volume last 24h", - value_fn=lambda x, y: x.data[y]["volume"][1], - entity_registry_enabled_default=False, - ), - KrakenSensorEntityDescription( - key="volume_weighted_average_today", - name="Volume weighted average today", - value_fn=lambda x, y: x.data[y]["volume_weighted_average"][0], - entity_registry_enabled_default=False, - ), - KrakenSensorEntityDescription( - key="volume_weighted_average_last_24h", - name="Volume weighted average last 24h", - value_fn=lambda x, y: x.data[y]["volume_weighted_average"][1], - entity_registry_enabled_default=False, - ), - KrakenSensorEntityDescription( - key="number_of_trades_today", - name="Number of trades today", - value_fn=lambda x, y: x.data[y]["number_of_trades"][0], - entity_registry_enabled_default=False, - ), - KrakenSensorEntityDescription( - key="number_of_trades_last_24h", - name="Number of trades last 24h", - value_fn=lambda x, y: x.data[y]["number_of_trades"][1], - entity_registry_enabled_default=False, - ), - KrakenSensorEntityDescription( - key="last_trade_closed", - name="Last trade closed", - value_fn=lambda x, y: x.data[y]["last_trade_closed"][0], - entity_registry_enabled_default=False, - ), - KrakenSensorEntityDescription( - key="low_today", - name="Low today", - value_fn=lambda x, y: x.data[y]["low"][0], - ), - KrakenSensorEntityDescription( - key="low_last_24h", - name="Low last 24h", - value_fn=lambda x, y: x.data[y]["low"][1], - entity_registry_enabled_default=False, - ), - KrakenSensorEntityDescription( - key="high_today", - name="High today", - value_fn=lambda x, y: x.data[y]["high"][0], - ), - KrakenSensorEntityDescription( - key="high_last_24h", - name="High last 24h", - value_fn=lambda x, y: x.data[y]["high"][1], - entity_registry_enabled_default=False, - ), - KrakenSensorEntityDescription( - key="opening_price_today", - name="Opening price today", - value_fn=lambda x, y: x.data[y]["opening_price"], - entity_registry_enabled_default=False, - ), -) diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index 0250f17052bc8b..4bbf232f84b966 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -1,9 +1,15 @@ """The kraken integration.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass import logging -from homeassistant.components.sensor import SensorEntity, SensorStateClass +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr @@ -20,14 +26,120 @@ CONF_TRACKED_ASSET_PAIRS, DISPATCH_CONFIG_UPDATED, DOMAIN, - SENSOR_TYPES, KrakenResponse, - KrakenSensorEntityDescription, ) _LOGGER = logging.getLogger(__name__) +@dataclass +class KrakenRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[DataUpdateCoordinator[KrakenResponse], str], float | int] + + +@dataclass +class KrakenSensorEntityDescription(SensorEntityDescription, KrakenRequiredKeysMixin): + """Describes Kraken sensor entity.""" + + +SENSOR_TYPES: tuple[KrakenSensorEntityDescription, ...] = ( + KrakenSensorEntityDescription( + key="ask", + name="Ask", + value_fn=lambda x, y: x.data[y]["ask"][0], + ), + KrakenSensorEntityDescription( + key="ask_volume", + name="Ask Volume", + value_fn=lambda x, y: x.data[y]["ask"][1], + entity_registry_enabled_default=False, + ), + KrakenSensorEntityDescription( + key="bid", + name="Bid", + value_fn=lambda x, y: x.data[y]["bid"][0], + ), + KrakenSensorEntityDescription( + key="bid_volume", + name="Bid Volume", + value_fn=lambda x, y: x.data[y]["bid"][1], + entity_registry_enabled_default=False, + ), + KrakenSensorEntityDescription( + key="volume_today", + name="Volume Today", + value_fn=lambda x, y: x.data[y]["volume"][0], + entity_registry_enabled_default=False, + ), + KrakenSensorEntityDescription( + key="volume_last_24h", + name="Volume last 24h", + value_fn=lambda x, y: x.data[y]["volume"][1], + entity_registry_enabled_default=False, + ), + KrakenSensorEntityDescription( + key="volume_weighted_average_today", + name="Volume weighted average today", + value_fn=lambda x, y: x.data[y]["volume_weighted_average"][0], + entity_registry_enabled_default=False, + ), + KrakenSensorEntityDescription( + key="volume_weighted_average_last_24h", + name="Volume weighted average last 24h", + value_fn=lambda x, y: x.data[y]["volume_weighted_average"][1], + entity_registry_enabled_default=False, + ), + KrakenSensorEntityDescription( + key="number_of_trades_today", + name="Number of trades today", + value_fn=lambda x, y: x.data[y]["number_of_trades"][0], + entity_registry_enabled_default=False, + ), + KrakenSensorEntityDescription( + key="number_of_trades_last_24h", + name="Number of trades last 24h", + value_fn=lambda x, y: x.data[y]["number_of_trades"][1], + entity_registry_enabled_default=False, + ), + KrakenSensorEntityDescription( + key="last_trade_closed", + name="Last trade closed", + value_fn=lambda x, y: x.data[y]["last_trade_closed"][0], + entity_registry_enabled_default=False, + ), + KrakenSensorEntityDescription( + key="low_today", + name="Low today", + value_fn=lambda x, y: x.data[y]["low"][0], + ), + KrakenSensorEntityDescription( + key="low_last_24h", + name="Low last 24h", + value_fn=lambda x, y: x.data[y]["low"][1], + entity_registry_enabled_default=False, + ), + KrakenSensorEntityDescription( + key="high_today", + name="High today", + value_fn=lambda x, y: x.data[y]["high"][0], + ), + KrakenSensorEntityDescription( + key="high_last_24h", + name="High last 24h", + value_fn=lambda x, y: x.data[y]["high"][1], + entity_registry_enabled_default=False, + ), + KrakenSensorEntityDescription( + key="opening_price_today", + name="Opening price today", + value_fn=lambda x, y: x.data[y]["opening_price"], + entity_registry_enabled_default=False, + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, From 6f880ec83764064116e4e82ebc83ad95e04dacc2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 21:39:28 +0200 Subject: [PATCH 0641/1009] Use device class naming for SMS (#96156) --- homeassistant/components/sms/sensor.py | 1 - homeassistant/components/sms/strings.json | 3 --- 2 files changed, 4 deletions(-) diff --git a/homeassistant/components/sms/sensor.py b/homeassistant/components/sms/sensor.py index cfa31d56e8075a..0ad727faf2cb4c 100644 --- a/homeassistant/components/sms/sensor.py +++ b/homeassistant/components/sms/sensor.py @@ -17,7 +17,6 @@ SIGNAL_SENSORS = ( SensorEntityDescription( key="SignalStrength", - translation_key="signal_strength", device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, diff --git a/homeassistant/components/sms/strings.json b/homeassistant/components/sms/strings.json index c005c241d790f4..ae3324aa15621a 100644 --- a/homeassistant/components/sms/strings.json +++ b/homeassistant/components/sms/strings.json @@ -38,9 +38,6 @@ "signal_percent": { "name": "Signal percent" }, - "signal_strength": { - "name": "[%key:component::sensor::entity_component::signal_strength::name%]" - }, "state": { "name": "Network status" } From a2495f494b2634104ca56c02aa8c6a091bad223a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 21:40:20 +0200 Subject: [PATCH 0642/1009] Migrate Soma to entity naming (#96158) --- homeassistant/components/soma/__init__.py | 9 +++------ homeassistant/components/soma/cover.py | 2 ++ homeassistant/components/soma/sensor.py | 5 ----- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 09576f07e6b630..a929bd24b25443 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -108,6 +108,8 @@ async def inner(self) -> dict: class SomaEntity(Entity): """Representation of a generic Soma device.""" + _attr_has_entity_name = True + def __init__(self, device, api): """Initialize the Soma device.""" self.device = device @@ -127,11 +129,6 @@ def unique_id(self): """Return the unique id base on the id returned by pysoma API.""" return self.device["mac"] - @property - def name(self): - """Return the name of the device.""" - return self.device["name"] - @property def device_info(self) -> DeviceInfo: """Return device specific attributes. @@ -141,7 +138,7 @@ def device_info(self) -> DeviceInfo: return DeviceInfo( identifiers={(DOMAIN, self.unique_id)}, manufacturer="Wazombi Labs", - name=self.name, + name=self.device["name"], ) def set_position(self, position: int) -> None: diff --git a/homeassistant/components/soma/cover.py b/homeassistant/components/soma/cover.py index 144c793ac57650..26487756a44a6b 100644 --- a/homeassistant/components/soma/cover.py +++ b/homeassistant/components/soma/cover.py @@ -43,6 +43,7 @@ async def async_setup_entry( class SomaTilt(SomaEntity, CoverEntity): """Representation of a Soma Tilt device.""" + _attr_name = None _attr_device_class = CoverDeviceClass.BLIND _attr_supported_features = ( CoverEntityFeature.OPEN_TILT @@ -118,6 +119,7 @@ async def async_update(self) -> None: class SomaShade(SomaEntity, CoverEntity): """Representation of a Soma Shade device.""" + _attr_name = None _attr_device_class = CoverDeviceClass.SHADE _attr_supported_features = ( CoverEntityFeature.OPEN diff --git a/homeassistant/components/soma/sensor.py b/homeassistant/components/soma/sensor.py index a53bcd26e835fd..6472f6934e00dc 100644 --- a/homeassistant/components/soma/sensor.py +++ b/homeassistant/components/soma/sensor.py @@ -34,11 +34,6 @@ class SomaSensor(SomaEntity, SensorEntity): _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE - @property - def name(self): - """Return the name of the device.""" - return self.device["name"] + " battery level" - @property def native_value(self): """Return the state of the entity.""" From 2b3a379b8e8cca4687b9b2252567ed449fa2d44a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 21:41:33 +0200 Subject: [PATCH 0643/1009] Migrate spider to entity name (#96170) --- homeassistant/components/spider/climate.py | 7 ++----- homeassistant/components/spider/sensor.py | 14 ++++---------- homeassistant/components/spider/strings.json | 10 ++++++++++ homeassistant/components/spider/switch.py | 8 +++----- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/spider/climate.py b/homeassistant/components/spider/climate.py index 261d1565160376..2769d045c0bff9 100644 --- a/homeassistant/components/spider/climate.py +++ b/homeassistant/components/spider/climate.py @@ -40,6 +40,8 @@ async def async_setup_entry( class SpiderThermostat(ClimateEntity): """Representation of a thermostat.""" + _attr_has_entity_name = True + _attr_name = None _attr_temperature_unit = UnitOfTemperature.CELSIUS def __init__(self, api, thermostat): @@ -77,11 +79,6 @@ def unique_id(self): """Return the id of the thermostat, if any.""" return self.thermostat.id - @property - def name(self): - """Return the name of the thermostat, if any.""" - return self.thermostat.name - @property def current_temperature(self): """Return the current temperature.""" diff --git a/homeassistant/components/spider/sensor.py b/homeassistant/components/spider/sensor.py index 259c0fa497496d..5b326db1e45dfe 100644 --- a/homeassistant/components/spider/sensor.py +++ b/homeassistant/components/spider/sensor.py @@ -32,6 +32,8 @@ async def async_setup_entry( class SpiderPowerPlugEnergy(SensorEntity): """Representation of a Spider Power Plug (energy).""" + _attr_has_entity_name = True + _attr_translation_key = "total_energy_today" _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR _attr_device_class = SensorDeviceClass.ENERGY _attr_state_class = SensorStateClass.TOTAL_INCREASING @@ -56,11 +58,6 @@ def unique_id(self) -> str: """Return the ID of this sensor.""" return f"{self.power_plug.id}_total_energy_today" - @property - def name(self) -> str: - """Return the name of the sensor if any.""" - return f"{self.power_plug.name} Total Energy Today" - @property def native_value(self) -> float: """Return todays energy usage in Kwh.""" @@ -74,6 +71,8 @@ def update(self) -> None: class SpiderPowerPlugPower(SensorEntity): """Representation of a Spider Power Plug (power).""" + _attr_has_entity_name = True + _attr_translation_key = "power_consumption" _attr_device_class = SensorDeviceClass.POWER _attr_state_class = SensorStateClass.MEASUREMENT _attr_native_unit_of_measurement = UnitOfPower.WATT @@ -98,11 +97,6 @@ def unique_id(self) -> str: """Return the ID of this sensor.""" return f"{self.power_plug.id}_power_consumption" - @property - def name(self) -> str: - """Return the name of the sensor if any.""" - return f"{self.power_plug.name} Power Consumption" - @property def native_value(self) -> float: """Return the current power usage in W.""" diff --git a/homeassistant/components/spider/strings.json b/homeassistant/components/spider/strings.json index 2e86f47dd2d405..c8d67be36ae4e9 100644 --- a/homeassistant/components/spider/strings.json +++ b/homeassistant/components/spider/strings.json @@ -16,5 +16,15 @@ "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } + }, + "entity": { + "sensor": { + "power_consumption": { + "name": "Power consumption" + }, + "total_energy_today": { + "name": "Total energy today" + } + } } } diff --git a/homeassistant/components/spider/switch.py b/homeassistant/components/spider/switch.py index 607e4c5b84a1b6..28bbf0fcc18285 100644 --- a/homeassistant/components/spider/switch.py +++ b/homeassistant/components/spider/switch.py @@ -26,6 +26,9 @@ async def async_setup_entry( class SpiderPowerPlug(SwitchEntity): """Representation of a Spider Power Plug.""" + _attr_has_entity_name = True + _attr_name = None + def __init__(self, api, power_plug): """Initialize the Spider Power Plug.""" self.api = api @@ -47,11 +50,6 @@ def unique_id(self): """Return the ID of this switch.""" return self.power_plug.id - @property - def name(self): - """Return the name of the switch if any.""" - return self.power_plug.name - @property def is_on(self): """Return true if switch is on. Standby is on.""" From c2d66cc14ac2095a904a7b68d878d6384b78ebdd Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 21:51:37 +0200 Subject: [PATCH 0644/1009] Add entity translations to Tautulli (#96239) --- homeassistant/components/tautulli/sensor.py | 34 ++++++------ .../components/tautulli/strings.json | 55 +++++++++++++++++++ 2 files changed, 72 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index 11dfdf67b3556b..a64f4312de1b16 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -61,14 +61,14 @@ class TautulliSensorEntityDescription( TautulliSensorEntityDescription( icon="mdi:plex", key="watching_count", - name="Watching", + translation_key="watching_count", native_unit_of_measurement="Watching", value_fn=lambda home_stats, activity, _: cast(int, activity.stream_count), ), TautulliSensorEntityDescription( icon="mdi:plex", key="stream_count_direct_play", - name="Direct plays", + translation_key="stream_count_direct_play", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="Streams", entity_registry_enabled_default=False, @@ -79,7 +79,7 @@ class TautulliSensorEntityDescription( TautulliSensorEntityDescription( icon="mdi:plex", key="stream_count_direct_stream", - name="Direct streams", + translation_key="stream_count_direct_stream", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="Streams", entity_registry_enabled_default=False, @@ -90,7 +90,7 @@ class TautulliSensorEntityDescription( TautulliSensorEntityDescription( icon="mdi:plex", key="stream_count_transcode", - name="Transcodes", + translation_key="stream_count_transcode", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="Streams", entity_registry_enabled_default=False, @@ -100,7 +100,7 @@ class TautulliSensorEntityDescription( ), TautulliSensorEntityDescription( key="total_bandwidth", - name="Total bandwidth", + translation_key="total_bandwidth", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.KILOBITS, device_class=SensorDeviceClass.DATA_SIZE, @@ -109,7 +109,7 @@ class TautulliSensorEntityDescription( ), TautulliSensorEntityDescription( key="lan_bandwidth", - name="LAN bandwidth", + translation_key="lan_bandwidth", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.KILOBITS, device_class=SensorDeviceClass.DATA_SIZE, @@ -119,7 +119,7 @@ class TautulliSensorEntityDescription( ), TautulliSensorEntityDescription( key="wan_bandwidth", - name="WAN bandwidth", + translation_key="wan_bandwidth", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.KILOBITS, device_class=SensorDeviceClass.DATA_SIZE, @@ -130,21 +130,21 @@ class TautulliSensorEntityDescription( TautulliSensorEntityDescription( icon="mdi:movie-open", key="top_movies", - name="Top movie", + translation_key="top_movies", entity_registry_enabled_default=False, value_fn=get_top_stats, ), TautulliSensorEntityDescription( icon="mdi:television", key="top_tv", - name="Top TV show", + translation_key="top_tv", entity_registry_enabled_default=False, value_fn=get_top_stats, ), TautulliSensorEntityDescription( icon="mdi:walk", key=ATTR_TOP_USER, - name="Top user", + translation_key="top_user", entity_registry_enabled_default=False, value_fn=get_top_stats, ), @@ -169,26 +169,26 @@ class TautulliSessionSensorEntityDescription( TautulliSessionSensorEntityDescription( icon="mdi:plex", key="state", - name="State", + translation_key="state", value_fn=lambda session: cast(str, session.state), ), TautulliSessionSensorEntityDescription( key="full_title", - name="Full title", + translation_key="full_title", entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.full_title), ), TautulliSessionSensorEntityDescription( icon="mdi:progress-clock", key="progress", - name="Progress", + translation_key="progress", native_unit_of_measurement=PERCENTAGE, entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.progress_percent), ), TautulliSessionSensorEntityDescription( key="stream_resolution", - name="Stream resolution", + translation_key="stream_resolution", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.stream_video_resolution), @@ -196,21 +196,21 @@ class TautulliSessionSensorEntityDescription( TautulliSessionSensorEntityDescription( icon="mdi:plex", key="transcode_decision", - name="Transcode decision", + translation_key="transcode_decision", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.transcode_decision), ), TautulliSessionSensorEntityDescription( key="session_thumb", - name="session thumbnail", + translation_key="session_thumb", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.user_thumb), ), TautulliSessionSensorEntityDescription( key="video_resolution", - name="Video resolution", + translation_key="video_resolution", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.video_resolution), diff --git a/homeassistant/components/tautulli/strings.json b/homeassistant/components/tautulli/strings.json index 90c64a6a8d62f6..4278c6a3bec6de 100644 --- a/homeassistant/components/tautulli/strings.json +++ b/homeassistant/components/tautulli/strings.json @@ -26,5 +26,60 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "entity": { + "sensor": { + "watching_count": { + "name": "Watching" + }, + "stream_count_direct_play": { + "name": "Direct plays" + }, + "stream_count_direct_stream": { + "name": "Direct streams" + }, + "stream_count_transcode": { + "name": "Transcodes" + }, + "total_bandwidth": { + "name": "Total bandwidth" + }, + "lan_bandwidth": { + "name": "LAN bandwidth" + }, + "wan_bandwidth": { + "name": "WAN bandwidth" + }, + "top_movies": { + "name": "Top movie" + }, + "top_tv": { + "name": "Top TV show" + }, + "top_user": { + "name": "Top user" + }, + "state": { + "name": "State" + }, + "full_title": { + "name": "Full title" + }, + "progress": { + "name": "Progress" + }, + "stream_resolution": { + "name": "Stream resolution" + }, + "transcode_decision": { + "name": "Transcode decision" + }, + "session_thumb": { + "name": "Session thumbnail" + }, + "video_resolution": { + "name": "Video resolution" + } + } } } From 3681816a43ad32164ee46551d5cfb0996e4e4fff Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 21:53:54 +0200 Subject: [PATCH 0645/1009] Add entity translations to Tesla Wall Connector (#96242) --- .../tesla_wall_connector/__init__.py | 7 +--- .../tesla_wall_connector/binary_sensor.py | 5 +-- .../components/tesla_wall_connector/sensor.py | 22 +++++----- .../tesla_wall_connector/strings.json | 42 +++++++++++++++++++ 4 files changed, 56 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/tesla_wall_connector/__init__.py b/homeassistant/components/tesla_wall_connector/__init__.py index dfb439133f6b23..2c2d0ca154b9bf 100644 --- a/homeassistant/components/tesla_wall_connector/__init__.py +++ b/homeassistant/components/tesla_wall_connector/__init__.py @@ -122,11 +122,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -def prefix_entity_name(name: str) -> str: - """Prefixes entity name.""" - return f"{WALLCONNECTOR_DEVICE_NAME} {name}" - - def get_unique_id(serial_number: str, key: str) -> str: """Get a unique entity name.""" return f"{serial_number}-{key}" @@ -135,6 +130,8 @@ def get_unique_id(serial_number: str, key: str) -> str: class WallConnectorEntity(CoordinatorEntity): """Base class for Wall Connector entities.""" + _attr_has_entity_name = True + def __init__(self, wall_connector_data: WallConnectorData) -> None: """Initialize WallConnector Entity.""" self.wall_connector_data = wall_connector_data diff --git a/homeassistant/components/tesla_wall_connector/binary_sensor.py b/homeassistant/components/tesla_wall_connector/binary_sensor.py index 2218ec2a6b4279..e0a34460c8ce44 100644 --- a/homeassistant/components/tesla_wall_connector/binary_sensor.py +++ b/homeassistant/components/tesla_wall_connector/binary_sensor.py @@ -16,7 +16,6 @@ WallConnectorData, WallConnectorEntity, WallConnectorLambdaValueGetterMixin, - prefix_entity_name, ) from .const import DOMAIN, WALLCONNECTOR_DATA_VITALS @@ -33,14 +32,14 @@ class WallConnectorBinarySensorDescription( WALL_CONNECTOR_SENSORS = [ WallConnectorBinarySensorDescription( key="vehicle_connected", - name=prefix_entity_name("Vehicle connected"), + translation_key="vehicle_connected", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].vehicle_connected, device_class=BinarySensorDeviceClass.PLUG, ), WallConnectorBinarySensorDescription( key="contactor_closed", - name=prefix_entity_name("Contactor closed"), + translation_key="contactor_closed", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].contactor_closed, device_class=BinarySensorDeviceClass.BATTERY_CHARGING, diff --git a/homeassistant/components/tesla_wall_connector/sensor.py b/homeassistant/components/tesla_wall_connector/sensor.py index 1f83e38030a19b..0322830890a8cd 100644 --- a/homeassistant/components/tesla_wall_connector/sensor.py +++ b/homeassistant/components/tesla_wall_connector/sensor.py @@ -24,7 +24,6 @@ WallConnectorData, WallConnectorEntity, WallConnectorLambdaValueGetterMixin, - prefix_entity_name, ) from .const import DOMAIN, WALLCONNECTOR_DATA_LIFETIME, WALLCONNECTOR_DATA_VITALS @@ -41,13 +40,13 @@ class WallConnectorSensorDescription( WALL_CONNECTOR_SENSORS = [ WallConnectorSensorDescription( key="evse_state", - name=prefix_entity_name("State"), + translation_key="evse_state", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].evse_state, ), WallConnectorSensorDescription( key="handle_temp_c", - name=prefix_entity_name("Handle Temperature"), + translation_key="handle_temp_c", native_unit_of_measurement=UnitOfTemperature.CELSIUS, value_fn=lambda data: round(data[WALLCONNECTOR_DATA_VITALS].handle_temp_c, 1), device_class=SensorDeviceClass.TEMPERATURE, @@ -56,7 +55,7 @@ class WallConnectorSensorDescription( ), WallConnectorSensorDescription( key="grid_v", - name=prefix_entity_name("Grid Voltage"), + translation_key="grid_v", native_unit_of_measurement=UnitOfElectricPotential.VOLT, value_fn=lambda data: round(data[WALLCONNECTOR_DATA_VITALS].grid_v, 1), device_class=SensorDeviceClass.VOLTAGE, @@ -65,7 +64,7 @@ class WallConnectorSensorDescription( ), WallConnectorSensorDescription( key="grid_hz", - name=prefix_entity_name("Grid Frequency"), + translation_key="grid_hz", native_unit_of_measurement=UnitOfFrequency.HERTZ, value_fn=lambda data: round(data[WALLCONNECTOR_DATA_VITALS].grid_hz, 3), device_class=SensorDeviceClass.FREQUENCY, @@ -74,7 +73,7 @@ class WallConnectorSensorDescription( ), WallConnectorSensorDescription( key="current_a_a", - name=prefix_entity_name("Phase A Current"), + translation_key="current_a_a", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].currentA_a, device_class=SensorDeviceClass.CURRENT, @@ -83,7 +82,7 @@ class WallConnectorSensorDescription( ), WallConnectorSensorDescription( key="current_b_a", - name=prefix_entity_name("Phase B Current"), + translation_key="current_b_a", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].currentB_a, device_class=SensorDeviceClass.CURRENT, @@ -92,7 +91,7 @@ class WallConnectorSensorDescription( ), WallConnectorSensorDescription( key="current_c_a", - name=prefix_entity_name("Phase C Current"), + translation_key="current_c_a", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].currentC_a, device_class=SensorDeviceClass.CURRENT, @@ -101,7 +100,7 @@ class WallConnectorSensorDescription( ), WallConnectorSensorDescription( key="voltage_a_v", - name=prefix_entity_name("Phase A Voltage"), + translation_key="voltage_a_v", native_unit_of_measurement=UnitOfElectricPotential.VOLT, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].voltageA_v, device_class=SensorDeviceClass.VOLTAGE, @@ -110,7 +109,7 @@ class WallConnectorSensorDescription( ), WallConnectorSensorDescription( key="voltage_b_v", - name=prefix_entity_name("Phase B Voltage"), + translation_key="voltage_b_v", native_unit_of_measurement=UnitOfElectricPotential.VOLT, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].voltageB_v, device_class=SensorDeviceClass.VOLTAGE, @@ -119,7 +118,7 @@ class WallConnectorSensorDescription( ), WallConnectorSensorDescription( key="voltage_c_v", - name=prefix_entity_name("Phase C Voltage"), + translation_key="voltage_c_v", native_unit_of_measurement=UnitOfElectricPotential.VOLT, value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].voltageC_v, device_class=SensorDeviceClass.VOLTAGE, @@ -128,7 +127,6 @@ class WallConnectorSensorDescription( ), WallConnectorSensorDescription( key="energy_kWh", - name=prefix_entity_name("Energy"), native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, value_fn=lambda data: data[WALLCONNECTOR_DATA_LIFETIME].energy_wh, device_class=SensorDeviceClass.ENERGY, diff --git a/homeassistant/components/tesla_wall_connector/strings.json b/homeassistant/components/tesla_wall_connector/strings.json index 907209cdccad2b..982894eb17c95a 100644 --- a/homeassistant/components/tesla_wall_connector/strings.json +++ b/homeassistant/components/tesla_wall_connector/strings.json @@ -16,5 +16,47 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "binary_sensor": { + "vehicle_connected": { + "name": "Vehicle connected" + }, + "contactor_closed": { + "name": "Contactor closed" + } + }, + "sensor": { + "evse_state": { + "name": "State" + }, + "handle_temp_c": { + "name": "Handle temperature" + }, + "grid_v": { + "name": "Grid voltage" + }, + "grid_hz": { + "name": "Grid frequency" + }, + "current_a_a": { + "name": "Phase A current" + }, + "current_b_a": { + "name": "Phase B current" + }, + "current_c_a": { + "name": "Phase C current" + }, + "voltage_a_v": { + "name": "Phase A voltage" + }, + "voltage_b_v": { + "name": "Phase B voltage" + }, + "voltage_c_v": { + "name": "Phase C voltage" + } + } } } From 3c072e50c7ae3dd7dc15d6fd9aaf7d103019b0e8 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 18 Jul 2023 22:09:19 +0200 Subject: [PATCH 0646/1009] Remove duplicated available property from Picnic coordinator entities (#96861) --- homeassistant/components/picnic/sensor.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py index 74c37e9d5ce26b..5e2e507e450497 100644 --- a/homeassistant/components/picnic/sensor.py +++ b/homeassistant/components/picnic/sensor.py @@ -271,11 +271,6 @@ def native_value(self) -> StateType | datetime: ) return self.entity_description.value_fn(data_set) - @property - def available(self) -> bool: - """Return True if last update was successful.""" - return self.coordinator.last_update_success - @property def device_info(self) -> DeviceInfo: """Return device info.""" From c853010f80de7e296fca5849447c441ededca0dd Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 22:28:04 +0200 Subject: [PATCH 0647/1009] Add entity translations to islamic prayer times (#95469) --- .../components/islamic_prayer_times/sensor.py | 14 ++++----- .../islamic_prayer_times/strings.json | 25 ++++++++++++++++ .../islamic_prayer_times/test_sensor.py | 29 ++++++++++++------- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index abaefec40824b6..2552be7717a92c 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -19,31 +19,31 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="Fajr", - name="Fajr prayer", + translation_key="fajr", ), SensorEntityDescription( key="Sunrise", - name="Sunrise time", + translation_key="sunrise", ), SensorEntityDescription( key="Dhuhr", - name="Dhuhr prayer", + translation_key="dhuhr", ), SensorEntityDescription( key="Asr", - name="Asr prayer", + translation_key="asr", ), SensorEntityDescription( key="Maghrib", - name="Maghrib prayer", + translation_key="maghrib", ), SensorEntityDescription( key="Isha", - name="Isha prayer", + translation_key="isha", ), SensorEntityDescription( key="Midnight", - name="Midnight time", + translation_key="midnight", ), ) diff --git a/homeassistant/components/islamic_prayer_times/strings.json b/homeassistant/components/islamic_prayer_times/strings.json index 73998913f41fee..7c09cc605bdf01 100644 --- a/homeassistant/components/islamic_prayer_times/strings.json +++ b/homeassistant/components/islamic_prayer_times/strings.json @@ -19,5 +19,30 @@ } } } + }, + "entity": { + "sensor": { + "fajr": { + "name": "Fajr prayer" + }, + "sunrise": { + "name": "Sunrise time" + }, + "dhuhr": { + "name": "Dhuhr prayer" + }, + "asr": { + "name": "Asr prayer" + }, + "maghrib": { + "name": "Maghrib prayer" + }, + "isha": { + "name": "Isha prayer" + }, + "midnight": { + "name": "Midnight time" + } + } } } diff --git a/tests/components/islamic_prayer_times/test_sensor.py b/tests/components/islamic_prayer_times/test_sensor.py index 3b291c9973d65e..a5b9b9c8a8d46f 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -5,9 +5,7 @@ import pytest from homeassistant.components.islamic_prayer_times.const import DOMAIN -from homeassistant.components.islamic_prayer_times.sensor import SENSOR_TYPES from homeassistant.core import HomeAssistant -from homeassistant.util import slugify import homeassistant.util.dt as dt_util from . import NOW, PRAYER_TIMES, PRAYER_TIMES_TIMESTAMPS @@ -21,7 +19,21 @@ def set_utc(hass: HomeAssistant) -> None: hass.config.set_time_zone("UTC") -async def test_islamic_prayer_times_sensors(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("key", "sensor_name"), + [ + ("Fajr", "sensor.islamic_prayer_times_fajr_prayer"), + ("Sunrise", "sensor.islamic_prayer_times_sunrise_time"), + ("Dhuhr", "sensor.islamic_prayer_times_dhuhr_prayer"), + ("Asr", "sensor.islamic_prayer_times_asr_prayer"), + ("Maghrib", "sensor.islamic_prayer_times_maghrib_prayer"), + ("Isha", "sensor.islamic_prayer_times_isha_prayer"), + ("Midnight", "sensor.islamic_prayer_times_midnight_time"), + ], +) +async def test_islamic_prayer_times_sensors( + hass: HomeAssistant, key: str, sensor_name: str +) -> None: """Test minimum Islamic prayer times configuration.""" entry = MockConfigEntry(domain=DOMAIN, data={}) entry.add_to_hass(hass) @@ -33,10 +45,7 @@ async def test_islamic_prayer_times_sensors(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - for prayer in SENSOR_TYPES: - assert ( - hass.states.get(f"sensor.{DOMAIN}_{slugify(prayer.name)}").state - == PRAYER_TIMES_TIMESTAMPS[prayer.key] - .astimezone(dt_util.UTC) - .isoformat() - ) + assert ( + hass.states.get(sensor_name).state + == PRAYER_TIMES_TIMESTAMPS[key].astimezone(dt_util.UTC).isoformat() + ) From fdb69efd67d18871251dc84070bde2101aec2402 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 18 Jul 2023 22:47:58 +0200 Subject: [PATCH 0648/1009] Migrate Starline to entity name (#96176) --- .../components/starline/binary_sensor.py | 45 ++++-------- .../components/starline/device_tracker.py | 4 +- homeassistant/components/starline/entity.py | 9 +-- homeassistant/components/starline/lock.py | 4 +- homeassistant/components/starline/sensor.py | 57 +++++---------- .../components/starline/strings.json | 69 +++++++++++++++++++ homeassistant/components/starline/switch.py | 11 ++- 7 files changed, 114 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/starline/binary_sensor.py b/homeassistant/components/starline/binary_sensor.py index b427967ded54e3..bef724392b7272 100644 --- a/homeassistant/components/starline/binary_sensor.py +++ b/homeassistant/components/starline/binary_sensor.py @@ -1,8 +1,6 @@ """Reads vehicle status from StarLine API.""" from __future__ import annotations -from dataclasses import dataclass - from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -16,45 +14,30 @@ from .const import DOMAIN from .entity import StarlineEntity - -@dataclass -class StarlineRequiredKeysMixin: - """Mixin for required keys.""" - - name_: str - - -@dataclass -class StarlineBinarySensorEntityDescription( - BinarySensorEntityDescription, StarlineRequiredKeysMixin -): - """Describes Starline binary_sensor entity.""" - - -BINARY_SENSOR_TYPES: tuple[StarlineBinarySensorEntityDescription, ...] = ( - StarlineBinarySensorEntityDescription( +BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( key="hbrake", - name_="Hand Brake", + translation_key="hand_brake", device_class=BinarySensorDeviceClass.POWER, ), - StarlineBinarySensorEntityDescription( + BinarySensorEntityDescription( key="hood", - name_="Hood", + translation_key="hood", device_class=BinarySensorDeviceClass.DOOR, ), - StarlineBinarySensorEntityDescription( + BinarySensorEntityDescription( key="trunk", - name_="Trunk", + translation_key="trunk", device_class=BinarySensorDeviceClass.DOOR, ), - StarlineBinarySensorEntityDescription( + BinarySensorEntityDescription( key="alarm", - name_="Alarm", + translation_key="alarm", device_class=BinarySensorDeviceClass.PROBLEM, ), - StarlineBinarySensorEntityDescription( + BinarySensorEntityDescription( key="door", - name_="Doors", + translation_key="doors", device_class=BinarySensorDeviceClass.LOCK, ), ) @@ -78,16 +61,14 @@ async def async_setup_entry( class StarlineSensor(StarlineEntity, BinarySensorEntity): """Representation of a StarLine binary sensor.""" - entity_description: StarlineBinarySensorEntityDescription - def __init__( self, account: StarlineAccount, device: StarlineDevice, - description: StarlineBinarySensorEntityDescription, + description: BinarySensorEntityDescription, ) -> None: """Initialize sensor.""" - super().__init__(account, device, description.key, description.name_) + super().__init__(account, device, description.key) self.entity_description = description @property diff --git a/homeassistant/components/starline/device_tracker.py b/homeassistant/components/starline/device_tracker.py index 6dadfdbd3ea3fe..ca8118d6b43f0b 100644 --- a/homeassistant/components/starline/device_tracker.py +++ b/homeassistant/components/starline/device_tracker.py @@ -25,9 +25,11 @@ async def async_setup_entry( class StarlineDeviceTracker(StarlineEntity, TrackerEntity, RestoreEntity): """StarLine device tracker.""" + _attr_translation_key = "location" + def __init__(self, account: StarlineAccount, device: StarlineDevice) -> None: """Set up StarLine entity.""" - super().__init__(account, device, "location", "Location") + super().__init__(account, device, "location") @property def extra_state_attributes(self): diff --git a/homeassistant/components/starline/entity.py b/homeassistant/components/starline/entity.py index 20e5eaed07e37b..7eee5e7a7f8a98 100644 --- a/homeassistant/components/starline/entity.py +++ b/homeassistant/components/starline/entity.py @@ -12,15 +12,15 @@ class StarlineEntity(Entity): """StarLine base entity class.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__( - self, account: StarlineAccount, device: StarlineDevice, key: str, name: str + self, account: StarlineAccount, device: StarlineDevice, key: str ) -> None: """Initialize StarLine entity.""" self._account = account self._device = device self._key = key - self._name = name self._unsubscribe_api: Callable | None = None @property @@ -33,11 +33,6 @@ def unique_id(self): """Return the unique ID of the entity.""" return f"starline-{self._key}-{self._device.device_id}" - @property - def name(self): - """Return the name of the entity.""" - return f"{self._device.name} {self._name}" - @property def device_info(self): """Return the device info.""" diff --git a/homeassistant/components/starline/lock.py b/homeassistant/components/starline/lock.py index 4fb8457a7795aa..f663c472a78cc7 100644 --- a/homeassistant/components/starline/lock.py +++ b/homeassistant/components/starline/lock.py @@ -30,9 +30,11 @@ async def async_setup_entry( class StarlineLock(StarlineEntity, LockEntity): """Representation of a StarLine lock.""" + _attr_translation_key = "security" + def __init__(self, account: StarlineAccount, device: StarlineDevice) -> None: """Initialize the lock.""" - super().__init__(account, device, "lock", "Security") + super().__init__(account, device, "lock") @property def available(self) -> bool: diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index 1acddb27721283..4b787ae5212504 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -1,8 +1,6 @@ """Reads vehicle status from StarLine API.""" from __future__ import annotations -from dataclasses import dataclass - from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -24,63 +22,48 @@ from .const import DOMAIN from .entity import StarlineEntity - -@dataclass -class StarlineRequiredKeysMixin: - """Mixin for required keys.""" - - name_: str - - -@dataclass -class StarlineSensorEntityDescription( - SensorEntityDescription, StarlineRequiredKeysMixin -): - """Describes Starline binary_sensor entity.""" - - -SENSOR_TYPES: tuple[StarlineSensorEntityDescription, ...] = ( - StarlineSensorEntityDescription( +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( key="battery", - name_="Battery", + translation_key="battery", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, ), - StarlineSensorEntityDescription( + SensorEntityDescription( key="balance", - name_="Balance", + translation_key="balance", icon="mdi:cash-multiple", ), - StarlineSensorEntityDescription( + SensorEntityDescription( key="ctemp", - name_="Interior Temperature", + translation_key="interior_temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), - StarlineSensorEntityDescription( + SensorEntityDescription( key="etemp", - name_="Engine Temperature", + translation_key="engine_temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), - StarlineSensorEntityDescription( + SensorEntityDescription( key="gsm_lvl", - name_="GSM Signal", + translation_key="gsm_signal", native_unit_of_measurement=PERCENTAGE, ), - StarlineSensorEntityDescription( + SensorEntityDescription( key="fuel", - name_="Fuel Volume", + translation_key="fuel", icon="mdi:fuel", ), - StarlineSensorEntityDescription( + SensorEntityDescription( key="errors", - name_="OBD Errors", + translation_key="errors", icon="mdi:alert-octagon", ), - StarlineSensorEntityDescription( + SensorEntityDescription( key="mileage", - name_="Mileage", + translation_key="mileage", native_unit_of_measurement=UnitOfLength.KILOMETERS, device_class=SensorDeviceClass.DISTANCE, icon="mdi:counter", @@ -106,16 +89,14 @@ async def async_setup_entry( class StarlineSensor(StarlineEntity, SensorEntity): """Representation of a StarLine sensor.""" - entity_description: StarlineSensorEntityDescription - def __init__( self, account: StarlineAccount, device: StarlineDevice, - description: StarlineSensorEntityDescription, + description: SensorEntityDescription, ) -> None: """Initialize StarLine sensor.""" - super().__init__(account, device, description.key, description.name_) + super().__init__(account, device, description.key) self.entity_description = description @property diff --git a/homeassistant/components/starline/strings.json b/homeassistant/components/starline/strings.json index 4d2c497dc8b6aa..800fd3a65f3bd4 100644 --- a/homeassistant/components/starline/strings.json +++ b/homeassistant/components/starline/strings.json @@ -38,6 +38,75 @@ "error_auth_mfa": "Incorrect code" } }, + "entity": { + "binary_sensor": { + "hand_brake": { + "name": "Hand brake" + }, + "hood": { + "name": "Hood" + }, + "trunk": { + "name": "Trunk" + }, + "alarm": { + "name": "Alarm" + }, + "doors": { + "name": "Doors" + } + }, + "device_tracker": { + "location": { + "name": "Location" + } + }, + "lock": { + "security": { + "name": "Security" + } + }, + "sensor": { + "battery": { + "name": "[%key:component::sensor::entity_component::battery::name%]" + }, + "balance": { + "name": "Balance" + }, + "interior_temperature": { + "name": "Interior temperature" + }, + "engine_temperature": { + "name": "Engine temperature" + }, + "gsm_signal": { + "name": "GSM signal" + }, + "fuel": { + "name": "Fuel volume" + }, + "errors": { + "name": "OBD errors" + }, + "mileage": { + "name": "Mileage" + } + }, + "switch": { + "engine": { + "name": "Engine" + }, + "webasto": { + "name": "Webasto" + }, + "additional_channel": { + "name": "Additional channel" + }, + "horn": { + "name": "Horn" + } + } + }, "services": { "update_state": { "name": "Update state", diff --git a/homeassistant/components/starline/switch.py b/homeassistant/components/starline/switch.py index 412c08b9ff707d..b254fa8133fc92 100644 --- a/homeassistant/components/starline/switch.py +++ b/homeassistant/components/starline/switch.py @@ -18,7 +18,6 @@ class StarlineRequiredKeysMixin: """Mixin for required keys.""" - name_: str icon_on: str icon_off: str @@ -33,25 +32,25 @@ class StarlineSwitchEntityDescription( SWITCH_TYPES: tuple[StarlineSwitchEntityDescription, ...] = ( StarlineSwitchEntityDescription( key="ign", - name_="Engine", + translation_key="engine", icon_on="mdi:engine-outline", icon_off="mdi:engine-off-outline", ), StarlineSwitchEntityDescription( key="webasto", - name_="Webasto", + translation_key="webasto", icon_on="mdi:radiator", icon_off="mdi:radiator-off", ), StarlineSwitchEntityDescription( key="out", - name_="Additional Channel", + translation_key="additional_channel", icon_on="mdi:access-point-network", icon_off="mdi:access-point-network-off", ), StarlineSwitchEntityDescription( key="poke", - name_="Horn", + translation_key="horn", icon_on="mdi:bullhorn-outline", icon_off="mdi:bullhorn-outline", ), @@ -85,7 +84,7 @@ def __init__( description: StarlineSwitchEntityDescription, ) -> None: """Initialize the switch.""" - super().__init__(account, device, description.key, description.name_) + super().__init__(account, device, description.key) self.entity_description = description @property From 4fefbf0408de1d0c2a3de49dcd3a0395cbc932ee Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 18 Jul 2023 23:15:06 +0200 Subject: [PATCH 0649/1009] Remove miflora integration (#96868) --- .coveragerc | 1 - CODEOWNERS | 1 - homeassistant/components/miflora/__init__.py | 1 - .../components/miflora/manifest.json | 8 ----- homeassistant/components/miflora/sensor.py | 29 ------------------- homeassistant/components/miflora/strings.json | 8 ----- homeassistant/generated/integrations.json | 6 ---- 7 files changed, 54 deletions(-) delete mode 100644 homeassistant/components/miflora/__init__.py delete mode 100644 homeassistant/components/miflora/manifest.json delete mode 100644 homeassistant/components/miflora/sensor.py delete mode 100644 homeassistant/components/miflora/strings.json diff --git a/.coveragerc b/.coveragerc index 6cf3f66d8af3a7..163065b7c2a1fe 100644 --- a/.coveragerc +++ b/.coveragerc @@ -707,7 +707,6 @@ omit = homeassistant/components/metoffice/sensor.py homeassistant/components/metoffice/weather.py homeassistant/components/microsoft/tts.py - homeassistant/components/miflora/sensor.py homeassistant/components/mikrotik/hub.py homeassistant/components/mill/climate.py homeassistant/components/mill/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 01f486e2704307..33ae9d167c2e02 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -747,7 +747,6 @@ build.json @home-assistant/supervisor /tests/components/meteoclimatic/ @adrianmo /homeassistant/components/metoffice/ @MrHarcombe @avee87 /tests/components/metoffice/ @MrHarcombe @avee87 -/homeassistant/components/miflora/ @danielhiversen @basnijholt /homeassistant/components/mikrotik/ @engrbm87 /tests/components/mikrotik/ @engrbm87 /homeassistant/components/mill/ @danielhiversen diff --git a/homeassistant/components/miflora/__init__.py b/homeassistant/components/miflora/__init__.py deleted file mode 100644 index ed1569e1af067f..00000000000000 --- a/homeassistant/components/miflora/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The miflora component.""" diff --git a/homeassistant/components/miflora/manifest.json b/homeassistant/components/miflora/manifest.json deleted file mode 100644 index 8a6e1843d86377..00000000000000 --- a/homeassistant/components/miflora/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "miflora", - "name": "Mi Flora", - "codeowners": ["@danielhiversen", "@basnijholt"], - "documentation": "https://www.home-assistant.io/integrations/miflora", - "iot_class": "local_polling", - "requirements": [] -} diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py deleted file mode 100644 index 764e03786f80c3..00000000000000 --- a/homeassistant/components/miflora/sensor.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Support for Xiaomi Mi Flora BLE plant sensor.""" -from __future__ import annotations - -from homeassistant.components.sensor import PLATFORM_SCHEMA_BASE -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -PLATFORM_SCHEMA = PLATFORM_SCHEMA_BASE - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the MiFlora sensor.""" - async_create_issue( - hass, - "miflora", - "replaced", - breaks_in_ha_version="2022.8.0", - is_fixable=False, - severity=IssueSeverity.ERROR, - translation_key="replaced", - learn_more_url="https://www.home-assistant.io/integrations/xiaomi_ble/", - ) diff --git a/homeassistant/components/miflora/strings.json b/homeassistant/components/miflora/strings.json deleted file mode 100644 index 03427e88af991d..00000000000000 --- a/homeassistant/components/miflora/strings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "replaced": { - "title": "The Mi Flora integration has been replaced", - "description": "The Mi Flora integration stopped working in Home Assistant 2022.7 and replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Mi Flora device using the new integration manually.\n\nYour existing Mi Flora YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } - } -} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 1da6a6be9dae57..4e892a2d499c38 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3353,12 +3353,6 @@ } } }, - "miflora": { - "name": "Mi Flora", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_polling" - }, "mijndomein_energie": { "name": "Mijndomein Energie", "integration_type": "virtual", From 4b2cbbe8c2ce998c7ffaeea8654d3c9900c386b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Weitzel?= <72104362+weitzelb@users.noreply.github.com> Date: Tue, 18 Jul 2023 23:18:02 +0200 Subject: [PATCH 0650/1009] Use dispatcher helper to add new Fronius inverter entities (#96782) Using dispatcher to add new entities for inverter --- homeassistant/components/fronius/__init__.py | 11 ++++------- homeassistant/components/fronius/const.py | 1 + homeassistant/components/fronius/sensor.py | 17 +++++++++++++++-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index f8dcb4f4a9cb71..6202b945d97982 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -15,12 +15,13 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from .const import ( DOMAIN, + SOLAR_NET_DISCOVERY_NEW, SOLAR_NET_ID_SYSTEM, SOLAR_NET_RESCAN_TIMER, FroniusDeviceInfo, @@ -34,7 +35,6 @@ FroniusPowerFlowUpdateCoordinator, FroniusStorageUpdateCoordinator, ) -from .sensor import InverterSensor _LOGGER: Final = logging.getLogger(__name__) PLATFORMS: Final = [Platform.SENSOR] @@ -76,7 +76,6 @@ def __init__( self.cleanup_callbacks: list[Callable[[], None]] = [] self.config_entry = entry self.coordinator_lock = asyncio.Lock() - self.sensor_async_add_entities: AddEntitiesCallback | None = None self.fronius = fronius self.host: str = entry.data[CONF_HOST] # entry.unique_id is either logger uid or first inverter uid if no logger available @@ -204,10 +203,8 @@ async def _init_devices_inverter(self, _now: datetime | None = None) -> None: self.inverter_coordinators.append(_coordinator) # Only for re-scans. Initial setup adds entities through sensor.async_setup_entry - if self.sensor_async_add_entities is not None: - _coordinator.add_entities_for_seen_keys( - self.sensor_async_add_entities, InverterSensor - ) + if self.config_entry.state == ConfigEntryState.LOADED: + dispatcher_send(self.hass, SOLAR_NET_DISCOVERY_NEW, _coordinator) _LOGGER.debug( "New inverter added (UID: %s)", diff --git a/homeassistant/components/fronius/const.py b/homeassistant/components/fronius/const.py index 042773472c5b0f..b65864ee0892f6 100644 --- a/homeassistant/components/fronius/const.py +++ b/homeassistant/components/fronius/const.py @@ -6,6 +6,7 @@ DOMAIN: Final = "fronius" SolarNetId = str +SOLAR_NET_DISCOVERY_NEW: Final = "fronius_discovery_new" SOLAR_NET_ID_POWER_FLOW: SolarNetId = "power_flow" SOLAR_NET_ID_SYSTEM: SolarNetId = "system" SOLAR_NET_RESCAN_TIMER: Final = 60 diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index d701d0d1860a22..ff949af0cbaa03 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -24,12 +24,13 @@ UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN +from .const import DOMAIN, SOLAR_NET_DISCOVERY_NEW if TYPE_CHECKING: from . import FroniusSolarNet @@ -53,7 +54,6 @@ async def async_setup_entry( ) -> None: """Set up Fronius sensor entities based on a config entry.""" solar_net: FroniusSolarNet = hass.data[DOMAIN][config_entry.entry_id] - solar_net.sensor_async_add_entities = async_add_entities for inverter_coordinator in solar_net.inverter_coordinators: inverter_coordinator.add_entities_for_seen_keys( @@ -80,6 +80,19 @@ async def async_setup_entry( async_add_entities, StorageSensor ) + @callback + def async_add_new_entities(coordinator: FroniusInverterUpdateCoordinator) -> None: + """Add newly found inverter entities.""" + coordinator.add_entities_for_seen_keys(async_add_entities, InverterSensor) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, + SOLAR_NET_DISCOVERY_NEW, + async_add_new_entities, + ) + ) + @dataclass class FroniusSensorEntityDescription(SensorEntityDescription): From 727a72fbaa54a6d3a65f24616acdb65a26d6d874 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 18 Jul 2023 23:19:03 +0200 Subject: [PATCH 0651/1009] Remove mitemp_bt integration (#96869) --- .coveragerc | 1 - .../components/mitemp_bt/__init__.py | 1 - .../components/mitemp_bt/manifest.json | 8 ----- homeassistant/components/mitemp_bt/sensor.py | 29 ------------------- .../components/mitemp_bt/strings.json | 8 ----- homeassistant/generated/integrations.json | 6 ---- 6 files changed, 53 deletions(-) delete mode 100644 homeassistant/components/mitemp_bt/__init__.py delete mode 100644 homeassistant/components/mitemp_bt/manifest.json delete mode 100644 homeassistant/components/mitemp_bt/sensor.py delete mode 100644 homeassistant/components/mitemp_bt/strings.json diff --git a/.coveragerc b/.coveragerc index 163065b7c2a1fe..b9ff3c4ea0b012 100644 --- a/.coveragerc +++ b/.coveragerc @@ -712,7 +712,6 @@ omit = homeassistant/components/mill/sensor.py homeassistant/components/minecraft_server/__init__.py homeassistant/components/minio/minio_helper.py - homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py homeassistant/components/mjpeg/util.py homeassistant/components/mochad/__init__.py diff --git a/homeassistant/components/mitemp_bt/__init__.py b/homeassistant/components/mitemp_bt/__init__.py deleted file mode 100644 index 785956572afc1f..00000000000000 --- a/homeassistant/components/mitemp_bt/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The mitemp_bt component.""" diff --git a/homeassistant/components/mitemp_bt/manifest.json b/homeassistant/components/mitemp_bt/manifest.json deleted file mode 100644 index 2709c08ad78a72..00000000000000 --- a/homeassistant/components/mitemp_bt/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "mitemp_bt", - "name": "Xiaomi Mijia BLE Temperature and Humidity Sensor", - "codeowners": [], - "documentation": "https://www.home-assistant.io/integrations/mitemp_bt", - "iot_class": "local_polling", - "requirements": [] -} diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py deleted file mode 100644 index a1646bed51c9b6..00000000000000 --- a/homeassistant/components/mitemp_bt/sensor.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Support for Xiaomi Mi Temp BLE environmental sensor.""" -from __future__ import annotations - -from homeassistant.components.sensor import PLATFORM_SCHEMA_BASE -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -PLATFORM_SCHEMA = PLATFORM_SCHEMA_BASE - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the MiTempBt sensor.""" - async_create_issue( - hass, - "mitemp_bt", - "replaced", - breaks_in_ha_version="2022.8.0", - is_fixable=False, - severity=IssueSeverity.ERROR, - translation_key="replaced", - learn_more_url="https://www.home-assistant.io/integrations/xiaomi_ble/", - ) diff --git a/homeassistant/components/mitemp_bt/strings.json b/homeassistant/components/mitemp_bt/strings.json deleted file mode 100644 index 1f9f031a3bbbb0..00000000000000 --- a/homeassistant/components/mitemp_bt/strings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "replaced": { - "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced", - "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity Sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } - } -} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 4e892a2d499c38..99566340ccdb65 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3382,12 +3382,6 @@ "config_flow": false, "iot_class": "cloud_push" }, - "mitemp_bt": { - "name": "Xiaomi Mijia BLE Temperature and Humidity Sensor", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_polling" - }, "mjpeg": { "name": "MJPEG IP Camera", "integration_type": "hub", From 0d69ba6797720856c97f29594d6cfde3da793794 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 18 Jul 2023 23:43:11 +0200 Subject: [PATCH 0652/1009] Allow number to be zero in gardena bluetooth (#96872) Allow number to be 0 in gardena --- homeassistant/components/gardena_bluetooth/number.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gardena_bluetooth/number.py b/homeassistant/components/gardena_bluetooth/number.py index ec7ae513a3ed2e..c425d17621d84e 100644 --- a/homeassistant/components/gardena_bluetooth/number.py +++ b/homeassistant/components/gardena_bluetooth/number.py @@ -105,10 +105,11 @@ class GardenaBluetoothNumber(GardenaBluetoothDescriptorEntity, NumberEntity): entity_description: GardenaBluetoothNumberEntityDescription def _handle_coordinator_update(self) -> None: - if data := self.coordinator.get_cached(self.entity_description.char): - self._attr_native_value = float(data) - else: + data = self.coordinator.get_cached(self.entity_description.char) + if data is None: self._attr_native_value = None + else: + self._attr_native_value = float(data) super()._handle_coordinator_update() async def async_set_native_value(self, value: float) -> None: From 22fbd2294336ad9f4365a2a0d6b2183f3ef0e16d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 19 Jul 2023 00:31:01 +0200 Subject: [PATCH 0653/1009] Add more complete test coverage to gardena bluetooth (#96874) * Add tests for switch * Add tests for number * Add tests for 0 sensor * Enable coverage for gardena bluetooth --- .coveragerc | 7 -- .../components/gardena_bluetooth/switch.py | 2 +- .../components/gardena_bluetooth/conftest.py | 15 ++- .../snapshots/test_number.ambr | 102 ++++++++++++++++++ .../snapshots/test_sensor.ambr | 13 +++ .../snapshots/test_switch.ambr | 25 +++++ .../gardena_bluetooth/test_number.py | 93 +++++++++++++++- .../gardena_bluetooth/test_sensor.py | 1 + .../gardena_bluetooth/test_switch.py | 84 +++++++++++++++ 9 files changed, 332 insertions(+), 10 deletions(-) create mode 100644 tests/components/gardena_bluetooth/snapshots/test_switch.ambr create mode 100644 tests/components/gardena_bluetooth/test_switch.py diff --git a/.coveragerc b/.coveragerc index b9ff3c4ea0b012..4a5c843f357377 100644 --- a/.coveragerc +++ b/.coveragerc @@ -406,13 +406,6 @@ omit = homeassistant/components/garages_amsterdam/__init__.py homeassistant/components/garages_amsterdam/binary_sensor.py homeassistant/components/garages_amsterdam/sensor.py - homeassistant/components/gardena_bluetooth/__init__.py - homeassistant/components/gardena_bluetooth/binary_sensor.py - homeassistant/components/gardena_bluetooth/const.py - homeassistant/components/gardena_bluetooth/coordinator.py - homeassistant/components/gardena_bluetooth/number.py - homeassistant/components/gardena_bluetooth/sensor.py - homeassistant/components/gardena_bluetooth/switch.py homeassistant/components/gc100/* homeassistant/components/geniushub/* homeassistant/components/geocaching/__init__.py diff --git a/homeassistant/components/gardena_bluetooth/switch.py b/homeassistant/components/gardena_bluetooth/switch.py index adb23c74c1d1a3..bc83e3ed5a9179 100644 --- a/homeassistant/components/gardena_bluetooth/switch.py +++ b/homeassistant/components/gardena_bluetooth/switch.py @@ -35,7 +35,7 @@ class GardenaBluetoothValveSwitch(GardenaBluetoothEntity, SwitchEntity): characteristics = { Valve.state.uuid, Valve.manual_watering_time.uuid, - Valve.manual_watering_time.uuid, + Valve.remaining_open_time.uuid, } def __init__( diff --git a/tests/components/gardena_bluetooth/conftest.py b/tests/components/gardena_bluetooth/conftest.py index a4d7170e945bfe..a1d31c45807986 100644 --- a/tests/components/gardena_bluetooth/conftest.py +++ b/tests/components/gardena_bluetooth/conftest.py @@ -63,7 +63,10 @@ def _read_char(char: Characteristic, default: Any = SENTINEL): def _read_char_raw(uuid: str, default: Any = SENTINEL): try: - return mock_read_char_raw[uuid] + val = mock_read_char_raw[uuid] + if isinstance(val, Exception): + raise val + return val except KeyError: if default is SENTINEL: raise CharacteristicNotFound from KeyError @@ -85,3 +88,13 @@ def _all_char(): "2023-01-01", tz_offset=1 ): yield client + + +@pytest.fixture(autouse=True) +def enable_all_entities(): + """Make sure all entities are enabled.""" + with patch( + "homeassistant.components.gardena_bluetooth.coordinator.GardenaBluetoothEntity.entity_registry_enabled_default", + new=Mock(return_value=True), + ): + yield diff --git a/tests/components/gardena_bluetooth/snapshots/test_number.ambr b/tests/components/gardena_bluetooth/snapshots/test_number.ambr index a12cce060194d9..0c464f7cbc1355 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_number.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_number.ambr @@ -1,4 +1,72 @@ # serializer version: 1 +# name: test_bluetooth_error_unavailable + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Remaining open time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_remaining_open_time', + 'last_changed': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_bluetooth_error_unavailable.1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Manual watering time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_manual_watering_time', + 'last_changed': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_bluetooth_error_unavailable.2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Remaining open time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_remaining_open_time', + 'last_changed': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_bluetooth_error_unavailable.3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Manual watering time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_manual_watering_time', + 'last_changed': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-number.mock_title_remaining_open_time] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -33,6 +101,40 @@ 'state': '10.0', }) # --- +# name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-number.mock_title_remaining_open_time].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Remaining open time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_remaining_open_time', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-number.mock_title_remaining_open_time].3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Remaining open time', + 'max': 86400, + 'min': 0.0, + 'mode': , + 'step': 60.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_title_remaining_open_time', + 'last_changed': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw2-number.mock_title_open_for] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr b/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr index 883f377c3a59af..5a23b6d7f504ca 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr @@ -25,6 +25,19 @@ 'state': '2023-01-01T01:00:10+00:00', }) # --- +# name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-sensor.mock_title_valve_closing].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Mock Title Valve closing', + }), + 'context': , + 'entity_id': 'sensor.mock_title_valve_closing', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_setup[98bd2a19-0b0e-421a-84e5-ddbf75dc6de4-raw0-sensor.mock_title_battery] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/gardena_bluetooth/snapshots/test_switch.ambr b/tests/components/gardena_bluetooth/snapshots/test_switch.ambr new file mode 100644 index 00000000000000..37dae0bff75c1c --- /dev/null +++ b/tests/components/gardena_bluetooth/snapshots/test_switch.ambr @@ -0,0 +1,25 @@ +# serializer version: 1 +# name: test_setup + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Open', + }), + 'context': , + 'entity_id': 'switch.mock_title_open', + 'last_changed': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_setup.1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Open', + }), + 'context': , + 'entity_id': 'switch.mock_title_open', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/gardena_bluetooth/test_number.py b/tests/components/gardena_bluetooth/test_number.py index f1955905cce4eb..588b73aadbbd5c 100644 --- a/tests/components/gardena_bluetooth/test_number.py +++ b/tests/components/gardena_bluetooth/test_number.py @@ -1,11 +1,27 @@ """Test Gardena Bluetooth sensor.""" +from typing import Any +from unittest.mock import Mock, call + from gardena_bluetooth.const import Valve +from gardena_bluetooth.exceptions import ( + CharacteristicNoAccess, + GardenaBluetoothException, +) +from gardena_bluetooth.parse import Characteristic import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.const import Platform +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + Platform, +) from homeassistant.core import HomeAssistant from . import setup_entry @@ -29,6 +45,8 @@ [ Valve.remaining_open_time.encode(100), Valve.remaining_open_time.encode(10), + CharacteristicNoAccess("Test for no access"), + GardenaBluetoothException("Test for errors on bluetooth"), ], "number.mock_title_remaining_open_time", ), @@ -58,3 +76,76 @@ async def test_setup( mock_read_char_raw[uuid] = char_raw await coordinator.async_refresh() assert hass.states.get(entity_id) == snapshot + + +@pytest.mark.parametrize( + ("char", "value", "expected", "entity_id"), + [ + ( + Valve.manual_watering_time, + 100, + 100, + "number.mock_title_manual_watering_time", + ), + ( + Valve.remaining_open_time, + 100, + 100 * 60, + "number.mock_title_open_for", + ), + ], +) +async def test_config( + hass: HomeAssistant, + mock_entry: MockConfigEntry, + mock_read_char_raw: dict[str, bytes], + mock_client: Mock, + char: Characteristic, + value: Any, + expected: Any, + entity_id: str, +) -> None: + """Test setup creates expected entities.""" + + mock_read_char_raw[char.uuid] = char.encode(value) + await setup_entry(hass, mock_entry, [Platform.NUMBER]) + assert hass.states.get(entity_id) + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: value}, + blocking=True, + ) + + assert mock_client.write_char.mock_calls == [ + call(char, expected), + ] + + +async def test_bluetooth_error_unavailable( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_read_char_raw: dict[str, bytes], +) -> None: + """Verify that a connectivity error makes all entities unavailable.""" + + mock_read_char_raw[ + Valve.manual_watering_time.uuid + ] = Valve.manual_watering_time.encode(0) + mock_read_char_raw[ + Valve.remaining_open_time.uuid + ] = Valve.remaining_open_time.encode(0) + + coordinator = await setup_entry(hass, mock_entry, [Platform.NUMBER]) + assert hass.states.get("number.mock_title_remaining_open_time") == snapshot + assert hass.states.get("number.mock_title_manual_watering_time") == snapshot + + mock_read_char_raw[Valve.manual_watering_time.uuid] = GardenaBluetoothException( + "Test for errors on bluetooth" + ) + + await coordinator.async_refresh() + assert hass.states.get("number.mock_title_remaining_open_time") == snapshot + assert hass.states.get("number.mock_title_manual_watering_time") == snapshot diff --git a/tests/components/gardena_bluetooth/test_sensor.py b/tests/components/gardena_bluetooth/test_sensor.py index d7cdc205f50c58..e9fd452e6a23fb 100644 --- a/tests/components/gardena_bluetooth/test_sensor.py +++ b/tests/components/gardena_bluetooth/test_sensor.py @@ -26,6 +26,7 @@ [ Valve.remaining_open_time.encode(100), Valve.remaining_open_time.encode(10), + Valve.remaining_open_time.encode(0), ], "sensor.mock_title_valve_closing", ), diff --git a/tests/components/gardena_bluetooth/test_switch.py b/tests/components/gardena_bluetooth/test_switch.py new file mode 100644 index 00000000000000..c2571b7a588fe4 --- /dev/null +++ b/tests/components/gardena_bluetooth/test_switch.py @@ -0,0 +1,84 @@ +"""Test Gardena Bluetooth sensor.""" + + +from unittest.mock import Mock, call + +from gardena_bluetooth.const import Valve +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + Platform, +) +from homeassistant.core import HomeAssistant + +from . import setup_entry + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_switch_chars(mock_read_char_raw): + """Mock data on device.""" + mock_read_char_raw[Valve.state.uuid] = b"\x00" + mock_read_char_raw[ + Valve.remaining_open_time.uuid + ] = Valve.remaining_open_time.encode(0) + mock_read_char_raw[ + Valve.manual_watering_time.uuid + ] = Valve.manual_watering_time.encode(1000) + return mock_read_char_raw + + +async def test_setup( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_client: Mock, + mock_switch_chars: dict[str, bytes], +) -> None: + """Test setup creates expected entities.""" + + entity_id = "switch.mock_title_open" + coordinator = await setup_entry(hass, mock_entry, [Platform.SWITCH]) + assert hass.states.get(entity_id) == snapshot + + mock_switch_chars[Valve.state.uuid] = b"\x01" + await coordinator.async_refresh() + assert hass.states.get(entity_id) == snapshot + + +async def test_switching( + hass: HomeAssistant, + mock_entry: MockConfigEntry, + mock_client: Mock, + mock_switch_chars: dict[str, bytes], +) -> None: + """Test switching makes correct calls.""" + + entity_id = "switch.mock_title_open" + await setup_entry(hass, mock_entry, [Platform.SWITCH]) + assert hass.states.get(entity_id) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert mock_client.write_char.mock_calls == [ + call(Valve.remaining_open_time, 1000), + call(Valve.remaining_open_time, 0), + ] From 1449df5649749541b31e47f93b9a74db1f1da3fe Mon Sep 17 00:00:00 2001 From: Luke Date: Tue, 18 Jul 2023 18:25:24 -0600 Subject: [PATCH 0654/1009] 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 0cf6db4ae816db..5f6aa63ce2f469 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 eaa70a0fd623d5..d1cd300ddbdf12 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2142,7 +2142,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 bd6ceda67a91ac..811e114a2f8b23 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1568,7 +1568,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 9b839041fa288f84a539f8c157d91821c9e86ff0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jul 2023 23:49:40 -0500 Subject: [PATCH 0655/1009] Bump aioesphomeapi to 15.1.11 (#96873) --- 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 a8665d76656f4c..a8324ed770ddba 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.1.9", + "aioesphomeapi==15.1.11", "bluetooth-data-tools==1.6.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index d1cd300ddbdf12..16c673ee1f1ca1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==15.1.9 +aioesphomeapi==15.1.11 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 811e114a2f8b23..d2424fdac95d41 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.1.9 +aioesphomeapi==15.1.11 # homeassistant.components.flo aioflo==2021.11.0 From b45369bb35c97fd508476cf3bfbe8d16ec8e1c83 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Jul 2023 23:50:29 -0500 Subject: [PATCH 0656/1009] Bump flux_led to 1.0.0 (#96879) --- homeassistant/components/flux_led/manifest.json | 5 ++++- homeassistant/generated/dhcp.py | 4 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 13f7ba36bcd146..224d98d92bf00b 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -42,6 +42,9 @@ { "hostname": "zengge_[0-9a-f][0-9a-f]_*" }, + { + "hostname": "zengge" + }, { "macaddress": "C82E47*", "hostname": "sta*" @@ -51,5 +54,5 @@ "iot_class": "local_push", "loggers": ["flux_led"], "quality_scale": "platinum", - "requirements": ["flux-led==0.28.37"] + "requirements": ["flux-led==1.0.0"] } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 63a0bb43d2aa1c..052edf09bec309 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -185,6 +185,10 @@ "domain": "flux_led", "hostname": "zengge_[0-9a-f][0-9a-f]_*", }, + { + "domain": "flux_led", + "hostname": "zengge", + }, { "domain": "flux_led", "hostname": "sta*", diff --git a/requirements_all.txt b/requirements_all.txt index 16c673ee1f1ca1..90f0f7aeb4a659 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -791,7 +791,7 @@ fjaraskupan==2.2.0 flipr-api==1.5.0 # homeassistant.components.flux_led -flux-led==0.28.37 +flux-led==1.0.0 # homeassistant.components.homekit # homeassistant.components.recorder diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d2424fdac95d41..255ce1ce7eca68 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -619,7 +619,7 @@ fjaraskupan==2.2.0 flipr-api==1.5.0 # homeassistant.components.flux_led -flux-led==0.28.37 +flux-led==1.0.0 # homeassistant.components.homekit # homeassistant.components.recorder From 22d0f4ff0af055d95ce90e9ff42953a0347b43fa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 19 Jul 2023 07:10:07 +0200 Subject: [PATCH 0657/1009] Remove legacy discovery integration (#96856) --- CODEOWNERS | 2 - .../components/discovery/__init__.py | 248 ------------------ .../components/discovery/manifest.json | 11 - .../components/xiaomi_aqara/manifest.json | 1 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - script/hassfest/manifest.py | 1 - tests/components/discovery/__init__.py | 1 - tests/components/discovery/test_init.py | 105 -------- 9 files changed, 375 deletions(-) delete mode 100644 homeassistant/components/discovery/__init__.py delete mode 100644 homeassistant/components/discovery/manifest.json delete mode 100644 tests/components/discovery/__init__.py delete mode 100644 tests/components/discovery/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 33ae9d167c2e02..5198f12519c5c5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -277,8 +277,6 @@ build.json @home-assistant/supervisor /tests/components/discord/ @tkdrob /homeassistant/components/discovergy/ @jpbede /tests/components/discovergy/ @jpbede -/homeassistant/components/discovery/ @home-assistant/core -/tests/components/discovery/ @home-assistant/core /homeassistant/components/dlink/ @tkdrob /tests/components/dlink/ @tkdrob /homeassistant/components/dlna_dmr/ @StevenLooman @chishm diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py deleted file mode 100644 index 79653e1c9bc16c..00000000000000 --- a/homeassistant/components/discovery/__init__.py +++ /dev/null @@ -1,248 +0,0 @@ -"""Starts a service to scan in intervals for new devices.""" -from __future__ import annotations - -from datetime import datetime, timedelta -import json -import logging -from typing import NamedTuple - -from netdisco.discovery import NetworkDiscovery -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.components import zeroconf -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED -from homeassistant.core import Event, HassJob, HomeAssistant, callback -from homeassistant.helpers import discovery_flow -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import async_discover, async_load_platform -from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.typing import ConfigType -from homeassistant.loader import async_get_zeroconf -import homeassistant.util.dt as dt_util - -DOMAIN = "discovery" - -SCAN_INTERVAL = timedelta(seconds=300) -SERVICE_APPLE_TV = "apple_tv" -SERVICE_DAIKIN = "daikin" -SERVICE_DLNA_DMR = "dlna_dmr" -SERVICE_ENIGMA2 = "enigma2" -SERVICE_HASS_IOS_APP = "hass_ios" -SERVICE_HASSIO = "hassio" -SERVICE_HEOS = "heos" -SERVICE_KONNECTED = "konnected" -SERVICE_MOBILE_APP = "hass_mobile_app" -SERVICE_NETGEAR = "netgear_router" -SERVICE_OCTOPRINT = "octoprint" -SERVICE_SABNZBD = "sabnzbd" -SERVICE_SAMSUNG_PRINTER = "samsung_printer" -SERVICE_TELLDUSLIVE = "tellstick" -SERVICE_YEELIGHT = "yeelight" -SERVICE_WEMO = "belkin_wemo" -SERVICE_XIAOMI_GW = "xiaomi_gw" - -# These have custom protocols -CONFIG_ENTRY_HANDLERS = { - SERVICE_TELLDUSLIVE: "tellduslive", - "logitech_mediaserver": "squeezebox", -} - - -class ServiceDetails(NamedTuple): - """Store service details.""" - - component: str - platform: str | None - - -# These have no config flows -SERVICE_HANDLERS = { - SERVICE_ENIGMA2: ServiceDetails("media_player", "enigma2"), - "yamaha": ServiceDetails("media_player", "yamaha"), - "bluesound": ServiceDetails("media_player", "bluesound"), -} - -OPTIONAL_SERVICE_HANDLERS: dict[str, tuple[str, str | None]] = {} - -MIGRATED_SERVICE_HANDLERS = [ - SERVICE_APPLE_TV, - "axis", - "bose_soundtouch", - "deconz", - SERVICE_DAIKIN, - "denonavr", - SERVICE_DLNA_DMR, - "esphome", - "google_cast", - SERVICE_HASS_IOS_APP, - SERVICE_HASSIO, - SERVICE_HEOS, - "harmony", - "homekit", - "ikea_tradfri", - "kodi", - SERVICE_KONNECTED, - SERVICE_MOBILE_APP, - SERVICE_NETGEAR, - SERVICE_OCTOPRINT, - "openhome", - "philips_hue", - SERVICE_SAMSUNG_PRINTER, - "sonos", - "songpal", - SERVICE_WEMO, - SERVICE_XIAOMI_GW, - "volumio", - SERVICE_YEELIGHT, - SERVICE_SABNZBD, - "nanoleaf_aurora", - "lg_smart_device", -] - -DEFAULT_ENABLED = ( - list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS) + MIGRATED_SERVICE_HANDLERS -) -DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS) + MIGRATED_SERVICE_HANDLERS - -CONF_IGNORE = "ignore" -CONF_ENABLE = "enable" - -CONFIG_SCHEMA = vol.Schema( - { - vol.Optional(DOMAIN): vol.Schema( - { - vol.Optional(CONF_IGNORE, default=[]): vol.All( - cv.ensure_list, [vol.In(DEFAULT_ENABLED)] - ), - vol.Optional(CONF_ENABLE, default=[]): vol.All( - cv.ensure_list, [vol.In(DEFAULT_DISABLED + DEFAULT_ENABLED)] - ), - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Start a discovery service.""" - - logger = logging.getLogger(__name__) - netdisco = NetworkDiscovery() - already_discovered = set() - - if DOMAIN in config: - # Platforms ignore by config - ignored_platforms = config[DOMAIN][CONF_IGNORE] - - # Optional platforms enabled by config - enabled_platforms = config[DOMAIN][CONF_ENABLE] - else: - ignored_platforms = [] - enabled_platforms = [] - - for platform in enabled_platforms: - if platform in DEFAULT_ENABLED: - logger.warning( - ( - "Please remove %s from your discovery.enable configuration " - "as it is now enabled by default" - ), - platform, - ) - - zeroconf_instance = await zeroconf.async_get_instance(hass) - # Do not scan for types that have already been converted - # as it will generate excess network traffic for questions - # the zeroconf instance already knows the answers - zeroconf_types = list(await async_get_zeroconf(hass)) - - async def new_service_found(service, info): - """Handle a new service if one is found.""" - if service in MIGRATED_SERVICE_HANDLERS: - return - - if service in ignored_platforms: - logger.info("Ignoring service: %s %s", service, info) - return - - discovery_hash = json.dumps([service, info], sort_keys=True) - if discovery_hash in already_discovered: - logger.debug("Already discovered service %s %s.", service, info) - return - - already_discovered.add(discovery_hash) - - if service in CONFIG_ENTRY_HANDLERS: - discovery_flow.async_create_flow( - hass, - CONFIG_ENTRY_HANDLERS[service], - context={"source": config_entries.SOURCE_DISCOVERY}, - data=info, - ) - return - - service_details = SERVICE_HANDLERS.get(service) - - if not service_details and service in enabled_platforms: - service_details = OPTIONAL_SERVICE_HANDLERS[service] - - # We do not know how to handle this service. - if not service_details: - logger.debug("Unknown service discovered: %s %s", service, info) - return - - logger.info("Found new service: %s %s", service, info) - - if service_details.platform is None: - await async_discover(hass, service, info, service_details.component, config) - else: - await async_load_platform( - hass, service_details.component, service_details.platform, info, config - ) - - async def scan_devices(now: datetime) -> None: - """Scan for devices.""" - try: - results = await hass.async_add_executor_job( - _discover, netdisco, zeroconf_instance, zeroconf_types - ) - - for result in results: - hass.async_create_task(new_service_found(*result)) - except OSError: - logger.error("Network is unreachable") - - async_track_point_in_utc_time( - hass, scan_devices_job, dt_util.utcnow() + SCAN_INTERVAL - ) - - @callback - def schedule_first(event: Event) -> None: - """Schedule the first discovery when Home Assistant starts up.""" - async_track_point_in_utc_time(hass, scan_devices_job, dt_util.utcnow()) - - scan_devices_job = HassJob(scan_devices, cancel_on_shutdown=True) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, schedule_first) - - return True - - -def _discover(netdisco, zeroconf_instance, zeroconf_types): - """Discover devices.""" - results = [] - try: - netdisco.scan( - zeroconf_instance=zeroconf_instance, suppress_mdns_types=zeroconf_types - ) - - for disc in netdisco.discover(): - for service in netdisco.get_info(disc): - results.append((disc, service)) - - finally: - netdisco.stop() - - return results diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json deleted file mode 100644 index d6d3443f562034..00000000000000 --- a/homeassistant/components/discovery/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "domain": "discovery", - "name": "Discovery", - "after_dependencies": ["zeroconf"], - "codeowners": ["@home-assistant/core"], - "documentation": "https://www.home-assistant.io/integrations/discovery", - "integration_type": "system", - "loggers": ["netdisco"], - "quality_scale": "internal", - "requirements": ["netdisco==3.0.0"] -} diff --git a/homeassistant/components/xiaomi_aqara/manifest.json b/homeassistant/components/xiaomi_aqara/manifest.json index 6d84a5ffd0a3ea..75d4b0b9a00921 100644 --- a/homeassistant/components/xiaomi_aqara/manifest.json +++ b/homeassistant/components/xiaomi_aqara/manifest.json @@ -1,7 +1,6 @@ { "domain": "xiaomi_aqara", "name": "Xiaomi Gateway (Aqara)", - "after_dependencies": ["discovery"], "codeowners": ["@danielhiversen", "@syssi"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara", diff --git a/requirements_all.txt b/requirements_all.txt index 90f0f7aeb4a659..9065b918ba5129 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1240,9 +1240,6 @@ nessclient==0.10.0 # homeassistant.components.netdata netdata==1.1.0 -# homeassistant.components.discovery -netdisco==3.0.0 - # homeassistant.components.nmap_tracker netmap==0.7.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 255ce1ce7eca68..e9b238f1d175d6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -951,9 +951,6 @@ ndms2-client==0.1.2 # homeassistant.components.ness_alarm nessclient==0.10.0 -# homeassistant.components.discovery -netdisco==3.0.0 - # homeassistant.components.nmap_tracker netmap==0.7.0.2 diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 9a7caec925bee3..4515f52d8a3c0c 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -62,7 +62,6 @@ class QualityScale(IntEnum): "device_automation", "device_tracker", "diagnostics", - "discovery", "downloader", "ffmpeg", "file_upload", diff --git a/tests/components/discovery/__init__.py b/tests/components/discovery/__init__.py deleted file mode 100644 index b5744b42d6b8d6..00000000000000 --- a/tests/components/discovery/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the discovery component.""" diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py deleted file mode 100644 index 7a9fda82511299..00000000000000 --- a/tests/components/discovery/test_init.py +++ /dev/null @@ -1,105 +0,0 @@ -"""The tests for the discovery component.""" -from unittest.mock import MagicMock, patch - -import pytest - -from homeassistant import config_entries -from homeassistant.bootstrap import async_setup_component -from homeassistant.components import discovery -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED -from homeassistant.core import HomeAssistant -from homeassistant.util.dt import utcnow - -from tests.common import async_fire_time_changed, mock_coro - -# One might consider to "mock" services, but it's easy enough to just use -# what is already available. -SERVICE = "yamaha" -SERVICE_COMPONENT = "media_player" - -SERVICE_INFO = {"key": "value"} # Can be anything - -UNKNOWN_SERVICE = "this_service_will_never_be_supported" - -BASE_CONFIG = {discovery.DOMAIN: {"ignore": [], "enable": []}} - - -@pytest.fixture(autouse=True) -def netdisco_mock(): - """Mock netdisco.""" - with patch.dict("sys.modules", {"netdisco.discovery": MagicMock()}): - yield - - -async def mock_discovery(hass, discoveries, config=BASE_CONFIG): - """Mock discoveries.""" - with patch("homeassistant.components.zeroconf.async_get_instance"), patch( - "homeassistant.components.zeroconf.async_setup", return_value=True - ), patch.object(discovery, "_discover", discoveries), patch( - "homeassistant.components.discovery.async_discover" - ) as mock_discover, patch( - "homeassistant.components.discovery.async_load_platform", - return_value=mock_coro(), - ) as mock_platform: - assert await async_setup_component(hass, "discovery", config) - await hass.async_block_till_done() - await hass.async_start() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow()) - # Work around an issue where our loop.call_soon not get caught - await hass.async_block_till_done() - await hass.async_block_till_done() - - return mock_discover, mock_platform - - -async def test_unknown_service(hass: HomeAssistant) -> None: - """Test that unknown service is ignored.""" - - def discover(netdisco, zeroconf_instance, suppress_mdns_types): - """Fake discovery.""" - return [("this_service_will_never_be_supported", {"info": "some"})] - - mock_discover, mock_platform = await mock_discovery(hass, discover) - - assert not mock_discover.called - assert not mock_platform.called - - -async def test_load_platform(hass: HomeAssistant) -> None: - """Test load a platform.""" - - def discover(netdisco, zeroconf_instance, suppress_mdns_types): - """Fake discovery.""" - return [(SERVICE, SERVICE_INFO)] - - mock_discover, mock_platform = await mock_discovery(hass, discover) - - assert not mock_discover.called - assert mock_platform.called - mock_platform.assert_called_with( - hass, SERVICE_COMPONENT, SERVICE, SERVICE_INFO, BASE_CONFIG - ) - - -async def test_discover_config_flow(hass: HomeAssistant) -> None: - """Test discovery triggering a config flow.""" - discovery_info = {"hello": "world"} - - def discover(netdisco, zeroconf_instance, suppress_mdns_types): - """Fake discovery.""" - return [("mock-service", discovery_info)] - - with patch.dict( - discovery.CONFIG_ENTRY_HANDLERS, {"mock-service": "mock-component"} - ), patch( - "homeassistant.config_entries.ConfigEntriesFlowManager.async_init" - ) as m_init: - await mock_discovery(hass, discover) - - assert len(m_init.mock_calls) == 1 - args, kwargs = m_init.mock_calls[0][1:] - assert args == ("mock-component",) - assert kwargs["context"]["source"] == config_entries.SOURCE_DISCOVERY - assert kwargs["data"] == discovery_info From f2bd122fde12f20dc8cf1189d6cb24254c7801fd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 19 Jul 2023 09:03:53 +0200 Subject: [PATCH 0658/1009] Clean up conversation agent attribution (#96883) * Clean up conversation agent attribution * Clean up google_generative_ai_conversation as well --- .../components/conversation/__init__.py | 24 ------------ .../components/conversation/agent.py | 14 +------ .../google_assistant_sdk/__init__.py | 8 ---- .../__init__.py | 8 ---- .../openai_conversation/__init__.py | 5 --- tests/components/conversation/__init__.py | 5 --- tests/components/conversation/test_init.py | 37 ------------------- .../google_assistant_sdk/test_init.py | 1 - 8 files changed, 1 insertion(+), 101 deletions(-) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 5b82b5dae72e0f..30ecf16bb373d8 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -195,7 +195,6 @@ async def handle_reload(service: core.ServiceCall) -> None: hass.http.register_view(ConversationProcessView()) websocket_api.async_register_command(hass, websocket_process) websocket_api.async_register_command(hass, websocket_prepare) - websocket_api.async_register_command(hass, websocket_get_agent_info) websocket_api.async_register_command(hass, websocket_list_agents) websocket_api.async_register_command(hass, websocket_hass_agent_debug) @@ -249,29 +248,6 @@ async def websocket_prepare( connection.send_result(msg["id"]) -@websocket_api.websocket_command( - { - vol.Required("type"): "conversation/agent/info", - vol.Optional("agent_id"): agent_id_validator, - } -) -@websocket_api.async_response -async def websocket_get_agent_info( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """Info about the agent in use.""" - agent = await _get_agent_manager(hass).async_get_agent(msg.get("agent_id")) - - connection.send_result( - msg["id"], - { - "attribution": agent.attribution, - }, - ) - - @websocket_api.websocket_command( { vol.Required("type"): "conversation/agent/list", diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py index 99b9c9392d8fd9..2eae36311875c1 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Any, Literal, TypedDict +from typing import Any, Literal from homeassistant.core import Context from homeassistant.helpers import intent @@ -35,21 +35,9 @@ def as_dict(self) -> dict[str, Any]: } -class Attribution(TypedDict): - """Attribution for a conversation agent.""" - - name: str - url: str - - class AbstractConversationAgent(ABC): """Abstract conversation agent.""" - @property - def attribution(self) -> Attribution | None: - """Return the attribution.""" - return None - @property @abstractmethod def supported_languages(self) -> list[str] | Literal["*"]: diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index db2a8d9512ed4a..4a294489c97bed 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -128,14 +128,6 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: self.session: OAuth2Session | None = None self.language: str | None = None - @property - def attribution(self): - """Return the attribution.""" - return { - "name": "Powered by Google Assistant SDK", - "url": "https://www.home-assistant.io/integrations/google_assistant_sdk/", - } - @property def supported_languages(self) -> list[str]: """Return a list of supported languages.""" diff --git a/homeassistant/components/google_generative_ai_conversation/__init__.py b/homeassistant/components/google_generative_ai_conversation/__init__.py index 3d0fac634207d7..1154c7132d21da 100644 --- a/homeassistant/components/google_generative_ai_conversation/__init__.py +++ b/homeassistant/components/google_generative_ai_conversation/__init__.py @@ -69,14 +69,6 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: self.entry = entry self.history: dict[str, list[dict]] = {} - @property - def attribution(self): - """Return the attribution.""" - return { - "name": "Powered by Google Generative AI", - "url": "https://developers.generativeai.google/", - } - @property def supported_languages(self) -> list[str] | Literal["*"]: """Return a list of supported languages.""" diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py index c1b569ce9e12d9..efa81c7b73c3fc 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -66,11 +66,6 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: self.entry = entry self.history: dict[str, list[dict]] = {} - @property - def attribution(self): - """Return the attribution.""" - return {"name": "Powered by OpenAI", "url": "https://www.openai.com"} - @property def supported_languages(self) -> list[str] | Literal["*"]: """Return a list of supported languages.""" diff --git a/tests/components/conversation/__init__.py b/tests/components/conversation/__init__.py index df57c78c9aa11b..648f8f33811e0a 100644 --- a/tests/components/conversation/__init__.py +++ b/tests/components/conversation/__init__.py @@ -24,11 +24,6 @@ def __init__( self.response = "Test response" self._supported_languages = supported_languages - @property - def attribution(self) -> conversation.Attribution | None: - """Return the attribution.""" - return {"name": "Mock assistant", "url": "https://assist.me"} - @property def supported_languages(self) -> list[str]: """Return a list of supported languages.""" diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 6ad9beb3362160..f89af1dc201a06 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -1611,43 +1611,6 @@ async def test_get_agent_info( assert agent_info == snapshot -async def test_ws_get_agent_info( - hass: HomeAssistant, - init_components, - mock_agent, - hass_ws_client: WebSocketGenerator, - snapshot: SnapshotAssertion, -) -> None: - """Test get agent info.""" - client = await hass_ws_client(hass) - - await client.send_json_auto_id({"type": "conversation/agent/info"}) - msg = await client.receive_json() - assert msg["success"] - assert msg["result"] == snapshot - - await client.send_json_auto_id( - {"type": "conversation/agent/info", "agent_id": "homeassistant"} - ) - msg = await client.receive_json() - assert msg["success"] - assert msg["result"] == snapshot - - await client.send_json_auto_id( - {"type": "conversation/agent/info", "agent_id": mock_agent.agent_id} - ) - msg = await client.receive_json() - assert msg["success"] - assert msg["result"] == snapshot - - await client.send_json_auto_id( - {"type": "conversation/agent/info", "agent_id": "not_exist"} - ) - msg = await client.receive_json() - assert not msg["success"] - assert msg["error"] == snapshot - - async def test_ws_hass_agent_debug( hass: HomeAssistant, init_components, diff --git a/tests/components/google_assistant_sdk/test_init.py b/tests/components/google_assistant_sdk/test_init.py index 99f264e4a3a0f2..3cb64a9a441800 100644 --- a/tests/components/google_assistant_sdk/test_init.py +++ b/tests/components/google_assistant_sdk/test_init.py @@ -326,7 +326,6 @@ async def test_conversation_agent( assert entry.state is ConfigEntryState.LOADED agent = await conversation._get_agent_manager(hass).async_get_agent(entry.entry_id) - assert agent.attribution.keys() == {"name", "url"} assert agent.supported_languages == SUPPORTED_LANGUAGE_CODES text1 = "tell me a joke" From 3e58e1987c318399d02ebdd683c8b68b0529b08a 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 0659/1009] 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 5ec27a1768c639..7276e7a0d9b67c 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 0dc67c37403382..e0152190d90839 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -245,7 +245,7 @@ def mux(self, packet): # 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 01e66d6fb2671fcbfba296a020e11e497a82bb50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Jul 2023 02:23:12 -0500 Subject: [PATCH 0660/1009] Improve handling of unrecoverable storage corruption (#96712) * Improve handling of unrecoverable storage corruption fixes #96574 If something in storage gets corrupted core can boot loop or if its integration specific, the integration will fail to start. We now complainly loudly in the log, move away the corrupt data and start fresh to allow startup to proceed so the user can get to the UI and restore from backup without having to attach a console (or otherwise login to the OS and manually modify files). * test for corruption * ensure OSError is still fatal * one more case * create an issue for corrupt storage * fix key * persist * feedback * feedback * better to give the full path * tweaks * grammar * add time * feedback * adjust * try to get issue_domain from storage key * coverage * tweak wording some more --- .../components/homeassistant/strings.json | 11 ++ homeassistant/helpers/storage.py | 73 +++++++- tests/helpers/test_storage.py | 159 +++++++++++++++++- 3 files changed, 236 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 791b1a2192951b..5404ee4af6496e 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -27,6 +27,17 @@ "no_platform_setup": { "title": "Unused YAML configuration for the {platform} integration", "description": "It's not possible to configure {platform} {domain} by adding `{platform_key}` to the {domain} configuration. Please check the documentation for more information on how to set up this integration.\n\nTo resolve this:\n1. Remove `{platform_key}` occurences from the `{domain}:` configuration in your YAML configuration file.\n2. Restart Home Assistant.\n\nExample that should be removed:\n{yaml_example}\n" + }, + "storage_corruption": { + "title": "Storage corruption detected for `{storage_key}`", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::homeassistant::issues::storage_corruption::title%]", + "description": "The `{storage_key}` storage could not be parsed and has been renamed to `{corrupt_path}` to allow Home Assistant to continue.\n\nA default `{storage_key}` may have been created automatically.\n\nIf you made manual edits to the storage file, fix any syntax errors in `{corrupt_path}`, restore the file to the original path `{original_path}`, and restart Home Assistant. Otherwise, restore the system from a backup.\n\nClick SUBMIT below to confirm you have repaired the file or restored from a backup.\n\nThe exact error was: {error}" + } + } + } } }, "system_health": { diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 128a36e3e14c4e..dd394c84f9111d 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -6,15 +6,24 @@ from contextlib import suppress from copy import deepcopy import inspect -from json import JSONEncoder +from json import JSONDecodeError, JSONEncoder import logging import os from typing import Any, Generic, TypeVar from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE -from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + DOMAIN as HOMEASSISTANT_DOMAIN, + CoreState, + Event, + HomeAssistant, + callback, +) +from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import MAX_LOAD_CONCURRENTLY, bind_hass from homeassistant.util import json as json_util +import homeassistant.util.dt as dt_util from homeassistant.util.file import WriteError from . import json as json_helper @@ -146,9 +155,63 @@ async def _async_load_data(self): # and we don't want that to mess with what we're trying to store. data = deepcopy(data) else: - data = await self.hass.async_add_executor_job( - json_util.load_json, self.path - ) + try: + data = await self.hass.async_add_executor_job( + json_util.load_json, self.path + ) + except HomeAssistantError as err: + if isinstance(err.__cause__, JSONDecodeError): + # If we have a JSONDecodeError, it means the file is corrupt. + # We can't recover from this, so we'll log an error, rename the file and + # return None so that we can start with a clean slate which will + # allow startup to continue so they can restore from a backup. + isotime = dt_util.utcnow().isoformat() + corrupt_postfix = f".corrupt.{isotime}" + corrupt_path = f"{self.path}{corrupt_postfix}" + await self.hass.async_add_executor_job( + os.rename, self.path, corrupt_path + ) + storage_key = self.key + _LOGGER.error( + "Unrecoverable error decoding storage %s at %s; " + "This may indicate an unclean shutdown, invalid syntax " + "from manual edits, or disk corruption; " + "The corrupt file has been saved as %s; " + "It is recommended to restore from backup: %s", + storage_key, + self.path, + corrupt_path, + err, + ) + from .issue_registry import ( # pylint: disable=import-outside-toplevel + IssueSeverity, + async_create_issue, + ) + + issue_domain = HOMEASSISTANT_DOMAIN + if ( + domain := (storage_key.partition(".")[0]) + ) and domain in self.hass.config.components: + issue_domain = domain + + async_create_issue( + self.hass, + HOMEASSISTANT_DOMAIN, + f"storage_corruption_{storage_key}_{isotime}", + is_fixable=True, + issue_domain=issue_domain, + translation_key="storage_corruption", + is_persistent=True, + severity=IssueSeverity.CRITICAL, + translation_placeholders={ + "storage_key": storage_key, + "original_path": self.path, + "corrupt_path": corrupt_path, + "error": str(err), + }, + ) + return None + raise if data == {}: return None diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 76dfbdbeb46ebb..81953c7d785b65 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import json +import os from typing import Any, NamedTuple from unittest.mock import Mock, patch @@ -12,8 +13,9 @@ EVENT_HOMEASSISTANT_FINAL_WRITE, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import CoreState, HomeAssistant -from homeassistant.helpers import storage +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, CoreState, HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import issue_registry as ir, storage from homeassistant.util import dt as dt_util from homeassistant.util.color import RGBColor @@ -548,3 +550,156 @@ class NamedTupleSubclass(NamedTuple): } await hass.async_stop(force=True) + + +async def test_loading_corrupt_core_file( + tmpdir: py.path.local, caplog: pytest.LogCaptureFixture +) -> None: + """Test we handle unrecoverable corruption in a core file.""" + loop = asyncio.get_running_loop() + hass = await async_test_home_assistant(loop) + + tmp_storage = await hass.async_add_executor_job(tmpdir.mkdir, "temp_storage") + hass.config.config_dir = tmp_storage + + storage_key = "core.anything" + store = storage.Store( + hass, MOCK_VERSION_2, storage_key, minor_version=MOCK_MINOR_VERSION_1 + ) + await store.async_save({"hello": "world"}) + storage_path = os.path.join(tmp_storage, ".storage") + store_file = os.path.join(storage_path, store.key) + + data = await store.async_load() + assert data == {"hello": "world"} + + def _corrupt_store(): + with open(store_file, "w") as f: + f.write("corrupt") + + await hass.async_add_executor_job(_corrupt_store) + + data = await store.async_load() + assert data is None + assert "Unrecoverable error decoding storage" in caplog.text + + issue_registry = ir.async_get(hass) + found_issue = None + issue_entry = None + for (domain, issue), entry in issue_registry.issues.items(): + if domain == HOMEASSISTANT_DOMAIN and issue.startswith( + f"storage_corruption_{storage_key}_" + ): + found_issue = issue + issue_entry = entry + break + + assert found_issue is not None + assert issue_entry is not None + assert issue_entry.is_fixable is True + assert issue_entry.translation_placeholders["storage_key"] == storage_key + assert issue_entry.issue_domain == HOMEASSISTANT_DOMAIN + assert ( + issue_entry.translation_placeholders["error"] + == "unexpected character: line 1 column 1 (char 0)" + ) + + files = await hass.async_add_executor_job( + os.listdir, os.path.join(tmp_storage, ".storage") + ) + assert ".corrupt" in files[0] + + await hass.async_stop(force=True) + + +async def test_loading_corrupt_file_known_domain( + tmpdir: py.path.local, caplog: pytest.LogCaptureFixture +) -> None: + """Test we handle unrecoverable corruption for a known domain.""" + loop = asyncio.get_running_loop() + hass = await async_test_home_assistant(loop) + hass.config.components.add("testdomain") + storage_key = "testdomain.testkey" + + tmp_storage = await hass.async_add_executor_job(tmpdir.mkdir, "temp_storage") + hass.config.config_dir = tmp_storage + + store = storage.Store( + hass, MOCK_VERSION_2, storage_key, minor_version=MOCK_MINOR_VERSION_1 + ) + await store.async_save({"hello": "world"}) + storage_path = os.path.join(tmp_storage, ".storage") + store_file = os.path.join(storage_path, store.key) + + data = await store.async_load() + assert data == {"hello": "world"} + + def _corrupt_store(): + with open(store_file, "w") as f: + f.write('{"valid":"json"}..with..corrupt') + + await hass.async_add_executor_job(_corrupt_store) + + data = await store.async_load() + assert data is None + assert "Unrecoverable error decoding storage" in caplog.text + + issue_registry = ir.async_get(hass) + found_issue = None + issue_entry = None + for (domain, issue), entry in issue_registry.issues.items(): + if domain == HOMEASSISTANT_DOMAIN and issue.startswith( + f"storage_corruption_{storage_key}_" + ): + found_issue = issue + issue_entry = entry + break + + assert found_issue is not None + assert issue_entry is not None + assert issue_entry.is_fixable is True + assert issue_entry.translation_placeholders["storage_key"] == storage_key + assert issue_entry.issue_domain == "testdomain" + assert ( + issue_entry.translation_placeholders["error"] + == "unexpected content after document: line 1 column 17 (char 16)" + ) + + files = await hass.async_add_executor_job( + os.listdir, os.path.join(tmp_storage, ".storage") + ) + assert ".corrupt" in files[0] + + await hass.async_stop(force=True) + + +async def test_os_error_is_fatal(tmpdir: py.path.local) -> None: + """Test OSError during load is fatal.""" + loop = asyncio.get_running_loop() + hass = await async_test_home_assistant(loop) + + tmp_storage = await hass.async_add_executor_job(tmpdir.mkdir, "temp_storage") + hass.config.config_dir = tmp_storage + + store = storage.Store( + hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1 + ) + await store.async_save({"hello": "world"}) + + with pytest.raises(OSError), patch( + "homeassistant.helpers.storage.json_util.load_json", side_effect=OSError + ): + await store.async_load() + + base_os_error = OSError() + base_os_error.errno = 30 + home_assistant_error = HomeAssistantError() + home_assistant_error.__cause__ = base_os_error + + with pytest.raises(HomeAssistantError), patch( + "homeassistant.helpers.storage.json_util.load_json", + side_effect=home_assistant_error, + ): + await store.async_load() + + await hass.async_stop(force=True) From 87d0b026c248f8661e96bd3ce6c256bba610163f Mon Sep 17 00:00:00 2001 From: Darren Foo Date: Wed, 19 Jul 2023 00:24:37 -0700 Subject: [PATCH 0661/1009] Add support for multiple Russound RNET controllers (#96793) * add mutiple russound rnet controller support * Update homeassistant/components/russound_rnet/media_player.py --------- Co-authored-by: Erik Montnemery --- .../components/russound_rnet/media_player.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/russound_rnet/media_player.py b/homeassistant/components/russound_rnet/media_player.py index 7a384656b6672c..b19f4b9dfeefaf 100644 --- a/homeassistant/components/russound_rnet/media_player.py +++ b/homeassistant/components/russound_rnet/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +import math from russound import russound import voluptuous as vol @@ -85,17 +86,25 @@ def __init__(self, hass, russ, sources, zone_id, extra): self._attr_name = extra["name"] self._russ = russ self._attr_source_list = sources - self._zone_id = zone_id + # Each controller has a maximum of 6 zones, every increment of 6 zones + # maps to an additional controller for easier backward compatibility + self._controller_id = str(math.ceil(zone_id / 6)) + # Each zone resets to 1-6 per controller + self._zone_id = (zone_id - 1) % 6 + 1 def update(self) -> None: """Retrieve latest state.""" # Updated this function to make a single call to get_zone_info, so that # with a single call we can get On/Off, Volume and Source, reducing the # amount of traffic and speeding up the update process. - ret = self._russ.get_zone_info("1", self._zone_id, 4) + ret = self._russ.get_zone_info(self._controller_id, self._zone_id, 4) _LOGGER.debug("ret= %s", ret) if ret is not None: - _LOGGER.debug("Updating status for zone %s", self._zone_id) + _LOGGER.debug( + "Updating status for RNET zone %s on controller %s", + self._zone_id, + self._controller_id, + ) if ret[0] == 0: self._attr_state = MediaPlayerState.OFF else: @@ -118,23 +127,23 @@ def set_volume_level(self, volume: float) -> None: Translate this to a range of (0..100) as expected by _russ.set_volume() """ - self._russ.set_volume("1", self._zone_id, volume * 100) + self._russ.set_volume(self._controller_id, self._zone_id, volume * 100) def turn_on(self) -> None: """Turn the media player on.""" - self._russ.set_power("1", self._zone_id, "1") + self._russ.set_power(self._controller_id, self._zone_id, "1") def turn_off(self) -> None: """Turn off media player.""" - self._russ.set_power("1", self._zone_id, "0") + self._russ.set_power(self._controller_id, self._zone_id, "0") def mute_volume(self, mute: bool) -> None: """Send mute command.""" - self._russ.toggle_mute("1", self._zone_id) + self._russ.toggle_mute(self._controller_id, self._zone_id) def select_source(self, source: str) -> None: """Set the input source.""" if self.source_list and source in self.source_list: index = self.source_list.index(source) # 0 based value for source - self._russ.set_source("1", self._zone_id, index) + self._russ.set_source(self._controller_id, self._zone_id, index) From 67e3203d004e39614dd54119e3100333a5524bcd Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 19 Jul 2023 03:50:09 -0400 Subject: [PATCH 0662/1009] Add tomorrow.io state translations and dynamically assign enum device class (#96603) * Add state translations and dynamically assign enum device class * Reference existing keys * Handle additional entity descriptions --- homeassistant/components/tomorrowio/sensor.py | 34 ++++--------------- .../components/tomorrowio/strings.json | 10 ++++++ 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py index 6f75679f124dd3..aba5b44f284187 100644 --- a/homeassistant/components/tomorrowio/sensor.py +++ b/homeassistant/components/tomorrowio/sensor.py @@ -95,6 +95,10 @@ def __post_init__(self) -> None: "they must both be None" ) + if self.value_map is not None: + self.device_class = SensorDeviceClass.ENUM + self.options = [item.name.lower() for item in self.value_map] + # From https://cfpub.epa.gov/ncer_abstracts/index.cfm/fuseaction/display.files/fileID/14285 # x ug/m^3 = y ppb * molecular weight / 24.45 @@ -176,8 +180,6 @@ def convert_ppb_to_ugm3(molecular_weight: int | float) -> Callable[[float], floa key=TMRW_ATTR_PRECIPITATION_TYPE, name="Precipitation Type", value_map=PrecipitationType, - device_class=SensorDeviceClass.ENUM, - options=["freezing_rain", "ice_pellets", "none", "rain", "snow"], translation_key="precipitation_type", icon="mdi:weather-snowy-rainy", ), @@ -237,20 +239,12 @@ def convert_ppb_to_ugm3(molecular_weight: int | float) -> Callable[[float], floa key=TMRW_ATTR_EPA_PRIMARY_POLLUTANT, name="US EPA Primary Pollutant", value_map=PrimaryPollutantType, + translation_key="primary_pollutant", ), TomorrowioSensorEntityDescription( key=TMRW_ATTR_EPA_HEALTH_CONCERN, name="US EPA Health Concern", value_map=HealthConcernType, - device_class=SensorDeviceClass.ENUM, - options=[ - "good", - "hazardous", - "moderate", - "unhealthy_for_sensitive_groups", - "unhealthy", - "very_unhealthy", - ], translation_key="health_concern", icon="mdi:hospital", ), @@ -263,20 +257,12 @@ def convert_ppb_to_ugm3(molecular_weight: int | float) -> Callable[[float], floa key=TMRW_ATTR_CHINA_PRIMARY_POLLUTANT, name="China MEP Primary Pollutant", value_map=PrimaryPollutantType, + translation_key="primary_pollutant", ), TomorrowioSensorEntityDescription( key=TMRW_ATTR_CHINA_HEALTH_CONCERN, name="China MEP Health Concern", value_map=HealthConcernType, - device_class=SensorDeviceClass.ENUM, - options=[ - "good", - "hazardous", - "moderate", - "unhealthy_for_sensitive_groups", - "unhealthy", - "very_unhealthy", - ], translation_key="health_concern", icon="mdi:hospital", ), @@ -284,8 +270,6 @@ def convert_ppb_to_ugm3(molecular_weight: int | float) -> Callable[[float], floa key=TMRW_ATTR_POLLEN_TREE, name="Tree Pollen Index", value_map=PollenIndex, - device_class=SensorDeviceClass.ENUM, - options=["high", "low", "medium", "none", "very_high", "very_low"], translation_key="pollen_index", icon="mdi:flower-pollen", ), @@ -293,8 +277,6 @@ def convert_ppb_to_ugm3(molecular_weight: int | float) -> Callable[[float], floa key=TMRW_ATTR_POLLEN_WEED, name="Weed Pollen Index", value_map=PollenIndex, - device_class=SensorDeviceClass.ENUM, - options=["high", "low", "medium", "none", "very_high", "very_low"], translation_key="pollen_index", icon="mdi:flower-pollen", ), @@ -302,8 +284,6 @@ def convert_ppb_to_ugm3(molecular_weight: int | float) -> Callable[[float], floa key=TMRW_ATTR_POLLEN_GRASS, name="Grass Pollen Index", value_map=PollenIndex, - device_class=SensorDeviceClass.ENUM, - options=["high", "low", "medium", "none", "very_high", "very_low"], translation_key="pollen_index", icon="mdi:flower-pollen", ), @@ -321,8 +301,6 @@ def convert_ppb_to_ugm3(molecular_weight: int | float) -> Callable[[float], floa key=TMRW_ATTR_UV_HEALTH_CONCERN, name="UV Radiation Health Concern", value_map=UVDescription, - device_class=SensorDeviceClass.ENUM, - options=["high", "low", "moderate", "very_high", "extreme"], translation_key="uv_index", icon="mdi:sun-wireless", ), diff --git a/homeassistant/components/tomorrowio/strings.json b/homeassistant/components/tomorrowio/strings.json index c795dbfdbafd10..a104570f5c88a8 100644 --- a/homeassistant/components/tomorrowio/strings.json +++ b/homeassistant/components/tomorrowio/strings.json @@ -62,6 +62,16 @@ "ice_pellets": "Ice Pellets" } }, + "primary_pollutant": { + "state": { + "pm25": "[%key:component::sensor::entity_component::pm25::name%]", + "pm10": "[%key:component::sensor::entity_component::pm10::name%]", + "o3": "[%key:component::sensor::entity_component::ozone::name%]", + "no2": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]", + "co": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", + "so2": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]" + } + }, "uv_index": { "state": { "low": "Low", From 80a74470306850817ce9292b7f6f10eaa818c405 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 19 Jul 2023 10:17:40 +0200 Subject: [PATCH 0663/1009] Add support for buttons in gardena bluetooth (#96871) * Add button to gardena * Add tests for button * Bump gardena bluetooth to 1.0.2 --------- Co-authored-by: Joost Lekkerkerker --- .../components/gardena_bluetooth/__init__.py | 1 + .../components/gardena_bluetooth/button.py | 60 +++++++++++++++++ .../gardena_bluetooth/manifest.json | 2 +- .../components/gardena_bluetooth/strings.json | 5 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../snapshots/test_button.ambr | 25 +++++++ .../gardena_bluetooth/test_button.py | 67 +++++++++++++++++++ 8 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/gardena_bluetooth/button.py create mode 100644 tests/components/gardena_bluetooth/snapshots/test_button.ambr create mode 100644 tests/components/gardena_bluetooth/test_button.py diff --git a/homeassistant/components/gardena_bluetooth/__init__.py b/homeassistant/components/gardena_bluetooth/__init__.py index c779d30b0fc3fe..2390f5af56184b 100644 --- a/homeassistant/components/gardena_bluetooth/__init__.py +++ b/homeassistant/components/gardena_bluetooth/__init__.py @@ -22,6 +22,7 @@ PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, diff --git a/homeassistant/components/gardena_bluetooth/button.py b/homeassistant/components/gardena_bluetooth/button.py new file mode 100644 index 00000000000000..cfaa4d72c2a76a --- /dev/null +++ b/homeassistant/components/gardena_bluetooth/button.py @@ -0,0 +1,60 @@ +"""Support for button entities.""" +from __future__ import annotations + +from dataclasses import dataclass, field + +from gardena_bluetooth.const import Reset +from gardena_bluetooth.parse import CharacteristicBool + +from homeassistant.components.button import ( + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import Coordinator, GardenaBluetoothDescriptorEntity + + +@dataclass +class GardenaBluetoothButtonEntityDescription(ButtonEntityDescription): + """Description of entity.""" + + char: CharacteristicBool = field(default_factory=lambda: CharacteristicBool("")) + + +DESCRIPTIONS = ( + GardenaBluetoothButtonEntityDescription( + key=Reset.factory_reset.uuid, + translation_key="factory_reset", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + char=Reset.factory_reset, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up binary sensor based on a config entry.""" + coordinator: Coordinator = hass.data[DOMAIN][entry.entry_id] + entities = [ + GardenaBluetoothButton(coordinator, description) + for description in DESCRIPTIONS + if description.key in coordinator.characteristics + ] + async_add_entities(entities) + + +class GardenaBluetoothButton(GardenaBluetoothDescriptorEntity, ButtonEntity): + """Representation of a binary sensor.""" + + entity_description: GardenaBluetoothButtonEntityDescription + + async def async_press(self) -> None: + """Trigger button action.""" + await self.coordinator.write(self.entity_description.char, True) diff --git a/homeassistant/components/gardena_bluetooth/manifest.json b/homeassistant/components/gardena_bluetooth/manifest.json index cdc43a802c99c7..0226460d4d83a5 100644 --- a/homeassistant/components/gardena_bluetooth/manifest.json +++ b/homeassistant/components/gardena_bluetooth/manifest.json @@ -13,5 +13,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth", "iot_class": "local_polling", - "requirements": ["gardena_bluetooth==1.0.1"] + "requirements": ["gardena_bluetooth==1.0.2"] } diff --git a/homeassistant/components/gardena_bluetooth/strings.json b/homeassistant/components/gardena_bluetooth/strings.json index 5a3f77eafa4fd2..1d9a281fdbcd4f 100644 --- a/homeassistant/components/gardena_bluetooth/strings.json +++ b/homeassistant/components/gardena_bluetooth/strings.json @@ -24,6 +24,11 @@ "name": "Valve connection" } }, + "button": { + "factory_reset": { + "name": "Factory reset" + } + }, "number": { "remaining_open_time": { "name": "Remaining open time" diff --git a/requirements_all.txt b/requirements_all.txt index 9065b918ba5129..74a6082c0bc44a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -820,7 +820,7 @@ fritzconnection[qr]==1.12.2 gTTS==2.2.4 # homeassistant.components.gardena_bluetooth -gardena_bluetooth==1.0.1 +gardena_bluetooth==1.0.2 # homeassistant.components.google_assistant_sdk gassist-text==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e9b238f1d175d6..3b968ee932e485 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -642,7 +642,7 @@ fritzconnection[qr]==1.12.2 gTTS==2.2.4 # homeassistant.components.gardena_bluetooth -gardena_bluetooth==1.0.1 +gardena_bluetooth==1.0.2 # homeassistant.components.google_assistant_sdk gassist-text==0.0.10 diff --git a/tests/components/gardena_bluetooth/snapshots/test_button.ambr b/tests/components/gardena_bluetooth/snapshots/test_button.ambr new file mode 100644 index 00000000000000..b9cdca0e03c633 --- /dev/null +++ b/tests/components/gardena_bluetooth/snapshots/test_button.ambr @@ -0,0 +1,25 @@ +# serializer version: 1 +# name: test_setup + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Factory reset', + }), + 'context': , + 'entity_id': 'button.mock_title_factory_reset', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup.1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Factory reset', + }), + 'context': , + 'entity_id': 'button.mock_title_factory_reset', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/gardena_bluetooth/test_button.py b/tests/components/gardena_bluetooth/test_button.py new file mode 100644 index 00000000000000..e184a2ecce88d8 --- /dev/null +++ b/tests/components/gardena_bluetooth/test_button.py @@ -0,0 +1,67 @@ +"""Test Gardena Bluetooth sensor.""" + + +from unittest.mock import Mock, call + +from gardena_bluetooth.const import Reset +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ( + ATTR_ENTITY_ID, + Platform, +) +from homeassistant.core import HomeAssistant + +from . import setup_entry + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_switch_chars(mock_read_char_raw): + """Mock data on device.""" + mock_read_char_raw[Reset.factory_reset.uuid] = b"\x00" + return mock_read_char_raw + + +async def test_setup( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_switch_chars: dict[str, bytes], +) -> None: + """Test setup creates expected entities.""" + + entity_id = "button.mock_title_factory_reset" + coordinator = await setup_entry(hass, mock_entry, [Platform.BUTTON]) + assert hass.states.get(entity_id) == snapshot + + mock_switch_chars[Reset.factory_reset.uuid] = b"\x01" + await coordinator.async_refresh() + assert hass.states.get(entity_id) == snapshot + + +async def test_switching( + hass: HomeAssistant, + mock_entry: MockConfigEntry, + mock_client: Mock, + mock_switch_chars: dict[str, bytes], +) -> None: + """Test switching makes correct calls.""" + + entity_id = "button.mock_title_factory_reset" + await setup_entry(hass, mock_entry, [Platform.BUTTON]) + assert hass.states.get(entity_id) + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert mock_client.write_char.mock_calls == [ + call(Reset.factory_reset, True), + ] From b53eae2846e0fb53bf720a23f14826ef07b56140 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 19 Jul 2023 10:48:32 +0200 Subject: [PATCH 0664/1009] Add WS command for changing thread channels (#94525) --- .../components/otbr/websocket_api.py | 50 +++++++- tests/components/otbr/__init__.py | 4 +- tests/components/otbr/conftest.py | 26 +++- tests/components/otbr/test_init.py | 36 +++--- .../otbr/test_silabs_multiprotocol.py | 20 ++- tests/components/otbr/test_websocket_api.py | 120 ++++++++++++++---- 6 files changed, 200 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/otbr/websocket_api.py b/homeassistant/components/otbr/websocket_api.py index 06bbca3a4abc27..3b631057529ca0 100644 --- a/homeassistant/components/otbr/websocket_api.py +++ b/homeassistant/components/otbr/websocket_api.py @@ -3,11 +3,14 @@ from typing import cast import python_otbr_api -from python_otbr_api import tlv_parser +from python_otbr_api import PENDING_DATASET_DELAY_TIMER, tlv_parser from python_otbr_api.tlv_parser import MeshcopTLVType import voluptuous as vol from homeassistant.components import websocket_api +from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( + is_multiprotocol_url, +) from homeassistant.components.thread import async_add_dataset, async_get_dataset from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -22,6 +25,7 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_info) websocket_api.async_register_command(hass, websocket_create_network) websocket_api.async_register_command(hass, websocket_get_extended_address) + websocket_api.async_register_command(hass, websocket_set_channel) websocket_api.async_register_command(hass, websocket_set_network) @@ -43,7 +47,8 @@ async def websocket_info( data: OTBRData = hass.data[DOMAIN] try: - dataset = await data.get_active_dataset_tlvs() + dataset = await data.get_active_dataset() + dataset_tlvs = await data.get_active_dataset_tlvs() except HomeAssistantError as exc: connection.send_error(msg["id"], "get_dataset_failed", str(exc)) return @@ -52,7 +57,8 @@ async def websocket_info( msg["id"], { "url": data.url, - "active_dataset_tlvs": dataset.hex() if dataset else None, + "active_dataset_tlvs": dataset_tlvs.hex() if dataset_tlvs else None, + "channel": dataset.channel if dataset else None, }, ) @@ -205,3 +211,41 @@ async def websocket_get_extended_address( return connection.send_result(msg["id"], {"extended_address": extended_address.hex()}) + + +@websocket_api.websocket_command( + { + "type": "otbr/set_channel", + vol.Required("channel"): int, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_set_channel( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Set current channel.""" + if DOMAIN not in hass.data: + connection.send_error(msg["id"], "not_loaded", "No OTBR API loaded") + return + + data: OTBRData = hass.data[DOMAIN] + + if is_multiprotocol_url(data.url): + connection.send_error( + msg["id"], + "multiprotocol_enabled", + "Channel change not allowed when in multiprotocol mode", + ) + return + + channel: int = msg["channel"] + delay: float = PENDING_DATASET_DELAY_TIMER / 1000 + + try: + await data.set_channel(channel) + except HomeAssistantError as exc: + connection.send_error(msg["id"], "set_channel_failed", str(exc)) + return + + connection.send_result(msg["id"], {"delay": delay}) diff --git a/tests/components/otbr/__init__.py b/tests/components/otbr/__init__.py index e641f67dfaf645..9f2fd4a4355aa4 100644 --- a/tests/components/otbr/__init__.py +++ b/tests/components/otbr/__init__.py @@ -1,7 +1,7 @@ """Tests for the Open Thread Border Router integration.""" BASE_URL = "http://core-silabs-multiprotocol:8081" -CONFIG_ENTRY_DATA = {"url": "http://core-silabs-multiprotocol:8081"} -CONFIG_ENTRY_DATA_2 = {"url": "http://core-silabs-multiprotocol_2:8081"} +CONFIG_ENTRY_DATA_MULTIPAN = {"url": "http://core-silabs-multiprotocol:8081"} +CONFIG_ENTRY_DATA_THREAD = {"url": "/dev/ttyAMA1"} DATASET_CH15 = bytes.fromhex( "0E080000000000010000000300000F35060004001FFFE00208F642646DA209B1D00708FDF57B5A" diff --git a/tests/components/otbr/conftest.py b/tests/components/otbr/conftest.py index bb3b474519ec60..e7d5ac8980e72c 100644 --- a/tests/components/otbr/conftest.py +++ b/tests/components/otbr/conftest.py @@ -6,16 +6,34 @@ from homeassistant.components import otbr from homeassistant.core import HomeAssistant -from . import CONFIG_ENTRY_DATA, DATASET_CH16 +from . import CONFIG_ENTRY_DATA_MULTIPAN, CONFIG_ENTRY_DATA_THREAD, DATASET_CH16 from tests.common import MockConfigEntry -@pytest.fixture(name="otbr_config_entry") -async def otbr_config_entry_fixture(hass): +@pytest.fixture(name="otbr_config_entry_multipan") +async def otbr_config_entry_multipan_fixture(hass): """Mock Open Thread Border Router config entry.""" config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, + data=CONFIG_ENTRY_DATA_MULTIPAN, + domain=otbr.DOMAIN, + options={}, + title="Open Thread Border Router", + ) + config_entry.add_to_hass(hass) + with patch( + "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 + ), patch( + "homeassistant.components.otbr.util.compute_pskc" + ): # Patch to speed up tests + assert await hass.config_entries.async_setup(config_entry.entry_id) + + +@pytest.fixture(name="otbr_config_entry_thread") +async def otbr_config_entry_thread_fixture(hass): + """Mock Open Thread Border Router config entry.""" + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA_THREAD, domain=otbr.DOMAIN, options={}, title="Open Thread Border Router", diff --git a/tests/components/otbr/test_init.py b/tests/components/otbr/test_init.py index 4ec99818b28bba..49694cf558541a 100644 --- a/tests/components/otbr/test_init.py +++ b/tests/components/otbr/test_init.py @@ -15,8 +15,8 @@ from . import ( BASE_URL, - CONFIG_ENTRY_DATA, - CONFIG_ENTRY_DATA_2, + CONFIG_ENTRY_DATA_MULTIPAN, + CONFIG_ENTRY_DATA_THREAD, DATASET_CH15, DATASET_CH16, DATASET_INSECURE_NW_KEY, @@ -38,7 +38,7 @@ async def test_import_dataset(hass: HomeAssistant) -> None: issue_registry = ir.async_get(hass) config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, + data=CONFIG_ENTRY_DATA_MULTIPAN, domain=otbr.DOMAIN, options={}, title="My OTBR", @@ -74,7 +74,7 @@ async def test_import_share_radio_channel_collision( multiprotocol_addon_manager_mock.async_get_channel.return_value = 15 config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, + data=CONFIG_ENTRY_DATA_MULTIPAN, domain=otbr.DOMAIN, options={}, title="My OTBR", @@ -107,7 +107,7 @@ async def test_import_share_radio_no_channel_collision( multiprotocol_addon_manager_mock.async_get_channel.return_value = 15 config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, + data=CONFIG_ENTRY_DATA_MULTIPAN, domain=otbr.DOMAIN, options={}, title="My OTBR", @@ -138,7 +138,7 @@ async def test_import_insecure_dataset(hass: HomeAssistant, dataset: bytes) -> N issue_registry = ir.async_get(hass) config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, + data=CONFIG_ENTRY_DATA_MULTIPAN, domain=otbr.DOMAIN, options={}, title="My OTBR", @@ -169,7 +169,7 @@ async def test_config_entry_not_ready(hass: HomeAssistant, error) -> None: """Test raising ConfigEntryNotReady .""" config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, + data=CONFIG_ENTRY_DATA_MULTIPAN, domain=otbr.DOMAIN, options={}, title="My OTBR", @@ -182,7 +182,7 @@ async def test_config_entry_not_ready(hass: HomeAssistant, error) -> None: async def test_config_entry_update(hass: HomeAssistant) -> None: """Test update config entry settings.""" config_entry = MockConfigEntry( - data=CONFIG_ENTRY_DATA, + data=CONFIG_ENTRY_DATA_MULTIPAN, domain=otbr.DOMAIN, options={}, title="My OTBR", @@ -193,10 +193,10 @@ async def test_config_entry_update(hass: HomeAssistant) -> None: with patch("python_otbr_api.OTBR", return_value=mock_api) as mock_otrb_api: assert await hass.config_entries.async_setup(config_entry.entry_id) - mock_otrb_api.assert_called_once_with(CONFIG_ENTRY_DATA["url"], ANY, ANY) + mock_otrb_api.assert_called_once_with(CONFIG_ENTRY_DATA_MULTIPAN["url"], ANY, ANY) new_config_entry_data = {"url": "http://core-silabs-multiprotocol:8082"} - assert CONFIG_ENTRY_DATA["url"] != new_config_entry_data["url"] + assert CONFIG_ENTRY_DATA_MULTIPAN["url"] != new_config_entry_data["url"] with patch("python_otbr_api.OTBR", return_value=mock_api) as mock_otrb_api: hass.config_entries.async_update_entry(config_entry, data=new_config_entry_data) await hass.async_block_till_done() @@ -205,7 +205,7 @@ async def test_config_entry_update(hass: HomeAssistant) -> None: async def test_remove_entry( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry_multipan ) -> None: """Test async_get_active_dataset_tlvs after removing the config entry.""" @@ -221,7 +221,7 @@ async def test_remove_entry( async def test_get_active_dataset_tlvs( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry_multipan ) -> None: """Test async_get_active_dataset_tlvs.""" @@ -239,7 +239,7 @@ async def test_get_active_dataset_tlvs( async def test_get_active_dataset_tlvs_empty( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry_multipan ) -> None: """Test async_get_active_dataset_tlvs.""" @@ -255,7 +255,7 @@ async def test_get_active_dataset_tlvs_addon_not_installed(hass: HomeAssistant) async def test_get_active_dataset_tlvs_404( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry_multipan ) -> None: """Test async_get_active_dataset_tlvs with error.""" @@ -265,7 +265,7 @@ async def test_get_active_dataset_tlvs_404( async def test_get_active_dataset_tlvs_201( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry_multipan ) -> None: """Test async_get_active_dataset_tlvs with error.""" @@ -275,7 +275,7 @@ async def test_get_active_dataset_tlvs_201( async def test_get_active_dataset_tlvs_invalid( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry_multipan ) -> None: """Test async_get_active_dataset_tlvs with error.""" @@ -290,13 +290,13 @@ async def test_remove_extra_entries( """Test we remove additional config entries.""" config_entry1 = MockConfigEntry( - data=CONFIG_ENTRY_DATA, + data=CONFIG_ENTRY_DATA_MULTIPAN, domain=otbr.DOMAIN, options={}, title="Open Thread Border Router", ) config_entry2 = MockConfigEntry( - data=CONFIG_ENTRY_DATA_2, + data=CONFIG_ENTRY_DATA_THREAD, domain=otbr.DOMAIN, options={}, title="Open Thread Border Router", diff --git a/tests/components/otbr/test_silabs_multiprotocol.py b/tests/components/otbr/test_silabs_multiprotocol.py index 8dd07db6f22e7d..83416ae297d831 100644 --- a/tests/components/otbr/test_silabs_multiprotocol.py +++ b/tests/components/otbr/test_silabs_multiprotocol.py @@ -31,7 +31,9 @@ ) -async def test_async_change_channel(hass: HomeAssistant, otbr_config_entry) -> None: +async def test_async_change_channel( + hass: HomeAssistant, otbr_config_entry_multipan +) -> None: """Test test_async_change_channel.""" store = await dataset_store.async_get_store(hass) @@ -55,7 +57,7 @@ async def test_async_change_channel(hass: HomeAssistant, otbr_config_entry) -> N async def test_async_change_channel_no_pending( - hass: HomeAssistant, otbr_config_entry + hass: HomeAssistant, otbr_config_entry_multipan ) -> None: """Test test_async_change_channel when the pending dataset already expired.""" @@ -83,7 +85,7 @@ async def test_async_change_channel_no_pending( async def test_async_change_channel_no_update( - hass: HomeAssistant, otbr_config_entry + hass: HomeAssistant, otbr_config_entry_multipan ) -> None: """Test test_async_change_channel when we didn't get a dataset from the OTBR.""" @@ -112,7 +114,9 @@ async def test_async_change_channel_no_otbr(hass: HomeAssistant) -> None: mock_set_channel.assert_not_awaited() -async def test_async_get_channel(hass: HomeAssistant, otbr_config_entry) -> None: +async def test_async_get_channel( + hass: HomeAssistant, otbr_config_entry_multipan +) -> None: """Test test_async_get_channel.""" with patch( @@ -124,7 +128,7 @@ async def test_async_get_channel(hass: HomeAssistant, otbr_config_entry) -> None async def test_async_get_channel_no_dataset( - hass: HomeAssistant, otbr_config_entry + hass: HomeAssistant, otbr_config_entry_multipan ) -> None: """Test test_async_get_channel.""" @@ -136,7 +140,9 @@ async def test_async_get_channel_no_dataset( mock_get_active_dataset.assert_awaited_once_with() -async def test_async_get_channel_error(hass: HomeAssistant, otbr_config_entry) -> None: +async def test_async_get_channel_error( + hass: HomeAssistant, otbr_config_entry_multipan +) -> None: """Test test_async_get_channel.""" with patch( @@ -160,7 +166,7 @@ async def test_async_get_channel_no_otbr(hass: HomeAssistant) -> None: [(OTBR_MULTIPAN_URL, True), (OTBR_NON_MULTIPAN_URL, False)], ) async def test_async_using_multipan( - hass: HomeAssistant, otbr_config_entry, url: str, expected: bool + hass: HomeAssistant, otbr_config_entry_multipan, url: str, expected: bool ) -> None: """Test async_change_channel when otbr is not configured.""" data: otbr.OTBRData = hass.data[otbr.DOMAIN] diff --git a/tests/components/otbr/test_websocket_api.py b/tests/components/otbr/test_websocket_api.py index 65bec9e8408e6b..b5dd7aa62c4315 100644 --- a/tests/components/otbr/test_websocket_api.py +++ b/tests/components/otbr/test_websocket_api.py @@ -23,20 +23,23 @@ async def websocket_client(hass, hass_ws_client): async def test_get_info( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - otbr_config_entry, + otbr_config_entry_multipan, websocket_client, ) -> None: """Test async_get_info.""" - aioclient_mock.get(f"{BASE_URL}/node/dataset/active", text=DATASET_CH16.hex()) - - await websocket_client.send_json_auto_id({"type": "otbr/info"}) + with patch( + "python_otbr_api.OTBR.get_active_dataset", + return_value=python_otbr_api.ActiveDataSet(channel=16), + ), patch("python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16): + await websocket_client.send_json_auto_id({"type": "otbr/info"}) + msg = await websocket_client.receive_json() - msg = await websocket_client.receive_json() assert msg["success"] assert msg["result"] == { "url": BASE_URL, "active_dataset_tlvs": DATASET_CH16.hex().lower(), + "channel": 16, } @@ -58,12 +61,12 @@ async def test_get_info_no_entry( async def test_get_info_fetch_fails( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - otbr_config_entry, + otbr_config_entry_multipan, websocket_client, ) -> None: """Test async_get_info.""" with patch( - "python_otbr_api.OTBR.get_active_dataset_tlvs", + "python_otbr_api.OTBR.get_active_dataset", side_effect=python_otbr_api.OTBRError, ): await websocket_client.send_json_auto_id({"type": "otbr/info"}) @@ -76,7 +79,7 @@ async def test_get_info_fetch_fails( async def test_create_network( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - otbr_config_entry, + otbr_config_entry_multipan, websocket_client, ) -> None: """Test create network.""" @@ -127,7 +130,7 @@ async def test_create_network_no_entry( async def test_create_network_fails_1( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - otbr_config_entry, + otbr_config_entry_multipan, websocket_client, ) -> None: """Test create network.""" @@ -145,7 +148,7 @@ async def test_create_network_fails_1( async def test_create_network_fails_2( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - otbr_config_entry, + otbr_config_entry_multipan, websocket_client, ) -> None: """Test create network.""" @@ -165,7 +168,7 @@ async def test_create_network_fails_2( async def test_create_network_fails_3( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - otbr_config_entry, + otbr_config_entry_multipan, websocket_client, ) -> None: """Test create network.""" @@ -187,7 +190,7 @@ async def test_create_network_fails_3( async def test_create_network_fails_4( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - otbr_config_entry, + otbr_config_entry_multipan, websocket_client, ) -> None: """Test create network.""" @@ -209,7 +212,7 @@ async def test_create_network_fails_4( async def test_create_network_fails_5( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - otbr_config_entry, + otbr_config_entry_multipan, websocket_client, ) -> None: """Test create network.""" @@ -228,7 +231,7 @@ async def test_create_network_fails_5( async def test_create_network_fails_6( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - otbr_config_entry, + otbr_config_entry_multipan, websocket_client, ) -> None: """Test create network.""" @@ -248,7 +251,7 @@ async def test_create_network_fails_6( async def test_set_network( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - otbr_config_entry, + otbr_config_entry_multipan, websocket_client, ) -> None: """Test set network.""" @@ -303,7 +306,7 @@ async def test_set_network_channel_conflict( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, multiprotocol_addon_manager_mock, - otbr_config_entry, + otbr_config_entry_multipan, websocket_client, ) -> None: """Test set network.""" @@ -329,7 +332,7 @@ async def test_set_network_channel_conflict( async def test_set_network_unknown_dataset( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - otbr_config_entry, + otbr_config_entry_multipan, websocket_client, ) -> None: """Test set network.""" @@ -350,7 +353,7 @@ async def test_set_network_unknown_dataset( async def test_set_network_fails_1( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - otbr_config_entry, + otbr_config_entry_multipan, websocket_client, ) -> None: """Test set network.""" @@ -377,7 +380,7 @@ async def test_set_network_fails_1( async def test_set_network_fails_2( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - otbr_config_entry, + otbr_config_entry_multipan, websocket_client, ) -> None: """Test set network.""" @@ -406,7 +409,7 @@ async def test_set_network_fails_2( async def test_set_network_fails_3( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - otbr_config_entry, + otbr_config_entry_multipan, websocket_client, ) -> None: """Test set network.""" @@ -435,7 +438,7 @@ async def test_set_network_fails_3( async def test_get_extended_address( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - otbr_config_entry, + otbr_config_entry_multipan, websocket_client, ) -> None: """Test get extended address.""" @@ -469,7 +472,7 @@ async def test_get_extended_address_no_entry( async def test_get_extended_address_fetch_fails( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - otbr_config_entry, + otbr_config_entry_multipan, websocket_client, ) -> None: """Test get extended address.""" @@ -482,3 +485,76 @@ async def test_get_extended_address_fetch_fails( assert not msg["success"] assert msg["error"]["code"] == "get_extended_address_failed" + + +async def test_set_channel( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + otbr_config_entry_thread, + websocket_client, +) -> None: + """Test set channel.""" + + with patch("python_otbr_api.OTBR.set_channel"): + await websocket_client.send_json_auto_id( + {"type": "otbr/set_channel", "channel": 12} + ) + msg = await websocket_client.receive_json() + + assert msg["success"] + assert msg["result"] == {"delay": 300.0} + + +async def test_set_channel_multiprotocol( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + otbr_config_entry_multipan, + websocket_client, +) -> None: + """Test set channel.""" + + with patch("python_otbr_api.OTBR.set_channel"): + await websocket_client.send_json_auto_id( + {"type": "otbr/set_channel", "channel": 12} + ) + msg = await websocket_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "multiprotocol_enabled" + + +async def test_set_channel_no_entry( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test set channel.""" + await async_setup_component(hass, "otbr", {}) + websocket_client = await hass_ws_client(hass) + await websocket_client.send_json_auto_id( + {"type": "otbr/set_channel", "channel": 12} + ) + + msg = await websocket_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == "not_loaded" + + +async def test_set_channel_fails( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + otbr_config_entry_thread, + websocket_client, +) -> None: + """Test set channel.""" + with patch( + "python_otbr_api.OTBR.set_channel", + side_effect=python_otbr_api.OTBRError, + ): + await websocket_client.send_json_auto_id( + {"type": "otbr/set_channel", "channel": 12} + ) + msg = await websocket_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "set_channel_failed" From e39187423f4292593859644db413fafa194490bf Mon Sep 17 00:00:00 2001 From: Renier Moorcroft <66512715+RenierM26@users.noreply.github.com> Date: Wed, 19 Jul 2023 10:56:11 +0200 Subject: [PATCH 0665/1009] Ezviz NumberEntity 1st update only when enabled (#96587) * Initial commit * Initial commit * Fix async_aded_to_hass --- homeassistant/components/ezviz/number.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ezviz/number.py b/homeassistant/components/ezviz/number.py index 074685c69f978e..77c5146cefa69a 100644 --- a/homeassistant/components/ezviz/number.py +++ b/homeassistant/components/ezviz/number.py @@ -66,14 +66,11 @@ async def async_setup_entry( ] async_add_entities( - [ - EzvizSensor(coordinator, camera, value, entry.entry_id) - for camera in coordinator.data - for capibility, value in coordinator.data[camera]["supportExt"].items() - if capibility == NUMBER_TYPE.supported_ext - if value in NUMBER_TYPE.supported_ext_value - ], - update_before_add=True, + EzvizSensor(coordinator, camera, value, entry.entry_id) + for camera in coordinator.data + for capibility, value in coordinator.data[camera]["supportExt"].items() + if capibility == NUMBER_TYPE.supported_ext + if value in NUMBER_TYPE.supported_ext_value ) @@ -98,6 +95,10 @@ def __init__( self.config_entry_id = config_entry_id self.sensor_value: int | None = None + async def async_added_to_hass(self) -> None: + """Run when about to be added to hass.""" + self.schedule_update_ha_state(True) + @property def native_value(self) -> float | None: """Return the state of the entity.""" From f4bc32ea089b3a6709e0e103bad23d8308fabd43 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 19 Jul 2023 11:02:42 +0200 Subject: [PATCH 0666/1009] Move Dynalite configuration panel to config entry (#96853) --- homeassistant/components/dynalite/panel.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/dynalite/panel.py b/homeassistant/components/dynalite/panel.py index e7a0890033c3f6..b7020367f7413c 100644 --- a/homeassistant/components/dynalite/panel.py +++ b/homeassistant/components/dynalite/panel.py @@ -108,9 +108,8 @@ async def async_register_dynalite_frontend(hass: HomeAssistant): await panel_custom.async_register_panel( hass=hass, frontend_url_path=DOMAIN, + config_panel_domain=DOMAIN, webcomponent_name="dynalite-panel", - sidebar_title=DOMAIN.capitalize(), - sidebar_icon="mdi:power", module_url=f"{URL_BASE}/entrypoint-{build_id}.js", embed_iframe=True, require_admin=True, From 90bdbf503a399e822c0d984f5c83c2c098f0383a Mon Sep 17 00:00:00 2001 From: Arjan <44190435+vingerha@users.noreply.github.com> Date: Wed, 19 Jul 2023 11:14:09 +0200 Subject: [PATCH 0667/1009] Add humidity to meteo_france weather forecast (#96524) Add humidity to forecast figures --- homeassistant/components/meteo_france/sensor.py | 7 +++++++ homeassistant/components/meteo_france/weather.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 8c27f2970a3f27..89faf6d80eb7fc 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -137,6 +137,13 @@ class MeteoFranceSensorEntityDescription( entity_registry_enabled_default=False, data_path="today_forecast:weather12H:desc", ), + MeteoFranceSensorEntityDescription( + key="humidity", + name="Humidity", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:water-percent", + data_path="current_forecast:humidity", + ), ) SENSOR_TYPES_RAIN: tuple[MeteoFranceSensorEntityDescription, ...] = ( diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 7709ba0a63880d..165cefc92402f2 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -6,6 +6,7 @@ from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, + ATTR_FORECAST_HUMIDITY, ATTR_FORECAST_NATIVE_PRECIPITATION, ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_TEMP_LOW, @@ -171,6 +172,7 @@ def forecast(self): ATTR_FORECAST_CONDITION: format_condition( forecast["weather"]["desc"] ), + ATTR_FORECAST_HUMIDITY: forecast["humidity"], ATTR_FORECAST_NATIVE_TEMP: forecast["T"]["value"], ATTR_FORECAST_NATIVE_PRECIPITATION: forecast["rain"].get("1h"), ATTR_FORECAST_NATIVE_WIND_SPEED: forecast["wind"]["speed"], @@ -192,6 +194,7 @@ def forecast(self): ATTR_FORECAST_CONDITION: format_condition( forecast["weather12H"]["desc"] ), + ATTR_FORECAST_HUMIDITY: forecast["humidity"]["max"], ATTR_FORECAST_NATIVE_TEMP: forecast["T"]["max"], ATTR_FORECAST_NATIVE_TEMP_LOW: forecast["T"]["min"], ATTR_FORECAST_NATIVE_PRECIPITATION: forecast["precipitation"][ From 6ffb1c3c2de0af2c928e36923963fd4b76b16224 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 19 Jul 2023 11:19:57 +0200 Subject: [PATCH 0668/1009] Remove version string from Ecowitt name (#96498) * Remove version string from station name * Use model as name --- homeassistant/components/ecowitt/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ecowitt/entity.py b/homeassistant/components/ecowitt/entity.py index ca5e14b6d7b295..76bd89af3d53d0 100644 --- a/homeassistant/components/ecowitt/entity.py +++ b/homeassistant/components/ecowitt/entity.py @@ -25,7 +25,7 @@ def __init__(self, sensor: EcoWittSensor) -> None: identifiers={ (DOMAIN, sensor.station.key), }, - name=sensor.station.station, + name=sensor.station.model, model=sensor.station.model, sw_version=sensor.station.version, ) From efbd82b5fb0218a322cd193755f30342dcd51362 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 19 Jul 2023 12:43:15 +0200 Subject: [PATCH 0669/1009] Add entity translations to Tuya (#96842) --- .../components/tuya/binary_sensor.py | 27 +- homeassistant/components/tuya/button.py | 12 +- homeassistant/components/tuya/cover.py | 16 +- homeassistant/components/tuya/light.py | 28 +- homeassistant/components/tuya/number.py | 76 +-- homeassistant/components/tuya/scene.py | 7 +- homeassistant/components/tuya/select.py | 63 +- homeassistant/components/tuya/sensor.py | 255 ++++---- homeassistant/components/tuya/strings.json | 599 +++++++++++++++++- homeassistant/components/tuya/switch.py | 208 +++--- 10 files changed, 912 insertions(+), 379 deletions(-) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index a392a338aba6c7..c57a37365ed87e 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -51,68 +51,64 @@ class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription): "dgnbj": ( TuyaBinarySensorEntityDescription( key=DPCode.GAS_SENSOR_STATE, - name="Gas", icon="mdi:gas-cylinder", device_class=BinarySensorDeviceClass.GAS, on_value="alarm", ), TuyaBinarySensorEntityDescription( key=DPCode.CH4_SENSOR_STATE, - name="Methane", + translation_key="methane", device_class=BinarySensorDeviceClass.GAS, on_value="alarm", ), TuyaBinarySensorEntityDescription( key=DPCode.VOC_STATE, - name="Volatile organic compound", + translation_key="voc", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), TuyaBinarySensorEntityDescription( key=DPCode.PM25_STATE, - name="Particulate matter 2.5 µm", + translation_key="pm25", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), TuyaBinarySensorEntityDescription( key=DPCode.CO_STATE, - name="Carbon monoxide", + translation_key="carbon_monoxide", icon="mdi:molecule-co", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), TuyaBinarySensorEntityDescription( key=DPCode.CO2_STATE, + translation_key="carbon_dioxide", icon="mdi:molecule-co2", - name="Carbon dioxide", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), TuyaBinarySensorEntityDescription( key=DPCode.CH2O_STATE, - name="Formaldehyde", + translation_key="formaldehyde", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), TuyaBinarySensorEntityDescription( key=DPCode.DOORCONTACT_STATE, - name="Door", device_class=BinarySensorDeviceClass.DOOR, ), TuyaBinarySensorEntityDescription( key=DPCode.WATERSENSOR_STATE, - name="Water leak", device_class=BinarySensorDeviceClass.MOISTURE, on_value="alarm", ), TuyaBinarySensorEntityDescription( key=DPCode.PRESSURE_STATE, - name="Pressure", + translation_key="pressure", on_value="alarm", ), TuyaBinarySensorEntityDescription( key=DPCode.SMOKE_SENSOR_STATE, - name="Smoke", icon="mdi:smoke-detector", device_class=BinarySensorDeviceClass.SMOKE, on_value="alarm", @@ -149,7 +145,7 @@ class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription): "cwwsq": ( TuyaBinarySensorEntityDescription( key=DPCode.FEED_STATE, - name="Feeding", + translation_key="feeding", icon="mdi:information", on_value="feeding", ), @@ -215,7 +211,6 @@ class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription): "ldcg": ( TuyaBinarySensorEntityDescription( key=DPCode.TEMPER_ALARM, - name="Tamper", device_class=BinarySensorDeviceClass.TAMPER, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -290,7 +285,6 @@ class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription): "wkf": ( TuyaBinarySensorEntityDescription( key=DPCode.WINDOW_STATE, - name="Window", device_class=BinarySensorDeviceClass.WINDOW, on_value="opened", ), @@ -328,21 +322,20 @@ class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription): TuyaBinarySensorEntityDescription( key=f"{DPCode.SHOCK_STATE}_vibration", dpcode=DPCode.SHOCK_STATE, - name="Vibration", device_class=BinarySensorDeviceClass.VIBRATION, on_value="vibration", ), TuyaBinarySensorEntityDescription( key=f"{DPCode.SHOCK_STATE}_drop", dpcode=DPCode.SHOCK_STATE, - name="Drop", + translation_key="drop", icon="mdi:icon=package-down", on_value="drop", ), TuyaBinarySensorEntityDescription( key=f"{DPCode.SHOCK_STATE}_tilt", dpcode=DPCode.SHOCK_STATE, - name="Tilt", + translation_key="tilt", icon="mdi:spirit-level", on_value="tilt", ), diff --git a/homeassistant/components/tuya/button.py b/homeassistant/components/tuya/button.py index 64d405ee5adbbe..4c73b70c29aac3 100644 --- a/homeassistant/components/tuya/button.py +++ b/homeassistant/components/tuya/button.py @@ -22,31 +22,31 @@ "sd": ( ButtonEntityDescription( key=DPCode.RESET_DUSTER_CLOTH, - name="Reset duster cloth", + translation_key="reset_duster_cloth", icon="mdi:restart", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_EDGE_BRUSH, - name="Reset edge brush", + translation_key="reset_edge_brush", icon="mdi:restart", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_FILTER, - name="Reset filter", + translation_key="reset_filter", icon="mdi:air-filter", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_MAP, - name="Reset map", + translation_key="reset_map", icon="mdi:map-marker-remove", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_ROLL_BRUSH, - name="Reset roll brush", + translation_key="reset_roll_brush", icon="mdi:restart", entity_category=EntityCategory.CONFIG, ), @@ -56,7 +56,7 @@ "hxd": ( ButtonEntityDescription( key=DPCode.SWITCH_USB6, - name="Snooze", + translation_key="snooze", icon="mdi:sleep", ), ), diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index 5bb9c794ca4ed6..3505bbf9f22bb2 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -44,7 +44,6 @@ class TuyaCoverEntityDescription(CoverEntityDescription): "cl": ( TuyaCoverEntityDescription( key=DPCode.CONTROL, - name="Curtain", current_state=DPCode.SITUATION_SET, current_position=(DPCode.PERCENT_CONTROL, DPCode.PERCENT_STATE), set_position=DPCode.PERCENT_CONTROL, @@ -52,21 +51,20 @@ class TuyaCoverEntityDescription(CoverEntityDescription): ), TuyaCoverEntityDescription( key=DPCode.CONTROL_2, - name="Curtain 2", + translation_key="curtain_2", current_position=DPCode.PERCENT_STATE_2, set_position=DPCode.PERCENT_CONTROL_2, device_class=CoverDeviceClass.CURTAIN, ), TuyaCoverEntityDescription( key=DPCode.CONTROL_3, - name="Curtain 3", + translation_key="curtain_3", current_position=DPCode.PERCENT_STATE_3, set_position=DPCode.PERCENT_CONTROL_3, device_class=CoverDeviceClass.CURTAIN, ), TuyaCoverEntityDescription( key=DPCode.MACH_OPERATE, - name="Curtain", current_position=DPCode.POSITION, set_position=DPCode.POSITION, device_class=CoverDeviceClass.CURTAIN, @@ -78,7 +76,6 @@ class TuyaCoverEntityDescription(CoverEntityDescription): # It is used by the Kogan Smart Blinds Driver TuyaCoverEntityDescription( key=DPCode.SWITCH_1, - name="Blind", current_position=DPCode.PERCENT_CONTROL, set_position=DPCode.PERCENT_CONTROL, device_class=CoverDeviceClass.BLIND, @@ -89,21 +86,21 @@ class TuyaCoverEntityDescription(CoverEntityDescription): "ckmkzq": ( TuyaCoverEntityDescription( key=DPCode.SWITCH_1, - name="Door", + translation_key="door", current_state=DPCode.DOORCONTACT_STATE, current_state_inverse=True, device_class=CoverDeviceClass.GARAGE, ), TuyaCoverEntityDescription( key=DPCode.SWITCH_2, - name="Door 2", + translation_key="door_2", current_state=DPCode.DOORCONTACT_STATE_2, current_state_inverse=True, device_class=CoverDeviceClass.GARAGE, ), TuyaCoverEntityDescription( key=DPCode.SWITCH_3, - name="Door 3", + translation_key="door_3", current_state=DPCode.DOORCONTACT_STATE_3, current_state_inverse=True, device_class=CoverDeviceClass.GARAGE, @@ -114,14 +111,13 @@ class TuyaCoverEntityDescription(CoverEntityDescription): "clkg": ( TuyaCoverEntityDescription( key=DPCode.CONTROL, - name="Curtain", current_position=DPCode.PERCENT_CONTROL, set_position=DPCode.PERCENT_CONTROL, device_class=CoverDeviceClass.CURTAIN, ), TuyaCoverEntityDescription( key=DPCode.CONTROL_2, - name="Curtain 2", + translation_key="curtain_2", current_position=DPCode.PERCENT_CONTROL_2, set_position=DPCode.PERCENT_CONTROL_2, device_class=CoverDeviceClass.CURTAIN, diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 3ab4c3568c4d67..5c2663d251c407 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -70,7 +70,7 @@ class TuyaLightEntityDescription(LightEntityDescription): "clkg": ( TuyaLightEntityDescription( key=DPCode.SWITCH_BACKLIGHT, - name="Backlight", + translation_key="backlight", entity_category=EntityCategory.CONFIG, ), ), @@ -114,7 +114,7 @@ class TuyaLightEntityDescription(LightEntityDescription): # Based on multiple reports: manufacturer customized Dimmer 2 switches TuyaLightEntityDescription( key=DPCode.SWITCH_1, - name="Light", + translation_key="light", brightness=DPCode.BRIGHT_VALUE_1, ), ), @@ -175,7 +175,7 @@ class TuyaLightEntityDescription(LightEntityDescription): "kg": ( TuyaLightEntityDescription( key=DPCode.SWITCH_BACKLIGHT, - name="Backlight", + translation_key="backlight", entity_category=EntityCategory.CONFIG, ), ), @@ -184,7 +184,7 @@ class TuyaLightEntityDescription(LightEntityDescription): "kj": ( TuyaLightEntityDescription( key=DPCode.LIGHT, - name="Backlight", + translation_key="backlight", entity_category=EntityCategory.CONFIG, ), ), @@ -193,7 +193,7 @@ class TuyaLightEntityDescription(LightEntityDescription): "kt": ( TuyaLightEntityDescription( key=DPCode.LIGHT, - name="Backlight", + translation_key="backlight", entity_category=EntityCategory.CONFIG, ), ), @@ -226,7 +226,7 @@ class TuyaLightEntityDescription(LightEntityDescription): "qn": ( TuyaLightEntityDescription( key=DPCode.LIGHT, - name="Backlight", + translation_key="backlight", entity_category=EntityCategory.CONFIG, ), ), @@ -249,21 +249,21 @@ class TuyaLightEntityDescription(LightEntityDescription): "tgkg": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED_1, - name="Light", + translation_key="light", brightness=DPCode.BRIGHT_VALUE_1, brightness_max=DPCode.BRIGHTNESS_MAX_1, brightness_min=DPCode.BRIGHTNESS_MIN_1, ), TuyaLightEntityDescription( key=DPCode.SWITCH_LED_2, - name="Light 2", + translation_key="light_2", brightness=DPCode.BRIGHT_VALUE_2, brightness_max=DPCode.BRIGHTNESS_MAX_2, brightness_min=DPCode.BRIGHTNESS_MIN_2, ), TuyaLightEntityDescription( key=DPCode.SWITCH_LED_3, - name="Light 3", + translation_key="light_3", brightness=DPCode.BRIGHT_VALUE_3, brightness_max=DPCode.BRIGHTNESS_MAX_3, brightness_min=DPCode.BRIGHTNESS_MIN_3, @@ -274,19 +274,19 @@ class TuyaLightEntityDescription(LightEntityDescription): "tgq": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, - name="Light", + translation_key="light", brightness=(DPCode.BRIGHT_VALUE_V2, DPCode.BRIGHT_VALUE), brightness_max=DPCode.BRIGHTNESS_MAX_1, brightness_min=DPCode.BRIGHTNESS_MIN_1, ), TuyaLightEntityDescription( key=DPCode.SWITCH_LED_1, - name="Light", + translation_key="light", brightness=DPCode.BRIGHT_VALUE_1, ), TuyaLightEntityDescription( key=DPCode.SWITCH_LED_2, - name="Light 2", + translation_key="light_2", brightness=DPCode.BRIGHT_VALUE_2, ), ), @@ -295,7 +295,7 @@ class TuyaLightEntityDescription(LightEntityDescription): "hxd": ( TuyaLightEntityDescription( key=DPCode.SWITCH_LED, - name="Light", + translation_key="light", brightness=(DPCode.BRIGHT_VALUE_V2, DPCode.BRIGHT_VALUE), brightness_max=DPCode.BRIGHTNESS_MAX_1, brightness_min=DPCode.BRIGHTNESS_MIN_1, @@ -326,7 +326,7 @@ class TuyaLightEntityDescription(LightEntityDescription): ), TuyaLightEntityDescription( key=DPCode.SWITCH_NIGHT_LIGHT, - name="Night light", + translation_key="night_light", ), ), # Remote Control diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 4430172e9a72f3..5e7bdcc260a11b 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -27,7 +27,7 @@ "dgnbj": ( NumberEntityDescription( key=DPCode.ALARM_TIME, - name="Time", + translation_key="time", entity_category=EntityCategory.CONFIG, ), ), @@ -36,35 +36,35 @@ "bh": ( NumberEntityDescription( key=DPCode.TEMP_SET, - name="Temperature", + translation_key="temperature", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_SET_F, - name="Temperature", + translation_key="temperature", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_BOILING_C, - name="Temperature after boiling", + translation_key="temperature_after_boiling", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_BOILING_F, - name="Temperature after boiling", + translation_key="temperature_after_boiling", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.WARM_TIME, - name="Heat preservation time", + translation_key="heat_preservation_time", icon="mdi:timer", entity_category=EntityCategory.CONFIG, ), @@ -74,12 +74,12 @@ "cwwsq": ( NumberEntityDescription( key=DPCode.MANUAL_FEED, - name="Feed", + translation_key="feed", icon="mdi:bowl", ), NumberEntityDescription( key=DPCode.VOICE_TIMES, - name="Voice times", + translation_key="voice_times", icon="mdi:microphone", ), ), @@ -88,18 +88,18 @@ "hps": ( NumberEntityDescription( key=DPCode.SENSITIVITY, - name="Sensitivity", + translation_key="sensitivity", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.NEAR_DETECTION, - name="Near detection", + translation_key="near_detection", icon="mdi:signal-distance-variant", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.FAR_DETECTION, - name="Far detection", + translation_key="far_detection", icon="mdi:signal-distance-variant", entity_category=EntityCategory.CONFIG, ), @@ -109,26 +109,26 @@ "kfj": ( NumberEntityDescription( key=DPCode.WATER_SET, - name="Water level", + translation_key="water_level", icon="mdi:cup-water", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_SET, - name="Temperature", + translation_key="temperature", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.WARM_TIME, - name="Heat preservation time", + translation_key="heat_preservation_time", icon="mdi:timer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.POWDER_SET, - name="Powder", + translation_key="powder", entity_category=EntityCategory.CONFIG, ), ), @@ -137,20 +137,20 @@ "mzj": ( NumberEntityDescription( key=DPCode.COOK_TEMPERATURE, - name="Cook temperature", + translation_key="cook_temperature", icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.COOK_TIME, - name="Cook time", + translation_key="cook_time", icon="mdi:timer", native_unit_of_measurement=UnitOfTime.MINUTES, entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.CLOUD_RECIPE_NUMBER, - name="Cloud recipe", + translation_key="cloud_recipe", entity_category=EntityCategory.CONFIG, ), ), @@ -159,7 +159,7 @@ "sd": ( NumberEntityDescription( key=DPCode.VOLUME_SET, - name="Volume", + translation_key="volume", icon="mdi:volume-high", entity_category=EntityCategory.CONFIG, ), @@ -169,7 +169,7 @@ "sgbj": ( NumberEntityDescription( key=DPCode.ALARM_TIME, - name="Time", + translation_key="time", entity_category=EntityCategory.CONFIG, ), ), @@ -178,7 +178,7 @@ "sp": ( NumberEntityDescription( key=DPCode.BASIC_DEVICE_VOLUME, - name="Volume", + translation_key="volume", icon="mdi:volume-high", entity_category=EntityCategory.CONFIG, ), @@ -188,37 +188,37 @@ "tgkg": ( NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_1, - name="Minimum brightness", + translation_key="minimum_brightness", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_1, - name="Maximum brightness", + translation_key="maximum_brightness", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_2, - name="Minimum brightness 2", + translation_key="minimum_brightness_2", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_2, - name="Maximum brightness 2", + translation_key="maximum_brightness_2", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_3, - name="Minimum brightness 3", + translation_key="minimum_brightness_3", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_3, - name="Maximum brightness 3", + translation_key="maximum_brightness_3", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), @@ -228,25 +228,25 @@ "tgq": ( NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_1, - name="Minimum brightness", + translation_key="minimum_brightness", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_1, - name="Maximum brightness", + translation_key="maximum_brightness", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_2, - name="Minimum brightness 2", + translation_key="minimum_brightness_2", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_2, - name="Maximum brightness 2", + translation_key="maximum_brightness_2", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), @@ -256,7 +256,7 @@ "zd": ( NumberEntityDescription( key=DPCode.SENSITIVITY, - name="Sensitivity", + translation_key="sensitivity", entity_category=EntityCategory.CONFIG, ), ), @@ -264,21 +264,21 @@ "szjqr": ( NumberEntityDescription( key=DPCode.ARM_DOWN_PERCENT, - name="Move down", + translation_key="move_down", icon="mdi:arrow-down-bold", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.ARM_UP_PERCENT, - name="Move up", + translation_key="move_up", icon="mdi:arrow-up-bold", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.CLICK_SUSTAIN_TIME, - name="Down delay", + translation_key="down_delay", icon="mdi:timer", entity_category=EntityCategory.CONFIG, ), @@ -288,7 +288,7 @@ "fs": ( NumberEntityDescription( key=DPCode.TEMP, - name="Temperature", + translation_key="temperature", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer-lines", ), @@ -298,13 +298,13 @@ "jsq": ( NumberEntityDescription( key=DPCode.TEMP_SET, - name="Temperature", + translation_key="temperature", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer-lines", ), NumberEntityDescription( key=DPCode.TEMP_SET_F, - name="Temperature", + translation_key="temperature", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer-lines", ), diff --git a/homeassistant/components/tuya/scene.py b/homeassistant/components/tuya/scene.py index dadc64f98469bb..90cf4266ae6d28 100644 --- a/homeassistant/components/tuya/scene.py +++ b/homeassistant/components/tuya/scene.py @@ -30,6 +30,8 @@ class TuyaSceneEntity(Scene): """Tuya Scene Remote.""" _should_poll = False + _attr_has_entity_name = True + _attr_name = None def __init__(self, home_manager: TuyaHomeManager, scene: TuyaScene) -> None: """Init Tuya Scene.""" @@ -38,11 +40,6 @@ def __init__(self, home_manager: TuyaHomeManager, scene: TuyaScene) -> None: self.home_manager = home_manager self.scene = scene - @property - def name(self) -> str | None: - """Return Tuya scene name.""" - return self.scene.name - @property def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index b84737f73602fd..3cc8c72f555566 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -23,7 +23,7 @@ "dgnbj": ( SelectEntityDescription( key=DPCode.ALARM_VOLUME, - name="Volume", + translation_key="volume", entity_category=EntityCategory.CONFIG, ), ), @@ -32,23 +32,23 @@ "kfj": ( SelectEntityDescription( key=DPCode.CUP_NUMBER, - name="Cups", + translation_key="cups", icon="mdi:numeric", ), SelectEntityDescription( key=DPCode.CONCENTRATION_SET, - name="Concentration", + translation_key="concentration", icon="mdi:altimeter", entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.MATERIAL, - name="Material", + translation_key="material", entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.MODE, - name="Mode", + translation_key="mode", icon="mdi:coffee", ), ), @@ -57,13 +57,11 @@ "kg": ( SelectEntityDescription( key=DPCode.RELAY_STATUS, - name="Power on behavior", entity_category=EntityCategory.CONFIG, translation_key="relay_status", ), SelectEntityDescription( key=DPCode.LIGHT_MODE, - name="Indicator light mode", entity_category=EntityCategory.CONFIG, translation_key="light_mode", ), @@ -73,7 +71,7 @@ "qn": ( SelectEntityDescription( key=DPCode.LEVEL, - name="Temperature level", + translation_key="temperature_level", icon="mdi:thermometer-lines", ), ), @@ -82,12 +80,12 @@ "sgbj": ( SelectEntityDescription( key=DPCode.ALARM_VOLUME, - name="Volume", + translation_key="volume", entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.BRIGHT_STATE, - name="Brightness", + translation_key="brightness", entity_category=EntityCategory.CONFIG, ), ), @@ -96,41 +94,35 @@ "sp": ( SelectEntityDescription( key=DPCode.IPC_WORK_MODE, - name="IPC mode", entity_category=EntityCategory.CONFIG, translation_key="ipc_work_mode", ), SelectEntityDescription( key=DPCode.DECIBEL_SENSITIVITY, - name="Sound detection densitivity", icon="mdi:volume-vibrate", entity_category=EntityCategory.CONFIG, translation_key="decibel_sensitivity", ), SelectEntityDescription( key=DPCode.RECORD_MODE, - name="Record mode", icon="mdi:record-rec", entity_category=EntityCategory.CONFIG, translation_key="record_mode", ), SelectEntityDescription( key=DPCode.BASIC_NIGHTVISION, - name="Night vision", icon="mdi:theme-light-dark", entity_category=EntityCategory.CONFIG, translation_key="basic_nightvision", ), SelectEntityDescription( key=DPCode.BASIC_ANTI_FLICKER, - name="Anti-flicker", icon="mdi:image-outline", entity_category=EntityCategory.CONFIG, translation_key="basic_anti_flicker", ), SelectEntityDescription( key=DPCode.MOTION_SENSITIVITY, - name="Motion detection sensitivity", icon="mdi:motion-sensor", entity_category=EntityCategory.CONFIG, translation_key="motion_sensitivity", @@ -141,13 +133,11 @@ "tdq": ( SelectEntityDescription( key=DPCode.RELAY_STATUS, - name="Power on behavior", entity_category=EntityCategory.CONFIG, translation_key="relay_status", ), SelectEntityDescription( key=DPCode.LIGHT_MODE, - name="Indicator light mode", entity_category=EntityCategory.CONFIG, translation_key="light_mode", ), @@ -157,33 +147,28 @@ "tgkg": ( SelectEntityDescription( key=DPCode.RELAY_STATUS, - name="Power on behavior", entity_category=EntityCategory.CONFIG, translation_key="relay_status", ), SelectEntityDescription( key=DPCode.LIGHT_MODE, - name="Indicator light mode", entity_category=EntityCategory.CONFIG, translation_key="light_mode", ), SelectEntityDescription( key=DPCode.LED_TYPE_1, - name="Light source type", entity_category=EntityCategory.CONFIG, translation_key="led_type", ), SelectEntityDescription( key=DPCode.LED_TYPE_2, - name="Light 2 source type", entity_category=EntityCategory.CONFIG, - translation_key="led_type", + translation_key="led_type_2", ), SelectEntityDescription( key=DPCode.LED_TYPE_3, - name="Light 3 source type", entity_category=EntityCategory.CONFIG, - translation_key="led_type", + translation_key="led_type_3", ), ), # Dimmer @@ -191,22 +176,19 @@ "tgq": ( SelectEntityDescription( key=DPCode.LED_TYPE_1, - name="Light source type", entity_category=EntityCategory.CONFIG, translation_key="led_type", ), SelectEntityDescription( key=DPCode.LED_TYPE_2, - name="Light 2 source type", entity_category=EntityCategory.CONFIG, - translation_key="led_type", + translation_key="led_type_2", ), ), # Fingerbot "szjqr": ( SelectEntityDescription( key=DPCode.MODE, - name="Mode", entity_category=EntityCategory.CONFIG, translation_key="fingerbot_mode", ), @@ -216,21 +198,18 @@ "sd": ( SelectEntityDescription( key=DPCode.CISTERN, - name="Water tank adjustment", entity_category=EntityCategory.CONFIG, icon="mdi:water-opacity", translation_key="vacuum_cistern", ), SelectEntityDescription( key=DPCode.COLLECTION_MODE, - name="Dust collection mode", entity_category=EntityCategory.CONFIG, icon="mdi:air-filter", translation_key="vacuum_collection", ), SelectEntityDescription( key=DPCode.MODE, - name="Mode", entity_category=EntityCategory.CONFIG, icon="mdi:layers-outline", translation_key="vacuum_mode", @@ -241,28 +220,24 @@ "fs": ( SelectEntityDescription( key=DPCode.FAN_VERTICAL, - name="Vertical swing flap angle", entity_category=EntityCategory.CONFIG, icon="mdi:format-vertical-align-center", - translation_key="fan_angle", + translation_key="vertical_fan_angle", ), SelectEntityDescription( key=DPCode.FAN_HORIZONTAL, - name="Horizontal swing flap angle", entity_category=EntityCategory.CONFIG, icon="mdi:format-horizontal-align-center", - translation_key="fan_angle", + translation_key="horizontal_fan_angle", ), SelectEntityDescription( key=DPCode.COUNTDOWN, - name="Countdown", entity_category=EntityCategory.CONFIG, icon="mdi:timer-cog-outline", translation_key="countdown", ), SelectEntityDescription( key=DPCode.COUNTDOWN_SET, - name="Countdown", entity_category=EntityCategory.CONFIG, icon="mdi:timer-cog-outline", translation_key="countdown", @@ -273,14 +248,12 @@ "cl": ( SelectEntityDescription( key=DPCode.CONTROL_BACK_MODE, - name="Motor mode", entity_category=EntityCategory.CONFIG, icon="mdi:swap-horizontal", translation_key="curtain_motor_mode", ), SelectEntityDescription( key=DPCode.MODE, - name="Mode", entity_category=EntityCategory.CONFIG, translation_key="curtain_mode", ), @@ -290,35 +263,30 @@ "jsq": ( SelectEntityDescription( key=DPCode.SPRAY_MODE, - name="Spray mode", entity_category=EntityCategory.CONFIG, icon="mdi:spray", translation_key="humidifier_spray_mode", ), SelectEntityDescription( key=DPCode.LEVEL, - name="Spraying level", entity_category=EntityCategory.CONFIG, icon="mdi:spray", translation_key="humidifier_level", ), SelectEntityDescription( key=DPCode.MOODLIGHTING, - name="Moodlighting", entity_category=EntityCategory.CONFIG, icon="mdi:lightbulb-multiple", translation_key="humidifier_moodlighting", ), SelectEntityDescription( key=DPCode.COUNTDOWN, - name="Countdown", entity_category=EntityCategory.CONFIG, icon="mdi:timer-cog-outline", translation_key="countdown", ), SelectEntityDescription( key=DPCode.COUNTDOWN_SET, - name="Countdown", entity_category=EntityCategory.CONFIG, icon="mdi:timer-cog-outline", translation_key="countdown", @@ -329,14 +297,12 @@ "kj": ( SelectEntityDescription( key=DPCode.COUNTDOWN, - name="Countdown", entity_category=EntityCategory.CONFIG, icon="mdi:timer-cog-outline", translation_key="countdown", ), SelectEntityDescription( key=DPCode.COUNTDOWN_SET, - name="Countdown", entity_category=EntityCategory.CONFIG, icon="mdi:timer-cog-outline", translation_key="countdown", @@ -347,14 +313,13 @@ "cs": ( SelectEntityDescription( key=DPCode.COUNTDOWN_SET, - name="Countdown", entity_category=EntityCategory.CONFIG, icon="mdi:timer-cog-outline", translation_key="countdown", ), SelectEntityDescription( key=DPCode.DEHUMIDITY_SET_ENUM, - name="Target humidity", + translation_key="target_humidity", entity_category=EntityCategory.CONFIG, icon="mdi:water-percent", ), diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 9483443a19c51c..1c7c2ab781dc12 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -49,7 +49,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): BATTERY_SENSORS: tuple[TuyaSensorEntityDescription, ...] = ( TuyaSensorEntityDescription( key=DPCode.BATTERY_PERCENTAGE, - name="Battery", + translation_key="battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, @@ -57,20 +57,20 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), TuyaSensorEntityDescription( key=DPCode.BATTERY_STATE, - name="Battery state", + translation_key="battery_state", icon="mdi:battery", entity_category=EntityCategory.DIAGNOSTIC, ), TuyaSensorEntityDescription( key=DPCode.BATTERY_VALUE, - name="Battery", + translation_key="battery", device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VA_BATTERY, - name="Battery", + translation_key="battery", device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, @@ -87,73 +87,74 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "dgnbj": ( TuyaSensorEntityDescription( key=DPCode.GAS_SENSOR_VALUE, - name="Gas", + translation_key="gas", icon="mdi:gas-cylinder", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CH4_SENSOR_VALUE, + translation_key="gas", name="Methane", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile organic compound", + translation_key="voc", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate matter 2.5 µm", + translation_key="pm25", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CO_VALUE, - name="Carbon monoxide", + translation_key="carbon_monoxide", icon="mdi:molecule-co", device_class=SensorDeviceClass.CO, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon dioxide", + translation_key="carbon_dioxide", icon="mdi:molecule-co2", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CH2O_VALUE, - name="Formaldehyde", + translation_key="formaldehyde", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.BRIGHT_STATE, - name="Luminosity", + translation_key="luminosity", icon="mdi:brightness-6", ), TuyaSensorEntityDescription( key=DPCode.BRIGHT_VALUE, - name="Luminosity", + translation_key="illuminance", icon="mdi:brightness-6", device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, - name="Humidity", + translation_key="humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.SMOKE_SENSOR_VALUE, - name="Smoke amount", + translation_key="smoke_amount", icon="mdi:smoke-detector", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, @@ -165,19 +166,19 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "bh": ( TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Current temperature", + translation_key="current_temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT_F, - name="Current temperature", + translation_key="current_temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.STATUS, - name="Status", + translation_key="status", ), ), # CO2 Detector @@ -185,19 +186,19 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "co2bj": ( TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, - name="Humidity", + translation_key="humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon dioxide", + translation_key="carbon_dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), @@ -209,13 +210,13 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "wkcz": ( TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, - name="Humidity", + translation_key="humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), @@ -225,7 +226,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "cobj": ( TuyaSensorEntityDescription( key=DPCode.CO_VALUE, - name="Carbon monoxide", + translation_key="carbon_monoxide", device_class=SensorDeviceClass.CO, state_class=SensorStateClass.MEASUREMENT, ), @@ -236,7 +237,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "cwwsq": ( TuyaSensorEntityDescription( key=DPCode.FEED_REPORT, - name="Last amount", + translation_key="last_amount", icon="mdi:counter", state_class=SensorStateClass.MEASUREMENT, ), @@ -246,36 +247,36 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "hjjcy": ( TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, - name="Humidity", + translation_key="humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon dioxide", + translation_key="carbon_dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CH2O_VALUE, - name="Formaldehyde", + translation_key="formaldehyde", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile organic compound", + translation_key="voc", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate matter 2.5 µm", + translation_key="pm25", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), @@ -285,37 +286,37 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "jqbj": ( TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon dioxide", + translation_key="carbon_dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile organic compound", + translation_key="voc", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate matter 2.5 µm", + translation_key="pm25", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VA_HUMIDITY, - name="Humidity", + translation_key="humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VA_TEMPERATURE, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CH2O_VALUE, - name="Formaldehyde", + translation_key="formaldehyde", state_class=SensorStateClass.MEASUREMENT, ), *BATTERY_SENSORS, @@ -325,7 +326,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "jwbj": ( TuyaSensorEntityDescription( key=DPCode.CH4_SENSOR_VALUE, - name="Methane", + translation_key="methane", state_class=SensorStateClass.MEASUREMENT, ), *BATTERY_SENSORS, @@ -335,21 +336,21 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "kg": ( TuyaSensorEntityDescription( key=DPCode.CUR_CURRENT, - name="Current", + translation_key="current", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), TuyaSensorEntityDescription( key=DPCode.CUR_POWER, - name="Power", + translation_key="power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), TuyaSensorEntityDescription( key=DPCode.CUR_VOLTAGE, - name="Voltage", + translation_key="voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, @@ -360,21 +361,21 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "tdq": ( TuyaSensorEntityDescription( key=DPCode.CUR_CURRENT, - name="Current", + translation_key="current", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), TuyaSensorEntityDescription( key=DPCode.CUR_POWER, - name="Power", + translation_key="power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), TuyaSensorEntityDescription( key=DPCode.CUR_VOLTAGE, - name="Voltage", + translation_key="voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, @@ -385,30 +386,30 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "ldcg": ( TuyaSensorEntityDescription( key=DPCode.BRIGHT_STATE, - name="Luminosity", + translation_key="luminosity", icon="mdi:brightness-6", ), TuyaSensorEntityDescription( key=DPCode.BRIGHT_VALUE, - name="Luminosity", + translation_key="illuminance", device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, - name="Humidity", + translation_key="humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon dioxide", + translation_key="carbon_dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), @@ -425,18 +426,17 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "mzj": ( TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Current temperature", + translation_key="current_temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.STATUS, - name="Status", - translation_key="status", + translation_key="sous_vide_status", ), TuyaSensorEntityDescription( key=DPCode.REMAIN_TIME, - name="Remaining time", + translation_key="remaining_time", native_unit_of_measurement=UnitOfTime.MINUTES, icon="mdi:timer", ), @@ -449,48 +449,48 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "pm2.5": ( TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate matter 2.5 µm", + translation_key="pm25", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CH2O_VALUE, - name="Formaldehyde", + translation_key="formaldehyde", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile organic compound", + translation_key="voc", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon dioxide", + translation_key="carbon_dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, - name="Humidity", + translation_key="humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM1, - name="Particulate matter 1.0 µm", + translation_key="pm1", device_class=SensorDeviceClass.PM1, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM10, - name="Particulate matter 10.0 µm", + translation_key="pm10", device_class=SensorDeviceClass.PM10, state_class=SensorStateClass.MEASUREMENT, ), @@ -501,7 +501,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "qn": ( TuyaSensorEntityDescription( key=DPCode.WORK_POWER, - name="Power", + translation_key="power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), @@ -528,19 +528,19 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "sp": ( TuyaSensorEntityDescription( key=DPCode.SENSOR_TEMPERATURE, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.SENSOR_HUMIDITY, - name="Humidity", + translation_key="humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.WIRELESS_ELECTRICITY, - name="Battery", + translation_key="battery", device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, @@ -556,36 +556,36 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "voc": ( TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon dioxide", + translation_key="carbon_dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate matter 2.5 µm", + translation_key="pm25", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CH2O_VALUE, - name="Formaldehyde", + translation_key="formaldehyde", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, - name="Humidity", + translation_key="humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile organic compound", + translation_key="voc", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), @@ -599,31 +599,31 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "wsdcg": ( TuyaSensorEntityDescription( key=DPCode.VA_TEMPERATURE, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VA_HUMIDITY, - name="Humidity", + translation_key="humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.HUMIDITY_VALUE, - name="Humidity", + translation_key="humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.BRIGHT_VALUE, - name="Luminosity", + translation_key="illuminance", device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, ), @@ -645,7 +645,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "ywbj": ( TuyaSensorEntityDescription( key=DPCode.SMOKE_SENSOR_VALUE, - name="Smoke amount", + translation_key="smoke_amount", icon="mdi:smoke-detector", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, @@ -660,13 +660,13 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "zndb": ( TuyaSensorEntityDescription( key=DPCode.FORWARD_ENERGY_TOTAL, - name="Total energy", + translation_key="total_energy", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A current", + translation_key="phase_a_current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -674,7 +674,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A power", + translation_key="phase_a_power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, @@ -682,7 +682,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A voltage", + translation_key="phase_a_voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, @@ -690,7 +690,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B current", + translation_key="phase_b_current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -698,7 +698,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B power", + translation_key="phase_b_power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, @@ -706,7 +706,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B voltage", + translation_key="phase_b_voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, @@ -714,7 +714,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C current", + translation_key="phase_c_current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -722,7 +722,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C power", + translation_key="phase_c_power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, @@ -730,7 +730,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C voltage", + translation_key="phase_c_voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, @@ -742,13 +742,13 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "dlq": ( TuyaSensorEntityDescription( key=DPCode.TOTAL_FORWARD_ENERGY, - name="Total energy", + translation_key="total_energy", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A current", + translation_key="phase_a_current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -756,7 +756,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A power", + translation_key="phase_a_power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, @@ -764,7 +764,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A voltage", + translation_key="phase_a_voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, @@ -772,7 +772,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B current", + translation_key="phase_b_current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -780,7 +780,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B power", + translation_key="phase_b_power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, @@ -788,7 +788,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B voltage", + translation_key="phase_b_voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, @@ -796,7 +796,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C current", + translation_key="phase_c_current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -804,7 +804,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C power", + translation_key="phase_c_power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, @@ -812,7 +812,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C voltage", + translation_key="phase_c_voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, @@ -824,55 +824,55 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "sd": ( TuyaSensorEntityDescription( key=DPCode.CLEAN_AREA, - name="Cleaning area", + translation_key="cleaning_area", icon="mdi:texture-box", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CLEAN_TIME, - name="Cleaning time", + translation_key="cleaning_time", icon="mdi:progress-clock", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TOTAL_CLEAN_AREA, - name="Total cleaning area", + translation_key="total_cleaning_area", icon="mdi:texture-box", state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.TOTAL_CLEAN_TIME, - name="Total cleaning time", + translation_key="total_cleaning_time", icon="mdi:history", state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.TOTAL_CLEAN_COUNT, - name="Total cleaning times", + translation_key="total_cleaning_times", icon="mdi:counter", state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.DUSTER_CLOTH, - name="Duster cloth lifetime", + translation_key="duster_cloth_life", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.EDGE_BRUSH, - name="Side brush lifetime", + translation_key="side_brush_life", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.FILTER_LIFE, - name="Filter lifetime", + translation_key="filter_life", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.ROLL_BRUSH, - name="Rolling brush lifetime", + translation_key="rolling_brush_life", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), @@ -882,7 +882,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "cl": ( TuyaSensorEntityDescription( key=DPCode.TIME_TOTAL, - name="Last operation duration", + translation_key="last_operation_duration", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:progress-clock", ), @@ -892,25 +892,25 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "jsq": ( TuyaSensorEntityDescription( key=DPCode.HUMIDITY_CURRENT, - name="Humidity", + translation_key="humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT_F, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.LEVEL_CURRENT, - name="Water level", + translation_key="water_level", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:waves-arrow-up", ), @@ -920,60 +920,59 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "kj": ( TuyaSensorEntityDescription( key=DPCode.FILTER, - name="Filter utilization", + translation_key="filter_utilization", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:ticket-percent-outline", ), TuyaSensorEntityDescription( key=DPCode.PM25, - name="Particulate matter 2.5 µm", + translation_key="pm25", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, icon="mdi:molecule", ), TuyaSensorEntityDescription( key=DPCode.TEMP, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.HUMIDITY, - name="Humidity", + translation_key="humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TVOC, - name="Total volatile organic compound", + translation_key="total_volatile_organic_compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.ECO2, - name="Concentration of carbon dioxide", + translation_key="concentration_carbon_dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TOTAL_TIME, - name="Total operating time", + translation_key="total_operating_time", icon="mdi:history", state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, ), TuyaSensorEntityDescription( key=DPCode.TOTAL_PM, - name="Total absorption of particles", + translation_key="total_absorption_particles", icon="mdi:texture-box", state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, ), TuyaSensorEntityDescription( key=DPCode.AIR_QUALITY, - name="Air quality", - icon="mdi:air-filter", translation_key="air_quality", + icon="mdi:air-filter", ), ), # Fan @@ -981,7 +980,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "fs": ( TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), @@ -990,13 +989,13 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "wnykq": ( TuyaSensorEntityDescription( key=DPCode.VA_TEMPERATURE, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VA_HUMIDITY, - name="Humidity", + translation_key="humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), @@ -1006,13 +1005,13 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "cs": ( TuyaSensorEntityDescription( key=DPCode.TEMP_INDOOR, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.HUMIDITY_INDOOR, - name="Humidity", + translation_key="humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), @@ -1021,13 +1020,13 @@ class TuyaSensorEntityDescription(SensorEntityDescription): "zwjcy": ( TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Temperature", + translation_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.HUMIDITY, - name="Humidity", + translation_key="humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index e7896f5da8623d..db16015ba56eb3 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -18,8 +18,193 @@ } }, "entity": { + "binary_sensor": { + "methane": { + "name": "Methane" + }, + "voc": { + "name": "VOCs" + }, + "pm25": { + "name": "PM2.5" + }, + "carbon_monoxide": { + "name": "Carbon monoxide" + }, + "carbon_dioxide": { + "name": "Carbon dioxide" + }, + "formaldehyde": { + "name": "Formaldehyde" + }, + "pressure": { + "name": "Pressure" + }, + "feeding": { + "name": "Feeding" + }, + "drop": { + "name": "Drop" + }, + "tilt": { + "name": "Tilt" + } + }, + "button": { + "reset_duster_cloth": { + "name": "Reset duster cloth" + }, + "reset_edge_brush": { + "name": "Reset edge brush" + }, + "reset_filter": { + "name": "Reset filter" + }, + "reset_map": { + "name": "Reset map" + }, + "reset_roll_brush": { + "name": "Reset roll brush" + }, + "snooze": { + "name": "Snooze" + } + }, + "cover": { + "curtain_2": { + "name": "Curtain 2" + }, + "curtain_3": { + "name": "Curtain 3" + }, + "door": { + "name": "[%key:component::cover::entity_component::door::name%]" + }, + "door_2": { + "name": "Door 2" + }, + "door_3": { + "name": "Door 3" + } + }, + "light": { + "backlight": { + "name": "Backlight" + }, + "light": { + "name": "[%key:component::light::title%]" + }, + "light_2": { + "name": "Light 2" + }, + "light_3": { + "name": "Light 3" + }, + "night_light": { + "name": "Night light" + } + }, + "number": { + "temperature": { + "name": "[%key:component::number::entity_component::temperature::name%]" + }, + "time": { + "name": "Time" + }, + "temperature_after_boiling": { + "name": "Temperature after boiling" + }, + "heat_preservation_time": { + "name": "Heat preservation time" + }, + "feed": { + "name": "Feed" + }, + "voice_times": { + "name": "Voice times" + }, + "sensitivity": { + "name": "Sensitivity" + }, + "near_detection": { + "name": "Near detection" + }, + "far_detection": { + "name": "Far detection" + }, + "water_level": { + "name": "Water level" + }, + "powder": { + "name": "Powder" + }, + "cook_temperature": { + "name": "Cook temperature" + }, + "cook_time": { + "name": "Cook time" + }, + "cloud_recipe": { + "name": "Cloud recipe" + }, + "volume": { + "name": "Volume" + }, + "minimum_brightness": { + "name": "Minimum brightness" + }, + "maximum_brightness": { + "name": "Maximum brightness" + }, + "minimum_brightness_2": { + "name": "Minimum brightness 2" + }, + "maximum_brightness_2": { + "name": "Maximum brightness 2" + }, + "minimum_brightness_3": { + "name": "Minimum brightness 3" + }, + "maximum_brightness_3": { + "name": "Maximum brightness 3" + }, + "move_down": { + "name": "Move down" + }, + "move_up": { + "name": "Move up" + }, + "down_delay": { + "name": "Down delay" + } + }, "select": { + "volume": { + "name": "[%key:component::tuya::entity::number::volume::name%]" + }, + "cups": { + "name": "Cups" + }, + "concentration": { + "name": "Concentration" + }, + "material": { + "name": "Material" + }, + "mode": { + "name": "Mode" + }, + "temperature_level": { + "name": "Temperature level" + }, + "brightness": { + "name": "Brightness" + }, + "target_humidity": { + "name": "Target humidity" + }, "basic_anti_flicker": { + "name": "Anti-flicker", "state": { "0": "[%key:common::state::disabled%]", "1": "50 Hz", @@ -27,6 +212,7 @@ } }, "basic_nightvision": { + "name": "Night vision", "state": { "0": "Automatic", "1": "[%key:common::state::off%]", @@ -34,25 +220,45 @@ } }, "decibel_sensitivity": { + "name": "Sound detection sensitivity", "state": { "0": "Low sensitivity", "1": "High sensitivity" } }, "ipc_work_mode": { + "name": "IPC mode", "state": { "0": "Low power mode", "1": "Continuous working mode" } }, "led_type": { + "name": "Light source type", "state": { "halogen": "Halogen", "incandescent": "Incandescent", "led": "LED" } }, + "led_type_2": { + "name": "Light 2 source type", + "state": { + "halogen": "[%key:component::tuya::entity::select::led_type::state::halogen%]", + "incandescent": "[%key:component::tuya::entity::select::led_type::state::incandescent%]", + "led": "[%key:component::tuya::entity::select::led_type::state::led%]" + } + }, + "led_type_3": { + "name": "Light 3 source type", + "state": { + "halogen": "[%key:component::tuya::entity::select::led_type::state::halogen%]", + "incandescent": "[%key:component::tuya::entity::select::led_type::state::incandescent%]", + "led": "[%key:component::tuya::entity::select::led_type::state::led%]" + } + }, "light_mode": { + "name": "Indicator light mode", "state": { "none": "[%key:common::state::off%]", "pos": "Indicate switch location", @@ -60,6 +266,7 @@ } }, "motion_sensitivity": { + "name": "Motion detection sensitivity", "state": { "0": "Low sensitivity", "1": "Medium sensitivity", @@ -67,12 +274,14 @@ } }, "record_mode": { + "name": "Record mode", "state": { "1": "Record events only", "2": "Continuous recording" } }, "relay_status": { + "name": "Power on behavior", "state": { "last": "Remember last state", "memory": "[%key:component::tuya::entity::select::relay_status::state::last%]", @@ -83,12 +292,14 @@ } }, "fingerbot_mode": { + "name": "Mode", "state": { "click": "Push", "switch": "Switch" } }, "vacuum_cistern": { + "name": "Water tank adjustment", "state": { "low": "Low", "middle": "Middle", @@ -97,6 +308,7 @@ } }, "vacuum_collection": { + "name": "Dust collection mode", "state": { "small": "Small", "middle": "Middle", @@ -104,29 +316,39 @@ } }, "vacuum_mode": { + "name": "Mode", "state": { "standby": "[%key:common::state::standby%]", "random": "Random", "smart": "Smart", - "wall_follow": "Follow Wall", + "wall_follow": "Follow wall", "mop": "Mop", "spiral": "Spiral", - "left_spiral": "Spiral Left", - "right_spiral": "Spiral Right", + "left_spiral": "Spiral left", + "right_spiral": "Spiral right", "bow": "Bow", - "left_bow": "Bow Left", - "right_bow": "Bow Right", - "partial_bow": "Bow Partially", + "left_bow": "Bow left", + "right_bow": "Bow right", + "partial_bow": "Bow partially", "chargego": "Return to dock", "single": "Single", "zone": "Zone", "pose": "Pose", "point": "Point", "part": "Part", - "pick_zone": "Pick Zone" + "pick_zone": "Pick zone" + } + }, + "vertical_fan_angle": { + "name": "Vertical swing flap angle", + "state": { + "30": "30°", + "60": "60°", + "90": "90°" } }, - "fan_angle": { + "horizontal_fan_angle": { + "name": "Horizontal swing flap angle", "state": { "30": "30°", "60": "60°", @@ -134,18 +356,21 @@ } }, "curtain_mode": { + "name": "Mode", "state": { "morning": "Morning", "night": "Night" } }, "curtain_motor_mode": { + "name": "Motor mode", "state": { "forward": "Forward", "back": "Back" } }, "countdown": { + "name": "Countdown", "state": { "cancel": "Cancel", "1h": "1 hour", @@ -157,6 +382,7 @@ } }, "humidifier_spray_mode": { + "name": "Spray mode", "state": { "auto": "Auto", "health": "Health", @@ -166,6 +392,7 @@ } }, "humidifier_level": { + "name": "Spraying level", "state": { "level_1": "Level 1", "level_2": "Level 2", @@ -180,6 +407,7 @@ } }, "humidifier_moodlighting": { + "name": "Moodlighting", "state": { "1": "Mood 1", "2": "Mood 2", @@ -190,7 +418,155 @@ } }, "sensor": { + "battery": { + "name": "[%key:component::sensor::entity_component::battery::name%]" + }, + "voc": { + "name": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]" + }, + "carbon_monoxide": { + "name": "[%key:component::sensor::entity_component::carbon_monoxide::name%]" + }, + "carbon_dioxide": { + "name": "[%key:component::sensor::entity_component::carbon_dioxide::name%]" + }, + "illuminance": { + "name": "[%key:component::sensor::entity_component::illuminance::name%]" + }, + "temperature": { + "name": "[%key:component::sensor::entity_component::temperature::name%]" + }, + "humidity": { + "name": "[%key:component::sensor::entity_component::humidity::name%]" + }, + "pm25": { + "name": "[%key:component::sensor::entity_component::pm25::name%]" + }, + "pm1": { + "name": "[%key:component::sensor::entity_component::pm1::name%]" + }, + "pm10": { + "name": "[%key:component::sensor::entity_component::pm10::name%]" + }, + "current": { + "name": "[%key:component::sensor::entity_component::current::name%]" + }, + "power": { + "name": "[%key:component::sensor::entity_component::power::name%]" + }, + "voltage": { + "name": "[%key:component::sensor::entity_component::voltage::name%]" + }, + "battery_state": { + "name": "Battery state" + }, + "gas": { + "name": "Gas" + }, + "formaldehyde": { + "name": "[%key:component::tuya::entity::binary_sensor::formaldehyde::name%]" + }, + "luminosity": { + "name": "Luminosity" + }, + "smoke_amount": { + "name": "Smoke amount" + }, + "current_temperature": { + "name": "Current temperature" + }, "status": { + "name": "Status" + }, + "last_amount": { + "name": "Last amount" + }, + "remaining_time": { + "name": "Remaining time" + }, + "methane": { + "name": "[%key:component::tuya::entity::binary_sensor::methane::name%]" + }, + "total_energy": { + "name": "Total energy" + }, + "phase_a_current": { + "name": "Phase A current" + }, + "phase_a_power": { + "name": "Phase A power" + }, + "phase_a_voltage": { + "name": "Phase A voltage" + }, + "phase_b_current": { + "name": "Phase B current" + }, + "phase_b_power": { + "name": "Phase B power" + }, + "phase_b_voltage": { + "name": "Phase B voltage" + }, + "phase_c_current": { + "name": "Phase C current" + }, + "phase_c_power": { + "name": "Phase C power" + }, + "phase_c_voltage": { + "name": "Phase C voltage" + }, + "cleaning_area": { + "name": "Cleaning area" + }, + "cleaning_time": { + "name": "Cleaning time" + }, + "total_cleaning_area": { + "name": "Total cleaning area" + }, + "total_cleaning_time": { + "name": "Total cleaning time" + }, + "total_cleaning_times": { + "name": "Total cleaning times" + }, + "duster_cloth_life": { + "name": "Duster cloth lifetime" + }, + "side_brush_life": { + "name": "Side brush lifetime" + }, + "filter_life": { + "name": "Filter lifetime" + }, + "rolling_brush_life": { + "name": "Rolling brush lifetime" + }, + "last_operation_duration": { + "name": "Last operation duration" + }, + "water_level": { + "name": "Water level" + }, + "filter_utilization": { + "name": "Filter utilization" + }, + "total_volatile_organic_compound": { + "name": "Total volatile organic compound" + }, + "concentration_carbon_dioxide": { + "name": "Concentration of carbon dioxide" + }, + "total_operating_time": { + "name": "Total operating time" + }, + "total_absorption_particles": { + "name": "Total absorption of particles" + }, + "sous_vide_status": { + "name": "Status", "state": { "boiling_temp": "Boiling temperature", "cooling": "Cooling", @@ -204,6 +580,7 @@ } }, "air_quality": { + "name": "Air quality", "state": { "great": "Great", "mild": "Mild", @@ -211,6 +588,212 @@ "severe": "Severe" } } + }, + "switch": { + "start": { + "name": "Start" + }, + "heat_preservation": { + "name": "Heat preservation" + }, + "disinfection": { + "name": "Disinfection" + }, + "water": { + "name": "Water" + }, + "slow_feed": { + "name": "Slow feed" + }, + "filter_reset": { + "name": "Filter reset" + }, + "water_pump_reset": { + "name": "Water pump reset" + }, + "power": { + "name": "Power" + }, + "reset_of_water_usage_days": { + "name": "Reset of water usage days" + }, + "uv_sterilization": { + "name": "UV sterilization" + }, + "plug": { + "name": "Plug" + }, + "child_lock": { + "name": "Child lock" + }, + "switch": { + "name": "Switch" + }, + "socket": { + "name": "Socket" + }, + "radio": { + "name": "Radio" + }, + "alarm_1": { + "name": "Alarm 1" + }, + "alarm_2": { + "name": "Alarm 2" + }, + "alarm_3": { + "name": "Alarm 3" + }, + "alarm_4": { + "name": "Alarm 4" + }, + "sleep_aid": { + "name": "Sleep aid" + }, + "switch_1": { + "name": "Switch 1" + }, + "switch_2": { + "name": "Switch 2" + }, + "switch_3": { + "name": "Switch 3" + }, + "switch_4": { + "name": "Switch 4" + }, + "switch_5": { + "name": "Switch 5" + }, + "switch_6": { + "name": "Switch 6" + }, + "switch_7": { + "name": "Switch 7" + }, + "switch_8": { + "name": "Switch 8" + }, + "usb_1": { + "name": "USB 1" + }, + "usb_2": { + "name": "USB 2" + }, + "usb_3": { + "name": "USB 3" + }, + "usb_4": { + "name": "USB 4" + }, + "usb_5": { + "name": "USB 5" + }, + "usb_6": { + "name": "USB 6" + }, + "socket_1": { + "name": "Socket 1" + }, + "socket_2": { + "name": "Socket 2" + }, + "socket_3": { + "name": "Socket 3" + }, + "socket_4": { + "name": "Socket 4" + }, + "socket_5": { + "name": "Socket 5" + }, + "socket_6": { + "name": "Socket 6" + }, + "ionizer": { + "name": "Ionizer" + }, + "filter_cartridge_reset": { + "name": "Filter cartridge reset" + }, + "humidification": { + "name": "Humidification" + }, + "do_not_disturb": { + "name": "Do not disturb" + }, + "mute_voice": { + "name": "Mute voice" + }, + "mute": { + "name": "Mute" + }, + "battery_lock": { + "name": "Battery lock" + }, + "cry_detection": { + "name": "Cry detection" + }, + "sound_detection": { + "name": "Sound detection" + }, + "video_recording": { + "name": "Video recording" + }, + "motion_recording": { + "name": "Motion recording" + }, + "privacy_mode": { + "name": "Privacy mode" + }, + "flip": { + "name": "Flip" + }, + "time_watermark": { + "name": "Time watermark" + }, + "wide_dynamic_range": { + "name": "Wide dynamic range" + }, + "motion_tracking": { + "name": "Motion tracking" + }, + "motion_alarm": { + "name": "Motion alarm" + }, + "energy_saving": { + "name": "Energy saving" + }, + "open_window_detection": { + "name": "Open window detection" + }, + "spray": { + "name": "Spray" + }, + "voice": { + "name": "Voice" + }, + "anion": { + "name": "Anion" + }, + "oxygen_bar": { + "name": "Oxygen bar" + }, + "natural_wind": { + "name": "Natural wind" + }, + "sound": { + "name": "Sound" + }, + "reverse": { + "name": "Reverse" + }, + "sleep": { + "name": "Sleep" + }, + "sterilization": { + "name": "Sterilization" + } } }, "issues": { diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index a7245913e73522..c99d6f3a0b207e 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -29,12 +29,12 @@ "bh": ( SwitchEntityDescription( key=DPCode.START, - name="Start", + translation_key="start", icon="mdi:kettle-steam", ), SwitchEntityDescription( key=DPCode.WARM, - name="Heat preservation", + translation_key="heat_preservation", entity_category=EntityCategory.CONFIG, ), ), @@ -43,12 +43,12 @@ "cn": ( SwitchEntityDescription( key=DPCode.DISINFECTION, - name="Disinfection", + translation_key="disinfection", icon="mdi:bacteria", ), SwitchEntityDescription( key=DPCode.WATER, - name="Water", + translation_key="water", icon="mdi:water", ), ), @@ -57,7 +57,7 @@ "cwwsq": ( SwitchEntityDescription( key=DPCode.SLOW_FEED, - name="Slow feed", + translation_key="slow_feed", icon="mdi:speedometer-slow", entity_category=EntityCategory.CONFIG, ), @@ -67,29 +67,29 @@ "cwysj": ( SwitchEntityDescription( key=DPCode.FILTER_RESET, - name="Filter reset", + translation_key="filter_reset", icon="mdi:filter", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.PUMP_RESET, - name="Water pump reset", + translation_key="water_pump_reset", icon="mdi:pump", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH, - name="Power", + translation_key="power", ), SwitchEntityDescription( key=DPCode.WATER_RESET, - name="Reset of water usage days", + translation_key="reset_of_water_usage_days", icon="mdi:water-sync", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.UV, - name="UV sterilization", + translation_key="uv_sterilization", icon="mdi:lightbulb", entity_category=EntityCategory.CONFIG, ), @@ -102,20 +102,20 @@ # switch to control the plug. SwitchEntityDescription( key=DPCode.SWITCH, - name="Plug", + translation_key="plug", ), ), # Cirquit Breaker "dlq": ( SwitchEntityDescription( key=DPCode.CHILD_LOCK, - name="Child lock", + translation_key="asd", icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH, - name="Switch", + translation_key="switch", ), ), # Wake Up Light II @@ -123,36 +123,36 @@ "hxd": ( SwitchEntityDescription( key=DPCode.SWITCH_1, - name="Radio", + translation_key="radio", icon="mdi:radio", ), SwitchEntityDescription( key=DPCode.SWITCH_2, - name="Alarm 1", + translation_key="alarm_1", icon="mdi:alarm", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_3, - name="Alarm 2", + translation_key="alarm_2", icon="mdi:alarm", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_4, - name="Alarm 3", + translation_key="alarm_3", icon="mdi:alarm", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_5, - name="Alarm 4", + translation_key="alarm_4", icon="mdi:alarm", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_6, - name="Sleep aid", + translation_key="sleep_aid", icon="mdi:power-sleep", ), ), @@ -162,12 +162,12 @@ "wkcz": ( SwitchEntityDescription( key=DPCode.SWITCH_1, - name="Switch 1", + translation_key="switch_1", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_2, - name="Switch 2", + translation_key="switch_2", device_class=SwitchDeviceClass.OUTLET, ), ), @@ -176,77 +176,77 @@ "kg": ( SwitchEntityDescription( key=DPCode.CHILD_LOCK, - name="Child lock", + translation_key="child_lock", icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_1, - name="Switch 1", + translation_key="switch_1", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_2, - name="Switch 2", + translation_key="switch_2", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_3, - name="Switch 3", + translation_key="switch_3", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_4, - name="Switch 4", + translation_key="switch_4", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_5, - name="Switch 5", + translation_key="switch_5", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_6, - name="Switch 6", + translation_key="switch_6", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_7, - name="Switch 7", + translation_key="switch_7", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_8, - name="Switch 8", + translation_key="switch_8", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_USB1, - name="USB 1", + translation_key="usb_1", ), SwitchEntityDescription( key=DPCode.SWITCH_USB2, - name="USB 2", + translation_key="usb_2", ), SwitchEntityDescription( key=DPCode.SWITCH_USB3, - name="USB 3", + translation_key="usb_3", ), SwitchEntityDescription( key=DPCode.SWITCH_USB4, - name="USB 4", + translation_key="usb_4", ), SwitchEntityDescription( key=DPCode.SWITCH_USB5, - name="USB 5", + translation_key="usb_5", ), SwitchEntityDescription( key=DPCode.SWITCH_USB6, - name="USB 6", + translation_key="usb_6", ), SwitchEntityDescription( key=DPCode.SWITCH, - name="Switch", + translation_key="switch", device_class=SwitchDeviceClass.OUTLET, ), ), @@ -255,35 +255,35 @@ "kj": ( SwitchEntityDescription( key=DPCode.ANION, - name="Ionizer", + translation_key="ionizer", icon="mdi:minus-circle-outline", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.FILTER_RESET, - name="Filter cartridge reset", + translation_key="filter_cartridge_reset", icon="mdi:filter", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.LOCK, - name="Child lock", + translation_key="child_lock", icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH, - name="Power", + translation_key="power", ), SwitchEntityDescription( key=DPCode.WET, - name="Humidification", + translation_key="humidification", icon="mdi:water-percent", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.UV, - name="UV sterilization", + translation_key="uv_sterilization", icon="mdi:minus-circle-outline", entity_category=EntityCategory.CONFIG, ), @@ -293,13 +293,13 @@ "kt": ( SwitchEntityDescription( key=DPCode.ANION, - name="Ionizer", + translation_key="ionizer", icon="mdi:minus-circle-outline", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.LOCK, - name="Child lock", + translation_key="child_lock", icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), @@ -309,13 +309,13 @@ "mzj": ( SwitchEntityDescription( key=DPCode.SWITCH, - name="Switch", + translation_key="switch", icon="mdi:power", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.START, - name="Start", + translation_key="start", icon="mdi:pot-steam", entity_category=EntityCategory.CONFIG, ), @@ -325,67 +325,67 @@ "pc": ( SwitchEntityDescription( key=DPCode.CHILD_LOCK, - name="Child lock", + translation_key="child_lock", icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_1, - name="Socket 1", + translation_key="socket_1", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_2, - name="Socket 2", + translation_key="socket_2", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_3, - name="Socket 3", + translation_key="socket_3", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_4, - name="Socket 4", + translation_key="socket_4", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_5, - name="Socket 5", + translation_key="socket_5", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_6, - name="Socket 6", + translation_key="socket_6", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_USB1, - name="USB 1", + translation_key="usb_1", ), SwitchEntityDescription( key=DPCode.SWITCH_USB2, - name="USB 2", + translation_key="usb_2", ), SwitchEntityDescription( key=DPCode.SWITCH_USB3, - name="USB 3", + translation_key="usb_3", ), SwitchEntityDescription( key=DPCode.SWITCH_USB4, - name="USB 4", + translation_key="usb_4", ), SwitchEntityDescription( key=DPCode.SWITCH_USB5, - name="USB 5", + translation_key="usb_5", ), SwitchEntityDescription( key=DPCode.SWITCH_USB6, - name="USB 6", + translation_key="usb_6", ), SwitchEntityDescription( key=DPCode.SWITCH, - name="Socket", + translation_key="socket", device_class=SwitchDeviceClass.OUTLET, ), ), @@ -395,7 +395,7 @@ "qjdcz": ( SwitchEntityDescription( key=DPCode.SWITCH_1, - name="Switch", + translation_key="switch", ), ), # Heater @@ -403,13 +403,13 @@ "qn": ( SwitchEntityDescription( key=DPCode.ANION, - name="Ionizer", + translation_key="ionizer", icon="mdi:minus-circle-outline", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.LOCK, - name="Child lock", + translation_key="child_lock", icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), @@ -419,13 +419,13 @@ "sd": ( SwitchEntityDescription( key=DPCode.SWITCH_DISTURB, - name="Do not disturb", + translation_key="do_not_disturb", icon="mdi:minus-circle", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.VOICE_SWITCH, - name="Mute voice", + translation_key="mute_voice", icon="mdi:account-voice", entity_category=EntityCategory.CONFIG, ), @@ -435,7 +435,7 @@ "sgbj": ( SwitchEntityDescription( key=DPCode.MUFFLING, - name="Mute", + translation_key="mute", entity_category=EntityCategory.CONFIG, ), ), @@ -444,68 +444,68 @@ "sp": ( SwitchEntityDescription( key=DPCode.WIRELESS_BATTERYLOCK, - name="Battery lock", + translation_key="battery_lock", icon="mdi:battery-lock", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.CRY_DETECTION_SWITCH, + translation_key="cry_detection", icon="mdi:emoticon-cry", - name="Cry detection", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.DECIBEL_SWITCH, + translation_key="sound_detection", icon="mdi:microphone-outline", - name="Sound detection", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.RECORD_SWITCH, + translation_key="video_recording", icon="mdi:record-rec", - name="Video recording", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.MOTION_RECORD, + translation_key="motion_recording", icon="mdi:record-rec", - name="Motion recording", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.BASIC_PRIVATE, + translation_key="privacy_mode", icon="mdi:eye-off", - name="Privacy mode", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.BASIC_FLIP, + translation_key="flip", icon="mdi:flip-horizontal", - name="Flip", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.BASIC_OSD, + translation_key="time_watermark", icon="mdi:watermark", - name="Time watermark", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.BASIC_WDR, + translation_key="wide_dynamic_range", icon="mdi:watermark", - name="Wide dynamic range", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.MOTION_TRACKING, + translation_key="motion_tracking", icon="mdi:motion-sensor", - name="Motion tracking", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.MOTION_SWITCH, + translation_key="motion_alarm", icon="mdi:motion-sensor", - name="Motion alarm", entity_category=EntityCategory.CONFIG, ), ), @@ -513,7 +513,7 @@ "szjqr": ( SwitchEntityDescription( key=DPCode.SWITCH, - name="Switch", + translation_key="switch", icon="mdi:cursor-pointer", ), ), @@ -522,27 +522,27 @@ "tdq": ( SwitchEntityDescription( key=DPCode.SWITCH_1, - name="Switch 1", + translation_key="switch_1", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_2, - name="Switch 2", + translation_key="switch_2", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_3, - name="Switch 3", + translation_key="switch_3", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.SWITCH_4, - name="Switch 4", + translation_key="switch_4", device_class=SwitchDeviceClass.OUTLET, ), SwitchEntityDescription( key=DPCode.CHILD_LOCK, - name="Child lock", + translation_key="child_lock", icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), @@ -552,7 +552,7 @@ "tyndj": ( SwitchEntityDescription( key=DPCode.SWITCH_SAVE_ENERGY, - name="Energy saving", + translation_key="energy_saving", icon="mdi:leaf", entity_category=EntityCategory.CONFIG, ), @@ -562,13 +562,13 @@ "wkf": ( SwitchEntityDescription( key=DPCode.CHILD_LOCK, - name="Child lock", + translation_key="child_lock", icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.WINDOW_CHECK, - name="Open window detection", + translation_key="open_window_detection", icon="mdi:window-open", entity_category=EntityCategory.CONFIG, ), @@ -578,7 +578,7 @@ "wsdcg": ( SwitchEntityDescription( key=DPCode.SWITCH, - name="Switch", + translation_key="switch", device_class=SwitchDeviceClass.OUTLET, ), ), @@ -587,7 +587,7 @@ "xdd": ( SwitchEntityDescription( key=DPCode.DO_NOT_DISTURB, - name="Do not disturb", + translation_key="do_not_disturb", icon="mdi:minus-circle-outline", entity_category=EntityCategory.CONFIG, ), @@ -597,16 +597,16 @@ "xxj": ( SwitchEntityDescription( key=DPCode.SWITCH, - name="Power", + translation_key="power", ), SwitchEntityDescription( key=DPCode.SWITCH_SPRAY, - name="Spray", + translation_key="spray", icon="mdi:spray", ), SwitchEntityDescription( key=DPCode.SWITCH_VOICE, - name="Voice", + translation_key="voice", icon="mdi:account-voice", entity_category=EntityCategory.CONFIG, ), @@ -616,7 +616,7 @@ "zndb": ( SwitchEntityDescription( key=DPCode.SWITCH, - name="Switch", + translation_key="switch", ), ), # Fan @@ -624,37 +624,37 @@ "fs": ( SwitchEntityDescription( key=DPCode.ANION, - name="Anion", + translation_key="anion", icon="mdi:atom", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.HUMIDIFIER, - name="Humidification", + translation_key="humidification", icon="mdi:air-humidifier", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.OXYGEN, - name="Oxygen bar", + translation_key="oxygen_bar", icon="mdi:molecule", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.FAN_COOL, - name="Natural wind", + translation_key="natural_wind", icon="mdi:weather-windy", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.FAN_BEEP, - name="Sound", + translation_key="sound", icon="mdi:minus-circle", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.CHILD_LOCK, - name="Child lock", + translation_key="child_lock", icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), @@ -664,13 +664,13 @@ "cl": ( SwitchEntityDescription( key=DPCode.CONTROL_BACK, - name="Reverse", + translation_key="reverse", icon="mdi:swap-horizontal", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.OPPOSITE, - name="Reverse", + translation_key="reverse", icon="mdi:swap-horizontal", entity_category=EntityCategory.CONFIG, ), @@ -680,19 +680,19 @@ "jsq": ( SwitchEntityDescription( key=DPCode.SWITCH_SOUND, - name="Voice", + translation_key="voice", icon="mdi:account-voice", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SLEEP, - name="Sleep", + translation_key="sleep", icon="mdi:power-sleep", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.STERILIZATION, - name="Sterilization", + translation_key="sterilization", icon="mdi:minus-circle-outline", entity_category=EntityCategory.CONFIG, ), From e18da97670aebc76fb99bb697dd04038157677c9 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:07:11 +0200 Subject: [PATCH 0670/1009] Improve pip caching [ci] (#96896) --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index da7d73c272df79..08407e46c1c10b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -492,9 +492,9 @@ jobs: python -m venv venv . venv/bin/activate python --version - pip install --cache-dir=$PIP_CACHE -U "pip>=21.3.1" setuptools wheel - pip install --cache-dir=$PIP_CACHE -r requirements_all.txt - pip install --cache-dir=$PIP_CACHE -r requirements_test.txt + PIP_CACHE_DIR=$PIP_CACHE pip install -U "pip>=21.3.1" setuptools wheel + PIP_CACHE_DIR=$PIP_CACHE pip install -r requirements_all.txt + PIP_CACHE_DIR=$PIP_CACHE pip install -r requirements_test.txt pip install -e . --config-settings editable_mode=compat hassfest: From f0953dde95a69d5bba763893f466c8009bb411e3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 19 Jul 2023 13:07:23 +0200 Subject: [PATCH 0671/1009] Add comment to EntityPlatform._async_add_entity about update_before_add (#96891) --- homeassistant/helpers/entity_platform.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 067d6430c9f8c6..b7dadcf0f67ed3 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -569,7 +569,8 @@ async def _async_add_entity( # noqa: C901 self._get_parallel_updates_semaphore(hasattr(entity, "update")), ) - # Update properties before we generate the entity_id + # Update properties before we generate the entity_id. This will happen + # also for disabled entities. if update_before_add: try: await entity.async_device_update(warning=False) From 33b3b8947a806065b7abed5a12d6ccf60a4e121d Mon Sep 17 00:00:00 2001 From: Renier Moorcroft <66512715+RenierM26@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:26:11 +0200 Subject: [PATCH 0672/1009] Add Ezviz SensorEntity name and translation (#95697) Co-authored-by: Franck Nijhof --- homeassistant/components/ezviz/sensor.py | 47 ++++++++++++++++----- homeassistant/components/ezviz/strings.json | 32 ++++++++++++++ 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py index 1a8bfba21fb7a8..9b19148bdb7f63 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -18,7 +18,6 @@ PARALLEL_UPDATES = 1 SENSOR_TYPES: dict[str, SensorEntityDescription] = { - "sw_version": SensorEntityDescription(key="sw_version"), "battery_level": SensorEntityDescription( key="battery_level", native_unit_of_measurement=PERCENTAGE, @@ -26,19 +25,48 @@ ), "alarm_sound_mod": SensorEntityDescription( key="alarm_sound_mod", + translation_key="alarm_sound_mod", + entity_registry_enabled_default=False, + ), + "last_alarm_time": SensorEntityDescription( + key="last_alarm_time", + translation_key="last_alarm_time", entity_registry_enabled_default=False, ), - "last_alarm_time": SensorEntityDescription(key="last_alarm_time"), "Seconds_Last_Trigger": SensorEntityDescription( key="Seconds_Last_Trigger", + translation_key="seconds_last_trigger", entity_registry_enabled_default=False, ), - "supported_channels": SensorEntityDescription(key="supported_channels"), - "local_ip": SensorEntityDescription(key="local_ip"), - "wan_ip": SensorEntityDescription(key="wan_ip"), - "PIR_Status": SensorEntityDescription(key="PIR_Status"), - "last_alarm_type_code": SensorEntityDescription(key="last_alarm_type_code"), - "last_alarm_type_name": SensorEntityDescription(key="last_alarm_type_name"), + "last_alarm_pic": SensorEntityDescription( + key="last_alarm_pic", + translation_key="last_alarm_pic", + entity_registry_enabled_default=False, + ), + "supported_channels": SensorEntityDescription( + key="supported_channels", + translation_key="supported_channels", + ), + "local_ip": SensorEntityDescription( + key="local_ip", + translation_key="local_ip", + ), + "wan_ip": SensorEntityDescription( + key="wan_ip", + translation_key="wan_ip", + ), + "PIR_Status": SensorEntityDescription( + key="PIR_Status", + translation_key="pir_status", + ), + "last_alarm_type_code": SensorEntityDescription( + key="last_alarm_type_code", + translation_key="last_alarm_type_code", + ), + "last_alarm_type_name": SensorEntityDescription( + key="last_alarm_type_name", + translation_key="last_alarm_type_name", + ), } @@ -64,7 +92,7 @@ async def async_setup_entry( class EzvizSensor(EzvizEntity, SensorEntity): """Representation of a EZVIZ sensor.""" - coordinator: EzvizDataUpdateCoordinator + _attr_has_entity_name = True def __init__( self, coordinator: EzvizDataUpdateCoordinator, serial: str, sensor: str @@ -72,7 +100,6 @@ def __init__( """Initialize the sensor.""" super().__init__(coordinator, serial) self._sensor_name = sensor - self._attr_name = f"{self._camera_name} {sensor.title()}" self._attr_unique_id = f"{serial}_{self._camera_name}.{sensor}" self.entity_description = SENSOR_TYPES[sensor] diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index 94a73fc16cd257..909a9b5f9c0966 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -98,6 +98,38 @@ "last_motion_image": { "name": "Last motion image" } + }, + "sensor": { + "alarm_sound_mod": { + "name": "Alarm sound level" + }, + "last_alarm_time": { + "name": "Last alarm time" + }, + "seconds_last_trigger": { + "name": "Seconds since last trigger" + }, + "last_alarm_pic": { + "name": "Last alarm picture URL" + }, + "supported_channels": { + "name": "Supported channels" + }, + "local_ip": { + "name": "Local IP" + }, + "wan_ip": { + "name": "WAN IP" + }, + "pir_status": { + "name": "PIR status" + }, + "last_alarm_type_code": { + "name": "Last alarm type code" + }, + "last_alarm_type_name": { + "name": "Last alarm type name" + } } }, "services": { From a305a9fe9c2a1fc76168b6d5c9dd5ad4f1e7ddbc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 19 Jul 2023 13:50:28 +0200 Subject: [PATCH 0673/1009] Update sentry-sdk to 1.28.1 (#96898) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index c3d0852e17ae9b..149e503d0f8e49 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/sentry", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["sentry-sdk==1.27.1"] + "requirements": ["sentry-sdk==1.28.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 74a6082c0bc44a..706bf13144fda6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2357,7 +2357,7 @@ sensorpro-ble==0.5.3 sensorpush-ble==1.5.5 # homeassistant.components.sentry -sentry-sdk==1.27.1 +sentry-sdk==1.28.1 # homeassistant.components.sfr_box sfrbox-api==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b968ee932e485..006c9084832de6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1720,7 +1720,7 @@ sensorpro-ble==0.5.3 sensorpush-ble==1.5.5 # homeassistant.components.sentry -sentry-sdk==1.27.1 +sentry-sdk==1.28.1 # homeassistant.components.sfr_box sfrbox-api==0.0.6 From 0502879d10a3eac03929ddc1d409075517459049 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 19 Jul 2023 14:35:54 +0200 Subject: [PATCH 0674/1009] Update PyJWT to 2.8.0 (#96899) --- 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 927f0db3f01b2d..5c94730b5db95a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -35,7 +35,7 @@ paho-mqtt==1.6.1 Pillow==10.0.0 pip>=21.3.1 psutil-home-assistant==0.0.1 -PyJWT==2.7.0 +PyJWT==2.8.0 PyNaCl==1.5.0 pyOpenSSL==23.2.0 pyserial==3.5 diff --git a/pyproject.toml b/pyproject.toml index 4e608c36b97966..6c5f1addd5a6a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ "ifaddr==0.2.0", "Jinja2==3.1.2", "lru-dict==1.2.0", - "PyJWT==2.7.0", + "PyJWT==2.8.0", # PyJWT has loose dependency. We want the latest one. "cryptography==41.0.2", # pyOpenSSL 23.2.0 is required to work with cryptography 41+ diff --git a/requirements.txt b/requirements.txt index 84fa6a0cbb457a..e725201bb7ba0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ home-assistant-bluetooth==1.10.0 ifaddr==0.2.0 Jinja2==3.1.2 lru-dict==1.2.0 -PyJWT==2.7.0 +PyJWT==2.8.0 cryptography==41.0.2 pyOpenSSL==23.2.0 orjson==3.9.2 From e449f8e0e5ba0fe97770cffc3914ca88c10e4a12 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 19 Jul 2023 14:40:00 +0200 Subject: [PATCH 0675/1009] Remove Reolink event connection sensor (#96903) --- homeassistant/components/reolink/host.py | 9 ----- homeassistant/components/reolink/sensor.py | 34 ++----------------- homeassistant/components/reolink/strings.json | 8 ----- 3 files changed, 2 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index dac02b913152b3..81fbda63fef00c 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -432,15 +432,6 @@ def unregister_webhook(self): webhook.async_unregister(self._hass, self.webhook_id) self.webhook_id = None - @property - def event_connection(self) -> str: - """Return the event connection type.""" - if self._webhook_reachable: - return "onvif_push" - if self._long_poll_received: - return "onvif_long_poll" - return "fast_poll" - async def _async_long_polling(self, *_) -> None: """Use ONVIF long polling to immediately receive events.""" # This task will be cancelled once _async_stop_long_polling is called diff --git a/homeassistant/components/reolink/sensor.py b/homeassistant/components/reolink/sensor.py index 42758dc99290ca..af8d049dbc67e2 100644 --- a/homeassistant/components/reolink/sensor.py +++ b/homeassistant/components/reolink/sensor.py @@ -9,7 +9,6 @@ from reolink_aio.api import Host from homeassistant.components.sensor import ( - SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, @@ -63,13 +62,11 @@ async def async_setup_entry( """Set up a Reolink IP Camera.""" reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id] - entities: list[ReolinkHostSensorEntity | EventConnectionSensorEntity] = [ + async_add_entities( ReolinkHostSensorEntity(reolink_data, entity_description) for entity_description in HOST_SENSORS if entity_description.supported(reolink_data.host.api) - ] - entities.append(EventConnectionSensorEntity(reolink_data)) - async_add_entities(entities) + ) class ReolinkHostSensorEntity(ReolinkHostCoordinatorEntity, SensorEntity): @@ -92,30 +89,3 @@ def __init__( def native_value(self) -> StateType | date | datetime | Decimal: """Return the value reported by the sensor.""" return self.entity_description.value(self._host.api) - - -class EventConnectionSensorEntity(ReolinkHostCoordinatorEntity, SensorEntity): - """Reolink Event connection sensor.""" - - def __init__( - self, - reolink_data: ReolinkData, - ) -> None: - """Initialize Reolink binary sensor.""" - super().__init__(reolink_data) - self.entity_description = SensorEntityDescription( - key="event_connection", - translation_key="event_connection", - icon="mdi:swap-horizontal", - device_class=SensorDeviceClass.ENUM, - entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, - options=["onvif_push", "onvif_long_poll", "fast_poll"], - ) - - self._attr_unique_id = f"{self._host.unique_id}_{self.entity_description.key}" - - @property - def native_value(self) -> str: - """Return the value reported by the sensor.""" - return self._host.event_connection diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index c0c2094eeb9d10..7d8c3a213eb08e 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -95,14 +95,6 @@ } }, "sensor": { - "event_connection": { - "name": "Event connection", - "state": { - "onvif_push": "ONVIF push", - "onvif_long_poll": "ONVIF long poll", - "fast_poll": "Fast poll" - } - }, "wifi_signal": { "name": "Wi-Fi signal" } From 93ac340d54a731a06de7b7c639f3f01d90ac4362 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 19 Jul 2023 14:42:24 +0200 Subject: [PATCH 0676/1009] Update syrupy to 4.0.6 (#96900) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 2834ea5967271e..803d0cb90a04b6 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -31,7 +31,7 @@ pytest-xdist==3.3.1 pytest==7.3.1 requests_mock==1.11.0 respx==0.20.1 -syrupy==4.0.2 +syrupy==4.0.6 tomli==2.0.1;python_version<"3.11" tqdm==4.64.0 types-atomicwrites==1.4.5.1 From 06aeacc324a2b853cdca0b5d0e41df50238fde3c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 19 Jul 2023 14:42:35 +0200 Subject: [PATCH 0677/1009] Update black to 23.7.0 (#96901) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6f9a24d6db0653..9db1f2ae2e7e2d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: args: - --fix - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.7.0 hooks: - id: black args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index f1dde9ca022fc3..28b51fcb447b9b 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,6 +1,6 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit -black==23.3.0 +black==23.7.0 codespell==2.2.2 ruff==0.0.277 yamllint==1.32.0 From 3b501fd2d7495b0336a5e254dc0c508daec189a1 Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 19 Jul 2023 09:25:10 -0400 Subject: [PATCH 0678/1009] 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 8b24fc912f1796..dab8353c7732e1 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -22,7 +22,12 @@ 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 @@ async def async_step_reauth_confirm( ) -> 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 @@ async def async_step_reauth_confirm( 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 @@ async def async_step_reauth_confirm( 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 a416f030a05a60..25ffa0a60936f7 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 c80085367df3b60574dfeb255ed7a0a4e7e4d93e Mon Sep 17 00:00:00 2001 From: steffenrapp <88974099+steffenrapp@users.noreply.github.com> Date: Wed, 19 Jul 2023 17:55:41 +0200 Subject: [PATCH 0679/1009] Fix typo in Nuki integration (#96908) --- homeassistant/components/nuki/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nuki/strings.json b/homeassistant/components/nuki/strings.json index 11c19bbee3ff2b..19aeae989f4be0 100644 --- a/homeassistant/components/nuki/strings.json +++ b/homeassistant/components/nuki/strings.json @@ -58,7 +58,7 @@ } }, "set_continuous_mode": { - "name": "Set continuous code", + "name": "Set continuous mode", "description": "Enables or disables continuous mode on Nuki Opener.", "fields": { "enable": { From dae264f79e74266b4779ff7f6c58556f2afe94a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Jul 2023 11:22:43 -0500 Subject: [PATCH 0680/1009] Fix websocket_api _state_diff_event using json_encoder_default (#96905) --- .../components/websocket_api/messages.py | 5 ++++- tests/components/websocket_api/test_messages.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 3d85f984e9af2d..e5fd5626302503 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -180,7 +180,10 @@ def _state_diff( if old_attributes.get(key) != value: additions.setdefault(COMPRESSED_STATE_ATTRIBUTES, {})[key] = value if removed := set(old_attributes).difference(new_attributes): - diff[STATE_DIFF_REMOVALS] = {COMPRESSED_STATE_ATTRIBUTES: removed} + # sets are not JSON serializable by default so we convert to list + # here if there are any values to avoid jumping into the json_encoder_default + # for every state diff with a removed attribute + diff[STATE_DIFF_REMOVALS] = {COMPRESSED_STATE_ATTRIBUTES: list(removed)} return {ENTITY_EVENT_CHANGE: {new_state.entity_id: diff}} diff --git a/tests/components/websocket_api/test_messages.py b/tests/components/websocket_api/test_messages.py index d2102b651b7f13..6aafb9f2685723 100644 --- a/tests/components/websocket_api/test_messages.py +++ b/tests/components/websocket_api/test_messages.py @@ -222,6 +222,21 @@ async def test_state_diff_event(hass: HomeAssistant) -> None: } } + hass.states.async_set("light.window", "green", {}, context=new_context) + await hass.async_block_till_done() + last_state_event: Event = state_change_events[-1] + new_state: State = last_state_event.data["new_state"] + message = _state_diff_event(last_state_event) + + assert message == { + "c": { + "light.window": { + "+": {"lc": new_state.last_changed.timestamp(), "s": "green"}, + "-": {"a": ["new"]}, + } + } + } + async def test_message_to_json(caplog: pytest.LogCaptureFixture) -> None: """Test we can serialize websocket messages.""" From 39b242f1549df0e653e1d6418d556b49aaec65cb Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 19 Jul 2023 14:30:39 -0400 Subject: [PATCH 0681/1009] 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 16b07e91446553..aa07a5248cf6d1 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 706bf13144fda6..ab97d677678b60 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 006c9084832de6..5be91cabebf635 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 29aa89bea095d174fae39d1cf33b45eb2a54c297 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Jul 2023 13:31:48 -0500 Subject: [PATCH 0682/1009] Add lightweight API to get core state (#96860) --- homeassistant/components/api/__init__.py | 20 ++++++++++++++++++++ homeassistant/const.py | 1 + tests/components/api/test_init.py | 16 ++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 6538bd345de427..b465a6b7037256 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -18,6 +18,7 @@ URL_API, URL_API_COMPONENTS, URL_API_CONFIG, + URL_API_CORE_STATE, URL_API_ERROR_LOG, URL_API_EVENTS, URL_API_SERVICES, @@ -55,6 +56,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Register the API with the HTTP interface.""" hass.http.register_view(APIStatusView) + hass.http.register_view(APICoreStateView) hass.http.register_view(APIEventStream) hass.http.register_view(APIConfigView) hass.http.register_view(APIStatesView) @@ -84,6 +86,24 @@ def get(self, request): return self.json_message("API running.") +class APICoreStateView(HomeAssistantView): + """View to handle core state requests.""" + + url = URL_API_CORE_STATE + name = "api:core:state" + + @ha.callback + def get(self, request: web.Request) -> web.Response: + """Retrieve the current core state. + + This API is intended to be a fast and lightweight way to check if the + Home Assistant core is running. Its primary use case is for supervisor + to check if Home Assistant is running. + """ + hass: HomeAssistant = request.app["hass"] + return self.json({"state": hass.state.value}) + + class APIEventStream(HomeAssistantView): """View to handle EventStream requests.""" diff --git a/homeassistant/const.py b/homeassistant/const.py index f3d3d48fdd25ab..5394e273a4c34c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1101,6 +1101,7 @@ class UnitOfDataRate(StrEnum): URL_ROOT: Final = "/" URL_API: Final = "/api/" URL_API_STREAM: Final = "/api/stream" +URL_API_CORE_STATE: Final = "/api/core/state" URL_API_CONFIG: Final = "/api/config" URL_API_STATES: Final = "/api/states" URL_API_STATES_ENTITY: Final = "/api/states/{}" diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index 61da000fc077c9..5ba9d60996bcc9 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -678,3 +678,19 @@ def listener(service_call): "/api/services/test_domain/test_service", json={"hello": 5} ) assert resp.status == HTTPStatus.BAD_REQUEST + + +async def test_api_status(hass: HomeAssistant, mock_api_client: TestClient) -> None: + """Test getting the api status.""" + resp = await mock_api_client.get("/api/") + assert resp.status == HTTPStatus.OK + json = await resp.json() + assert json["message"] == "API running." + + +async def test_api_core_state(hass: HomeAssistant, mock_api_client: TestClient) -> None: + """Test getting core status.""" + resp = await mock_api_client.get("/api/core/state") + assert resp.status == HTTPStatus.OK + json = await resp.json() + assert json["state"] == "RUNNING" From 0f4c71f9934681c2a20f9f7cf14e78755b5125a5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 19 Jul 2023 20:37:33 +0200 Subject: [PATCH 0683/1009] Handle nullable context in Spotify (#96913) --- homeassistant/components/spotify/media_player.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 3c2f9ef729cadf..952e6c606c2cf3 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -398,7 +398,7 @@ def update(self) -> None: ) self._currently_playing = current or {} - context = self._currently_playing.get("context", {}) + context = self._currently_playing.get("context") or {} # For some users in some cases, the uri is formed like # "spotify:user:{name}:playlist:{id}" and spotipy wants @@ -409,9 +409,7 @@ def update(self) -> None: if len(parts) == 5 and parts[1] == "user" and parts[3] == "playlist": uri = ":".join([parts[0], parts[3], parts[4]]) - if context is not None and ( - self._playlist is None or self._playlist["uri"] != uri - ): + if context and (self._playlist is None or self._playlist["uri"] != uri): self._playlist = None if context["type"] == MediaType.PLAYLIST: self._playlist = self.data.client.playlist(uri) From deafdc3005ca8150a5f777776d37beaf836046ce Mon Sep 17 00:00:00 2001 From: Guy Martin Date: Wed, 19 Jul 2023 22:11:05 +0200 Subject: [PATCH 0684/1009] Allow match quirk_class of custom quirks to ZHA (#93268) * Allow matching custom quirks when self.quirk_classes might not contain the full class path but only the module and the class. * Add test for matching custom quirk classes. --- homeassistant/components/zha/core/registries.py | 5 ++++- tests/components/zha/test_registries.py | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 0c7369f15e7964..03fdc7e37c1f0e 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -244,7 +244,10 @@ def _matched( if callable(self.quirk_classes): matches.append(self.quirk_classes(quirk_class)) else: - matches.append(quirk_class in self.quirk_classes) + matches.append( + quirk_class.split(".")[-2:] + in [x.split(".")[-2:] for x in self.quirk_classes] + ) return matches diff --git a/tests/components/zha/test_registries.py b/tests/components/zha/test_registries.py index 057921f80a9f5c..6f36ee624e9d63 100644 --- a/tests/components/zha/test_registries.py +++ b/tests/components/zha/test_registries.py @@ -18,7 +18,8 @@ MANUFACTURER = "mock manufacturer" MODEL = "mock model" -QUIRK_CLASS = "mock.class" +QUIRK_CLASS = "mock.test.quirk.class" +QUIRK_CLASS_SHORT = "quirk.class" @pytest.fixture @@ -209,6 +210,12 @@ def cluster_handlers(cluster_handler): ), False, ), + ( + registries.MatchRule( + cluster_handler_names="on_off", quirk_classes=QUIRK_CLASS_SHORT + ), + True, + ), ], ) def test_registry_matching(rule, matched, cluster_handlers) -> None: From daa53118b3ae9a2509dd909cc2e6cae63418aefe Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 19 Jul 2023 23:58:31 +0200 Subject: [PATCH 0685/1009] Correct invalid docstring in gardena button (#96922) --- homeassistant/components/gardena_bluetooth/button.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/gardena_bluetooth/button.py b/homeassistant/components/gardena_bluetooth/button.py index cfaa4d72c2a76a..b984d3420ae540 100644 --- a/homeassistant/components/gardena_bluetooth/button.py +++ b/homeassistant/components/gardena_bluetooth/button.py @@ -40,7 +40,7 @@ class GardenaBluetoothButtonEntityDescription(ButtonEntityDescription): async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up binary sensor based on a config entry.""" + """Set up button based on a config entry.""" coordinator: Coordinator = hass.data[DOMAIN][entry.entry_id] entities = [ GardenaBluetoothButton(coordinator, description) @@ -51,7 +51,7 @@ async def async_setup_entry( class GardenaBluetoothButton(GardenaBluetoothDescriptorEntity, ButtonEntity): - """Representation of a binary sensor.""" + """Representation of a button.""" entity_description: GardenaBluetoothButtonEntityDescription From f310d6ca582b9111642cc131fbc7daf4dafe01eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Jul 2023 17:04:46 -0500 Subject: [PATCH 0686/1009] Bump bleak-retry-connector to 3.1.0 (#96917) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index f4c690dcffc556..cbe4bf9069cc2c 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -15,7 +15,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.20.2", - "bleak-retry-connector==3.0.2", + "bleak-retry-connector==3.1.0", "bluetooth-adapters==0.16.0", "bluetooth-auto-recovery==1.2.1", "bluetooth-data-tools==1.6.0", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5c94730b5db95a..b4126b1c261dff 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ atomicwrites-homeassistant==1.4.1 attrs==22.2.0 awesomeversion==22.9.0 bcrypt==4.0.1 -bleak-retry-connector==3.0.2 +bleak-retry-connector==3.1.0 bleak==0.20.2 bluetooth-adapters==0.16.0 bluetooth-auto-recovery==1.2.1 diff --git a/requirements_all.txt b/requirements_all.txt index ab97d677678b60..64653cdb3f0066 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -503,7 +503,7 @@ bimmer-connected==0.13.8 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==3.0.2 +bleak-retry-connector==3.1.0 # homeassistant.components.bluetooth bleak==0.20.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5be91cabebf635..b948846cdde288 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -424,7 +424,7 @@ bellows==0.35.8 bimmer-connected==0.13.8 # homeassistant.components.bluetooth -bleak-retry-connector==3.0.2 +bleak-retry-connector==3.1.0 # homeassistant.components.bluetooth bleak==0.20.2 From 955bed0128e3e04177a0526a8eda4c75f5c63fb4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Jul 2023 18:39:50 -0500 Subject: [PATCH 0687/1009] Bump aioesphomeapi to 15.1.12 (#96924) --- 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 a8324ed770ddba..6a4c1a66334d7b 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.1.11", + "aioesphomeapi==15.1.12", "bluetooth-data-tools==1.6.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 64653cdb3f0066..5d4d9648d7199d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==15.1.11 +aioesphomeapi==15.1.12 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b948846cdde288..649c25c4d6ba05 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.1.11 +aioesphomeapi==15.1.12 # homeassistant.components.flo aioflo==2021.11.0 From 6bb81b862cbd47a019341c2d056a5837e8c9dbd1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Jul 2023 19:22:38 -0500 Subject: [PATCH 0688/1009] Add a message to the config entry cancel call (#96925) --- homeassistant/config_entries.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 825064e541097f..eccac004b7ee4c 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -692,8 +692,9 @@ async def _async_process_on_unload(self, hass: HomeAssistant) -> None: if not self._tasks and not self._background_tasks: return + cancel_message = f"Config entry {self.title} with {self.domain} unloading" for task in self._background_tasks: - task.cancel() + task.cancel(cancel_message) _, pending = await asyncio.wait( [*self._tasks, *self._background_tasks], timeout=10 @@ -885,7 +886,9 @@ async def async_shutdown(self) -> None: """Cancel any initializing flows.""" for task_list in self._initialize_tasks.values(): for task in task_list: - task.cancel() + task.cancel( + "Config entry initialize canceled: Home Assistant is shutting down" + ) await self._discovery_debouncer.async_shutdown() async def async_finish_flow( From 822d840f81238a820244a76a8378f8d2211abc90 Mon Sep 17 00:00:00 2001 From: Renier Moorcroft <66512715+RenierM26@users.noreply.github.com> Date: Thu, 20 Jul 2023 08:25:54 +0200 Subject: [PATCH 0689/1009] EZVIZ NumberEntity async added to hass (#96930) Update number.py --- homeassistant/components/ezviz/number.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ezviz/number.py b/homeassistant/components/ezviz/number.py index 77c5146cefa69a..74d496ef6c1557 100644 --- a/homeassistant/components/ezviz/number.py +++ b/homeassistant/components/ezviz/number.py @@ -97,7 +97,7 @@ def __init__( async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" - self.schedule_update_ha_state(True) + self.async_schedule_update_ha_state(True) @property def native_value(self) -> float | None: From 23810752edfea60129bcb61e8edaed4651321cbd Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 20 Jul 2023 08:31:37 +0200 Subject: [PATCH 0690/1009] Fix mock assert_called_with (#96929) * Fix mock assert_called_with * Fix sonos test * Revert zeroconf test changes --- tests/components/esphome/test_voice_assistant.py | 2 +- tests/components/sonos/conftest.py | 3 +++ tests/components/sonos/test_number.py | 14 +++++++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index 322e057ec15ca0..4188e375907092 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -273,7 +273,7 @@ async def test_error_event_type( ) ) - assert voice_assistant_udp_server_v1.handle_event.called_with( + voice_assistant_udp_server_v1.handle_event.assert_called_with( VoiceAssistantEventType.VOICE_ASSISTANT_ERROR, {"code": "code", "message": "message"}, ) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 730f0f5e8f33ef..bab2b89009f575 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -107,6 +107,9 @@ def config_entry_fixture(): class MockSoCo(MagicMock): """Mock the Soco Object.""" + audio_delay = 2 + sub_gain = 5 + @property def visible_zones(self): """Return visible zones and allow property to be overridden by device classes.""" diff --git a/tests/components/sonos/test_number.py b/tests/components/sonos/test_number.py index d5da2af629e183..38456058d8a155 100644 --- a/tests/components/sonos/test_number.py +++ b/tests/components/sonos/test_number.py @@ -1,5 +1,5 @@ """Tests for the Sonos number platform.""" -from unittest.mock import patch +from unittest.mock import PropertyMock, patch from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE from homeassistant.const import ATTR_ENTITY_ID @@ -37,24 +37,28 @@ async def test_number_entities( music_surround_level_state = hass.states.get(music_surround_level_number.entity_id) assert music_surround_level_state.state == "4" - with patch("soco.SoCo.audio_delay") as mock_audio_delay: + with patch.object( + type(soco), "audio_delay", new_callable=PropertyMock + ) as mock_audio_delay: await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, {ATTR_ENTITY_ID: audio_delay_number.entity_id, "value": 3}, blocking=True, ) - assert mock_audio_delay.called_with(3) + mock_audio_delay.assert_called_once_with(3) sub_gain_number = entity_registry.entities["number.zone_a_sub_gain"] sub_gain_state = hass.states.get(sub_gain_number.entity_id) assert sub_gain_state.state == "5" - with patch("soco.SoCo.sub_gain") as mock_sub_gain: + with patch.object( + type(soco), "sub_gain", new_callable=PropertyMock + ) as mock_sub_gain: await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, {ATTR_ENTITY_ID: sub_gain_number.entity_id, "value": -8}, blocking=True, ) - assert mock_sub_gain.called_with(-8) + mock_sub_gain.assert_called_once_with(-8) From 9da155955ab1b1b3d20a64f495ae3d18e17b9bb8 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 20 Jul 2023 16:35:26 +1000 Subject: [PATCH 0691/1009] Transport NSW: Set DeviceClass and StateClass (#96928) * 2023.7.16 - Fix bug with values defaulting to "n/a" in stead of None * 2023.7.16 - Set device class and state classes on entities * 2023.7.16 - Set StateClass and DeviceClass directly on the entitiy * 2023.7.16 - Fix black and ruff issues * 2023.7.17 - Update logic catering for the 'n/a' response on an API failure - Add testcase * - Fix bug in formatting * 2023.7.17 - Refacotr to consider the "n/a" response returned from the Python lib on an error or faliure - Remove setting of StateClass and DeviceClass as requested - Add "n/a" test case * 2023.7.17 - Remove unused imports * 2023.7.18 - Apply review requested changes * - Additional review change resolved * Add State and Device class attributes --- homeassistant/components/transport_nsw/sensor.py | 4 ++++ tests/components/transport_nsw/test_sensor.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index 0a740ec4347a0f..520b1a5626bad8 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -8,7 +8,9 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.const import ATTR_MODE, CONF_API_KEY, CONF_NAME, UnitOfTime from homeassistant.core import HomeAssistant @@ -73,6 +75,8 @@ class TransportNSWSensor(SensorEntity): """Implementation of an Transport NSW sensor.""" _attr_attribution = "Data provided by Transport NSW" + _attr_device_class = SensorDeviceClass.DURATION + _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, data, stop_id, name): """Initialize the sensor.""" diff --git a/tests/components/transport_nsw/test_sensor.py b/tests/components/transport_nsw/test_sensor.py index f9ead2a3054ca8..46aee182b53327 100644 --- a/tests/components/transport_nsw/test_sensor.py +++ b/tests/components/transport_nsw/test_sensor.py @@ -1,6 +1,10 @@ """The tests for the Transport NSW (AU) sensor platform.""" from unittest.mock import patch +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass, +) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -42,6 +46,8 @@ async def test_transportnsw_config(mocked_get_departures, hass: HomeAssistant) - assert state.attributes["real_time"] == "y" assert state.attributes["destination"] == "Palm Beach" assert state.attributes["mode"] == "Bus" + assert state.attributes["device_class"] == SensorDeviceClass.DURATION + assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT def get_departuresMock_notFound(_stop_id, route, destination, api_key): From 1c19c54e380539e303e58382087a59be9fbd25b4 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 20 Jul 2023 08:47:26 +0200 Subject: [PATCH 0692/1009] Avoid accessing coordinator in gardena_bluetooth tests (#96921) Avoid accessing coordinator in tests --- .../components/gardena_bluetooth/__init__.py | 6 +--- .../components/gardena_bluetooth/conftest.py | 33 ++++++++++++++----- .../snapshots/test_sensor.ambr | 2 +- .../gardena_bluetooth/test_binary_sensor.py | 7 ++-- .../gardena_bluetooth/test_button.py | 6 ++-- .../gardena_bluetooth/test_number.py | 11 ++++--- .../gardena_bluetooth/test_sensor.py | 7 ++-- .../gardena_bluetooth/test_switch.py | 6 ++-- 8 files changed, 51 insertions(+), 27 deletions(-) diff --git a/tests/components/gardena_bluetooth/__init__.py b/tests/components/gardena_bluetooth/__init__.py index a5ea94088fd522..7de0780e129c2a 100644 --- a/tests/components/gardena_bluetooth/__init__.py +++ b/tests/components/gardena_bluetooth/__init__.py @@ -2,8 +2,6 @@ from unittest.mock import patch -from homeassistant.components.gardena_bluetooth.const import DOMAIN -from homeassistant.components.gardena_bluetooth.coordinator import Coordinator from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo @@ -74,7 +72,7 @@ async def setup_entry( hass: HomeAssistant, mock_entry: MockConfigEntry, platforms: list[Platform] -) -> Coordinator: +) -> None: """Make sure the device is available.""" inject_bluetooth_service_info(hass, WATER_TIMER_SERVICE_INFO) @@ -83,5 +81,3 @@ async def setup_entry( mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - - return hass.data[DOMAIN][mock_entry.entry_id] diff --git a/tests/components/gardena_bluetooth/conftest.py b/tests/components/gardena_bluetooth/conftest.py index a1d31c45807986..98ae41d195ba9c 100644 --- a/tests/components/gardena_bluetooth/conftest.py +++ b/tests/components/gardena_bluetooth/conftest.py @@ -1,5 +1,5 @@ """Common fixtures for the Gardena Bluetooth tests.""" -from collections.abc import Generator +from collections.abc import Awaitable, Callable, Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch @@ -11,11 +11,13 @@ import pytest from homeassistant.components.gardena_bluetooth.const import DOMAIN +from homeassistant.components.gardena_bluetooth.coordinator import SCAN_INTERVAL from homeassistant.const import CONF_ADDRESS +from homeassistant.core import HomeAssistant from . import WATER_TIMER_SERVICE_INFO -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture @@ -45,8 +47,27 @@ def mock_read_char_raw(): } +@pytest.fixture +async def scan_step( + hass: HomeAssistant, +) -> Generator[None, None, Callable[[], Awaitable[None]]]: + """Step system time forward.""" + + with freeze_time("2023-01-01", tz_offset=1) as frozen_time: + + async def delay(): + """Trigger delay in system.""" + frozen_time.tick(delta=SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + yield delay + + @pytest.fixture(autouse=True) -def mock_client(enable_bluetooth: None, mock_read_char_raw: dict[str, Any]) -> None: +def mock_client( + enable_bluetooth: None, scan_step, mock_read_char_raw: dict[str, Any] +) -> None: """Auto mock bluetooth.""" client = Mock(spec_set=Client) @@ -82,11 +103,7 @@ def _all_char(): with patch( "homeassistant.components.gardena_bluetooth.config_flow.Client", return_value=client, - ), patch( - "homeassistant.components.gardena_bluetooth.Client", return_value=client - ), freeze_time( - "2023-01-01", tz_offset=1 - ): + ), patch("homeassistant.components.gardena_bluetooth.Client", return_value=client): yield client diff --git a/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr b/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr index 5a23b6d7f504ca..14135cb390c88c 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr @@ -22,7 +22,7 @@ 'entity_id': 'sensor.mock_title_valve_closing', 'last_changed': , 'last_updated': , - 'state': '2023-01-01T01:00:10+00:00', + 'state': '2023-01-01T01:01:10+00:00', }) # --- # name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-sensor.mock_title_valve_closing].2 diff --git a/tests/components/gardena_bluetooth/test_binary_sensor.py b/tests/components/gardena_bluetooth/test_binary_sensor.py index cda24f871e8952..d12f825b1a7004 100644 --- a/tests/components/gardena_bluetooth/test_binary_sensor.py +++ b/tests/components/gardena_bluetooth/test_binary_sensor.py @@ -1,6 +1,8 @@ """Test Gardena Bluetooth binary sensor.""" +from collections.abc import Awaitable, Callable + from gardena_bluetooth.const import Valve import pytest from syrupy.assertion import SnapshotAssertion @@ -28,6 +30,7 @@ async def test_setup( snapshot: SnapshotAssertion, mock_entry: MockConfigEntry, mock_read_char_raw: dict[str, bytes], + scan_step: Callable[[], Awaitable[None]], uuid: str, raw: list[bytes], entity_id: str, @@ -35,10 +38,10 @@ async def test_setup( """Test setup creates expected entities.""" mock_read_char_raw[uuid] = raw[0] - coordinator = await setup_entry(hass, mock_entry, [Platform.BINARY_SENSOR]) + await setup_entry(hass, mock_entry, [Platform.BINARY_SENSOR]) assert hass.states.get(entity_id) == snapshot for char_raw in raw[1:]: mock_read_char_raw[uuid] = char_raw - await coordinator.async_refresh() + await scan_step() assert hass.states.get(entity_id) == snapshot diff --git a/tests/components/gardena_bluetooth/test_button.py b/tests/components/gardena_bluetooth/test_button.py index e184a2ecce88d8..52fa3d4b00e132 100644 --- a/tests/components/gardena_bluetooth/test_button.py +++ b/tests/components/gardena_bluetooth/test_button.py @@ -1,6 +1,7 @@ """Test Gardena Bluetooth sensor.""" +from collections.abc import Awaitable, Callable from unittest.mock import Mock, call from gardena_bluetooth.const import Reset @@ -31,15 +32,16 @@ async def test_setup( snapshot: SnapshotAssertion, mock_entry: MockConfigEntry, mock_switch_chars: dict[str, bytes], + scan_step: Callable[[], Awaitable[None]], ) -> None: """Test setup creates expected entities.""" entity_id = "button.mock_title_factory_reset" - coordinator = await setup_entry(hass, mock_entry, [Platform.BUTTON]) + await setup_entry(hass, mock_entry, [Platform.BUTTON]) assert hass.states.get(entity_id) == snapshot mock_switch_chars[Reset.factory_reset.uuid] = b"\x01" - await coordinator.async_refresh() + await scan_step() assert hass.states.get(entity_id) == snapshot diff --git a/tests/components/gardena_bluetooth/test_number.py b/tests/components/gardena_bluetooth/test_number.py index 588b73aadbbd5c..3b04d0cc818011 100644 --- a/tests/components/gardena_bluetooth/test_number.py +++ b/tests/components/gardena_bluetooth/test_number.py @@ -1,6 +1,7 @@ """Test Gardena Bluetooth sensor.""" +from collections.abc import Awaitable, Callable from typing import Any from unittest.mock import Mock, call @@ -62,6 +63,7 @@ async def test_setup( snapshot: SnapshotAssertion, mock_entry: MockConfigEntry, mock_read_char_raw: dict[str, bytes], + scan_step: Callable[[], Awaitable[None]], uuid: str, raw: list[bytes], entity_id: str, @@ -69,12 +71,12 @@ async def test_setup( """Test setup creates expected entities.""" mock_read_char_raw[uuid] = raw[0] - coordinator = await setup_entry(hass, mock_entry, [Platform.NUMBER]) + await setup_entry(hass, mock_entry, [Platform.NUMBER]) assert hass.states.get(entity_id) == snapshot for char_raw in raw[1:]: mock_read_char_raw[uuid] = char_raw - await coordinator.async_refresh() + await scan_step() assert hass.states.get(entity_id) == snapshot @@ -128,6 +130,7 @@ async def test_bluetooth_error_unavailable( snapshot: SnapshotAssertion, mock_entry: MockConfigEntry, mock_read_char_raw: dict[str, bytes], + scan_step: Callable[[], Awaitable[None]], ) -> None: """Verify that a connectivity error makes all entities unavailable.""" @@ -138,7 +141,7 @@ async def test_bluetooth_error_unavailable( Valve.remaining_open_time.uuid ] = Valve.remaining_open_time.encode(0) - coordinator = await setup_entry(hass, mock_entry, [Platform.NUMBER]) + await setup_entry(hass, mock_entry, [Platform.NUMBER]) assert hass.states.get("number.mock_title_remaining_open_time") == snapshot assert hass.states.get("number.mock_title_manual_watering_time") == snapshot @@ -146,6 +149,6 @@ async def test_bluetooth_error_unavailable( "Test for errors on bluetooth" ) - await coordinator.async_refresh() + await scan_step() assert hass.states.get("number.mock_title_remaining_open_time") == snapshot assert hass.states.get("number.mock_title_manual_watering_time") == snapshot diff --git a/tests/components/gardena_bluetooth/test_sensor.py b/tests/components/gardena_bluetooth/test_sensor.py index e9fd452e6a23fb..307a9467f00d18 100644 --- a/tests/components/gardena_bluetooth/test_sensor.py +++ b/tests/components/gardena_bluetooth/test_sensor.py @@ -1,5 +1,5 @@ """Test Gardena Bluetooth sensor.""" - +from collections.abc import Awaitable, Callable from gardena_bluetooth.const import Battery, Valve import pytest @@ -37,6 +37,7 @@ async def test_setup( snapshot: SnapshotAssertion, mock_entry: MockConfigEntry, mock_read_char_raw: dict[str, bytes], + scan_step: Callable[[], Awaitable[None]], uuid: str, raw: list[bytes], entity_id: str, @@ -44,10 +45,10 @@ async def test_setup( """Test setup creates expected entities.""" mock_read_char_raw[uuid] = raw[0] - coordinator = await setup_entry(hass, mock_entry, [Platform.SENSOR]) + await setup_entry(hass, mock_entry, [Platform.SENSOR]) assert hass.states.get(entity_id) == snapshot for char_raw in raw[1:]: mock_read_char_raw[uuid] = char_raw - await coordinator.async_refresh() + await scan_step() assert hass.states.get(entity_id) == snapshot diff --git a/tests/components/gardena_bluetooth/test_switch.py b/tests/components/gardena_bluetooth/test_switch.py index c2571b7a588fe4..40e8c14833524a 100644 --- a/tests/components/gardena_bluetooth/test_switch.py +++ b/tests/components/gardena_bluetooth/test_switch.py @@ -1,6 +1,7 @@ """Test Gardena Bluetooth sensor.""" +from collections.abc import Awaitable, Callable from unittest.mock import Mock, call from gardena_bluetooth.const import Valve @@ -40,15 +41,16 @@ async def test_setup( mock_entry: MockConfigEntry, mock_client: Mock, mock_switch_chars: dict[str, bytes], + scan_step: Callable[[], Awaitable[None]], ) -> None: """Test setup creates expected entities.""" entity_id = "switch.mock_title_open" - coordinator = await setup_entry(hass, mock_entry, [Platform.SWITCH]) + await setup_entry(hass, mock_entry, [Platform.SWITCH]) assert hass.states.get(entity_id) == snapshot mock_switch_chars[Valve.state.uuid] = b"\x01" - await coordinator.async_refresh() + await scan_step() assert hass.states.get(entity_id) == snapshot From 660c95d78409ebe828e8c231ab774e8f19d162cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Jul 2023 02:59:17 -0500 Subject: [PATCH 0693/1009] Pre-split unifiprotect nested attribute lookups (#96862) * Pre-split unifiprotect nested attribute lookups replaces and closes #96631 * Pre-split unifiprotect nested attribute lookups replaces and closes #96631 * comments --- .../components/unifiprotect/models.py | 64 +++++++++++++++---- .../components/unifiprotect/utils.py | 8 +-- 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index 375784d0323e69..c250a021340907 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from enum import Enum import logging -from typing import Any, Generic, TypeVar, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast from pyunifiprotect.data import NVR, Event, ProtectAdoptableDeviceModel @@ -19,6 +19,15 @@ T = TypeVar("T", bound=ProtectAdoptableDeviceModel | NVR) +def split_tuple(value: tuple[str, ...] | str | None) -> tuple[str, ...] | None: + """Split string to tuple.""" + if value is None: + return None + if TYPE_CHECKING: + assert isinstance(value, str) + return tuple(value.split(".")) + + class PermRequired(int, Enum): """Type of permission level required for entity.""" @@ -31,18 +40,34 @@ class PermRequired(int, Enum): class ProtectRequiredKeysMixin(EntityDescription, Generic[T]): """Mixin for required keys.""" - ufp_required_field: str | None = None - ufp_value: str | None = None + # `ufp_required_field`, `ufp_value`, and `ufp_enabled` are defined as + # a `str` in the dataclass, but `__post_init__` converts it to a + # `tuple[str, ...]` to avoid doing it at run time in `get_nested_attr` + # which is usually called millions of times per day. + ufp_required_field: tuple[str, ...] | str | None = None + ufp_value: tuple[str, ...] | str | None = None ufp_value_fn: Callable[[T], Any] | None = None - ufp_enabled: str | None = None + ufp_enabled: tuple[str, ...] | str | None = None ufp_perm: PermRequired | None = None + def __post_init__(self) -> None: + """Pre-convert strings to tuples for faster get_nested_attr.""" + self.ufp_required_field = split_tuple(self.ufp_required_field) + self.ufp_value = split_tuple(self.ufp_value) + self.ufp_enabled = split_tuple(self.ufp_enabled) + def get_ufp_value(self, obj: T) -> Any: """Return value from UniFi Protect device.""" - if self.ufp_value is not None: - return get_nested_attr(obj, self.ufp_value) - if self.ufp_value_fn is not None: - return self.ufp_value_fn(obj) + if (ufp_value := self.ufp_value) is not None: + if TYPE_CHECKING: + # `ufp_value` is defined as a `str` in the dataclass, but + # `__post_init__` converts it to a `tuple[str, ...]` to avoid + # doing it at run time in `get_nested_attr` which is usually called + # millions of times per day. This tells mypy that it's a tuple. + assert isinstance(ufp_value, tuple) + return get_nested_attr(obj, ufp_value) + if (ufp_value_fn := self.ufp_value_fn) is not None: + return ufp_value_fn(obj) # reminder for future that one is required raise RuntimeError( # pragma: no cover @@ -51,16 +76,27 @@ def get_ufp_value(self, obj: T) -> Any: def get_ufp_enabled(self, obj: T) -> bool: """Return value from UniFi Protect device.""" - if self.ufp_enabled is not None: - return bool(get_nested_attr(obj, self.ufp_enabled)) + if (ufp_enabled := self.ufp_enabled) is not None: + if TYPE_CHECKING: + # `ufp_enabled` is defined as a `str` in the dataclass, but + # `__post_init__` converts it to a `tuple[str, ...]` to avoid + # doing it at run time in `get_nested_attr` which is usually called + # millions of times per day. This tells mypy that it's a tuple. + assert isinstance(ufp_enabled, tuple) + return bool(get_nested_attr(obj, ufp_enabled)) return True def has_required(self, obj: T) -> bool: """Return if has required field.""" - - if self.ufp_required_field is None: + if (ufp_required_field := self.ufp_required_field) is None: return True - return bool(get_nested_attr(obj, self.ufp_required_field)) + if TYPE_CHECKING: + # `ufp_required_field` is defined as a `str` in the dataclass, but + # `__post_init__` converts it to a `tuple[str, ...]` to avoid + # doing it at run time in `get_nested_attr` which is usually called + # millions of times per day. This tells mypy that it's a tuple. + assert isinstance(ufp_required_field, tuple) + return bool(get_nested_attr(obj, ufp_required_field)) @dataclass @@ -73,7 +109,7 @@ def get_event_obj(self, obj: T) -> Event | None: """Return value from UniFi Protect device.""" if self.ufp_event_obj is not None: - return cast(Event, get_nested_attr(obj, self.ufp_event_obj)) + return cast(Event, getattr(obj, self.ufp_event_obj, None)) return None def get_is_on(self, event: Event | None) -> bool: diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index e0c56cfd5fc486..3e2b5e1b19e1fe 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -41,13 +41,13 @@ _SENTINEL = object() -def get_nested_attr(obj: Any, attr: str) -> Any: +def get_nested_attr(obj: Any, attrs: tuple[str, ...]) -> Any: """Fetch a nested attribute.""" - if "." not in attr: - value = getattr(obj, attr, None) + if len(attrs) == 1: + value = getattr(obj, attrs[0], None) else: value = obj - for key in attr.split("."): + for key in attrs: if (value := getattr(value, key, _SENTINEL)) is _SENTINEL: return None From 0349e47372257c8a58deb379f13f4f2caa5bb18d Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Thu, 20 Jul 2023 10:01:19 +0200 Subject: [PATCH 0694/1009] Add support for MiScale V2 (#96807) * Add support for MiScale V2 * Add icon to impedance * Reduce mass sensors --- .../components/xiaomi_ble/manifest.json | 6 +- homeassistant/components/xiaomi_ble/sensor.py | 23 +++++++ homeassistant/generated/bluetooth.py | 5 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/xiaomi_ble/__init__.py | 16 +++++ tests/components/xiaomi_ble/test_sensor.py | 64 ++++++++++++++++++- 7 files changed, 114 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 69a95ea8a9c427..73b22ddab9fc23 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -2,6 +2,10 @@ "domain": "xiaomi_ble", "name": "Xiaomi BLE", "bluetooth": [ + { + "connectable": false, + "service_data_uuid": "0000181b-0000-1000-8000-00805f9b34fb" + }, { "connectable": false, "service_data_uuid": "0000fd50-0000-1000-8000-00805f9b34fb" @@ -16,5 +20,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "iot_class": "local_push", - "requirements": ["xiaomi-ble==0.17.2"] + "requirements": ["xiaomi-ble==0.18.2"] } diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index 81739db4d11923..84ef91bf5a81a8 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -24,6 +24,7 @@ SIGNAL_STRENGTH_DECIBELS_MILLIWATT, EntityCategory, UnitOfElectricPotential, + UnitOfMass, UnitOfPressure, UnitOfTemperature, ) @@ -68,6 +69,28 @@ native_unit_of_measurement=LIGHT_LUX, state_class=SensorStateClass.MEASUREMENT, ), + # Impedance sensor (ohm) + (DeviceClass.IMPEDANCE, Units.OHM): SensorEntityDescription( + key=f"{DeviceClass.IMPEDANCE}_{Units.OHM}", + icon="mdi:omega", + native_unit_of_measurement=Units.OHM, + state_class=SensorStateClass.MEASUREMENT, + ), + # Mass sensor (kg) + (DeviceClass.MASS, Units.MASS_KILOGRAMS): SensorEntityDescription( + key=f"{DeviceClass.MASS}_{Units.MASS_KILOGRAMS}", + device_class=SensorDeviceClass.WEIGHT, + native_unit_of_measurement=UnitOfMass.KILOGRAMS, + state_class=SensorStateClass.MEASUREMENT, + ), + # Mass non stabilized sensor (kg) + (DeviceClass.MASS_NON_STABILIZED, Units.MASS_KILOGRAMS): SensorEntityDescription( + key=f"{DeviceClass.MASS_NON_STABILIZED}_{Units.MASS_KILOGRAMS}", + device_class=SensorDeviceClass.WEIGHT, + native_unit_of_measurement=UnitOfMass.KILOGRAMS, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), (DeviceClass.MOISTURE, Units.PERCENTAGE): SensorEntityDescription( key=f"{DeviceClass.MOISTURE}_{Units.PERCENTAGE}", device_class=SensorDeviceClass.MOISTURE, diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 64fae252975a66..aba97c8ea8cc02 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -504,6 +504,11 @@ ], "manufacturer_id": 76, }, + { + "connectable": False, + "domain": "xiaomi_ble", + "service_data_uuid": "0000181b-0000-1000-8000-00805f9b34fb", + }, { "connectable": False, "domain": "xiaomi_ble", diff --git a/requirements_all.txt b/requirements_all.txt index 5d4d9648d7199d..d57d0cc1c69bbb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2684,7 +2684,7 @@ wyoming==1.1.0 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.17.2 +xiaomi-ble==0.18.2 # homeassistant.components.knx xknx==2.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 649c25c4d6ba05..bc665abf4c8898 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1966,7 +1966,7 @@ wyoming==1.1.0 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.17.2 +xiaomi-ble==0.18.2 # homeassistant.components.knx xknx==2.11.1 diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py index ea11feab9c2931..879ab4f7bc4aa2 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -105,6 +105,22 @@ connectable=False, ) +MISCALE_V2_SERVICE_INFO = BluetoothServiceInfoBleak( + name="MIBFS", + address="50:FB:19:1B:B5:DC", + device=generate_ble_device("00:00:00:00:00:00", None), + rssi=-60, + manufacturer_data={}, + service_data={ + "0000181b-0000-1000-8000-00805f9b34fb": b"\x02\xa6\xe7\x07\x07\x07\x0b\x1f\x1d\x1f\x02\xfa-" + }, + service_uuids=["0000181b-0000-1000-8000-00805f9b34fb"], + source="local", + advertisement=generate_advertisement_data(local_name="Not it"), + time=0, + connectable=False, +) + MISSING_PAYLOAD_ENCRYPTED = BluetoothServiceInfoBleak( name="LYWSD02MMC", address="A4:C1:38:56:53:84", diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 1d6344063b5fa3..40d89a8214d2af 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -4,7 +4,12 @@ from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant -from . import HHCCJCY10_SERVICE_INFO, MMC_T201_1_SERVICE_INFO, make_advertisement +from . import ( + HHCCJCY10_SERVICE_INFO, + MISCALE_V2_SERVICE_INFO, + MMC_T201_1_SERVICE_INFO, + make_advertisement, +) from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info_bleak @@ -506,3 +511,60 @@ async def test_hhccjcy10_uuid(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_miscale_v2_uuid(hass: HomeAssistant) -> None: + """Test MiScale V2 UUID. + + This device uses a different UUID compared to the other Xiaomi sensors. + """ + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="50:FB:19:1B:B5:DC", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + inject_bluetooth_service_info_bleak(hass, MISCALE_V2_SERVICE_INFO) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 3 + + mass_non_stabilized_sensor = hass.states.get( + "sensor.mi_body_composition_scale_2_b5dc_mass_non_stabilized" + ) + mass_non_stabilized_sensor_attr = mass_non_stabilized_sensor.attributes + assert mass_non_stabilized_sensor.state == "58.85" + assert ( + mass_non_stabilized_sensor_attr[ATTR_FRIENDLY_NAME] + == "Mi Body Composition Scale 2 (B5DC) Mass Non Stabilized" + ) + assert mass_non_stabilized_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "kg" + assert mass_non_stabilized_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + mass_sensor = hass.states.get("sensor.mi_body_composition_scale_2_b5dc_mass") + mass_sensor_attr = mass_sensor.attributes + assert mass_sensor.state == "58.85" + assert ( + mass_sensor_attr[ATTR_FRIENDLY_NAME] + == "Mi Body Composition Scale 2 (B5DC) Mass" + ) + assert mass_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "kg" + assert mass_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + impedance_sensor = hass.states.get( + "sensor.mi_body_composition_scale_2_b5dc_impedance" + ) + impedance_sensor_attr = impedance_sensor.attributes + assert impedance_sensor.state == "543" + assert ( + impedance_sensor_attr[ATTR_FRIENDLY_NAME] + == "Mi Body Composition Scale 2 (B5DC) Impedance" + ) + assert impedance_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "ohm" + assert impedance_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 5ffffd8dbc0d8b0e27d1532404998f8683766edf Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Thu, 20 Jul 2023 01:06:16 -0700 Subject: [PATCH 0695/1009] Fully unload wemo config entry (#96620) * Fully unload wemo config entity * Test reloading the config entry * Encapsulate data with dataclasses * Fix missing test coverage * Replace if with assert for options that are always set * Move WemoData/WemoConfigEntryData to models.py * Use _ to indicate unused argument * Test that the entry and entity work after reloading * Nit: Slight test reordering * Reset the correct mock (get_state) * from .const import DOMAIN * Nit: _async_wemo_data -> async_wemo_data; not module private --- homeassistant/components/wemo/__init__.py | 142 +++++++++++------- .../components/wemo/binary_sensor.py | 15 +- homeassistant/components/wemo/fan.py | 15 +- homeassistant/components/wemo/light.py | 12 +- homeassistant/components/wemo/models.py | 43 ++++++ homeassistant/components/wemo/sensor.py | 15 +- homeassistant/components/wemo/switch.py | 15 +- homeassistant/components/wemo/wemo_device.py | 38 ++++- tests/components/wemo/conftest.py | 3 - tests/components/wemo/test_init.py | 71 ++++++++- 10 files changed, 245 insertions(+), 124 deletions(-) create mode 100644 homeassistant/components/wemo/models.py diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 4488e881938e55..a58169aa6e5e0d 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -1,9 +1,10 @@ """Support for WeMo device discovery.""" from __future__ import annotations -from collections.abc import Sequence +from collections.abc import Callable, Coroutine, Sequence from datetime import datetime import logging +from typing import Any import pywemo import voluptuous as vol @@ -13,13 +14,13 @@ from homeassistant.const import CONF_DISCOVERY, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType from homeassistant.util.async_ import gather_with_concurrency from .const import DOMAIN -from .wemo_device import async_register_device +from .models import WemoConfigEntryData, WemoData, async_wemo_data +from .wemo_device import DeviceCoordinator, async_register_device # Max number of devices to initialize at once. This limit is in place to # avoid tying up too many executor threads with WeMo device setup. @@ -42,6 +43,7 @@ _LOGGER = logging.getLogger(__name__) +DispatchCallback = Callable[[DeviceCoordinator], Coroutine[Any, Any, None]] HostPortTuple = tuple[str, int | None] @@ -81,11 +83,26 @@ def coerce_host_port(value: str) -> HostPortTuple: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up for WeMo devices.""" - hass.data[DOMAIN] = { - "config": config.get(DOMAIN, {}), - "registry": None, - "pending": {}, - } + # Keep track of WeMo device subscriptions for push updates + registry = pywemo.SubscriptionRegistry() + await hass.async_add_executor_job(registry.start) + + # Respond to discovery requests from WeMo devices. + discovery_responder = pywemo.ssdp.DiscoveryResponder(registry.port) + await hass.async_add_executor_job(discovery_responder.start) + + async def _on_hass_stop(_: Event) -> None: + await hass.async_add_executor_job(discovery_responder.stop) + await hass.async_add_executor_job(registry.stop) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _on_hass_stop) + + yaml_config = config.get(DOMAIN, {}) + hass.data[DOMAIN] = WemoData( + discovery_enabled=yaml_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY), + static_config=yaml_config.get(CONF_STATIC, []), + registry=registry, + ) if DOMAIN in config: hass.async_create_task( @@ -99,45 +116,48 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a wemo config entry.""" - config = hass.data[DOMAIN].pop("config") - - # Keep track of WeMo device subscriptions for push updates - registry = hass.data[DOMAIN]["registry"] = pywemo.SubscriptionRegistry() - await hass.async_add_executor_job(registry.start) - - # Respond to discovery requests from WeMo devices. - discovery_responder = pywemo.ssdp.DiscoveryResponder(registry.port) - await hass.async_add_executor_job(discovery_responder.start) - - static_conf: Sequence[HostPortTuple] = config.get(CONF_STATIC, []) - wemo_dispatcher = WemoDispatcher(entry) - wemo_discovery = WemoDiscovery(hass, wemo_dispatcher, static_conf) - - async def async_stop_wemo(_: Event | None = None) -> None: - """Shutdown Wemo subscriptions and subscription thread on exit.""" - _LOGGER.debug("Shutting down WeMo event subscriptions") - await hass.async_add_executor_job(registry.stop) - await hass.async_add_executor_job(discovery_responder.stop) - wemo_discovery.async_stop_discovery() - - entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_wemo) + wemo_data = async_wemo_data(hass) + dispatcher = WemoDispatcher(entry) + discovery = WemoDiscovery(hass, dispatcher, wemo_data.static_config) + wemo_data.config_entry_data = WemoConfigEntryData( + device_coordinators={}, + discovery=discovery, + dispatcher=dispatcher, ) - entry.async_on_unload(async_stop_wemo) # Need to do this at least once in case statistics are defined and discovery is disabled - await wemo_discovery.discover_statics() + await discovery.discover_statics() - if config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY): - await wemo_discovery.async_discover_and_schedule() + if wemo_data.discovery_enabled: + await discovery.async_discover_and_schedule() return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a wemo config entry.""" - # This makes sure that `entry.async_on_unload` routines run correctly on unload - return True + _LOGGER.debug("Unloading WeMo") + wemo_data = async_wemo_data(hass) + + wemo_data.config_entry_data.discovery.async_stop_discovery() + + dispatcher = wemo_data.config_entry_data.dispatcher + if unload_ok := await dispatcher.async_unload_platforms(hass): + assert not wemo_data.config_entry_data.device_coordinators + wemo_data.config_entry_data = None # type: ignore[assignment] + return unload_ok + + +async def async_wemo_dispatcher_connect( + hass: HomeAssistant, + dispatch: DispatchCallback, +) -> None: + """Connect a wemo platform with the WemoDispatcher.""" + module = dispatch.__module__ # Example: "homeassistant.components.wemo.switch" + platform = Platform(module.rsplit(".", 1)[1]) + + dispatcher = async_wemo_data(hass).config_entry_data.dispatcher + await dispatcher.async_connect_platform(platform, dispatch) class WemoDispatcher: @@ -148,7 +168,8 @@ def __init__(self, config_entry: ConfigEntry) -> None: self._config_entry = config_entry self._added_serial_numbers: set[str] = set() self._failed_serial_numbers: set[str] = set() - self._loaded_platforms: set[Platform] = set() + self._dispatch_backlog: dict[Platform, list[DeviceCoordinator]] = {} + self._dispatch_callbacks: dict[Platform, DispatchCallback] = {} async def async_add_unique_device( self, hass: HomeAssistant, wemo: pywemo.WeMoDevice @@ -171,32 +192,47 @@ async def async_add_unique_device( platforms.add(Platform.SENSOR) for platform in platforms: # Three cases: - # - First time we see platform, we need to load it and initialize the backlog + # - Platform is loaded, dispatch discovery # - Platform is being loaded, add to backlog - # - Platform is loaded, backlog is gone, dispatch discovery + # - First time we see platform, we need to load it and initialize the backlog - if platform not in self._loaded_platforms: - hass.data[DOMAIN]["pending"][platform] = [coordinator] - self._loaded_platforms.add(platform) + if platform in self._dispatch_callbacks: + await self._dispatch_callbacks[platform](coordinator) + elif platform in self._dispatch_backlog: + self._dispatch_backlog[platform].append(coordinator) + else: + self._dispatch_backlog[platform] = [coordinator] hass.async_create_task( hass.config_entries.async_forward_entry_setup( self._config_entry, platform ) ) - elif platform in hass.data[DOMAIN]["pending"]: - hass.data[DOMAIN]["pending"][platform].append(coordinator) - - else: - async_dispatcher_send( - hass, - f"{DOMAIN}.{platform}", - coordinator, - ) - self._added_serial_numbers.add(wemo.serial_number) self._failed_serial_numbers.discard(wemo.serial_number) + async def async_connect_platform( + self, platform: Platform, dispatch: DispatchCallback + ) -> None: + """Consider a platform as loaded and dispatch any backlog of discovered devices.""" + self._dispatch_callbacks[platform] = dispatch + + await gather_with_concurrency( + MAX_CONCURRENCY, + *( + dispatch(coordinator) + for coordinator in self._dispatch_backlog.pop(platform) + ), + ) + + async def async_unload_platforms(self, hass: HomeAssistant) -> bool: + """Forward the unloading of an entry to platforms.""" + platforms: set[Platform] = set(self._dispatch_backlog.keys()) + platforms.update(self._dispatch_callbacks.keys()) + return await hass.config_entries.async_unload_platforms( + self._config_entry, platforms + ) + class WemoDiscovery: """Use SSDP to discover WeMo devices.""" diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index ce7dfc2fa119f8..396a555e4f4dc6 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -1,22 +1,20 @@ """Support for WeMo binary sensors.""" -import asyncio from pywemo import Insight, Maker, StandbyState from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN as WEMO_DOMAIN +from . import async_wemo_dispatcher_connect from .entity import WemoBinaryStateEntity, WemoEntity from .wemo_device import DeviceCoordinator async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + _config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up WeMo binary sensors.""" @@ -30,14 +28,7 @@ async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: else: async_add_entities([WemoBinarySensor(coordinator)]) - async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.binary_sensor", _discovered_wemo) - - await asyncio.gather( - *( - _discovered_wemo(coordinator) - for coordinator in hass.data[WEMO_DOMAIN]["pending"].pop("binary_sensor") - ) - ) + await async_wemo_dispatcher_connect(hass, _discovered_wemo) class WemoBinarySensor(WemoBinaryStateEntity, BinarySensorEntity): diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 1d2c2c9252dc9b..aaa85455c56cc5 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -1,7 +1,6 @@ """Support for WeMo humidifier.""" from __future__ import annotations -import asyncio from datetime import timedelta import math from typing import Any @@ -13,7 +12,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( int_states_in_range, @@ -21,8 +19,8 @@ ranged_value_to_percentage, ) +from . import async_wemo_dispatcher_connect from .const import ( - DOMAIN as WEMO_DOMAIN, SERVICE_RESET_FILTER_LIFE, SERVICE_SET_HUMIDITY, ) @@ -50,7 +48,7 @@ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + _config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up WeMo binary sensors.""" @@ -59,14 +57,7 @@ async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: """Handle a discovered Wemo device.""" async_add_entities([WemoHumidifier(coordinator)]) - async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.fan", _discovered_wemo) - - await asyncio.gather( - *( - _discovered_wemo(coordinator) - for coordinator in hass.data[WEMO_DOMAIN]["pending"].pop("fan") - ) - ) + await async_wemo_dispatcher_connect(hass, _discovered_wemo) platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 2767d44032cae1..fb01d117c08b40 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -1,7 +1,6 @@ """Support for Belkin WeMo lights.""" from __future__ import annotations -import asyncio from typing import Any, cast from pywemo import Bridge, BridgeLight, Dimmer @@ -18,11 +17,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util +from . import async_wemo_dispatcher_connect from .const import DOMAIN as WEMO_DOMAIN from .entity import WemoBinaryStateEntity, WemoEntity from .wemo_device import DeviceCoordinator @@ -45,14 +44,7 @@ async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: else: async_add_entities([WemoDimmer(coordinator)]) - async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.light", _discovered_wemo) - - await asyncio.gather( - *( - _discovered_wemo(coordinator) - for coordinator in hass.data[WEMO_DOMAIN]["pending"].pop("light") - ) - ) + await async_wemo_dispatcher_connect(hass, _discovered_wemo) @callback diff --git a/homeassistant/components/wemo/models.py b/homeassistant/components/wemo/models.py new file mode 100644 index 00000000000000..ee12ccbf846a0f --- /dev/null +++ b/homeassistant/components/wemo/models.py @@ -0,0 +1,43 @@ +"""Common data structures and helpers for accessing them.""" + +from collections.abc import Sequence +from dataclasses import dataclass +from typing import TYPE_CHECKING, cast + +import pywemo + +from homeassistant.core import HomeAssistant, callback + +from .const import DOMAIN + +if TYPE_CHECKING: # Avoid circular dependencies. + from . import HostPortTuple, WemoDiscovery, WemoDispatcher + from .wemo_device import DeviceCoordinator + + +@dataclass +class WemoConfigEntryData: + """Config entry state data.""" + + device_coordinators: dict[str, "DeviceCoordinator"] + discovery: "WemoDiscovery" + dispatcher: "WemoDispatcher" + + +@dataclass +class WemoData: + """Component state data.""" + + discovery_enabled: bool + static_config: Sequence["HostPortTuple"] + registry: pywemo.SubscriptionRegistry + # config_entry_data is set when the config entry is loaded and unset when it's + # unloaded. It's a programmer error if config_entry_data is accessed when the + # config entry is not loaded + config_entry_data: WemoConfigEntryData = None # type: ignore[assignment] + + +@callback +def async_wemo_data(hass: HomeAssistant) -> WemoData: + """Fetch WemoData with proper typing.""" + return cast(WemoData, hass.data[DOMAIN]) diff --git a/homeassistant/components/wemo/sensor.py b/homeassistant/components/wemo/sensor.py index 15e396cc660687..2547dc0ad0d859 100644 --- a/homeassistant/components/wemo/sensor.py +++ b/homeassistant/components/wemo/sensor.py @@ -1,7 +1,6 @@ """Support for power sensors in WeMo Insight devices.""" from __future__ import annotations -import asyncio from collections.abc import Callable from dataclasses import dataclass from typing import cast @@ -15,11 +14,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfEnergy, UnitOfPower from homeassistant.core import HomeAssistant -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from .const import DOMAIN as WEMO_DOMAIN +from . import async_wemo_dispatcher_connect from .entity import WemoEntity from .wemo_device import DeviceCoordinator @@ -59,7 +57,7 @@ class AttributeSensorDescription(SensorEntityDescription): async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + _config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up WeMo sensors.""" @@ -72,14 +70,7 @@ async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: if hasattr(coordinator.wemo, description.key) ) - async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.sensor", _discovered_wemo) - - await asyncio.gather( - *( - _discovered_wemo(coordinator) - for coordinator in hass.data[WEMO_DOMAIN]["pending"].pop("sensor") - ) - ) + await async_wemo_dispatcher_connect(hass, _discovered_wemo) class AttributeSensor(WemoEntity, SensorEntity): diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 6d5e6b678b4dbe..508621ba41581b 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -1,7 +1,6 @@ """Support for WeMo switches.""" from __future__ import annotations -import asyncio from datetime import datetime, timedelta from typing import Any @@ -11,10 +10,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN from homeassistant.core import HomeAssistant -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN as WEMO_DOMAIN +from . import async_wemo_dispatcher_connect from .entity import WemoBinaryStateEntity from .wemo_device import DeviceCoordinator @@ -36,7 +34,7 @@ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + _config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up WeMo switches.""" @@ -45,14 +43,7 @@ async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: """Handle a discovered Wemo device.""" async_add_entities([WemoSwitch(coordinator)]) - async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.switch", _discovered_wemo) - - await asyncio.gather( - *( - _discovered_wemo(coordinator) - for coordinator in hass.data[WEMO_DOMAIN]["pending"].pop("switch") - ) - ) + await async_wemo_dispatcher_connect(hass, _discovered_wemo) class WemoSwitch(WemoBinaryStateEntity, SwitchEntity): diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py index 65431fb7657994..abb8aa186c9093 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -9,7 +9,7 @@ from pywemo import Insight, LongPressMixin, WeMoDevice from pywemo.exceptions import ActionException, PyWeMoException -from pywemo.subscribe import EVENT_TYPE_LONG_PRESS +from pywemo.subscribe import EVENT_TYPE_LONG_PRESS, SubscriptionRegistry from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -30,6 +30,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, WEMO_SUBSCRIPTION_EVENT +from .models import async_wemo_data _LOGGER = logging.getLogger(__name__) @@ -124,9 +125,21 @@ def subscription_callback( updated = self.wemo.subscription_update(event_type, params) self.hass.create_task(self._async_subscription_callback(updated)) + async def async_shutdown(self) -> None: + """Unregister push subscriptions and remove from coordinators dict.""" + await super().async_shutdown() + del _async_coordinators(self.hass)[self.device_id] + assert self.options # Always set by async_register_device. + if self.options.enable_subscription: + await self._async_set_enable_subscription(False) + # Check that the device is available (last_update_success) before disabling long + # press. That avoids long shutdown times for devices that are no longer connected. + if self.options.enable_long_press and self.last_update_success: + await self._async_set_enable_long_press(False) + async def _async_set_enable_subscription(self, enable_subscription: bool) -> None: """Turn on/off push updates from the device.""" - registry = self.hass.data[DOMAIN]["registry"] + registry = _async_registry(self.hass) if enable_subscription: registry.on(self.wemo, None, self.subscription_callback) await self.hass.async_add_executor_job(registry.register, self.wemo) @@ -199,8 +212,10 @@ def should_poll(self) -> bool: # this case so the Sensor entities are properly populated. return True - registry = self.hass.data[DOMAIN]["registry"] - return not (registry.is_subscribed(self.wemo) and self.last_update_success) + return not ( + _async_registry(self.hass).is_subscribed(self.wemo) + and self.last_update_success + ) async def _async_update_data(self) -> None: """Update WeMo state.""" @@ -258,7 +273,7 @@ async def async_register_device( ) device = DeviceCoordinator(hass, wemo, entry.id) - hass.data[DOMAIN].setdefault("devices", {})[entry.id] = device + _async_coordinators(hass)[entry.id] = device config_entry.async_on_unload( config_entry.add_update_listener(device.async_set_options) @@ -271,5 +286,14 @@ async def async_register_device( @callback def async_get_coordinator(hass: HomeAssistant, device_id: str) -> DeviceCoordinator: """Return DeviceCoordinator for device_id.""" - coordinator: DeviceCoordinator = hass.data[DOMAIN]["devices"][device_id] - return coordinator + return _async_coordinators(hass)[device_id] + + +@callback +def _async_coordinators(hass: HomeAssistant) -> dict[str, DeviceCoordinator]: + return async_wemo_data(hass).config_entry_data.device_coordinators + + +@callback +def _async_registry(hass: HomeAssistant) -> SubscriptionRegistry: + return async_wemo_data(hass).registry diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index 5fe798004dadd4..6c4d28ecae7df0 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -1,5 +1,4 @@ """Fixtures for pywemo.""" -import asyncio import contextlib from unittest.mock import create_autospec, patch @@ -33,11 +32,9 @@ async def async_pywemo_registry_fixture(): registry = create_autospec(pywemo.SubscriptionRegistry, instance=True) registry.callbacks = {} - registry.semaphore = asyncio.Semaphore(value=0) def on_func(device, type_filter, callback): registry.callbacks[device.name] = callback - registry.semaphore.release() registry.on.side_effect = on_func registry.is_subscribed.return_value = False diff --git a/tests/components/wemo/test_init.py b/tests/components/wemo/test_init.py index 0e9ba19af4295b..1d4271063f28c2 100644 --- a/tests/components/wemo/test_init.py +++ b/tests/components/wemo/test_init.py @@ -1,16 +1,24 @@ """Tests for the wemo component.""" +import asyncio from datetime import timedelta from unittest.mock import create_autospec, patch import pywemo -from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC, WemoDiscovery +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.wemo import ( + CONF_DISCOVERY, + CONF_STATIC, + WemoDiscovery, + async_wemo_dispatcher_connect, +) from homeassistant.components.wemo.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from . import entity_test_helpers from .conftest import ( MOCK_FIRMWARE_VERSION, MOCK_HOST, @@ -92,6 +100,54 @@ async def test_static_config_without_port(hass: HomeAssistant, pywemo_device) -> assert len(entity_entries) == 1 +async def test_reload_config_entry( + hass: HomeAssistant, + pywemo_device: pywemo.WeMoDevice, + pywemo_registry: pywemo.SubscriptionRegistry, +) -> None: + """Config entry can be reloaded without errors.""" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DISCOVERY: False, + CONF_STATIC: [MOCK_HOST], + }, + }, + ) + + async def _async_test_entry_and_entity() -> tuple[str, str]: + await hass.async_block_till_done() + + pywemo_device.get_state.assert_called() + pywemo_device.get_state.reset_mock() + + pywemo_registry.register.assert_called_once_with(pywemo_device) + pywemo_registry.register.reset_mock() + + entity_registry = er.async_get(hass) + entity_entries = list(entity_registry.entities.values()) + assert len(entity_entries) == 1 + await entity_test_helpers.test_turn_off_state( + hass, entity_entries[0], SWITCH_DOMAIN + ) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + + return entries[0].entry_id, entity_entries[0].entity_id + + entry_id, entity_id = await _async_test_entry_and_entity() + pywemo_registry.unregister.assert_not_called() + + assert await hass.config_entries.async_reload(entry_id) + + ids = await _async_test_entry_and_entity() + pywemo_registry.unregister.assert_called_once_with(pywemo_device) + assert ids == (entry_id, entity_id) + + async def test_static_config_with_invalid_host(hass: HomeAssistant) -> None: """Component setup fails if a static host is invalid.""" setup_success = await async_setup_component( @@ -146,17 +202,26 @@ def create_device(counter): device.supports_long_press.return_value = False return device + semaphore = asyncio.Semaphore(value=0) + + async def async_connect(*args): + await async_wemo_dispatcher_connect(*args) + semaphore.release() + pywemo_devices = [create_device(0), create_device(1)] # Setup the component and start discovery. with patch( "pywemo.discover_devices", return_value=pywemo_devices ) as mock_discovery, patch( "homeassistant.components.wemo.WemoDiscovery.discover_statics" - ) as mock_discover_statics: + ) as mock_discover_statics, patch( + "homeassistant.components.wemo.binary_sensor.async_wemo_dispatcher_connect", + side_effect=async_connect, + ): assert await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_DISCOVERY: True}} ) - await pywemo_registry.semaphore.acquire() # Returns after platform setup. + await semaphore.acquire() # Returns after platform setup. mock_discovery.assert_called() mock_discover_statics.assert_called() pywemo_devices.append(create_device(2)) From df19d4fd155eb08646d92bf7ad7194a222526806 Mon Sep 17 00:00:00 2001 From: quthla Date: Thu, 20 Jul 2023 10:07:03 +0200 Subject: [PATCH 0696/1009] 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 bdcf08bb2f625c..9299b1ed0b0b4b 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 @@ 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 @@ def is_available_updated(is_available: bool) -> None: 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 ce0027a84e0bc1c45e414b2a74bd9364601cde8d Mon Sep 17 00:00:00 2001 From: Blastoise186 <40033667+blastoise186@users.noreply.github.com> Date: Thu, 20 Jul 2023 09:21:52 +0100 Subject: [PATCH 0697/1009] Upgrade yt-dlp to fix security issue (#96453) * Bump yt-dlp from 2023.3.4 to 2023.7.6 Bumps [yt-dlp](https://github.com/yt-dlp/yt-dlp) from 2023.3.4 to 2023.7.6. - [Release notes](https://github.com/yt-dlp/yt-dlp/releases) - [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md) - [Commits](https://github.com/yt-dlp/yt-dlp/compare/2023.03.04...2023.07.06) --- updated-dependencies: - dependency-name: yt-dlp dependency-type: direct:production ... Signed-off-by: dependabot[bot] * Bump yt-dlp to 2023.7.6 --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index ccab196032f45d..0e5d9ead0f8a8a 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -7,5 +7,5 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp==2023.3.4"] + "requirements": ["yt-dlp==2023.7.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index d57d0cc1c69bbb..e8ddc96de203f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2726,7 +2726,7 @@ yolink-api==0.2.9 youless-api==1.0.1 # homeassistant.components.media_extractor -yt-dlp==2023.3.4 +yt-dlp==2023.7.6 # homeassistant.components.zamg zamg==0.2.4 From 4e460f71f8960b7de4caf71d842d96a691365186 Mon Sep 17 00:00:00 2001 From: Renier Moorcroft <66512715+RenierM26@users.noreply.github.com> Date: Thu, 20 Jul 2023 10:35:06 +0200 Subject: [PATCH 0698/1009] Add EZVIZ BinarySensorEntity proper names and translation key (#95698) * Update binary_sensor.py * Add proper naming and translation keys * Apply suggestions from code review Co-authored-by: G Johansson * Fix strings after merge. --------- Co-authored-by: G Johansson --- homeassistant/components/ezviz/binary_sensor.py | 11 ++++++++--- homeassistant/components/ezviz/strings.json | 8 ++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ezviz/binary_sensor.py b/homeassistant/components/ezviz/binary_sensor.py index 77e95fa221d031..3ed61d8fc3d5c0 100644 --- a/homeassistant/components/ezviz/binary_sensor.py +++ b/homeassistant/components/ezviz/binary_sensor.py @@ -22,9 +22,13 @@ device_class=BinarySensorDeviceClass.MOTION, ), "alarm_schedules_enabled": BinarySensorEntityDescription( - key="alarm_schedules_enabled" + key="alarm_schedules_enabled", + translation_key="alarm_schedules_enabled", + ), + "encrypted": BinarySensorEntityDescription( + key="encrypted", + translation_key="encrypted", ), - "encrypted": BinarySensorEntityDescription(key="encrypted"), } @@ -50,6 +54,8 @@ async def async_setup_entry( class EzvizBinarySensor(EzvizEntity, BinarySensorEntity): """Representation of a EZVIZ sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: EzvizDataUpdateCoordinator, @@ -59,7 +65,6 @@ def __init__( """Initialize the sensor.""" super().__init__(coordinator, serial) self._sensor_name = binary_sensor - self._attr_name = f"{self._camera_name} {binary_sensor.title()}" self._attr_unique_id = f"{serial}_{self._camera_name}.{binary_sensor}" self.entity_description = BINARY_SENSOR_TYPES[binary_sensor] diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index 909a9b5f9c0966..0245edc0e3eb0c 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -99,6 +99,14 @@ "name": "Last motion image" } }, + "binary_sensor": { + "alarm_schedules_enabled": { + "name": "Alarm schedules enabled" + }, + "encrypted": { + "name": "Encryption" + } + }, "sensor": { "alarm_sound_mod": { "name": "Alarm sound level" From db76bf3a9ffb61eefe8f4987f2e23c03e81e387f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 20 Jul 2023 10:40:34 +0200 Subject: [PATCH 0699/1009] Implement coordinator in Trafikverket Train (#96916) * Implement coordinator TVT * Review comments * Review changes --- .coveragerc | 1 + .../components/trafikverket_train/__init__.py | 9 +- .../trafikverket_train/coordinator.py | 149 ++++++++++++++++ .../components/trafikverket_train/sensor.py | 159 ++++-------------- 4 files changed, 190 insertions(+), 128 deletions(-) create mode 100644 homeassistant/components/trafikverket_train/coordinator.py diff --git a/.coveragerc b/.coveragerc index 4a5c843f357377..acd218a2d1bac1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1317,6 +1317,7 @@ omit = homeassistant/components/tradfri/sensor.py homeassistant/components/tradfri/switch.py homeassistant/components/trafikverket_train/__init__.py + homeassistant/components/trafikverket_train/coordinator.py homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/__init__.py homeassistant/components/trafikverket_weatherstation/coordinator.py diff --git a/homeassistant/components/trafikverket_train/__init__.py b/homeassistant/components/trafikverket_train/__init__.py index 8047cf2046d339..dd35d058ed5aa0 100644 --- a/homeassistant/components/trafikverket_train/__init__.py +++ b/homeassistant/components/trafikverket_train/__init__.py @@ -15,6 +15,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_FROM, CONF_TO, DOMAIN, PLATFORMS +from .coordinator import TVDataUpdateCoordinator async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -34,11 +35,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f" {entry.data[CONF_TO]}. Error: {error} " ) from error - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - CONF_TO: to_station, - CONF_FROM: from_station, - "train_api": train_api, - } + coordinator = TVDataUpdateCoordinator(hass, entry, to_station, from_station) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/trafikverket_train/coordinator.py b/homeassistant/components/trafikverket_train/coordinator.py new file mode 100644 index 00000000000000..fba6eb93dd9254 --- /dev/null +++ b/homeassistant/components/trafikverket_train/coordinator.py @@ -0,0 +1,149 @@ +"""DataUpdateCoordinator for the Trafikverket Train integration.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import date, datetime, time, timedelta +import logging + +from pytrafikverket import TrafikverketTrain +from pytrafikverket.trafikverket_train import StationInfo, TrainStop + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_WEEKDAY, WEEKDAYS +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util + +from .const import CONF_TIME, DOMAIN + + +@dataclass +class TrainData: + """Dataclass for Trafikverket Train data.""" + + departure_time: datetime | None + departure_state: str + cancelled: bool + delayed_time: int | None + planned_time: datetime | None + estimated_time: datetime | None + actual_time: datetime | None + other_info: str | None + deviation: str | None + + +_LOGGER = logging.getLogger(__name__) +TIME_BETWEEN_UPDATES = timedelta(minutes=5) + + +def _next_weekday(fromdate: date, weekday: int) -> date: + """Return the date of the next time a specific weekday happen.""" + days_ahead = weekday - fromdate.weekday() + if days_ahead <= 0: + days_ahead += 7 + return fromdate + timedelta(days_ahead) + + +def _next_departuredate(departure: list[str]) -> date: + """Calculate the next departuredate from an array input of short days.""" + today_date = date.today() + today_weekday = date.weekday(today_date) + if WEEKDAYS[today_weekday] in departure: + return today_date + for day in departure: + next_departure = WEEKDAYS.index(day) + if next_departure > today_weekday: + return _next_weekday(today_date, next_departure) + return _next_weekday(today_date, WEEKDAYS.index(departure[0])) + + +def _get_as_utc(date_value: datetime | None) -> datetime | None: + """Return utc datetime or None.""" + if date_value: + return dt_util.as_utc(date_value) + return None + + +def _get_as_joined(information: list[str] | None) -> str | None: + """Return joined information or None.""" + if information: + return ", ".join(information) + return None + + +class TVDataUpdateCoordinator(DataUpdateCoordinator[TrainData]): + """A Trafikverket Data Update Coordinator.""" + + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + to_station: StationInfo, + from_station: StationInfo, + ) -> None: + """Initialize the Trafikverket coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=TIME_BETWEEN_UPDATES, + ) + self._train_api = TrafikverketTrain( + async_get_clientsession(hass), entry.data[CONF_API_KEY] + ) + self.from_station: StationInfo = from_station + self.to_station: StationInfo = to_station + self._time: time | None = dt_util.parse_time(entry.data[CONF_TIME]) + self._weekdays: list[str] = entry.data[CONF_WEEKDAY] + + async def _async_update_data(self) -> TrainData: + """Fetch data from Trafikverket.""" + + when = dt_util.now() + state: TrainStop | None = None + if self._time: + departure_day = _next_departuredate(self._weekdays) + when = datetime.combine( + departure_day, + self._time, + dt_util.get_time_zone(self.hass.config.time_zone), + ) + try: + if self._time: + state = await self._train_api.async_get_train_stop( + self.from_station, self.to_station, when + ) + else: + state = await self._train_api.async_get_next_train_stop( + self.from_station, self.to_station, when + ) + except ValueError as error: + if "Invalid authentication" in error.args[0]: + raise ConfigEntryAuthFailed from error + raise UpdateFailed( + f"Train departure {when} encountered a problem: {error}" + ) from error + + departure_time = state.advertised_time_at_location + if state.estimated_time_at_location: + departure_time = state.estimated_time_at_location + elif state.time_at_location: + departure_time = state.time_at_location + + delay_time = state.get_delay_time() + + states = TrainData( + departure_time=_get_as_utc(departure_time), + departure_state=state.get_state().value, + cancelled=state.canceled, + delayed_time=delay_time.seconds if delay_time else None, + planned_time=_get_as_utc(state.advertised_time_at_location), + estimated_time=_get_as_utc(state.estimated_time_at_location), + actual_time=_get_as_utc(state.time_at_location), + other_info=_get_as_joined(state.other_information), + deviation=_get_as_joined(state.deviations), + ) + + return states diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index c0643858f42108..f57850e51b8f0b 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -1,31 +1,25 @@ """Train information for departures and delays, provided by Trafikverket.""" from __future__ import annotations -from datetime import date, datetime, time, timedelta -import logging -from typing import TYPE_CHECKING, Any +from datetime import time, timedelta +from typing import TYPE_CHECKING -from pytrafikverket import TrafikverketTrain -from pytrafikverket.exceptions import ( - MultipleTrainAnnouncementFound, - NoTrainAnnouncementFound, -) -from pytrafikverket.trafikverket_train import StationInfo, TrainStop +from pytrafikverket.trafikverket_train import StationInfo from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, CONF_WEEKDAY, WEEKDAYS -from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_NAME, CONF_WEEKDAY +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util -from .const import CONF_FROM, CONF_TIME, CONF_TO, DOMAIN +from .const import CONF_TIME, DOMAIN +from .coordinator import TVDataUpdateCoordinator from .util import create_unique_id -_LOGGER = logging.getLogger(__name__) - ATTR_DEPARTURE_STATE = "departure_state" ATTR_CANCELED = "canceled" ATTR_DELAY_TIME = "number_of_minutes_delayed" @@ -44,16 +38,17 @@ async def async_setup_entry( ) -> None: """Set up the Trafikverket sensor entry.""" - train_api = hass.data[DOMAIN][entry.entry_id]["train_api"] - to_station = hass.data[DOMAIN][entry.entry_id][CONF_TO] - from_station = hass.data[DOMAIN][entry.entry_id][CONF_FROM] + coordinator: TVDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + to_station = coordinator.to_station + from_station = coordinator.from_station get_time: str | None = entry.data.get(CONF_TIME) train_time = dt_util.parse_time(get_time) if get_time else None async_add_entities( [ TrainSensor( - train_api, + coordinator, entry.data[CONF_NAME], from_station, to_station, @@ -66,33 +61,7 @@ async def async_setup_entry( ) -def next_weekday(fromdate: date, weekday: int) -> date: - """Return the date of the next time a specific weekday happen.""" - days_ahead = weekday - fromdate.weekday() - if days_ahead <= 0: - days_ahead += 7 - return fromdate + timedelta(days_ahead) - - -def next_departuredate(departure: list[str]) -> date: - """Calculate the next departuredate from an array input of short days.""" - today_date = date.today() - today_weekday = date.weekday(today_date) - if WEEKDAYS[today_weekday] in departure: - return today_date - for day in departure: - next_departure = WEEKDAYS.index(day) - if next_departure > today_weekday: - return next_weekday(today_date, next_departure) - return next_weekday(today_date, WEEKDAYS.index(departure[0])) - - -def _to_iso_format(traintime: datetime) -> str: - """Return isoformatted utc time.""" - return dt_util.as_utc(traintime).isoformat() - - -class TrainSensor(SensorEntity): +class TrainSensor(CoordinatorEntity[TVDataUpdateCoordinator], SensorEntity): """Contains data about a train depature.""" _attr_icon = ICON @@ -102,7 +71,7 @@ class TrainSensor(SensorEntity): def __init__( self, - train_api: TrafikverketTrain, + coordinator: TVDataUpdateCoordinator, name: str, from_station: StationInfo, to_station: StationInfo, @@ -111,11 +80,7 @@ def __init__( entry_id: str, ) -> None: """Initialize the sensor.""" - self._train_api = train_api - self._from_station = from_station - self._to_station = to_station - self._weekday = weekday - self._time = departuretime + super().__init__(coordinator) self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, entry_id)}, @@ -129,80 +94,28 @@ def __init__( self._attr_unique_id = create_unique_id( from_station.name, to_station.name, departuretime, weekday ) + self._update_attr() - async def async_update(self) -> None: - """Retrieve latest state.""" - when = dt_util.now() - _state: TrainStop | None = None - if self._time: - departure_day = next_departuredate(self._weekday) - when = datetime.combine( - departure_day, - self._time, - dt_util.get_time_zone(self.hass.config.time_zone), - ) - try: - if self._time: - _LOGGER.debug("%s, %s, %s", self._from_station, self._to_station, when) - _state = await self._train_api.async_get_train_stop( - self._from_station, self._to_station, when - ) - else: - _state = await self._train_api.async_get_next_train_stop( - self._from_station, self._to_station, when - ) - except (NoTrainAnnouncementFound, MultipleTrainAnnouncementFound) as error: - _LOGGER.error("Departure %s encountered a problem: %s", when, error) - - if not _state: - self._attr_available = False - self._attr_native_value = None - self._attr_extra_state_attributes = {} - return - - self._attr_available = True - - # The original datetime doesn't provide a timezone so therefore attaching it here. - if TYPE_CHECKING: - assert _state.advertised_time_at_location - self._attr_native_value = dt_util.as_utc(_state.advertised_time_at_location) - if _state.time_at_location: - self._attr_native_value = dt_util.as_utc(_state.time_at_location) - if _state.estimated_time_at_location: - self._attr_native_value = dt_util.as_utc(_state.estimated_time_at_location) - - self._update_attributes(_state) - - def _update_attributes(self, state: TrainStop) -> None: - """Return extra state attributes.""" - - attributes: dict[str, Any] = { - ATTR_DEPARTURE_STATE: state.get_state().value, - ATTR_CANCELED: state.canceled, - ATTR_DELAY_TIME: None, - ATTR_PLANNED_TIME: None, - ATTR_ESTIMATED_TIME: None, - ATTR_ACTUAL_TIME: None, - ATTR_OTHER_INFORMATION: None, - ATTR_DEVIATIONS: None, - } - - if delay_in_minutes := state.get_delay_time(): - attributes[ATTR_DELAY_TIME] = delay_in_minutes.total_seconds() / 60 + @callback + def _handle_coordinator_update(self) -> None: + self._update_attr() + return super()._handle_coordinator_update() - if advert_time := state.advertised_time_at_location: - attributes[ATTR_PLANNED_TIME] = _to_iso_format(advert_time) - - if est_time := state.estimated_time_at_location: - attributes[ATTR_ESTIMATED_TIME] = _to_iso_format(est_time) - - if time_location := state.time_at_location: - attributes[ATTR_ACTUAL_TIME] = _to_iso_format(time_location) + @callback + def _update_attr(self) -> None: + """Retrieve latest state.""" - if other_info := state.other_information: - attributes[ATTR_OTHER_INFORMATION] = ", ".join(other_info) + data = self.coordinator.data - if deviation := state.deviations: - attributes[ATTR_DEVIATIONS] = ", ".join(deviation) + self._attr_native_value = data.departure_time - self._attr_extra_state_attributes = attributes + self._attr_extra_state_attributes = { + ATTR_DEPARTURE_STATE: data.departure_state, + ATTR_CANCELED: data.cancelled, + ATTR_DELAY_TIME: data.delayed_time, + ATTR_PLANNED_TIME: data.planned_time, + ATTR_ESTIMATED_TIME: data.estimated_time, + ATTR_ACTUAL_TIME: data.actual_time, + ATTR_OTHER_INFORMATION: data.other_info, + ATTR_DEVIATIONS: data.deviation, + } From fa0d68b1d73211f52c33462add67558bcb4135bd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 20 Jul 2023 11:10:03 +0200 Subject: [PATCH 0700/1009] Add NumberDeviceClass.DURATION (#96932) --- homeassistant/components/number/const.py | 14 ++++++++++++++ homeassistant/components/sensor/const.py | 12 ++++++------ tests/components/number/test_init.py | 12 +++--------- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 1a2580cfc613e4..849581b6f9fd14 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -31,6 +31,7 @@ UnitOfSoundPressure, UnitOfSpeed, UnitOfTemperature, + UnitOfTime, UnitOfVolume, UnitOfVolumetricFlux, ) @@ -122,6 +123,12 @@ class NumberDeviceClass(StrEnum): - USCS / imperial: `in`, `ft`, `yd`, `mi` """ + DURATION = "duration" + """Fixed duration. + + Unit of measurement: `d`, `h`, `min`, `s`, `ms` + """ + ENERGY = "energy" """Energy. @@ -392,6 +399,13 @@ class NumberMode(StrEnum): NumberDeviceClass.DATA_RATE: set(UnitOfDataRate), NumberDeviceClass.DATA_SIZE: set(UnitOfInformation), NumberDeviceClass.DISTANCE: set(UnitOfLength), + NumberDeviceClass.DURATION: { + UnitOfTime.DAYS, + UnitOfTime.HOURS, + UnitOfTime.MINUTES, + UnitOfTime.SECONDS, + UnitOfTime.MILLISECONDS, + }, NumberDeviceClass.ENERGY: set(UnitOfEnergy), NumberDeviceClass.ENERGY_STORAGE: set(UnitOfEnergy), NumberDeviceClass.FREQUENCY: set(UnitOfFrequency), diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index fe01058fda7e0b..2c6883e4a71df5 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -73,12 +73,6 @@ class SensorDeviceClass(StrEnum): ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601 """ - DURATION = "duration" - """Fixed duration. - - Unit of measurement: `d`, `h`, `min`, `s`, `ms` - """ - ENUM = "enum" """Enumeration. @@ -158,6 +152,12 @@ class SensorDeviceClass(StrEnum): - USCS / imperial: `in`, `ft`, `yd`, `mi` """ + DURATION = "duration" + """Fixed duration. + + Unit of measurement: `d`, `h`, `min`, `s`, `ms` + """ + ENERGY = "energy" """Energy. diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index d9cf27c12aad63..37c0b175faa2f4 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -22,6 +22,7 @@ ) from homeassistant.components.sensor import ( DEVICE_CLASS_UNITS as SENSOR_DEVICE_CLASS_UNITS, + NON_NUMERIC_DEVICE_CLASSES, SensorDeviceClass, ) from homeassistant.config_entries import ConfigEntry, ConfigFlow @@ -769,22 +770,15 @@ async def test_custom_unit_change( def test_device_classes_aligned() -> None: """Make sure all sensor device classes are also available in NumberDeviceClass.""" - non_numeric_device_classes = { - SensorDeviceClass.DATE, - SensorDeviceClass.DURATION, - SensorDeviceClass.ENUM, - SensorDeviceClass.TIMESTAMP, - } - for device_class in SensorDeviceClass: - if device_class in non_numeric_device_classes: + if device_class in NON_NUMERIC_DEVICE_CLASSES: continue assert hasattr(NumberDeviceClass, device_class.name) assert getattr(NumberDeviceClass, device_class.name).value == device_class.value for device_class in SENSOR_DEVICE_CLASS_UNITS: - if device_class in non_numeric_device_classes: + if device_class in NON_NUMERIC_DEVICE_CLASSES: continue assert ( SENSOR_DEVICE_CLASS_UNITS[device_class] From 34e30570c159d51c469c64d420d5648c1996fe1d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 20 Jul 2023 11:15:54 +0200 Subject: [PATCH 0701/1009] Migrate airtouch 4 to use has entity name (#96356) --- homeassistant/components/airtouch4/climate.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/airtouch4/climate.py b/homeassistant/components/airtouch4/climate.py index e7d73ec0f1c40a..52e234505c11c6 100644 --- a/homeassistant/components/airtouch4/climate.py +++ b/homeassistant/components/airtouch4/climate.py @@ -84,6 +84,9 @@ async def async_setup_entry( class AirtouchAC(CoordinatorEntity, ClimateEntity): """Representation of an AirTouch 4 ac.""" + _attr_has_entity_name = True + _attr_name = None + _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE ) @@ -107,7 +110,7 @@ def device_info(self) -> DeviceInfo: """Return device info for this device.""" return DeviceInfo( identifiers={(DOMAIN, self.unique_id)}, - name=self.name, + name=f"AC {self._ac_number}", manufacturer="Airtouch", model="Airtouch 4", ) @@ -122,11 +125,6 @@ def current_temperature(self): """Return the current temperature.""" return self._unit.Temperature - @property - def name(self): - """Return the name of the climate device.""" - return f"AC {self._ac_number}" - @property def fan_mode(self): """Return fan mode of the AC this group belongs to.""" @@ -200,6 +198,8 @@ async def async_turn_off(self) -> None: class AirtouchGroup(CoordinatorEntity, ClimateEntity): """Representation of an AirTouch 4 group.""" + _attr_has_entity_name = True + _attr_name = None _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_hvac_modes = AT_GROUP_MODES @@ -224,7 +224,7 @@ def device_info(self) -> DeviceInfo: identifiers={(DOMAIN, self.unique_id)}, manufacturer="Airtouch", model="Airtouch 4", - name=self.name, + name=self._unit.GroupName, ) @property @@ -242,11 +242,6 @@ def max_temp(self): """Return Max Temperature for AC of this group.""" return self._airtouch.acs[self._unit.BelongsToAc].MaxSetpoint - @property - def name(self): - """Return the name of the climate device.""" - return self._unit.GroupName - @property def current_temperature(self): """Return the current temperature.""" From effa90272d6068bdc444bc341bd76feaab3bd718 Mon Sep 17 00:00:00 2001 From: Dmitry Vasilyev Date: Thu, 20 Jul 2023 13:16:38 +0400 Subject: [PATCH 0702/1009] Support Tuya Air Conditioner Mate (WiFi) - Smart IR socket with power monitoring (#95027) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/sensor.py | 25 +++++++++++++++++++++++++ homeassistant/components/tuya/switch.py | 7 +++++++ 2 files changed, 32 insertions(+) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 1c7c2ab781dc12..9f055a6262ef73 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -986,6 +986,7 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), ), # eMylo Smart WiFi IR Remote + # Air Conditioner Mate (Smart IR Socket) "wnykq": ( TuyaSensorEntityDescription( key=DPCode.VA_TEMPERATURE, @@ -999,6 +1000,30 @@ class TuyaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), + TuyaSensorEntityDescription( + key=DPCode.CUR_CURRENT, + translation_key="current", + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TuyaSensorEntityDescription( + key=DPCode.CUR_POWER, + translation_key="power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TuyaSensorEntityDescription( + key=DPCode.CUR_VOLTAGE, + translation_key="voltage", + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), ), # Dehumidifier # https://developer.tuya.com/en/docs/iot/s?id=K9gf48r6jke8e diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index c99d6f3a0b207e..676991fe1673a6 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -573,6 +573,13 @@ entity_category=EntityCategory.CONFIG, ), ), + # Air Conditioner Mate (Smart IR Socket) + "wnykq": ( + SwitchEntityDescription( + key=DPCode.SWITCH, + name=None, + ), + ), # SIREN: Siren (switch) with Temperature and humidity sensor # https://developer.tuya.com/en/docs/iot/f?id=Kavck4sr3o5ek "wsdcg": ( From 3fbdf4a184d43ae0382210baa03d5fbac68003d9 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 20 Jul 2023 11:27:30 +0200 Subject: [PATCH 0703/1009] 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 20167ddd184224..204ed622f13e11 100644 --- a/homeassistant/components/sensibo/switch.py +++ b/homeassistant/components/sensibo/switch.py @@ -151,9 +151,10 @@ def extra_state_attributes(self) -> Mapping[str, Any] | None: @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 4e2b00a44369365066868d78f4664b1b93e5b057 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 20 Jul 2023 11:35:08 +0200 Subject: [PATCH 0704/1009] Refactor SQL with ManualTriggerEntity (#95116) * First go * Finalize sensor * Add tests * Remove not need _attr_name * device_class * _process_manual_data allow Any as value --- homeassistant/components/sql/__init__.py | 7 ++- homeassistant/components/sql/sensor.py | 68 +++++++++++++++-------- homeassistant/helpers/template_entity.py | 2 +- tests/components/sql/__init__.py | 19 +++++++ tests/components/sql/test_sensor.py | 70 +++++++++++++++++++++++- 5 files changed, 140 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/sql/__init__.py b/homeassistant/components/sql/__init__.py index dd5480450e28d4..316e816fd6f12e 100644 --- a/homeassistant/components/sql/__init__.py +++ b/homeassistant/components/sql/__init__.py @@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_CLASS, + CONF_ICON, CONF_NAME, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, @@ -23,6 +24,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.template_entity import CONF_AVAILABILITY, CONF_PICTURE from homeassistant.helpers.typing import ConfigType from .const import CONF_COLUMN_NAME, CONF_QUERY, DOMAIN, PLATFORMS @@ -41,7 +43,7 @@ def validate_sql_select(value: str) -> str: QUERY_SCHEMA = vol.Schema( { vol.Required(CONF_COLUMN_NAME): cv.string, - vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_NAME): cv.template, vol.Required(CONF_QUERY): vol.All(cv.string, validate_sql_select), vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, @@ -49,6 +51,9 @@ def validate_sql_select(value: str) -> str: vol.Optional(CONF_DB_URL): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, + vol.Optional(CONF_AVAILABILITY): cv.template, + vol.Optional(CONF_ICON): cv.template, + vol.Optional(CONF_PICTURE): cv.template, } ) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 96fc4bc943a227..cbdef90f623274 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -4,6 +4,7 @@ from datetime import date import decimal import logging +from typing import Any import sqlalchemy from sqlalchemy import lambda_stmt @@ -27,6 +28,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_CLASS, + CONF_ICON, CONF_NAME, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, @@ -40,6 +42,11 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template +from homeassistant.helpers.template_entity import ( + CONF_AVAILABILITY, + CONF_PICTURE, + ManualTriggerEntity, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_COLUMN_NAME, CONF_QUERY, DOMAIN @@ -61,7 +68,7 @@ async def async_setup_platform( if (conf := discovery_info) is None: return - name: str = conf[CONF_NAME] + name: Template = conf[CONF_NAME] query_str: str = conf[CONF_QUERY] unit: str | None = conf.get(CONF_UNIT_OF_MEASUREMENT) value_template: Template | None = conf.get(CONF_VALUE_TEMPLATE) @@ -70,13 +77,24 @@ async def async_setup_platform( db_url: str = resolve_db_url(hass, conf.get(CONF_DB_URL)) device_class: SensorDeviceClass | None = conf.get(CONF_DEVICE_CLASS) state_class: SensorStateClass | None = conf.get(CONF_STATE_CLASS) + availability: Template | None = conf.get(CONF_AVAILABILITY) + icon: Template | None = conf.get(CONF_ICON) + picture: Template | None = conf.get(CONF_PICTURE) if value_template is not None: value_template.hass = hass + trigger_entity_config = {CONF_NAME: name, CONF_DEVICE_CLASS: device_class} + if availability: + trigger_entity_config[CONF_AVAILABILITY] = availability + if icon: + trigger_entity_config[CONF_ICON] = icon + if picture: + trigger_entity_config[CONF_PICTURE] = picture + await async_setup_sensor( hass, - name, + trigger_entity_config, query_str, column_name, unit, @@ -84,7 +102,6 @@ async def async_setup_platform( unique_id, db_url, True, - device_class, state_class, async_add_entities, ) @@ -114,9 +131,12 @@ async def async_setup_entry( if value_template is not None: value_template.hass = hass + name_template = Template(name, hass) + trigger_entity_config = {CONF_NAME: name_template, CONF_DEVICE_CLASS: device_class} + await async_setup_sensor( hass, - name, + trigger_entity_config, query_str, column_name, unit, @@ -124,7 +144,6 @@ async def async_setup_entry( entry.entry_id, db_url, False, - device_class, state_class, async_add_entities, ) @@ -162,7 +181,7 @@ def _shutdown_db_engines(event: Event) -> None: async def async_setup_sensor( hass: HomeAssistant, - name: str, + trigger_entity_config: ConfigType, query_str: str, column_name: str, unit: str | None, @@ -170,7 +189,6 @@ async def async_setup_sensor( unique_id: str | None, db_url: str, yaml: bool, - device_class: SensorDeviceClass | None, state_class: SensorStateClass | None, async_add_entities: AddEntitiesCallback, ) -> None: @@ -245,7 +263,7 @@ async def async_setup_sensor( async_add_entities( [ SQLSensor( - name, + trigger_entity_config, sessmaker, query_str, column_name, @@ -253,12 +271,10 @@ async def async_setup_sensor( value_template, unique_id, yaml, - device_class, state_class, use_database_executor, ) ], - True, ) @@ -295,15 +311,12 @@ def _generate_lambda_stmt(query: str) -> StatementLambdaElement: return lambda_stmt(lambda: text, lambda_cache=_SQL_LAMBDA_CACHE) -class SQLSensor(SensorEntity): +class SQLSensor(ManualTriggerEntity, SensorEntity): """Representation of an SQL sensor.""" - _attr_icon = "mdi:database-search" - _attr_has_entity_name = True - def __init__( self, - name: str, + trigger_entity_config: ConfigType, sessmaker: scoped_session, query: str, column: str, @@ -311,15 +324,13 @@ def __init__( value_template: Template | None, unique_id: str | None, yaml: bool, - device_class: SensorDeviceClass | None, state_class: SensorStateClass | None, use_database_executor: bool, ) -> None: """Initialize the SQL sensor.""" + super().__init__(self.hass, trigger_entity_config) self._query = query - self._attr_name = name if yaml else None self._attr_native_unit_of_measurement = unit - self._attr_device_class = device_class self._attr_state_class = state_class self._template = value_template self._column_name = column @@ -328,22 +339,34 @@ def __init__( self._attr_unique_id = unique_id self._use_database_executor = use_database_executor self._lambda_stmt = _generate_lambda_stmt(query) + self._attr_has_entity_name = not yaml if not yaml and unique_id: self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, unique_id)}, manufacturer="SQL", - name=name, + name=trigger_entity_config[CONF_NAME].template, ) + async def async_added_to_hass(self) -> None: + """Call when entity about to be added to hass.""" + await super().async_added_to_hass() + await self.async_update() + + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return extra attributes.""" + return dict(self._attr_extra_state_attributes) + async def async_update(self) -> None: """Retrieve sensor data from the query using the right executor.""" if self._use_database_executor: - await get_instance(self.hass).async_add_executor_job(self._update) + data = await get_instance(self.hass).async_add_executor_job(self._update) else: - await self.hass.async_add_executor_job(self._update) + data = await self.hass.async_add_executor_job(self._update) + self._process_manual_data(data) - def _update(self) -> None: + def _update(self) -> Any: """Retrieve sensor data from the query.""" data = None self._attr_extra_state_attributes = {} @@ -384,3 +407,4 @@ def _update(self) -> None: _LOGGER.warning("%s returned no results", self._query) sess.close() + return data diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py index 42d578555abb69..fcd98a778312d8 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -624,7 +624,7 @@ def __init__( TriggerBaseEntity.__init__(self, hass, config) @callback - def _process_manual_data(self, value: str | None = None) -> None: + def _process_manual_data(self, value: Any | None = None) -> None: """Process new data manually. Implementing class should call this last in update method to render templates. diff --git a/tests/components/sql/__init__.py b/tests/components/sql/__init__.py index a1417cd38dfc71..53356a85c4e69d 100644 --- a/tests/components/sql/__init__.py +++ b/tests/components/sql/__init__.py @@ -13,12 +13,14 @@ from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_DEVICE_CLASS, + CONF_ICON, CONF_NAME, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.template_entity import CONF_AVAILABILITY, CONF_PICTURE from tests.common import MockConfigEntry @@ -148,6 +150,23 @@ } } +YAML_CONFIG_ALL_TEMPLATES = { + "sql": { + CONF_DB_URL: "sqlite://", + CONF_NAME: "Get values with template", + CONF_QUERY: "SELECT 5 as output", + CONF_COLUMN_NAME: "output", + CONF_UNIT_OF_MEASUREMENT: "MiB/s", + CONF_UNIQUE_ID: "unique_id_123456", + CONF_VALUE_TEMPLATE: "{{ value }}", + CONF_ICON: '{% if states("sensor.input1")=="on" %} mdi:on {% else %} mdi:off {% endif %}', + CONF_PICTURE: '{% if states("sensor.input1")=="on" %} /local/picture1.jpg {% else %} /local/picture2.jpg {% endif %}', + CONF_AVAILABILITY: '{{ states("sensor.input2")=="on" }}', + CONF_DEVICE_CLASS: SensorDeviceClass.DATA_RATE, + CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, + } +} + async def init_integration( hass: HomeAssistant, diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index 0fe0e881c95381..3d0e2768adea66 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -13,7 +13,12 @@ from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.sql.const import CONF_QUERY, DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_UNIQUE_ID, STATE_UNKNOWN +from homeassistant.const import ( + CONF_ICON, + CONF_UNIQUE_ID, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component @@ -21,6 +26,7 @@ from . import ( YAML_CONFIG, + YAML_CONFIG_ALL_TEMPLATES, YAML_CONFIG_BINARY, YAML_CONFIG_FULL_TABLE_SCAN, YAML_CONFIG_FULL_TABLE_SCAN_NO_UNIQUE_ID, @@ -32,13 +38,14 @@ from tests.common import MockConfigEntry, async_fire_time_changed -async def test_query(recorder_mock: Recorder, hass: HomeAssistant) -> None: +async def test_query_basic(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test the SQL sensor.""" config = { "db_url": "sqlite://", "query": "SELECT 5 as value", "column": "value", "name": "Select value SQL query", + "unique_id": "very_unique_id", } await init_integration(hass, config) @@ -235,6 +242,65 @@ async def test_query_from_yaml(recorder_mock: Recorder, hass: HomeAssistant) -> assert state.state == "5" +async def test_templates_with_yaml( + recorder_mock: Recorder, hass: HomeAssistant +) -> None: + """Test the SQL sensor from yaml config with templates.""" + + hass.states.async_set("sensor.input1", "on") + hass.states.async_set("sensor.input2", "on") + await hass.async_block_till_done() + + assert await async_setup_component(hass, DOMAIN, YAML_CONFIG_ALL_TEMPLATES) + await hass.async_block_till_done() + + state = hass.states.get("sensor.get_values_with_template") + assert state.state == "5" + assert state.attributes[CONF_ICON] == "mdi:on" + assert state.attributes["entity_picture"] == "/local/picture1.jpg" + + hass.states.async_set("sensor.input1", "off") + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(minutes=1), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.get_values_with_template") + assert state.state == "5" + assert state.attributes[CONF_ICON] == "mdi:off" + assert state.attributes["entity_picture"] == "/local/picture2.jpg" + + hass.states.async_set("sensor.input2", "off") + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(minutes=2), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.get_values_with_template") + assert state.state == STATE_UNAVAILABLE + + hass.states.async_set("sensor.input1", "on") + hass.states.async_set("sensor.input2", "on") + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(minutes=3), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.get_values_with_template") + assert state.state == "5" + assert state.attributes[CONF_ICON] == "mdi:on" + assert state.attributes["entity_picture"] == "/local/picture1.jpg" + + async def test_config_from_old_yaml( recorder_mock: Recorder, hass: HomeAssistant ) -> None: From 0ba2531ca4ef613115125359670b97a482106e02 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 20 Jul 2023 11:45:44 +0200 Subject: [PATCH 0705/1009] Fix bug in check_config when an integration is removed by its own validator (#96068) * Raise if present is False * Fix feedback * Update homeassistant/helpers/check_config.py Co-authored-by: Erik Montnemery * Update homeassistant/helpers/check_config.py Co-authored-by: Erik Montnemery * Fix tests --------- Co-authored-by: Erik Montnemery --- homeassistant/helpers/check_config.py | 4 +++- tests/helpers/test_check_config.py | 20 +++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 21a54d647289df..ba69a76fbdd400 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -181,7 +181,9 @@ def _comp_error(ex: Exception, domain: str, config: ConfigType) -> None: if config_schema is not None: try: config = config_schema(config) - result[domain] = config[domain] + # Don't fail if the validator removed the domain from the config + if domain in config: + result[domain] = config[domain] except vol.Invalid as ex: _comp_error(ex, domain, config) continue diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 91ef17d526d983..3b9b3cf65584d8 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -8,9 +8,10 @@ CheckConfigError, async_check_ha_config_file, ) +import homeassistant.helpers.config_validation as cv from homeassistant.requirements import RequirementsNotFound -from tests.common import mock_platform, patch_yaml_files +from tests.common import MockModule, mock_integration, mock_platform, patch_yaml_files _LOGGER = logging.getLogger(__name__) @@ -246,3 +247,20 @@ async def test_config_platform_raise(hass: HomeAssistant) -> None: assert err.domain == "bla" assert err.message == "Unexpected error calling config validator: Broken" assert err.config == {"value": 1} + + +async def test_removed_yaml_support(hass: HomeAssistant) -> None: + """Test config validation check with removed CONFIG_SCHEMA without raise if present.""" + mock_integration( + hass, + MockModule( + domain="bla", config_schema=cv.removed("bla", raise_if_present=False) + ), + False, + ) + files = {YAML_CONFIG_FILE: BASE_CONFIG + "bla:\n platform: demo"} + with patch("os.path.isfile", return_value=True), patch_yaml_files(files): + res = await async_check_ha_config_file(hass) + log_ha_config(res) + + assert res.keys() == {"homeassistant"} From c433b251fa125789a2b8a68e67014d0eeb7bf6e0 Mon Sep 17 00:00:00 2001 From: RoboMagus <68224306+RoboMagus@users.noreply.github.com> Date: Thu, 20 Jul 2023 11:53:57 +0200 Subject: [PATCH 0706/1009] Shell command response (#96695) * Add service response to shell_commands * Add shell_command response tests * Fix mypy * Return empty dict instead of None on error * Improved response type hint * Cleanup after removing type cast * Raise exceptions i.s.o. returning * Fix ruff --- .../components/shell_command/__init__.py | 31 ++++++++++-- tests/components/shell_command/test_init.py | 49 ++++++++++++++++--- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 36c3a5dbda5021..8430d7284ee4fa 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -9,10 +9,16 @@ import async_timeout import voluptuous as vol -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, +) from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.typing import ConfigType +from homeassistant.util.json import JsonObjectType DOMAIN = "shell_command" @@ -31,7 +37,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: cache: dict[str, tuple[str, str | None, template.Template | None]] = {} - async def async_service_handler(service: ServiceCall) -> None: + async def async_service_handler(service: ServiceCall) -> ServiceResponse: """Execute a shell command service.""" cmd = conf[service.service] @@ -54,7 +60,7 @@ async def async_service_handler(service: ServiceCall) -> None: ) except TemplateError as ex: _LOGGER.exception("Error rendering command template: %s", ex) - return + raise else: rendered_args = None @@ -97,9 +103,16 @@ async def async_service_handler(service: ServiceCall) -> None: process._transport.close() # type: ignore[attr-defined] del process - return + raise + + service_response: JsonObjectType = { + "stdout": "", + "stderr": "", + "returncode": process.returncode, + } if stdout_data: + service_response["stdout"] = stdout_data.decode("utf-8").strip() _LOGGER.debug( "Stdout of command: `%s`, return code: %s:\n%s", cmd, @@ -107,6 +120,7 @@ async def async_service_handler(service: ServiceCall) -> None: stdout_data, ) if stderr_data: + service_response["stderr"] = stderr_data.decode("utf-8").strip() _LOGGER.debug( "Stderr of command: `%s`, return code: %s:\n%s", cmd, @@ -118,6 +132,13 @@ async def async_service_handler(service: ServiceCall) -> None: "Error running command: `%s`, return code: %s", cmd, process.returncode ) + return service_response + for name in conf: - hass.services.async_register(DOMAIN, name, async_service_handler) + hass.services.async_register( + DOMAIN, + name, + async_service_handler, + supports_response=SupportsResponse.OPTIONAL, + ) return True diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index fe685398c5d4fe..ac594c811edf29 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -10,6 +10,7 @@ from homeassistant.components import shell_command from homeassistant.core import HomeAssistant +from homeassistant.exceptions import TemplateError from homeassistant.setup import async_setup_component @@ -83,6 +84,28 @@ async def test_template_render_no_template(mock_call, hass: HomeAssistant) -> No assert cmd == "ls /bin" +@patch("homeassistant.components.shell_command.asyncio.create_subprocess_shell") +async def test_incorrect_template(mock_call, hass: HomeAssistant) -> None: + """Ensure shell_commands with invalid templates are handled properly.""" + mock_call.return_value = mock_process_creator(error=False) + assert await async_setup_component( + hass, + shell_command.DOMAIN, + { + shell_command.DOMAIN: { + "test_service": ("ls /bin {{ states['invalid/domain'] }}") + } + }, + ) + + with pytest.raises(TemplateError): + await hass.services.async_call( + "shell_command", "test_service", blocking=True, return_response=True + ) + + await hass.async_block_till_done() + + @patch("homeassistant.components.shell_command.asyncio.create_subprocess_exec") async def test_template_render(mock_call, hass: HomeAssistant) -> None: """Ensure shell_commands with templates get rendered properly.""" @@ -120,11 +143,14 @@ async def test_subprocess_error(mock_error, mock_call, hass: HomeAssistant) -> N {shell_command.DOMAIN: {"test_service": f"touch {path}"}}, ) - await hass.services.async_call("shell_command", "test_service", blocking=True) + response = await hass.services.async_call( + "shell_command", "test_service", blocking=True, return_response=True + ) await hass.async_block_till_done() assert mock_call.call_count == 1 assert mock_error.call_count == 1 assert not os.path.isfile(path) + assert response["returncode"] == 1 @patch("homeassistant.components.shell_command._LOGGER.debug") @@ -137,11 +163,15 @@ async def test_stdout_captured(mock_output, hass: HomeAssistant) -> None: {shell_command.DOMAIN: {"test_service": f"echo {test_phrase}"}}, ) - await hass.services.async_call("shell_command", "test_service", blocking=True) + response = await hass.services.async_call( + "shell_command", "test_service", blocking=True, return_response=True + ) await hass.async_block_till_done() assert mock_output.call_count == 1 assert test_phrase.encode() + b"\n" == mock_output.call_args_list[0][0][-1] + assert response["stdout"] == test_phrase + assert response["returncode"] == 0 @patch("homeassistant.components.shell_command._LOGGER.debug") @@ -154,11 +184,14 @@ async def test_stderr_captured(mock_output, hass: HomeAssistant) -> None: {shell_command.DOMAIN: {"test_service": f">&2 echo {test_phrase}"}}, ) - await hass.services.async_call("shell_command", "test_service", blocking=True) + response = await hass.services.async_call( + "shell_command", "test_service", blocking=True, return_response=True + ) await hass.async_block_till_done() assert mock_output.call_count == 1 assert test_phrase.encode() + b"\n" == mock_output.call_args_list[0][0][-1] + assert response["stderr"] == test_phrase async def test_do_not_run_forever( @@ -187,9 +220,13 @@ async def block(): "homeassistant.components.shell_command.asyncio.create_subprocess_shell", side_effect=mock_create_subprocess_shell, ): - await hass.services.async_call( - shell_command.DOMAIN, "test_service", blocking=True - ) + with pytest.raises(asyncio.TimeoutError): + await hass.services.async_call( + shell_command.DOMAIN, + "test_service", + blocking=True, + return_response=True, + ) await hass.async_block_till_done() mock_process.kill.assert_called_once() From db83dc9accc62c712db1e77454e32c88b85ac74e Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 20 Jul 2023 11:11:05 +0000 Subject: [PATCH 0707/1009] Create an issue if push updates fail for Shelly gen1 devices (#96910) * Create an issue if push updates fail * Improve strings * Delete the issue when reloading configuration entry * Change MAX_PUSH_UPDATE_FAILURES to 5 * Improve issue strings * Add test * Use for * Update homeassistant/components/shelly/strings.json Co-authored-by: Charles Garwood * Simplify deleting the issue --------- Co-authored-by: Charles Garwood --- homeassistant/components/shelly/__init__.py | 10 +++++++ homeassistant/components/shelly/const.py | 4 +++ .../components/shelly/coordinator.py | 23 +++++++++++++++ homeassistant/components/shelly/strings.json | 6 ++++ tests/components/shelly/test_coordinator.py | 29 +++++++++++++++++++ 5 files changed, 72 insertions(+) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 8f08aab8d3072f..e5e90bf19af166 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -14,6 +14,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import ( @@ -30,6 +31,7 @@ DEFAULT_COAP_PORT, DOMAIN, LOGGER, + PUSH_UPDATE_ISSUE_ID, ) from .coordinator import ( ShellyBlockCoordinator, @@ -323,6 +325,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok + # delete push update issue if it exists + LOGGER.debug( + "Deleting issue %s", PUSH_UPDATE_ISSUE_ID.format(unique=entry.unique_id) + ) + ir.async_delete_issue( + hass, DOMAIN, PUSH_UPDATE_ISSUE_ID.format(unique=entry.unique_id) + ) + platforms = BLOCK_SLEEPING_PLATFORMS if not entry.data.get(CONF_SLEEP_PERIOD): diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 7aa86af1e9a957..e678f92c480341 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -174,3 +174,7 @@ class BLEScannerMode(StrEnum): DISABLED = "disabled" ACTIVE = "active" PASSIVE = "passive" + + +MAX_PUSH_UPDATE_FAILURES = 5 +PUSH_UPDATE_ISSUE_ID = "push_update_{unique}" diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 6d7b3496880224..0d4a091b72936f 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -17,6 +17,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, @@ -41,7 +42,9 @@ EVENT_SHELLY_CLICK, INPUTS_EVENTS_DICT, LOGGER, + MAX_PUSH_UPDATE_FAILURES, MODELS_SUPPORTING_LIGHT_EFFECTS, + PUSH_UPDATE_ISSUE_ID, REST_SENSORS_UPDATE_INTERVAL, RPC_INPUTS_EVENTS_TYPES, RPC_RECONNECT_INTERVAL, @@ -162,6 +165,7 @@ def __init__( self._last_effect: int | None = None self._last_input_events_count: dict = {} self._last_target_temp: float | None = None + self._push_update_failures: int = 0 entry.async_on_unload( self.async_add_listener(self._async_device_updates_handler) @@ -270,6 +274,25 @@ async def _async_update_data(self) -> None: except InvalidAuthError: self.entry.async_start_reauth(self.hass) else: + self._push_update_failures += 1 + if self._push_update_failures > MAX_PUSH_UPDATE_FAILURES: + LOGGER.debug( + "Creating issue %s", PUSH_UPDATE_ISSUE_ID.format(unique=self.mac) + ) + ir.async_create_issue( + self.hass, + DOMAIN, + PUSH_UPDATE_ISSUE_ID.format(unique=self.mac), + is_fixable=False, + is_persistent=False, + severity=ir.IssueSeverity.ERROR, + learn_more_url="https://www.home-assistant.io/integrations/shelly/#shelly-device-configuration-generation-1", + translation_key="push_update_failure", + translation_placeholders={ + "device_name": self.entry.title, + "ip_address": self.device.ip_address, + }, + ) device_update_info(self.hass, self.device, self.entry) def async_setup(self) -> None: diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index eeb2c3d3224fc2..7c3f6033d07ce2 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -118,5 +118,11 @@ } } } + }, + "issues": { + "push_update_failure": { + "title": "Shelly device {device_name} push update failure", + "description": "Home Assistant is not receiving push updates from the Shelly device {device_name} with IP address {ip_address}. Check the CoIoT configuration in the web panel of the device and your network configuration." + } } } diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 9039893999d1dc..8536c3d72e6d52 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -13,6 +13,7 @@ ATTR_GENERATION, DOMAIN, ENTRY_RELOAD_COOLDOWN, + MAX_PUSH_UPDATE_FAILURES, RPC_RECONNECT_INTERVAL, SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, @@ -24,15 +25,18 @@ async_entries_for_config_entry, async_get as async_get_dev_reg, ) +import homeassistant.helpers.issue_registry as ir from homeassistant.util import dt as dt_util from . import ( + MOCK_MAC, init_integration, inject_rpc_device_event, mock_polling_rpc_update, mock_rest_update, register_entity, ) +from .conftest import MOCK_BLOCKS from tests.common import async_fire_time_changed @@ -249,6 +253,31 @@ async def test_block_sleeping_device_no_periodic_updates( assert hass.states.get(entity_id).state == STATE_UNAVAILABLE +async def test_block_device_push_updates_failure( + hass: HomeAssistant, mock_block_device, monkeypatch +) -> None: + """Test block device with push updates failure.""" + issue_registry: ir.IssueRegistry = ir.async_get(hass) + + monkeypatch.setattr( + mock_block_device, + "update", + AsyncMock(return_value=MOCK_BLOCKS), + ) + await init_integration(hass, 1) + + # Move time to force polling + for _ in range(MAX_PUSH_UPDATE_FAILURES + 1): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 15) + ) + await hass.async_block_till_done() + + assert issue_registry.async_get_issue( + domain=DOMAIN, issue_id=f"push_update_{MOCK_MAC}" + ) + + async def test_block_button_click_event( hass: HomeAssistant, mock_block_device, events, monkeypatch ) -> None: From 8896c164be2ed5cb495a4614767c63d36b347d2d Mon Sep 17 00:00:00 2001 From: lkshrk Date: Thu, 20 Jul 2023 13:11:43 +0200 Subject: [PATCH 0708/1009] Update .devcontainer.json structure (#96537) --- .devcontainer/devcontainer.json | 76 +++++++++++++++++---------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 042eb94b1954d1..27e2d2e5ad0d0c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,42 +7,46 @@ "containerEnv": { "DEVCONTAINER": "1" }, "appPort": ["8123:8123"], "runArgs": ["-e", "GIT_EDITOR=code --wait"], - "extensions": [ - "ms-python.vscode-pylance", - "visualstudioexptteam.vscodeintellicode", - "redhat.vscode-yaml", - "esbenp.prettier-vscode", - "GitHub.vscode-pull-request-github" - ], - // Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json - "settings": { - "python.pythonPath": "/usr/local/bin/python", - "python.linting.enabled": true, - "python.linting.pylintEnabled": true, - "python.formatting.blackPath": "/usr/local/bin/black", - "python.linting.pycodestylePath": "/usr/local/bin/pycodestyle", - "python.linting.pydocstylePath": "/usr/local/bin/pydocstyle", - "python.linting.mypyPath": "/usr/local/bin/mypy", - "python.linting.pylintPath": "/usr/local/bin/pylint", - "python.formatting.provider": "black", - "python.testing.pytestArgs": ["--no-cov"], - "editor.formatOnPaste": false, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "files.trimTrailingWhitespace": true, - "terminal.integrated.profiles.linux": { - "zsh": { - "path": "/usr/bin/zsh" + "customizations": { + "vscode": { + "extensions": [ + "ms-python.vscode-pylance", + "visualstudioexptteam.vscodeintellicode", + "redhat.vscode-yaml", + "esbenp.prettier-vscode", + "GitHub.vscode-pull-request-github" + ], + // Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json + "settings": { + "python.pythonPath": "/usr/local/bin/python", + "python.linting.enabled": true, + "python.linting.pylintEnabled": true, + "python.formatting.blackPath": "/usr/local/bin/black", + "python.linting.pycodestylePath": "/usr/local/bin/pycodestyle", + "python.linting.pydocstylePath": "/usr/local/bin/pydocstyle", + "python.linting.mypyPath": "/usr/local/bin/mypy", + "python.linting.pylintPath": "/usr/local/bin/pylint", + "python.formatting.provider": "black", + "python.testing.pytestArgs": ["--no-cov"], + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "files.trimTrailingWhitespace": true, + "terminal.integrated.profiles.linux": { + "zsh": { + "path": "/usr/bin/zsh" + } + }, + "terminal.integrated.defaultProfile.linux": "zsh", + "yaml.customTags": [ + "!input scalar", + "!secret scalar", + "!include_dir_named scalar", + "!include_dir_list scalar", + "!include_dir_merge_list scalar", + "!include_dir_merge_named scalar" + ] } - }, - "terminal.integrated.defaultProfile.linux": "zsh", - "yaml.customTags": [ - "!input scalar", - "!secret scalar", - "!include_dir_named scalar", - "!include_dir_list scalar", - "!include_dir_merge_list scalar", - "!include_dir_merge_named scalar" - ] + } } } From df46179d26fd46a41dc18ccaf0075f77e7d7fe07 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 20 Jul 2023 13:11:55 +0200 Subject: [PATCH 0709/1009] Fix broken service test (#96943) --- tests/helpers/test_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index d41b55c0b482b4..7348e1bf3e22d0 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -759,6 +759,7 @@ async def test_async_get_all_descriptions_dynamically_created_services( "description": "", "fields": {}, "name": "", + "response": {"optional": True}, } From 14b553ddbc3074d3e0ce44ea7757929f50c6f39b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 20 Jul 2023 13:16:02 +0200 Subject: [PATCH 0710/1009] 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 e8ddc96de203f0..63ca182ce2cca2 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 d36b3f61d9db85..96d8bd03a52f07 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -33,6 +33,7 @@ "face-recognition", "opencv-python-headless", "pybluez", + "pycocotools", "pycups", "python-eq3bt", "python-gammu", From f809ce90337693abe9e337f1491a4727dd678e2e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 20 Jul 2023 13:34:24 +0200 Subject: [PATCH 0711/1009] Update bind_hass docstring to discourage its use (#96933) --- homeassistant/loader.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 6a8131d2454ebc..6c083b6a024c64 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -1098,7 +1098,11 @@ def __getattr__(self, helper_name: str) -> ModuleWrapper: def bind_hass(func: _CallableT) -> _CallableT: - """Decorate function to indicate that first argument is hass.""" + """Decorate function to indicate that first argument is hass. + + The use of this decorator is discouraged, and it should not be used + for new functions. + """ setattr(func, "__bind_hass", True) return func From a381ceed86b0b8ca39260cfb022ddbdcd6807ab9 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Thu, 20 Jul 2023 14:43:38 +0200 Subject: [PATCH 0712/1009] 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 ef2ecc7aa23e94..c59150266d9aef 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -61,6 +61,8 @@ class 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 fff254e0dc79883e9f56446534f913948e399b1d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 20 Jul 2023 14:45:07 +0200 Subject: [PATCH 0713/1009] Avoid using name in Subaru migrations (#96221) * Avoid using name in Subaru migrations * Add feedback * Update tests/components/subaru/test_sensor.py Co-authored-by: G Johansson * Update tests/components/subaru/test_sensor.py Co-authored-by: G-Two <7310260+G-Two@users.noreply.github.com> --------- Co-authored-by: G Johansson Co-authored-by: G-Two <7310260+G-Two@users.noreply.github.com> --- homeassistant/components/subaru/sensor.py | 21 +++++++++++++-------- tests/components/subaru/test_sensor.py | 21 +++++++++++---------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py index 6c8e8fc100b165..50e8f89716bd7f 100644 --- a/homeassistant/components/subaru/sensor.py +++ b/homeassistant/components/subaru/sensor.py @@ -276,14 +276,19 @@ async def _async_migrate_entries( """Migrate sensor entries from HA<=2022.10 to use preferred unique_id.""" entity_registry = er.async_get(hass) - all_sensors = [] - all_sensors.extend(EV_SENSORS) - all_sensors.extend(API_GEN_2_SENSORS) - all_sensors.extend(SAFETY_SENSORS) - - # Old unique_id is (previously title-cased) sensor name - # (e.g. "VIN_Avg Fuel Consumption") - replacements = {str(s.name).upper(): s.key for s in all_sensors} + replacements = { + "ODOMETER": sc.ODOMETER, + "AVG FUEL CONSUMPTION": sc.AVG_FUEL_CONSUMPTION, + "RANGE": sc.DIST_TO_EMPTY, + "TIRE PRESSURE FL": sc.TIRE_PRESSURE_FL, + "TIRE PRESSURE FR": sc.TIRE_PRESSURE_FR, + "TIRE PRESSURE RL": sc.TIRE_PRESSURE_RL, + "TIRE PRESSURE RR": sc.TIRE_PRESSURE_RR, + "FUEL LEVEL": sc.REMAINING_FUEL_PERCENT, + "EV RANGE": sc.EV_DISTANCE_TO_EMPTY, + "EV BATTERY LEVEL": sc.EV_STATE_OF_CHARGE_PERCENT, + "EV TIME TO FULL CHARGE": sc.EV_TIME_TO_FULLY_CHARGED_UTC, + } @callback def update_unique_id(entry: er.RegistryEntry) -> dict[str, Any] | None: diff --git a/tests/components/subaru/test_sensor.py b/tests/components/subaru/test_sensor.py index aa351f7ccbd2ea..fd03ed3044b4a4 100644 --- a/tests/components/subaru/test_sensor.py +++ b/tests/components/subaru/test_sensor.py @@ -1,4 +1,5 @@ """Test Subaru sensors.""" +from typing import Any from unittest.mock import patch import pytest @@ -12,7 +13,6 @@ ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.util import slugify from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM from .api_responses import ( @@ -25,7 +25,6 @@ from .conftest import ( MOCK_API_FETCH, MOCK_API_GET_DATA, - TEST_DEVICE_NAME, advance_time_to_next_fetch, setup_subaru_config_entry, ) @@ -65,9 +64,9 @@ async def test_sensors_missing_vin_data(hass: HomeAssistant, ev_entry) -> None: { "domain": SENSOR_DOMAIN, "platform": SUBARU_DOMAIN, - "unique_id": f"{TEST_VIN_2_EV}_{API_GEN_2_SENSORS[0].name}", + "unique_id": f"{TEST_VIN_2_EV}_Avg fuel consumption", }, - f"{TEST_VIN_2_EV}_{API_GEN_2_SENSORS[0].name}", + f"{TEST_VIN_2_EV}_Avg fuel consumption", f"{TEST_VIN_2_EV}_{API_GEN_2_SENSORS[0].key}", ), ], @@ -97,9 +96,9 @@ async def test_sensor_migrate_unique_ids( { "domain": SENSOR_DOMAIN, "platform": SUBARU_DOMAIN, - "unique_id": f"{TEST_VIN_2_EV}_{API_GEN_2_SENSORS[0].name}", + "unique_id": f"{TEST_VIN_2_EV}_Avg fuel consumption", }, - f"{TEST_VIN_2_EV}_{API_GEN_2_SENSORS[0].name}", + f"{TEST_VIN_2_EV}_Avg fuel consumption", f"{TEST_VIN_2_EV}_{API_GEN_2_SENSORS[0].key}", ) ], @@ -136,15 +135,17 @@ async def test_sensor_migrate_unique_ids_duplicate( assert entity_migrated != entity_not_changed -def _assert_data(hass, expected_state): +def _assert_data(hass: HomeAssistant, expected_state: dict[str, Any]) -> None: sensor_list = EV_SENSORS sensor_list.extend(API_GEN_2_SENSORS) sensor_list.extend(SAFETY_SENSORS) expected_states = {} + entity_registry = er.async_get(hass) for item in sensor_list: - expected_states[ - f"sensor.{slugify(f'{TEST_DEVICE_NAME} {item.name}')}" - ] = expected_state[item.key] + entity = entity_registry.async_get_entity_id( + SENSOR_DOMAIN, SUBARU_DOMAIN, f"{TEST_VIN_2_EV}_{item.key}" + ) + expected_states[entity] = expected_state[item.key] for sensor, value in expected_states.items(): actual = hass.states.get(sensor) From c99adf54b42fdf6baadab476e2f7a85288ce9372 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 20 Jul 2023 16:11:14 +0200 Subject: [PATCH 0714/1009] 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 b4126b1c261dff..e118f7ae39b8ed 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 6c5f1addd5a6a6..6575d2f8fb329a 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 e725201bb7ba0b..d4445c953692d2 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 d36d2338856e631fee4943ae96cb25ab59e222be Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 20 Jul 2023 16:12:14 +0200 Subject: [PATCH 0715/1009] Update pipdeptree to 2.10.2 (#96940) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 803d0cb90a04b6..02a8a04e1b94a8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -16,7 +16,7 @@ pre-commit==3.3.3 pydantic==1.10.11 pylint==2.17.4 pylint-per-file-ignores==1.2.1 -pipdeptree==2.9.4 +pipdeptree==2.10.2 pytest-asyncio==0.20.3 pytest-aiohttp==1.0.4 pytest-cov==3.0.0 From da5cba8083b29bdcdc5ef99c3f5463fb794b0f50 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 0716/1009] 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 01f77cb2d38a4f..dd29d02d655760 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 63ca182ce2cca2..3e67f654d6c602 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1813,7 +1813,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 bc665abf4c8898..73f0a2300566a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1341,7 +1341,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 2a13515759d909eae06c0ecaf09eaa03a12c937e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Jul 2023 13:18:33 -0500 Subject: [PATCH 0717/1009] Bump aiohomekit to 2.6.9 (#96956) --- 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 82d91863fadf3c..9528ae568fd4e4 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.8"], + "requirements": ["aiohomekit==2.6.9"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 3e67f654d6c602..b69361458dce0e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -249,7 +249,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==2.6.8 +aiohomekit==2.6.9 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 73f0a2300566a5..9280daf186c7b3 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.8 +aiohomekit==2.6.9 # homeassistant.components.emulated_hue # homeassistant.components.http From e9620c62b8dc091d572234963f855bc437764ce0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Jul 2023 13:36:46 -0500 Subject: [PATCH 0718/1009] Fix assertions in zeroconf tests (#96957) --- tests/components/zeroconf/test_init.py | 81 ++------------------------ tests/conftest.py | 6 +- 2 files changed, 9 insertions(+), 78 deletions(-) diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 5740abef789b41..b07e2d5880a123 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -12,7 +12,6 @@ from zeroconf.asyncio import AsyncServiceInfo from homeassistant.components import zeroconf -from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6 from homeassistant.const import ( EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START, @@ -231,82 +230,10 @@ async def test_setup_with_overly_long_url_and_name( assert "German Umlaut" in caplog.text -async def test_setup_with_default_interface( - hass: HomeAssistant, mock_async_zeroconf: None +async def test_setup_with_defaults( + hass: HomeAssistant, mock_zeroconf: None, mock_async_zeroconf: None ) -> None: """Test default interface config.""" - with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock - ), patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_service_info_mock, - ): - assert await async_setup_component( - hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_DEFAULT_INTERFACE: True}} - ) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - assert mock_async_zeroconf.called_with(interface_choice=InterfaceChoice.Default) - - -async def test_setup_without_default_interface( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: - """Test without default interface config.""" - with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock - ), patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_service_info_mock, - ): - assert await async_setup_component( - hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_DEFAULT_INTERFACE: False}} - ) - - assert mock_async_zeroconf.called_with() - - -async def test_setup_without_ipv6( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: - """Test without ipv6.""" - with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock - ), patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_service_info_mock, - ): - assert await async_setup_component( - hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_IPV6: False}} - ) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - assert mock_async_zeroconf.called_with(ip_version=IPVersion.V4Only) - - -async def test_setup_with_ipv6(hass: HomeAssistant, mock_async_zeroconf: None) -> None: - """Test without ipv6.""" - with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock - ), patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_service_info_mock, - ): - assert await async_setup_component( - hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_IPV6: True}} - ) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - assert mock_async_zeroconf.called_with() - - -async def test_setup_with_ipv6_default( - hass: HomeAssistant, mock_async_zeroconf: None -) -> None: - """Test without ipv6 as default.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock ), patch( @@ -317,7 +244,9 @@ async def test_setup_with_ipv6_default( hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert mock_async_zeroconf.called_with() + mock_zeroconf.assert_called_with( + interfaces=InterfaceChoice.Default, ip_version=IPVersion.V4Only + ) async def test_zeroconf_match_macaddress( diff --git a/tests/conftest.py b/tests/conftest.py index 922e42c7a7e33f..6b4c35c4a377cc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1104,10 +1104,12 @@ def mock_get_source_ip() -> Generator[None, None, None]: @pytest.fixture def mock_zeroconf() -> Generator[None, None, None]: """Mock zeroconf.""" - with patch("homeassistant.components.zeroconf.HaZeroconf", autospec=True), patch( + with patch( + "homeassistant.components.zeroconf.HaZeroconf", autospec=True + ) as mock_zc, patch( "homeassistant.components.zeroconf.HaAsyncServiceBrowser", autospec=True ): - yield + yield mock_zc @pytest.fixture From b7bcc1eae4ec89f8d35a2011e815280a2f5cc743 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Jul 2023 15:20:15 -0500 Subject: [PATCH 0719/1009] Bump yalexs-ble to 2.2.3 (#96927) --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index b8d77d5d82a318..0dbc4c8f7d6f3b 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==1.5.1", "yalexs-ble==2.2.1"] + "requirements": ["yalexs==1.5.1", "yalexs-ble==2.2.3"] } diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 8cac3fb81f7ed8..3aefeea048a75a 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", "iot_class": "local_push", - "requirements": ["yalexs-ble==2.2.1"] + "requirements": ["yalexs-ble==2.2.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index b69361458dce0e..e0390171ebd5e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2708,7 +2708,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.2.1 +yalexs-ble==2.2.3 # homeassistant.components.august yalexs==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9280daf186c7b3..4179c2fe8e122a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1987,7 +1987,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.2.1 +yalexs-ble==2.2.3 # homeassistant.components.august yalexs==1.5.1 From e9a63b7501887533088d77f83c87b448b4d5f006 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 20 Jul 2023 23:02:59 +0200 Subject: [PATCH 0720/1009] Use default icon for demo button entity (#96961) --- homeassistant/components/demo/button.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/demo/button.py b/homeassistant/components/demo/button.py index 02f3f584003a26..8b4a94a40afd85 100644 --- a/homeassistant/components/demo/button.py +++ b/homeassistant/components/demo/button.py @@ -22,7 +22,6 @@ async def async_setup_entry( DemoButton( unique_id="push", device_name="Push", - icon="mdi:gesture-tap-button", ), ] ) @@ -39,11 +38,9 @@ def __init__( self, unique_id: str, device_name: str, - icon: str, ) -> None: """Initialize the Demo button entity.""" self._attr_unique_id = unique_id - self._attr_icon = icon self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, unique_id)}, name=device_name, From 6818cae0722f4ef41b69569eb643d09e58a69d91 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Jul 2023 16:05:17 -0500 Subject: [PATCH 0721/1009] Bump aioesphomeapi to 15.1.13 (#96964) --- 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 6a4c1a66334d7b..6ecdf0fddbdfa8 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.1.12", + "aioesphomeapi==15.1.13", "bluetooth-data-tools==1.6.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index e0390171ebd5e3..6fb3c6fdd7b8d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==15.1.12 +aioesphomeapi==15.1.13 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4179c2fe8e122a..35c64169868962 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.1.12 +aioesphomeapi==15.1.13 # homeassistant.components.flo aioflo==2021.11.0 From 99def97ed976e65bddb9e54d2b3642b4912c5668 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Jul 2023 18:03:36 -0500 Subject: [PATCH 0722/1009] Add cancel messages to core task cancelation (#96972) --- homeassistant/core.py | 4 ++-- homeassistant/runner.py | 2 +- homeassistant/util/timeout.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 54f44d0998c546..6ea03e85c43437 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -764,7 +764,7 @@ async def async_stop(self, exit_code: int = 0, *, force: bool = False) -> None: for task in self._background_tasks: self._tasks.add(task) task.add_done_callback(self._tasks.remove) - task.cancel() + task.cancel("Home Assistant is stopping") self._cancel_cancellable_timers() self.exit_code = exit_code @@ -814,7 +814,7 @@ async def async_stop(self, exit_code: int = 0, *, force: bool = False) -> None: "the stop event to prevent delaying shutdown", task, ) - task.cancel() + task.cancel("Home Assistant stage 2 shutdown") try: async with async_timeout.timeout(0.1): await task diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 9a86bed75945b0..67ec232db9c29f 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -196,7 +196,7 @@ def _cancel_all_tasks_with_timeout( return for task in to_cancel: - task.cancel() + task.cancel("Final process shutdown") loop.run_until_complete(asyncio.wait(to_cancel, timeout=timeout)) diff --git a/homeassistant/util/timeout.py b/homeassistant/util/timeout.py index df225580daedce..9d9f5d986a032f 100644 --- a/homeassistant/util/timeout.py +++ b/homeassistant/util/timeout.py @@ -232,7 +232,7 @@ def _cancel_task(self) -> None: """Cancel own task.""" if self._task.done(): return - self._task.cancel() + self._task.cancel("Global task timeout") def pause(self) -> None: """Pause timers while it freeze.""" @@ -330,7 +330,7 @@ def _on_timeout(self) -> None: # Timeout if self._task.done(): return - self._task.cancel() + self._task.cancel("Zone timeout") def pause(self) -> None: """Pause timers while it freeze.""" From 9fba6870fe85afcdf0986222acad0d67a3ed03cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Jul 2023 20:00:07 -0500 Subject: [PATCH 0723/1009] 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 eccac004b7ee4c..a1c09b8815f0ed 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -687,7 +687,7 @@ async def _async_process_on_unload(self, hass: HomeAssistant) -> None: 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 c067c52cf486e79b11cb70d6aac1d7543094a2bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Jul 2023 21:40:38 -0500 Subject: [PATCH 0724/1009] Fix translation key in profiler integration (#96979) --- homeassistant/components/debugpy/strings.json | 2 +- homeassistant/components/profiler/strings.json | 2 +- homeassistant/components/timer/strings.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/debugpy/strings.json b/homeassistant/components/debugpy/strings.json index 74334a15f4a5f9..2de92fc3827c02 100644 --- a/homeassistant/components/debugpy/strings.json +++ b/homeassistant/components/debugpy/strings.json @@ -1,7 +1,7 @@ { "services": { "start": { - "name": "[%key:common::action::stop%]", + "name": "[%key:common::action::start%]", "description": "Starts the Remote Python Debugger." } } diff --git a/homeassistant/components/profiler/strings.json b/homeassistant/components/profiler/strings.json index b9aae585d9f596..a14324a9082e2c 100644 --- a/homeassistant/components/profiler/strings.json +++ b/homeassistant/components/profiler/strings.json @@ -11,7 +11,7 @@ }, "services": { "start": { - "name": "[%key:common::action::stop%]", + "name": "[%key:common::action::start%]", "description": "Starts the Profiler.", "fields": { "seconds": { diff --git a/homeassistant/components/timer/strings.json b/homeassistant/components/timer/strings.json index c52f2627253ba3..56cb46d26b4583 100644 --- a/homeassistant/components/timer/strings.json +++ b/homeassistant/components/timer/strings.json @@ -32,7 +32,7 @@ }, "services": { "start": { - "name": "[%key:common::action::stop%]", + "name": "[%key:common::action::start%]", "description": "Starts a timer.", "fields": { "duration": { From b504665b56dbbf19c616d0f21c32b1b1d5a4b1d9 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 21 Jul 2023 06:35:58 +0200 Subject: [PATCH 0725/1009] Do not override extra_state_attributes property for MqttEntity (#96890) --- homeassistant/components/mqtt/mixins.py | 10 +-------- homeassistant/components/mqtt/siren.py | 30 +++++++++++++++---------- tests/components/mqtt/test_common.py | 2 +- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 314800f33f2d64..57ec933cd589c4 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -342,7 +342,6 @@ class MqttAttributes(Entity): def __init__(self, config: ConfigType) -> None: """Initialize the JSON attributes mixin.""" - self._attributes: dict[str, Any] | None = None self._attributes_sub_state: dict[str, EntitySubscription] = {} self._attributes_config = config @@ -380,16 +379,14 @@ def attributes_message_received(msg: ReceiveMessage) -> None: if k not in MQTT_ATTRIBUTES_BLOCKED and k not in self._attributes_extra_blocked } - self._attributes = filtered_dict + self._attr_extra_state_attributes = filtered_dict get_mqtt_data(self.hass).state_write_requests.write_state_request( self ) else: _LOGGER.warning("JSON result was not a dictionary") - self._attributes = None except ValueError: _LOGGER.warning("Erroneous JSON: %s", payload) - self._attributes = None self._attributes_sub_state = async_prepare_subscribe_topics( self.hass, @@ -414,11 +411,6 @@ async def async_will_remove_from_hass(self) -> None: self.hass, self._attributes_sub_state ) - @property - def extra_state_attributes(self) -> dict[str, Any] | None: - """Return the state attributes.""" - return self._attributes - class MqttAvailability(Entity): """Mixin used for platforms that report availability.""" diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index 4134dd9714863b..d30080f4647d17 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Callable -import copy import functools import logging from typing import Any, cast @@ -141,6 +140,7 @@ class MqttSiren(MqttEntity, SirenEntity): _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_SIREN_ATTRIBUTES_BLOCKED + _extra_attributes: dict[str, Any] _command_templates: dict[ str, Callable[[PublishPayloadType, TemplateVarsType], PublishPayloadType] | None @@ -158,6 +158,7 @@ def __init__( discovery_data: DiscoveryInfoType | None, ) -> None: """Initialize the MQTT siren.""" + self._extra_attributes: dict[str, Any] = {} MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod @@ -174,21 +175,21 @@ def _setup_from_config(self, config: ConfigType) -> None: state_off: str | None = config.get(CONF_STATE_OFF) self._state_off = state_off if state_off else config[CONF_PAYLOAD_OFF] - self._attr_extra_state_attributes = {} + self._extra_attributes = {} _supported_features = SUPPORTED_BASE if config[CONF_SUPPORT_DURATION]: _supported_features |= SirenEntityFeature.DURATION - self._attr_extra_state_attributes[ATTR_DURATION] = None + self._extra_attributes[ATTR_DURATION] = None if config.get(CONF_AVAILABLE_TONES): _supported_features |= SirenEntityFeature.TONES self._attr_available_tones = config[CONF_AVAILABLE_TONES] - self._attr_extra_state_attributes[ATTR_TONE] = None + self._extra_attributes[ATTR_TONE] = None if config[CONF_SUPPORT_VOLUME_SET]: _supported_features |= SirenEntityFeature.VOLUME_SET - self._attr_extra_state_attributes[ATTR_VOLUME_LEVEL] = None + self._extra_attributes[ATTR_VOLUME_LEVEL] = None self._attr_supported_features = _supported_features self._optimistic = config[CONF_OPTIMISTIC] or CONF_STATE_TOPIC not in config @@ -305,14 +306,19 @@ def assumed_state(self) -> bool: return self._optimistic @property - def extra_state_attributes(self) -> dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" - mqtt_attributes = super().extra_state_attributes - attributes = ( - copy.deepcopy(mqtt_attributes) if mqtt_attributes is not None else {} + extra_attributes = ( + self._attr_extra_state_attributes + if hasattr(self, "_attr_extra_state_attributes") + else {} ) - attributes.update(self._attr_extra_state_attributes) - return attributes + if extra_attributes: + return ( + dict({*self._extra_attributes.items(), *extra_attributes.items()}) + or None + ) + return self._extra_attributes or None async def _async_publish( self, @@ -376,6 +382,6 @@ def _update(self, data: SirenTurnOnServiceParameters) -> None: """Update the extra siren state attributes.""" for attribute, support in SUPPORTED_ATTRIBUTES.items(): if self._attr_supported_features & support and attribute in data: - self._attr_extra_state_attributes[attribute] = data[ + self._extra_attributes[attribute] = data[ attribute # type: ignore[literal-required] ] diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index cfd714725c43e8..fd760044f3c809 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -682,7 +682,7 @@ async def help_test_discovery_update_attr( # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, "attr-topic1", '{ "val": "50" }') state = hass.states.get(f"{domain}.test") - assert state and state.attributes.get("val") == "100" + assert state and state.attributes.get("val") != "50" # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, "attr-topic2", '{ "val": "75" }') From 28ff173f164cb80fe9e7e25268e311334abb0531 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Jul 2023 00:07:06 -0500 Subject: [PATCH 0726/1009] Only lookup hostname/ip_address/mac_address once in device_tracker (#96984) --- .../components/device_tracker/config_entry.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 05edfbad91db1f..7d8d0791b4d394 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -405,13 +405,13 @@ async def async_internal_added_to_hass(self) -> None: @property def state_attributes(self) -> dict[str, StateType]: """Return the device state attributes.""" - attr: dict[str, StateType] = {} - attr.update(super().state_attributes) - if self.ip_address: - attr[ATTR_IP] = self.ip_address - if self.mac_address is not None: - attr[ATTR_MAC] = self.mac_address - if self.hostname is not None: - attr[ATTR_HOST_NAME] = self.hostname + attr = super().state_attributes + + if ip_address := self.ip_address: + attr[ATTR_IP] = ip_address + if (mac_address := self.mac_address) is not None: + attr[ATTR_MAC] = mac_address + if (hostname := self.hostname) is not None: + attr[ATTR_HOST_NAME] = hostname return attr From 4e964c3819a74e8abbdfb10477ad62af678e3e9a Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Fri, 21 Jul 2023 07:13:56 +0200 Subject: [PATCH 0727/1009] Bump xiaomi-ble to 0.19.1 (#96967) * Bump xiaomi-ble to 0.19.0 * Bump xiaomi-ble to 0.19.1 --------- Co-authored-by: J. Nick Koston --- homeassistant/components/xiaomi_ble/config_flow.py | 4 ++-- homeassistant/components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index d168835b394344..9115fc5991b4a1 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -123,7 +123,7 @@ async def async_step_get_encryption_key_legacy( if len(bindkey) != 24: errors["bindkey"] = "expected_24_characters" else: - self._discovered_device.bindkey = bytes.fromhex(bindkey) + self._discovered_device.set_bindkey(bytes.fromhex(bindkey)) # If we got this far we already know supported will # return true so we don't bother checking that again @@ -157,7 +157,7 @@ async def async_step_get_encryption_key_4_5( if len(bindkey) != 32: errors["bindkey"] = "expected_32_characters" else: - self._discovered_device.bindkey = bytes.fromhex(bindkey) + self._discovered_device.set_bindkey(bytes.fromhex(bindkey)) # If we got this far we already know supported will # return true so we don't bother checking that again diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 73b22ddab9fc23..683a5dab9dd848 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -20,5 +20,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "iot_class": "local_push", - "requirements": ["xiaomi-ble==0.18.2"] + "requirements": ["xiaomi-ble==0.19.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6fb3c6fdd7b8d5..4efb95e7769dbf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2684,7 +2684,7 @@ wyoming==1.1.0 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.18.2 +xiaomi-ble==0.19.1 # homeassistant.components.knx xknx==2.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 35c64169868962..8bbd5666dee7db 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1966,7 +1966,7 @@ wyoming==1.1.0 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.18.2 +xiaomi-ble==0.19.1 # homeassistant.components.knx xknx==2.11.1 From 92eaef9b18bde1da4829b120c9efd4801ddcc206 Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Fri, 21 Jul 2023 02:54:57 -0400 Subject: [PATCH 0728/1009] 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 4a8a9dec5874c2..0575ac132d47a5 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 4efb95e7769dbf..497cbbf875ccc7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -727,7 +727,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 8bbd5666dee7db..e4021b23e912b2 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 32d63ae890f6277b877539c2da9049fc26410888 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 21 Jul 2023 08:55:44 +0200 Subject: [PATCH 0729/1009] Fix sentry test assert (#96983) --- tests/components/sentry/test_init.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/sentry/test_init.py b/tests/components/sentry/test_init.py index f4486ca5a196f0..25b77922878f9c 100644 --- a/tests/components/sentry/test_init.py +++ b/tests/components/sentry/test_init.py @@ -47,8 +47,8 @@ async def test_setup_entry(hass: HomeAssistant) -> None: assert entry.options[CONF_ENVIRONMENT] == "production" assert sentry_logging_mock.call_count == 1 - assert sentry_logging_mock.called_once_with( - level=logging.WARNING, event_level=logging.WARNING + sentry_logging_mock.assert_called_once_with( + level=logging.WARNING, event_level=logging.ERROR ) assert sentry_aiohttp_mock.call_count == 1 From e2394b34bd37bd86bec659cc2581cb5355f89c65 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Jul 2023 01:56:34 -0500 Subject: [PATCH 0730/1009] Cache version compare in update entity (#96978) --- homeassistant/components/update/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index f788ad21098833..13ab6d38e8a85b 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from datetime import timedelta +from functools import lru_cache import logging from typing import Any, Final, final @@ -182,6 +183,12 @@ class UpdateEntityDescription(EntityDescription): entity_category: EntityCategory | None = EntityCategory.CONFIG +@lru_cache(maxsize=256) +def _version_is_newer(latest_version: str, installed_version: str) -> bool: + """Return True if version is newer.""" + return AwesomeVersion(latest_version) > installed_version + + class UpdateEntity(RestoreEntity): """Representation of an update entity.""" @@ -355,7 +362,7 @@ def state(self) -> str | None: return STATE_OFF try: - newer = AwesomeVersion(latest_version) > installed_version + newer = _version_is_newer(latest_version, installed_version) return STATE_ON if newer else STATE_OFF except AwesomeVersionCompareException: # Can't compare versions, already tried exact match From e9eb8a475473cc823ada7022e55f0e2d7f5e2578 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 21 Jul 2023 09:00:04 +0200 Subject: [PATCH 0731/1009] 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 8dc04e7da86f05..7f0866ce62ecb0 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -205,7 +205,6 @@ class SysMonitorSensorEntityDescription(SensorEntityDescription): key="process", name="Process", icon=CPU_ICON, - state_class=SensorStateClass.MEASUREMENT, mandatory_arg=True, ), "processor_use": SysMonitorSensorEntityDescription( From b39f7d6a71aaf2480a34a5fea9124a3071c60bfe Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 21 Jul 2023 09:54:06 +0200 Subject: [PATCH 0732/1009] Add snapshot testing to YouTube (#96974) --- .../youtube/snapshots/test_sensor.ambr | 31 +++++++++++++++++++ tests/components/youtube/test_sensor.py | 22 ++++--------- 2 files changed, 37 insertions(+), 16 deletions(-) create mode 100644 tests/components/youtube/snapshots/test_sensor.ambr diff --git a/tests/components/youtube/snapshots/test_sensor.ambr b/tests/components/youtube/snapshots/test_sensor.ambr new file mode 100644 index 00000000000000..c5aac39156d925 --- /dev/null +++ b/tests/components/youtube/snapshots/test_sensor.ambr @@ -0,0 +1,31 @@ +# serializer version: 1 +# name: test_sensor + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'entity_picture': 'https://i.ytimg.com/vi/wysukDrMdqU/sddefault.jpg', + 'friendly_name': 'Google for Developers Latest upload', + 'icon': 'mdi:youtube', + 'video_id': 'wysukDrMdqU', + }), + 'context': , + 'entity_id': 'sensor.google_for_developers_latest_upload', + 'last_changed': , + 'last_updated': , + 'state': "What's new in Google Home in less than 1 minute", + }) +# --- +# name: test_sensor.1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'entity_picture': 'https://yt3.ggpht.com/fca_HuJ99xUxflWdex0XViC3NfctBFreIl8y4i9z411asnGTWY-Ql3MeH_ybA4kNaOjY7kyA=s800-c-k-c0x00ffffff-no-rj', + 'friendly_name': 'Google for Developers Subscribers', + 'icon': 'mdi:youtube-subscription', + 'unit_of_measurement': 'subscribers', + }), + 'context': , + 'entity_id': 'sensor.google_for_developers_subscribers', + 'last_changed': , + 'last_updated': , + 'state': '2290000', + }) +# --- diff --git a/tests/components/youtube/test_sensor.py b/tests/components/youtube/test_sensor.py index 6bd993999527a2..f2c5274c4a71c5 100644 --- a/tests/components/youtube/test_sensor.py +++ b/tests/components/youtube/test_sensor.py @@ -4,6 +4,7 @@ from google.auth.exceptions import RefreshError import pytest +from syrupy import SnapshotAssertion from homeassistant import config_entries from homeassistant.components.youtube import DOMAIN @@ -16,28 +17,17 @@ from tests.common import async_fire_time_changed -async def test_sensor(hass: HomeAssistant, setup_integration: ComponentSetup) -> None: +async def test_sensor( + hass: HomeAssistant, snapshot: SnapshotAssertion, setup_integration: ComponentSetup +) -> None: """Test sensor.""" await setup_integration() state = hass.states.get("sensor.google_for_developers_latest_upload") - assert state - assert state.name == "Google for Developers Latest upload" - assert state.state == "What's new in Google Home in less than 1 minute" - assert ( - state.attributes["entity_picture"] - == "https://i.ytimg.com/vi/wysukDrMdqU/sddefault.jpg" - ) - assert state.attributes["video_id"] == "wysukDrMdqU" + assert state == snapshot state = hass.states.get("sensor.google_for_developers_subscribers") - assert state - assert state.name == "Google for Developers Subscribers" - assert state.state == "2290000" - assert ( - state.attributes["entity_picture"] - == "https://yt3.ggpht.com/fca_HuJ99xUxflWdex0XViC3NfctBFreIl8y4i9z411asnGTWY-Ql3MeH_ybA4kNaOjY7kyA=s800-c-k-c0x00ffffff-no-rj" - ) + assert state == snapshot async def test_sensor_updating( From d935c18f38010845d9e5b7526b63b4a378a8a622 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 21 Jul 2023 10:02:05 +0200 Subject: [PATCH 0733/1009] Add entity translations to Daikin (#95181) --- homeassistant/components/daikin/sensor.py | 19 ++++++------ homeassistant/components/daikin/strings.json | 31 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index 2660a1a9d3a62b..ae5f10088204a6 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -55,7 +55,7 @@ class DaikinSensorEntityDescription(SensorEntityDescription, DaikinRequiredKeysM SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( DaikinSensorEntityDescription( key=ATTR_INSIDE_TEMPERATURE, - name="Inside temperature", + translation_key="inside_temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -63,7 +63,7 @@ class DaikinSensorEntityDescription(SensorEntityDescription, DaikinRequiredKeysM ), DaikinSensorEntityDescription( key=ATTR_OUTSIDE_TEMPERATURE, - name="Outside temperature", + translation_key="outside_temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -71,7 +71,6 @@ class DaikinSensorEntityDescription(SensorEntityDescription, DaikinRequiredKeysM ), DaikinSensorEntityDescription( key=ATTR_HUMIDITY, - name="Humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, @@ -79,7 +78,7 @@ class DaikinSensorEntityDescription(SensorEntityDescription, DaikinRequiredKeysM ), DaikinSensorEntityDescription( key=ATTR_TARGET_HUMIDITY, - name="Target humidity", + translation_key="target_humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, @@ -87,7 +86,7 @@ class DaikinSensorEntityDescription(SensorEntityDescription, DaikinRequiredKeysM ), DaikinSensorEntityDescription( key=ATTR_TOTAL_POWER, - name="Compressor estimated power consumption", + translation_key="compressor_estimated_power_consumption", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, @@ -95,7 +94,7 @@ class DaikinSensorEntityDescription(SensorEntityDescription, DaikinRequiredKeysM ), DaikinSensorEntityDescription( key=ATTR_COOL_ENERGY, - name="Cool energy consumption", + translation_key="cool_energy_consumption", icon="mdi:snowflake", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, @@ -104,7 +103,7 @@ class DaikinSensorEntityDescription(SensorEntityDescription, DaikinRequiredKeysM ), DaikinSensorEntityDescription( key=ATTR_HEAT_ENERGY, - name="Heat energy consumption", + translation_key="heat_energy_consumption", icon="mdi:fire", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, @@ -113,7 +112,7 @@ class DaikinSensorEntityDescription(SensorEntityDescription, DaikinRequiredKeysM ), DaikinSensorEntityDescription( key=ATTR_ENERGY_TODAY, - name="Energy consumption", + translation_key="energy_consumption", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, @@ -121,7 +120,7 @@ class DaikinSensorEntityDescription(SensorEntityDescription, DaikinRequiredKeysM ), DaikinSensorEntityDescription( key=ATTR_COMPRESSOR_FREQUENCY, - name="Compressor frequency", + translation_key="compressor_frequency", icon="mdi:fan", device_class=SensorDeviceClass.FREQUENCY, state_class=SensorStateClass.MEASUREMENT, @@ -131,7 +130,7 @@ class DaikinSensorEntityDescription(SensorEntityDescription, DaikinRequiredKeysM ), DaikinSensorEntityDescription( key=ATTR_TOTAL_ENERGY_TODAY, - name="Compressor energy consumption", + translation_key="compressor_energy_consumption", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, diff --git a/homeassistant/components/daikin/strings.json b/homeassistant/components/daikin/strings.json index 7848949831b6fd..93ee636c72618d 100644 --- a/homeassistant/components/daikin/strings.json +++ b/homeassistant/components/daikin/strings.json @@ -21,5 +21,36 @@ "api_password": "Invalid authentication, use either API Key or Password.", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } + }, + "entity": { + "sensor": { + "inside_temperature": { + "name": "Inside temperature" + }, + "outside_temperature": { + "name": "Outside temperature" + }, + "target_humidity": { + "name": "Target humidity" + }, + "compressor_estimated_power_consumption": { + "name": "Compressor estimated power consumption" + }, + "cool_energy_consumption": { + "name": "Cool energy consumption" + }, + "heat_energy_consumption": { + "name": "Heat energy consumption" + }, + "energy_consumption": { + "name": "Energy consumption" + }, + "compressor_frequency": { + "name": "Compressor frequency" + }, + "compressor_energy_consumption": { + "name": "Compressor energy consumption" + } + } } } From 4fa9f25e38a0b5625cb10acac092aa5650ecea6e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 21 Jul 2023 10:03:49 +0200 Subject: [PATCH 0734/1009] Clean up logi circle const (#95540) --- .../components/logi_circle/__init__.py | 2 +- homeassistant/components/logi_circle/const.py | 38 ------------------ .../components/logi_circle/sensor.py | 39 ++++++++++++++++++- 3 files changed, 39 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 7e5d0df02595c2..93e23be5d8d307 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -35,11 +35,11 @@ DOMAIN, LED_MODE_KEY, RECORDING_MODE_KEY, - SENSOR_TYPES, SIGNAL_LOGI_CIRCLE_RECONFIGURE, SIGNAL_LOGI_CIRCLE_RECORD, SIGNAL_LOGI_CIRCLE_SNAPSHOT, ) +from .sensor import SENSOR_TYPES NOTIFICATION_ID = "logi_circle_notification" NOTIFICATION_TITLE = "Logi Circle Setup" diff --git a/homeassistant/components/logi_circle/const.py b/homeassistant/components/logi_circle/const.py index 02e519931982c0..3e74611f767f13 100644 --- a/homeassistant/components/logi_circle/const.py +++ b/homeassistant/components/logi_circle/const.py @@ -1,9 +1,6 @@ """Constants in Logi Circle component.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntityDescription -from homeassistant.const import PERCENTAGE - DOMAIN = "logi_circle" DATA_LOGI = DOMAIN @@ -15,41 +12,6 @@ LED_MODE_KEY = "LED" RECORDING_MODE_KEY = "RECORDING_MODE" -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="battery_level", - name="Battery", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:battery-50", - ), - SensorEntityDescription( - key="last_activity_time", - name="Last Activity", - icon="mdi:history", - ), - SensorEntityDescription( - key="recording", - name="Recording Mode", - icon="mdi:eye", - ), - SensorEntityDescription( - key="signal_strength_category", - name="WiFi Signal Category", - icon="mdi:wifi", - ), - SensorEntityDescription( - key="signal_strength_percentage", - name="WiFi Signal Strength", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:wifi", - ), - SensorEntityDescription( - key="streaming", - name="Streaming Mode", - icon="mdi:camera", - ), -) - SIGNAL_LOGI_CIRCLE_RECONFIGURE = "logi_circle_reconfigure" SIGNAL_LOGI_CIRCLE_SNAPSHOT = "logi_circle_snapshot" SIGNAL_LOGI_CIRCLE_RECORD = "logi_circle_record" diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py index 7d4697adb64247..b27ba30128f520 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -10,6 +10,7 @@ ATTR_BATTERY_CHARGING, CONF_MONITORED_CONDITIONS, CONF_SENSORS, + PERCENTAGE, STATE_OFF, STATE_ON, ) @@ -20,11 +21,47 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.dt import as_local -from .const import ATTRIBUTION, DEVICE_BRAND, DOMAIN as LOGI_CIRCLE_DOMAIN, SENSOR_TYPES +from .const import ATTRIBUTION, DEVICE_BRAND, DOMAIN as LOGI_CIRCLE_DOMAIN _LOGGER = logging.getLogger(__name__) +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="battery_level", + name="Battery", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:battery-50", + ), + SensorEntityDescription( + key="last_activity_time", + name="Last Activity", + icon="mdi:history", + ), + SensorEntityDescription( + key="recording", + name="Recording Mode", + icon="mdi:eye", + ), + SensorEntityDescription( + key="signal_strength_category", + name="WiFi Signal Category", + icon="mdi:wifi", + ), + SensorEntityDescription( + key="signal_strength_percentage", + name="WiFi Signal Strength", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:wifi", + ), + SensorEntityDescription( + key="streaming", + name="Streaming Mode", + icon="mdi:camera", + ), +) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, From 52313bfce5ac6f29157cf34eac50c83f244d6e68 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 21 Jul 2023 11:55:31 +0200 Subject: [PATCH 0735/1009] Clean up Ombi const file (#95541) --- homeassistant/components/ombi/const.py | 35 ------------------------ homeassistant/components/ombi/sensor.py | 36 ++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/ombi/const.py b/homeassistant/components/ombi/const.py index 3ed6738900357c..59a57a480c23c5 100644 --- a/homeassistant/components/ombi/const.py +++ b/homeassistant/components/ombi/const.py @@ -1,8 +1,6 @@ """Support for Ombi.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntityDescription - ATTR_SEASON = "season" CONF_URLBASE = "urlbase" @@ -16,36 +14,3 @@ SERVICE_MOVIE_REQUEST = "submit_movie_request" SERVICE_MUSIC_REQUEST = "submit_music_request" SERVICE_TV_REQUEST = "submit_tv_request" - -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="movies", - name="Movie requests", - icon="mdi:movie", - ), - SensorEntityDescription( - key="tv", - name="TV show requests", - icon="mdi:television-classic", - ), - SensorEntityDescription( - key="music", - name="Music album requests", - icon="mdi:album", - ), - SensorEntityDescription( - key="pending", - name="Pending requests", - icon="mdi:clock-alert-outline", - ), - SensorEntityDescription( - key="approved", - name="Approved requests", - icon="mdi:check", - ), - SensorEntityDescription( - key="available", - name="Available requests", - icon="mdi:download", - ), -) diff --git a/homeassistant/components/ombi/sensor.py b/homeassistant/components/ombi/sensor.py index 1ab4b170e00397..f534144d02cd8d 100644 --- a/homeassistant/components/ombi/sensor.py +++ b/homeassistant/components/ombi/sensor.py @@ -11,13 +11,47 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DOMAIN, SENSOR_TYPES +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=60) +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="movies", + name="Movie requests", + icon="mdi:movie", + ), + SensorEntityDescription( + key="tv", + name="TV show requests", + icon="mdi:television-classic", + ), + SensorEntityDescription( + key="music", + name="Music album requests", + icon="mdi:album", + ), + SensorEntityDescription( + key="pending", + name="Pending requests", + icon="mdi:clock-alert-outline", + ), + SensorEntityDescription( + key="approved", + name="Approved requests", + icon="mdi:check", + ), + SensorEntityDescription( + key="available", + name="Available requests", + icon="mdi:download", + ), +) + + def setup_platform( hass: HomeAssistant, config: ConfigType, From e4d65cbae1447b93b2f9c2be96e229ee886e7b8e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 21 Jul 2023 11:57:40 +0200 Subject: [PATCH 0736/1009] Update syrupy to 4.0.8 (#96990) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 02a8a04e1b94a8..2db6d8fe3d43f2 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -31,7 +31,7 @@ pytest-xdist==3.3.1 pytest==7.3.1 requests_mock==1.11.0 respx==0.20.1 -syrupy==4.0.6 +syrupy==4.0.8 tomli==2.0.1;python_version<"3.11" tqdm==4.64.0 types-atomicwrites==1.4.5.1 From 33c2fc008ad2fb7841bbb87d552ffd5b363f74a4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 21 Jul 2023 11:58:49 +0200 Subject: [PATCH 0737/1009] Add diagnostics to YouTube (#96975) --- .../components/youtube/diagnostics.py | 24 +++++++++++++++++++ .../youtube/snapshots/test_diagnostics.ambr | 17 +++++++++++++ tests/components/youtube/test_diagnostics.py | 23 ++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 homeassistant/components/youtube/diagnostics.py create mode 100644 tests/components/youtube/snapshots/test_diagnostics.ambr create mode 100644 tests/components/youtube/test_diagnostics.py diff --git a/homeassistant/components/youtube/diagnostics.py b/homeassistant/components/youtube/diagnostics.py new file mode 100644 index 00000000000000..380033e450a90b --- /dev/null +++ b/homeassistant/components/youtube/diagnostics.py @@ -0,0 +1,24 @@ +"""Diagnostics support for YouTube.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import ATTR_DESCRIPTION, ATTR_LATEST_VIDEO, COORDINATOR, DOMAIN +from .coordinator import YouTubeDataUpdateCoordinator + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: YouTubeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + COORDINATOR + ] + sensor_data = {} + for channel_id, channel_data in coordinator.data.items(): + channel_data.get(ATTR_LATEST_VIDEO, {}).pop(ATTR_DESCRIPTION) + sensor_data[channel_id] = channel_data + return sensor_data diff --git a/tests/components/youtube/snapshots/test_diagnostics.ambr b/tests/components/youtube/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000000..6a41465ac92596 --- /dev/null +++ b/tests/components/youtube/snapshots/test_diagnostics.ambr @@ -0,0 +1,17 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'UC_x5XG1OV2P6uZZ5FSM9Ttw': dict({ + 'icon': 'https://yt3.ggpht.com/fca_HuJ99xUxflWdex0XViC3NfctBFreIl8y4i9z411asnGTWY-Ql3MeH_ybA4kNaOjY7kyA=s800-c-k-c0x00ffffff-no-rj', + 'id': 'UC_x5XG1OV2P6uZZ5FSM9Ttw', + 'latest_video': dict({ + 'published_at': '2023-05-11T00:20:46Z', + 'thumbnail': 'https://i.ytimg.com/vi/wysukDrMdqU/sddefault.jpg', + 'title': "What's new in Google Home in less than 1 minute", + 'video_id': 'wysukDrMdqU', + }), + 'subscriber_count': 2290000, + 'title': 'Google for Developers', + }), + }) +# --- diff --git a/tests/components/youtube/test_diagnostics.py b/tests/components/youtube/test_diagnostics.py new file mode 100644 index 00000000000000..4fe16c3a8b671e --- /dev/null +++ b/tests/components/youtube/test_diagnostics.py @@ -0,0 +1,23 @@ +"""Tests for the diagnostics data provided by the YouTube integration.""" +from syrupy import SnapshotAssertion + +from homeassistant.components.youtube.const import DOMAIN +from homeassistant.core import HomeAssistant + +from .conftest import ComponentSetup + +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + setup_integration: ComponentSetup, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + await setup_integration() + entry = hass.config_entries.async_entries(DOMAIN)[0] + + assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot From 4916351d9ae8eb62814ff3ed90103a6e33fe230d Mon Sep 17 00:00:00 2001 From: Renier Moorcroft <66512715+RenierM26@users.noreply.github.com> Date: Fri, 21 Jul 2023 12:01:02 +0200 Subject: [PATCH 0738/1009] Add EZVIZ AlarmControlPanelEntity (#96602) * Add ezviz alarm panel --------- Co-authored-by: Joost Lekkerkerker Co-authored-by: Joakim Plate --- .coveragerc | 1 + homeassistant/components/ezviz/__init__.py | 1 + .../components/ezviz/alarm_control_panel.py | 165 ++++++++++++++++++ homeassistant/components/ezviz/const.py | 2 - 4 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/ezviz/alarm_control_panel.py diff --git a/.coveragerc b/.coveragerc index acd218a2d1bac1..6b3270f85e9e64 100644 --- a/.coveragerc +++ b/.coveragerc @@ -315,6 +315,7 @@ omit = homeassistant/components/everlights/light.py homeassistant/components/evohome/* homeassistant/components/ezviz/__init__.py + homeassistant/components/ezviz/alarm_control_panel.py homeassistant/components/ezviz/binary_sensor.py homeassistant/components/ezviz/camera.py homeassistant/components/ezviz/image.py diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index 4355d3d2595cb3..59dfb7c269c832 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -33,6 +33,7 @@ PLATFORMS_BY_TYPE: dict[str, list] = { ATTR_TYPE_CAMERA: [], ATTR_TYPE_CLOUD: [ + Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, Platform.CAMERA, Platform.IMAGE, diff --git a/homeassistant/components/ezviz/alarm_control_panel.py b/homeassistant/components/ezviz/alarm_control_panel.py new file mode 100644 index 00000000000000..32f9b38888f931 --- /dev/null +++ b/homeassistant/components/ezviz/alarm_control_panel.py @@ -0,0 +1,165 @@ +"""Support for Ezviz alarm.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +import logging + +from pyezviz import PyEzvizError +from pyezviz.constants import DefenseModeType + +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanelEntity, + AlarmControlPanelEntityDescription, + AlarmControlPanelEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, +) +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 ( + DATA_COORDINATOR, + DOMAIN, + MANUFACTURER, +) +from .coordinator import EzvizDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=60) +PARALLEL_UPDATES = 0 + + +@dataclass +class EzvizAlarmControlPanelEntityDescriptionMixin: + """Mixin values for EZVIZ Alarm control panel entities.""" + + ezviz_alarm_states: list + + +@dataclass +class EzvizAlarmControlPanelEntityDescription( + AlarmControlPanelEntityDescription, EzvizAlarmControlPanelEntityDescriptionMixin +): + """Describe an EZVIZ Alarm control panel entity.""" + + +ALARM_TYPE = EzvizAlarmControlPanelEntityDescription( + key="ezviz_alarm", + ezviz_alarm_states=[ + None, + STATE_ALARM_DISARMED, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + ], +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Ezviz alarm control panel.""" + coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR + ] + + device_info: DeviceInfo = { + "identifiers": {(DOMAIN, entry.unique_id)}, # type: ignore[arg-type] + "name": "EZVIZ Alarm", + "model": "EZVIZ Alarm", + "manufacturer": MANUFACTURER, + } + + async_add_entities( + [EzvizAlarm(coordinator, entry.entry_id, device_info, ALARM_TYPE)] + ) + + +class EzvizAlarm(AlarmControlPanelEntity): + """Representation of an Ezviz alarm control panel.""" + + entity_description: EzvizAlarmControlPanelEntityDescription + _attr_has_entity_name = True + _attr_name = None + _attr_supported_features = ( + AlarmControlPanelEntityFeature.ARM_AWAY + | AlarmControlPanelEntityFeature.ARM_HOME + ) + _attr_code_arm_required = False + + def __init__( + self, + coordinator: EzvizDataUpdateCoordinator, + entry_id: str, + device_info: DeviceInfo, + entity_description: EzvizAlarmControlPanelEntityDescription, + ) -> None: + """Initialize alarm control panel entity.""" + self._attr_unique_id = f"{entry_id}_{entity_description.key}" + self._attr_device_info = device_info + self.entity_description = entity_description + self.coordinator = coordinator + self._attr_state = None + + async def async_added_to_hass(self) -> None: + """Entity added to hass.""" + self.async_schedule_update_ha_state(True) + + def alarm_disarm(self, code: str | None = None) -> None: + """Send disarm command.""" + try: + if self.coordinator.ezviz_client.api_set_defence_mode( + DefenseModeType.HOME_MODE.value + ): + self._attr_state = STATE_ALARM_DISARMED + + except PyEzvizError as err: + raise HomeAssistantError("Cannot disarm EZVIZ alarm") from err + + def alarm_arm_away(self, code: str | None = None) -> None: + """Send arm away command.""" + try: + if self.coordinator.ezviz_client.api_set_defence_mode( + DefenseModeType.AWAY_MODE.value + ): + self._attr_state = STATE_ALARM_ARMED_AWAY + + except PyEzvizError as err: + raise HomeAssistantError("Cannot arm EZVIZ alarm") from err + + def alarm_arm_home(self, code: str | None = None) -> None: + """Send arm home command.""" + try: + if self.coordinator.ezviz_client.api_set_defence_mode( + DefenseModeType.SLEEP_MODE.value + ): + self._attr_state = STATE_ALARM_ARMED_HOME + + except PyEzvizError as err: + raise HomeAssistantError("Cannot arm EZVIZ alarm") from err + + def update(self) -> None: + """Fetch data from EZVIZ.""" + ezviz_alarm_state_number = "0" + try: + ezviz_alarm_state_number = ( + self.coordinator.ezviz_client.get_group_defence_mode() + ) + _LOGGER.debug( + "Updating EZVIZ alarm with response %s", ezviz_alarm_state_number + ) + self._attr_state = self.entity_description.ezviz_alarm_states[ + int(ezviz_alarm_state_number) + ] + + except PyEzvizError as error: + raise HomeAssistantError( + f"Could not fetch EZVIZ alarm status: {error}" + ) from error diff --git a/homeassistant/components/ezviz/const.py b/homeassistant/components/ezviz/const.py index d052a4b8216679..c28d84552d6fa2 100644 --- a/homeassistant/components/ezviz/const.py +++ b/homeassistant/components/ezviz/const.py @@ -6,8 +6,6 @@ # Configuration ATTR_SERIAL = "serial" CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" -ATTR_HOME = "HOME_MODE" -ATTR_AWAY = "AWAY_MODE" ATTR_TYPE_CLOUD = "EZVIZ_CLOUD_ACCOUNT" ATTR_TYPE_CAMERA = "CAMERA_ACCOUNT" CONF_SESSION_ID = "session_id" From 747f4d4a7328a2d0b4fd7083ac2d7cd45a21bdf7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 21 Jul 2023 12:16:35 +0200 Subject: [PATCH 0739/1009] Add event entity (#96797) --- .core_files.yaml | 1 + .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/components/demo/__init__.py | 1 + homeassistant/components/demo/button.py | 1 + homeassistant/components/demo/event.py | 47 +++ homeassistant/components/demo/strings.json | 11 + homeassistant/components/event/__init__.py | 209 +++++++++++ homeassistant/components/event/const.py | 5 + homeassistant/components/event/manifest.json | 8 + homeassistant/components/event/recorder.py | 12 + homeassistant/components/event/strings.json | 25 ++ homeassistant/const.py | 1 + mypy.ini | 10 + tests/components/event/__init__.py | 1 + tests/components/event/test_init.py | 352 ++++++++++++++++++ tests/components/event/test_recorder.py | 50 +++ .../custom_components/test/event.py | 42 +++ 18 files changed, 779 insertions(+) create mode 100644 homeassistant/components/demo/event.py create mode 100644 homeassistant/components/event/__init__.py create mode 100644 homeassistant/components/event/const.py create mode 100644 homeassistant/components/event/manifest.json create mode 100644 homeassistant/components/event/recorder.py create mode 100644 homeassistant/components/event/strings.json create mode 100644 tests/components/event/__init__.py create mode 100644 tests/components/event/test_init.py create mode 100644 tests/components/event/test_recorder.py create mode 100644 tests/testing_config/custom_components/test/event.py diff --git a/.core_files.yaml b/.core_files.yaml index b1870654be06f2..5e9b1d50defa27 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -24,6 +24,7 @@ base_platforms: &base_platforms - homeassistant/components/datetime/** - homeassistant/components/device_tracker/** - homeassistant/components/diagnostics/** + - homeassistant/components/event/** - homeassistant/components/fan/** - homeassistant/components/geo_location/** - homeassistant/components/humidifier/** diff --git a/.strict-typing b/.strict-typing index 67ebca7aea772f..9818e3d3197dfa 100644 --- a/.strict-typing +++ b/.strict-typing @@ -113,6 +113,7 @@ homeassistant.components.elkm1.* homeassistant.components.emulated_hue.* homeassistant.components.energy.* homeassistant.components.esphome.* +homeassistant.components.event.* homeassistant.components.evil_genius_labs.* homeassistant.components.fan.* homeassistant.components.fastdotcom.* diff --git a/CODEOWNERS b/CODEOWNERS index 5198f12519c5c5..918ad4c23439a8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -358,6 +358,8 @@ build.json @home-assistant/supervisor /tests/components/esphome/ @OttoWinter @jesserockz @bdraco /homeassistant/components/eufylife_ble/ @bdr99 /tests/components/eufylife_ble/ @bdr99 +/homeassistant/components/event/ @home-assistant/core +/tests/components/event/ @home-assistant/core /homeassistant/components/evil_genius_labs/ @balloob /tests/components/evil_genius_labs/ @balloob /homeassistant/components/evohome/ @zxdavb diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 246c952e219417..6d54255f8ed59c 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -30,6 +30,7 @@ Platform.COVER, Platform.DATE, Platform.DATETIME, + Platform.EVENT, Platform.FAN, Platform.HUMIDIFIER, Platform.LIGHT, diff --git a/homeassistant/components/demo/button.py b/homeassistant/components/demo/button.py index 8b4a94a40afd85..3c0498fefefd51 100644 --- a/homeassistant/components/demo/button.py +++ b/homeassistant/components/demo/button.py @@ -51,3 +51,4 @@ async def async_press(self) -> None: persistent_notification.async_create( self.hass, "Button pressed", title="Button" ) + self.hass.bus.async_fire("demo_button_pressed") diff --git a/homeassistant/components/demo/event.py b/homeassistant/components/demo/event.py new file mode 100644 index 00000000000000..e9d26d9f54de45 --- /dev/null +++ b/homeassistant/components/demo/event.py @@ -0,0 +1,47 @@ +"""Demo platform that offers a fake event entity.""" +from __future__ import annotations + +from homeassistant.components.event import EventDeviceClass, EventEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the demo event platform.""" + async_add_entities([DemoEvent()]) + + +class DemoEvent(EventEntity): + """Representation of a demo event entity.""" + + _attr_device_class = EventDeviceClass.BUTTON + _attr_event_types = ["pressed"] + _attr_has_entity_name = True + _attr_name = "Button press" + _attr_should_poll = False + _attr_translation_key = "push" + _attr_unique_id = "push" + + def __init__(self) -> None: + """Initialize the Demo event entity.""" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, "push")}, + ) + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self.hass.bus.async_listen("demo_button_pressed", self._async_handle_event) + + @callback + def _async_handle_event(self, _: Event) -> None: + """Handle the demo button event.""" + self._trigger_event("pressed") + self.async_write_ha_state() diff --git a/homeassistant/components/demo/strings.json b/homeassistant/components/demo/strings.json index d9b896080722e0..555760a5af9546 100644 --- a/homeassistant/components/demo/strings.json +++ b/homeassistant/components/demo/strings.json @@ -46,6 +46,17 @@ } } }, + "event": { + "push": { + "state_attributes": { + "event_type": { + "state": { + "pressed": "Pressed" + } + } + } + } + }, "select": { "speed": { "state": { diff --git a/homeassistant/components/event/__init__.py b/homeassistant/components/event/__init__.py new file mode 100644 index 00000000000000..6eeab6a32bbf09 --- /dev/null +++ b/homeassistant/components/event/__init__.py @@ -0,0 +1,209 @@ +"""Component for handling incoming events as a platform.""" +from __future__ import annotations + +from dataclasses import asdict, dataclass +from datetime import datetime, timedelta +import logging +from typing import Any, final + +from typing_extensions import Self + +from homeassistant.backports.enum import StrEnum +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.config_validation import ( # noqa: F401 + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity +from homeassistant.helpers.typing import ConfigType +from homeassistant.util import dt as dt_util + +from .const import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES, DOMAIN + +SCAN_INTERVAL = timedelta(seconds=30) + +ENTITY_ID_FORMAT = DOMAIN + ".{}" + +_LOGGER = logging.getLogger(__name__) + + +class EventDeviceClass(StrEnum): + """Device class for events.""" + + DOORBELL = "doorbell" + BUTTON = "button" + MOTION = "motion" + + +__all__ = [ + "ATTR_EVENT_TYPE", + "ATTR_EVENT_TYPES", + "DOMAIN", + "PLATFORM_SCHEMA_BASE", + "PLATFORM_SCHEMA", + "EventDeviceClass", + "EventEntity", + "EventEntityDescription", + "EventEntityFeature", +] + +# mypy: disallow-any-generics + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Event entities.""" + component = hass.data[DOMAIN] = EntityComponent[EventEntity]( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) + await component.async_setup(config) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + component: EntityComponent[EventEntity] = hass.data[DOMAIN] + return await component.async_setup_entry(entry) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + component: EntityComponent[EventEntity] = hass.data[DOMAIN] + return await component.async_unload_entry(entry) + + +@dataclass +class EventEntityDescription(EntityDescription): + """A class that describes event entities.""" + + device_class: EventDeviceClass | None = None + event_types: list[str] | None = None + + +@dataclass +class EventExtraStoredData(ExtraStoredData): + """Object to hold extra stored data.""" + + last_event_type: str | None + last_event_attributes: dict[str, Any] | None + + def as_dict(self) -> dict[str, Any]: + """Return a dict representation of the event data.""" + return asdict(self) + + @classmethod + def from_dict(cls, restored: dict[str, Any]) -> Self | None: + """Initialize a stored event state from a dict.""" + try: + return cls( + restored["last_event_type"], + restored["last_event_attributes"], + ) + except KeyError: + return None + + +class EventEntity(RestoreEntity): + """Representation of a Event entity.""" + + entity_description: EventEntityDescription + _attr_device_class: EventDeviceClass | None + _attr_event_types: list[str] + _attr_state: None + + __last_event_triggered: datetime | None = None + __last_event_type: str | None = None + __last_event_attributes: dict[str, Any] | None = None + + @property + def device_class(self) -> EventDeviceClass | None: + """Return the class of this entity.""" + if hasattr(self, "_attr_device_class"): + return self._attr_device_class + if hasattr(self, "entity_description"): + return self.entity_description.device_class + return None + + @property + def event_types(self) -> list[str]: + """Return a list of possible events.""" + if hasattr(self, "_attr_event_types"): + return self._attr_event_types + if ( + hasattr(self, "entity_description") + and self.entity_description.event_types is not None + ): + return self.entity_description.event_types + raise AttributeError() + + @final + def _trigger_event( + self, event_type: str, event_attributes: dict[str, Any] | None = None + ) -> None: + """Process a new event.""" + if event_type not in self.event_types: + raise ValueError(f"Invalid event type {event_type} for {self.entity_id}") + self.__last_event_triggered = dt_util.utcnow() + self.__last_event_type = event_type + self.__last_event_attributes = event_attributes + + def _default_to_device_class_name(self) -> bool: + """Return True if an unnamed entity should be named by its device class. + + For events this is True if the entity has a device class. + """ + return self.device_class is not None + + @property + @final + def capability_attributes(self) -> dict[str, list[str]]: + """Return capability attributes.""" + return { + ATTR_EVENT_TYPES: self.event_types, + } + + @property + @final + def state(self) -> str | None: + """Return the entity state.""" + if (last_event := self.__last_event_triggered) is None: + return None + return last_event.isoformat(timespec="milliseconds") + + @final + @property + def state_attributes(self) -> dict[str, Any]: + """Return the state attributes.""" + attributes = {ATTR_EVENT_TYPE: self.__last_event_type} + if self.__last_event_attributes: + attributes |= self.__last_event_attributes + return attributes + + @final + async def async_internal_added_to_hass(self) -> None: + """Call when the event entity is added to hass.""" + await super().async_internal_added_to_hass() + if ( + (state := await self.async_get_last_state()) + and state.state is not None + and (event_data := await self.async_get_last_event_data()) + ): + self.__last_event_triggered = dt_util.parse_datetime(state.state) + self.__last_event_type = event_data.last_event_type + self.__last_event_attributes = event_data.last_event_attributes + + @property + def extra_restore_state_data(self) -> EventExtraStoredData: + """Return event specific state data to be restored.""" + return EventExtraStoredData( + self.__last_event_type, + self.__last_event_attributes, + ) + + async def async_get_last_event_data(self) -> EventExtraStoredData | None: + """Restore event specific state date.""" + if (restored_last_extra_data := await self.async_get_last_extra_data()) is None: + return None + return EventExtraStoredData.from_dict(restored_last_extra_data.as_dict()) diff --git a/homeassistant/components/event/const.py b/homeassistant/components/event/const.py new file mode 100644 index 00000000000000..cd6a8b96f7a3bd --- /dev/null +++ b/homeassistant/components/event/const.py @@ -0,0 +1,5 @@ +"""Provides the constants needed for the component.""" + +DOMAIN = "event" +ATTR_EVENT_TYPE = "event_type" +ATTR_EVENT_TYPES = "event_types" diff --git a/homeassistant/components/event/manifest.json b/homeassistant/components/event/manifest.json new file mode 100644 index 00000000000000..2da0940012a660 --- /dev/null +++ b/homeassistant/components/event/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "event", + "name": "Event", + "codeowners": ["@home-assistant/core"], + "documentation": "https://www.home-assistant.io/integrations/event", + "integration_type": "entity", + "quality_scale": "internal" +} diff --git a/homeassistant/components/event/recorder.py b/homeassistant/components/event/recorder.py new file mode 100644 index 00000000000000..759fd80bcf0e0d --- /dev/null +++ b/homeassistant/components/event/recorder.py @@ -0,0 +1,12 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + +from . import ATTR_EVENT_TYPES + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude static attributes from being recorded in the database.""" + return {ATTR_EVENT_TYPES} diff --git a/homeassistant/components/event/strings.json b/homeassistant/components/event/strings.json new file mode 100644 index 00000000000000..02f4da8ca0820d --- /dev/null +++ b/homeassistant/components/event/strings.json @@ -0,0 +1,25 @@ +{ + "title": "Event", + "entity_component": { + "_": { + "name": "[%key:component::button::title%]", + "state_attributes": { + "event_type": { + "name": "Event type" + }, + "event_types": { + "name": "Event types" + } + } + }, + "doorbell": { + "name": "Doorbell" + }, + "button": { + "name": "Button" + }, + "motion": { + "name": "Motion" + } + } +} diff --git a/homeassistant/const.py b/homeassistant/const.py index 5394e273a4c34c..94fa194fa09e37 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -34,6 +34,7 @@ class Platform(StrEnum): DATE = "date" DATETIME = "datetime" DEVICE_TRACKER = "device_tracker" + EVENT = "event" FAN = "fan" GEO_LOCATION = "geo_location" HUMIDIFIER = "humidifier" diff --git a/mypy.ini b/mypy.ini index ab8b5a5df8989d..4c2d803a549fbe 100644 --- a/mypy.ini +++ b/mypy.ini @@ -892,6 +892,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.event.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.evil_genius_labs.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/event/__init__.py b/tests/components/event/__init__.py new file mode 100644 index 00000000000000..e8236163d05227 --- /dev/null +++ b/tests/components/event/__init__.py @@ -0,0 +1 @@ +"""The tests for the event integration.""" diff --git a/tests/components/event/test_init.py b/tests/components/event/test_init.py new file mode 100644 index 00000000000000..66cda6a088a3bb --- /dev/null +++ b/tests/components/event/test_init.py @@ -0,0 +1,352 @@ +"""The tests for the event integration.""" +from collections.abc import Generator +from typing import Any + +from freezegun import freeze_time +import pytest + +from homeassistant.components.event import ( + ATTR_EVENT_TYPE, + ATTR_EVENT_TYPES, + DOMAIN, + EventDeviceClass, + EventEntity, + EventEntityDescription, +) +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import CONF_PLATFORM, STATE_UNKNOWN +from homeassistant.core import HomeAssistant, State +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import ( + MockConfigEntry, + MockModule, + MockPlatform, + async_mock_restore_state_shutdown_restart, + mock_config_flow, + mock_integration, + mock_platform, + mock_restore_cache, + mock_restore_cache_with_extra_data, +) + +TEST_DOMAIN = "test" + + +async def test_event() -> None: + """Test the event entity.""" + event = EventEntity() + event.entity_id = "event.doorbell" + # Test event with no data at all + assert event.state is None + assert event.state_attributes == {ATTR_EVENT_TYPE: None} + assert not event.extra_state_attributes + assert event.device_class is None + + # No event types defined, should raise + with pytest.raises(AttributeError): + event.event_types + + # Test retrieving data from entity description + event.entity_description = EventEntityDescription( + key="test_event", + event_types=["short_press", "long_press"], + device_class=EventDeviceClass.DOORBELL, + ) + assert event.event_types == ["short_press", "long_press"] + assert event.device_class == EventDeviceClass.DOORBELL + + # Test attrs win over entity description + event._attr_event_types = ["short_press", "long_press", "double_press"] + assert event.event_types == ["short_press", "long_press", "double_press"] + event._attr_device_class = EventDeviceClass.BUTTON + assert event.device_class == EventDeviceClass.BUTTON + + # Test triggering an event + now = dt_util.utcnow() + with freeze_time(now): + event._trigger_event("long_press") + + assert event.state == now.isoformat(timespec="milliseconds") + assert event.state_attributes == {ATTR_EVENT_TYPE: "long_press"} + assert not event.extra_state_attributes + + # Test triggering an event, with extra attribute data + now = dt_util.utcnow() + with freeze_time(now): + event._trigger_event("short_press", {"hello": "world"}) + + assert event.state == now.isoformat(timespec="milliseconds") + assert event.state_attributes == { + ATTR_EVENT_TYPE: "short_press", + "hello": "world", + } + + # Test triggering an unknown event + with pytest.raises( + ValueError, match="^Invalid event type unknown_event for event.doorbell$" + ): + event._trigger_event("unknown_event") + + +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_restore_state(hass: HomeAssistant) -> None: + """Test we restore state integration.""" + mock_restore_cache_with_extra_data( + hass, + ( + ( + State( + "event.doorbell", + "2021-01-01T23:59:59.123+00:00", + attributes={ + ATTR_EVENT_TYPE: "ignored", + ATTR_EVENT_TYPES: [ + "single_press", + "double_press", + "do", + "not", + "restore", + ], + "hello": "worm", + }, + ), + { + "last_event_type": "double_press", + "last_event_attributes": { + "hello": "world", + }, + }, + ), + ), + ) + + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + state = hass.states.get("event.doorbell") + assert state + assert state.state == "2021-01-01T23:59:59.123+00:00" + assert state.attributes[ATTR_EVENT_TYPES] == ["short_press", "long_press"] + assert state.attributes[ATTR_EVENT_TYPE] == "double_press" + assert state.attributes["hello"] == "world" + + +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_invalid_extra_restore_state(hass: HomeAssistant) -> None: + """Test we restore state integration.""" + mock_restore_cache_with_extra_data( + hass, + ( + ( + State( + "event.doorbell", + "2021-01-01T23:59:59.123+00:00", + ), + { + "invalid_unexpected_key": "double_press", + "last_event_attributes": { + "hello": "world", + }, + }, + ), + ), + ) + + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + state = hass.states.get("event.doorbell") + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_EVENT_TYPES] == ["short_press", "long_press"] + assert state.attributes[ATTR_EVENT_TYPE] is None + assert "hello" not in state.attributes + + +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_no_extra_restore_state(hass: HomeAssistant) -> None: + """Test we restore state integration.""" + mock_restore_cache( + hass, + ( + State( + "event.doorbell", + "2021-01-01T23:59:59.123+00:00", + attributes={ + ATTR_EVENT_TYPES: [ + "single_press", + "double_press", + ], + ATTR_EVENT_TYPE: "double_press", + "hello": "world", + }, + ), + ), + ) + + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + state = hass.states.get("event.doorbell") + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_EVENT_TYPES] == ["short_press", "long_press"] + assert state.attributes[ATTR_EVENT_TYPE] is None + assert "hello" not in state.attributes + + +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_saving_state(hass: HomeAssistant, hass_storage: dict[str, Any]) -> None: + """Test we restore state integration.""" + restore_data = {"last_event_type": "double_press", "last_event_attributes": None} + + mock_restore_cache_with_extra_data( + hass, + ( + ( + State( + "event.doorbell", + "2021-01-01T23:59:59.123+00:00", + ), + restore_data, + ), + ), + ) + + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + await async_mock_restore_state_shutdown_restart(hass) + + assert len(hass_storage[RESTORE_STATE_KEY]["data"]) == 1 + state = hass_storage[RESTORE_STATE_KEY]["data"][0]["state"] + assert state["entity_id"] == "event.doorbell" + extra_data = hass_storage[RESTORE_STATE_KEY]["data"][0]["extra_data"] + assert extra_data == restore_data + + +class MockFlow(ConfigFlow): + """Test flow.""" + + +@pytest.fixture +def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: + """Mock config flow.""" + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + + with mock_config_flow(TEST_DOMAIN, MockFlow): + yield + + +@pytest.mark.usefixtures("config_flow_fixture") +async def test_name(hass: HomeAssistant) -> None: + """Test event name.""" + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + return True + + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + mock_integration( + hass, + MockModule( + TEST_DOMAIN, + async_setup_entry=async_setup_entry_init, + ), + ) + + # Unnamed event without device class -> no name + entity1 = EventEntity() + entity1._attr_event_types = ["ding"] + entity1.entity_id = "event.test1" + + # Unnamed event with device class but has_entity_name False -> no name + entity2 = EventEntity() + entity2._attr_event_types = ["ding"] + entity2.entity_id = "event.test2" + entity2._attr_device_class = EventDeviceClass.DOORBELL + + # Unnamed event with device class and has_entity_name True -> named + entity3 = EventEntity() + entity3._attr_event_types = ["ding"] + entity3.entity_id = "event.test3" + entity3._attr_device_class = EventDeviceClass.DOORBELL + entity3._attr_has_entity_name = True + + # Unnamed event with device class and has_entity_name True -> named + entity4 = EventEntity() + entity4._attr_event_types = ["ding"] + entity4.entity_id = "event.test4" + entity4.entity_description = EventEntityDescription( + "test", + EventDeviceClass.DOORBELL, + has_entity_name=True, + ) + + async def async_setup_entry_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test event platform via config entry.""" + async_add_entities([entity1, entity2, entity3, entity4]) + + mock_platform( + hass, + f"{TEST_DOMAIN}.{DOMAIN}", + MockPlatform(async_setup_entry=async_setup_entry_platform), + ) + + config_entry = MockConfigEntry(domain=TEST_DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity1.entity_id) + assert state + assert state.attributes == {"event_types": ["ding"], "event_type": None} + + state = hass.states.get(entity2.entity_id) + assert state + assert state.attributes == { + "event_types": ["ding"], + "event_type": None, + "device_class": "doorbell", + } + + state = hass.states.get(entity3.entity_id) + assert state + assert state.attributes == { + "event_types": ["ding"], + "event_type": None, + "device_class": "doorbell", + "friendly_name": "Doorbell", + } + + state = hass.states.get(entity4.entity_id) + assert state + assert state.attributes == { + "event_types": ["ding"], + "event_type": None, + "device_class": "doorbell", + "friendly_name": "Doorbell", + } diff --git a/tests/components/event/test_recorder.py b/tests/components/event/test_recorder.py new file mode 100644 index 00000000000000..133f7e173e3636 --- /dev/null +++ b/tests/components/event/test_recorder.py @@ -0,0 +1,50 @@ +"""The tests for event recorder.""" +from __future__ import annotations + +from unittest.mock import patch + +import pytest + +from homeassistant.components import select +from homeassistant.components.event import ATTR_EVENT_TYPES +from homeassistant.components.recorder import Recorder +from homeassistant.components.recorder.history import get_significant_states +from homeassistant.const import ATTR_FRIENDLY_NAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.components.recorder.common import async_wait_recording_done + + +@pytest.fixture(autouse=True) +async def event_only() -> None: + """Enable only the event platform.""" + with patch( + "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", + [Platform.EVENT], + ): + yield + + +async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None: + """Test select registered attributes to be excluded.""" + now = dt_util.utcnow() + assert await async_setup_component(hass, "homeassistant", {}) + await async_setup_component( + hass, select.DOMAIN, {select.DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() + hass.bus.async_fire("demo_button_pressed") + await hass.async_block_till_done() + await async_wait_recording_done(hass) + + states = await hass.async_add_executor_job( + get_significant_states, hass, now, None, hass.states.async_entity_ids() + ) + assert len(states) >= 1 + for entity_states in states.values(): + for state in entity_states: + assert state + assert ATTR_EVENT_TYPES not in state.attributes + assert ATTR_FRIENDLY_NAME in state.attributes diff --git a/tests/testing_config/custom_components/test/event.py b/tests/testing_config/custom_components/test/event.py new file mode 100644 index 00000000000000..9acb24f37cffae --- /dev/null +++ b/tests/testing_config/custom_components/test/event.py @@ -0,0 +1,42 @@ +"""Provide a mock event platform. + +Call init before using it in your tests to ensure clean test data. +""" +from homeassistant.components.event import EventEntity + +from tests.common import MockEntity + +ENTITIES = [] + + +class MockEventEntity(MockEntity, EventEntity): + """Mock EventEntity class.""" + + @property + def event_types(self) -> list[str]: + """Return a list of possible events.""" + return self._handle("event_types") + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + [] + if empty + else [ + MockEventEntity( + name="doorbell", + unique_id="unique_doorbell", + event_types=["short_press", "long_press"], + ), + ] + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(ENTITIES) From 447fbf58c98b860bde283d2918dcad3917e08704 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 21 Jul 2023 12:52:10 +0200 Subject: [PATCH 0740/1009] Change naming of MQTT entities to correspond with HA guidelines (#95159) * Set has_entity_name if device_name is set * revert unneeded formatting change * Add image platform * Follow up comment * Don't set `has_entity_name` without device name * Only set has_entity_name if a valid name is set * Follow device_class name and add tests * Follow up comments add extra tests * Move to helper - Log a warning * fix test * Allow to assign None as name explictly * Refactor * Log info messages when device name is not set * Revert scene schema change - no device link * Always set has_entity_name with device mapping * Always set `_attr_has_entity_name` * Cleanup --- .../components/mqtt/alarm_control_panel.py | 3 +- .../components/mqtt/binary_sensor.py | 3 +- homeassistant/components/mqtt/button.py | 3 +- homeassistant/components/mqtt/camera.py | 3 +- homeassistant/components/mqtt/climate.py | 3 +- homeassistant/components/mqtt/cover.py | 3 +- .../components/mqtt/device_tracker.py | 3 +- homeassistant/components/mqtt/fan.py | 3 +- homeassistant/components/mqtt/humidifier.py | 3 +- homeassistant/components/mqtt/image.py | 3 +- .../components/mqtt/light/schema_basic.py | 3 +- .../components/mqtt/light/schema_json.py | 3 +- .../components/mqtt/light/schema_template.py | 3 +- homeassistant/components/mqtt/lock.py | 3 +- homeassistant/components/mqtt/mixins.py | 33 +++- homeassistant/components/mqtt/number.py | 3 +- homeassistant/components/mqtt/scene.py | 3 +- homeassistant/components/mqtt/select.py | 3 +- homeassistant/components/mqtt/sensor.py | 3 +- homeassistant/components/mqtt/siren.py | 3 +- homeassistant/components/mqtt/switch.py | 3 +- homeassistant/components/mqtt/text.py | 3 +- homeassistant/components/mqtt/update.py | 3 +- .../components/mqtt/vacuum/schema_legacy.py | 3 +- .../components/mqtt/vacuum/schema_state.py | 3 +- homeassistant/components/mqtt/water_heater.py | 3 +- .../mqtt/test_alarm_control_panel.py | 19 ++ tests/components/mqtt/test_binary_sensor.py | 19 ++ tests/components/mqtt/test_button.py | 24 +++ tests/components/mqtt/test_common.py | 49 ++++- tests/components/mqtt/test_diagnostics.py | 6 +- tests/components/mqtt/test_discovery.py | 52 +++-- tests/components/mqtt/test_init.py | 4 +- tests/components/mqtt/test_mixins.py | 180 ++++++++++++++++++ tests/components/mqtt/test_number.py | 19 ++ tests/components/mqtt/test_sensor.py | 19 ++ 36 files changed, 433 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index dbed1c8aa9e8bd..06f91403057b02 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -89,7 +89,7 @@ CONF_COMMAND_TEMPLATE, default=DEFAULT_COMMAND_TEMPLATE ): cv.template, vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, vol.Optional(CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_ARM_NIGHT): cv.string, @@ -136,6 +136,7 @@ async def _async_setup_entity( class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): """Representation of a MQTT alarm status.""" + _default_name = DEFAULT_NAME _entity_id_format = alarm.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_ALARM_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 50af9ef8a555e5..0d4b2c4a7b4572 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -61,7 +61,7 @@ vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_OFF_DELAY): cv.positive_int, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, @@ -97,6 +97,7 @@ async def _async_setup_entity( class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): """Representation a binary sensor that is updated by MQTT.""" + _default_name = DEFAULT_NAME _entity_id_format = binary_sensor.ENTITY_ID_FORMAT _expired: bool | None _expire_after: int | None diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 46ecc16d385513..9b3b04a54f5c62 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -35,7 +35,7 @@ vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_PRESS, default=DEFAULT_PAYLOAD_PRESS): cv.string, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } @@ -70,6 +70,7 @@ async def _async_setup_entity( class MqttButton(MqttEntity, ButtonEntity): """Representation of a switch that can be toggled using MQTT.""" + _default_name = DEFAULT_NAME _entity_id_format = button.ENTITY_ID_FORMAT def __init__( diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 75ab25efcfa28f..166bfdd38ccdc8 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -41,7 +41,7 @@ PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Required(CONF_TOPIC): valid_subscribe_topic, vol.Optional(CONF_IMAGE_ENCODING): "b64", } @@ -80,6 +80,7 @@ async def _async_setup_entity( class MqttCamera(MqttEntity, Camera): """representation of a MQTT camera.""" + _default_name = DEFAULT_NAME _entity_id_format: str = camera.ENTITY_ID_FORMAT _attributes_extra_blocked: frozenset[str] = MQTT_CAMERA_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 676e5b50f49440..f29a114620ace8 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -296,7 +296,7 @@ def valid_humidity_state_configuration(config: ConfigType) -> ConfigType: ): cv.ensure_list, vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_MODE_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, @@ -597,6 +597,7 @@ async def async_set_temperature(self, **kwargs: Any) -> None: class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): """Representation of an MQTT climate device.""" + _default_name = DEFAULT_NAME _entity_id_format = climate.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_CLIMATE_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 0b435db0b7a97e..c11cf2dfb85d6e 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -159,7 +159,7 @@ def validate_options(config: ConfigType) -> ConfigType: vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), vol.Optional(CONF_GET_POSITION_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): vol.Any( cv.string, None @@ -236,6 +236,7 @@ async def _async_setup_entity( class MqttCover(MqttEntity, CoverEntity): """Representation of a cover that can be controlled using MQTT.""" + _default_name = DEFAULT_NAME _entity_id_format: str = cover.ENTITY_ID_FORMAT _attributes_extra_blocked: frozenset[str] = MQTT_COVER_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py index a9c4017593c9a9..dd4eca9878a4eb 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -61,7 +61,7 @@ def valid_config(config: ConfigType) -> ConfigType: { vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string, vol.Optional(CONF_PAYLOAD_RESET, default=DEFAULT_PAYLOAD_RESET): cv.string, @@ -104,6 +104,7 @@ async def _async_setup_entity( class MqttDeviceTracker(MqttEntity, TrackerEntity): """Representation of a device tracker using MQTT.""" + _default_name = None _entity_id_format = device_tracker.ENTITY_ID_FORMAT _value_template: Callable[..., ReceivePayloadType] diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index f5e92d8ecf958a..58189c3cb3e8c3 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -127,7 +127,7 @@ def valid_preset_mode_configuration(config: ConfigType) -> ConfigType: _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_DIRECTION_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_DIRECTION_COMMAND_TEMPLATE): cv.template, @@ -215,6 +215,7 @@ async def _async_setup_entity( class MqttFan(MqttEntity, FanEntity): """A MQTT fan component.""" + _default_name = DEFAULT_NAME _entity_id_format = fan.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_FAN_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 392a112bcdb79a..aebb05c19f7cd7 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -136,7 +136,7 @@ def valid_humidity_range_configuration(config: ConfigType) -> ConfigType: vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, @@ -207,6 +207,7 @@ async def _async_setup_entity( class MqttHumidifier(MqttEntity, HumidifierEntity): """A MQTT humidifier component.""" + _default_name = DEFAULT_NAME _entity_id_format = humidifier.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/image.py b/homeassistant/components/mqtt/image.py index 2764539770daf8..a21d45369f8fd5 100644 --- a/homeassistant/components/mqtt/image.py +++ b/homeassistant/components/mqtt/image.py @@ -61,7 +61,7 @@ def validate_topic_required(config: ConfigType) -> ConfigType: PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_CONTENT_TYPE): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Exclusive(CONF_URL_TOPIC, "image_topic"): valid_subscribe_topic, vol.Exclusive(CONF_IMAGE_TOPIC, "image_topic"): valid_subscribe_topic, vol.Optional(CONF_IMAGE_ENCODING): "b64", @@ -102,6 +102,7 @@ async def _async_setup_entity( class MqttImage(MqttEntity, ImageEntity): """representation of a MQTT image.""" + _default_name = DEFAULT_NAME _entity_id_format: str = image.ENTITY_ID_FORMAT _last_image: bytes | None = None _client: httpx.AsyncClient diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index fe09667ca4aaa1..2a726075bb0da2 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -190,7 +190,7 @@ vol.Optional(CONF_HS_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_MAX_MIREDS): cv.positive_int, vol.Optional(CONF_MIN_MIREDS): cv.positive_int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_ON_COMMAND_TYPE, default=DEFAULT_ON_COMMAND_TYPE): vol.In( VALUES_ON_COMMAND_TYPE ), @@ -242,6 +242,7 @@ async def async_setup_entity_basic( class MqttLight(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT light.""" + _default_name = DEFAULT_NAME _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED _topic: dict[str, str | None] diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 70992887ca7f2e..8f710eb5ea66bc 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -132,7 +132,7 @@ def valid_color_configuration(config: ConfigType) -> ConfigType: vol.Optional(CONF_HS, default=DEFAULT_HS): cv.boolean, vol.Optional(CONF_MAX_MIREDS): cv.positive_int, vol.Optional(CONF_MIN_MIREDS): cv.positive_int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All( vol.Coerce(int), vol.In([0, 1, 2]) ), @@ -180,6 +180,7 @@ async def async_setup_entity_json( class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT JSON light.""" + _default_name = DEFAULT_NAME _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 063895d738c99a..98ee7648eebd85 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -100,7 +100,7 @@ vol.Optional(CONF_GREEN_TEMPLATE): cv.template, vol.Optional(CONF_MAX_MIREDS): cv.positive_int, vol.Optional(CONF_MIN_MIREDS): cv.positive_int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_RED_TEMPLATE): cv.template, vol.Optional(CONF_STATE_TEMPLATE): cv.template, } @@ -128,6 +128,7 @@ async def async_setup_entity_template( class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT Template light.""" + _default_name = DEFAULT_NAME _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED _optimistic: bool diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 966cbc211055e7..cb586c0630929f 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -76,7 +76,7 @@ { vol.Optional(CONF_CODE_FORMAT): cv.is_regex, vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string, vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string, vol.Optional(CONF_PAYLOAD_OPEN): cv.string, @@ -126,6 +126,7 @@ async def _async_setup_entity( class MqttLock(MqttEntity, LockEntity): """Representation of a lock that can be toggled using MQTT.""" + _default_name = DEFAULT_NAME _entity_id_format = lock.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LOCK_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 57ec933cd589c4..ec437f08d39bca 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -50,7 +50,12 @@ async_track_device_registry_updated_event, async_track_entity_registry_updated_event, ) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ( + UNDEFINED, + ConfigType, + DiscoveryInfoType, + UndefinedType, +) from homeassistant.util.json import json_loads from . import debug_info, subscription @@ -999,7 +1004,9 @@ class MqttEntity( ): """Representation of an MQTT entity.""" + _attr_has_entity_name = True _attr_should_poll = False + _default_name: str | None _entity_id_format: str def __init__( @@ -1016,8 +1023,8 @@ def __init__( self._sub_state: dict[str, EntitySubscription] = {} # Load config - self._setup_common_attributes_from_config(self._config) self._setup_from_config(self._config) + self._setup_common_attributes_from_config(self._config) # Initialize entity_id from config self._init_entity_id() @@ -1058,8 +1065,8 @@ async def discovery_update(self, discovery_payload: MQTTDiscoveryPayload) -> Non async_handle_schema_error(discovery_payload, err) return self._config = config - self._setup_common_attributes_from_config(self._config) self._setup_from_config(self._config) + self._setup_common_attributes_from_config(self._config) # Prepare MQTT subscriptions self.attributes_prepare_discovery_update(config) @@ -1107,6 +1114,23 @@ async def async_publish( def config_schema() -> vol.Schema: """Return the config schema.""" + def _set_entity_name(self, config: ConfigType) -> None: + """Help setting the entity name if needed.""" + entity_name: str | None | UndefinedType = config.get(CONF_NAME, UNDEFINED) + # Only set _attr_name if it is needed + if entity_name is not UNDEFINED: + self._attr_name = entity_name + elif not self._default_to_device_class_name(): + # Assign the default name + self._attr_name = self._default_name + if CONF_DEVICE in config: + if CONF_NAME not in config[CONF_DEVICE]: + _LOGGER.info( + "MQTT device information always needs to include a name, got %s, " + "if device information is shared between multiple entities, the device " + "name must be included in each entity's device configuration", + ) + def _setup_common_attributes_from_config(self, config: ConfigType) -> None: """(Re)Setup the common attributes for the entity.""" self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) @@ -1114,7 +1138,8 @@ def _setup_common_attributes_from_config(self, config: ConfigType) -> None: config.get(CONF_ENABLED_BY_DEFAULT) ) self._attr_icon = config.get(CONF_ICON) - self._attr_name = config.get(CONF_NAME) + # Set the entity name if needed + self._set_entity_name(config) def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 5986eab1207e6b..971b44b43bfccd 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -87,7 +87,7 @@ def validate_config(config: ConfigType) -> ConfigType: vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): vol.Coerce(float), vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): vol.Coerce(float), vol.Optional(CONF_MODE, default=NumberMode.AUTO): vol.Coerce(NumberMode), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_RESET, default=DEFAULT_PAYLOAD_RESET): cv.string, vol.Optional(CONF_STEP, default=DEFAULT_STEP): vol.All( vol.Coerce(float), vol.Range(min=1e-3) @@ -134,6 +134,7 @@ async def _async_setup_entity( class MqttNumber(MqttEntity, RestoreNumber): """representation of an MQTT number.""" + _default_name = DEFAULT_NAME _entity_id_format = number.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_NUMBER_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index f716e4fe46f8dd..5e12f67a698f34 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -34,7 +34,7 @@ { vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PAYLOAD_ON): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, @@ -77,6 +77,7 @@ class MqttScene( ): """Representation of a scene that can be activated using MQTT.""" + _default_name = DEFAULT_NAME _entity_id_format = scene.DOMAIN + ".{}" def __init__( diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 26e72af9192390..df8cf024bd26c3 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -54,7 +54,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Required(CONF_OPTIONS): cv.ensure_list, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }, @@ -89,6 +89,7 @@ async def _async_setup_entity( class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): """representation of an MQTT select.""" + _default_name = DEFAULT_NAME _entity_id_format = select.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_SELECT_ATTRIBUTES_BLOCKED _command_template: Callable[[PublishPayloadType], PublishPayloadType] diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index e4b5f61bda0c23..ae94b0df0ce68c 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -78,7 +78,7 @@ vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, vol.Optional(CONF_LAST_RESET_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_SUGGESTED_DISPLAY_PRECISION): cv.positive_int, vol.Optional(CONF_STATE_CLASS): vol.Any(STATE_CLASSES_SCHEMA, None), vol.Optional(CONF_UNIT_OF_MEASUREMENT): vol.Any(cv.string, None), @@ -126,6 +126,7 @@ async def _async_setup_entity( class MqttSensor(MqttEntity, RestoreSensor): """Representation of a sensor that can be updated using MQTT.""" + _default_name = DEFAULT_NAME _entity_id_format = ENTITY_ID_FORMAT _attr_last_reset: datetime | None = None _attributes_extra_blocked = MQTT_SENSOR_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index d30080f4647d17..328812a6e49ebe 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -79,7 +79,7 @@ vol.Optional(CONF_AVAILABLE_TONES): cv.ensure_list, vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_COMMAND_OFF_TEMPLATE): cv.template, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_STATE_OFF): cv.string, @@ -138,6 +138,7 @@ async def _async_setup_entity( class MqttSiren(MqttEntity, SirenEntity): """Representation of a siren that can be controlled using MQTT.""" + _default_name = DEFAULT_NAME _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_SIREN_ATTRIBUTES_BLOCKED _extra_attributes: dict[str, Any] diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 7f4f609f265efe..107b0b1cb10d99 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -49,7 +49,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_STATE_OFF): cv.string, @@ -88,6 +88,7 @@ async def _async_setup_entity( class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): """Representation of a switch that can be toggled using MQTT.""" + _default_name = DEFAULT_NAME _entity_id_format = switch.ENTITY_ID_FORMAT _optimistic: bool diff --git a/homeassistant/components/mqtt/text.py b/homeassistant/components/mqtt/text.py index 01622c10a6de6d..13677b7f35b223 100644 --- a/homeassistant/components/mqtt/text.py +++ b/homeassistant/components/mqtt/text.py @@ -78,7 +78,7 @@ def valid_text_size_configuration(config: ConfigType) -> ConfigType: _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_MAX, default=MAX_LENGTH_STATE_STATE): cv.positive_int, vol.Optional(CONF_MIN, default=0): cv.positive_int, vol.Optional(CONF_MODE, default=text.TextMode.TEXT): vol.In( @@ -125,6 +125,7 @@ class MqttTextEntity(MqttEntity, TextEntity): """Representation of the MQTT text entity.""" _attributes_extra_blocked = MQTT_TEXT_ATTRIBUTES_BLOCKED + _default_name = DEFAULT_NAME _entity_id_format = text.ENTITY_ID_FORMAT _compiled_pattern: re.Pattern[Any] | None diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index 930f4d225063c1..f6db0d3fd64c40 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -57,7 +57,7 @@ vol.Optional(CONF_ENTITY_PICTURE): cv.string, vol.Optional(CONF_LATEST_VERSION_TEMPLATE): cv.template, vol.Optional(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_INSTALL): cv.string, vol.Optional(CONF_RELEASE_SUMMARY): cv.string, vol.Optional(CONF_RELEASE_URL): cv.string, @@ -107,6 +107,7 @@ async def _async_setup_entity( class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity): """Representation of the MQTT update entity.""" + _default_name = DEFAULT_NAME _entity_id_format = update.ENTITY_ID_FORMAT def __init__( diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 7c73e579112488..516a7772c11172 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -131,7 +131,7 @@ ), vol.Inclusive(CONF_FAN_SPEED_TEMPLATE, "fan_speed"): cv.template, vol.Inclusive(CONF_FAN_SPEED_TOPIC, "fan_speed"): valid_publish_topic, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional( CONF_PAYLOAD_CLEAN_SPOT, default=DEFAULT_PAYLOAD_CLEAN_SPOT ): cv.string, @@ -215,6 +215,7 @@ async def async_setup_entity_legacy( class MqttVacuum(MqttEntity, VacuumEntity): """Representation of a MQTT-controlled legacy vacuum.""" + _default_name = DEFAULT_NAME _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index ee06131af0251d..5113e19f097e10 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -126,7 +126,7 @@ vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( cv.ensure_list, [cv.string] ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional( CONF_PAYLOAD_CLEAN_SPOT, default=DEFAULT_PAYLOAD_CLEAN_SPOT ): cv.string, @@ -170,6 +170,7 @@ async def async_setup_entity_state( class MqttStateVacuum(MqttEntity, StateVacuumEntity): """Representation of a MQTT-controlled state vacuum.""" + _default_name = DEFAULT_NAME _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_VACUUM_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/water_heater.py b/homeassistant/components/mqtt/water_heater.py index 0f622d55b84f7c..17e9430dba34ff 100644 --- a/homeassistant/components/mqtt/water_heater.py +++ b/homeassistant/components/mqtt/water_heater.py @@ -123,7 +123,7 @@ ): cv.ensure_list, vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_MODE_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, @@ -180,6 +180,7 @@ async def _async_setup_entity( class MqttWaterHeater(MqttTemperatureControlEntity, WaterHeaterEntity): """Representation of an MQTT water heater device.""" + _default_name = DEFAULT_NAME _entity_id_format = water_heater.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_WATER_HEATER_ATTRIBUTES_BLOCKED diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index d1b1d6b68b38a0..e69839e6b16314 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -55,6 +55,7 @@ help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_entity_name, help_test_publishing_with_custom_encoding, help_test_reloadable, help_test_setting_attribute_via_mqtt_json_message, @@ -1120,3 +1121,21 @@ async def test_unload_entry( await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry, domain, config ) + + +@pytest.mark.parametrize( + ("expected_friendly_name", "device_class"), + [("test", None)], +) +async def test_entity_name( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + expected_friendly_name: str | None, + device_class: str | None, +) -> None: + """Test the entity name setup.""" + domain = alarm_control_panel.DOMAIN + config = DEFAULT_CONFIG + await help_test_entity_name( + hass, mqtt_mock_entry, domain, config, expected_friendly_name, device_class + ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index d32754625f4238..28bf5f558cbcfa 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -41,6 +41,7 @@ help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_entity_name, help_test_reload_with_config, help_test_reloadable, help_test_setting_attribute_via_mqtt_json_message, @@ -1227,3 +1228,21 @@ async def test_unload_entry( await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry, domain, config ) + + +@pytest.mark.parametrize( + ("expected_friendly_name", "device_class"), + [("test", None), ("Door", "door"), ("Battery", "battery"), ("Motion", "motion")], +) +async def test_entity_name( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + expected_friendly_name: str | None, + device_class: str | None, +) -> None: + """Test the entity name setup.""" + domain = binary_sensor.DOMAIN + config = DEFAULT_CONFIG + await help_test_entity_name( + hass, mqtt_mock_entry, domain, config, expected_friendly_name, device_class + ) diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index fa16ef77817805..481e98f00997ed 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -30,6 +30,7 @@ help_test_entity_device_info_with_connection, help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, + help_test_entity_name, help_test_publishing_with_custom_encoding, help_test_reloadable, help_test_setting_attribute_via_mqtt_json_message, @@ -569,3 +570,26 @@ async def test_unload_entry( await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry, domain, config ) + + +@pytest.mark.parametrize( + ("expected_friendly_name", "device_class"), + [ + ("test", None), + ("Update", "update"), + ("Identify", "identify"), + ("Restart", "restart"), + ], +) +async def test_entity_name( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + expected_friendly_name: str | None, + device_class: str | None, +) -> None: + """Test the entity name setup.""" + domain = button.DOMAIN + config = DEFAULT_CONFIG + await help_test_entity_name( + hass, mqtt_mock_entry, domain, config, expected_friendly_name, device_class + ) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index fd760044f3c809..9d580da073edb5 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -1117,6 +1117,45 @@ async def help_test_entity_device_info_update( assert device.name == "Milk" +async def help_test_entity_name( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + domain: str, + config: ConfigType, + expected_friendly_name: str | None = None, + device_class: str | None = None, +) -> None: + """Test device name setup with and without a device_class set. + + This is a test helper for the _setup_common_attributes_from_config mixin. + """ + await mqtt_mock_entry() + # Add device settings to config + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + expected_entity_name = "test" + if device_class is not None: + config["device_class"] = device_class + # Do not set a name + config.pop("name") + expected_entity_name = device_class + + registry = dr.async_get(hass) + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}) + assert device is not None + + entity_id = f"{domain}.beer_{expected_entity_name}" + state = hass.states.get(entity_id) + assert state is not None + assert state.name == f"Beer {expected_friendly_name}" + + async def help_test_entity_id_update_subscriptions( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -1390,7 +1429,7 @@ async def help_test_entity_debug_info_message( with patch("homeassistant.util.dt.utcnow") as dt_utcnow: dt_utcnow.return_value = start_dt if service: - service_data = {ATTR_ENTITY_ID: f"{domain}.test"} + service_data = {ATTR_ENTITY_ID: f"{domain}.beer_test"} if service_parameters: service_data.update(service_parameters) @@ -1458,7 +1497,7 @@ async def help_test_entity_debug_info_remove( "subscriptions" ] assert len(debug_info_data["triggers"]) == 0 - assert debug_info_data["entities"][0]["entity_id"] == f"{domain}.test" + assert debug_info_data["entities"][0]["entity_id"] == f"{domain}.beer_test" entity_id = debug_info_data["entities"][0]["entity_id"] async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", "") @@ -1503,7 +1542,7 @@ async def help_test_entity_debug_info_update_entity_id( == f"homeassistant/{domain}/bla/config" ) assert debug_info_data["entities"][0]["discovery_data"]["payload"] == config - assert debug_info_data["entities"][0]["entity_id"] == f"{domain}.test" + assert debug_info_data["entities"][0]["entity_id"] == f"{domain}.beer_test" assert len(debug_info_data["entities"][0]["subscriptions"]) == 1 assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ "subscriptions" @@ -1511,7 +1550,7 @@ async def help_test_entity_debug_info_update_entity_id( assert len(debug_info_data["triggers"]) == 0 entity_registry.async_update_entity( - f"{domain}.test", new_entity_id=f"{domain}.milk" + f"{domain}.beer_test", new_entity_id=f"{domain}.milk" ) await hass.async_block_till_done() await hass.async_block_till_done() @@ -1529,7 +1568,7 @@ async def help_test_entity_debug_info_update_entity_id( "subscriptions" ] assert len(debug_info_data["triggers"]) == 0 - assert f"{domain}.test" not in hass.data["mqtt"].debug_info_entities + assert f"{domain}.beer_test" not in hass.data["mqtt"].debug_info_entities async def help_test_entity_disabled_by_default( diff --git a/tests/components/mqtt/test_diagnostics.py b/tests/components/mqtt/test_diagnostics.py index fb1033848743ca..eb923ac2f0768c 100644 --- a/tests/components/mqtt/test_diagnostics.py +++ b/tests/components/mqtt/test_diagnostics.py @@ -80,7 +80,7 @@ async def test_entry_diagnostics( expected_debug_info = { "entities": [ { - "entity_id": "sensor.mqtt_sensor", + "entity_id": "sensor.none_mqtt_sensor", "subscriptions": [{"topic": "foobar/sensor", "messages": []}], "discovery_data": { "payload": config_sensor, @@ -109,13 +109,13 @@ async def test_entry_diagnostics( "disabled": False, "disabled_by": None, "entity_category": None, - "entity_id": "sensor.mqtt_sensor", + "entity_id": "sensor.none_mqtt_sensor", "icon": None, "original_device_class": None, "original_icon": None, "state": { "attributes": {"friendly_name": "MQTT Sensor"}, - "entity_id": "sensor.mqtt_sensor", + "entity_id": "sensor.none_mqtt_sensor", "last_changed": ANY, "last_updated": ANY, "state": "unknown", diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 62b87bdb791c04..d3b8a145df7d8c 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -729,10 +729,10 @@ async def test_cleanup_device( # Verify device and registry entries are created device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is not None - entity_entry = entity_registry.async_get("sensor.mqtt_sensor") + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert entity_entry is not None - state = hass.states.get("sensor.mqtt_sensor") + state = hass.states.get("sensor.none_mqtt_sensor") assert state is not None # Remove MQTT from the device @@ -753,11 +753,11 @@ async def test_cleanup_device( # Verify device and registry entries are cleared device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is None - entity_entry = entity_registry.async_get("sensor.mqtt_sensor") + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert entity_entry is None # Verify state is removed - state = hass.states.get("sensor.mqtt_sensor") + state = hass.states.get("sensor.none_mqtt_sensor") assert state is None await hass.async_block_till_done() @@ -788,10 +788,10 @@ async def test_cleanup_device_mqtt( # Verify device and registry entries are created device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is not None - entity_entry = entity_registry.async_get("sensor.mqtt_sensor") + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert entity_entry is not None - state = hass.states.get("sensor.mqtt_sensor") + state = hass.states.get("sensor.none_mqtt_sensor") assert state is not None async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", "") @@ -801,11 +801,11 @@ async def test_cleanup_device_mqtt( # Verify device and registry entries are cleared device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is None - entity_entry = entity_registry.async_get("sensor.mqtt_sensor") + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert entity_entry is None # Verify state is removed - state = hass.states.get("sensor.mqtt_sensor") + state = hass.states.get("sensor.none_mqtt_sensor") assert state is None await hass.async_block_till_done() @@ -873,10 +873,10 @@ async def test_cleanup_device_multiple_config_entries( mqtt_config_entry.entry_id, config_entry.entry_id, } - entity_entry = entity_registry.async_get("sensor.mqtt_sensor") + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert entity_entry is not None - state = hass.states.get("sensor.mqtt_sensor") + state = hass.states.get("sensor.none_mqtt_sensor") assert state is not None # Remove MQTT from the device @@ -900,12 +900,12 @@ async def test_cleanup_device_multiple_config_entries( connections={("mac", "12:34:56:AB:CD:EF")} ) assert device_entry is not None - entity_entry = entity_registry.async_get("sensor.mqtt_sensor") + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert device_entry.config_entries == {config_entry.entry_id} assert entity_entry is None # Verify state is removed - state = hass.states.get("sensor.mqtt_sensor") + state = hass.states.get("sensor.none_mqtt_sensor") assert state is None await hass.async_block_till_done() @@ -973,10 +973,10 @@ async def test_cleanup_device_multiple_config_entries_mqtt( mqtt_config_entry.entry_id, config_entry.entry_id, } - entity_entry = entity_registry.async_get("sensor.mqtt_sensor") + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert entity_entry is not None - state = hass.states.get("sensor.mqtt_sensor") + state = hass.states.get("sensor.none_mqtt_sensor") assert state is not None # Send MQTT messages to remove @@ -992,12 +992,12 @@ async def test_cleanup_device_multiple_config_entries_mqtt( connections={("mac", "12:34:56:AB:CD:EF")} ) assert device_entry is not None - entity_entry = entity_registry.async_get("sensor.mqtt_sensor") + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert device_entry.config_entries == {config_entry.entry_id} assert entity_entry is None # Verify state is removed - state = hass.states.get("sensor.mqtt_sensor") + state = hass.states.get("sensor.none_mqtt_sensor") assert state is None await hass.async_block_till_done() @@ -1474,13 +1474,12 @@ async def test_clear_config_topic_disabled_entity( mqtt_mock = await mqtt_mock_entry() # discover an entity that is not enabled by default config = { - "name": "sbfspot_12345", "state_topic": "homeassistant_test/sensor/sbfspot_0/sbfspot_12345/", "unique_id": "sbfspot_12345", "enabled_by_default": False, "device": { "identifiers": ["sbfspot_12345"], - "name": "sbfspot_12345", + "name": "abc123", "sw_version": "1.0", "connections": [["mac", "12:34:56:AB:CD:EF"]], }, @@ -1512,9 +1511,9 @@ async def test_clear_config_topic_disabled_entity( await hass.async_block_till_done() assert "Platform mqtt does not generate unique IDs" in caplog.text - assert hass.states.get("sensor.sbfspot_12345") is None # disabled - assert hass.states.get("sensor.sbfspot_12345_1") is not None # enabled - assert hass.states.get("sensor.sbfspot_12345_2") is None # not unique + assert hass.states.get("sensor.abc123_sbfspot_12345") is None # disabled + assert hass.states.get("sensor.abc123_sbfspot_12345_1") is not None # enabled + assert hass.states.get("sensor.abc123_sbfspot_12345_2") is None # not unique # Verify device is created device_entry = device_registry.async_get_device( @@ -1603,13 +1602,12 @@ async def test_unique_id_collission_has_priority( """Test the unique_id collision detection has priority over registry disabled items.""" await mqtt_mock_entry() config = { - "name": "sbfspot_12345", "state_topic": "homeassistant_test/sensor/sbfspot_0/sbfspot_12345/", "unique_id": "sbfspot_12345", "enabled_by_default": False, "device": { "identifiers": ["sbfspot_12345"], - "name": "sbfspot_12345", + "name": "abc123", "sw_version": "1.0", "connections": [["mac", "12:34:56:AB:CD:EF"]], }, @@ -1633,13 +1631,13 @@ async def test_unique_id_collission_has_priority( ) await hass.async_block_till_done() - assert hass.states.get("sensor.sbfspot_12345_1") is None # not enabled - assert hass.states.get("sensor.sbfspot_12345_2") is None # not unique + assert hass.states.get("sensor.abc123_sbfspot_12345_1") is None # not enabled + assert hass.states.get("sensor.abc123_sbfspot_12345_2") is None # not unique # Verify the first entity is created - assert entity_registry.async_get("sensor.sbfspot_12345_1") is not None + assert entity_registry.async_get("sensor.abc123_sbfspot_12345_1") is not None # Verify the second entity is not created because it is not unique - assert entity_registry.async_get("sensor.sbfspot_12345_2") is None + assert entity_registry.async_get("sensor.abc123_sbfspot_12345_2") is None @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 3395dc0825f233..c0d7a94de5b79f 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -2821,7 +2821,7 @@ async def test_mqtt_ws_get_device_debug_info( expected_result = { "entities": [ { - "entity_id": "sensor.mqtt_sensor", + "entity_id": "sensor.none_mqtt_sensor", "subscriptions": [{"topic": "foobar/sensor", "messages": []}], "discovery_data": { "payload": config_sensor, @@ -2884,7 +2884,7 @@ async def test_mqtt_ws_get_device_debug_info_binary( expected_result = { "entities": [ { - "entity_id": "camera.mqtt_camera", + "entity_id": "camera.none_mqtt_camera", "subscriptions": [ { "topic": "foobar/image", diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py index c7285f0fa5f494..5a30a3a65de052 100644 --- a/tests/components/mqtt/test_mixins.py +++ b/tests/components/mqtt/test_mixins.py @@ -5,8 +5,12 @@ import pytest from homeassistant.components import mqtt, sensor +from homeassistant.components.mqtt.sensor import DEFAULT_NAME as DEFAULT_SENSOR_NAME from homeassistant.const import EVENT_STATE_CHANGED, Platform from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import ( + device_registry as dr, +) from tests.common import async_fire_mqtt_message from tests.typing import MqttMockHAClientGenerator @@ -73,3 +77,179 @@ def test_callback(event) -> None: # The availability is changed but the topic is shared, # hence there the state will be written when the value is updated assert len(events) == 1 + + +@pytest.mark.parametrize( + ("hass_config", "entity_id", "friendly_name", "device_name", "assert_log"), + [ + ( # default_entity_name_without_device_name + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "state_topic": "test-topic", + "unique_id": "veryunique", + "device": {"identifiers": ["helloworld"]}, + } + } + }, + "sensor.none_mqtt_sensor", + DEFAULT_SENSOR_NAME, + None, + True, + ), + ( # default_entity_name_with_device_name + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "state_topic": "test-topic", + "unique_id": "veryunique", + "device": {"name": "Test", "identifiers": ["helloworld"]}, + } + } + }, + "sensor.test_mqtt_sensor", + "Test MQTT Sensor", + "Test", + False, + ), + ( # name_follows_device_class + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "state_topic": "test-topic", + "unique_id": "veryunique", + "device_class": "humidity", + "device": {"name": "Test", "identifiers": ["helloworld"]}, + } + } + }, + "sensor.test_humidity", + "Test Humidity", + "Test", + False, + ), + ( # name_follows_device_class_without_device_name + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "state_topic": "test-topic", + "unique_id": "veryunique", + "device_class": "humidity", + "device": {"identifiers": ["helloworld"]}, + } + } + }, + "sensor.none_humidity", + "Humidity", + None, + True, + ), + ( # name_overrides_device_class + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "MySensor", + "state_topic": "test-topic", + "unique_id": "veryunique", + "device_class": "humidity", + "device": {"name": "Test", "identifiers": ["helloworld"]}, + } + } + }, + "sensor.test_mysensor", + "Test MySensor", + "Test", + False, + ), + ( # name_set_no_device_name_set + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "MySensor", + "state_topic": "test-topic", + "unique_id": "veryunique", + "device_class": "humidity", + "device": {"identifiers": ["helloworld"]}, + } + } + }, + "sensor.none_mysensor", + "MySensor", + None, + True, + ), + ( # none_entity_name_with_device_name + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": None, + "state_topic": "test-topic", + "unique_id": "veryunique", + "device_class": "humidity", + "device": {"name": "Test", "identifiers": ["helloworld"]}, + } + } + }, + "sensor.test", + "Test", + "Test", + False, + ), + ( # none_entity_name_without_device_name + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": None, + "state_topic": "test-topic", + "unique_id": "veryunique", + "device_class": "humidity", + "device": {"identifiers": ["helloworld"]}, + } + } + }, + "sensor.mqtt_veryunique", + "mqtt veryunique", + None, + True, + ), + ], + ids=[ + "default_entity_name_without_device_name", + "default_entity_name_with_device_name", + "name_follows_device_class", + "name_follows_device_class_without_device_name", + "name_overrides_device_class", + "name_set_no_device_name_set", + "none_entity_name_with_device_name", + "none_entity_name_without_device_name", + ], +) +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) +async def test_default_entity_and_device_name( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, + entity_id: str, + friendly_name: str, + device_name: str | None, + assert_log: bool, +) -> None: + """Test device name setup with and without a device_class set. + + This is a test helper for the _setup_common_attributes_from_config mixin. + """ + await mqtt_mock_entry() + + registry = dr.async_get(hass) + + device = registry.async_get_device({("mqtt", "helloworld")}) + assert device is not None + assert device.name == device_name + + state = hass.states.get(entity_id) + assert state is not None + assert state.name == friendly_name + + assert ( + "MQTT device information always needs to include a name" in caplog.text + ) is assert_log diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 96d9cdcef64178..dbdd373a659d85 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -48,6 +48,7 @@ help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_entity_name, help_test_publishing_with_custom_encoding, help_test_reloadable, help_test_setting_attribute_via_mqtt_json_message, @@ -1121,3 +1122,21 @@ async def test_unload_entry( await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry, domain, config ) + + +@pytest.mark.parametrize( + ("expected_friendly_name", "device_class"), + [("test", None), ("Humidity", "humidity"), ("Temperature", "temperature")], +) +async def test_entity_name( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + expected_friendly_name: str | None, + device_class: str | None, +) -> None: + """Test the entity name setup.""" + domain = number.DOMAIN + config = DEFAULT_CONFIG + await help_test_entity_name( + hass, mqtt_mock_entry, domain, config, expected_friendly_name, device_class + ) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index d6ab692af52742..30eb0fd1939712 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -53,6 +53,7 @@ help_test_entity_disabled_by_default, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_entity_name, help_test_reload_with_config, help_test_reloadable, help_test_setting_attribute_via_mqtt_json_message, @@ -1409,3 +1410,21 @@ async def test_unload_entry( await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry, domain, config ) + + +@pytest.mark.parametrize( + ("expected_friendly_name", "device_class"), + [("test", None), ("Humidity", "humidity"), ("Temperature", "temperature")], +) +async def test_entity_name( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + expected_friendly_name: str | None, + device_class: str | None, +) -> None: + """Test the entity name setup.""" + domain = sensor.DOMAIN + config = DEFAULT_CONFIG + await help_test_entity_name( + hass, mqtt_mock_entry, domain, config, expected_friendly_name, device_class + ) From 9b0d4c8c03377626c50f492614e998e60752725f Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Fri, 21 Jul 2023 04:00:18 -0700 Subject: [PATCH 0741/1009] Fix a translation bug for water price issue (#96958) --- homeassistant/components/energy/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/energy/strings.json b/homeassistant/components/energy/strings.json index 611d36882ee3d7..9a72541bb505d7 100644 --- a/homeassistant/components/energy/strings.json +++ b/homeassistant/components/energy/strings.json @@ -39,11 +39,11 @@ }, "entity_unexpected_unit_gas_price": { "title": "[%key:component::energy::issues::entity_unexpected_unit_energy_price::title%]", - "description": "[%key:component::energy::issues::entity_unexpected_unit_energy::description%]" + "description": "[%key:component::energy::issues::entity_unexpected_unit_energy_price::description%]" }, "entity_unexpected_unit_water_price": { - "title": "[%key:component::energy::issues::entity_unexpected_unit_energy::title%]", - "description": "[%key:component::energy::issues::entity_unexpected_unit_energy::description%]" + "title": "[%key:component::energy::issues::entity_unexpected_unit_energy_price::title%]", + "description": "[%key:component::energy::issues::entity_unexpected_unit_energy_price::description%]" }, "entity_unexpected_state_class": { "title": "Unexpected state class", From 58ce35787087e1c199b1ae2192e7c11f745c686d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 21 Jul 2023 14:07:10 +0200 Subject: [PATCH 0742/1009] Add uv_index to Weather Entity (#96951) * Add uv_index to Weather Entity * translation * Update homeassistant/components/weather/__init__.py Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/weather/__init__.py | 12 ++++++++++++ homeassistant/components/weather/const.py | 1 + homeassistant/components/weather/strings.json | 3 +++ tests/components/weather/test_init.py | 9 ++++++++- .../testing_config/custom_components/test/weather.py | 7 +++++++ 5 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index c4a6a0ad777c3e..45b5cbe9fbabad 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -40,6 +40,7 @@ ATTR_WEATHER_PRESSURE_UNIT, ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_TEMPERATURE_UNIT, + ATTR_WEATHER_UV_INDEX, ATTR_WEATHER_VISIBILITY, ATTR_WEATHER_VISIBILITY_UNIT, ATTR_WEATHER_WIND_BEARING, @@ -93,6 +94,7 @@ ATTR_FORECAST_NATIVE_DEW_POINT: Final = "native_dew_point" ATTR_FORECAST_DEW_POINT: Final = "dew_point" ATTR_FORECAST_CLOUD_COVERAGE: Final = "cloud_coverage" +ATTR_FORECAST_UV_INDEX: Final = "uv_index" ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -146,6 +148,7 @@ class Forecast(TypedDict, total=False): native_wind_speed: float | None wind_speed: None native_dew_point: float | None + uv_index: float | None async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -184,6 +187,7 @@ class WeatherEntity(Entity): _attr_humidity: float | None = None _attr_ozone: float | None = None _attr_cloud_coverage: int | None = None + _attr_uv_index: float | None = None _attr_precision: float _attr_pressure: None = ( None # Provide backwards compatibility. Use _attr_native_pressure @@ -503,6 +507,11 @@ def cloud_coverage(self) -> float | None: """Return the Cloud coverage in %.""" return self._attr_cloud_coverage + @property + def uv_index(self) -> float | None: + """Return the UV index.""" + return self._attr_uv_index + @final @property def visibility(self) -> float | None: @@ -680,6 +689,9 @@ def state_attributes(self) -> dict[str, Any]: # noqa: C901 if (cloud_coverage := self.cloud_coverage) is not None: data[ATTR_WEATHER_CLOUD_COVERAGE] = cloud_coverage + if (uv_index := self.uv_index) is not None: + data[ATTR_WEATHER_UV_INDEX] = uv_index + if (pressure := self.native_pressure) is not None: from_unit = self.native_pressure_unit or self._default_pressure_unit to_unit = self._pressure_unit diff --git a/homeassistant/components/weather/const.py b/homeassistant/components/weather/const.py index b995ce2b7299e5..759021741ffce7 100644 --- a/homeassistant/components/weather/const.py +++ b/homeassistant/components/weather/const.py @@ -34,6 +34,7 @@ ATTR_WEATHER_WIND_SPEED_UNIT = "wind_speed_unit" ATTR_WEATHER_PRECIPITATION_UNIT = "precipitation_unit" ATTR_WEATHER_CLOUD_COVERAGE = "cloud_coverage" +ATTR_WEATHER_UV_INDEX = "uv_index" DOMAIN: Final = "weather" diff --git a/homeassistant/components/weather/strings.json b/homeassistant/components/weather/strings.json index 26ccd731828267..21029c77284508 100644 --- a/homeassistant/components/weather/strings.json +++ b/homeassistant/components/weather/strings.json @@ -71,6 +71,9 @@ }, "wind_speed_unit": { "name": "Wind speed unit" + }, + "uv_index": { + "name": "UV index" } } } diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 5ed6a02f24b9d9..53753ad4a72596 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -13,6 +13,7 @@ ATTR_FORECAST_PRESSURE, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_UV_INDEX, ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_GUST_SPEED, ATTR_FORECAST_WIND_SPEED, @@ -23,6 +24,7 @@ ATTR_WEATHER_PRESSURE_UNIT, ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_TEMPERATURE_UNIT, + ATTR_WEATHER_UV_INDEX, ATTR_WEATHER_VISIBILITY, ATTR_WEATHER_VISIBILITY_UNIT, ATTR_WEATHER_WIND_BEARING, @@ -583,7 +585,7 @@ async def test_precipitation_no_unit( ) -async def test_wind_bearing_ozone_and_cloud_coverage( +async def test_wind_bearing_ozone_and_cloud_coverage_and_uv_index( hass: HomeAssistant, enable_custom_integrations: None, ) -> None: @@ -591,18 +593,23 @@ async def test_wind_bearing_ozone_and_cloud_coverage( wind_bearing_value = 180 ozone_value = 10 cloud_coverage = 75 + uv_index = 1.2 entity0 = await create_entity( hass, wind_bearing=wind_bearing_value, ozone=ozone_value, cloud_coverage=cloud_coverage, + uv_index=uv_index, ) state = hass.states.get(entity0.entity_id) + forecast = state.attributes[ATTR_FORECAST][0] assert float(state.attributes[ATTR_WEATHER_WIND_BEARING]) == 180 assert float(state.attributes[ATTR_WEATHER_OZONE]) == 10 assert float(state.attributes[ATTR_WEATHER_CLOUD_COVERAGE]) == 75 + assert float(state.attributes[ATTR_WEATHER_UV_INDEX]) == 1.2 + assert float(forecast[ATTR_FORECAST_UV_INDEX]) == 1.2 async def test_humidity( diff --git a/tests/testing_config/custom_components/test/weather.py b/tests/testing_config/custom_components/test/weather.py index a5c49fb92c2473..df6a43ad40caf6 100644 --- a/tests/testing_config/custom_components/test/weather.py +++ b/tests/testing_config/custom_components/test/weather.py @@ -19,6 +19,7 @@ ATTR_FORECAST_PRESSURE, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_UV_INDEX, ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, Forecast, @@ -111,6 +112,11 @@ def cloud_coverage(self) -> float | None: """Return the cloud coverage in %.""" return self._handle("cloud_coverage") + @property + def uv_index(self) -> float | None: + """Return the UV index.""" + return self._handle("uv_index") + @property def native_visibility(self) -> float | None: """Return the visibility.""" @@ -228,6 +234,7 @@ def forecast(self) -> list[Forecast] | None: ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: self.native_wind_gust_speed, ATTR_FORECAST_NATIVE_WIND_SPEED: self.native_wind_speed, ATTR_FORECAST_WIND_BEARING: self.wind_bearing, + ATTR_FORECAST_UV_INDEX: self.uv_index, ATTR_FORECAST_NATIVE_PRECIPITATION: self._values.get( "native_precipitation" ), From 878a4f1bb9af119576b970872aad396bc2d2a967 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 21 Jul 2023 14:15:15 +0200 Subject: [PATCH 0743/1009] Update pytest-freezer to 0.4.8 (#97000) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 2db6d8fe3d43f2..c4e943baceadaf 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -20,7 +20,7 @@ pipdeptree==2.10.2 pytest-asyncio==0.20.3 pytest-aiohttp==1.0.4 pytest-cov==3.0.0 -pytest-freezer==0.4.6 +pytest-freezer==0.4.8 pytest-socket==0.5.1 pytest-test-groups==1.0.3 pytest-sugar==0.9.6 From 2e156e56bf47d5b00b632be82cfe6494c0396452 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 21 Jul 2023 12:20:03 +0000 Subject: [PATCH 0744/1009] Create an issue if Shelly TRV is not calibrated (#96952) * Create issue if Shelly Valve is not calibrated * Add test * Improve test * Improve issue description * Restart -> reboot --- homeassistant/components/shelly/climate.py | 29 ++++++++++++- homeassistant/components/shelly/const.py | 2 + homeassistant/components/shelly/strings.json | 4 ++ tests/components/shelly/test_climate.py | 44 +++++++++++++++++++- 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 2027cf73d25747..04c211a98cb1f2 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -20,6 +20,7 @@ from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -33,7 +34,12 @@ from homeassistant.util.unit_conversion import TemperatureConverter from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM -from .const import LOGGER, SHTRV_01_TEMPERATURE_SETTINGS +from .const import ( + DOMAIN, + LOGGER, + NOT_CALIBRATED_ISSUE_ID, + SHTRV_01_TEMPERATURE_SETTINGS, +) from .coordinator import ShellyBlockCoordinator, get_entry_data @@ -339,6 +345,27 @@ def _handle_coordinator_update(self) -> None: self.async_write_ha_state() return + if self.coordinator.device.status.get("calibrated") is False: + ir.async_create_issue( + self.hass, + DOMAIN, + NOT_CALIBRATED_ISSUE_ID.format(unique=self.coordinator.mac), + is_fixable=False, + is_persistent=False, + severity=ir.IssueSeverity.ERROR, + translation_key="device_not_calibrated", + translation_placeholders={ + "device_name": self.name, + "ip_address": self.coordinator.device.ip_address, + }, + ) + else: + ir.async_delete_issue( + self.hass, + DOMAIN, + NOT_CALIBRATED_ISSUE_ID.format(unique=self.coordinator.mac), + ) + assert self.coordinator.device.blocks for block in self.coordinator.device.blocks: diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index e678f92c480341..608798976ba8f0 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -178,3 +178,5 @@ class BLEScannerMode(StrEnum): MAX_PUSH_UPDATE_FAILURES = 5 PUSH_UPDATE_ISSUE_ID = "push_update_{unique}" + +NOT_CALIBRATED_ISSUE_ID = "not_calibrated_{unique}" diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 7c3f6033d07ce2..6ff48f5b85b178 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -120,6 +120,10 @@ } }, "issues": { + "device_not_calibrated": { + "title": "Shelly device {device_name} is not calibrated", + "description": "Shelly device {device_name} with IP address {ip_address} requires calibration. To calibrate the device, it must be rebooted after proper installation on the valve. You can reboot the device in its web panel, go to 'Settings' > 'Device Reboot'." + }, "push_update_failure": { "title": "Shelly device {device_name} push update failure", "description": "Home Assistant is not receiving push updates from the Shelly device {device_name} with IP address {ip_address}. Check the CoIoT configuration in the web panel of the device and your network configuration." diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index 505d1d463e8829..c806cb5e74200e 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -21,9 +21,11 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.issue_registry as ir from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM -from . import init_integration, register_device, register_entity +from . import MOCK_MAC, init_integration, register_device, register_entity +from .conftest import MOCK_STATUS_COAP from tests.common import mock_restore_cache, mock_restore_cache_with_extra_data @@ -486,3 +488,43 @@ async def test_block_restored_climate_auth_error( assert "context" in flow assert flow["context"].get("source") == SOURCE_REAUTH assert flow["context"].get("entry_id") == entry.entry_id + + +async def test_device_not_calibrated( + hass: HomeAssistant, mock_block_device, monkeypatch +) -> None: + """Test to create an issue when the device is not calibrated.""" + issue_registry: ir.IssueRegistry = ir.async_get(hass) + + await init_integration(hass, 1, sleep_period=1000, model="SHTRV-01") + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + mock_status = MOCK_STATUS_COAP.copy() + mock_status["calibrated"] = False + monkeypatch.setattr( + mock_block_device, + "status", + mock_status, + ) + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert issue_registry.async_get_issue( + domain=DOMAIN, issue_id=f"not_calibrated_{MOCK_MAC}" + ) + + # The device has been calibrated + monkeypatch.setattr( + mock_block_device, + "status", + MOCK_STATUS_COAP, + ) + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert not issue_registry.async_get_issue( + domain=DOMAIN, issue_id=f"not_calibrated_{MOCK_MAC}" + ) From 7d173bf4e5b6189052e1a33a23528b9ede551911 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 21 Jul 2023 15:07:12 +0200 Subject: [PATCH 0745/1009] Update pytest-cov to 4.1.0 (#97010) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index c4e943baceadaf..9dd7c75e22ad09 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -19,7 +19,7 @@ pylint-per-file-ignores==1.2.1 pipdeptree==2.10.2 pytest-asyncio==0.20.3 pytest-aiohttp==1.0.4 -pytest-cov==3.0.0 +pytest-cov==4.1.0 pytest-freezer==0.4.8 pytest-socket==0.5.1 pytest-test-groups==1.0.3 From 9954208d3ac0f4948ccb22e4a787bdd90fa4afc8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 21 Jul 2023 15:20:24 +0200 Subject: [PATCH 0746/1009] Move OpenSky constants to separate const file (#97013) --- homeassistant/components/opensky/const.py | 15 ++++++++ homeassistant/components/opensky/sensor.py | 43 ++++++---------------- 2 files changed, 26 insertions(+), 32 deletions(-) create mode 100644 homeassistant/components/opensky/const.py diff --git a/homeassistant/components/opensky/const.py b/homeassistant/components/opensky/const.py new file mode 100644 index 00000000000000..7e511ed7d2c5e7 --- /dev/null +++ b/homeassistant/components/opensky/const.py @@ -0,0 +1,15 @@ +"""OpenSky constants.""" +DEFAULT_NAME = "OpenSky" +DOMAIN = "opensky" + +CONF_ALTITUDE = "altitude" +ATTR_ICAO24 = "icao24" +ATTR_CALLSIGN = "callsign" +ATTR_ALTITUDE = "altitude" +ATTR_ON_GROUND = "on_ground" +ATTR_SENSOR = "sensor" +ATTR_STATES = "states" +DEFAULT_ALTITUDE = 0 + +EVENT_OPENSKY_ENTRY = f"{DOMAIN}_entry" +EVENT_OPENSKY_EXIT = f"{DOMAIN}_exit" diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index cdedd0c9620b7d..0616b774951845 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -21,42 +21,21 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -CONF_ALTITUDE = "altitude" - -ATTR_ICAO24 = "icao24" -ATTR_CALLSIGN = "callsign" -ATTR_ALTITUDE = "altitude" -ATTR_ON_GROUND = "on_ground" -ATTR_SENSOR = "sensor" -ATTR_STATES = "states" - -DOMAIN = "opensky" - -DEFAULT_ALTITUDE = 0 +from .const import ( + ATTR_ALTITUDE, + ATTR_CALLSIGN, + ATTR_ICAO24, + ATTR_SENSOR, + CONF_ALTITUDE, + DEFAULT_ALTITUDE, + DOMAIN, + EVENT_OPENSKY_ENTRY, + EVENT_OPENSKY_EXIT, +) -EVENT_OPENSKY_ENTRY = f"{DOMAIN}_entry" -EVENT_OPENSKY_EXIT = f"{DOMAIN}_exit" # OpenSky free user has 400 credits, with 4 credits per API call. 100/24 = ~4 requests per hour SCAN_INTERVAL = timedelta(minutes=15) -OPENSKY_API_URL = "https://opensky-network.org/api/states/all" -OPENSKY_API_FIELDS = [ - ATTR_ICAO24, - ATTR_CALLSIGN, - "origin_country", - "time_position", - "time_velocity", - ATTR_LONGITUDE, - ATTR_LATITUDE, - ATTR_ALTITUDE, - ATTR_ON_GROUND, - "velocity", - "heading", - "vertical_rate", - "sensors", -] - - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_RADIUS): vol.Coerce(float), From 9434a64b8766076dc3c61c8860afb842a05ffee5 Mon Sep 17 00:00:00 2001 From: rappenze Date: Fri, 21 Jul 2023 15:22:45 +0200 Subject: [PATCH 0747/1009] 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 4b3721eed15cd8..d90a9d286624a4 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 497cbbf875ccc7..f2345630dbbf62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1669,7 +1669,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 e4021b23e912b2..21bdcf31527706 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1230,7 +1230,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 From b3da2ea9a602336946f5fb752dc8e72f47f547a5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 21 Jul 2023 15:29:15 +0200 Subject: [PATCH 0748/1009] Update pytest-socket to 0.6.0 (#97011) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9dd7c75e22ad09..f2901d885571d2 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -21,7 +21,7 @@ pytest-asyncio==0.20.3 pytest-aiohttp==1.0.4 pytest-cov==4.1.0 pytest-freezer==0.4.8 -pytest-socket==0.5.1 +pytest-socket==0.6.0 pytest-test-groups==1.0.3 pytest-sugar==0.9.6 pytest-timeout==2.1.0 From 530556015f98097664d02372701efb15442c6949 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 21 Jul 2023 15:32:27 +0200 Subject: [PATCH 0749/1009] Use walrus in event entity last event attributes (#97005) --- homeassistant/components/event/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/event/__init__.py b/homeassistant/components/event/__init__.py index 6eeab6a32bbf09..48bb2fd1726a1f 100644 --- a/homeassistant/components/event/__init__.py +++ b/homeassistant/components/event/__init__.py @@ -177,8 +177,8 @@ def state(self) -> str | None: def state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" attributes = {ATTR_EVENT_TYPE: self.__last_event_type} - if self.__last_event_attributes: - attributes |= self.__last_event_attributes + if last_event_attributes := self.__last_event_attributes: + attributes |= last_event_attributes return attributes @final From 9f98a418cdf1a9f7552b561f178979b5109dc105 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 21 Jul 2023 15:18:14 +0000 Subject: [PATCH 0750/1009] Add new sensors for Shelly Pro 3EM (#97006) * Add new sensors * Fix typo --- homeassistant/components/shelly/sensor.py | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 0260a540f0cc6b..3e9dfaad923aa0 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -360,6 +360,14 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), + "total_act_power": RpcSensorDescription( + key="em", + sub_key="total_act_power", + name="Total active power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), "a_aprt_power": RpcSensorDescription( key="em", sub_key="a_aprt_power", @@ -384,6 +392,14 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): device_class=SensorDeviceClass.APPARENT_POWER, state_class=SensorStateClass.MEASUREMENT, ), + "total_aprt_power": RpcSensorDescription( + key="em", + sub_key="total_aprt_power", + name="Total apparent power", + native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, + device_class=SensorDeviceClass.APPARENT_POWER, + state_class=SensorStateClass.MEASUREMENT, + ), "a_pf": RpcSensorDescription( key="em", sub_key="a_pf", @@ -480,6 +496,15 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), + "total_current": RpcSensorDescription( + key="em", + sub_key="total_current", + name="Total current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), "energy": RpcSensorDescription( key="switch", sub_key="aenergy", From 4e300568303c34019ed26f25881591af80e60f99 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 21 Jul 2023 17:30:48 +0200 Subject: [PATCH 0751/1009] Add new Forecasting to Weather (#75219) * Add new Forecasting to Weather * Add is_daytime for forecast_twice_daily * Fix test * Fix demo test * Adjust tests * Fix typing * Add demo * Mod demo more realistic * Fix test * Remove one weather * Fix weather example * kitchen_sink * Reverse demo partially * mod kitchen sink * Fix twice_daily * kitchen_sink * Add test weathers * Add twice daily to demo * dt_util * Fix names * Expose forecast via WS instead of as state attributes * Regularly update demo + kitchen_sink weather forecasts * Run linters * Fix rebase mistake * Improve demo test coverage * Improve weather test coverage * Exclude kitchen_sink weather from test coverage * Rename async_update_forecast to async_update_listeners * Add async_has_listeners helper * Revert "Add async_has_listeners helper" This reverts commit 52af3664bb06d9feac2c5ff963ee0022077c23ba. * Fix rebase mistake --------- Co-authored-by: Erik --- .coveragerc | 1 + homeassistant/components/demo/__init__.py | 1 + homeassistant/components/demo/weather.py | 117 ++++- .../components/kitchen_sink/__init__.py | 7 +- .../components/kitchen_sink/weather.py | 446 ++++++++++++++++++ homeassistant/components/weather/__init__.py | 417 +++++++++------- homeassistant/components/weather/const.py | 10 + .../components/weather/websocket_api.py | 72 ++- tests/components/demo/test_weather.py | 143 +++++- tests/components/weather/__init__.py | 31 ++ tests/components/weather/test_init.py | 90 ++-- tests/components/weather/test_recorder.py | 44 +- .../components/weather/test_websocket_api.py | 119 +++++ .../custom_components/test/weather.py | 55 +++ 14 files changed, 1323 insertions(+), 230 deletions(-) create mode 100644 homeassistant/components/kitchen_sink/weather.py diff --git a/.coveragerc b/.coveragerc index 6b3270f85e9e64..9e5541a07bc578 100644 --- a/.coveragerc +++ b/.coveragerc @@ -596,6 +596,7 @@ omit = homeassistant/components/keymitt_ble/entity.py homeassistant/components/keymitt_ble/switch.py homeassistant/components/keymitt_ble/coordinator.py + homeassistant/components/kitchen_sink/weather.py homeassistant/components/kiwi/lock.py homeassistant/components/kodi/__init__.py homeassistant/components/kodi/browse_media.py diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 6d54255f8ed59c..04eba5f05863ab 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -56,6 +56,7 @@ Platform.IMAGE_PROCESSING, Platform.CALENDAR, Platform.DEVICE_TRACKER, + Platform.WEATHER, ] CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py index e64d0bcc28d26c..887a9212335381 100644 --- a/homeassistant/components/demo/weather.py +++ b/homeassistant/components/demo/weather.py @@ -1,7 +1,7 @@ """Demo platform that offers fake meteorological data.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta from homeassistant.components.weather import ( ATTR_CONDITION_CLOUDY, @@ -20,11 +20,13 @@ ATTR_CONDITION_WINDY_VARIANT, Forecast, WeatherEntity, + WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfPressure, UnitOfSpeed, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -45,6 +47,8 @@ ATTR_CONDITION_EXCEPTIONAL: [], } +WEATHER_UPDATE_INTERVAL = timedelta(minutes=30) + async def async_setup_entry( hass: HomeAssistant, @@ -83,6 +87,8 @@ def setup_platform( [ATTR_CONDITION_RAINY, 15, 18, 7, 0], [ATTR_CONDITION_FOG, 0.2, 21, 12, 100], ], + None, + None, ), DemoWeather( "North", @@ -103,6 +109,24 @@ def setup_platform( [ATTR_CONDITION_SUNNY, 0.3, -14, -19, 0], [ATTR_CONDITION_SUNNY, 0, -9, -12, 0], ], + [ + [ATTR_CONDITION_SUNNY, 2, -10, -15, 60], + [ATTR_CONDITION_SUNNY, 1, -13, -14, 25], + [ATTR_CONDITION_SUNNY, 0, -18, -22, 70], + [ATTR_CONDITION_SUNNY, 0.1, -23, -23, 90], + [ATTR_CONDITION_SUNNY, 4, -19, -20, 40], + [ATTR_CONDITION_SUNNY, 0.3, -14, -19, 0], + [ATTR_CONDITION_SUNNY, 0, -9, -12, 0], + ], + [ + [ATTR_CONDITION_SNOWY, 2, -10, -15, 60, True], + [ATTR_CONDITION_PARTLYCLOUDY, 1, -13, -14, 25, False], + [ATTR_CONDITION_SUNNY, 0, -18, -22, 70, True], + [ATTR_CONDITION_SUNNY, 0.1, -23, -23, 90, False], + [ATTR_CONDITION_SNOWY, 4, -19, -20, 40, True], + [ATTR_CONDITION_SUNNY, 0.3, -14, -19, 0, False], + [ATTR_CONDITION_SUNNY, 0, -9, -12, 0, True], + ], ), ] ) @@ -125,10 +149,13 @@ def __init__( temperature_unit: str, pressure_unit: str, wind_speed_unit: str, - forecast: list[list], + forecast_daily: list[list] | None, + forecast_hourly: list[list] | None, + forecast_twice_daily: list[list] | None, ) -> None: """Initialize the Demo weather.""" self._attr_name = f"Demo Weather {name}" + self._attr_unique_id = f"demo-weather-{name.lower()}" self._condition = condition self._native_temperature = temperature self._native_temperature_unit = temperature_unit @@ -137,7 +164,40 @@ def __init__( self._native_pressure_unit = pressure_unit self._native_wind_speed = wind_speed self._native_wind_speed_unit = wind_speed_unit - self._forecast = forecast + self._forecast_daily = forecast_daily + self._forecast_hourly = forecast_hourly + self._forecast_twice_daily = forecast_twice_daily + self._attr_supported_features = 0 + if self._forecast_daily: + self._attr_supported_features |= WeatherEntityFeature.FORECAST_DAILY + if self._forecast_hourly: + self._attr_supported_features |= WeatherEntityFeature.FORECAST_HOURLY + if self._forecast_twice_daily: + self._attr_supported_features |= WeatherEntityFeature.FORECAST_TWICE_DAILY + + async def async_added_to_hass(self) -> None: + """Set up a timer updating the forecasts.""" + + async def update_forecasts(_: datetime) -> None: + if self._forecast_daily: + self._forecast_daily = ( + self._forecast_daily[1:] + self._forecast_daily[:1] + ) + if self._forecast_hourly: + self._forecast_hourly = ( + self._forecast_hourly[1:] + self._forecast_hourly[:1] + ) + if self._forecast_twice_daily: + self._forecast_twice_daily = ( + self._forecast_twice_daily[1:] + self._forecast_twice_daily[:1] + ) + await self.async_update_listeners(None) + + self.async_on_remove( + async_track_time_interval( + self.hass, update_forecasts, WEATHER_UPDATE_INTERVAL + ) + ) @property def native_temperature(self) -> float: @@ -181,13 +241,53 @@ def condition(self) -> str: k for k, v in CONDITION_CLASSES.items() if self._condition.lower() in v ][0] - @property - def forecast(self) -> list[Forecast]: - """Return the forecast.""" + async def async_forecast_daily(self) -> list[Forecast]: + """Return the daily forecast.""" reftime = dt_util.now().replace(hour=16, minute=00) forecast_data = [] - for entry in self._forecast: + assert self._forecast_daily is not None + for entry in self._forecast_daily: + data_dict = Forecast( + datetime=reftime.isoformat(), + condition=entry[0], + precipitation=entry[1], + temperature=entry[2], + templow=entry[3], + precipitation_probability=entry[4], + ) + reftime = reftime + timedelta(hours=24) + forecast_data.append(data_dict) + + return forecast_data + + async def async_forecast_hourly(self) -> list[Forecast]: + """Return the hourly forecast.""" + reftime = dt_util.now().replace(hour=16, minute=00) + + forecast_data = [] + assert self._forecast_hourly is not None + for entry in self._forecast_hourly: + data_dict = Forecast( + datetime=reftime.isoformat(), + condition=entry[0], + precipitation=entry[1], + temperature=entry[2], + templow=entry[3], + precipitation_probability=entry[4], + ) + reftime = reftime + timedelta(hours=1) + forecast_data.append(data_dict) + + return forecast_data + + async def async_forecast_twice_daily(self) -> list[Forecast]: + """Return the twice daily forecast.""" + reftime = dt_util.now().replace(hour=11, minute=00) + + forecast_data = [] + assert self._forecast_twice_daily is not None + for entry in self._forecast_twice_daily: data_dict = Forecast( datetime=reftime.isoformat(), condition=entry[0], @@ -195,8 +295,9 @@ def forecast(self) -> list[Forecast]: temperature=entry[2], templow=entry[3], precipitation_probability=entry[4], + is_daytime=entry[5], ) - reftime = reftime + timedelta(hours=4) + reftime = reftime + timedelta(hours=12) forecast_data.append(data_dict) return forecast_data diff --git a/homeassistant/components/kitchen_sink/__init__.py b/homeassistant/components/kitchen_sink/__init__.py index 7857e6b31495d4..a85221108f8e32 100644 --- a/homeassistant/components/kitchen_sink/__init__.py +++ b/homeassistant/components/kitchen_sink/__init__.py @@ -26,7 +26,12 @@ DOMAIN = "kitchen_sink" -COMPONENTS_WITH_DEMO_PLATFORM = [Platform.SENSOR, Platform.LOCK, Platform.IMAGE] +COMPONENTS_WITH_DEMO_PLATFORM = [ + Platform.SENSOR, + Platform.LOCK, + Platform.IMAGE, + Platform.WEATHER, +] CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) diff --git a/homeassistant/components/kitchen_sink/weather.py b/homeassistant/components/kitchen_sink/weather.py new file mode 100644 index 00000000000000..aba30013746e12 --- /dev/null +++ b/homeassistant/components/kitchen_sink/weather.py @@ -0,0 +1,446 @@ +"""Demo platform that offers fake meteorological data.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.components.weather import ( + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_EXCEPTIONAL, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, + ATTR_CONDITION_WINDY_VARIANT, + Forecast, + WeatherEntity, + WeatherEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfPressure, UnitOfSpeed, UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_time_interval +import homeassistant.util.dt as dt_util + +CONDITION_CLASSES: dict[str, list[str]] = { + ATTR_CONDITION_CLOUDY: [], + ATTR_CONDITION_FOG: [], + ATTR_CONDITION_HAIL: [], + ATTR_CONDITION_LIGHTNING: [], + ATTR_CONDITION_LIGHTNING_RAINY: [], + ATTR_CONDITION_PARTLYCLOUDY: [], + ATTR_CONDITION_POURING: [], + ATTR_CONDITION_RAINY: ["shower rain"], + ATTR_CONDITION_SNOWY: [], + ATTR_CONDITION_SNOWY_RAINY: [], + ATTR_CONDITION_SUNNY: ["sunshine"], + ATTR_CONDITION_WINDY: [], + ATTR_CONDITION_WINDY_VARIANT: [], + ATTR_CONDITION_EXCEPTIONAL: [], +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Demo config entry.""" + async_add_entities( + [ + DemoWeather( + "Legacy weather", + "Sunshine", + 21.6414, + 92, + 1099, + 0.5, + UnitOfTemperature.CELSIUS, + UnitOfPressure.HPA, + UnitOfSpeed.METERS_PER_SECOND, + [ + [ATTR_CONDITION_RAINY, 1, 22, 15, 60], + [ATTR_CONDITION_RAINY, 5, 19, 8, 30], + [ATTR_CONDITION_CLOUDY, 0, 15, 9, 10], + [ATTR_CONDITION_SUNNY, 0, 12, 6, 0], + [ATTR_CONDITION_PARTLYCLOUDY, 2, 14, 7, 20], + [ATTR_CONDITION_RAINY, 15, 18, 7, 0], + [ATTR_CONDITION_FOG, 0.2, 21, 12, 100], + ], + None, + None, + None, + ), + DemoWeather( + "Legacy + daily weather", + "Sunshine", + 21.6414, + 92, + 1099, + 0.5, + UnitOfTemperature.CELSIUS, + UnitOfPressure.HPA, + UnitOfSpeed.METERS_PER_SECOND, + [ + [ATTR_CONDITION_RAINY, 1, 22, 15, 60], + [ATTR_CONDITION_RAINY, 5, 19, 8, 30], + [ATTR_CONDITION_CLOUDY, 0, 15, 9, 10], + [ATTR_CONDITION_SUNNY, 0, 12, 6, 0], + [ATTR_CONDITION_PARTLYCLOUDY, 2, 14, 7, 20], + [ATTR_CONDITION_RAINY, 15, 18, 7, 0], + [ATTR_CONDITION_FOG, 0.2, 21, 12, 100], + ], + [ + [ATTR_CONDITION_RAINY, 1, 22, 15, 60], + [ATTR_CONDITION_RAINY, 5, 19, 8, 30], + [ATTR_CONDITION_CLOUDY, 0, 15, 9, 10], + [ATTR_CONDITION_SUNNY, 0, 12, 6, 0], + [ATTR_CONDITION_PARTLYCLOUDY, 2, 14, 7, 20], + [ATTR_CONDITION_RAINY, 15, 18, 7, 0], + [ATTR_CONDITION_FOG, 0.2, 21, 12, 100], + ], + None, + None, + ), + DemoWeather( + "Daily + hourly weather", + "Shower rain", + -12, + 54, + 987, + 4.8, + UnitOfTemperature.FAHRENHEIT, + UnitOfPressure.INHG, + UnitOfSpeed.MILES_PER_HOUR, + None, + [ + [ATTR_CONDITION_SNOWY, 2, -10, -15, 60], + [ATTR_CONDITION_PARTLYCLOUDY, 1, -13, -14, 25], + [ATTR_CONDITION_SUNNY, 0, -18, -22, 70], + [ATTR_CONDITION_SUNNY, 0.1, -23, -23, 90], + [ATTR_CONDITION_SNOWY, 4, -19, -20, 40], + [ATTR_CONDITION_SUNNY, 0.3, -14, -19, 0], + [ATTR_CONDITION_SUNNY, 0, -9, -12, 0], + ], + [ + [ATTR_CONDITION_SUNNY, 2, -10, -15, 60], + [ATTR_CONDITION_SUNNY, 1, -13, -14, 25], + [ATTR_CONDITION_SUNNY, 0, -18, -22, 70], + [ATTR_CONDITION_SUNNY, 0.1, -23, -23, 90], + [ATTR_CONDITION_SUNNY, 4, -19, -20, 40], + [ATTR_CONDITION_SUNNY, 0.3, -14, -19, 0], + [ATTR_CONDITION_SUNNY, 0, -9, -12, 0], + ], + None, + ), + DemoWeather( + "Daily + bi-daily + hourly weather", + "Sunshine", + 21.6414, + 92, + 1099, + 0.5, + UnitOfTemperature.CELSIUS, + UnitOfPressure.HPA, + UnitOfSpeed.METERS_PER_SECOND, + None, + [ + [ATTR_CONDITION_RAINY, 1, 22, 15, 60], + [ATTR_CONDITION_RAINY, 5, 19, 8, 30], + [ATTR_CONDITION_RAINY, 0, 15, 9, 10], + [ATTR_CONDITION_RAINY, 0, 12, 6, 0], + [ATTR_CONDITION_RAINY, 2, 14, 7, 20], + [ATTR_CONDITION_RAINY, 15, 18, 7, 0], + [ATTR_CONDITION_RAINY, 0.2, 21, 12, 100], + ], + [ + [ATTR_CONDITION_CLOUDY, 1, 22, 15, 60], + [ATTR_CONDITION_CLOUDY, 5, 19, 8, 30], + [ATTR_CONDITION_CLOUDY, 0, 15, 9, 10], + [ATTR_CONDITION_CLOUDY, 0, 12, 6, 0], + [ATTR_CONDITION_CLOUDY, 2, 14, 7, 20], + [ATTR_CONDITION_CLOUDY, 15, 18, 7, 0], + [ATTR_CONDITION_CLOUDY, 0.2, 21, 12, 100], + ], + [ + [ATTR_CONDITION_RAINY, 1, 22, 15, 60, True], + [ATTR_CONDITION_RAINY, 5, 19, 8, 30, False], + [ATTR_CONDITION_CLOUDY, 0, 15, 9, 10, True], + [ATTR_CONDITION_SUNNY, 0, 12, 6, 0, False], + [ATTR_CONDITION_PARTLYCLOUDY, 2, 14, 7, 20, True], + [ATTR_CONDITION_RAINY, 15, 18, 7, 0, False], + [ATTR_CONDITION_FOG, 0.2, 21, 12, 100, True], + ], + ), + DemoWeather( + "Hourly + bi-daily weather", + "Sunshine", + 21.6414, + 92, + 1099, + 0.5, + UnitOfTemperature.CELSIUS, + UnitOfPressure.HPA, + UnitOfSpeed.METERS_PER_SECOND, + None, + None, + [ + [ATTR_CONDITION_CLOUDY, 1, 22, 15, 60], + [ATTR_CONDITION_CLOUDY, 5, 19, 8, 30], + [ATTR_CONDITION_CLOUDY, 0, 15, 9, 10], + [ATTR_CONDITION_CLOUDY, 0, 12, 6, 0], + [ATTR_CONDITION_CLOUDY, 2, 14, 7, 20], + [ATTR_CONDITION_CLOUDY, 15, 18, 7, 0], + [ATTR_CONDITION_CLOUDY, 0.2, 21, 12, 100], + ], + [ + [ATTR_CONDITION_RAINY, 1, 22, 15, 60, True], + [ATTR_CONDITION_RAINY, 5, 19, 8, 30, False], + [ATTR_CONDITION_CLOUDY, 0, 15, 9, 10, True], + [ATTR_CONDITION_SUNNY, 0, 12, 6, 0, False], + [ATTR_CONDITION_PARTLYCLOUDY, 2, 14, 7, 20, True], + [ATTR_CONDITION_RAINY, 15, 18, 7, 0, False], + [ATTR_CONDITION_FOG, 0.2, 21, 12, 100, True], + ], + ), + DemoWeather( + "Daily + broken bi-daily weather", + "Sunshine", + 21.6414, + 92, + 1099, + 0.5, + UnitOfTemperature.CELSIUS, + UnitOfPressure.HPA, + UnitOfSpeed.METERS_PER_SECOND, + None, + [ + [ATTR_CONDITION_RAINY, 1, 22, 15, 60], + [ATTR_CONDITION_RAINY, 5, 19, 8, 30], + [ATTR_CONDITION_RAINY, 0, 15, 9, 10], + [ATTR_CONDITION_RAINY, 0, 12, 6, 0], + [ATTR_CONDITION_RAINY, 2, 14, 7, 20], + [ATTR_CONDITION_RAINY, 15, 18, 7, 0], + [ATTR_CONDITION_RAINY, 0.2, 21, 12, 100], + ], + None, + [ + [ATTR_CONDITION_RAINY, 1, 22, 15, 60], + [ATTR_CONDITION_RAINY, 5, 19, 8, 30], + [ATTR_CONDITION_CLOUDY, 0, 15, 9, 10], + [ATTR_CONDITION_SUNNY, 0, 12, 6, 0], + [ATTR_CONDITION_PARTLYCLOUDY, 2, 14, 7, 20], + [ATTR_CONDITION_RAINY, 15, 18, 7, 0], + [ATTR_CONDITION_FOG, 0.2, 21, 12, 100], + ], + ), + ] + ) + + +class DemoWeather(WeatherEntity): + """Representation of a weather condition.""" + + _attr_attribution = "Powered by Home Assistant" + _attr_should_poll = False + + def __init__( + self, + name: str, + condition: str, + temperature: float, + humidity: float, + pressure: float, + wind_speed: float, + temperature_unit: str, + pressure_unit: str, + wind_speed_unit: str, + forecast: list[list] | None, + forecast_daily: list[list] | None, + forecast_hourly: list[list] | None, + forecast_twice_daily: list[list] | None, + ) -> None: + """Initialize the Demo weather.""" + self._attr_name = f"Test Weather {name}" + self._attr_unique_id = f"test-weather-{name.lower()}" + self._condition = condition + self._native_temperature = temperature + self._native_temperature_unit = temperature_unit + self._humidity = humidity + self._native_pressure = pressure + self._native_pressure_unit = pressure_unit + self._native_wind_speed = wind_speed + self._native_wind_speed_unit = wind_speed_unit + self._forecast = forecast + self._forecast_daily = forecast_daily + self._forecast_hourly = forecast_hourly + self._forecast_twice_daily = forecast_twice_daily + self._attr_supported_features = 0 + if self._forecast_daily: + self._attr_supported_features |= WeatherEntityFeature.FORECAST_DAILY + if self._forecast_hourly: + self._attr_supported_features |= WeatherEntityFeature.FORECAST_HOURLY + if self._forecast_twice_daily: + self._attr_supported_features |= WeatherEntityFeature.FORECAST_TWICE_DAILY + + async def async_added_to_hass(self) -> None: + """Set up a timer updating the forecasts.""" + + async def update_forecasts(_) -> None: + if self._forecast_daily: + self._forecast_daily = ( + self._forecast_daily[1:] + self._forecast_daily[:1] + ) + if self._forecast_hourly: + self._forecast_hourly = ( + self._forecast_hourly[1:] + self._forecast_hourly[:1] + ) + if self._forecast_twice_daily: + self._forecast_twice_daily = ( + self._forecast_twice_daily[1:] + self._forecast_twice_daily[:1] + ) + await self.async_update_listeners(None) + + self.async_on_remove( + async_track_time_interval( + self.hass, update_forecasts, timedelta(seconds=30) + ) + ) + + @property + def native_temperature(self) -> float: + """Return the temperature.""" + return self._native_temperature + + @property + def native_temperature_unit(self) -> str: + """Return the unit of measurement.""" + return self._native_temperature_unit + + @property + def humidity(self) -> float: + """Return the humidity.""" + return self._humidity + + @property + def native_wind_speed(self) -> float: + """Return the wind speed.""" + return self._native_wind_speed + + @property + def native_wind_speed_unit(self) -> str: + """Return the wind speed.""" + return self._native_wind_speed_unit + + @property + def native_pressure(self) -> float: + """Return the pressure.""" + return self._native_pressure + + @property + def native_pressure_unit(self) -> str: + """Return the pressure.""" + return self._native_pressure_unit + + @property + def condition(self) -> str: + """Return the weather condition.""" + return [ + k for k, v in CONDITION_CLASSES.items() if self._condition.lower() in v + ][0] + + @property + def forecast(self) -> list[Forecast]: + """Return legacy forecast.""" + if self._forecast is None: + return [] + reftime = dt_util.now().replace(hour=16, minute=00) + + forecast_data = [] + for entry in self._forecast: + data_dict = Forecast( + datetime=reftime.isoformat(), + condition=entry[0], + precipitation=entry[1], + temperature=entry[2], + templow=entry[3], + precipitation_probability=entry[4], + ) + reftime = reftime + timedelta(hours=24) + forecast_data.append(data_dict) + + return forecast_data + + async def async_forecast_daily(self) -> list[Forecast]: + """Return the daily forecast.""" + if self._forecast_daily is None: + return [] + reftime = dt_util.now().replace(hour=16, minute=00) + + forecast_data = [] + for entry in self._forecast_daily: + data_dict = Forecast( + datetime=reftime.isoformat(), + condition=entry[0], + precipitation=entry[1], + temperature=entry[2], + templow=entry[3], + precipitation_probability=entry[4], + ) + reftime = reftime + timedelta(hours=24) + forecast_data.append(data_dict) + + return forecast_data + + async def async_forecast_hourly(self) -> list[Forecast]: + """Return the hourly forecast.""" + if self._forecast_hourly is None: + return [] + reftime = dt_util.now().replace(hour=16, minute=00) + + forecast_data = [] + for entry in self._forecast_hourly: + data_dict = Forecast( + datetime=reftime.isoformat(), + condition=entry[0], + precipitation=entry[1], + temperature=entry[2], + templow=entry[3], + precipitation_probability=entry[4], + ) + reftime = reftime + timedelta(hours=1) + forecast_data.append(data_dict) + + return forecast_data + + async def async_forecast_twice_daily(self) -> list[Forecast]: + """Return the twice daily forecast.""" + if self._forecast_twice_daily is None: + return [] + reftime = dt_util.now().replace(hour=11, minute=00) + + forecast_data = [] + for entry in self._forecast_twice_daily: + try: + data_dict = Forecast( + datetime=reftime.isoformat(), + condition=entry[0], + precipitation=entry[1], + temperature=entry[2], + templow=entry[3], + precipitation_probability=entry[4], + is_daytime=entry[5], + ) + reftime = reftime + timedelta(hours=12) + forecast_data.append(data_dict) + except IndexError: + continue + + return forecast_data diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 45b5cbe9fbabad..c63db816711984 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -1,12 +1,13 @@ """Weather component that handles meteorological data for your location.""" from __future__ import annotations +from collections.abc import Callable, Iterable from contextlib import suppress from dataclasses import dataclass from datetime import timedelta import inspect import logging -from typing import Any, Final, TypedDict, final +from typing import Any, Final, Literal, TypedDict, final from typing_extensions import Required @@ -19,7 +20,7 @@ UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -29,7 +30,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM -from .const import ( +from .const import ( # noqa: F401 ATTR_WEATHER_APPARENT_TEMPERATURE, ATTR_WEATHER_CLOUD_COVERAGE, ATTR_WEATHER_DEW_POINT, @@ -50,6 +51,7 @@ DOMAIN, UNIT_CONVERSIONS, VALID_UNITS, + WeatherEntityFeature, ) from .websocket_api import async_setup as async_setup_ws_api @@ -72,6 +74,7 @@ ATTR_CONDITION_WINDY = "windy" ATTR_CONDITION_WINDY_VARIANT = "windy-variant" ATTR_FORECAST = "forecast" +ATTR_FORECAST_IS_DAYTIME: Final = "is_daytime" ATTR_FORECAST_CONDITION: Final = "condition" ATTR_FORECAST_HUMIDITY: Final = "humidity" ATTR_FORECAST_NATIVE_PRECIPITATION: Final = "native_precipitation" @@ -149,6 +152,7 @@ class Forecast(TypedDict, total=False): wind_speed: None native_dew_point: float | None uv_index: float | None + is_daytime: bool | None # Mandatory to use with forecast_twice_daily async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -183,6 +187,8 @@ class WeatherEntity(Entity): entity_description: WeatherEntityDescription _attr_condition: str | None + # _attr_forecast is deprecated, implement async_forecast_daily, + # async_forecast_hourly or async_forecast_twice daily instead _attr_forecast: list[Forecast] | None = None _attr_humidity: float | None = None _attr_ozone: float | None = None @@ -232,6 +238,11 @@ class WeatherEntity(Entity): _attr_native_wind_speed_unit: str | None = None _attr_native_dew_point: float | None = None + _forecast_listeners: dict[ + Literal["daily", "hourly", "twice_daily"], + list[Callable[[list[dict[str, Any]] | None], None]], + ] + _weather_option_temperature_unit: str | None = None _weather_option_pressure_unit: str | None = None _weather_option_visibility_unit: str | None = None @@ -263,6 +274,8 @@ def __init_subclass__(cls, **kwargs: Any) -> None: "visibility_unit", "_attr_precipitation_unit", "precipitation_unit", + "_attr_forecast", + "forecast", ) ): if _reported is False: @@ -291,8 +304,9 @@ def __init_subclass__(cls, **kwargs: Any) -> None: ) async def async_internal_added_to_hass(self) -> None: - """Call when the sensor entity is added to hass.""" + """Call when the weather entity is added to hass.""" await super().async_internal_added_to_hass() + self._forecast_listeners = {"daily": [], "hourly": [], "twice_daily": []} if not self.registry_entry: return self.async_registry_entry_updated() @@ -571,9 +585,24 @@ def _visibility_unit(self) -> str: @property def forecast(self) -> list[Forecast] | None: - """Return the forecast in native units.""" + """Return the forecast in native units. + + Should not be overridden by integrations. Kept for backwards compatibility. + """ return self._attr_forecast + async def async_forecast_daily(self) -> list[Forecast] | None: + """Return the daily forecast in native units.""" + raise NotImplementedError + + async def async_forecast_twice_daily(self) -> list[Forecast] | None: + """Return the daily forecast in native units.""" + raise NotImplementedError + + async def async_forecast_hourly(self) -> list[Forecast] | None: + """Return the hourly forecast in native units.""" + raise NotImplementedError + @property def native_precipitation_unit(self) -> str | None: """Return the native unit of measurement for accumulated precipitation.""" @@ -756,196 +785,196 @@ def state_attributes(self) -> dict[str, Any]: # noqa: C901 data[ATTR_WEATHER_VISIBILITY_UNIT] = self._visibility_unit data[ATTR_WEATHER_PRECIPITATION_UNIT] = self._precipitation_unit - if self.forecast is not None: - forecast: list[dict[str, Any]] = [] - for existing_forecast_entry in self.forecast: - forecast_entry: dict[str, Any] = dict(existing_forecast_entry) + if self.forecast: + data[ATTR_FORECAST] = self._convert_forecast(self.forecast) - temperature = forecast_entry.pop( - ATTR_FORECAST_NATIVE_TEMP, forecast_entry.get(ATTR_FORECAST_TEMP) - ) + return data - from_temp_unit = ( - self.native_temperature_unit or self._default_temperature_unit - ) - to_temp_unit = self._temperature_unit + @final + def _convert_forecast( + self, native_forecast_list: list[Forecast] + ) -> list[dict[str, Any]]: + """Convert a forecast in native units to the unit configured by the user.""" + converted_forecast_list: list[dict[str, Any]] = [] + precision = self.precision - if temperature is None: - forecast_entry[ATTR_FORECAST_TEMP] = None - else: - with suppress(TypeError, ValueError): - temperature_f = float(temperature) - value_temp = UNIT_CONVERSIONS[ATTR_WEATHER_TEMPERATURE_UNIT]( - temperature_f, - from_temp_unit, - to_temp_unit, - ) - forecast_entry[ATTR_FORECAST_TEMP] = round_temperature( - value_temp, precision - ) + from_temp_unit = self.native_temperature_unit or self._default_temperature_unit + to_temp_unit = self._temperature_unit - if ( - forecast_apparent_temp := forecast_entry.pop( - ATTR_FORECAST_NATIVE_APPARENT_TEMP, - forecast_entry.get(ATTR_FORECAST_NATIVE_APPARENT_TEMP), - ) - ) is not None: - with suppress(TypeError, ValueError): - forecast_apparent_temp = float(forecast_apparent_temp) - value_apparent_temp = UNIT_CONVERSIONS[ - ATTR_WEATHER_TEMPERATURE_UNIT - ]( - forecast_apparent_temp, - from_temp_unit, - to_temp_unit, - ) + for _forecast_entry in native_forecast_list: + forecast_entry: dict[str, Any] = dict(_forecast_entry) - forecast_entry[ATTR_FORECAST_APPARENT_TEMP] = round_temperature( - value_apparent_temp, precision - ) + temperature = forecast_entry.pop( + ATTR_FORECAST_NATIVE_TEMP, forecast_entry.get(ATTR_FORECAST_TEMP) + ) - if ( - forecast_temp_low := forecast_entry.pop( - ATTR_FORECAST_NATIVE_TEMP_LOW, - forecast_entry.get(ATTR_FORECAST_TEMP_LOW), + if temperature is None: + forecast_entry[ATTR_FORECAST_TEMP] = None + else: + with suppress(TypeError, ValueError): + temperature_f = float(temperature) + value_temp = UNIT_CONVERSIONS[ATTR_WEATHER_TEMPERATURE_UNIT]( + temperature_f, + from_temp_unit, + to_temp_unit, ) - ) is not None: - with suppress(TypeError, ValueError): - forecast_temp_low_f = float(forecast_temp_low) - value_temp_low = UNIT_CONVERSIONS[ - ATTR_WEATHER_TEMPERATURE_UNIT - ]( - forecast_temp_low_f, - from_temp_unit, - to_temp_unit, - ) - - forecast_entry[ATTR_FORECAST_TEMP_LOW] = round_temperature( - value_temp_low, precision - ) - - if ( - forecast_dew_point := forecast_entry.pop( - ATTR_FORECAST_NATIVE_DEW_POINT, - None, + forecast_entry[ATTR_FORECAST_TEMP] = round_temperature( + value_temp, precision ) - ) is not None: - with suppress(TypeError, ValueError): - forecast_dew_point_f = float(forecast_dew_point) - value_dew_point = UNIT_CONVERSIONS[ - ATTR_WEATHER_TEMPERATURE_UNIT - ]( - forecast_dew_point_f, - from_temp_unit, - to_temp_unit, - ) - forecast_entry[ATTR_FORECAST_DEW_POINT] = round_temperature( - value_dew_point, precision - ) + if ( + forecast_apparent_temp := forecast_entry.pop( + ATTR_FORECAST_NATIVE_APPARENT_TEMP, + forecast_entry.get(ATTR_FORECAST_NATIVE_APPARENT_TEMP), + ) + ) is not None: + with suppress(TypeError, ValueError): + forecast_apparent_temp = float(forecast_apparent_temp) + value_apparent_temp = UNIT_CONVERSIONS[ + ATTR_WEATHER_TEMPERATURE_UNIT + ]( + forecast_apparent_temp, + from_temp_unit, + to_temp_unit, + ) - if ( - forecast_pressure := forecast_entry.pop( - ATTR_FORECAST_NATIVE_PRESSURE, - forecast_entry.get(ATTR_FORECAST_PRESSURE), + forecast_entry[ATTR_FORECAST_APPARENT_TEMP] = round_temperature( + value_apparent_temp, precision ) - ) is not None: - from_pressure_unit = ( - self.native_pressure_unit or self._default_pressure_unit + + if ( + forecast_temp_low := forecast_entry.pop( + ATTR_FORECAST_NATIVE_TEMP_LOW, + forecast_entry.get(ATTR_FORECAST_TEMP_LOW), + ) + ) is not None: + with suppress(TypeError, ValueError): + forecast_temp_low_f = float(forecast_temp_low) + value_temp_low = UNIT_CONVERSIONS[ATTR_WEATHER_TEMPERATURE_UNIT]( + forecast_temp_low_f, + from_temp_unit, + to_temp_unit, ) - to_pressure_unit = self._pressure_unit - with suppress(TypeError, ValueError): - forecast_pressure_f = float(forecast_pressure) - forecast_entry[ATTR_FORECAST_PRESSURE] = round( - UNIT_CONVERSIONS[ATTR_WEATHER_PRESSURE_UNIT]( - forecast_pressure_f, - from_pressure_unit, - to_pressure_unit, - ), - ROUNDING_PRECISION, - ) - if ( - forecast_wind_gust_speed := forecast_entry.pop( - ATTR_FORECAST_NATIVE_WIND_GUST_SPEED, - None, + forecast_entry[ATTR_FORECAST_TEMP_LOW] = round_temperature( + value_temp_low, precision ) - ) is not None: - from_wind_speed_unit = ( - self.native_wind_speed_unit or self._default_wind_speed_unit + + if ( + forecast_dew_point := forecast_entry.pop( + ATTR_FORECAST_NATIVE_DEW_POINT, + None, + ) + ) is not None: + with suppress(TypeError, ValueError): + forecast_dew_point_f = float(forecast_dew_point) + value_dew_point = UNIT_CONVERSIONS[ATTR_WEATHER_TEMPERATURE_UNIT]( + forecast_dew_point_f, + from_temp_unit, + to_temp_unit, ) - to_wind_speed_unit = self._wind_speed_unit - with suppress(TypeError, ValueError): - forecast_wind_gust_speed_f = float(forecast_wind_gust_speed) - forecast_entry[ATTR_FORECAST_WIND_GUST_SPEED] = round( - UNIT_CONVERSIONS[ATTR_WEATHER_WIND_SPEED_UNIT]( - forecast_wind_gust_speed_f, - from_wind_speed_unit, - to_wind_speed_unit, - ), - ROUNDING_PRECISION, - ) - if ( - forecast_wind_speed := forecast_entry.pop( - ATTR_FORECAST_NATIVE_WIND_SPEED, - forecast_entry.get(ATTR_FORECAST_WIND_SPEED), + forecast_entry[ATTR_FORECAST_DEW_POINT] = round_temperature( + value_dew_point, precision ) - ) is not None: - from_wind_speed_unit = ( - self.native_wind_speed_unit or self._default_wind_speed_unit + + if ( + forecast_pressure := forecast_entry.pop( + ATTR_FORECAST_NATIVE_PRESSURE, + forecast_entry.get(ATTR_FORECAST_PRESSURE), + ) + ) is not None: + from_pressure_unit = ( + self.native_pressure_unit or self._default_pressure_unit + ) + to_pressure_unit = self._pressure_unit + with suppress(TypeError, ValueError): + forecast_pressure_f = float(forecast_pressure) + forecast_entry[ATTR_FORECAST_PRESSURE] = round( + UNIT_CONVERSIONS[ATTR_WEATHER_PRESSURE_UNIT]( + forecast_pressure_f, + from_pressure_unit, + to_pressure_unit, + ), + ROUNDING_PRECISION, ) - to_wind_speed_unit = self._wind_speed_unit - with suppress(TypeError, ValueError): - forecast_wind_speed_f = float(forecast_wind_speed) - forecast_entry[ATTR_FORECAST_WIND_SPEED] = round( - UNIT_CONVERSIONS[ATTR_WEATHER_WIND_SPEED_UNIT]( - forecast_wind_speed_f, - from_wind_speed_unit, - to_wind_speed_unit, - ), - ROUNDING_PRECISION, - ) - if ( - forecast_precipitation := forecast_entry.pop( - ATTR_FORECAST_NATIVE_PRECIPITATION, - forecast_entry.get(ATTR_FORECAST_PRECIPITATION), + if ( + forecast_wind_gust_speed := forecast_entry.pop( + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED, + None, + ) + ) is not None: + from_wind_speed_unit = ( + self.native_wind_speed_unit or self._default_wind_speed_unit + ) + to_wind_speed_unit = self._wind_speed_unit + with suppress(TypeError, ValueError): + forecast_wind_gust_speed_f = float(forecast_wind_gust_speed) + forecast_entry[ATTR_FORECAST_WIND_GUST_SPEED] = round( + UNIT_CONVERSIONS[ATTR_WEATHER_WIND_SPEED_UNIT]( + forecast_wind_gust_speed_f, + from_wind_speed_unit, + to_wind_speed_unit, + ), + ROUNDING_PRECISION, ) - ) is not None: - from_precipitation_unit = ( - self.native_precipitation_unit - or self._default_precipitation_unit + + if ( + forecast_wind_speed := forecast_entry.pop( + ATTR_FORECAST_NATIVE_WIND_SPEED, + forecast_entry.get(ATTR_FORECAST_WIND_SPEED), + ) + ) is not None: + from_wind_speed_unit = ( + self.native_wind_speed_unit or self._default_wind_speed_unit + ) + to_wind_speed_unit = self._wind_speed_unit + with suppress(TypeError, ValueError): + forecast_wind_speed_f = float(forecast_wind_speed) + forecast_entry[ATTR_FORECAST_WIND_SPEED] = round( + UNIT_CONVERSIONS[ATTR_WEATHER_WIND_SPEED_UNIT]( + forecast_wind_speed_f, + from_wind_speed_unit, + to_wind_speed_unit, + ), + ROUNDING_PRECISION, ) - to_precipitation_unit = self._precipitation_unit - with suppress(TypeError, ValueError): - forecast_precipitation_f = float(forecast_precipitation) - forecast_entry[ATTR_FORECAST_PRECIPITATION] = round( - UNIT_CONVERSIONS[ATTR_WEATHER_PRECIPITATION_UNIT]( - forecast_precipitation_f, - from_precipitation_unit, - to_precipitation_unit, - ), - ROUNDING_PRECISION, - ) - if ( - forecast_humidity := forecast_entry.pop( - ATTR_FORECAST_HUMIDITY, - None, + if ( + forecast_precipitation := forecast_entry.pop( + ATTR_FORECAST_NATIVE_PRECIPITATION, + forecast_entry.get(ATTR_FORECAST_PRECIPITATION), + ) + ) is not None: + from_precipitation_unit = ( + self.native_precipitation_unit or self._default_precipitation_unit + ) + to_precipitation_unit = self._precipitation_unit + with suppress(TypeError, ValueError): + forecast_precipitation_f = float(forecast_precipitation) + forecast_entry[ATTR_FORECAST_PRECIPITATION] = round( + UNIT_CONVERSIONS[ATTR_WEATHER_PRECIPITATION_UNIT]( + forecast_precipitation_f, + from_precipitation_unit, + to_precipitation_unit, + ), + ROUNDING_PRECISION, ) - ) is not None: - with suppress(TypeError, ValueError): - forecast_humidity_f = float(forecast_humidity) - forecast_entry[ATTR_FORECAST_HUMIDITY] = round( - forecast_humidity_f - ) - forecast.append(forecast_entry) + if ( + forecast_humidity := forecast_entry.pop( + ATTR_FORECAST_HUMIDITY, + None, + ) + ) is not None: + with suppress(TypeError, ValueError): + forecast_humidity_f = float(forecast_humidity) + forecast_entry[ATTR_FORECAST_HUMIDITY] = round(forecast_humidity_f) - data[ATTR_FORECAST] = forecast + converted_forecast_list.append(forecast_entry) - return data + return converted_forecast_list @property @final @@ -998,3 +1027,53 @@ def async_registry_entry_updated(self) -> None: ) ) and custom_unit_visibility in VALID_UNITS[ATTR_WEATHER_VISIBILITY_UNIT]: self._weather_option_visibility_unit = custom_unit_visibility + + @final + @callback + def async_subscribe_forecast( + self, + forecast_type: Literal["daily", "hourly", "twice_daily"], + forecast_listener: Callable[[list[dict[str, Any]] | None], None], + ) -> CALLBACK_TYPE: + """Subscribe to forecast updates. + + Called by websocket API. + """ + self._forecast_listeners[forecast_type].append(forecast_listener) + + @callback + def unsubscribe() -> None: + self._forecast_listeners[forecast_type].remove(forecast_listener) + + return unsubscribe + + @final + async def async_update_listeners( + self, forecast_types: Iterable[Literal["daily", "hourly", "twice_daily"]] | None + ) -> None: + """Push updated forecast to all listeners.""" + if forecast_types is None: + forecast_types = {"daily", "hourly", "twice_daily"} + for forecast_type in forecast_types: + if not self._forecast_listeners[forecast_type]: + continue + + native_forecast_list: list[Forecast] | None = await getattr( + self, f"async_forecast_{forecast_type}" + )() + + if native_forecast_list is None: + for listener in self._forecast_listeners[forecast_type]: + listener(None) + continue + + if forecast_type == "twice_daily": + for fc_twice_daily in native_forecast_list: + if fc_twice_daily.get(ATTR_FORECAST_IS_DAYTIME) is None: + raise ValueError( + "is_daytime mandatory attribute for forecast_twice_daily is missing" + ) + + converted_forecast_list = self._convert_forecast(native_forecast_list) + for listener in self._forecast_listeners[forecast_type]: + listener(converted_forecast_list) diff --git a/homeassistant/components/weather/const.py b/homeassistant/components/weather/const.py index 759021741ffce7..c6da2c28c71ffb 100644 --- a/homeassistant/components/weather/const.py +++ b/homeassistant/components/weather/const.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable +from enum import IntFlag from typing import Final from homeassistant.const import ( @@ -18,6 +19,15 @@ TemperatureConverter, ) + +class WeatherEntityFeature(IntFlag): + """Supported features of the update entity.""" + + FORECAST_DAILY = 1 + FORECAST_HOURLY = 2 + FORECAST_TWICE_DAILY = 4 + + ATTR_WEATHER_HUMIDITY = "humidity" ATTR_WEATHER_OZONE = "ozone" ATTR_WEATHER_DEW_POINT = "dew_point" diff --git a/homeassistant/components/weather/websocket_api.py b/homeassistant/components/weather/websocket_api.py index 51f129fc4a2ca6..f2be4dfec6daee 100644 --- a/homeassistant/components/weather/websocket_api.py +++ b/homeassistant/components/weather/websocket_api.py @@ -1,20 +1,29 @@ """The weather websocket API.""" from __future__ import annotations -from typing import Any +from typing import Any, Literal import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_component import EntityComponent -from .const import VALID_UNITS +from .const import DOMAIN, VALID_UNITS, WeatherEntityFeature + +FORECAST_TYPE_TO_FLAG = { + "daily": WeatherEntityFeature.FORECAST_DAILY, + "hourly": WeatherEntityFeature.FORECAST_HOURLY, + "twice_daily": WeatherEntityFeature.FORECAST_TWICE_DAILY, +} @callback def async_setup(hass: HomeAssistant) -> None: """Set up the weather websocket API.""" websocket_api.async_register_command(hass, ws_convertible_units) + websocket_api.async_register_command(hass, ws_subscribe_forecast) @callback @@ -31,3 +40,62 @@ def ws_convertible_units( key: sorted(units, key=str.casefold) for key, units in VALID_UNITS.items() } connection.send_result(msg["id"], {"units": sorted_units}) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "weather/subscribe_forecast", + vol.Required("entity_id"): cv.entity_domain(DOMAIN), + vol.Required("forecast_type"): vol.In(["daily", "hourly", "twice_daily"]), + } +) +@websocket_api.async_response +async def ws_subscribe_forecast( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: + """Subscribe to weather forecasts.""" + from . import WeatherEntity # pylint: disable=import-outside-toplevel + + component: EntityComponent[WeatherEntity] = hass.data[DOMAIN] + entity_id: str = msg["entity_id"] + forecast_type: Literal["daily", "hourly", "twice_daily"] = msg["forecast_type"] + + if not (entity := component.get_entity(msg["entity_id"])): + connection.send_error( + msg["id"], + "invalid_entity_id", + f"Weather entity not found: {entity_id}", + ) + return + + if ( + entity.supported_features is None + or not entity.supported_features & FORECAST_TYPE_TO_FLAG[forecast_type] + ): + connection.send_error( + msg["id"], + "forecast_not_supported", + f"The weather entity does not support forecast type: {forecast_type}", + ) + return + + @callback + def forecast_listener(forecast: list[dict[str, Any]] | None) -> None: + """Push a new forecast to websocket.""" + connection.send_message( + websocket_api.event_message( + msg["id"], + { + "type": forecast_type, + "forecast": forecast, + }, + ) + ) + + connection.subscriptions[msg["id"]] = entity.async_subscribe_forecast( + forecast_type, forecast_listener + ) + connection.send_message(websocket_api.result_message(msg["id"])) + + # Push an initial forecast update + await entity.async_update_listeners({forecast_type}) diff --git a/tests/components/demo/test_weather.py b/tests/components/demo/test_weather.py index b2b789a084f448..ced801a4d464b5 100644 --- a/tests/components/demo/test_weather.py +++ b/tests/components/demo/test_weather.py @@ -1,12 +1,13 @@ """The tests for the demo weather component.""" +import datetime +from typing import Any + +from freezegun.api import FrozenDateTimeFactory +import pytest + from homeassistant.components import weather +from homeassistant.components.demo.weather import WEATHER_UPDATE_INTERVAL from homeassistant.components.weather import ( - ATTR_FORECAST, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_OZONE, ATTR_WEATHER_PRESSURE, @@ -19,6 +20,8 @@ from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import METRIC_SYSTEM +from tests.typing import WebSocketGenerator + async def test_attributes(hass: HomeAssistant, disable_platforms) -> None: """Test weather attributes.""" @@ -41,16 +44,120 @@ async def test_attributes(hass: HomeAssistant, disable_platforms) -> None: assert data.get(ATTR_WEATHER_WIND_BEARING) is None assert data.get(ATTR_WEATHER_OZONE) is None assert data.get(ATTR_ATTRIBUTION) == "Powered by Home Assistant" - assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_CONDITION) == "rainy" - assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_PRECIPITATION) == 1 - assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 60 - assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_TEMP) == 22 - assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_TEMP_LOW) == 15 - assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_CONDITION) == "fog" - assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_PRECIPITATION) == 0.2 - assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_TEMP) == 21 - assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_TEMP_LOW) == 12 - assert ( - data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 100 + + +TEST_TIME_ADVANCE_INTERVAL = datetime.timedelta(seconds=5 + 1) + + +@pytest.mark.parametrize( + ("forecast_type", "expected_forecast"), + [ + ( + "daily", + [ + { + "condition": "snowy", + "precipitation": 2.0, + "temperature": -23.3, + "templow": -26.1, + "precipitation_probability": 60, + }, + { + "condition": "sunny", + "precipitation": 0.0, + "temperature": -22.8, + "templow": -24.4, + "precipitation_probability": 0, + }, + ], + ), + ( + "hourly", + [ + { + "condition": "sunny", + "precipitation": 2.0, + "temperature": -23.3, + "templow": -26.1, + "precipitation_probability": 60, + }, + { + "condition": "sunny", + "precipitation": 0.0, + "temperature": -22.8, + "templow": -24.4, + "precipitation_probability": 0, + }, + ], + ), + ( + "twice_daily", + [ + { + "condition": "snowy", + "precipitation": 2.0, + "temperature": -23.3, + "templow": -26.1, + "precipitation_probability": 60, + }, + { + "condition": "sunny", + "precipitation": 0.0, + "temperature": -22.8, + "templow": -24.4, + "precipitation_probability": 0, + }, + ], + ), + ], +) +async def test_forecast( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, + disable_platforms: None, + forecast_type: str, + expected_forecast: list[dict[str, Any]], +) -> None: + """Test multiple forecast.""" + assert await async_setup_component( + hass, weather.DOMAIN, {"weather": {"platform": "demo"}} ) - assert len(data.get(ATTR_FORECAST)) == 7 + hass.config.units = METRIC_SYSTEM + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "weather/subscribe_forecast", + "forecast_type": forecast_type, + "entity_id": "weather.demo_weather_north", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + subscription_id = msg["id"] + + msg = await client.receive_json() + assert msg["id"] == subscription_id + assert msg["type"] == "event" + forecast1 = msg["event"]["forecast"] + + assert len(forecast1) == 7 + for key, val in expected_forecast[0].items(): + assert forecast1[0][key] == val + for key, val in expected_forecast[1].items(): + assert forecast1[6][key] == val + + freezer.tick(WEATHER_UPDATE_INTERVAL + datetime.timedelta(seconds=1)) + await hass.async_block_till_done() + + msg = await client.receive_json() + assert msg["id"] == subscription_id + assert msg["type"] == "event" + forecast2 = msg["event"]["forecast"] + + assert forecast2 != forecast1 + assert len(forecast2) == 7 diff --git a/tests/components/weather/__init__.py b/tests/components/weather/__init__.py index 24df7abb1f36cc..91097dfae14bc8 100644 --- a/tests/components/weather/__init__.py +++ b/tests/components/weather/__init__.py @@ -1 +1,32 @@ """The tests for Weather platforms.""" + + +from homeassistant.components.weather import ATTR_CONDITION_SUNNY +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.testing_config.custom_components.test import weather as WeatherPlatform + + +async def create_entity(hass: HomeAssistant, **kwargs): + """Create the weather entity to run tests on.""" + kwargs = { + "native_temperature": None, + "native_temperature_unit": None, + "is_daytime": True, + **kwargs, + } + platform: WeatherPlatform = getattr(hass.components, "test.weather") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockWeatherMockForecast( + name="Test", condition=ATTR_CONDITION_SUNNY, **kwargs + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component( + hass, "weather", {"weather": {"platform": "test"}} + ) + await hass.async_block_till_done() + return entity0 diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 53753ad4a72596..92643b616c911c 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -34,6 +34,7 @@ ROUNDING_PRECISION, Forecast, WeatherEntity, + WeatherEntityFeature, round_temperature, ) from homeassistant.components.weather.const import ( @@ -54,6 +55,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( DistanceConverter, PressureConverter, @@ -62,7 +64,10 @@ ) from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM +from . import create_entity + from tests.testing_config.custom_components.test import weather as WeatherPlatform +from tests.typing import WebSocketGenerator class MockWeatherEntity(WeatherEntity): @@ -86,12 +91,19 @@ def __init__(self) -> None: self._attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND self._attr_forecast = [ Forecast( - datetime=datetime(2022, 6, 20, 20, 00, 00), + datetime=datetime(2022, 6, 20, 00, 00, 00, tzinfo=dt_util.UTC), native_precipitation=1, native_temperature=20, native_dew_point=2, ) ] + self._attr_forecast_twice_daily = [ + Forecast( + datetime=datetime(2022, 6, 20, 8, 00, 00, tzinfo=dt_util.UTC), + native_precipitation=10, + native_temperature=25, + ) + ] class MockWeatherEntityPrecision(WeatherEntity): @@ -126,32 +138,13 @@ def __init__(self) -> None: self._attr_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND self._attr_forecast = [ Forecast( - datetime=datetime(2022, 6, 20, 20, 00, 00), + datetime=datetime(2022, 6, 20, 0, 00, 00, tzinfo=dt_util.UTC), precipitation=1, temperature=20, ) ] -async def create_entity(hass: HomeAssistant, **kwargs): - """Create the weather entity to run tests on.""" - kwargs = {"native_temperature": None, "native_temperature_unit": None, **kwargs} - platform: WeatherPlatform = getattr(hass.components, "test.weather") - platform.init(empty=True) - platform.ENTITIES.append( - platform.MockWeatherMockForecast( - name="Test", condition=ATTR_CONDITION_SUNNY, **kwargs - ) - ) - - entity0 = platform.ENTITIES[0] - assert await async_setup_component( - hass, "weather", {"weather": {"platform": "test"}} - ) - await hass.async_block_till_done() - return entity0 - - @pytest.mark.parametrize( "native_unit", (UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS) ) @@ -192,7 +185,7 @@ async def test_temperature( ) state = hass.states.get(entity0.entity_id) - forecast = state.attributes[ATTR_FORECAST][0] + forecast_daily = state.attributes[ATTR_FORECAST][0] expected = state_value apparent_expected = apparent_state_value @@ -207,14 +200,20 @@ async def test_temperature( dew_point_expected, rel=0.1 ) assert state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == state_unit - assert float(forecast[ATTR_FORECAST_TEMP]) == pytest.approx(expected, rel=0.1) - assert float(forecast[ATTR_FORECAST_APPARENT_TEMP]) == pytest.approx( + assert float(forecast_daily[ATTR_FORECAST_TEMP]) == pytest.approx(expected, rel=0.1) + assert float(forecast_daily[ATTR_FORECAST_APPARENT_TEMP]) == pytest.approx( apparent_expected, rel=0.1 ) - assert float(forecast[ATTR_FORECAST_DEW_POINT]) == pytest.approx( + assert float(forecast_daily[ATTR_FORECAST_DEW_POINT]) == pytest.approx( dew_point_expected, rel=0.1 ) - assert float(forecast[ATTR_FORECAST_TEMP_LOW]) == pytest.approx(expected, rel=0.1) + assert float(forecast_daily[ATTR_FORECAST_TEMP_LOW]) == pytest.approx( + expected, rel=0.1 + ) + assert float(forecast_daily[ATTR_FORECAST_TEMP]) == pytest.approx(expected, rel=0.1) + assert float(forecast_daily[ATTR_FORECAST_TEMP_LOW]) == pytest.approx( + expected, rel=0.1 + ) @pytest.mark.parametrize("native_unit", (None,)) @@ -695,6 +694,7 @@ async def test_custom_units( native_visibility_unit=visibility_unit, native_precipitation=precipitation_value, native_precipitation_unit=precipitation_unit, + is_daytime=True, unique_id="very_unique", ) ) @@ -1031,7 +1031,7 @@ async def test_attr_compatibility(hass: HomeAssistant) -> None: forecast_entry = [ Forecast( - datetime=datetime(2022, 6, 20, 20, 00, 00), + datetime=datetime(2022, 6, 20, 0, 00, 00, tzinfo=dt_util.UTC), precipitation=1, temperature=20, ) @@ -1067,3 +1067,39 @@ async def test_precision_for_temperature(hass: HomeAssistant) -> None: assert weather.state_attributes[ATTR_WEATHER_TEMPERATURE] == 20.5 assert weather.state_attributes[ATTR_WEATHER_DEW_POINT] == 2.5 + + +async def test_forecast_twice_daily_missing_is_daytime( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + enable_custom_integrations: None, +) -> None: + """Test forecast_twice_daily missing mandatory attribute is_daytime.""" + + entity0 = await create_entity( + hass, + native_temperature=38, + native_temperature_unit=UnitOfTemperature.CELSIUS, + is_daytime=None, + supported_features=WeatherEntityFeature.FORECAST_TWICE_DAILY, + ) + + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "weather/subscribe_forecast", + "forecast_type": "twice_daily", + "entity_id": entity0.entity_id, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + subscription_id = msg["id"] + + msg = await client.receive_json() + assert msg["id"] == subscription_id + assert msg["error"] == {"code": "unknown_error", "message": "Unknown error"} + assert not msg["success"] + assert msg["type"] == "result" diff --git a/tests/components/weather/test_recorder.py b/tests/components/weather/test_recorder.py index 5d7928124dd943..2864abf58bbfc4 100644 --- a/tests/components/weather/test_recorder.py +++ b/tests/components/weather/test_recorder.py @@ -5,7 +5,11 @@ from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states -from homeassistant.components.weather import ATTR_FORECAST, DOMAIN +from homeassistant.components.weather import ( + ATTR_CONDITION_SUNNY, + ATTR_FORECAST, +) +from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -13,17 +17,47 @@ from tests.common import async_fire_time_changed from tests.components.recorder.common import async_wait_recording_done +from tests.testing_config.custom_components.test import weather as WeatherPlatform -async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) -> None: +async def create_entity(hass: HomeAssistant, **kwargs): + """Create the weather entity to run tests on.""" + kwargs = { + "native_temperature": None, + "native_temperature_unit": None, + "is_daytime": True, + **kwargs, + } + platform: WeatherPlatform = getattr(hass.components, "test.weather") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockWeatherMockForecast( + name="Test", condition=ATTR_CONDITION_SUNNY, **kwargs + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component( + hass, "weather", {"weather": {"platform": "test"}} + ) + await hass.async_block_till_done() + return entity0 + + +async def test_exclude_attributes( + recorder_mock: Recorder, hass: HomeAssistant, enable_custom_integrations: None +) -> None: """Test weather attributes to be excluded.""" now = dt_util.utcnow() - await async_setup_component(hass, "homeassistant", {}) - await async_setup_component(hass, DOMAIN, {DOMAIN: {"platform": "demo"}}) + entity0 = await create_entity( + hass, + native_temperature=38, + native_temperature_unit=UnitOfTemperature.CELSIUS, + ) hass.config.units = METRIC_SYSTEM await hass.async_block_till_done() - state = hass.states.get("weather.demo_weather_south") + state = hass.states.get(entity0.entity_id) assert state.attributes[ATTR_FORECAST] await hass.async_block_till_done() diff --git a/tests/components/weather/test_websocket_api.py b/tests/components/weather/test_websocket_api.py index 760acbb2bb0bb8..4f5223c6f79b3b 100644 --- a/tests/components/weather/test_websocket_api.py +++ b/tests/components/weather/test_websocket_api.py @@ -1,8 +1,12 @@ """Test the weather websocket API.""" +from homeassistant.components.weather import WeatherEntityFeature from homeassistant.components.weather.const import DOMAIN +from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from . import create_entity + from tests.typing import WebSocketGenerator @@ -31,3 +35,118 @@ async def test_device_class_units( "wind_speed_unit": ["ft/s", "km/h", "kn", "m/s", "mph"], } } + + +async def test_subscribe_forecast( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + enable_custom_integrations: None, +) -> None: + """Test multiple forecast.""" + + entity0 = await create_entity( + hass, + native_temperature=38, + native_temperature_unit=UnitOfTemperature.CELSIUS, + supported_features=WeatherEntityFeature.FORECAST_DAILY, + ) + + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "weather/subscribe_forecast", + "forecast_type": "daily", + "entity_id": entity0.entity_id, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + subscription_id = msg["id"] + + msg = await client.receive_json() + assert msg["id"] == subscription_id + assert msg["type"] == "event" + forecast = msg["event"] + assert forecast == { + "type": "daily", + "forecast": [ + { + "cloud_coverage": None, + "temperature": 38.0, + "templow": 38.0, + "uv_index": None, + "wind_bearing": None, + } + ], + } + + await entity0.async_update_listeners(None) + msg = await client.receive_json() + assert msg["event"] == forecast + + await entity0.async_update_listeners(["daily"]) + msg = await client.receive_json() + assert msg["event"] == forecast + + entity0.forecast_list = None + await entity0.async_update_listeners(None) + msg = await client.receive_json() + assert msg["event"] == {"type": "daily", "forecast": None} + + +async def test_subscribe_forecast_unknown_entity( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + enable_custom_integrations: None, +) -> None: + """Test multiple forecast.""" + + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "weather/subscribe_forecast", + "forecast_type": "daily", + "entity_id": "weather.unknown", + } + ) + msg = await client.receive_json() + assert not msg["success"] + assert msg["error"] == { + "code": "invalid_entity_id", + "message": "Weather entity not found: weather.unknown", + } + + +async def test_subscribe_forecast_unsupported( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + enable_custom_integrations: None, +) -> None: + """Test multiple forecast.""" + + entity0 = await create_entity( + hass, + native_temperature=38, + native_temperature_unit=UnitOfTemperature.CELSIUS, + ) + + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "weather/subscribe_forecast", + "forecast_type": "daily", + "entity_id": entity0.entity_id, + } + ) + msg = await client.receive_json() + assert not msg["success"] + assert msg["error"] == { + "code": "forecast_not_supported", + "message": "The weather entity does not support forecast type: daily", + } diff --git a/tests/testing_config/custom_components/test/weather.py b/tests/testing_config/custom_components/test/weather.py index df6a43ad40caf6..e2d026ec8401bc 100644 --- a/tests/testing_config/custom_components/test/weather.py +++ b/tests/testing_config/custom_components/test/weather.py @@ -4,9 +4,12 @@ """ from __future__ import annotations +from typing import Any + from homeassistant.components.weather import ( ATTR_FORECAST_CLOUD_COVERAGE, ATTR_FORECAST_HUMIDITY, + ATTR_FORECAST_IS_DAYTIME, ATTR_FORECAST_NATIVE_APPARENT_TEMP, ATTR_FORECAST_NATIVE_DEW_POINT, ATTR_FORECAST_NATIVE_PRECIPITATION, @@ -220,9 +223,61 @@ def condition(self) -> str | None: class MockWeatherMockForecast(MockWeather): """Mock weather class with mocked forecast.""" + def __init__(self, **values: Any) -> None: + """Initialize.""" + super().__init__(**values) + self.forecast_list: list[Forecast] | None = [ + { + ATTR_FORECAST_NATIVE_TEMP: self.native_temperature, + ATTR_FORECAST_NATIVE_APPARENT_TEMP: self.native_apparent_temperature, + ATTR_FORECAST_NATIVE_TEMP_LOW: self.native_temperature, + ATTR_FORECAST_NATIVE_DEW_POINT: self.native_dew_point, + ATTR_FORECAST_CLOUD_COVERAGE: self.cloud_coverage, + ATTR_FORECAST_NATIVE_PRESSURE: self.native_pressure, + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: self.native_wind_gust_speed, + ATTR_FORECAST_NATIVE_WIND_SPEED: self.native_wind_speed, + ATTR_FORECAST_WIND_BEARING: self.wind_bearing, + ATTR_FORECAST_UV_INDEX: self.uv_index, + ATTR_FORECAST_NATIVE_PRECIPITATION: self._values.get( + "native_precipitation" + ), + ATTR_FORECAST_HUMIDITY: self.humidity, + } + ] + @property def forecast(self) -> list[Forecast] | None: """Return the forecast.""" + return self.forecast_list + + async def async_forecast_daily(self) -> list[Forecast] | None: + """Return the forecast_daily.""" + return self.forecast_list + + async def async_forecast_twice_daily(self) -> list[Forecast] | None: + """Return the forecast_twice_daily.""" + return [ + { + ATTR_FORECAST_NATIVE_TEMP: self.native_temperature, + ATTR_FORECAST_NATIVE_APPARENT_TEMP: self.native_apparent_temperature, + ATTR_FORECAST_NATIVE_TEMP_LOW: self.native_temperature, + ATTR_FORECAST_NATIVE_DEW_POINT: self.native_dew_point, + ATTR_FORECAST_CLOUD_COVERAGE: self.cloud_coverage, + ATTR_FORECAST_NATIVE_PRESSURE: self.native_pressure, + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: self.native_wind_gust_speed, + ATTR_FORECAST_NATIVE_WIND_SPEED: self.native_wind_speed, + ATTR_FORECAST_WIND_BEARING: self.wind_bearing, + ATTR_FORECAST_UV_INDEX: self.uv_index, + ATTR_FORECAST_NATIVE_PRECIPITATION: self._values.get( + "native_precipitation" + ), + ATTR_FORECAST_HUMIDITY: self.humidity, + ATTR_FORECAST_IS_DAYTIME: self._values.get("is_daytime"), + } + ] + + async def async_forecast_hourly(self) -> list[Forecast] | None: + """Return the forecast_hourly.""" return [ { ATTR_FORECAST_NATIVE_TEMP: self.native_temperature, From 0b0f072faf5a08b6ab5e4543419e972b7d125a43 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Jul 2023 12:05:46 -0500 Subject: [PATCH 0752/1009] Bump aioesphomeapi to 15.1.14 (#97019) changelog: https://github.com/esphome/aioesphomeapi/compare/v15.1.13...v15.1.14 --- 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 6ecdf0fddbdfa8..1caf0e01efcc57 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.1.13", + "aioesphomeapi==15.1.14", "bluetooth-data-tools==1.6.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index f2345630dbbf62..3fb2ff29f9a8e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==15.1.13 +aioesphomeapi==15.1.14 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21bdcf31527706..b99ba4b372cf0d 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.1.13 +aioesphomeapi==15.1.14 # homeassistant.components.flo aioflo==2021.11.0 From 432ac1f3131cd235a283dd9fa40f66502fe98ec9 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 21 Jul 2023 19:07:49 +0200 Subject: [PATCH 0753/1009] Update pytest-sugar to 0.9.7 (#97001) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index f2901d885571d2..3bc592e98eb2c5 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -23,7 +23,7 @@ pytest-cov==4.1.0 pytest-freezer==0.4.8 pytest-socket==0.6.0 pytest-test-groups==1.0.3 -pytest-sugar==0.9.6 +pytest-sugar==0.9.7 pytest-timeout==2.1.0 pytest-unordered==0.5.2 pytest-picked==0.4.6 From cd89f660d400ecd18681f459ac9d1e9b859b8a07 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 21 Jul 2023 19:08:05 +0200 Subject: [PATCH 0754/1009] Update pytest-asyncio to 0.21.0 (#96999) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 3bc592e98eb2c5..fbe3fa1a66c2c1 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -17,7 +17,7 @@ pydantic==1.10.11 pylint==2.17.4 pylint-per-file-ignores==1.2.1 pipdeptree==2.10.2 -pytest-asyncio==0.20.3 +pytest-asyncio==0.21.0 pytest-aiohttp==1.0.4 pytest-cov==4.1.0 pytest-freezer==0.4.8 From 6e90a757791bb97908a7e6aea4ec990837ceef55 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 21 Jul 2023 19:08:24 +0200 Subject: [PATCH 0755/1009] Update tqdm to 4.65.0 (#96997) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index fbe3fa1a66c2c1..7341c0da62ecc5 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -33,7 +33,7 @@ requests_mock==1.11.0 respx==0.20.1 syrupy==4.0.8 tomli==2.0.1;python_version<"3.11" -tqdm==4.64.0 +tqdm==4.65.0 types-atomicwrites==1.4.5.1 types-croniter==1.0.6 types-backports==0.1.3 From a2b18e46b9c04352f0c95d44f62369cce16c3d7e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 21 Jul 2023 19:08:38 +0200 Subject: [PATCH 0756/1009] Update respx to 0.20.2 (#96996) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 7341c0da62ecc5..855731be72909a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -30,7 +30,7 @@ pytest-picked==0.4.6 pytest-xdist==3.3.1 pytest==7.3.1 requests_mock==1.11.0 -respx==0.20.1 +respx==0.20.2 syrupy==4.0.8 tomli==2.0.1;python_version<"3.11" tqdm==4.65.0 From 7814ce06f40f4b5862206a3ea5fd6bdefb8f9800 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Jul 2023 13:44:13 -0500 Subject: [PATCH 0757/1009] Fix ESPHome bluetooth client cancel behavior when device unexpectedly disconnects (#96918) --- .../components/esphome/bluetooth/client.py | 33 ++++++------------- .../components/esphome/manifest.json | 1 + requirements_all.txt | 3 ++ requirements_test_all.txt | 3 ++ 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index f7c9da48883d96..35e66ea7e472b3 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -4,7 +4,6 @@ import asyncio from collections.abc import Callable, Coroutine import contextlib -from functools import partial import logging from typing import Any, TypeVar, cast import uuid @@ -17,6 +16,7 @@ ) from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError from aioesphomeapi.core import BluetoothGATTAPIError +from async_interrupt import interrupt import async_timeout from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.client import BaseBleakClient, NotifyCallback @@ -57,11 +57,6 @@ def mac_to_int(address: str) -> int: return int(address.replace(":", ""), 16) -def _on_disconnected(task: asyncio.Task[Any], _: asyncio.Future[None]) -> None: - if task and not task.done(): - task.cancel() - - def verify_connected(func: _WrapFuncType) -> _WrapFuncType: """Define a wrapper throw BleakError if not connected.""" @@ -72,25 +67,17 @@ async def _async_wrap_bluetooth_connected_operation( loop = self._loop disconnected_futures = self._disconnected_futures disconnected_future = loop.create_future() - disconnect_handler = partial(_on_disconnected, asyncio.current_task(loop)) - disconnected_future.add_done_callback(disconnect_handler) disconnected_futures.add(disconnected_future) + ble_device = self._ble_device + disconnect_message = ( + f"{self._source_name }: {ble_device.name} - {ble_device.address}: " + "Disconnected during operation" + ) try: - return await func(self, *args, **kwargs) - except asyncio.CancelledError as ex: - if not disconnected_future.done(): - # If the disconnected future is not done, the task was cancelled - # externally and we need to raise cancelled error to avoid - # blocking the cancellation. - raise - ble_device = self._ble_device - raise BleakError( - f"{self._source_name }: {ble_device.name} - {ble_device.address}: " - "Disconnected during operation" - ) from ex + async with interrupt(disconnected_future, BleakError, disconnect_message): + return await func(self, *args, **kwargs) finally: disconnected_futures.discard(disconnected_future) - disconnected_future.remove_done_callback(disconnect_handler) return cast(_WrapFuncType, _async_wrap_bluetooth_connected_operation) @@ -340,7 +327,7 @@ def _on_bluetooth_connection_state( # exception. await connected_future raise - except Exception: + except Exception as ex: if connected_future.done(): with contextlib.suppress(BleakError): # If the connect call throws an exception, @@ -350,7 +337,7 @@ def _on_bluetooth_connection_state( # exception from the connect call as it # will be more descriptive. await connected_future - connected_future.cancel() + connected_future.cancel(f"Unhandled exception in connect call: {ex}") raise await connected_future diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 1caf0e01efcc57..33c43936544430 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,6 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ + "async_interrupt==1.1.1", "aioesphomeapi==15.1.14", "bluetooth-data-tools==1.6.0", "esphome-dashboard-api==1.2.3" diff --git a/requirements_all.txt b/requirements_all.txt index 3fb2ff29f9a8e6..8f16fbeb22ceb0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -442,6 +442,9 @@ asterisk-mbox==0.5.0 # homeassistant.components.yeelight async-upnp-client==0.33.2 +# homeassistant.components.esphome +async_interrupt==1.1.1 + # homeassistant.components.keyboard_remote asyncinotify==4.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b99ba4b372cf0d..3ff331e2a00d3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -396,6 +396,9 @@ arcam-fmj==1.4.0 # homeassistant.components.yeelight async-upnp-client==0.33.2 +# homeassistant.components.esphome +async_interrupt==1.1.1 + # homeassistant.components.sleepiq asyncsleepiq==1.3.5 From facd6ef76529f4629f985d612196edc1f8caf16e Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 21 Jul 2023 21:58:18 +0200 Subject: [PATCH 0758/1009] Display current version in common format in AVM Fritz!Tools (#96424) --- homeassistant/components/fritz/common.py | 8 +++++++- tests/components/fritz/const.py | 2 +- tests/components/fritz/test_diagnostics.py | 2 +- tests/components/fritz/test_update.py | 7 +++---- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 81fdcde236a408..cdea8ebee54dbf 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta from functools import partial import logging +import re from types import MappingProxyType from typing import Any, TypedDict, cast @@ -259,7 +260,12 @@ def setup(self) -> None: self._unique_id = info.serial_number self._model = info.model_name - self._current_firmware = info.software_version + if ( + version_normalized := re.search(r"^\d+\.[0]?(.*)", info.software_version) + ) is not None: + self._current_firmware = version_normalized.group(1) + else: + self._current_firmware = info.software_version ( self._update_available, diff --git a/tests/components/fritz/const.py b/tests/components/fritz/const.py index c19327fbf5e303..dc27e8aab96491 100644 --- a/tests/components/fritz/const.py +++ b/tests/components/fritz/const.py @@ -29,7 +29,7 @@ MOCK_IPS = {"fritz.box": "192.168.178.1", "printer": "192.168.178.2"} MOCK_MODELNAME = "FRITZ!Box 7530 AX" MOCK_FIRMWARE = "256.07.29" -MOCK_FIRMWARE_AVAILABLE = "256.07.50" +MOCK_FIRMWARE_AVAILABLE = "7.50" MOCK_FIRMWARE_RELEASE_URL = ( "http://download.avm.de/fritzbox/fritzbox-7530-ax/deutschland/fritz.os/info_de.txt" ) diff --git a/tests/components/fritz/test_diagnostics.py b/tests/components/fritz/test_diagnostics.py index f7e5980720dd2a..760b5f32d0c66c 100644 --- a/tests/components/fritz/test_diagnostics.py +++ b/tests/components/fritz/test_diagnostics.py @@ -50,7 +50,7 @@ async def test_entry_diagnostics( for _, device in avm_wrapper.devices.items() ], "connection_type": "WANPPPConnection", - "current_firmware": "256.07.29", + "current_firmware": "7.29", "discovered_services": [ "DeviceInfo1", "Hosts1", diff --git a/tests/components/fritz/test_update.py b/tests/components/fritz/test_update.py index 915a6bb6fd033e..99ca7a3b6c539f 100644 --- a/tests/components/fritz/test_update.py +++ b/tests/components/fritz/test_update.py @@ -9,7 +9,6 @@ from homeassistant.setup import async_setup_component from .const import ( - MOCK_FIRMWARE, MOCK_FIRMWARE_AVAILABLE, MOCK_FIRMWARE_RELEASE_URL, MOCK_USER_DATA, @@ -60,7 +59,7 @@ async def test_update_available( update = hass.states.get("update.mock_title_fritz_os") assert update is not None assert update.state == "on" - assert update.attributes.get("installed_version") == MOCK_FIRMWARE + assert update.attributes.get("installed_version") == "7.29" assert update.attributes.get("latest_version") == MOCK_FIRMWARE_AVAILABLE assert update.attributes.get("release_url") == MOCK_FIRMWARE_RELEASE_URL @@ -83,8 +82,8 @@ async def test_no_update_available( update = hass.states.get("update.mock_title_fritz_os") assert update is not None assert update.state == "off" - assert update.attributes.get("installed_version") == MOCK_FIRMWARE - assert update.attributes.get("latest_version") == MOCK_FIRMWARE + assert update.attributes.get("installed_version") == "7.29" + assert update.attributes.get("latest_version") == "7.29" async def test_available_update_can_be_installed( From 2c4e4428e9f2ede57e03ad94476ed51add2b81e8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Jul 2023 15:41:50 -0500 Subject: [PATCH 0759/1009] Decouple more of ESPHome Bluetooth support (#96502) * Decouple more of ESPHome Bluetooth support The goal is to be able to move more of this into an external library * Decouple more of ESPHome Bluetooth support The goal is to be able to move more of this into an external library * Decouple more of ESPHome Bluetooth support The goal is to be able to move more of this into an external library * Decouple more of ESPHome Bluetooth support The goal is to be able to move more of this into an external library * Decouple more of ESPHome Bluetooth support The goal is to be able to move more of this into an external library * fix diag * remove need for hass in the client * refactor * decouple more * decouple more * decouple more * decouple more * decouple more * remove unreachable code * remove unreachable code --- .coveragerc | 1 - .../components/esphome/bluetooth/__init__.py | 75 ++++--- .../components/esphome/bluetooth/cache.py | 50 +++++ .../components/esphome/bluetooth/client.py | 204 ++++++++++-------- .../components/esphome/bluetooth/device.py | 54 +++++ .../components/esphome/diagnostics.py | 10 +- .../components/esphome/domain_data.py | 45 +--- .../components/esphome/entry_data.py | 40 +--- homeassistant/components/esphome/manager.py | 4 +- 9 files changed, 280 insertions(+), 203 deletions(-) create mode 100644 homeassistant/components/esphome/bluetooth/cache.py create mode 100644 homeassistant/components/esphome/bluetooth/device.py diff --git a/.coveragerc b/.coveragerc index 9e5541a07bc578..a5397971d1fc67 100644 --- a/.coveragerc +++ b/.coveragerc @@ -305,7 +305,6 @@ omit = homeassistant/components/escea/climate.py homeassistant/components/escea/discovery.py homeassistant/components/esphome/bluetooth/* - homeassistant/components/esphome/domain_data.py homeassistant/components/esphome/entry_data.py homeassistant/components/esphome/manager.py homeassistant/components/etherscan/sensor.py diff --git a/homeassistant/components/esphome/bluetooth/__init__.py b/homeassistant/components/esphome/bluetooth/__init__.py index aea65f9358e65a..4acd335c1b8851 100644 --- a/homeassistant/components/esphome/bluetooth/__init__.py +++ b/homeassistant/components/esphome/bluetooth/__init__.py @@ -1,7 +1,6 @@ """Bluetooth support for esphome.""" from __future__ import annotations -from collections.abc import Callable from functools import partial import logging @@ -16,36 +15,35 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback from ..entry_data import RuntimeEntryData -from .client import ESPHomeClient +from .cache import ESPHomeBluetoothCache +from .client import ( + ESPHomeClient, + ESPHomeClientData, +) +from .device import ESPHomeBluetoothDevice from .scanner import ESPHomeScanner _LOGGER = logging.getLogger(__name__) @hass_callback -def _async_can_connect_factory( - entry_data: RuntimeEntryData, source: str -) -> Callable[[], bool]: - """Create a can_connect function for a specific RuntimeEntryData instance.""" - - @hass_callback - def _async_can_connect() -> bool: - """Check if a given source can make another connection.""" - can_connect = bool(entry_data.available and entry_data.ble_connections_free) - _LOGGER.debug( - ( - "%s [%s]: Checking can connect, available=%s, ble_connections_free=%s" - " result=%s" - ), - entry_data.name, - source, - entry_data.available, - entry_data.ble_connections_free, - can_connect, - ) - return can_connect - - return _async_can_connect +def _async_can_connect( + entry_data: RuntimeEntryData, bluetooth_device: ESPHomeBluetoothDevice, source: str +) -> bool: + """Check if a given source can make another connection.""" + can_connect = bool(entry_data.available and bluetooth_device.ble_connections_free) + _LOGGER.debug( + ( + "%s [%s]: Checking can connect, available=%s, ble_connections_free=%s" + " result=%s" + ), + entry_data.name, + source, + entry_data.available, + bluetooth_device.ble_connections_free, + can_connect, + ) + return can_connect async def async_connect_scanner( @@ -53,16 +51,20 @@ async def async_connect_scanner( entry: ConfigEntry, cli: APIClient, entry_data: RuntimeEntryData, + cache: ESPHomeBluetoothCache, ) -> CALLBACK_TYPE: """Connect scanner.""" assert entry.unique_id is not None source = str(entry.unique_id) new_info_callback = async_get_advertisement_callback(hass) - assert entry_data.device_info is not None - feature_flags = entry_data.device_info.bluetooth_proxy_feature_flags_compat( + device_info = entry_data.device_info + assert device_info is not None + feature_flags = device_info.bluetooth_proxy_feature_flags_compat( entry_data.api_version ) connectable = bool(feature_flags & BluetoothProxyFeature.ACTIVE_CONNECTIONS) + bluetooth_device = ESPHomeBluetoothDevice(entry_data.name, device_info.mac_address) + entry_data.bluetooth_device = bluetooth_device _LOGGER.debug( "%s [%s]: Connecting scanner feature_flags=%s, connectable=%s", entry.title, @@ -70,22 +72,35 @@ async def async_connect_scanner( feature_flags, connectable, ) + client_data = ESPHomeClientData( + bluetooth_device=bluetooth_device, + cache=cache, + client=cli, + device_info=device_info, + api_version=entry_data.api_version, + title=entry.title, + scanner=None, + disconnect_callbacks=entry_data.disconnect_callbacks, + ) connector = HaBluetoothConnector( # MyPy doesn't like partials, but this is correct # https://github.com/python/mypy/issues/1484 - client=partial(ESPHomeClient, config_entry=entry), # type: ignore[arg-type] + client=partial(ESPHomeClient, client_data=client_data), # type: ignore[arg-type] source=source, - can_connect=_async_can_connect_factory(entry_data, source), + can_connect=hass_callback( + partial(_async_can_connect, entry_data, bluetooth_device, source) + ), ) scanner = ESPHomeScanner( hass, source, entry.title, new_info_callback, connector, connectable ) + client_data.scanner = scanner if connectable: # If its connectable be sure not to register the scanner # until we know the connection is fully setup since otherwise # there is a race condition where the connection can fail await cli.subscribe_bluetooth_connections_free( - entry_data.async_update_ble_connection_limits + bluetooth_device.async_update_ble_connection_limits ) unload_callbacks = [ async_register_scanner(hass, scanner, connectable), diff --git a/homeassistant/components/esphome/bluetooth/cache.py b/homeassistant/components/esphome/bluetooth/cache.py new file mode 100644 index 00000000000000..3ec29121382ab3 --- /dev/null +++ b/homeassistant/components/esphome/bluetooth/cache.py @@ -0,0 +1,50 @@ +"""Bluetooth cache for esphome.""" +from __future__ import annotations + +from collections.abc import MutableMapping +from dataclasses import dataclass, field + +from bleak.backends.service import BleakGATTServiceCollection +from lru import LRU # pylint: disable=no-name-in-module + +MAX_CACHED_SERVICES = 128 + + +@dataclass(slots=True) +class ESPHomeBluetoothCache: + """Shared cache between all ESPHome bluetooth devices.""" + + _gatt_services_cache: MutableMapping[int, BleakGATTServiceCollection] = field( + default_factory=lambda: LRU(MAX_CACHED_SERVICES) + ) + _gatt_mtu_cache: MutableMapping[int, int] = field( + default_factory=lambda: LRU(MAX_CACHED_SERVICES) + ) + + def get_gatt_services_cache( + self, address: int + ) -> BleakGATTServiceCollection | None: + """Get the BleakGATTServiceCollection for the given address.""" + return self._gatt_services_cache.get(address) + + def set_gatt_services_cache( + self, address: int, services: BleakGATTServiceCollection + ) -> None: + """Set the BleakGATTServiceCollection for the given address.""" + self._gatt_services_cache[address] = services + + def clear_gatt_services_cache(self, address: int) -> None: + """Clear the BleakGATTServiceCollection for the given address.""" + self._gatt_services_cache.pop(address, None) + + def get_gatt_mtu_cache(self, address: int) -> int | None: + """Get the mtu cache for the given address.""" + return self._gatt_mtu_cache.get(address) + + def set_gatt_mtu_cache(self, address: int, mtu: int) -> None: + """Set the mtu cache for the given address.""" + self._gatt_mtu_cache[address] = mtu + + def clear_gatt_mtu_cache(self, address: int) -> None: + """Clear the mtu cache for the given address.""" + self._gatt_mtu_cache.pop(address, None) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 35e66ea7e472b3..ee629eed6f972b 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -4,6 +4,8 @@ import asyncio from collections.abc import Callable, Coroutine import contextlib +from dataclasses import dataclass, field +from functools import partial import logging from typing import Any, TypeVar, cast import uuid @@ -11,8 +13,11 @@ from aioesphomeapi import ( ESP_CONNECTION_ERROR_DESCRIPTION, ESPHOME_GATT_ERRORS, + APIClient, + APIVersion, BLEConnectionError, BluetoothProxyFeature, + DeviceInfo, ) from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError from aioesphomeapi.core import BluetoothGATTAPIError @@ -24,13 +29,13 @@ from bleak.backends.service import BleakGATTServiceCollection from bleak.exc import BleakError -from homeassistant.components.bluetooth import async_scanner_by_source -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.core import CALLBACK_TYPE -from ..domain_data import DomainData +from .cache import ESPHomeBluetoothCache from .characteristic import BleakGATTCharacteristicESPHome from .descriptor import BleakGATTDescriptorESPHome +from .device import ESPHomeBluetoothDevice +from .scanner import ESPHomeScanner from .service import BleakGATTServiceESPHome DEFAULT_MTU = 23 @@ -118,6 +123,20 @@ async def _async_wrap_bluetooth_operation( return cast(_WrapFuncType, _async_wrap_bluetooth_operation) +@dataclass(slots=True) +class ESPHomeClientData: + """Define a class that stores client data for an esphome client.""" + + bluetooth_device: ESPHomeBluetoothDevice + cache: ESPHomeBluetoothCache + client: APIClient + device_info: DeviceInfo + api_version: APIVersion + title: str + scanner: ESPHomeScanner | None + disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list) + + class ESPHomeClient(BaseBleakClient): """ESPHome Bleak Client.""" @@ -125,36 +144,38 @@ def __init__( self, address_or_ble_device: BLEDevice | str, *args: Any, - config_entry: ConfigEntry, + client_data: ESPHomeClientData, **kwargs: Any, ) -> None: """Initialize the ESPHomeClient.""" + device_info = client_data.device_info + self._disconnect_callbacks = client_data.disconnect_callbacks assert isinstance(address_or_ble_device, BLEDevice) super().__init__(address_or_ble_device, *args, **kwargs) - self._hass: HomeAssistant = kwargs["hass"] + self._loop = asyncio.get_running_loop() self._ble_device = address_or_ble_device self._address_as_int = mac_to_int(self._ble_device.address) assert self._ble_device.details is not None self._source = self._ble_device.details["source"] - self.domain_data = DomainData.get(self._hass) - self.entry_data = self.domain_data.get_entry_data(config_entry) - self._client = self.entry_data.client + self._cache = client_data.cache + self._bluetooth_device = client_data.bluetooth_device + self._client = client_data.client self._is_connected = False self._mtu: int | None = None self._cancel_connection_state: CALLBACK_TYPE | None = None self._notify_cancels: dict[ int, tuple[Callable[[], Coroutine[Any, Any, None]], Callable[[], None]] ] = {} - self._loop = asyncio.get_running_loop() self._disconnected_futures: set[asyncio.Future[None]] = set() - device_info = self.entry_data.device_info - assert device_info is not None - self._device_info = device_info + self._device_info = client_data.device_info self._feature_flags = device_info.bluetooth_proxy_feature_flags_compat( - self.entry_data.api_version + client_data.api_version ) self._address_type = address_or_ble_device.details["address_type"] - self._source_name = f"{config_entry.title} [{self._source}]" + self._source_name = f"{client_data.title} [{self._source}]" + scanner = client_data.scanner + assert scanner is not None + self._scanner = scanner def __str__(self) -> str: """Return the string representation of the client.""" @@ -206,14 +227,14 @@ def _async_ble_device_disconnected(self) -> None: self._async_call_bleak_disconnected_callback() def _async_esp_disconnected(self) -> None: - """Handle the esp32 client disconnecting from hass.""" + """Handle the esp32 client disconnecting from us.""" _LOGGER.debug( "%s: %s - %s: ESP device disconnected", self._source_name, self._ble_device.name, self._ble_device.address, ) - self.entry_data.disconnect_callbacks.remove(self._async_esp_disconnected) + self._disconnect_callbacks.remove(self._async_esp_disconnected) self._async_ble_device_disconnected() def _async_call_bleak_disconnected_callback(self) -> None: @@ -222,6 +243,65 @@ def _async_call_bleak_disconnected_callback(self) -> None: self._disconnected_callback() self._disconnected_callback = None + def _on_bluetooth_connection_state( + self, + connected_future: asyncio.Future[bool], + connected: bool, + mtu: int, + error: int, + ) -> None: + """Handle a connect or disconnect.""" + _LOGGER.debug( + "%s: %s - %s: Connection state changed to connected=%s mtu=%s error=%s", + self._source_name, + self._ble_device.name, + self._ble_device.address, + connected, + mtu, + error, + ) + if connected: + self._is_connected = True + if not self._mtu: + self._mtu = mtu + self._cache.set_gatt_mtu_cache(self._address_as_int, mtu) + else: + self._async_ble_device_disconnected() + + if connected_future.done(): + return + + if error: + try: + ble_connection_error = BLEConnectionError(error) + ble_connection_error_name = ble_connection_error.name + human_error = ESP_CONNECTION_ERROR_DESCRIPTION[ble_connection_error] + except (KeyError, ValueError): + ble_connection_error_name = str(error) + human_error = ESPHOME_GATT_ERRORS.get( + error, f"Unknown error code {error}" + ) + connected_future.set_exception( + BleakError( + f"Error {ble_connection_error_name} while connecting:" + f" {human_error}" + ) + ) + return + + if not connected: + connected_future.set_exception(BleakError("Disconnected")) + return + + _LOGGER.debug( + "%s: %s - %s: connected, registering for disconnected callbacks", + self._source_name, + self._ble_device.name, + self._ble_device.address, + ) + self._disconnect_callbacks.append(self._async_esp_disconnected) + connected_future.set_result(connected) + @api_error_as_bleak_error async def connect( self, dangerous_use_bleak_cache: bool = False, **kwargs: Any @@ -236,82 +316,24 @@ async def connect( Boolean representing connection status. """ await self._wait_for_free_connection_slot(CONNECT_FREE_SLOT_TIMEOUT) - domain_data = self.domain_data - entry_data = self.entry_data + cache = self._cache - self._mtu = domain_data.get_gatt_mtu_cache(self._address_as_int) + self._mtu = cache.get_gatt_mtu_cache(self._address_as_int) has_cache = bool( dangerous_use_bleak_cache and self._feature_flags & BluetoothProxyFeature.REMOTE_CACHING - and domain_data.get_gatt_services_cache(self._address_as_int) + and cache.get_gatt_services_cache(self._address_as_int) and self._mtu ) - connected_future: asyncio.Future[bool] = asyncio.Future() - - def _on_bluetooth_connection_state( - connected: bool, mtu: int, error: int - ) -> None: - """Handle a connect or disconnect.""" - _LOGGER.debug( - "%s: %s - %s: Connection state changed to connected=%s mtu=%s error=%s", - self._source_name, - self._ble_device.name, - self._ble_device.address, - connected, - mtu, - error, - ) - if connected: - self._is_connected = True - if not self._mtu: - self._mtu = mtu - domain_data.set_gatt_mtu_cache(self._address_as_int, mtu) - else: - self._async_ble_device_disconnected() - - if connected_future.done(): - return - - if error: - try: - ble_connection_error = BLEConnectionError(error) - ble_connection_error_name = ble_connection_error.name - human_error = ESP_CONNECTION_ERROR_DESCRIPTION[ble_connection_error] - except (KeyError, ValueError): - ble_connection_error_name = str(error) - human_error = ESPHOME_GATT_ERRORS.get( - error, f"Unknown error code {error}" - ) - connected_future.set_exception( - BleakError( - f"Error {ble_connection_error_name} while connecting:" - f" {human_error}" - ) - ) - return - - if not connected: - connected_future.set_exception(BleakError("Disconnected")) - return - - _LOGGER.debug( - "%s: %s - %s: connected, registering for disconnected callbacks", - self._source_name, - self._ble_device.name, - self._ble_device.address, - ) - entry_data.disconnect_callbacks.append(self._async_esp_disconnected) - connected_future.set_result(connected) + connected_future: asyncio.Future[bool] = self._loop.create_future() timeout = kwargs.get("timeout", self._timeout) - if not (scanner := async_scanner_by_source(self._hass, self._source)): - raise BleakError("Scanner disappeared for {self._source_name}") - with scanner.connecting(): + with self._scanner.connecting(): try: self._cancel_connection_state = ( await self._client.bluetooth_device_connect( self._address_as_int, - _on_bluetooth_connection_state, + partial(self._on_bluetooth_connection_state, connected_future), timeout=timeout, has_cache=has_cache, feature_flags=self._feature_flags, @@ -366,7 +388,8 @@ async def disconnect(self) -> bool: async def _wait_for_free_connection_slot(self, timeout: float) -> None: """Wait for a free connection slot.""" - if self.entry_data.ble_connections_free: + bluetooth_device = self._bluetooth_device + if bluetooth_device.ble_connections_free: return _LOGGER.debug( "%s: %s - %s: Out of connection slots, waiting for a free one", @@ -375,7 +398,7 @@ async def _wait_for_free_connection_slot(self, timeout: float) -> None: self._ble_device.address, ) async with async_timeout.timeout(timeout): - await self.entry_data.wait_for_ble_connections_free() + await bluetooth_device.wait_for_ble_connections_free() @property def is_connected(self) -> bool: @@ -432,14 +455,14 @@ async def get_services( with this device's services tree. """ address_as_int = self._address_as_int - domain_data = self.domain_data + cache = self._cache # If the connection version >= 3, we must use the cache # because the esp has already wiped the services list to # save memory. if ( self._feature_flags & BluetoothProxyFeature.REMOTE_CACHING or dangerous_use_bleak_cache - ) and (cached_services := domain_data.get_gatt_services_cache(address_as_int)): + ) and (cached_services := cache.get_gatt_services_cache(address_as_int)): _LOGGER.debug( "%s: %s - %s: Cached services hit", self._source_name, @@ -498,7 +521,7 @@ async def get_services( self._ble_device.name, self._ble_device.address, ) - domain_data.set_gatt_services_cache(address_as_int, services) + cache.set_gatt_services_cache(address_as_int, services) return services def _resolve_characteristic( @@ -518,8 +541,9 @@ def _resolve_characteristic( @api_error_as_bleak_error async def clear_cache(self) -> bool: """Clear the GATT cache.""" - self.domain_data.clear_gatt_services_cache(self._address_as_int) - self.domain_data.clear_gatt_mtu_cache(self._address_as_int) + cache = self._cache + cache.clear_gatt_services_cache(self._address_as_int) + cache.clear_gatt_mtu_cache(self._address_as_int) if not self._feature_flags & BluetoothProxyFeature.CACHE_CLEARING: _LOGGER.warning( "On device cache clear is not available with this ESPHome version; " @@ -734,5 +758,5 @@ def __del__(self) -> None: self._ble_device.name, self._ble_device.address, ) - if not self._hass.loop.is_closed(): - self._hass.loop.call_soon_threadsafe(self._async_disconnected_cleanup) + if not self._loop.is_closed(): + self._loop.call_soon_threadsafe(self._async_disconnected_cleanup) diff --git a/homeassistant/components/esphome/bluetooth/device.py b/homeassistant/components/esphome/bluetooth/device.py new file mode 100644 index 00000000000000..8d060151dbf254 --- /dev/null +++ b/homeassistant/components/esphome/bluetooth/device.py @@ -0,0 +1,54 @@ +"""Bluetooth device models for esphome.""" +from __future__ import annotations + +import asyncio +from dataclasses import dataclass, field +import logging + +from homeassistant.core import callback + +_LOGGER = logging.getLogger(__name__) + + +@dataclass(slots=True) +class ESPHomeBluetoothDevice: + """Bluetooth data for a specific ESPHome device.""" + + name: str + mac_address: str + ble_connections_free: int = 0 + ble_connections_limit: int = 0 + _ble_connection_free_futures: list[asyncio.Future[int]] = field( + default_factory=list + ) + + @callback + def async_update_ble_connection_limits(self, free: int, limit: int) -> None: + """Update the BLE connection limits.""" + _LOGGER.debug( + "%s [%s]: BLE connection limits: used=%s free=%s limit=%s", + self.name, + self.mac_address, + limit - free, + free, + limit, + ) + self.ble_connections_free = free + self.ble_connections_limit = limit + if not free: + return + for fut in self._ble_connection_free_futures: + # If wait_for_ble_connections_free gets cancelled, it will + # leave a future in the list. We need to check if it's done + # before setting the result. + if not fut.done(): + fut.set_result(free) + self._ble_connection_free_futures.clear() + + async def wait_for_ble_connections_free(self) -> int: + """Wait until there are free BLE connections.""" + if self.ble_connections_free > 0: + return self.ble_connections_free + fut: asyncio.Future[int] = asyncio.Future() + self._ble_connection_free_futures.append(fut) + return await fut diff --git a/homeassistant/components/esphome/diagnostics.py b/homeassistant/components/esphome/diagnostics.py index 292d1921abfce0..a984d057c0c741 100644 --- a/homeassistant/components/esphome/diagnostics.py +++ b/homeassistant/components/esphome/diagnostics.py @@ -30,12 +30,14 @@ async def async_get_config_entry_diagnostics( if (storage_data := await entry_data.store.async_load()) is not None: diag["storage_data"] = storage_data - if config_entry.unique_id and ( - scanner := async_scanner_by_source(hass, config_entry.unique_id) + if ( + config_entry.unique_id + and (scanner := async_scanner_by_source(hass, config_entry.unique_id)) + and (bluetooth_device := entry_data.bluetooth_device) ): diag["bluetooth"] = { - "connections_free": entry_data.ble_connections_free, - "connections_limit": entry_data.ble_connections_limit, + "connections_free": bluetooth_device.ble_connections_free, + "connections_limit": bluetooth_device.ble_connections_limit, "scanner": await scanner.async_diagnostics(), } diff --git a/homeassistant/components/esphome/domain_data.py b/homeassistant/components/esphome/domain_data.py index aacda1083985f1..3203964fdc1e68 100644 --- a/homeassistant/components/esphome/domain_data.py +++ b/homeassistant/components/esphome/domain_data.py @@ -1,65 +1,31 @@ """Support for esphome domain data.""" from __future__ import annotations -from collections.abc import MutableMapping from dataclasses import dataclass, field from typing import cast -from bleak.backends.service import BleakGATTServiceCollection -from lru import LRU # pylint: disable=no-name-in-module from typing_extensions import Self from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.json import JSONEncoder +from .bluetooth.cache import ESPHomeBluetoothCache from .const import DOMAIN from .entry_data import ESPHomeStorage, RuntimeEntryData STORAGE_VERSION = 1 -MAX_CACHED_SERVICES = 128 -@dataclass +@dataclass(slots=True) class DomainData: """Define a class that stores global esphome data in hass.data[DOMAIN].""" _entry_datas: dict[str, RuntimeEntryData] = field(default_factory=dict) _stores: dict[str, ESPHomeStorage] = field(default_factory=dict) - _gatt_services_cache: MutableMapping[int, BleakGATTServiceCollection] = field( - default_factory=lambda: LRU(MAX_CACHED_SERVICES) + bluetooth_cache: ESPHomeBluetoothCache = field( + default_factory=ESPHomeBluetoothCache ) - _gatt_mtu_cache: MutableMapping[int, int] = field( - default_factory=lambda: LRU(MAX_CACHED_SERVICES) - ) - - def get_gatt_services_cache( - self, address: int - ) -> BleakGATTServiceCollection | None: - """Get the BleakGATTServiceCollection for the given address.""" - return self._gatt_services_cache.get(address) - - def set_gatt_services_cache( - self, address: int, services: BleakGATTServiceCollection - ) -> None: - """Set the BleakGATTServiceCollection for the given address.""" - self._gatt_services_cache[address] = services - - def clear_gatt_services_cache(self, address: int) -> None: - """Clear the BleakGATTServiceCollection for the given address.""" - self._gatt_services_cache.pop(address, None) - - def get_gatt_mtu_cache(self, address: int) -> int | None: - """Get the mtu cache for the given address.""" - return self._gatt_mtu_cache.get(address) - - def set_gatt_mtu_cache(self, address: int, mtu: int) -> None: - """Set the mtu cache for the given address.""" - self._gatt_mtu_cache[address] = mtu - - def clear_gatt_mtu_cache(self, address: int) -> None: - """Clear the mtu cache for the given address.""" - self._gatt_mtu_cache.pop(address, None) def get_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: """Return the runtime entry data associated with this config entry. @@ -70,8 +36,7 @@ def get_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: def set_entry_data(self, entry: ConfigEntry, entry_data: RuntimeEntryData) -> None: """Set the runtime entry data associated with this config entry.""" - if entry.entry_id in self._entry_datas: - raise ValueError("Entry data for this entry is already set") + assert entry.entry_id not in self._entry_datas, "Entry data already set!" self._entry_datas[entry.entry_id] = entry_data def pop_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 3391d02a829aeb..2d147d243f200c 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -40,6 +40,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store +from .bluetooth.device import ESPHomeBluetoothDevice from .dashboard import async_get_dashboard INFO_TO_COMPONENT_TYPE: Final = {v: k for k, v in COMPONENT_TYPE_TO_INFO.items()} @@ -80,7 +81,7 @@ class ESPHomeStorage(Store[StoreData]): """ESPHome Storage.""" -@dataclass +@dataclass(slots=True) class RuntimeEntryData: """Store runtime data for esphome config entries.""" @@ -97,6 +98,7 @@ class RuntimeEntryData: available: bool = False expected_disconnect: bool = False # Last disconnect was expected (e.g. deep sleep) device_info: DeviceInfo | None = None + bluetooth_device: ESPHomeBluetoothDevice | None = None api_version: APIVersion = field(default_factory=APIVersion) cleanup_callbacks: list[Callable[[], None]] = field(default_factory=list) disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list) @@ -107,11 +109,6 @@ class RuntimeEntryData: platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock) _storage_contents: StoreData | None = None _pending_storage: Callable[[], StoreData] | None = None - ble_connections_free: int = 0 - ble_connections_limit: int = 0 - _ble_connection_free_futures: list[asyncio.Future[int]] = field( - default_factory=list - ) assist_pipeline_update_callbacks: list[Callable[[], None]] = field( default_factory=list ) @@ -196,37 +193,6 @@ def _unsub() -> None: return _unsub - @callback - def async_update_ble_connection_limits(self, free: int, limit: int) -> None: - """Update the BLE connection limits.""" - _LOGGER.debug( - "%s [%s]: BLE connection limits: used=%s free=%s limit=%s", - self.name, - self.device_info.mac_address if self.device_info else "unknown", - limit - free, - free, - limit, - ) - self.ble_connections_free = free - self.ble_connections_limit = limit - if not free: - return - for fut in self._ble_connection_free_futures: - # If wait_for_ble_connections_free gets cancelled, it will - # leave a future in the list. We need to check if it's done - # before setting the result. - if not fut.done(): - fut.set_result(free) - self._ble_connection_free_futures.clear() - - async def wait_for_ble_connections_free(self) -> int: - """Wait until there are free BLE connections.""" - if self.ble_connections_free > 0: - return self.ble_connections_free - fut: asyncio.Future[int] = asyncio.Future() - self._ble_connection_free_futures.append(fut) - return await fut - @callback def async_set_assist_pipeline_state(self, state: bool) -> None: """Set the assist pipeline state.""" diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 026d0315238b26..4741eaaa6fb42b 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -390,7 +390,9 @@ async def on_connect(self) -> None: if device_info.bluetooth_proxy_feature_flags_compat(cli.api_version): entry_data.disconnect_callbacks.append( - await async_connect_scanner(hass, entry, cli, entry_data) + await async_connect_scanner( + hass, entry, cli, entry_data, self.domain_data.bluetooth_cache + ) ) self.device_id = _async_setup_device_registry( From 52ab6b0b9d2534b2200abc11d840bf29e6dd54b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Jul 2023 19:15:28 -0500 Subject: [PATCH 0760/1009] Bump httpcore to 0.17.3 (#97032) --- homeassistant/package_constraints.txt | 2 +- script/gen_requirements_all.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e118f7ae39b8ed..c8f4bc835ceefd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -102,7 +102,7 @@ regex==2021.8.28 # requirements so we can directly link HA versions to these library versions. anyio==3.7.0 h11==0.14.0 -httpcore==0.17.2 +httpcore==0.17.3 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 96d8bd03a52f07..f3d0defac4de5c 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -107,7 +107,7 @@ # requirements so we can directly link HA versions to these library versions. anyio==3.7.0 h11==0.14.0 -httpcore==0.17.2 +httpcore==0.17.3 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation From 4bc57c0466c6162f20b6dc3a34e11b142802d4a7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 22 Jul 2023 12:39:28 +0200 Subject: [PATCH 0761/1009] Update coverage to 7.2.7 (#96998) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 855731be72909a..e20e28b3d0a2a6 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt astroid==2.15.4 -coverage==7.2.4 +coverage==7.2.7 freezegun==1.2.2 mock-open==1.4.0 mypy==1.4.1 From 8495da19641391ed35e1e356ed4e2c781cc573c9 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 22 Jul 2023 12:43:51 +0200 Subject: [PATCH 0762/1009] Add entity translations for PoolSense (#95814) --- .../components/poolsense/binary_sensor.py | 4 +-- homeassistant/components/poolsense/sensor.py | 16 ++++----- .../components/poolsense/strings.json | 33 +++++++++++++++++++ 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/poolsense/binary_sensor.py b/homeassistant/components/poolsense/binary_sensor.py index 2a350816685b38..e206521c3d9b8a 100644 --- a/homeassistant/components/poolsense/binary_sensor.py +++ b/homeassistant/components/poolsense/binary_sensor.py @@ -17,12 +17,12 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( BinarySensorEntityDescription( key="pH Status", - name="pH Status", + translation_key="ph_status", device_class=BinarySensorDeviceClass.PROBLEM, ), BinarySensorEntityDescription( key="Chlorine Status", - name="Chlorine Status", + translation_key="chlorine_status", device_class=BinarySensorDeviceClass.PROBLEM, ), ) diff --git a/homeassistant/components/poolsense/sensor.py b/homeassistant/components/poolsense/sensor.py index f8f9162032103e..fe3535b378f705 100644 --- a/homeassistant/components/poolsense/sensor.py +++ b/homeassistant/components/poolsense/sensor.py @@ -22,55 +22,53 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="Chlorine", + translation_key="chlorine", native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, icon="mdi:pool", - name="Chlorine", ), SensorEntityDescription( key="pH", + translation_key="ph", icon="mdi:pool", - name="pH", ), SensorEntityDescription( key="Battery", native_unit_of_measurement=PERCENTAGE, - name="Battery", device_class=SensorDeviceClass.BATTERY, ), SensorEntityDescription( key="Water Temp", native_unit_of_measurement=UnitOfTemperature.CELSIUS, icon="mdi:coolant-temperature", - name="Temperature", device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="Last Seen", + translation_key="last_seen", icon="mdi:clock", - name="Last Seen", device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( key="Chlorine High", + translation_key="chlorine_high", native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, icon="mdi:pool", - name="Chlorine High", ), SensorEntityDescription( key="Chlorine Low", + translation_key="chlorine_low", native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, icon="mdi:pool", - name="Chlorine Low", ), SensorEntityDescription( key="pH High", + translation_key="ph_high", icon="mdi:pool", - name="pH High", ), SensorEntityDescription( key="pH Low", + translation_key="ph_low", icon="mdi:pool", - name="pH Low", ), ) diff --git a/homeassistant/components/poolsense/strings.json b/homeassistant/components/poolsense/strings.json index 2ddf3ee77e8193..9ec67e223a1002 100644 --- a/homeassistant/components/poolsense/strings.json +++ b/homeassistant/components/poolsense/strings.json @@ -14,5 +14,38 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "binary_sensor": { + "ph_status": { + "name": "pH status" + }, + "chlorine_status": { + "name": "Chlorine status" + } + }, + "sensor": { + "chlorine": { + "name": "Chlorine" + }, + "ph": { + "name": "pH" + }, + "last_seen": { + "name": "Last seen" + }, + "chlorine_high": { + "name": "Chlorine high" + }, + "chlorine_low": { + "name": "Chlorine low" + }, + "ph_high": { + "name": "pH high" + }, + "ph_low": { + "name": "pH low" + } + } } } From fb460d343e574bd85d71aed93a604a89fbefd6e5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 22 Jul 2023 12:45:55 +0200 Subject: [PATCH 0763/1009] Add upload date to Youtube state attributes (#96976) --- homeassistant/components/youtube/sensor.py | 4 +++- homeassistant/components/youtube/strings.json | 10 +++++++++- tests/components/youtube/snapshots/test_sensor.ambr | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/youtube/sensor.py b/homeassistant/components/youtube/sensor.py index 4560dcfda8cd7a..b5d3fc79b396c0 100644 --- a/homeassistant/components/youtube/sensor.py +++ b/homeassistant/components/youtube/sensor.py @@ -15,6 +15,7 @@ from . import YouTubeDataUpdateCoordinator from .const import ( ATTR_LATEST_VIDEO, + ATTR_PUBLISHED_AT, ATTR_SUBSCRIBER_COUNT, ATTR_THUMBNAIL, ATTR_TITLE, @@ -47,7 +48,8 @@ class YouTubeSensorEntityDescription(SensorEntityDescription, YouTubeMixin): value_fn=lambda channel: channel[ATTR_LATEST_VIDEO][ATTR_TITLE], entity_picture_fn=lambda channel: channel[ATTR_LATEST_VIDEO][ATTR_THUMBNAIL], attributes_fn=lambda channel: { - ATTR_VIDEO_ID: channel[ATTR_LATEST_VIDEO][ATTR_VIDEO_ID] + ATTR_VIDEO_ID: channel[ATTR_LATEST_VIDEO][ATTR_VIDEO_ID], + ATTR_PUBLISHED_AT: channel[ATTR_LATEST_VIDEO][ATTR_PUBLISHED_AT], }, ), YouTubeSensorEntityDescription( diff --git a/homeassistant/components/youtube/strings.json b/homeassistant/components/youtube/strings.json index 7f369e9909bd91..ccb7e9c506e4a0 100644 --- a/homeassistant/components/youtube/strings.json +++ b/homeassistant/components/youtube/strings.json @@ -37,7 +37,15 @@ "entity": { "sensor": { "latest_upload": { - "name": "Latest upload" + "name": "Latest upload", + "state_attributes": { + "video_id": { + "name": "Video ID" + }, + "published_at": { + "name": "Published at" + } + } }, "subscribers": { "name": "Subscribers" diff --git a/tests/components/youtube/snapshots/test_sensor.ambr b/tests/components/youtube/snapshots/test_sensor.ambr index c5aac39156d925..b643bdeb979903 100644 --- a/tests/components/youtube/snapshots/test_sensor.ambr +++ b/tests/components/youtube/snapshots/test_sensor.ambr @@ -5,6 +5,7 @@ 'entity_picture': 'https://i.ytimg.com/vi/wysukDrMdqU/sddefault.jpg', 'friendly_name': 'Google for Developers Latest upload', 'icon': 'mdi:youtube', + 'published_at': '2023-05-11T00:20:46Z', 'video_id': 'wysukDrMdqU', }), 'context': , From 9b717cb84f8fbc664766f4e339e18631e0ee909d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 22 Jul 2023 12:47:26 +0200 Subject: [PATCH 0764/1009] Use snapshot testing in LastFM (#97009) --- tests/components/lastfm/conftest.py | 8 +- .../lastfm/snapshots/test_sensor.ambr | 51 +++++++++++ tests/components/lastfm/test_sensor.py | 86 ++++--------------- 3 files changed, 77 insertions(+), 68 deletions(-) create mode 100644 tests/components/lastfm/snapshots/test_sensor.ambr diff --git a/tests/components/lastfm/conftest.py b/tests/components/lastfm/conftest.py index 119d4796f57a38..8b8548ad1f9fa9 100644 --- a/tests/components/lastfm/conftest.py +++ b/tests/components/lastfm/conftest.py @@ -2,7 +2,7 @@ from collections.abc import Awaitable, Callable from unittest.mock import patch -from pylast import Track +from pylast import Track, WSError import pytest from homeassistant.components.lastfm.const import CONF_MAIN_USER, CONF_USERS, DOMAIN @@ -65,3 +65,9 @@ def mock_default_user() -> MockUser: def mock_first_time_user() -> MockUser: """Return first time mock user.""" return MockUser(now_playing_result=None, top_tracks=[], recent_tracks=[]) + + +@pytest.fixture(name="not_found_user") +def mock_not_found_user() -> MockUser: + """Return not found mock user.""" + return MockUser(thrown_error=WSError("network", "status", "User not found")) diff --git a/tests/components/lastfm/snapshots/test_sensor.ambr b/tests/components/lastfm/snapshots/test_sensor.ambr new file mode 100644 index 00000000000000..a28e085c1040eb --- /dev/null +++ b/tests/components/lastfm/snapshots/test_sensor.ambr @@ -0,0 +1,51 @@ +# serializer version: 1 +# name: test_sensors[default_user] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Last.fm', + 'entity_picture': '', + 'friendly_name': 'testaccount1', + 'icon': 'mdi:radio-fm', + 'last_played': 'artist - title', + 'play_count': 1, + 'top_played': 'artist - title', + }), + 'context': , + 'entity_id': 'sensor.testaccount1', + 'last_changed': , + 'last_updated': , + 'state': 'artist - title', + }) +# --- +# name: test_sensors[first_time_user] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Last.fm', + 'entity_picture': '', + 'friendly_name': 'testaccount1', + 'icon': 'mdi:radio-fm', + 'last_played': None, + 'play_count': 0, + 'top_played': None, + }), + 'context': , + 'entity_id': 'sensor.testaccount1', + 'last_changed': , + 'last_updated': , + 'state': 'Not Scrobbling', + }) +# --- +# name: test_sensors[not_found_user] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Last.fm', + 'friendly_name': 'testaccount1', + 'icon': 'mdi:radio-fm', + }), + 'context': , + 'entity_id': 'sensor.testaccount1', + 'last_changed': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- diff --git a/tests/components/lastfm/test_sensor.py b/tests/components/lastfm/test_sensor.py index e46cf99ffdc58d..ab9358be1d3da7 100644 --- a/tests/components/lastfm/test_sensor.py +++ b/tests/components/lastfm/test_sensor.py @@ -1,15 +1,12 @@ """Tests for the lastfm sensor.""" from unittest.mock import patch -from pylast import WSError +import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components.lastfm.const import ( - ATTR_LAST_PLAYED, - ATTR_PLAY_COUNT, - ATTR_TOP_PLAYED, CONF_USERS, DOMAIN, - STATE_NOT_SCROBBLING, ) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_PLATFORM, Platform @@ -17,7 +14,7 @@ from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component -from . import API_KEY, USERNAME_1, MockUser +from . import API_KEY, USERNAME_1 from .conftest import ComponentSetup from tests.common import MockConfigEntry @@ -41,73 +38,28 @@ async def test_legacy_migration(hass: HomeAssistant) -> None: assert len(issue_registry.issues) == 1 -async def test_user_unavailable( - hass: HomeAssistant, - setup_integration: ComponentSetup, - config_entry: MockConfigEntry, -) -> None: - """Test update when user can't be fetched.""" - await setup_integration( - config_entry, - MockUser(thrown_error=WSError("network", "status", "User not found")), - ) - - entity_id = "sensor.testaccount1" - - state = hass.states.get(entity_id) - - assert state.state == "unavailable" - - -async def test_first_time_user( - hass: HomeAssistant, - setup_integration: ComponentSetup, - config_entry: MockConfigEntry, - first_time_user: MockUser, -) -> None: - """Test first time user.""" - await setup_integration(config_entry, first_time_user) - - entity_id = "sensor.testaccount1" - - state = hass.states.get(entity_id) - - assert state.state == STATE_NOT_SCROBBLING - assert state.attributes[ATTR_LAST_PLAYED] is None - assert state.attributes[ATTR_TOP_PLAYED] is None - assert state.attributes[ATTR_PLAY_COUNT] == 0 - - -async def test_update_not_playing( - hass: HomeAssistant, - setup_integration: ComponentSetup, - config_entry: MockConfigEntry, - first_time_user: MockUser, -) -> None: - """Test update when no playing song.""" - await setup_integration(config_entry, first_time_user) - - entity_id = "sensor.testaccount1" - - state = hass.states.get(entity_id) - - assert state.state == STATE_NOT_SCROBBLING - - -async def test_update_playing( +@pytest.mark.parametrize( + ("fixture"), + [ + ("not_found_user"), + ("first_time_user"), + ("default_user"), + ], +) +async def test_sensors( hass: HomeAssistant, setup_integration: ComponentSetup, config_entry: MockConfigEntry, - default_user: MockUser, + snapshot: SnapshotAssertion, + fixture: str, + request: pytest.FixtureRequest, ) -> None: - """Test update when playing a song.""" - await setup_integration(config_entry, default_user) + """Test sensors.""" + user = request.getfixturevalue(fixture) + await setup_integration(config_entry, user) entity_id = "sensor.testaccount1" state = hass.states.get(entity_id) - assert state.state == "artist - title" - assert state.attributes[ATTR_LAST_PLAYED] == "artist - title" - assert state.attributes[ATTR_TOP_PLAYED] == "artist - title" - assert state.attributes[ATTR_PLAY_COUNT] == 1 + assert state == snapshot From 123cf07920fbf6c5f626819067b1a1cba2b6648e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 22 Jul 2023 13:07:34 +0200 Subject: [PATCH 0765/1009] Clean up fitbit const (#95545) --- homeassistant/components/fitbit/const.py | 246 --------------------- homeassistant/components/fitbit/sensor.py | 256 +++++++++++++++++++++- 2 files changed, 251 insertions(+), 251 deletions(-) diff --git a/homeassistant/components/fitbit/const.py b/homeassistant/components/fitbit/const.py index d746e63ca5226b..1578359356d205 100644 --- a/homeassistant/components/fitbit/const.py +++ b/homeassistant/components/fitbit/const.py @@ -1,18 +1,11 @@ """Constants for the Fitbit platform.""" from __future__ import annotations -from dataclasses import dataclass from typing import Final -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntityDescription, - SensorStateClass, -) from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, - PERCENTAGE, UnitOfLength, UnitOfMass, UnitOfTime, @@ -49,245 +42,6 @@ DEFAULT_CLOCK_FORMAT: Final = "24H" -@dataclass -class FitbitSensorEntityDescription(SensorEntityDescription): - """Describes Fitbit sensor entity.""" - - unit_type: str | None = None - - -FITBIT_RESOURCES_LIST: Final[tuple[FitbitSensorEntityDescription, ...]] = ( - FitbitSensorEntityDescription( - key="activities/activityCalories", - name="Activity Calories", - native_unit_of_measurement="cal", - icon="mdi:fire", - ), - FitbitSensorEntityDescription( - key="activities/calories", - name="Calories", - native_unit_of_measurement="cal", - icon="mdi:fire", - ), - FitbitSensorEntityDescription( - key="activities/caloriesBMR", - name="Calories BMR", - native_unit_of_measurement="cal", - icon="mdi:fire", - ), - FitbitSensorEntityDescription( - key="activities/distance", - name="Distance", - unit_type="distance", - icon="mdi:map-marker", - device_class=SensorDeviceClass.DISTANCE, - ), - FitbitSensorEntityDescription( - key="activities/elevation", - name="Elevation", - unit_type="elevation", - icon="mdi:walk", - device_class=SensorDeviceClass.DISTANCE, - ), - FitbitSensorEntityDescription( - key="activities/floors", - name="Floors", - native_unit_of_measurement="floors", - icon="mdi:walk", - ), - FitbitSensorEntityDescription( - key="activities/heart", - name="Resting Heart Rate", - native_unit_of_measurement="bpm", - icon="mdi:heart-pulse", - ), - FitbitSensorEntityDescription( - key="activities/minutesFairlyActive", - name="Minutes Fairly Active", - native_unit_of_measurement=UnitOfTime.MINUTES, - icon="mdi:walk", - device_class=SensorDeviceClass.DURATION, - ), - FitbitSensorEntityDescription( - key="activities/minutesLightlyActive", - name="Minutes Lightly Active", - native_unit_of_measurement=UnitOfTime.MINUTES, - icon="mdi:walk", - device_class=SensorDeviceClass.DURATION, - ), - FitbitSensorEntityDescription( - key="activities/minutesSedentary", - name="Minutes Sedentary", - native_unit_of_measurement=UnitOfTime.MINUTES, - icon="mdi:seat-recline-normal", - device_class=SensorDeviceClass.DURATION, - ), - FitbitSensorEntityDescription( - key="activities/minutesVeryActive", - name="Minutes Very Active", - native_unit_of_measurement=UnitOfTime.MINUTES, - icon="mdi:run", - device_class=SensorDeviceClass.DURATION, - ), - FitbitSensorEntityDescription( - key="activities/steps", - name="Steps", - native_unit_of_measurement="steps", - icon="mdi:walk", - ), - FitbitSensorEntityDescription( - key="activities/tracker/activityCalories", - name="Tracker Activity Calories", - native_unit_of_measurement="cal", - icon="mdi:fire", - ), - FitbitSensorEntityDescription( - key="activities/tracker/calories", - name="Tracker Calories", - native_unit_of_measurement="cal", - icon="mdi:fire", - ), - FitbitSensorEntityDescription( - key="activities/tracker/distance", - name="Tracker Distance", - unit_type="distance", - icon="mdi:map-marker", - device_class=SensorDeviceClass.DISTANCE, - ), - FitbitSensorEntityDescription( - key="activities/tracker/elevation", - name="Tracker Elevation", - unit_type="elevation", - icon="mdi:walk", - device_class=SensorDeviceClass.DISTANCE, - ), - FitbitSensorEntityDescription( - key="activities/tracker/floors", - name="Tracker Floors", - native_unit_of_measurement="floors", - icon="mdi:walk", - ), - FitbitSensorEntityDescription( - key="activities/tracker/minutesFairlyActive", - name="Tracker Minutes Fairly Active", - native_unit_of_measurement=UnitOfTime.MINUTES, - icon="mdi:walk", - device_class=SensorDeviceClass.DURATION, - ), - FitbitSensorEntityDescription( - key="activities/tracker/minutesLightlyActive", - name="Tracker Minutes Lightly Active", - native_unit_of_measurement=UnitOfTime.MINUTES, - icon="mdi:walk", - device_class=SensorDeviceClass.DURATION, - ), - FitbitSensorEntityDescription( - key="activities/tracker/minutesSedentary", - name="Tracker Minutes Sedentary", - native_unit_of_measurement=UnitOfTime.MINUTES, - icon="mdi:seat-recline-normal", - device_class=SensorDeviceClass.DURATION, - ), - FitbitSensorEntityDescription( - key="activities/tracker/minutesVeryActive", - name="Tracker Minutes Very Active", - native_unit_of_measurement=UnitOfTime.MINUTES, - icon="mdi:run", - device_class=SensorDeviceClass.DURATION, - ), - FitbitSensorEntityDescription( - key="activities/tracker/steps", - name="Tracker Steps", - native_unit_of_measurement="steps", - icon="mdi:walk", - ), - FitbitSensorEntityDescription( - key="body/bmi", - name="BMI", - native_unit_of_measurement="BMI", - icon="mdi:human", - state_class=SensorStateClass.MEASUREMENT, - ), - FitbitSensorEntityDescription( - key="body/fat", - name="Body Fat", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:human", - state_class=SensorStateClass.MEASUREMENT, - ), - FitbitSensorEntityDescription( - key="body/weight", - name="Weight", - unit_type="weight", - icon="mdi:human", - state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.WEIGHT, - ), - FitbitSensorEntityDescription( - key="sleep/awakeningsCount", - name="Awakenings Count", - native_unit_of_measurement="times awaken", - icon="mdi:sleep", - ), - FitbitSensorEntityDescription( - key="sleep/efficiency", - name="Sleep Efficiency", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:sleep", - state_class=SensorStateClass.MEASUREMENT, - ), - FitbitSensorEntityDescription( - key="sleep/minutesAfterWakeup", - name="Minutes After Wakeup", - native_unit_of_measurement=UnitOfTime.MINUTES, - icon="mdi:sleep", - device_class=SensorDeviceClass.DURATION, - ), - FitbitSensorEntityDescription( - key="sleep/minutesAsleep", - name="Sleep Minutes Asleep", - native_unit_of_measurement=UnitOfTime.MINUTES, - icon="mdi:sleep", - device_class=SensorDeviceClass.DURATION, - ), - FitbitSensorEntityDescription( - key="sleep/minutesAwake", - name="Sleep Minutes Awake", - native_unit_of_measurement=UnitOfTime.MINUTES, - icon="mdi:sleep", - device_class=SensorDeviceClass.DURATION, - ), - FitbitSensorEntityDescription( - key="sleep/minutesToFallAsleep", - name="Sleep Minutes to Fall Asleep", - native_unit_of_measurement=UnitOfTime.MINUTES, - icon="mdi:sleep", - device_class=SensorDeviceClass.DURATION, - ), - FitbitSensorEntityDescription( - key="sleep/startTime", - name="Sleep Start Time", - icon="mdi:clock", - ), - FitbitSensorEntityDescription( - key="sleep/timeInBed", - name="Sleep Time in Bed", - native_unit_of_measurement=UnitOfTime.MINUTES, - icon="mdi:hotel", - device_class=SensorDeviceClass.DURATION, - ), -) - -FITBIT_RESOURCE_BATTERY = FitbitSensorEntityDescription( - key="devices/battery", - name="Battery", - icon="mdi:battery", -) - -FITBIT_RESOURCES_KEYS: Final[list[str]] = [ - desc.key for desc in (*FITBIT_RESOURCES_LIST, FITBIT_RESOURCE_BATTERY) -] - FITBIT_MEASUREMENTS: Final[dict[str, dict[str, str]]] = { "en_US": { ATTR_DURATION: UnitOfTime.MILLISECONDS, diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 11946c421730a0..6c93fbe35c1d2e 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -1,6 +1,7 @@ """Support for the Fitbit API.""" from __future__ import annotations +from dataclasses import dataclass import datetime import logging import os @@ -17,9 +18,18 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.components.sensor import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_UNIT_SYSTEM, + PERCENTAGE, + UnitOfTime, ) -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_UNIT_SYSTEM from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -45,10 +55,6 @@ FITBIT_CONFIG_FILE, FITBIT_DEFAULT_RESOURCES, FITBIT_MEASUREMENTS, - FITBIT_RESOURCE_BATTERY, - FITBIT_RESOURCES_KEYS, - FITBIT_RESOURCES_LIST, - FitbitSensorEntityDescription, ) _LOGGER: Final = logging.getLogger(__name__) @@ -57,6 +63,246 @@ SCAN_INTERVAL: Final = datetime.timedelta(minutes=30) + +@dataclass +class FitbitSensorEntityDescription(SensorEntityDescription): + """Describes Fitbit sensor entity.""" + + unit_type: str | None = None + + +FITBIT_RESOURCES_LIST: Final[tuple[FitbitSensorEntityDescription, ...]] = ( + FitbitSensorEntityDescription( + key="activities/activityCalories", + name="Activity Calories", + native_unit_of_measurement="cal", + icon="mdi:fire", + ), + FitbitSensorEntityDescription( + key="activities/calories", + name="Calories", + native_unit_of_measurement="cal", + icon="mdi:fire", + ), + FitbitSensorEntityDescription( + key="activities/caloriesBMR", + name="Calories BMR", + native_unit_of_measurement="cal", + icon="mdi:fire", + ), + FitbitSensorEntityDescription( + key="activities/distance", + name="Distance", + unit_type="distance", + icon="mdi:map-marker", + device_class=SensorDeviceClass.DISTANCE, + ), + FitbitSensorEntityDescription( + key="activities/elevation", + name="Elevation", + unit_type="elevation", + icon="mdi:walk", + device_class=SensorDeviceClass.DISTANCE, + ), + FitbitSensorEntityDescription( + key="activities/floors", + name="Floors", + native_unit_of_measurement="floors", + icon="mdi:walk", + ), + FitbitSensorEntityDescription( + key="activities/heart", + name="Resting Heart Rate", + native_unit_of_measurement="bpm", + icon="mdi:heart-pulse", + ), + FitbitSensorEntityDescription( + key="activities/minutesFairlyActive", + name="Minutes Fairly Active", + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:walk", + device_class=SensorDeviceClass.DURATION, + ), + FitbitSensorEntityDescription( + key="activities/minutesLightlyActive", + name="Minutes Lightly Active", + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:walk", + device_class=SensorDeviceClass.DURATION, + ), + FitbitSensorEntityDescription( + key="activities/minutesSedentary", + name="Minutes Sedentary", + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:seat-recline-normal", + device_class=SensorDeviceClass.DURATION, + ), + FitbitSensorEntityDescription( + key="activities/minutesVeryActive", + name="Minutes Very Active", + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:run", + device_class=SensorDeviceClass.DURATION, + ), + FitbitSensorEntityDescription( + key="activities/steps", + name="Steps", + native_unit_of_measurement="steps", + icon="mdi:walk", + ), + FitbitSensorEntityDescription( + key="activities/tracker/activityCalories", + name="Tracker Activity Calories", + native_unit_of_measurement="cal", + icon="mdi:fire", + ), + FitbitSensorEntityDescription( + key="activities/tracker/calories", + name="Tracker Calories", + native_unit_of_measurement="cal", + icon="mdi:fire", + ), + FitbitSensorEntityDescription( + key="activities/tracker/distance", + name="Tracker Distance", + unit_type="distance", + icon="mdi:map-marker", + device_class=SensorDeviceClass.DISTANCE, + ), + FitbitSensorEntityDescription( + key="activities/tracker/elevation", + name="Tracker Elevation", + unit_type="elevation", + icon="mdi:walk", + device_class=SensorDeviceClass.DISTANCE, + ), + FitbitSensorEntityDescription( + key="activities/tracker/floors", + name="Tracker Floors", + native_unit_of_measurement="floors", + icon="mdi:walk", + ), + FitbitSensorEntityDescription( + key="activities/tracker/minutesFairlyActive", + name="Tracker Minutes Fairly Active", + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:walk", + device_class=SensorDeviceClass.DURATION, + ), + FitbitSensorEntityDescription( + key="activities/tracker/minutesLightlyActive", + name="Tracker Minutes Lightly Active", + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:walk", + device_class=SensorDeviceClass.DURATION, + ), + FitbitSensorEntityDescription( + key="activities/tracker/minutesSedentary", + name="Tracker Minutes Sedentary", + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:seat-recline-normal", + device_class=SensorDeviceClass.DURATION, + ), + FitbitSensorEntityDescription( + key="activities/tracker/minutesVeryActive", + name="Tracker Minutes Very Active", + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:run", + device_class=SensorDeviceClass.DURATION, + ), + FitbitSensorEntityDescription( + key="activities/tracker/steps", + name="Tracker Steps", + native_unit_of_measurement="steps", + icon="mdi:walk", + ), + FitbitSensorEntityDescription( + key="body/bmi", + name="BMI", + native_unit_of_measurement="BMI", + icon="mdi:human", + state_class=SensorStateClass.MEASUREMENT, + ), + FitbitSensorEntityDescription( + key="body/fat", + name="Body Fat", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:human", + state_class=SensorStateClass.MEASUREMENT, + ), + FitbitSensorEntityDescription( + key="body/weight", + name="Weight", + unit_type="weight", + icon="mdi:human", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.WEIGHT, + ), + FitbitSensorEntityDescription( + key="sleep/awakeningsCount", + name="Awakenings Count", + native_unit_of_measurement="times awaken", + icon="mdi:sleep", + ), + FitbitSensorEntityDescription( + key="sleep/efficiency", + name="Sleep Efficiency", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:sleep", + state_class=SensorStateClass.MEASUREMENT, + ), + FitbitSensorEntityDescription( + key="sleep/minutesAfterWakeup", + name="Minutes After Wakeup", + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:sleep", + device_class=SensorDeviceClass.DURATION, + ), + FitbitSensorEntityDescription( + key="sleep/minutesAsleep", + name="Sleep Minutes Asleep", + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:sleep", + device_class=SensorDeviceClass.DURATION, + ), + FitbitSensorEntityDescription( + key="sleep/minutesAwake", + name="Sleep Minutes Awake", + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:sleep", + device_class=SensorDeviceClass.DURATION, + ), + FitbitSensorEntityDescription( + key="sleep/minutesToFallAsleep", + name="Sleep Minutes to Fall Asleep", + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:sleep", + device_class=SensorDeviceClass.DURATION, + ), + FitbitSensorEntityDescription( + key="sleep/startTime", + name="Sleep Start Time", + icon="mdi:clock", + ), + FitbitSensorEntityDescription( + key="sleep/timeInBed", + name="Sleep Time in Bed", + native_unit_of_measurement=UnitOfTime.MINUTES, + icon="mdi:hotel", + device_class=SensorDeviceClass.DURATION, + ), +) + +FITBIT_RESOURCE_BATTERY = FitbitSensorEntityDescription( + key="devices/battery", + name="Battery", + icon="mdi:battery", +) + +FITBIT_RESOURCES_KEYS: Final[list[str]] = [ + desc.key for desc in (*FITBIT_RESOURCES_LIST, FITBIT_RESOURCE_BATTERY) +] + PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend( { vol.Optional( From 24b9bde9e5792cb3635e21e23ffd3377a8a7d2bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Jul 2023 06:10:41 -0500 Subject: [PATCH 0766/1009] Fix duplicate and missing decorators in ESPHome Bluetooth client (#97027) --- .../components/esphome/bluetooth/client.py | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index ee629eed6f972b..748035bedac3ea 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -364,16 +364,18 @@ async def connect( await connected_future try: - await self.get_services(dangerous_use_bleak_cache=dangerous_use_bleak_cache) + await self._get_services( + dangerous_use_bleak_cache=dangerous_use_bleak_cache + ) except asyncio.CancelledError: # On cancel we must still raise cancelled error # to avoid blocking the cancellation even if the # disconnect call fails. with contextlib.suppress(Exception): - await self.disconnect() + await self._disconnect() raise except Exception: - await self.disconnect() + await self._disconnect() raise return True @@ -381,6 +383,9 @@ async def connect( @api_error_as_bleak_error async def disconnect(self) -> bool: """Disconnect from the peripheral device.""" + return await self._disconnect() + + async def _disconnect(self) -> bool: self._async_disconnected_cleanup() await self._client.bluetooth_device_disconnect(self._address_as_int) await self._wait_for_free_connection_slot(DISCONNECT_TIMEOUT) @@ -454,6 +459,18 @@ async def get_services( A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree. """ + return await self._get_services( + dangerous_use_bleak_cache=dangerous_use_bleak_cache, **kwargs + ) + + @verify_connected + async def _get_services( + self, dangerous_use_bleak_cache: bool = False, **kwargs: Any + ) -> BleakGATTServiceCollection: + """Get all services registered for this GATT server. + + Must only be called from get_services or connected + """ address_as_int = self._address_as_int cache = self._cache # If the connection version >= 3, we must use the cache @@ -538,6 +555,7 @@ def _resolve_characteristic( raise BleakError(f"Characteristic {char_specifier} was not found!") return characteristic + @verify_connected @api_error_as_bleak_error async def clear_cache(self) -> bool: """Clear the GATT cache.""" @@ -726,6 +744,7 @@ def callback(sender: int, data: bytearray): wait_for_response=False, ) + @verify_connected @api_error_as_bleak_error async def stop_notify( self, From e2fdc6a98bdd22187688e70701fc3617423a714b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 22 Jul 2023 13:16:07 +0200 Subject: [PATCH 0767/1009] Add entity translations for Ondilo Ico (#95809) --- homeassistant/components/ondilo_ico/sensor.py | 15 +++++++-------- .../components/ondilo_ico/strings.json | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index 5e226dcead7656..8b4cfcb61a461c 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -35,48 +35,46 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="temperature", - name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="orp", - name="Oxydo Reduction Potential", + translation_key="oxydo_reduction_potential", native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, icon="mdi:pool", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ph", - name="pH", + translation_key="ph", icon="mdi:pool", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="tds", - name="TDS", + translation_key="tds", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, icon="mdi:pool", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="battery", - name="Battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="rssi", - name="RSSI", + translation_key="rssi", icon="mdi:wifi", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="salt", - name="Salt", + translation_key="salt", native_unit_of_measurement="mg/L", icon="mdi:pool", state_class=SensorStateClass.MEASUREMENT, @@ -139,6 +137,8 @@ class OndiloICO( ): """Representation of a Sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator[list[dict[str, Any]]], @@ -154,7 +154,6 @@ def __init__( pooldata = self._pooldata() self._attr_unique_id = f"{pooldata['ICO']['serial_number']}-{description.key}" self._device_name = pooldata["name"] - self._attr_name = f"{self._device_name} {description.name}" def _pooldata(self): """Get pool data dict.""" diff --git a/homeassistant/components/ondilo_ico/strings.json b/homeassistant/components/ondilo_ico/strings.json index 4e5f2330840ee7..3843670bc50c5a 100644 --- a/homeassistant/components/ondilo_ico/strings.json +++ b/homeassistant/components/ondilo_ico/strings.json @@ -12,5 +12,24 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "entity": { + "sensor": { + "oxydo_reduction_potential": { + "name": "Oxydo reduction potential" + }, + "ph": { + "name": "pH" + }, + "tds": { + "name": "TDS" + }, + "rssi": { + "name": "RSSI" + }, + "salt": { + "name": "Salt" + } + } } } From fe0d33d97cfbe400b7f4200d54cb83e23a3686a8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 22 Jul 2023 16:12:28 +0200 Subject: [PATCH 0768/1009] Move Aseko coordinator to separate file (#95120) --- .coveragerc | 1 + .../components/aseko_pool_live/__init__.py | 30 +-------------- .../aseko_pool_live/binary_sensor.py | 4 +- .../components/aseko_pool_live/coordinator.py | 37 +++++++++++++++++++ .../components/aseko_pool_live/entity.py | 2 +- .../components/aseko_pool_live/sensor.py | 4 +- 6 files changed, 45 insertions(+), 33 deletions(-) create mode 100644 homeassistant/components/aseko_pool_live/coordinator.py diff --git a/.coveragerc b/.coveragerc index a5397971d1fc67..4f3e82042f6cc0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -82,6 +82,7 @@ omit = homeassistant/components/arwn/sensor.py homeassistant/components/aseko_pool_live/__init__.py homeassistant/components/aseko_pool_live/binary_sensor.py + homeassistant/components/aseko_pool_live/coordinator.py homeassistant/components/aseko_pool_live/entity.py homeassistant/components/aseko_pool_live/sensor.py homeassistant/components/asterisk_cdr/mailbox.py diff --git a/homeassistant/components/aseko_pool_live/__init__.py b/homeassistant/components/aseko_pool_live/__init__.py index 70a66251bdcf8d..b09682fcaf99f4 100644 --- a/homeassistant/components/aseko_pool_live/__init__.py +++ b/homeassistant/components/aseko_pool_live/__init__.py @@ -1,19 +1,18 @@ """The Aseko Pool Live integration.""" from __future__ import annotations -from datetime import timedelta import logging -from aioaseko import APIUnavailable, MobileAccount, Unit, Variable +from aioaseko import APIUnavailable, MobileAccount from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN +from .coordinator import AsekoDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -49,28 +48,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok - - -class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Variable]]): - """Class to manage fetching Aseko unit data from single endpoint.""" - - def __init__(self, hass: HomeAssistant, unit: Unit) -> None: - """Initialize global Aseko unit data updater.""" - self._unit = unit - - if self._unit.name: - name = self._unit.name - else: - name = f"{self._unit.type}-{self._unit.serial_number}" - - super().__init__( - hass, - _LOGGER, - name=name, - update_interval=timedelta(minutes=2), - ) - - async def _async_update_data(self) -> dict[str, Variable]: - """Fetch unit data.""" - await self._unit.get_state() - return {variable.type: variable for variable in self._unit.variables} diff --git a/homeassistant/components/aseko_pool_live/binary_sensor.py b/homeassistant/components/aseko_pool_live/binary_sensor.py index f67ea58bfc4d24..8178e2432798f6 100644 --- a/homeassistant/components/aseko_pool_live/binary_sensor.py +++ b/homeassistant/components/aseko_pool_live/binary_sensor.py @@ -15,8 +15,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AsekoDataUpdateCoordinator from .const import DOMAIN +from .coordinator import AsekoDataUpdateCoordinator from .entity import AsekoEntity @@ -31,7 +31,7 @@ class AsekoBinarySensorDescriptionMixin: class AsekoBinarySensorEntityDescription( BinarySensorEntityDescription, AsekoBinarySensorDescriptionMixin ): - """Describes a Aseko binary sensor entity.""" + """Describes an Aseko binary sensor entity.""" UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = ( diff --git a/homeassistant/components/aseko_pool_live/coordinator.py b/homeassistant/components/aseko_pool_live/coordinator.py new file mode 100644 index 00000000000000..383ab7116b68ef --- /dev/null +++ b/homeassistant/components/aseko_pool_live/coordinator.py @@ -0,0 +1,37 @@ +"""The Aseko Pool Live integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from aioaseko import Unit, Variable + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Variable]]): + """Class to manage fetching Aseko unit data from single endpoint.""" + + def __init__(self, hass: HomeAssistant, unit: Unit) -> None: + """Initialize global Aseko unit data updater.""" + self._unit = unit + + if self._unit.name: + name = self._unit.name + else: + name = f"{self._unit.type}-{self._unit.serial_number}" + + super().__init__( + hass, + _LOGGER, + name=name, + update_interval=timedelta(minutes=2), + ) + + async def _async_update_data(self) -> dict[str, Variable]: + """Fetch unit data.""" + await self._unit.get_state() + return {variable.type: variable for variable in self._unit.variables} diff --git a/homeassistant/components/aseko_pool_live/entity.py b/homeassistant/components/aseko_pool_live/entity.py index 58974bcc326014..9cc402e014c018 100644 --- a/homeassistant/components/aseko_pool_live/entity.py +++ b/homeassistant/components/aseko_pool_live/entity.py @@ -4,8 +4,8 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import AsekoDataUpdateCoordinator from .const import DOMAIN +from .coordinator import AsekoDataUpdateCoordinator class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]): diff --git a/homeassistant/components/aseko_pool_live/sensor.py b/homeassistant/components/aseko_pool_live/sensor.py index 74051ef454f079..09c4af31428b7e 100644 --- a/homeassistant/components/aseko_pool_live/sensor.py +++ b/homeassistant/components/aseko_pool_live/sensor.py @@ -12,8 +12,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AsekoDataUpdateCoordinator from .const import DOMAIN +from .coordinator import AsekoDataUpdateCoordinator from .entity import AsekoEntity @@ -36,7 +36,7 @@ async def async_setup_entry( class VariableSensorEntity(AsekoEntity, SensorEntity): """Representation of a unit variable sensor entity.""" - attr_state_class = SensorStateClass.MEASUREMENT + _attr_state_class = SensorStateClass.MEASUREMENT def __init__( self, unit: Unit, variable: Variable, coordinator: AsekoDataUpdateCoordinator From d708c159e748fde3e2a13333c21afbff4b455602 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 22 Jul 2023 16:16:39 +0200 Subject: [PATCH 0769/1009] Add entity translations to iCloud (#95461) --- homeassistant/components/icloud/device_tracker.py | 8 +++----- homeassistant/components/icloud/sensor.py | 6 +----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index d9bd215d2a1b59..6cabe51fff5a7b 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -56,6 +56,9 @@ def add_entities(account: IcloudAccount, async_add_entities, tracked): class IcloudTrackerEntity(TrackerEntity): """Represent a tracked device.""" + _attr_has_entity_name = True + _attr_name = None + def __init__(self, account: IcloudAccount, device: IcloudDevice) -> None: """Set up the iCloud tracker entity.""" self._account = account @@ -67,11 +70,6 @@ def unique_id(self) -> str: """Return a unique ID.""" return self._device.unique_id - @property - def name(self) -> str: - """Return the name of the device.""" - return self._device.name - @property def location_accuracy(self): """Return the location accuracy of the device.""" diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index e7c982607cbf85..01aabc5871c960 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -56,6 +56,7 @@ class IcloudDeviceBatterySensor(SensorEntity): _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE _attr_should_poll = False + _attr_has_entity_name = True def __init__(self, account: IcloudAccount, device: IcloudDevice) -> None: """Initialize the battery sensor.""" @@ -68,11 +69,6 @@ def unique_id(self) -> str: """Return a unique ID.""" return f"{self._device.unique_id}_battery" - @property - def name(self) -> str: - """Sensor name.""" - return f"{self._device.name} battery state" - @property def native_value(self) -> int | None: """Battery state percentage.""" From 47426e50d3abcd0c7ab7cdd3369c8158caf7c1a7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 22 Jul 2023 16:19:37 +0200 Subject: [PATCH 0770/1009] Add entity translations to Modern Forms (#95738) --- .../components/modern_forms/__init__.py | 4 +-- .../components/modern_forms/binary_sensor.py | 9 ++--- homeassistant/components/modern_forms/fan.py | 2 +- .../components/modern_forms/light.py | 2 +- .../components/modern_forms/sensor.py | 11 +++--- .../components/modern_forms/strings.json | 36 +++++++++++++++++++ .../components/modern_forms/switch.py | 11 +++--- 7 files changed, 53 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index d00fe793bf8be5..d7f30ce5c3b272 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -121,12 +121,13 @@ async def _async_update_data(self) -> ModernFormsDevice: class ModernFormsDeviceEntity(CoordinatorEntity[ModernFormsDataUpdateCoordinator]): """Defines a Modern Forms device entity.""" + _attr_has_entity_name = True + def __init__( self, *, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator, - name: str, icon: str | None = None, enabled_default: bool = True, ) -> None: @@ -135,7 +136,6 @@ def __init__( self._attr_enabled_default = enabled_default self._entry_id = entry_id self._attr_icon = icon - self._attr_name = name @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/modern_forms/binary_sensor.py b/homeassistant/components/modern_forms/binary_sensor.py index f8e3f8bbcf8f22..b3361c3f143a7a 100644 --- a/homeassistant/components/modern_forms/binary_sensor.py +++ b/homeassistant/components/modern_forms/binary_sensor.py @@ -40,14 +40,11 @@ def __init__( *, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator, - name: str, icon: str, key: str, ) -> None: """Initialize Modern Forms switch.""" - super().__init__( - entry_id=entry_id, coordinator=coordinator, name=name, icon=icon - ) + super().__init__(entry_id=entry_id, coordinator=coordinator, icon=icon) self._attr_unique_id = f"{coordinator.data.info.mac_address}_{key}" @@ -56,6 +53,7 @@ class ModernFormsLightSleepTimerActive(ModernFormsBinarySensor): """Defines a Modern Forms Light Sleep Timer Active sensor.""" _attr_entity_registry_enabled_default = False + _attr_translation_key = "light_sleep_timer_active" def __init__( self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator @@ -66,7 +64,6 @@ def __init__( entry_id=entry_id, icon="mdi:av-timer", key="light_sleep_timer_active", - name=f"{coordinator.data.info.device_name} Light Sleep Timer Active", ) @property @@ -88,6 +85,7 @@ class ModernFormsFanSleepTimerActive(ModernFormsBinarySensor): """Defines a Modern Forms Fan Sleep Timer Active sensor.""" _attr_entity_registry_enabled_default = False + _attr_translation_key = "fan_sleep_timer_active" def __init__( self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator @@ -98,7 +96,6 @@ def __init__( entry_id=entry_id, icon="mdi:av-timer", key="fan_sleep_timer_active", - name=f"{coordinator.data.info.device_name} Fan Sleep Timer Active", ) @property diff --git a/homeassistant/components/modern_forms/fan.py b/homeassistant/components/modern_forms/fan.py index 8bd8665dc3b569..9d5a3c3223557a 100644 --- a/homeassistant/components/modern_forms/fan.py +++ b/homeassistant/components/modern_forms/fan.py @@ -73,6 +73,7 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity): SPEED_RANGE = (1, 6) # off is not included _attr_supported_features = FanEntityFeature.DIRECTION | FanEntityFeature.SET_SPEED + _attr_translation_key = "fan" def __init__( self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator @@ -81,7 +82,6 @@ def __init__( super().__init__( entry_id=entry_id, coordinator=coordinator, - name=f"{coordinator.data.info.device_name} Fan", ) self._attr_unique_id = f"{self.coordinator.data.info.mac_address}" diff --git a/homeassistant/components/modern_forms/light.py b/homeassistant/components/modern_forms/light.py index 55569054ac46d8..013d6a17d6d666 100644 --- a/homeassistant/components/modern_forms/light.py +++ b/homeassistant/components/modern_forms/light.py @@ -81,6 +81,7 @@ class ModernFormsLightEntity(ModernFormsDeviceEntity, LightEntity): _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} + _attr_translation_key = "light" def __init__( self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator @@ -89,7 +90,6 @@ def __init__( super().__init__( entry_id=entry_id, coordinator=coordinator, - name=f"{coordinator.data.info.device_name} Light", icon=None, ) self._attr_unique_id = f"{self.coordinator.data.info.mac_address}" diff --git a/homeassistant/components/modern_forms/sensor.py b/homeassistant/components/modern_forms/sensor.py index 6d2ef5b6dab47e..efd659f3ae0166 100644 --- a/homeassistant/components/modern_forms/sensor.py +++ b/homeassistant/components/modern_forms/sensor.py @@ -43,21 +43,20 @@ def __init__( *, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator, - name: str, icon: str, key: str, ) -> None: """Initialize Modern Forms switch.""" self._key = key - super().__init__( - entry_id=entry_id, coordinator=coordinator, name=name, icon=icon - ) + super().__init__(entry_id=entry_id, coordinator=coordinator, icon=icon) self._attr_unique_id = f"{self.coordinator.data.info.mac_address}_{self._key}" class ModernFormsLightTimerRemainingTimeSensor(ModernFormsSensor): """Defines the Modern Forms Light Timer remaining time sensor.""" + _attr_translation_key = "light_timer_remaining_time" + def __init__( self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator ) -> None: @@ -67,7 +66,6 @@ def __init__( entry_id=entry_id, icon="mdi:timer-outline", key="light_timer_remaining_time", - name=f"{coordinator.data.info.device_name} Light Sleep Time", ) self._attr_device_class = SensorDeviceClass.TIMESTAMP @@ -88,6 +86,8 @@ def native_value(self) -> StateType | datetime: class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor): """Defines the Modern Forms Light Timer remaining time sensor.""" + _attr_translation_key = "fan_timer_remaining_time" + def __init__( self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator ) -> None: @@ -97,7 +97,6 @@ def __init__( entry_id=entry_id, icon="mdi:timer-outline", key="fan_timer_remaining_time", - name=f"{coordinator.data.info.device_name} Fan Sleep Time", ) self._attr_device_class = SensorDeviceClass.TIMESTAMP diff --git a/homeassistant/components/modern_forms/strings.json b/homeassistant/components/modern_forms/strings.json index defe412e96dd83..dd47ef721af316 100644 --- a/homeassistant/components/modern_forms/strings.json +++ b/homeassistant/components/modern_forms/strings.json @@ -21,6 +21,42 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } }, + "entity": { + "binary_sensor": { + "light_sleep_timer_active": { + "name": "Light sleep timer active" + }, + "fan_sleep_timer_active": { + "name": "Fan sleep timer active" + } + }, + "fan": { + "fan": { + "name": "[%key:component::fan::title%]" + } + }, + "light": { + "light": { + "name": "[%key:component::light::title%]" + } + }, + "sensor": { + "light_timer_remaining_time": { + "name": "Light sleep time" + }, + "fan_timer_remaining_time": { + "name": "Fan sleep time" + } + }, + "switch": { + "away_mode": { + "name": "Away mode" + }, + "adaptive_learning": { + "name": "Adaptive learning" + } + } + }, "services": { "set_light_sleep_timer": { "name": "Set light sleep timer", diff --git a/homeassistant/components/modern_forms/switch.py b/homeassistant/components/modern_forms/switch.py index 90d5d13d64996d..18d8caccbd60d0 100644 --- a/homeassistant/components/modern_forms/switch.py +++ b/homeassistant/components/modern_forms/switch.py @@ -39,21 +39,20 @@ def __init__( *, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator, - name: str, icon: str, key: str, ) -> None: """Initialize Modern Forms switch.""" self._key = key - super().__init__( - entry_id=entry_id, coordinator=coordinator, name=name, icon=icon - ) + super().__init__(entry_id=entry_id, coordinator=coordinator, icon=icon) self._attr_unique_id = f"{self.coordinator.data.info.mac_address}_{self._key}" class ModernFormsAwaySwitch(ModernFormsSwitch): """Defines a Modern Forms Away mode switch.""" + _attr_translation_key = "away_mode" + def __init__( self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator ) -> None: @@ -63,7 +62,6 @@ def __init__( entry_id=entry_id, icon="mdi:airplane-takeoff", key="away_mode", - name=f"{coordinator.data.info.device_name} Away Mode", ) @property @@ -85,6 +83,8 @@ async def async_turn_on(self, **kwargs: Any) -> None: class ModernFormsAdaptiveLearningSwitch(ModernFormsSwitch): """Defines a Modern Forms Adaptive Learning switch.""" + _attr_translation_key = "adaptive_learning" + def __init__( self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator ) -> None: @@ -94,7 +94,6 @@ def __init__( entry_id=entry_id, icon="mdi:school-outline", key="adaptive_learning", - name=f"{coordinator.data.info.device_name} Adaptive Learning", ) @property From 11fd43b1fc33c35b68858878f94bfb49a407f3e6 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 22 Jul 2023 16:28:48 +0200 Subject: [PATCH 0771/1009] Add entity translations to Wiz (#96826) --- homeassistant/components/wiz/binary_sensor.py | 1 - homeassistant/components/wiz/number.py | 4 ++-- homeassistant/components/wiz/sensor.py | 2 -- homeassistant/components/wiz/strings.json | 10 ++++++++++ tests/components/wiz/test_sensor.py | 2 +- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wiz/binary_sensor.py b/homeassistant/components/wiz/binary_sensor.py index 538bd3a741de83..6b3caf23a1c9e4 100644 --- a/homeassistant/components/wiz/binary_sensor.py +++ b/homeassistant/components/wiz/binary_sensor.py @@ -66,7 +66,6 @@ class WizOccupancyEntity(WizEntity, BinarySensorEntity): """Representation of WiZ Occupancy sensor.""" _attr_device_class = BinarySensorDeviceClass.OCCUPANCY - _attr_name = "Occupancy" def __init__(self, wiz_data: WizData, name: str) -> None: """Initialize an WiZ device.""" diff --git a/homeassistant/components/wiz/number.py b/homeassistant/components/wiz/number.py index be1cf61ae09a72..f1212c75f25102 100644 --- a/homeassistant/components/wiz/number.py +++ b/homeassistant/components/wiz/number.py @@ -49,11 +49,11 @@ async def _async_set_ratio(device: wizlight, ratio: int) -> None: NUMBERS: tuple[WizNumberEntityDescription, ...] = ( WizNumberEntityDescription( key="effect_speed", + translation_key="effect_speed", native_min_value=10, native_max_value=200, native_step=1, icon="mdi:speedometer", - name="Effect speed", value_fn=lambda device: cast(int | None, device.state.get_speed()), set_value_fn=_async_set_speed, required_feature="effect", @@ -61,11 +61,11 @@ async def _async_set_ratio(device: wizlight, ratio: int) -> None: ), WizNumberEntityDescription( key="dual_head_ratio", + translation_key="dual_head_ratio", native_min_value=0, native_max_value=100, native_step=1, icon="mdi:floor-lamp-dual", - name="Dual head ratio", value_fn=lambda device: cast(int | None, device.state.get_ratio()), set_value_fn=_async_set_ratio, required_feature="dual_head", diff --git a/homeassistant/components/wiz/sensor.py b/homeassistant/components/wiz/sensor.py index e5346e00081233..a66c37fabb5442 100644 --- a/homeassistant/components/wiz/sensor.py +++ b/homeassistant/components/wiz/sensor.py @@ -23,7 +23,6 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="rssi", - name="Signal strength", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -36,7 +35,6 @@ POWER_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="power", - name="Current power", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.WATT, diff --git a/homeassistant/components/wiz/strings.json b/homeassistant/components/wiz/strings.json index 656219f13bbe1c..b75e199fe33b72 100644 --- a/homeassistant/components/wiz/strings.json +++ b/homeassistant/components/wiz/strings.json @@ -29,5 +29,15 @@ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "number": { + "effect_speed": { + "name": "Effect speed" + }, + "dual_head_ratio": { + "name": "Dual head ratio" + } + } } } diff --git a/tests/components/wiz/test_sensor.py b/tests/components/wiz/test_sensor.py index a1eb6ded51d681..522eb5c7cbaedf 100644 --- a/tests/components/wiz/test_sensor.py +++ b/tests/components/wiz/test_sensor.py @@ -49,7 +49,7 @@ async def test_power_monitoring(hass: HomeAssistant) -> None: _, entry = await async_setup_integration( hass, wizlight=socket, bulb_type=FAKE_SOCKET_WITH_POWER_MONITORING ) - entity_id = "sensor.mock_title_current_power" + entity_id = "sensor.mock_title_power" entity_registry = er.async_get(hass) reg_entry = entity_registry.async_get(entity_id) assert reg_entry.unique_id == f"{FAKE_MAC}_power" From 9ca288858b5888b884103d9d74e5c60229f9b07d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 22 Jul 2023 16:31:36 +0200 Subject: [PATCH 0772/1009] Add entity translations to IntelliFire (#95466) --- .../components/intellifire/binary_sensor.py | 30 +++--- homeassistant/components/intellifire/fan.py | 2 +- homeassistant/components/intellifire/light.py | 2 +- .../components/intellifire/number.py | 4 +- .../components/intellifire/sensor.py | 19 ++-- .../components/intellifire/strings.json | 101 ++++++++++++++++++ .../components/intellifire/switch.py | 4 +- 7 files changed, 131 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/intellifire/binary_sensor.py b/homeassistant/components/intellifire/binary_sensor.py index 5a7407836f2529..b19c592a5cf818 100644 --- a/homeassistant/components/intellifire/binary_sensor.py +++ b/homeassistant/components/intellifire/binary_sensor.py @@ -38,45 +38,45 @@ class IntellifireBinarySensorEntityDescription( INTELLIFIRE_BINARY_SENSORS: tuple[IntellifireBinarySensorEntityDescription, ...] = ( IntellifireBinarySensorEntityDescription( key="on_off", # This is the sensor name - name="Flame", # This is the human readable name + translation_key="flame", # This is the translation key icon="mdi:fire", value_fn=lambda data: data.is_on, ), IntellifireBinarySensorEntityDescription( key="timer_on", - name="Timer on", + translation_key="timer_on", icon="mdi:camera-timer", value_fn=lambda data: data.timer_on, ), IntellifireBinarySensorEntityDescription( key="pilot_light_on", - name="Pilot light on", + translation_key="pilot_light_on", icon="mdi:fire-alert", value_fn=lambda data: data.pilot_on, ), IntellifireBinarySensorEntityDescription( key="thermostat_on", - name="Thermostat on", + translation_key="thermostat_on", icon="mdi:home-thermometer-outline", value_fn=lambda data: data.thermostat_on, ), IntellifireBinarySensorEntityDescription( key="error_pilot_flame", - name="Pilot flame error", + translation_key="pilot_flame_error", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.error_pilot_flame, device_class=BinarySensorDeviceClass.PROBLEM, ), IntellifireBinarySensorEntityDescription( key="error_flame", - name="Flame Error", + translation_key="flame_error", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.error_flame, device_class=BinarySensorDeviceClass.PROBLEM, ), IntellifireBinarySensorEntityDescription( key="error_fan_delay", - name="Fan delay error", + translation_key="fan_delay_error", icon="mdi:fan-alert", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.error_fan_delay, @@ -84,21 +84,21 @@ class IntellifireBinarySensorEntityDescription( ), IntellifireBinarySensorEntityDescription( key="error_maintenance", - name="Maintenance error", + translation_key="maintenance_error", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.error_maintenance, device_class=BinarySensorDeviceClass.PROBLEM, ), IntellifireBinarySensorEntityDescription( key="error_disabled", - name="Disabled error", + translation_key="disabled_error", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.error_disabled, device_class=BinarySensorDeviceClass.PROBLEM, ), IntellifireBinarySensorEntityDescription( key="error_fan", - name="Fan error", + translation_key="fan_error", icon="mdi:fan-alert", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.error_fan, @@ -106,35 +106,35 @@ class IntellifireBinarySensorEntityDescription( ), IntellifireBinarySensorEntityDescription( key="error_lights", - name="Lights error", + translation_key="lights_error", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.error_lights, device_class=BinarySensorDeviceClass.PROBLEM, ), IntellifireBinarySensorEntityDescription( key="error_accessory", - name="Accessory error", + translation_key="accessory_error", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.error_accessory, device_class=BinarySensorDeviceClass.PROBLEM, ), IntellifireBinarySensorEntityDescription( key="error_soft_lock_out", - name="Soft lock out error", + translation_key="soft_lock_out_error", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.error_soft_lock_out, device_class=BinarySensorDeviceClass.PROBLEM, ), IntellifireBinarySensorEntityDescription( key="error_ecm_offline", - name="ECM offline error", + translation_key="ecm_offline_error", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.error_ecm_offline, device_class=BinarySensorDeviceClass.PROBLEM, ), IntellifireBinarySensorEntityDescription( key="error_offline", - name="Offline error", + translation_key="offline_error", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.error_offline, device_class=BinarySensorDeviceClass.PROBLEM, diff --git a/homeassistant/components/intellifire/fan.py b/homeassistant/components/intellifire/fan.py index debc8237fc837e..3911efeb5b9bf0 100644 --- a/homeassistant/components/intellifire/fan.py +++ b/homeassistant/components/intellifire/fan.py @@ -45,7 +45,7 @@ class IntellifireFanEntityDescription( INTELLIFIRE_FANS: tuple[IntellifireFanEntityDescription, ...] = ( IntellifireFanEntityDescription( key="fan", - name="Fan", + translation_key="fan", set_fn=lambda control_api, speed: control_api.set_fan_speed(speed=speed), value_fn=lambda data: data.fanspeed, speed_range=(1, 4), diff --git a/homeassistant/components/intellifire/light.py b/homeassistant/components/intellifire/light.py index 383d61b8d410e9..05994919296dbc 100644 --- a/homeassistant/components/intellifire/light.py +++ b/homeassistant/components/intellifire/light.py @@ -40,7 +40,7 @@ class IntellifireLightEntityDescription( INTELLIFIRE_LIGHTS: tuple[IntellifireLightEntityDescription, ...] = ( IntellifireLightEntityDescription( key="lights", - name="Lights", + translation_key="lights", set_fn=lambda control_api, level: control_api.set_lights(level=level), value_fn=lambda data: data.light_level, ), diff --git a/homeassistant/components/intellifire/number.py b/homeassistant/components/intellifire/number.py index efa567d55cbdb0..5da3c3cdbf8925 100644 --- a/homeassistant/components/intellifire/number.py +++ b/homeassistant/components/intellifire/number.py @@ -27,7 +27,7 @@ async def async_setup_entry( description = NumberEntityDescription( key="flame_control", - name="Flame control", + translation_key="flame_control", icon="mdi:arrow-expand-vertical", ) @@ -54,7 +54,7 @@ def __init__( coordinator: IntellifireDataUpdateCoordinator, description: NumberEntityDescription, ) -> None: - """Initilaize Flame height Sensor.""" + """Initialize Flame height Sensor.""" super().__init__(coordinator, description) @property diff --git a/homeassistant/components/intellifire/sensor.py b/homeassistant/components/intellifire/sensor.py index e888ea1bbcf502..bc42b977f12cff 100644 --- a/homeassistant/components/intellifire/sensor.py +++ b/homeassistant/components/intellifire/sensor.py @@ -56,15 +56,14 @@ def _downtime_to_timestamp(data: IntellifirePollData) -> datetime | None: INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = ( IntellifireSensorEntityDescription( key="flame_height", + translation_key="flame_height", icon="mdi:fire-circle", - name="Flame height", state_class=SensorStateClass.MEASUREMENT, # UI uses 1-5 for flame height, backing lib uses 0-4 value_fn=lambda data: (data.flameheight + 1), ), IntellifireSensorEntityDescription( key="temperature", - name="Temperature", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -72,7 +71,7 @@ def _downtime_to_timestamp(data: IntellifirePollData) -> datetime | None: ), IntellifireSensorEntityDescription( key="target_temp", - name="Target temperature", + translation_key="target_temp", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -80,50 +79,50 @@ def _downtime_to_timestamp(data: IntellifirePollData) -> datetime | None: ), IntellifireSensorEntityDescription( key="fan_speed", + translation_key="fan_speed", icon="mdi:fan", - name="Fan Speed", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.fanspeed, ), IntellifireSensorEntityDescription( key="timer_end_timestamp", + translation_key="timer_end_timestamp", icon="mdi:timer-sand", - name="Timer End", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TIMESTAMP, value_fn=_time_remaining_to_timestamp, ), IntellifireSensorEntityDescription( key="downtime", - name="Downtime", + translation_key="downtime", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.TIMESTAMP, value_fn=_downtime_to_timestamp, ), IntellifireSensorEntityDescription( key="uptime", - name="Uptime", + translation_key="uptime", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: utcnow() - timedelta(seconds=data.uptime), ), IntellifireSensorEntityDescription( key="connection_quality", - name="Connection Quality", + translation_key="connection_quality", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.connection_quality, entity_registry_enabled_default=False, ), IntellifireSensorEntityDescription( key="ecm_latency", - name="ECM latency", + translation_key="ecm_latency", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.ecm_latency, entity_registry_enabled_default=False, ), IntellifireSensorEntityDescription( key="ipv4_address", - name="IP", + translation_key="ipv4_address", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.ipv4_address, ), diff --git a/homeassistant/components/intellifire/strings.json b/homeassistant/components/intellifire/strings.json index a8c8d76a601922..6393a4e070d16f 100644 --- a/homeassistant/components/intellifire/strings.json +++ b/homeassistant/components/intellifire/strings.json @@ -35,5 +35,106 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "not_intellifire_device": "Not an IntelliFire Device." } + }, + "entity": { + "binary_sensor": { + "flame": { + "name": "Flame" + }, + "timer_on": { + "name": "Timer on" + }, + "pilot_light_on": { + "name": "Pilot light on" + }, + "thermostat_on": { + "name": "Thermostat on" + }, + "pilot_flame_error": { + "name": "Pilot flame error" + }, + "flame_error": { + "name": "Flame Error" + }, + "fan_delay_error": { + "name": "Fan delay error" + }, + "maintenance_error": { + "name": "Maintenance error" + }, + "disabled_error": { + "name": "Disabled error" + }, + "fan_error": { + "name": "Fan error" + }, + "lights_error": { + "name": "Lights error" + }, + "accessory_error": { + "name": "Accessory error" + }, + "soft_lock_out_error": { + "name": "Soft lock out error" + }, + "ecm_offline_error": { + "name": "ECM offline error" + }, + "offline_error": { + "name": "Offline error" + } + }, + "fan": { + "fan": { + "name": "[%key:component::fan::title%]" + } + }, + "light": { + "lights": { + "name": "Lights" + } + }, + "number": { + "flame_control": { + "name": "Flame control" + } + }, + "sensor": { + "flame_height": { + "name": "Flame height" + }, + "target_temp": { + "name": "Target temperature" + }, + "fan_speed": { + "name": "Fan Speed" + }, + "timer_end_timestamp": { + "name": "Timer end" + }, + "downtime": { + "name": "Downtime" + }, + "uptime": { + "name": "Uptime" + }, + "connection_quality": { + "name": "Connection quality" + }, + "ecm_latency": { + "name": "ECM latency" + }, + "ipv4_address": { + "name": "IP address" + } + }, + "switch": { + "flame": { + "name": "Flame" + }, + "pilot_light": { + "name": "Pilot light" + } + } } } diff --git a/homeassistant/components/intellifire/switch.py b/homeassistant/components/intellifire/switch.py index 98abaa38849d85..1af4d8c0e91cff 100644 --- a/homeassistant/components/intellifire/switch.py +++ b/homeassistant/components/intellifire/switch.py @@ -37,14 +37,14 @@ class IntellifireSwitchEntityDescription( INTELLIFIRE_SWITCHES: tuple[IntellifireSwitchEntityDescription, ...] = ( IntellifireSwitchEntityDescription( key="on_off", - name="Flame", + translation_key="flame", on_fn=lambda control_api: control_api.flame_on(), off_fn=lambda control_api: control_api.flame_off(), value_fn=lambda data: data.is_on, ), IntellifireSwitchEntityDescription( key="pilot", - name="Pilot light", + translation_key="pilot_light", icon="mdi:fire-alert", on_fn=lambda control_api: control_api.pilot_on(), off_fn=lambda control_api: control_api.pilot_off(), From 13fd5a59e328d7bc73af8b2a705cde02db63f327 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 22 Jul 2023 16:33:06 +0200 Subject: [PATCH 0773/1009] Clean up Vilfo const file (#95543) --- homeassistant/components/vilfo/const.py | 35 ------------------ homeassistant/components/vilfo/sensor.py | 45 ++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/vilfo/const.py b/homeassistant/components/vilfo/const.py index 5ed9bc3efdd2e0..e562add4e0f990 100644 --- a/homeassistant/components/vilfo/const.py +++ b/homeassistant/components/vilfo/const.py @@ -1,11 +1,6 @@ """Constants for the Vilfo Router integration.""" from __future__ import annotations -from dataclasses import dataclass - -from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription -from homeassistant.const import PERCENTAGE - DOMAIN = "vilfo" ATTR_API_DATA_FIELD_LOAD = "load" @@ -17,33 +12,3 @@ ROUTER_DEFAULT_MODEL = "Vilfo Router" ROUTER_DEFAULT_NAME = "Vilfo Router" ROUTER_MANUFACTURER = "Vilfo AB" - - -@dataclass -class VilfoRequiredKeysMixin: - """Mixin for required keys.""" - - api_key: str - - -@dataclass -class VilfoSensorEntityDescription(SensorEntityDescription, VilfoRequiredKeysMixin): - """Describes Vilfo sensor entity.""" - - -SENSOR_TYPES: tuple[VilfoSensorEntityDescription, ...] = ( - VilfoSensorEntityDescription( - key=ATTR_LOAD, - name="Load", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:memory", - api_key=ATTR_API_DATA_FIELD_LOAD, - ), - VilfoSensorEntityDescription( - key=ATTR_BOOT_TIME, - name="Boot time", - icon="mdi:timer-outline", - api_key=ATTR_API_DATA_FIELD_BOOT_TIME, - device_class=SensorDeviceClass.TIMESTAMP, - ), -) diff --git a/homeassistant/components/vilfo/sensor.py b/homeassistant/components/vilfo/sensor.py index b6339cea0d6e69..7bdba371f490d0 100644 --- a/homeassistant/components/vilfo/sensor.py +++ b/homeassistant/components/vilfo/sensor.py @@ -1,16 +1,55 @@ """Support for Vilfo Router sensors.""" -from homeassistant.components.sensor import SensorEntity +from dataclasses import dataclass + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( + ATTR_API_DATA_FIELD_BOOT_TIME, + ATTR_API_DATA_FIELD_LOAD, + ATTR_BOOT_TIME, + ATTR_LOAD, DOMAIN, ROUTER_DEFAULT_MODEL, ROUTER_DEFAULT_NAME, ROUTER_MANUFACTURER, - SENSOR_TYPES, - VilfoSensorEntityDescription, +) + + +@dataclass +class VilfoRequiredKeysMixin: + """Mixin for required keys.""" + + api_key: str + + +@dataclass +class VilfoSensorEntityDescription(SensorEntityDescription, VilfoRequiredKeysMixin): + """Describes Vilfo sensor entity.""" + + +SENSOR_TYPES: tuple[VilfoSensorEntityDescription, ...] = ( + VilfoSensorEntityDescription( + key=ATTR_LOAD, + name="Load", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:memory", + api_key=ATTR_API_DATA_FIELD_LOAD, + ), + VilfoSensorEntityDescription( + key=ATTR_BOOT_TIME, + name="Boot time", + icon="mdi:timer-outline", + api_key=ATTR_API_DATA_FIELD_BOOT_TIME, + device_class=SensorDeviceClass.TIMESTAMP, + ), ) From 44803e117768a5fddb09cdd16efce4126cef20ce Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 22 Jul 2023 16:55:05 +0200 Subject: [PATCH 0774/1009] Migrate Uptimerobot to has entity name (#96770) --- homeassistant/components/uptimerobot/binary_sensor.py | 1 - homeassistant/components/uptimerobot/entity.py | 2 ++ homeassistant/components/uptimerobot/sensor.py | 1 - homeassistant/components/uptimerobot/switch.py | 1 - tests/components/uptimerobot/common.py | 2 +- 5 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index 248212a8345daf..a4aeeb3151b1bc 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -27,7 +27,6 @@ async def async_setup_entry( coordinator, BinarySensorEntityDescription( key=str(monitor.id), - name=monitor.friendly_name, device_class=BinarySensorDeviceClass.CONNECTIVITY, ), monitor=monitor, diff --git a/homeassistant/components/uptimerobot/entity.py b/homeassistant/components/uptimerobot/entity.py index 7991525c2a0365..d5caf36fa18071 100644 --- a/homeassistant/components/uptimerobot/entity.py +++ b/homeassistant/components/uptimerobot/entity.py @@ -15,6 +15,8 @@ class UptimeRobotEntity(CoordinatorEntity[UptimeRobotDataUpdateCoordinator]): """Base UptimeRobot entity.""" _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True + _attr_name = None def __init__( self, diff --git a/homeassistant/components/uptimerobot/sensor.py b/homeassistant/components/uptimerobot/sensor.py index 219dd304dbd911..f9d4097fe4034a 100644 --- a/homeassistant/components/uptimerobot/sensor.py +++ b/homeassistant/components/uptimerobot/sensor.py @@ -46,7 +46,6 @@ async def async_setup_entry( coordinator, SensorEntityDescription( key=str(monitor.id), - name=monitor.friendly_name, entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENUM, options=["down", "not_checked_yet", "pause", "seems_down", "up"], diff --git a/homeassistant/components/uptimerobot/switch.py b/homeassistant/components/uptimerobot/switch.py index 619f72ae47fe17..397d2085357f80 100644 --- a/homeassistant/components/uptimerobot/switch.py +++ b/homeassistant/components/uptimerobot/switch.py @@ -29,7 +29,6 @@ async def async_setup_entry( coordinator, SwitchEntityDescription( key=str(monitor.id), - name=f"{monitor.friendly_name} Active", device_class=SwitchDeviceClass.SWITCH, ), monitor=monitor, diff --git a/tests/components/uptimerobot/common.py b/tests/components/uptimerobot/common.py index 6a82d75a9f8878..15f6e153b19461 100644 --- a/tests/components/uptimerobot/common.py +++ b/tests/components/uptimerobot/common.py @@ -66,7 +66,7 @@ UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY = "binary_sensor.test_monitor" UPTIMEROBOT_SENSOR_TEST_ENTITY = "sensor.test_monitor" -UPTIMEROBOT_SWITCH_TEST_ENTITY = "switch.test_monitor_active" +UPTIMEROBOT_SWITCH_TEST_ENTITY = "switch.test_monitor" class MockApiResponseKey(str, Enum): From 15c52e67a0bce156c4ba6c5c31de95b41a5e8bdb Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 22 Jul 2023 16:59:15 +0200 Subject: [PATCH 0775/1009] Clean up Enphase Envoy const file (#95536) --- .../components/enphase_envoy/__init__.py | 3 +- .../components/enphase_envoy/const.py | 64 +------------------ .../components/enphase_envoy/sensor.py | 61 +++++++++++++++++- 3 files changed, 62 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 147eddacf818cd..1a4feb593768c3 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -16,7 +16,8 @@ from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import COORDINATOR, DOMAIN, NAME, PLATFORMS, SENSORS +from .const import COORDINATOR, DOMAIN, NAME, PLATFORMS +from .sensor import SENSORS SCAN_INTERVAL = timedelta(seconds=60) diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index 4a105e5a067ddf..e7c0b7f2a5e35b 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -1,10 +1,5 @@ """The enphase_envoy component.""" -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import Platform, UnitOfEnergy, UnitOfPower +from homeassistant.const import Platform DOMAIN = "enphase_envoy" @@ -13,60 +8,3 @@ COORDINATOR = "coordinator" NAME = "name" - -SENSORS = ( - SensorEntityDescription( - key="production", - name="Current Power Production", - native_unit_of_measurement=UnitOfPower.WATT, - state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.POWER, - ), - SensorEntityDescription( - key="daily_production", - name="Today's Energy Production", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, - device_class=SensorDeviceClass.ENERGY, - ), - SensorEntityDescription( - key="seven_days_production", - name="Last Seven Days Energy Production", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - ), - SensorEntityDescription( - key="lifetime_production", - name="Lifetime Energy Production", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, - device_class=SensorDeviceClass.ENERGY, - ), - SensorEntityDescription( - key="consumption", - name="Current Power Consumption", - native_unit_of_measurement=UnitOfPower.WATT, - state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.POWER, - ), - SensorEntityDescription( - key="daily_consumption", - name="Today's Energy Consumption", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, - device_class=SensorDeviceClass.ENERGY, - ), - SensorEntityDescription( - key="seven_days_consumption", - name="Last Seven Days Energy Consumption", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - ), - SensorEntityDescription( - key="lifetime_consumption", - name="Lifetime Energy Consumption", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, - device_class=SensorDeviceClass.ENERGY, - ), -) diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 44ffbcdb497829..f42c8d94ea2049 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -14,7 +14,7 @@ SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import UnitOfPower +from homeassistant.const import UnitOfEnergy, UnitOfPower from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -25,7 +25,7 @@ ) from homeassistant.util import dt as dt_util -from .const import COORDINATOR, DOMAIN, NAME, SENSORS +from .const import COORDINATOR, DOMAIN, NAME ICON = "mdi:flash" _LOGGER = logging.getLogger(__name__) @@ -75,6 +75,63 @@ def _inverter_last_report_time( ), ) +SENSORS = ( + SensorEntityDescription( + key="production", + name="Current Power Production", + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + ), + SensorEntityDescription( + key="daily_production", + name="Today's Energy Production", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + ), + SensorEntityDescription( + key="seven_days_production", + name="Last Seven Days Energy Production", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + ), + SensorEntityDescription( + key="lifetime_production", + name="Lifetime Energy Production", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + ), + SensorEntityDescription( + key="consumption", + name="Current Power Consumption", + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + ), + SensorEntityDescription( + key="daily_consumption", + name="Today's Energy Consumption", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + ), + SensorEntityDescription( + key="seven_days_consumption", + name="Last Seven Days Energy Consumption", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + ), + SensorEntityDescription( + key="lifetime_consumption", + name="Lifetime Energy Consumption", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + ), +) + async def async_setup_entry( hass: HomeAssistant, From 5249660a6a1ba4e1766b2becdecbac6df853b995 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 22 Jul 2023 15:11:39 +0000 Subject: [PATCH 0776/1009] Add `uv_index` to AccuWeather weather entity (#97015) --- homeassistant/components/accuweather/weather.py | 7 +++++++ tests/components/accuweather/test_weather.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 20cb12179eeb3e..30dae28c4081b9 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -14,6 +14,7 @@ ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TIME, + ATTR_FORECAST_UV_INDEX, ATTR_FORECAST_WIND_BEARING, Forecast, WeatherEntity, @@ -147,6 +148,11 @@ def native_visibility(self) -> float: """Return the visibility.""" return cast(float, self.coordinator.data["Visibility"][API_METRIC][ATTR_VALUE]) + @property + def uv_index(self) -> float: + """Return the UV index.""" + return cast(float, self.coordinator.data["UVIndex"]) + @property def forecast(self) -> list[Forecast] | None: """Return the forecast array.""" @@ -172,6 +178,7 @@ def forecast(self) -> list[Forecast] | None: ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: item["WindGustDay"][ATTR_SPEED][ ATTR_VALUE ], + ATTR_FORECAST_UV_INDEX: item["UVIndex"][ATTR_VALUE], ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"], ATTR_FORECAST_CONDITION: [ k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py index dd5dca8c06985a..b9e66d518742d4 100644 --- a/tests/components/accuweather/test_weather.py +++ b/tests/components/accuweather/test_weather.py @@ -22,6 +22,7 @@ ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_UV_INDEX, ATTR_WEATHER_VISIBILITY, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_GUST_SPEED, @@ -61,6 +62,7 @@ async def test_weather_without_forecast(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_WEATHER_DEW_POINT) == 16.2 assert state.attributes.get(ATTR_WEATHER_CLOUD_COVERAGE) == 10 assert state.attributes.get(ATTR_WEATHER_WIND_GUST_SPEED) == 20.3 + assert state.attributes.get(ATTR_WEATHER_UV_INDEX) == 6 assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION entry = registry.async_get("weather.home") @@ -86,6 +88,7 @@ async def test_weather_with_forecast(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_WEATHER_DEW_POINT) == 16.2 assert state.attributes.get(ATTR_WEATHER_CLOUD_COVERAGE) == 10 assert state.attributes.get(ATTR_WEATHER_WIND_GUST_SPEED) == 20.3 + assert state.attributes.get(ATTR_WEATHER_UV_INDEX) == 6 assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION forecast = state.attributes.get(ATTR_FORECAST)[0] assert forecast.get(ATTR_FORECAST_CONDITION) == "lightning-rainy" @@ -99,6 +102,7 @@ async def test_weather_with_forecast(hass: HomeAssistant) -> None: assert forecast.get(ATTR_FORECAST_CLOUD_COVERAGE) == 58 assert forecast.get(ATTR_FORECAST_APPARENT_TEMP) == 29.8 assert forecast.get(ATTR_FORECAST_WIND_GUST_SPEED) == 29.6 + assert forecast.get(ATTR_WEATHER_UV_INDEX) == 5 entry = registry.async_get("weather.home") assert entry From e68832a889e7c8d429e1971d7942ac980b1c2db2 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Sat, 22 Jul 2023 17:23:01 +0200 Subject: [PATCH 0777/1009] Fix Vicare cleanup token file on uninstall (#95992) --- homeassistant/components/vicare/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index b177a4c524f71a..269695a668d920 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -2,8 +2,10 @@ from __future__ import annotations from collections.abc import Callable +from contextlib import suppress from dataclasses import dataclass import logging +import os from PyViCare.PyViCare import PyViCare from PyViCare.PyViCareDevice import Device @@ -25,6 +27,7 @@ ) _LOGGER = logging.getLogger(__name__) +_TOKEN_FILENAME = "vicare_token.save" @dataclass() @@ -64,7 +67,7 @@ def vicare_login(hass, entry_data): entry_data[CONF_USERNAME], entry_data[CONF_PASSWORD], entry_data[CONF_CLIENT_ID], - hass.config.path(STORAGE_DIR, "vicare_token.save"), + hass.config.path(STORAGE_DIR, _TOKEN_FILENAME), ) return vicare_api @@ -93,4 +96,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) + with suppress(FileNotFoundError): + await hass.async_add_executor_job( + os.remove, hass.config.path(STORAGE_DIR, _TOKEN_FILENAME) + ) + return unload_ok From 9a5fe9f6446a5991dcef9473f4de397eb44b61bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Sat, 22 Jul 2023 17:24:06 +0200 Subject: [PATCH 0778/1009] Airthings BLE: Improve supported devices (#95883) --- .../components/airthings_ble/manifest.json | 15 ++++++++++++++- homeassistant/generated/bluetooth.py | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/airthings_ble/manifest.json b/homeassistant/components/airthings_ble/manifest.json index e06324f93ec54a..8c78bbfb58d32d 100644 --- a/homeassistant/components/airthings_ble/manifest.json +++ b/homeassistant/components/airthings_ble/manifest.json @@ -3,7 +3,20 @@ "name": "Airthings BLE", "bluetooth": [ { - "manufacturer_id": 820 + "manufacturer_id": 820, + "service_uuid": "b42e1f6e-ade7-11e4-89d3-123b93f75cba" + }, + { + "manufacturer_id": 820, + "service_uuid": "b42e4a8e-ade7-11e4-89d3-123b93f75cba" + }, + { + "manufacturer_id": 820, + "service_uuid": "b42e1c08-ade7-11e4-89d3-123b93f75cba" + }, + { + "manufacturer_id": 820, + "service_uuid": "b42e3882-ade7-11e4-89d3-123b93f75cba" } ], "codeowners": ["@vincegio"], diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index aba97c8ea8cc02..b99b621c614240 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -9,6 +9,22 @@ { "domain": "airthings_ble", "manufacturer_id": 820, + "service_uuid": "b42e1f6e-ade7-11e4-89d3-123b93f75cba", + }, + { + "domain": "airthings_ble", + "manufacturer_id": 820, + "service_uuid": "b42e4a8e-ade7-11e4-89d3-123b93f75cba", + }, + { + "domain": "airthings_ble", + "manufacturer_id": 820, + "service_uuid": "b42e1c08-ade7-11e4-89d3-123b93f75cba", + }, + { + "domain": "airthings_ble", + "manufacturer_id": 820, + "service_uuid": "b42e3882-ade7-11e4-89d3-123b93f75cba", }, { "connectable": False, From 77f2eb0ac9c82d2d9ac0dc82b612e862c928c872 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 22 Jul 2023 17:29:04 +0200 Subject: [PATCH 0779/1009] Add entity translations to Subaru (#96186) --- homeassistant/components/subaru/lock.py | 4 +- homeassistant/components/subaru/sensor.py | 22 +++++----- homeassistant/components/subaru/strings.json | 42 ++++++++++++++++++++ 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/subaru/lock.py b/homeassistant/components/subaru/lock.py index 0e57373625a62b..342fe34b97ddf9 100644 --- a/homeassistant/components/subaru/lock.py +++ b/homeassistant/components/subaru/lock.py @@ -58,13 +58,15 @@ class SubaruLock(LockEntity): Note that the Subaru API currently does not support returning the status of the locks. Lock status is always unknown. """ + _attr_has_entity_name = True + _attr_translation_key = "door_locks" + def __init__(self, vehicle_info, controller): """Initialize the locks for the vehicle.""" self.controller = controller self.vehicle_info = vehicle_info vin = vehicle_info[VEHICLE_VIN] self.car_name = vehicle_info[VEHICLE_NAME] - self._attr_name = f"{self.car_name} Door Locks" self._attr_unique_id = f"{vin}_door_locks" self._attr_device_info = get_device_info(vehicle_info) diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py index 50e8f89716bd7f..eda8c20b10e26f 100644 --- a/homeassistant/components/subaru/sensor.py +++ b/homeassistant/components/subaru/sensor.py @@ -55,9 +55,9 @@ SAFETY_SENSORS = [ SensorEntityDescription( key=sc.ODOMETER, + translation_key="odometer", device_class=SensorDeviceClass.DISTANCE, icon="mdi:road-variant", - name="Odometer", native_unit_of_measurement=UnitOfLength.KILOMETERS, state_class=SensorStateClass.TOTAL_INCREASING, ), @@ -67,44 +67,44 @@ API_GEN_2_SENSORS = [ SensorEntityDescription( key=sc.AVG_FUEL_CONSUMPTION, + translation_key="average_fuel_consumption", icon="mdi:leaf", - name="Avg fuel consumption", native_unit_of_measurement=FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=sc.DIST_TO_EMPTY, + translation_key="range", device_class=SensorDeviceClass.DISTANCE, icon="mdi:gas-station", - name="Range", native_unit_of_measurement=UnitOfLength.KILOMETERS, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=sc.TIRE_PRESSURE_FL, + translation_key="tire_pressure_front_left", device_class=SensorDeviceClass.PRESSURE, - name="Tire pressure FL", native_unit_of_measurement=UnitOfPressure.HPA, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=sc.TIRE_PRESSURE_FR, + translation_key="tire_pressure_front_right", device_class=SensorDeviceClass.PRESSURE, - name="Tire pressure FR", native_unit_of_measurement=UnitOfPressure.HPA, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=sc.TIRE_PRESSURE_RL, + translation_key="tire_pressure_rear_left", device_class=SensorDeviceClass.PRESSURE, - name="Tire pressure RL", native_unit_of_measurement=UnitOfPressure.HPA, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=sc.TIRE_PRESSURE_RR, + translation_key="tire_pressure_rear_right", device_class=SensorDeviceClass.PRESSURE, - name="Tire pressure RR", native_unit_of_measurement=UnitOfPressure.HPA, state_class=SensorStateClass.MEASUREMENT, ), @@ -114,8 +114,8 @@ API_GEN_3_SENSORS = [ SensorEntityDescription( key=sc.REMAINING_FUEL_PERCENT, + translation_key="fuel_level", icon="mdi:gas-station", - name="Fuel level", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), @@ -125,23 +125,23 @@ EV_SENSORS = [ SensorEntityDescription( key=sc.EV_DISTANCE_TO_EMPTY, + translation_key="ev_range", device_class=SensorDeviceClass.DISTANCE, icon="mdi:ev-station", - name="EV range", native_unit_of_measurement=UnitOfLength.MILES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=sc.EV_STATE_OF_CHARGE_PERCENT, + translation_key="ev_battery_level", device_class=SensorDeviceClass.BATTERY, - name="EV battery level", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=sc.EV_TIME_TO_FULLY_CHARGED_UTC, + translation_key="ev_time_to_full_charge", device_class=SensorDeviceClass.TIMESTAMP, - name="EV time to full charge", ), ] diff --git a/homeassistant/components/subaru/strings.json b/homeassistant/components/subaru/strings.json index 8474d391141b81..5e6db32d4add11 100644 --- a/homeassistant/components/subaru/strings.json +++ b/homeassistant/components/subaru/strings.json @@ -57,6 +57,48 @@ } } }, + "entity": { + "lock": { + "door_locks": { + "name": "Door locks" + } + }, + "sensor": { + "odometer": { + "name": "Odometer" + }, + "average_fuel_consumption": { + "name": "Average fuel consumption" + }, + "range": { + "name": "Range" + }, + "tire_pressure_front_left": { + "name": "Tire pressure front left" + }, + "tire_pressure_front_right": { + "name": "Tire pressure front right" + }, + "tire_pressure_rear_left": { + "name": "Tire pressure rear left" + }, + "tire_pressure_rear_right": { + "name": "Tire pressure rear right" + }, + "fuel_level": { + "name": "Fuel level" + }, + "ev_range": { + "name": "EV range" + }, + "ev_battery_level": { + "name": "EV battery level" + }, + "ev_time_to_full_charge": { + "name": "EV time to full charge" + } + } + }, "services": { "unlock_specific_door": { "name": "Unlock specific door", From a8d77cc5adfe2909d3e1fdb9c1bcf0dd93233256 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 22 Jul 2023 17:29:24 +0200 Subject: [PATCH 0780/1009] Teach zwave_js device trigger about entity registry ids (#96303) --- .../components/zwave_js/device_trigger.py | 4 +- .../zwave_js/test_device_trigger.py | 83 ++++++++++++++++++- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index da26e4f293e6ba..d2b6ab7af15728 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -142,7 +142,7 @@ # State based trigger schemas BASE_STATE_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { - vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid, } ) @@ -272,7 +272,7 @@ async def async_get_triggers( and not entity.disabled ): triggers.append( - {**base_trigger, CONF_TYPE: NODE_STATUS, CONF_ENTITY_ID: entity_id} + {**base_trigger, CONF_TYPE: NODE_STATUS, CONF_ENTITY_ID: entity.id} ) # Handle notification event triggers diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py index fd091b2bfe78a7..8551427cf3e350 100644 --- a/tests/components/zwave_js/test_device_trigger.py +++ b/tests/components/zwave_js/test_device_trigger.py @@ -345,7 +345,7 @@ async def test_get_node_status_triggers( entity_id = async_get_node_status_sensor_entity_id( hass, device.id, ent_reg, dev_reg ) - ent_reg.async_update_entity(entity_id, **{"disabled_by": None}) + entity = ent_reg.async_update_entity(entity_id, **{"disabled_by": None}) await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() @@ -354,7 +354,7 @@ async def test_get_node_status_triggers( "domain": DOMAIN, "type": "state.node_status", "device_id": device.id, - "entity_id": entity_id, + "entity_id": entity.id, "metadata": {"secondary": True}, } triggers = await async_get_device_automations( @@ -377,6 +377,85 @@ async def test_if_node_status_change_fires( entity_id = async_get_node_status_sensor_entity_id( hass, device.id, ent_reg, dev_reg ) + entity = ent_reg.async_update_entity(entity_id, **{"disabled_by": None}) + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + # from + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "entity_id": entity.id, + "type": "state.node_status", + "from": "alive", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "state.node_status - " + "{{ trigger.platform}} - " + "{{ trigger.from_state.state }}" + ) + }, + }, + }, + # no from or to + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "entity_id": entity.id, + "type": "state.node_status", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "state.node_status2 - " + "{{ trigger.platform}} - " + "{{ trigger.from_state.state }}" + ) + }, + }, + }, + ] + }, + ) + + # Test status change + event = Event( + "dead", data={"source": "node", "event": "dead", "nodeId": node.node_id} + ) + node.receive_event(event) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[0].data["some"] == "state.node_status - device - alive" + assert calls[1].data["some"] == "state.node_status2 - device - alive" + + +async def test_if_node_status_change_fires_legacy( + hass: HomeAssistant, client, lock_schlage_be469, integration, calls +) -> None: + """Test for node_status trigger firing.""" + node: Node = lock_schlage_be469 + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device + ent_reg = async_get_ent_reg(hass) + entity_id = async_get_node_status_sensor_entity_id( + hass, device.id, ent_reg, dev_reg + ) ent_reg.async_update_entity(entity_id, **{"disabled_by": None}) await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() From d4f301f4a3ecdf5af222eb7e0dfc14251b7b8e41 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 22 Jul 2023 17:39:11 +0200 Subject: [PATCH 0781/1009] Migrate Tolo to entity name (#96244) --- homeassistant/components/tolo/__init__.py | 2 + .../components/tolo/binary_sensor.py | 4 +- homeassistant/components/tolo/button.py | 2 +- homeassistant/components/tolo/climate.py | 2 +- homeassistant/components/tolo/fan.py | 2 +- homeassistant/components/tolo/light.py | 2 +- homeassistant/components/tolo/number.py | 6 +-- homeassistant/components/tolo/select.py | 1 - homeassistant/components/tolo/sensor.py | 10 ++-- homeassistant/components/tolo/strings.json | 52 +++++++++++++++++++ 10 files changed, 68 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py index a75cb7ce298e96..bb894753fb8cac 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -95,6 +95,8 @@ def _get_tolo_sauna_data(self) -> ToloSaunaData: class ToloSaunaCoordinatorEntity(CoordinatorEntity[ToloSaunaUpdateCoordinator]): """CoordinatorEntity for TOLO Sauna.""" + _attr_has_entity_name = True + def __init__( self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry ) -> None: diff --git a/homeassistant/components/tolo/binary_sensor.py b/homeassistant/components/tolo/binary_sensor.py index 0ee1cb08bb2219..124cd45d78bddd 100644 --- a/homeassistant/components/tolo/binary_sensor.py +++ b/homeassistant/components/tolo/binary_sensor.py @@ -32,7 +32,7 @@ class ToloFlowInBinarySensor(ToloSaunaCoordinatorEntity, BinarySensorEntity): """Water In Valve Sensor.""" _attr_entity_category = EntityCategory.DIAGNOSTIC - _attr_name = "Water In Valve" + _attr_translation_key = "water_in_valve" _attr_device_class = BinarySensorDeviceClass.OPENING _attr_icon = "mdi:water-plus-outline" @@ -54,7 +54,7 @@ class ToloFlowOutBinarySensor(ToloSaunaCoordinatorEntity, BinarySensorEntity): """Water Out Valve Sensor.""" _attr_entity_category = EntityCategory.DIAGNOSTIC - _attr_name = "Water Out Valve" + _attr_translation_key = "water_out_valve" _attr_device_class = BinarySensorDeviceClass.OPENING _attr_icon = "mdi:water-minus-outline" diff --git a/homeassistant/components/tolo/button.py b/homeassistant/components/tolo/button.py index 5d041a741045f2..3b81477ab37fc4 100644 --- a/homeassistant/components/tolo/button.py +++ b/homeassistant/components/tolo/button.py @@ -31,7 +31,7 @@ class ToloLampNextColorButton(ToloSaunaCoordinatorEntity, ButtonEntity): _attr_entity_category = EntityCategory.CONFIG _attr_icon = "mdi:palette" - _attr_name = "Next Color" + _attr_translation_key = "next_color" def __init__( self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry diff --git a/homeassistant/components/tolo/climate.py b/homeassistant/components/tolo/climate.py index 849a9f5b3ed3ba..74f2a5a6f55c64 100644 --- a/homeassistant/components/tolo/climate.py +++ b/homeassistant/components/tolo/climate.py @@ -47,7 +47,7 @@ class SaunaClimate(ToloSaunaCoordinatorEntity, ClimateEntity): _attr_max_temp = DEFAULT_MAX_TEMP _attr_min_humidity = DEFAULT_MIN_HUMIDITY _attr_min_temp = DEFAULT_MIN_TEMP - _attr_name = "Sauna Climate" + _attr_name = None _attr_precision = PRECISION_WHOLE _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE diff --git a/homeassistant/components/tolo/fan.py b/homeassistant/components/tolo/fan.py index e767be9a3ce806..7065290f2a848a 100644 --- a/homeassistant/components/tolo/fan.py +++ b/homeassistant/components/tolo/fan.py @@ -26,7 +26,7 @@ async def async_setup_entry( class ToloFan(ToloSaunaCoordinatorEntity, FanEntity): """Sauna fan control.""" - _attr_name = "Fan" + _attr_translation_key = "fan" def __init__( self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry diff --git a/homeassistant/components/tolo/light.py b/homeassistant/components/tolo/light.py index 715a1327e4b1f6..4b76d4270c66fc 100644 --- a/homeassistant/components/tolo/light.py +++ b/homeassistant/components/tolo/light.py @@ -26,7 +26,7 @@ class ToloLight(ToloSaunaCoordinatorEntity, LightEntity): """Sauna light control.""" _attr_color_mode = ColorMode.ONOFF - _attr_name = "Sauna Light" + _attr_translation_key = "light" _attr_supported_color_modes = {ColorMode.ONOFF} def __init__( diff --git a/homeassistant/components/tolo/number.py b/homeassistant/components/tolo/number.py index aa12198b52c917..3e07392c336359 100644 --- a/homeassistant/components/tolo/number.py +++ b/homeassistant/components/tolo/number.py @@ -40,8 +40,8 @@ class ToloNumberEntityDescription( NUMBERS = ( ToloNumberEntityDescription( key="power_timer", + translation_key="power_timer", icon="mdi:power-settings", - name="Power Timer", native_unit_of_measurement=UnitOfTime.MINUTES, native_max_value=POWER_TIMER_MAX, getter=lambda settings: settings.power_timer, @@ -49,8 +49,8 @@ class ToloNumberEntityDescription( ), ToloNumberEntityDescription( key="salt_bath_timer", + translation_key="salt_bath_timer", icon="mdi:shaker-outline", - name="Salt Bath Timer", native_unit_of_measurement=UnitOfTime.MINUTES, native_max_value=SALT_BATH_TIMER_MAX, getter=lambda settings: settings.salt_bath_timer, @@ -58,8 +58,8 @@ class ToloNumberEntityDescription( ), ToloNumberEntityDescription( key="fan_timer", + translation_key="fan_timer", icon="mdi:fan-auto", - name="Fan Timer", native_unit_of_measurement=UnitOfTime.MINUTES, native_max_value=FAN_TIMER_MAX, getter=lambda settings: settings.fan_timer, diff --git a/homeassistant/components/tolo/select.py b/homeassistant/components/tolo/select.py index a47207d3d98b69..8e4ecb47f48075 100644 --- a/homeassistant/components/tolo/select.py +++ b/homeassistant/components/tolo/select.py @@ -29,7 +29,6 @@ class ToloLampModeSelect(ToloSaunaCoordinatorEntity, SelectEntity): _attr_entity_category = EntityCategory.CONFIG _attr_icon = "mdi:lightbulb-multiple-outline" - _attr_name = "Lamp Mode" _attr_options = [lamp_mode.name.lower() for lamp_mode in LampMode] _attr_translation_key = "lamp_mode" diff --git a/homeassistant/components/tolo/sensor.py b/homeassistant/components/tolo/sensor.py index 2c5eccc1c1d6b4..2ff901939ae150 100644 --- a/homeassistant/components/tolo/sensor.py +++ b/homeassistant/components/tolo/sensor.py @@ -46,27 +46,27 @@ class ToloSensorEntityDescription( SENSORS = ( ToloSensorEntityDescription( key="water_level", + translation_key="water_level", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:waves-arrow-up", - name="Water Level", native_unit_of_measurement=PERCENTAGE, getter=lambda status: status.water_level_percent, availability_checker=None, ), ToloSensorEntityDescription( key="tank_temperature", + translation_key="tank_temperature", device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, - name="Tank Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, getter=lambda status: status.tank_temperature, availability_checker=None, ), ToloSensorEntityDescription( key="power_timer_remaining", + translation_key="power_timer_remaining", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:power-settings", - name="Power Timer", native_unit_of_measurement=UnitOfTime.MINUTES, getter=lambda status: status.power_timer, availability_checker=lambda settings, status: status.power_on @@ -74,9 +74,9 @@ class ToloSensorEntityDescription( ), ToloSensorEntityDescription( key="salt_bath_timer_remaining", + translation_key="salt_bath_timer_remaining", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:shaker-outline", - name="Salt Bath Timer", native_unit_of_measurement=UnitOfTime.MINUTES, getter=lambda status: status.salt_bath_timer, availability_checker=lambda settings, status: status.salt_bath_on @@ -84,9 +84,9 @@ class ToloSensorEntityDescription( ), ToloSensorEntityDescription( key="fan_timer_remaining", + translation_key="fan_timer_remaining", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:fan-auto", - name="Fan Timer", native_unit_of_measurement=UnitOfTime.MINUTES, getter=lambda status: status.fan_timer, availability_checker=lambda settings, status: status.fan_on diff --git a/homeassistant/components/tolo/strings.json b/homeassistant/components/tolo/strings.json index 5e6647edae4fae..f48e26c52765be 100644 --- a/homeassistant/components/tolo/strings.json +++ b/homeassistant/components/tolo/strings.json @@ -20,13 +20,65 @@ } }, "entity": { + "binary_sensor": { + "water_in_valve": { + "name": "Water in valve" + }, + "water_out_valve": { + "name": "Water out valve" + } + }, + "button": { + "next_color": { + "name": "Next color" + } + }, + "fan": { + "fan": { + "name": "[%key:component::fan::title%]" + } + }, + "light": { + "light": { + "name": "[%key:component::light::title%]" + } + }, + "number": { + "power_timer": { + "name": "Power timer" + }, + "salt_bath_timer": { + "name": "Salt bath timer" + }, + "fan_timer": { + "name": "Fan timer" + } + }, "select": { "lamp_mode": { + "name": "Lamp mode", "state": { "automatic": "Automatic", "manual": "Manual" } } + }, + "sensor": { + "water_level": { + "name": "Water level" + }, + "tank_temperature": { + "name": "Tank temperature" + }, + "power_timer_remaining": { + "name": "Power timer" + }, + "salt_bath_timer_remaining": { + "name": "Salt bath timer" + }, + "fan_timer_remaining": { + "name": "Fan timer" + } } } } From 9a5774a95d13c044078645bc04ae3cb038a1e8dc Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 22 Jul 2023 18:00:27 +0200 Subject: [PATCH 0782/1009] Apply common entity schema for MQTT Scene (#96949) --- homeassistant/components/mqtt/scene.py | 23 +--- tests/components/mqtt/test_scene.py | 180 +++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 5e12f67a698f34..87c56869d0c8e8 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -9,19 +9,16 @@ from homeassistant.components import scene from homeassistant.components.scene import Scene from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ICON, CONF_NAME, CONF_PAYLOAD_ON, CONF_UNIQUE_ID +from homeassistant.const import CONF_NAME, CONF_PAYLOAD_ON from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .client import async_publish from .config import MQTT_BASE_SCHEMA from .const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from .mixins import ( - CONF_ENABLED_BY_DEFAULT, - CONF_OBJECT_ID, - MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper, ) @@ -30,20 +27,16 @@ DEFAULT_NAME = "MQTT Scene" DEFAULT_RETAIN = False +ENTITY_ID_FORMAT = scene.DOMAIN + ".{}" + PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_ON): cv.string, - vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - vol.Optional(CONF_OBJECT_ID): cv.string, - # CONF_ENABLED_BY_DEFAULT is not added by default because - # we are not using the common schema here - vol.Optional(CONF_ENABLED_BY_DEFAULT, default=True): cv.boolean, } -).extend(MQTT_AVAILABILITY_SCHEMA.schema) +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) @@ -97,7 +90,6 @@ def config_schema() -> vol.Schema: def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" - self._config = config def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @@ -110,8 +102,7 @@ async def async_activate(self, **kwargs: Any) -> None: This method is a coroutine. """ - await async_publish( - self.hass, + await self.async_publish( self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_ON], self._config[CONF_QOS], diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index dfea7b3f915d3f..141bfc526d3766 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -1,5 +1,6 @@ """The tests for the MQTT scene platform.""" import copy +from typing import Any from unittest.mock import patch import pytest @@ -16,10 +17,23 @@ help_test_discovery_broken, help_test_discovery_removal, help_test_discovery_update, + help_test_discovery_update_attr, help_test_discovery_update_unchanged, + help_test_entity_debug_info_message, + help_test_entity_device_info_remove, + help_test_entity_device_info_update, + help_test_entity_device_info_with_connection, + help_test_entity_device_info_with_identifier, + help_test_entity_id_update_discovery_update, + help_test_publishing_with_custom_encoding, help_test_reloadable, + help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_unique_id, help_test_unload_config_entry_with_platform, + help_test_update_with_json_attrs_bad_json, + help_test_update_with_json_attrs_not_dict, ) from tests.common import mock_restore_cache @@ -241,6 +255,172 @@ async def test_discovery_broken( ) +async def test_setting_attribute_via_mqtt_json_message( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry, scene.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry, scene.DOMAIN, DEFAULT_CONFIG, None + ) + + +async def test_setting_attribute_with_template( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_with_template( + hass, mqtt_mock_entry, scene.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_not_dict( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_not_dict( + hass, + mqtt_mock_entry, + caplog, + scene.DOMAIN, + DEFAULT_CONFIG, + ) + + +async def test_update_with_json_attrs_bad_json( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_bad_json( + hass, + mqtt_mock_entry, + caplog, + scene.DOMAIN, + DEFAULT_CONFIG, + ) + + +async def test_discovery_update_attr( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update of discovered MQTTAttributes.""" + await help_test_discovery_update_attr( + hass, + mqtt_mock_entry, + caplog, + scene.DOMAIN, + DEFAULT_CONFIG, + ) + + +async def test_entity_device_info_with_connection( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT button device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock_entry, scene.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_with_identifier( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT button device registry integration.""" + await help_test_entity_device_info_with_identifier( + hass, mqtt_mock_entry, scene.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_update( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test device registry update.""" + await help_test_entity_device_info_update( + hass, mqtt_mock_entry, scene.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_remove( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test device registry remove.""" + await help_test_entity_device_info_remove( + hass, mqtt_mock_entry, scene.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_discovery_update( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT discovery update when entity_id is updated.""" + await help_test_entity_id_update_discovery_update( + hass, mqtt_mock_entry, scene.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_message( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, + mqtt_mock_entry, + scene.DOMAIN, + DEFAULT_CONFIG, + scene.SERVICE_TURN_ON, + command_payload="test-payload-on", + state_topic=None, + ) + + +@pytest.mark.parametrize( + ("service", "topic", "parameters", "payload", "template"), + [ + (scene.SERVICE_TURN_ON, "command_topic", None, "test-payload-on", None), + ], +) +async def test_publishing_with_custom_encoding( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, + service: str, + topic: str, + parameters: dict[str, Any], + payload: str, + template: str | None, +) -> None: + """Test publishing MQTT payload with different encoding.""" + domain = scene.DOMAIN + config = DEFAULT_CONFIG + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock_entry, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + ) + + async def test_reloadable( hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient, From f36930f1655a6e4a15be34d51b02c7da71de8e02 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Jul 2023 12:33:37 -0500 Subject: [PATCH 0783/1009] Fix zeroconf tests with cython 3 (#97054) --- tests/conftest.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 6b4c35c4a377cc..1b8a37e48f44fa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1104,23 +1104,34 @@ def mock_get_source_ip() -> Generator[None, None, None]: @pytest.fixture def mock_zeroconf() -> Generator[None, None, None]: """Mock zeroconf.""" + from zeroconf import DNSCache # pylint: disable=import-outside-toplevel + with patch( "homeassistant.components.zeroconf.HaZeroconf", autospec=True ) as mock_zc, patch( "homeassistant.components.zeroconf.HaAsyncServiceBrowser", autospec=True ): + zc = mock_zc.return_value + # DNSCache has strong Cython type checks, and MagicMock does not work + # so we must mock the class directly + zc.cache = DNSCache() yield mock_zc @pytest.fixture def mock_async_zeroconf(mock_zeroconf: None) -> Generator[None, None, None]: """Mock AsyncZeroconf.""" + from zeroconf import DNSCache # pylint: disable=import-outside-toplevel + with patch("homeassistant.components.zeroconf.HaAsyncZeroconf") as mock_aiozc: zc = mock_aiozc.return_value zc.async_unregister_service = AsyncMock() zc.async_register_service = AsyncMock() zc.async_update_service = AsyncMock() zc.zeroconf.async_wait_for_start = AsyncMock() + # DNSCache has strong Cython type checks, and MagicMock does not work + # so we must mock the class directly + zc.zeroconf.cache = DNSCache() zc.zeroconf.done = False zc.async_close = AsyncMock() zc.ha_async_close = AsyncMock() From 75f3054cc231878824a179a3ca2e41c2bfa98108 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Jul 2023 13:34:36 -0500 Subject: [PATCH 0784/1009] Bump aiohomekit to 2.6.10 (#97057) --- 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 9528ae568fd4e4..86937f3eee6214 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.9"], + "requirements": ["aiohomekit==2.6.10"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 8f16fbeb22ceb0..0d32d476340d4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -249,7 +249,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==2.6.9 +aiohomekit==2.6.10 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ff331e2a00d3c..b37a365ae2decf 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.9 +aiohomekit==2.6.10 # homeassistant.components.emulated_hue # homeassistant.components.http From 9424d1140876a78ae02496a3424c236431b2eb5e Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 22 Jul 2023 22:50:58 +0200 Subject: [PATCH 0785/1009] Allow homeassistant in MQTT configuration_url schema (#96107) --- homeassistant/components/mqtt/mixins.py | 2 +- homeassistant/helpers/config_validation.py | 29 ++++++++++++++++++++-- tests/helpers/test_config_validation.py | 29 ++++++++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index ec437f08d39bca..54dea780dabaa4 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -215,7 +215,7 @@ def validate_device_has_at_least_one_identifier(value: ConfigType) -> ConfigType vol.Optional(CONF_SW_VERSION): cv.string, vol.Optional(CONF_VIA_DEVICE): cv.string, vol.Optional(CONF_SUGGESTED_AREA): cv.string, - vol.Optional(CONF_CONFIGURATION_URL): cv.url, + vol.Optional(CONF_CONFIGURATION_URL): cv.configuration_url, } ), validate_device_has_at_least_one_identifier, diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 90aa499af4bab8..8d0ee78eca7831 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -25,6 +25,7 @@ import voluptuous as vol import voluptuous_serialize +from homeassistant.backports.enum import StrEnum from homeassistant.const import ( ATTR_AREA_ID, ATTR_DEVICE_ID, @@ -106,6 +107,22 @@ TIME_PERIOD_ERROR = "offset {} should be format 'HH:MM', 'HH:MM:SS' or 'HH:MM:SS.F'" + +class UrlProtocolSchema(StrEnum): + """Valid URL protocol schema values.""" + + HTTP = "http" + HTTPS = "https" + HOMEASSISTANT = "homeassistant" + + +EXTERNAL_URL_PROTOCOL_SCHEMA_LIST = frozenset( + {UrlProtocolSchema.HTTP, UrlProtocolSchema.HTTPS} +) +CONFIGURATION_URL_PROTOCOL_SCHEMA_LIST = frozenset( + {UrlProtocolSchema.HOMEASSISTANT, UrlProtocolSchema.HTTP, UrlProtocolSchema.HTTPS} +) + # Home Assistant types byte = vol.All(vol.Coerce(int), vol.Range(min=0, max=255)) small_float = vol.All(vol.Coerce(float), vol.Range(min=0, max=1)) @@ -728,16 +745,24 @@ def socket_timeout(value: Any | None) -> object: # pylint: disable=no-value-for-parameter -def url(value: Any) -> str: +def url( + value: Any, + _schema_list: frozenset[UrlProtocolSchema] = EXTERNAL_URL_PROTOCOL_SCHEMA_LIST, +) -> str: """Validate an URL.""" url_in = str(value) - if urlparse(url_in).scheme in ["http", "https"]: + if urlparse(url_in).scheme in _schema_list: return cast(str, vol.Schema(vol.Url())(url_in)) raise vol.Invalid("invalid url") +def configuration_url(value: Any) -> str: + """Validate an URL that allows the homeassistant schema.""" + return url(value, CONFIGURATION_URL_PROTOCOL_SCHEMA_LIST) + + def url_no_path(value: Any) -> str: """Validate a url without a path.""" url_in = url(value) diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 5ea6df42349d28..b5c8cc1716e2f9 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -127,6 +127,35 @@ def test_url() -> None: assert schema(value) +def test_configuration_url() -> None: + """Test URL.""" + schema = vol.Schema(cv.configuration_url) + + for value in ( + "invalid", + None, + 100, + "htp://ha.io", + "http//ha.io", + "http://??,**", + "https://??,**", + "homeassistant://??,**", + ): + with pytest.raises(vol.MultipleInvalid): + schema(value) + + for value in ( + "http://localhost", + "https://localhost/test/index.html", + "http://home-assistant.io", + "http://home-assistant.io/test/", + "https://community.home-assistant.io/", + "homeassistant://api", + "homeassistant://api/hassio_ingress/XXXXXXX", + ): + assert schema(value) + + def test_url_no_path() -> None: """Test URL.""" schema = vol.Schema(cv.url_no_path) From ce1f5f997eb35fafc308179b145e079306734705 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 22 Jul 2023 23:03:45 +0200 Subject: [PATCH 0786/1009] Drop Python 3.10 support (#97007) --- .github/workflows/ci.yaml | 4 ++-- homeassistant/components/actiontec/device_tracker.py | 2 +- homeassistant/components/denon/media_player.py | 2 +- homeassistant/components/hddtemp/sensor.py | 2 +- homeassistant/components/pioneer/media_player.py | 2 +- homeassistant/components/telnet/switch.py | 2 +- homeassistant/components/thomson/device_tracker.py | 2 +- homeassistant/const.py | 4 ++-- homeassistant/package_constraints.txt | 4 ---- mypy.ini | 2 +- pyproject.toml | 5 ++--- script/gen_requirements_all.py | 4 ---- 12 files changed, 13 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 08407e46c1c10b..4561e8a53e1910 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,8 +33,8 @@ env: PIP_CACHE_VERSION: 4 MYPY_CACHE_VERSION: 4 HA_SHORT_VERSION: 2023.8 - DEFAULT_PYTHON: "3.10" - ALL_PYTHON_VERSIONS: "['3.10', '3.11']" + DEFAULT_PYTHON: "3.11" + ALL_PYTHON_VERSIONS: "['3.11']" # 10.3 is the oldest supported version # - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022) # 10.6 is the current long-term-support diff --git a/homeassistant/components/actiontec/device_tracker.py b/homeassistant/components/actiontec/device_tracker.py index 5397fed5e1d4f8..40ff869c43b666 100644 --- a/homeassistant/components/actiontec/device_tracker.py +++ b/homeassistant/components/actiontec/device_tracker.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -import telnetlib +import telnetlib # pylint: disable=deprecated-module from typing import Final import voluptuous as vol diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index 60da3df393ed0a..b3b9e1a98efdec 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -import telnetlib +import telnetlib # pylint: disable=deprecated-module import voluptuous as vol diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index 117de2116a45ef..77c2a28190b32e 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -4,7 +4,7 @@ from datetime import timedelta import logging import socket -from telnetlib import Telnet +from telnetlib import Telnet # pylint: disable=deprecated-module import voluptuous as vol diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index a124362251a325..741d2b580e4bc0 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -import telnetlib +import telnetlib # pylint: disable=deprecated-module from typing import Final import voluptuous as vol diff --git a/homeassistant/components/telnet/switch.py b/homeassistant/components/telnet/switch.py index f919834139b7b3..14e8900f000a35 100644 --- a/homeassistant/components/telnet/switch.py +++ b/homeassistant/components/telnet/switch.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging -import telnetlib +import telnetlib # pylint: disable=deprecated-module from typing import Any import voluptuous as vol diff --git a/homeassistant/components/thomson/device_tracker.py b/homeassistant/components/thomson/device_tracker.py index e42ee4478e01d4..0ad2942fb04fb8 100644 --- a/homeassistant/components/thomson/device_tracker.py +++ b/homeassistant/components/thomson/device_tracker.py @@ -3,7 +3,7 @@ import logging import re -import telnetlib +import telnetlib # pylint: disable=deprecated-module import voluptuous as vol diff --git a/homeassistant/const.py b/homeassistant/const.py index 94fa194fa09e37..85f0f4eee15169 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -11,10 +11,10 @@ PATCH_VERSION: Final = "0.dev0" __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) +REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) # Truthy date string triggers showing related deprecation warning messages. -REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "2023.8" +REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "" # Format for platform files PLATFORM_FORMAT: Final = "{platform}.{domain}" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c8f4bc835ceefd..aa0aceb7365950 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -139,10 +139,6 @@ pubnub!=6.4.0 # https://github.com/dahlia/iso4217/issues/16 iso4217!=1.10.20220401 -# Pandas 1.4.4 has issues with wheels om armhf + Py3.10 -# Limit this to Python 3.10, to be able to install Python 3.11 wheels for now -pandas==1.4.3;python_version<'3.11' - # Matplotlib 3.6.2 has issues building wheels on armhf/armv7 # We need at least >=2.1.0 (tensorflow integration -> pycocotools) matplotlib==3.6.1 diff --git a/mypy.ini b/mypy.ini index 4c2d803a549fbe..66568cf5400129 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3,7 +3,7 @@ # To update, run python3 -m script.hassfest -p mypy_config [mypy] -python_version = 3.10 +python_version = 3.11 plugins = pydantic.mypy show_error_codes = true follow_imports = silent diff --git a/pyproject.toml b/pyproject.toml index 6575d2f8fb329a..1df65353855901 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,11 +18,10 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Topic :: Home Automation", ] -requires-python = ">=3.10.0" +requires-python = ">=3.11.0" dependencies = [ "aiohttp==3.8.5", "astral==2.2", @@ -80,7 +79,7 @@ include = ["homeassistant*"] extend-exclude = "/generated/" [tool.pylint.MAIN] -py-version = "3.10" +py-version = "3.11" ignore = [ "tests", ] diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index f3d0defac4de5c..8258543df1d9af 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -144,10 +144,6 @@ # https://github.com/dahlia/iso4217/issues/16 iso4217!=1.10.20220401 -# Pandas 1.4.4 has issues with wheels om armhf + Py3.10 -# Limit this to Python 3.10, to be able to install Python 3.11 wheels for now -pandas==1.4.3;python_version<'3.11' - # Matplotlib 3.6.2 has issues building wheels on armhf/armv7 # We need at least >=2.1.0 (tensorflow integration -> pycocotools) matplotlib==3.6.1 From 7c55dbdb173f24a03fec48e04eccc2944a182e84 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Jul 2023 16:47:13 -0500 Subject: [PATCH 0787/1009] Bump aiohomekit to 2.6.11 (#97061) --- 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 86937f3eee6214..f859919fe0745a 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.10"], + "requirements": ["aiohomekit==2.6.11"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 0d32d476340d4d..e31b5a32d7003d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -249,7 +249,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==2.6.10 +aiohomekit==2.6.11 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b37a365ae2decf..00f14b65a1303d 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.10 +aiohomekit==2.6.11 # homeassistant.components.emulated_hue # homeassistant.components.http From 77f38e33e58320f876535d7138b4e4cf9e7ee37d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 23 Jul 2023 00:03:44 +0200 Subject: [PATCH 0788/1009] Import names from typing instead of typing_extensions [3.11] (#97065) --- homeassistant/backports/enum.py | 4 +--- homeassistant/backports/functools.py | 4 +--- homeassistant/components/counter/__init__.py | 2 +- homeassistant/components/esphome/domain_data.py | 4 +--- homeassistant/components/event/__init__.py | 4 +--- homeassistant/components/hassio/issues.py | 4 +--- homeassistant/components/input_boolean/__init__.py | 3 +-- homeassistant/components/input_button/__init__.py | 3 +-- homeassistant/components/input_datetime/__init__.py | 3 +-- homeassistant/components/input_number/__init__.py | 2 +- homeassistant/components/input_select/__init__.py | 3 +-- homeassistant/components/input_text/__init__.py | 2 +- homeassistant/components/integration/sensor.py | 3 +-- homeassistant/components/light/__init__.py | 3 +-- homeassistant/components/media_player/__init__.py | 3 +-- homeassistant/components/met/__init__.py | 3 +-- homeassistant/components/minio/minio_helper.py | 2 +- homeassistant/components/number/__init__.py | 3 +-- homeassistant/components/recorder/db_schema.py | 3 +-- homeassistant/components/sensor/__init__.py | 4 +--- homeassistant/components/stream/worker.py | 3 +-- homeassistant/components/template/binary_sensor.py | 3 +-- homeassistant/components/timer/__init__.py | 2 +- homeassistant/components/tuya/base.py | 3 +-- homeassistant/components/utility_meter/sensor.py | 3 +-- homeassistant/components/weather/__init__.py | 4 +--- homeassistant/components/yeelight/scanner.py | 2 +- homeassistant/components/zha/button.py | 3 +-- homeassistant/components/zha/core/device.py | 3 +-- homeassistant/components/zha/entity.py | 4 +--- homeassistant/components/zha/number.py | 3 +-- homeassistant/components/zha/select.py | 3 +-- homeassistant/components/zha/sensor.py | 3 +-- homeassistant/components/zha/switch.py | 3 +-- homeassistant/components/zone/__init__.py | 3 +-- homeassistant/config_entries.py | 4 +--- homeassistant/core.py | 2 +- homeassistant/data_entry_flow.py | 3 +-- homeassistant/helpers/check_config.py | 3 +-- homeassistant/helpers/httpx_client.py | 3 +-- homeassistant/helpers/restore_state.py | 4 +--- homeassistant/helpers/selector.py | 3 +-- homeassistant/util/timeout.py | 4 +--- tests/components/recorder/db_schema_30.py | 3 +-- tests/components/recorder/db_schema_32.py | 3 +-- 45 files changed, 45 insertions(+), 94 deletions(-) diff --git a/homeassistant/backports/enum.py b/homeassistant/backports/enum.py index 178859ecbe77dd..33cafe3b1dd2f7 100644 --- a/homeassistant/backports/enum.py +++ b/homeassistant/backports/enum.py @@ -2,9 +2,7 @@ from __future__ import annotations from enum import Enum -from typing import Any - -from typing_extensions import Self +from typing import Any, Self class StrEnum(str, Enum): diff --git a/homeassistant/backports/functools.py b/homeassistant/backports/functools.py index c7ab0d08693921..ddcbab7dfc0d5c 100644 --- a/homeassistant/backports/functools.py +++ b/homeassistant/backports/functools.py @@ -3,9 +3,7 @@ from collections.abc import Callable from types import GenericAlias -from typing import Any, Generic, TypeVar, overload - -from typing_extensions import Self +from typing import Any, Generic, Self, TypeVar, overload _T = TypeVar("_T") _R = TypeVar("_R") diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 6f3d48fc1bbd4e..f946f29bdaa32d 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -2,8 +2,8 @@ from __future__ import annotations import logging +from typing import Self -from typing_extensions import Self import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/components/esphome/domain_data.py b/homeassistant/components/esphome/domain_data.py index 3203964fdc1e68..bf7c5d9c969335 100644 --- a/homeassistant/components/esphome/domain_data.py +++ b/homeassistant/components/esphome/domain_data.py @@ -2,9 +2,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import cast - -from typing_extensions import Self +from typing import Self, cast from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/event/__init__.py b/homeassistant/components/event/__init__.py index 48bb2fd1726a1f..a98a3fa6c3f522 100644 --- a/homeassistant/components/event/__init__.py +++ b/homeassistant/components/event/__init__.py @@ -4,9 +4,7 @@ from dataclasses import asdict, dataclass from datetime import datetime, timedelta import logging -from typing import Any, final - -from typing_extensions import Self +from typing import Any, Self, final from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/hassio/issues.py b/homeassistant/components/hassio/issues.py index 0bbd89aab86852..8bd47faef080fe 100644 --- a/homeassistant/components/hassio/issues.py +++ b/homeassistant/components/hassio/issues.py @@ -4,9 +4,7 @@ import asyncio from dataclasses import dataclass, field import logging -from typing import Any, TypedDict - -from typing_extensions import NotRequired +from typing import Any, NotRequired, TypedDict from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index 33cb4b9e576685..a074b3b9b650a3 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -2,9 +2,8 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, Self -from typing_extensions import Self import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/components/input_button/__init__.py b/homeassistant/components/input_button/__init__.py index 8a1f0785435b7b..c04b18b0c25a3c 100644 --- a/homeassistant/components/input_button/__init__.py +++ b/homeassistant/components/input_button/__init__.py @@ -2,9 +2,8 @@ from __future__ import annotations import logging -from typing import cast +from typing import Self, cast -from typing_extensions import Self import voluptuous as vol from homeassistant.components.button import SERVICE_PRESS, ButtonEntity diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 8762769194fa80..81882137fad371 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -3,9 +3,8 @@ import datetime as py_datetime import logging -from typing import Any +from typing import Any, Self -from typing_extensions import Self import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 061b388ace559b..197a35246d214b 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -3,8 +3,8 @@ from contextlib import suppress import logging +from typing import Self -from typing_extensions import Self import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 2c5a1c87f29e90..e1354cb26a505e 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -2,9 +2,8 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import Any, Self, cast -from typing_extensions import Self import voluptuous as vol from homeassistant.components.select import ( diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index efd58e38e72095..096e7cbb10564d 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -2,8 +2,8 @@ from __future__ import annotations import logging +from typing import Self -from typing_extensions import Self import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index af4248e5e3ba45..e4ae3cde88302a 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -4,9 +4,8 @@ from dataclasses import dataclass from decimal import Decimal, DecimalException, InvalidOperation import logging -from typing import Any, Final +from typing import Any, Final, Self -from typing_extensions import Self import voluptuous as vol from homeassistant.components.sensor import ( diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 0c3a711a738798..0f49ab605a7086 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -8,9 +8,8 @@ from enum import IntFlag import logging import os -from typing import Any, cast, final +from typing import Any, Self, cast, final -from typing_extensions import Self import voluptuous as vol from homeassistant.backports.enum import StrEnum diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index f9c68e2b1f01d7..36512620e516fd 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -12,14 +12,13 @@ from http import HTTPStatus import logging import secrets -from typing import Any, Final, TypedDict, final +from typing import Any, Final, Required, TypedDict, final from urllib.parse import quote, urlparse from aiohttp import web from aiohttp.hdrs import CACHE_CONTROL, CONTENT_TYPE from aiohttp.typedefs import LooseHeaders import async_timeout -from typing_extensions import Required import voluptuous as vol from yarl import URL diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 32b095230d90f2..16bfc93f715a98 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -6,10 +6,9 @@ import logging from random import randrange from types import MappingProxyType -from typing import Any +from typing import Any, Self import metno -from typing_extensions import Self from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py index a5068e1a47bd79..7edb11797ebecc 100644 --- a/homeassistant/components/minio/minio_helper.py +++ b/homeassistant/components/minio/minio_helper.py @@ -8,10 +8,10 @@ import re import threading import time +from typing import Self from urllib.parse import unquote from minio import Minio -from typing_extensions import Self from urllib3.exceptions import HTTPError _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 24c44b901a1304..aa3566c5a95d89 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -8,9 +8,8 @@ import inspect import logging from math import ceil, floor -from typing import Any, final +from typing import Any, Self, final -from typing_extensions import Self import voluptuous as vol from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 0743864aaf767c..c99aadb8caa9f2 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta import logging import time -from typing import Any, cast +from typing import Any, Self, cast import ciso8601 from fnv_hash_fast import fnv1a_32 @@ -33,7 +33,6 @@ from sqlalchemy.ext.compiler import compiles from sqlalchemy.orm import DeclarativeBase, Mapped, aliased, mapped_column, relationship from sqlalchemy.types import TypeDecorator -from typing_extensions import Self from homeassistant.const import ( MAX_LENGTH_EVENT_EVENT_TYPE, diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index e8303c12c10dc4..4d76c803da627f 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -11,9 +11,7 @@ from math import ceil, floor, log10 import re import sys -from typing import Any, Final, cast, final - -from typing_extensions import Self +from typing import Any, Final, Self, cast, final from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index caa3d974d0469e..c237a820e58ab0 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -8,11 +8,10 @@ from io import SEEK_END, BytesIO import logging from threading import Event -from typing import Any, cast +from typing import Any, Self, cast import attr import av -from typing_extensions import Self from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 923b61678517a1..61df78307f050e 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -5,9 +5,8 @@ from datetime import datetime, timedelta from functools import partial import logging -from typing import Any +from typing import Any, Self -from typing_extensions import Self import voluptuous as vol from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 3752f9c9cb5bcb..228e2071b4a194 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -4,8 +4,8 @@ from collections.abc import Callable from datetime import datetime, timedelta import logging +from typing import Self -from typing_extensions import Self import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 2658f50edad9b4..998e5a55e639b0 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -5,10 +5,9 @@ from dataclasses import dataclass import json import struct -from typing import Any, Literal, overload +from typing import Any, Literal, Self, overload from tuya_iot import TuyaDevice, TuyaDeviceManager -from typing_extensions import Self from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index f52b78b5a5269b..db03a1ccf2e1d9 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -5,10 +5,9 @@ from datetime import datetime, timedelta from decimal import Decimal, DecimalException, InvalidOperation import logging -from typing import Any +from typing import Any, Self from croniter import croniter -from typing_extensions import Self import voluptuous as vol from homeassistant.components.sensor import ( diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index c63db816711984..89bd601fdae815 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -7,9 +7,7 @@ from datetime import timedelta import inspect import logging -from typing import Any, Final, Literal, TypedDict, final - -from typing_extensions import Required +from typing import Any, Final, Literal, Required, TypedDict, final from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py index dc4283b4a765dd..7c6bbd2d2eec98 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -7,12 +7,12 @@ from datetime import datetime from ipaddress import IPv4Address import logging +from typing import Self from urllib.parse import urlparse import async_timeout from async_upnp_client.search import SsdpSearchListener from async_upnp_client.utils import CaseInsensitiveDict -from typing_extensions import Self from homeassistant import config_entries from homeassistant.components import network, ssdp diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py index 010c0f63e277db..b3b6e7f0483ce0 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -4,9 +4,8 @@ import abc import functools import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Self -from typing_extensions import Self import zigpy.exceptions from zigpy.zcl.foundation import Status diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 51ab65e3318a9d..1455173b27c217 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -8,9 +8,8 @@ import logging import random import time -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Self -from typing_extensions import Self from zigpy import types import zigpy.device import zigpy.exceptions diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 97258a77e2b66f..43f487f61d4e9d 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -5,9 +5,7 @@ from collections.abc import Callable import functools import logging -from typing import TYPE_CHECKING, Any - -from typing_extensions import Self +from typing import TYPE_CHECKING, Any, Self from homeassistant.const import ATTR_NAME from homeassistant.core import CALLBACK_TYPE, Event, callback diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 29d6cafe3c845c..807a5e73d00f29 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -3,9 +3,8 @@ import functools import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Self -from typing_extensions import Self import zigpy.exceptions from zigpy.zcl.foundation import Status diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 27b71484f3e1a5..e6f2f6ab4828c4 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -4,9 +4,8 @@ from enum import Enum import functools import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Self -from typing_extensions import Self from zigpy import types from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.clusters.security import IasWd diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index d13dd871865668..49ba46038f9d5b 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -5,9 +5,8 @@ import functools import numbers import sys -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Self -from typing_extensions import Self from zigpy import types from homeassistant.components.climate import HVACAction diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 451d96a122b8c8..f975cc5116dc18 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -3,9 +3,8 @@ import functools import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Self -from typing_extensions import Self import zigpy.exceptions from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.foundation import Status diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 35d835c8f16eee..8d04987d4fa1d0 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -4,9 +4,8 @@ from collections.abc import Callable import logging from operator import attrgetter -from typing import Any, cast +from typing import Any, Self, cast -from typing_extensions import Self import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a1c09b8815f0ed..6fa80406e61b79 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -11,9 +11,7 @@ import logging from random import randint from types import MappingProxyType -from typing import TYPE_CHECKING, Any, TypeVar, cast - -from typing_extensions import Self +from typing import TYPE_CHECKING, Any, Self, TypeVar, cast from . import data_entry_flow, loader from .backports.enum import StrEnum diff --git a/homeassistant/core.py b/homeassistant/core.py index 6ea03e85c43437..8bb30f5d57df02 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -31,6 +31,7 @@ Any, Generic, ParamSpec, + Self, TypeVar, cast, overload, @@ -38,7 +39,6 @@ from urllib.parse import urlparse import async_timeout -from typing_extensions import Self import voluptuous as vol import yarl diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 6f125ce359a940..c0a5860529e521 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -7,9 +7,8 @@ from dataclasses import dataclass import logging from types import MappingProxyType -from typing import Any, TypedDict +from typing import Any, Required, TypedDict -from typing_extensions import Required import voluptuous as vol from .backports.enum import StrEnum diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index ba69a76fbdd400..a580c013cd0f13 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -5,9 +5,8 @@ import logging import os from pathlib import Path -from typing import NamedTuple +from typing import NamedTuple, Self -from typing_extensions import Self import voluptuous as vol from homeassistant import loader diff --git a/homeassistant/helpers/httpx_client.py b/homeassistant/helpers/httpx_client.py index 93b199b1db56cf..ed02f8a710e3fb 100644 --- a/homeassistant/helpers/httpx_client.py +++ b/homeassistant/helpers/httpx_client.py @@ -3,10 +3,9 @@ from collections.abc import Callable import sys -from typing import Any +from typing import Any, Self import httpx -from typing_extensions import Self from homeassistant.const import APPLICATION_NAME, EVENT_HOMEASSISTANT_CLOSE, __version__ from homeassistant.core import Event, HomeAssistant, callback diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index ab3b93cf3c4734..4dd71a584ec58f 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -4,9 +4,7 @@ from abc import ABC, abstractmethod from datetime import datetime, timedelta import logging -from typing import Any, cast - -from typing_extensions import Self +from typing import Any, Self, cast from homeassistant.const import ATTR_RESTORED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, State, callback, valid_entity_id diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index c7087918cf0aaf..b97f781eaf369d 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -4,10 +4,9 @@ from collections.abc import Callable, Mapping, Sequence from enum import IntFlag from functools import cache -from typing import Any, Generic, Literal, TypedDict, TypeVar, cast +from typing import Any, Generic, Literal, Required, TypedDict, TypeVar, cast from uuid import UUID -from typing_extensions import Required import voluptuous as vol from homeassistant.backports.enum import StrEnum diff --git a/homeassistant/util/timeout.py b/homeassistant/util/timeout.py index 9d9f5d986a032f..6c1de55748f525 100644 --- a/homeassistant/util/timeout.py +++ b/homeassistant/util/timeout.py @@ -8,9 +8,7 @@ import asyncio import enum from types import TracebackType -from typing import Any - -from typing_extensions import Self +from typing import Any, Self from .async_ import run_callback_threadsafe diff --git a/tests/components/recorder/db_schema_30.py b/tests/components/recorder/db_schema_30.py index 40417752719184..55bee20df566b2 100644 --- a/tests/components/recorder/db_schema_30.py +++ b/tests/components/recorder/db_schema_30.py @@ -9,7 +9,7 @@ from datetime import datetime, timedelta import logging import time -from typing import Any, TypedDict, cast, overload +from typing import Any, Self, TypedDict, cast, overload import ciso8601 from fnv_hash_fast import fnv1a_32 @@ -34,7 +34,6 @@ from sqlalchemy.dialects import mysql, oracle, postgresql, sqlite from sqlalchemy.orm import aliased, declarative_base, relationship from sqlalchemy.orm.session import Session -from typing_extensions import Self from homeassistant.components.recorder.const import SupportedDialect from homeassistant.const import ( diff --git a/tests/components/recorder/db_schema_32.py b/tests/components/recorder/db_schema_32.py index 03a71697227b5a..660a2a54d4b521 100644 --- a/tests/components/recorder/db_schema_32.py +++ b/tests/components/recorder/db_schema_32.py @@ -9,7 +9,7 @@ from datetime import datetime, timedelta import logging import time -from typing import Any, TypedDict, cast, overload +from typing import Any, Self, TypedDict, cast, overload import ciso8601 from fnv_hash_fast import fnv1a_32 @@ -34,7 +34,6 @@ from sqlalchemy.dialects import mysql, oracle, postgresql, sqlite from sqlalchemy.orm import aliased, declarative_base, relationship from sqlalchemy.orm.session import Session -from typing_extensions import Self from homeassistant.components.recorder.const import SupportedDialect from homeassistant.const import ( From 45ec31423202cc27f862a18f3db645d7c35c72c3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 23 Jul 2023 00:03:53 +0200 Subject: [PATCH 0789/1009] Replace typing.Optional with new typing syntax (#97068) --- tests/components/mystrom/__init__.py | 28 ++++++++++++++-------------- tests/components/twitch/__init__.py | 8 ++++---- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/components/mystrom/__init__.py b/tests/components/mystrom/__init__.py index 21f6bd7a549b10..acd520cebaaa5b 100644 --- a/tests/components/mystrom/__init__.py +++ b/tests/components/mystrom/__init__.py @@ -1,5 +1,5 @@ """Tests for the myStrom integration.""" -from typing import Any, Optional +from typing import Any def get_default_device_response(device_type: int | None) -> dict[str, Any]: @@ -79,49 +79,49 @@ def __init__(self, mac: str, state: dict[str, Any]) -> None: self.mac = mac @property - def firmware(self) -> Optional[str]: + def firmware(self) -> str | None: """Return current firmware.""" if not self._requested_state: return None return self._state["fw_version"] @property - def consumption(self) -> Optional[float]: + def consumption(self) -> float | None: """Return current firmware.""" if not self._requested_state: return None return self._state["power"] @property - def color(self) -> Optional[str]: + def color(self) -> str | None: """Return current color settings.""" if not self._requested_state: return None return self._state["color"] @property - def mode(self) -> Optional[str]: + def mode(self) -> str | None: """Return current mode.""" if not self._requested_state: return None return self._state["mode"] @property - def transition_time(self) -> Optional[int]: + def transition_time(self) -> int | None: """Return current transition time (ramp).""" if not self._requested_state: return None return self._state["ramp"] @property - def bulb_type(self) -> Optional[str]: + def bulb_type(self) -> str | None: """Return the type of the bulb.""" if not self._requested_state: return None return self._state["type"] @property - def state(self) -> Optional[bool]: + def state(self) -> bool | None: """Return the current state of the bulb.""" if not self._requested_state: return None @@ -132,42 +132,42 @@ class MyStromSwitchMock(MyStromDeviceMock): """MyStrom Switch mock.""" @property - def relay(self) -> Optional[bool]: + def relay(self) -> bool | None: """Return the relay state.""" if not self._requested_state: return None return self._state["on"] @property - def consumption(self) -> Optional[float]: + def consumption(self) -> float | None: """Return the current power consumption in mWh.""" if not self._requested_state: return None return self._state["power"] @property - def consumedWs(self) -> Optional[float]: + def consumedWs(self) -> float | None: """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]: + def firmware(self) -> str | None: """Return the current firmware.""" if not self._requested_state: return None return self._state["version"] @property - def mac(self) -> Optional[str]: + def mac(self) -> str | None: """Return the MAC address.""" if not self._requested_state: return None return self._state["mac"] @property - def temperature(self) -> Optional[float]: + def temperature(self) -> float | None: """Return the current temperature in celsius.""" if not self._requested_state: return None diff --git a/tests/components/twitch/__init__.py b/tests/components/twitch/__init__.py index 5c371a0e2ee4c5..bf35484f53e812 100644 --- a/tests/components/twitch/__init__.py +++ b/tests/components/twitch/__init__.py @@ -2,7 +2,7 @@ import asyncio from collections.abc import AsyncGenerator from dataclasses import dataclass -from typing import Any, Optional +from typing import Any from twitchAPI.object import TwitchUser from twitchAPI.twitch import ( @@ -88,7 +88,7 @@ async def _noop(self): pass async def get_users( - self, user_ids: Optional[list[str]] = None, logins: Optional[list[str]] = None + self, user_ids: list[str] | None = None, logins: list[str] | None = None ) -> AsyncGenerator[TwitchUser, None]: """Get list of mock users.""" for user in [USER_OBJECT]: @@ -101,7 +101,7 @@ def has_required_auth( return True async def get_users_follows( - self, to_id: Optional[str] = None, from_id: Optional[str] = None + self, to_id: str | None = None, from_id: str | None = None ) -> TwitchUserFollowResultMock: """Return the followers of the user.""" if self._is_following: @@ -169,7 +169,7 @@ class TwitchInvalidUserMock(TwitchMock): """Twitch mock to test invalid user.""" async def get_users( - self, user_ids: Optional[list[str]] = None, logins: Optional[list[str]] = None + self, user_ids: list[str] | None = None, logins: list[str] | None = None ) -> AsyncGenerator[TwitchUser, None]: """Get list of mock users.""" if user_ids is not None or logins is not None: From da6802b009d5902e30736d81a340ad3d76fd741e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 23 Jul 2023 00:04:45 +0200 Subject: [PATCH 0790/1009] Drop tomli (#97064) --- requirements_test.txt | 2 -- script/gen_requirements_all.py | 7 ++----- script/hassfest/metadata.py | 7 +------ 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index e20e28b3d0a2a6..5972f9809c0e67 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -32,7 +32,6 @@ pytest==7.3.1 requests_mock==1.11.0 respx==0.20.2 syrupy==4.0.8 -tomli==2.0.1;python_version<"3.11" tqdm==4.65.0 types-atomicwrites==1.4.5.1 types-croniter==1.0.6 @@ -48,4 +47,3 @@ types-python-slugify==0.1.2 types-pytz==2023.3.0.0 types-PyYAML==6.0.12.2 types-requests==2.31.0.1 -types-toml==0.10.8.6 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 8258543df1d9af..f215b649bb2ad3 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -11,14 +11,11 @@ import sys from typing import Any +import tomllib + from homeassistant.util.yaml.loader import load_yaml from script.hassfest.model import Integration -if sys.version_info >= (3, 11): - import tomllib -else: - import tomli as tomllib - COMMENT_REQUIREMENTS = ( "Adafruit-BBIO", "atenpdu", # depends on pysnmp which is not maintained at this time diff --git a/script/hassfest/metadata.py b/script/hassfest/metadata.py index 88a433fe3fa208..091c1b88e3058a 100644 --- a/script/hassfest/metadata.py +++ b/script/hassfest/metadata.py @@ -1,15 +1,10 @@ """Package metadata validation.""" -import sys +import tomllib from homeassistant.const import REQUIRED_PYTHON_VER, __version__ from .model import Config, Integration -if sys.version_info >= (3, 11): - import tomllib -else: - import tomli as tomllib - def validate(integrations: dict[str, Integration], config: Config) -> None: """Validate project metadata keys.""" From fe0fe19be9a0c0e0e426dd7d2e206dc836162a54 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 23 Jul 2023 00:05:11 +0200 Subject: [PATCH 0791/1009] Use datetime.UTC alias [3.11] (#97067) --- homeassistant/components/google/api.py | 6 ++---- homeassistant/components/withings/common.py | 2 +- tests/components/gdacs/test_geo_location.py | 12 ++++-------- .../generic_hygrostat/test_humidifier.py | 16 ++++------------ .../geonetnz_quakes/test_geo_location.py | 6 ++---- tests/components/geonetnz_quakes/test_sensor.py | 2 +- .../ign_sismologia/test_geo_location.py | 4 ++-- tests/components/input_datetime/test_init.py | 2 +- tests/components/metoffice/test_init.py | 4 +--- tests/components/metoffice/test_sensor.py | 8 ++------ tests/components/metoffice/test_weather.py | 16 ++++------------ .../test_geo_location.py | 6 ++---- .../components/qld_bushfire/test_geo_location.py | 8 ++++---- tests/components/recorder/test_websocket_api.py | 12 +++--------- .../usgs_earthquakes_feed/test_geo_location.py | 12 ++++-------- tests/conftest.py | 2 +- 16 files changed, 38 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index a3a5b7246b6160..f37e120db68e4c 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -51,9 +51,7 @@ class DeviceAuth(AuthImplementation): async def async_resolve_external_data(self, external_data: Any) -> dict: """Resolve a Google API Credentials object to Home Assistant token.""" creds: Credentials = external_data[DEVICE_AUTH_CREDS] - delta = ( - creds.token_expiry.replace(tzinfo=datetime.timezone.utc) - dt_util.utcnow() - ) + delta = creds.token_expiry.replace(tzinfo=datetime.UTC) - dt_util.utcnow() _LOGGER.debug( "Token expires at %s (in %s)", creds.token_expiry, delta.total_seconds() ) @@ -116,7 +114,7 @@ def async_start_exchange(self) -> None: # For some reason, oauth.step1_get_device_and_user_codes() returns a datetime # object without tzinfo. For the comparison below to work, it needs one. user_code_expiry = self._device_flow_info.user_code_expiry.replace( - tzinfo=datetime.timezone.utc + tzinfo=datetime.UTC ) expiration_time = min(user_code_expiry, max_timeout) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 1b173e3a3778a5..da43ae973cddb4 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -449,7 +449,7 @@ async def async_get_sleep_summary(self) -> dict[Measurement, Any]: 0, 0, 0, - datetime.timezone.utc, + datetime.UTC, ) def get_sleep_summary() -> SleepGetSummaryResponse: diff --git a/tests/components/gdacs/test_geo_location.py b/tests/components/gdacs/test_geo_location.py index fc20ecd406c943..d279fe981d4e04 100644 --- a/tests/components/gdacs/test_geo_location.py +++ b/tests/components/gdacs/test_geo_location.py @@ -58,8 +58,8 @@ async def test_setup(hass: HomeAssistant) -> None: alert_level="Alert Level 1", country="Country 1", attribution="Attribution 1", - from_date=datetime.datetime(2020, 1, 10, 8, 0, tzinfo=datetime.timezone.utc), - to_date=datetime.datetime(2020, 1, 20, 8, 0, tzinfo=datetime.timezone.utc), + from_date=datetime.datetime(2020, 1, 10, 8, 0, tzinfo=datetime.UTC), + to_date=datetime.datetime(2020, 1, 20, 8, 0, tzinfo=datetime.UTC), duration_in_week=1, population="Population 1", severity="Severity 1", @@ -120,12 +120,8 @@ async def test_setup(hass: HomeAssistant) -> None: ATTR_DESCRIPTION: "Description 1", ATTR_COUNTRY: "Country 1", ATTR_ATTRIBUTION: "Attribution 1", - ATTR_FROM_DATE: datetime.datetime( - 2020, 1, 10, 8, 0, tzinfo=datetime.timezone.utc - ), - ATTR_TO_DATE: datetime.datetime( - 2020, 1, 20, 8, 0, tzinfo=datetime.timezone.utc - ), + ATTR_FROM_DATE: datetime.datetime(2020, 1, 10, 8, 0, tzinfo=datetime.UTC), + ATTR_TO_DATE: datetime.datetime(2020, 1, 20, 8, 0, tzinfo=datetime.UTC), ATTR_DURATION_IN_WEEK: 1, ATTR_ALERT_LEVEL: "Alert Level 1", ATTR_POPULATION: "Population 1", diff --git a/tests/components/generic_hygrostat/test_humidifier.py b/tests/components/generic_hygrostat/test_humidifier.py index dcb1608b7102ef..e3fb26ffe229a9 100644 --- a/tests/components/generic_hygrostat/test_humidifier.py +++ b/tests/components/generic_hygrostat/test_humidifier.py @@ -869,9 +869,7 @@ async def test_humidity_change_dry_trigger_on_long_enough( hass: HomeAssistant, setup_comp_4 ) -> None: """Test if humidity change turn dry on.""" - fake_changed = datetime.datetime( - 1970, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc - ) + fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=datetime.UTC) with freeze_time(fake_changed): calls = await _setup_switch(hass, False) _setup_sensor(hass, 35) @@ -905,9 +903,7 @@ async def test_humidity_change_dry_trigger_off_long_enough( hass: HomeAssistant, setup_comp_4 ) -> None: """Test if humidity change turn dry on.""" - fake_changed = datetime.datetime( - 1970, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc - ) + fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=datetime.UTC) with freeze_time(fake_changed): calls = await _setup_switch(hass, True) _setup_sensor(hass, 45) @@ -1031,9 +1027,7 @@ async def test_humidity_change_humidifier_trigger_on_long_enough( hass: HomeAssistant, setup_comp_6 ) -> None: """Test if humidity change turn humidifier on after min cycle.""" - fake_changed = datetime.datetime( - 1970, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc - ) + fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=datetime.UTC) with freeze_time(fake_changed): calls = await _setup_switch(hass, False) _setup_sensor(hass, 45) @@ -1053,9 +1047,7 @@ async def test_humidity_change_humidifier_trigger_off_long_enough( hass: HomeAssistant, setup_comp_6 ) -> None: """Test if humidity change turn humidifier off after min cycle.""" - fake_changed = datetime.datetime( - 1970, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc - ) + fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=datetime.UTC) with freeze_time(fake_changed): calls = await _setup_switch(hass, True) _setup_sensor(hass, 35) diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py index 19c85be0d9d2b7..bfe94bbf304f21 100644 --- a/tests/components/geonetnz_quakes/test_geo_location.py +++ b/tests/components/geonetnz_quakes/test_geo_location.py @@ -48,7 +48,7 @@ async def test_setup(hass: HomeAssistant) -> None: (38.0, -3.0), locality="Locality 1", attribution="Attribution 1", - time=datetime.datetime(2018, 9, 22, 8, 0, tzinfo=datetime.timezone.utc), + time=datetime.datetime(2018, 9, 22, 8, 0, tzinfo=datetime.UTC), magnitude=5.7, mmi=5, depth=10.5, @@ -93,9 +93,7 @@ async def test_setup(hass: HomeAssistant) -> None: ATTR_FRIENDLY_NAME: "Title 1", ATTR_LOCALITY: "Locality 1", ATTR_ATTRIBUTION: "Attribution 1", - ATTR_TIME: datetime.datetime( - 2018, 9, 22, 8, 0, tzinfo=datetime.timezone.utc - ), + ATTR_TIME: datetime.datetime(2018, 9, 22, 8, 0, tzinfo=datetime.UTC), ATTR_MAGNITUDE: 5.7, ATTR_DEPTH: 10.5, ATTR_MMI: 5, diff --git a/tests/components/geonetnz_quakes/test_sensor.py b/tests/components/geonetnz_quakes/test_sensor.py index 253d44ee9ee445..27f67dad322410 100644 --- a/tests/components/geonetnz_quakes/test_sensor.py +++ b/tests/components/geonetnz_quakes/test_sensor.py @@ -41,7 +41,7 @@ async def test_setup(hass: HomeAssistant) -> None: (38.0, -3.0), locality="Locality 1", attribution="Attribution 1", - time=datetime.datetime(2018, 9, 22, 8, 0, tzinfo=datetime.timezone.utc), + time=datetime.datetime(2018, 9, 22, 8, 0, tzinfo=datetime.UTC), magnitude=5.7, mmi=5, depth=10.5, diff --git a/tests/components/ign_sismologia/test_geo_location.py b/tests/components/ign_sismologia/test_geo_location.py index 4769da29019765..02a11b3fe7aba5 100644 --- a/tests/components/ign_sismologia/test_geo_location.py +++ b/tests/components/ign_sismologia/test_geo_location.py @@ -81,7 +81,7 @@ async def test_setup(hass: HomeAssistant) -> None: (38.0, -3.0), region="Region 1", attribution="Attribution 1", - published=datetime.datetime(2018, 9, 22, 8, 0, tzinfo=datetime.timezone.utc), + published=datetime.datetime(2018, 9, 22, 8, 0, tzinfo=datetime.UTC), magnitude=5.7, image_url="http://image.url/map.jpg", ) @@ -125,7 +125,7 @@ async def test_setup(hass: HomeAssistant) -> None: ATTR_REGION: "Region 1", ATTR_ATTRIBUTION: "Attribution 1", ATTR_PUBLICATION_DATE: datetime.datetime( - 2018, 9, 22, 8, 0, tzinfo=datetime.timezone.utc + 2018, 9, 22, 8, 0, tzinfo=datetime.UTC ), ATTR_IMAGE_URL: "http://image.url/map.jpg", ATTR_MAGNITUDE: 5.7, diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index e9f9458611a933..940d0ff6c55a46 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -736,7 +736,7 @@ async def test_timestamp(hass: HomeAssistant) -> None: assert ( dt_util.as_local( datetime.datetime.fromtimestamp( - state_without_tz.attributes[ATTR_TIMESTAMP], datetime.timezone.utc + state_without_tz.attributes[ATTR_TIMESTAMP], datetime.UTC ) ).strftime(FORMAT_DATETIME) == "2020-12-13 10:00:00" diff --git a/tests/components/metoffice/test_init.py b/tests/components/metoffice/test_init.py index f21f3a1b26fe52..a9e286907d5035 100644 --- a/tests/components/metoffice/test_init.py +++ b/tests/components/metoffice/test_init.py @@ -15,9 +15,7 @@ from tests.common import MockConfigEntry -@pytest.mark.freeze_time( - datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.timezone.utc) -) +@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) @pytest.mark.parametrize( ("old_unique_id", "new_unique_id", "migration_needed"), [ diff --git a/tests/components/metoffice/test_sensor.py b/tests/components/metoffice/test_sensor.py index 28bf8eda997367..6e40dd66efe32a 100644 --- a/tests/components/metoffice/test_sensor.py +++ b/tests/components/metoffice/test_sensor.py @@ -24,9 +24,7 @@ from tests.common import MockConfigEntry, load_fixture -@pytest.mark.freeze_time( - datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.timezone.utc) -) +@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) async def test_one_sensor_site_running( hass: HomeAssistant, requests_mock: requests_mock.Mocker ) -> None: @@ -74,9 +72,7 @@ async def test_one_sensor_site_running( assert sensor.attributes.get("attribution") == ATTRIBUTION -@pytest.mark.freeze_time( - datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.timezone.utc) -) +@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) async def test_two_sensor_sites_running( hass: HomeAssistant, requests_mock: requests_mock.Mocker ) -> None: diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 0e5a934c7d0b92..673475c0303695 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -23,9 +23,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture -@pytest.mark.freeze_time( - datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.timezone.utc) -) +@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) async def test_site_cannot_connect( hass: HomeAssistant, requests_mock: requests_mock.Mocker ) -> None: @@ -54,9 +52,7 @@ async def test_site_cannot_connect( assert sensor is None -@pytest.mark.freeze_time( - datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.timezone.utc) -) +@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) async def test_site_cannot_update( hass: HomeAssistant, requests_mock: requests_mock.Mocker ) -> None: @@ -104,9 +100,7 @@ async def test_site_cannot_update( assert weather.state == STATE_UNAVAILABLE -@pytest.mark.freeze_time( - datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.timezone.utc) -) +@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) async def test_one_weather_site_running( hass: HomeAssistant, requests_mock: requests_mock.Mocker ) -> None: @@ -189,9 +183,7 @@ async def test_one_weather_site_running( assert weather.attributes.get("forecast")[3]["wind_bearing"] == "SE" -@pytest.mark.freeze_time( - datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.timezone.utc) -) +@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) async def test_two_weather_sites_running( hass: HomeAssistant, requests_mock: requests_mock.Mocker ) -> None: diff --git a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py index 85d4b16048a27e..673ac1a72d46fd 100644 --- a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py +++ b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py @@ -101,9 +101,7 @@ async def test_setup(hass: HomeAssistant) -> None: category="Category 1", location="Location 1", attribution="Attribution 1", - publication_date=datetime.datetime( - 2018, 9, 22, 8, 0, tzinfo=datetime.timezone.utc - ), + publication_date=datetime.datetime(2018, 9, 22, 8, 0, tzinfo=datetime.UTC), council_area="Council Area 1", status="Status 1", entry_type="Type 1", @@ -148,7 +146,7 @@ async def test_setup(hass: HomeAssistant) -> None: ATTR_LOCATION: "Location 1", ATTR_ATTRIBUTION: "Attribution 1", ATTR_PUBLICATION_DATE: datetime.datetime( - 2018, 9, 22, 8, 0, tzinfo=datetime.timezone.utc + 2018, 9, 22, 8, 0, tzinfo=datetime.UTC ), ATTR_FIRE: True, ATTR_COUNCIL_AREA: "Council Area 1", diff --git a/tests/components/qld_bushfire/test_geo_location.py b/tests/components/qld_bushfire/test_geo_location.py index a164cfbeb78f62..18b33a6ef0cd27 100644 --- a/tests/components/qld_bushfire/test_geo_location.py +++ b/tests/components/qld_bushfire/test_geo_location.py @@ -80,8 +80,8 @@ async def test_setup(hass: HomeAssistant) -> None: (38.0, -3.0), category="Category 1", attribution="Attribution 1", - published=datetime.datetime(2018, 9, 22, 8, 0, tzinfo=datetime.timezone.utc), - updated=datetime.datetime(2018, 9, 22, 8, 10, tzinfo=datetime.timezone.utc), + published=datetime.datetime(2018, 9, 22, 8, 0, tzinfo=datetime.UTC), + updated=datetime.datetime(2018, 9, 22, 8, 10, tzinfo=datetime.UTC), status="Status 1", ) mock_entry_2 = _generate_mock_feed_entry("2345", "Title 2", 20.5, (38.1, -3.1)) @@ -119,10 +119,10 @@ async def test_setup(hass: HomeAssistant) -> None: ATTR_CATEGORY: "Category 1", ATTR_ATTRIBUTION: "Attribution 1", ATTR_PUBLICATION_DATE: datetime.datetime( - 2018, 9, 22, 8, 0, tzinfo=datetime.timezone.utc + 2018, 9, 22, 8, 0, tzinfo=datetime.UTC ), ATTR_UPDATED_DATE: datetime.datetime( - 2018, 9, 22, 8, 10, tzinfo=datetime.timezone.utc + 2018, 9, 22, 8, 10, tzinfo=datetime.UTC ), ATTR_STATUS: "Status 1", ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 2c76c9473505dd..32d4fabb02bead 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -215,9 +215,7 @@ async def test_statistics_during_period( } -@pytest.mark.freeze_time( - datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.timezone.utc) -) +@pytest.mark.freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.UTC)) @pytest.mark.parametrize("offset", (0, 1, 2)) async def test_statistic_during_period( recorder_mock: Recorder, @@ -632,9 +630,7 @@ def next_id(): } -@pytest.mark.freeze_time( - datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.timezone.utc) -) +@pytest.mark.freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.UTC)) async def test_statistic_during_period_hole( recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -797,9 +793,7 @@ def next_id(): } -@pytest.mark.freeze_time( - datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.timezone.utc) -) +@pytest.mark.freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.UTC)) @pytest.mark.parametrize( ("calendar_period", "start_time", "end_time"), ( diff --git a/tests/components/usgs_earthquakes_feed/test_geo_location.py b/tests/components/usgs_earthquakes_feed/test_geo_location.py index 1288c0ae177a83..6307125930c7de 100644 --- a/tests/components/usgs_earthquakes_feed/test_geo_location.py +++ b/tests/components/usgs_earthquakes_feed/test_geo_location.py @@ -102,8 +102,8 @@ async def test_setup(hass: HomeAssistant) -> None: (-31.0, 150.0), place="Location 1", attribution="Attribution 1", - time=datetime.datetime(2018, 9, 22, 8, 0, tzinfo=datetime.timezone.utc), - updated=datetime.datetime(2018, 9, 22, 9, 0, tzinfo=datetime.timezone.utc), + time=datetime.datetime(2018, 9, 22, 8, 0, tzinfo=datetime.UTC), + updated=datetime.datetime(2018, 9, 22, 9, 0, tzinfo=datetime.UTC), magnitude=5.7, status="Status 1", entry_type="Type 1", @@ -143,12 +143,8 @@ async def test_setup(hass: HomeAssistant) -> None: ATTR_FRIENDLY_NAME: "Title 1", ATTR_PLACE: "Location 1", ATTR_ATTRIBUTION: "Attribution 1", - ATTR_TIME: datetime.datetime( - 2018, 9, 22, 8, 0, tzinfo=datetime.timezone.utc - ), - ATTR_UPDATED: datetime.datetime( - 2018, 9, 22, 9, 0, tzinfo=datetime.timezone.utc - ), + ATTR_TIME: datetime.datetime(2018, 9, 22, 8, 0, tzinfo=datetime.UTC), + ATTR_UPDATED: datetime.datetime(2018, 9, 22, 9, 0, tzinfo=datetime.UTC), ATTR_STATUS: "Status 1", ATTR_TYPE: "Type 1", ATTR_ALERT: "Alert 1", diff --git a/tests/conftest.py b/tests/conftest.py index 1b8a37e48f44fa..40fd1c2eef0920 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -111,7 +111,7 @@ def _utcnow() -> datetime.datetime: """Make utcnow patchable by freezegun.""" - return datetime.datetime.now(datetime.timezone.utc) + return datetime.datetime.now(datetime.UTC) dt_util.utcnow = _utcnow # type: ignore[assignment] From e60313628f8fd06971e34213b5ca7128ec744927 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Jul 2023 17:06:32 -0500 Subject: [PATCH 0792/1009] Add a cancel message to the aiohttp compatiblity layer (#97058) --- homeassistant/helpers/aiohttp_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/aiohttp_compat.py b/homeassistant/helpers/aiohttp_compat.py index 1780cd053f59ac..78aad44fa66593 100644 --- a/homeassistant/helpers/aiohttp_compat.py +++ b/homeassistant/helpers/aiohttp_compat.py @@ -12,7 +12,7 @@ def connection_lost(self, exc: BaseException | None) -> None: task_handler = self._task_handler super().connection_lost(exc) if task_handler is not None: - task_handler.cancel() + task_handler.cancel("aiohttp connection lost") def restore_original_aiohttp_cancel_behavior() -> None: From b90137f4c62c74743ea8d1efde5cc47c4c72fc90 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Jul 2023 17:52:38 -0500 Subject: [PATCH 0793/1009] Add another OUI to tplink (#97062) --- homeassistant/components/tplink/manifest.json | 44 ++++++++++-------- homeassistant/generated/dhcp.py | 46 +++++++++++-------- 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 581360050535f9..c0e85f3dc58344 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -32,6 +32,10 @@ "hostname": "hs*", "macaddress": "9C5322*" }, + { + "hostname": "k[lps]*", + "macaddress": "9C5322*" + }, { "hostname": "hs*", "macaddress": "1C3BF3*" @@ -77,76 +81,80 @@ "macaddress": "B09575*" }, { - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "60A4B7*" }, { - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "005F67*" }, { - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "1027F5*" }, { - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "B0A7B9*" }, { - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "403F8C*" }, { - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "C0C9E3*" }, { - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "909A4A*" }, { - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "E848B8*" }, { - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "003192*" }, { - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "1C3BF3*" }, { - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "50C7BF*" }, { - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "68FF7B*" }, { - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "98DAC4*" }, { - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "B09575*" }, { - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "C006C3*" }, { - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "6C5AB0*" }, { - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "54AF97*" }, { - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "AC15A2*" + }, + { + "hostname": "k[lps]*", + "macaddress": "788C5B*" } ], "documentation": "https://www.home-assistant.io/integrations/tplink", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 052edf09bec309..8b5dd91f64cdb1 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -629,6 +629,11 @@ "hostname": "hs*", "macaddress": "9C5322*", }, + { + "domain": "tplink", + "hostname": "k[lps]*", + "macaddress": "9C5322*", + }, { "domain": "tplink", "hostname": "hs*", @@ -686,94 +691,99 @@ }, { "domain": "tplink", - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "60A4B7*", }, { "domain": "tplink", - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "005F67*", }, { "domain": "tplink", - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "1027F5*", }, { "domain": "tplink", - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "B0A7B9*", }, { "domain": "tplink", - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "403F8C*", }, { "domain": "tplink", - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "C0C9E3*", }, { "domain": "tplink", - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "909A4A*", }, { "domain": "tplink", - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "E848B8*", }, { "domain": "tplink", - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "003192*", }, { "domain": "tplink", - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "1C3BF3*", }, { "domain": "tplink", - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "50C7BF*", }, { "domain": "tplink", - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "68FF7B*", }, { "domain": "tplink", - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "98DAC4*", }, { "domain": "tplink", - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "B09575*", }, { "domain": "tplink", - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "C006C3*", }, { "domain": "tplink", - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "6C5AB0*", }, { "domain": "tplink", - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "54AF97*", }, { "domain": "tplink", - "hostname": "k[lp]*", + "hostname": "k[lps]*", "macaddress": "AC15A2*", }, + { + "domain": "tplink", + "hostname": "k[lps]*", + "macaddress": "788C5B*", + }, { "domain": "tuya", "macaddress": "105A17*", From bf66dc7a912caa1d27e512afc25c68ae8d280f42 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 23 Jul 2023 04:59:56 +0200 Subject: [PATCH 0794/1009] Use entity name naming for Nanoleaf (#95741) * Use device class naming for Nanoleaf * Remove device class icon --- homeassistant/components/nanoleaf/button.py | 8 ++++---- homeassistant/components/nanoleaf/entity.py | 4 +++- homeassistant/components/nanoleaf/light.py | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nanoleaf/button.py b/homeassistant/components/nanoleaf/button.py index 1c6acc516b84d8..950dc2a591a0eb 100644 --- a/homeassistant/components/nanoleaf/button.py +++ b/homeassistant/components/nanoleaf/button.py @@ -2,7 +2,7 @@ from aionanoleaf import Nanoleaf -from homeassistant.components.button import ButtonEntity +from homeassistant.components.button import ButtonDeviceClass, ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant @@ -27,15 +27,15 @@ async def async_setup_entry( class NanoleafIdentifyButton(NanoleafEntity, ButtonEntity): """Representation of a Nanoleaf identify button.""" + _attr_entity_category = EntityCategory.CONFIG + _attr_device_class = ButtonDeviceClass.IDENTIFY + def __init__( self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator[None] ) -> None: """Initialize the Nanoleaf button.""" super().__init__(nanoleaf, coordinator) self._attr_unique_id = f"{nanoleaf.serial_no}_identify" - self._attr_name = f"Identify {nanoleaf.name}" - self._attr_icon = "mdi:magnify" - self._attr_entity_category = EntityCategory.CONFIG async def async_press(self) -> None: """Identify the Nanoleaf.""" diff --git a/homeassistant/components/nanoleaf/entity.py b/homeassistant/components/nanoleaf/entity.py index 0fb043c4cc432e..16fb746049db81 100644 --- a/homeassistant/components/nanoleaf/entity.py +++ b/homeassistant/components/nanoleaf/entity.py @@ -14,10 +14,12 @@ class NanoleafEntity(CoordinatorEntity[DataUpdateCoordinator[None]]): """Representation of a Nanoleaf entity.""" + _attr_has_entity_name = True + def __init__( self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator[None] ) -> None: - """Initialize an Nanoleaf entity.""" + """Initialize a Nanoleaf entity.""" super().__init__(coordinator) self._nanoleaf = nanoleaf self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index 20992594cb87d7..f0425594763e12 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -46,6 +46,7 @@ class NanoleafLight(NanoleafEntity, LightEntity): _attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} _attr_supported_features = LightEntityFeature.EFFECT | LightEntityFeature.TRANSITION + _attr_name = None def __init__( self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator[None] @@ -53,7 +54,6 @@ def __init__( """Initialize the Nanoleaf light.""" super().__init__(nanoleaf, coordinator) self._attr_unique_id = nanoleaf.serial_no - self._attr_name = nanoleaf.name self._attr_min_mireds = math.ceil(1000000 / nanoleaf.color_temperature_max) self._attr_max_mireds = kelvin_to_mired(nanoleaf.color_temperature_min) From 095146b1639c5ccf80137d9544fd0de399f02a96 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Jul 2023 03:45:48 -0500 Subject: [PATCH 0795/1009] Fix has_entity_name not always being set in ESPHome (#97055) --- .coveragerc | 1 - homeassistant/components/esphome/__init__.py | 1 + homeassistant/components/esphome/entity.py | 2 +- .../components/esphome/entry_data.py | 39 +++++++++------ homeassistant/components/esphome/manager.py | 13 ++--- tests/components/esphome/conftest.py | 14 +++--- tests/components/esphome/test_entity.py | 35 +++++++++++++ tests/components/esphome/test_sensor.py | 49 +++++++++++++++++-- 8 files changed, 120 insertions(+), 34 deletions(-) diff --git a/.coveragerc b/.coveragerc index 4f3e82042f6cc0..db1914055226c1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -306,7 +306,6 @@ omit = homeassistant/components/escea/climate.py homeassistant/components/escea/discovery.py homeassistant/components/esphome/bluetooth/* - homeassistant/components/esphome/entry_data.py homeassistant/components/esphome/manager.py homeassistant/components/etherscan/sensor.py homeassistant/components/eufy/* diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index fb13e86dd1d64b..4a36535cc9bfbe 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -59,6 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_data = RuntimeEntryData( client=cli, entry_id=entry.entry_id, + title=entry.title, store=domain_data.get_or_create_store(hass, entry), original_options=dict(entry.options), ) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 2cfbc537dbb150..6b0a4cd6b26841 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -140,6 +140,7 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): """Define a base esphome entity.""" _attr_should_poll = False + _attr_has_entity_name = True _static_info: _InfoT _state: _StateT _has_state: bool @@ -164,7 +165,6 @@ def __init__( if object_id := entity_info.object_id: # Use the object_id to suggest the entity_id self.entity_id = f"{domain}.{device_info.name}_{object_id}" - self._attr_has_entity_name = bool(device_info.friendly_name) self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)} ) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 2d147d243f200c..b7870e9cca0847 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -86,6 +86,7 @@ class RuntimeEntryData: """Store runtime data for esphome config entries.""" entry_id: str + title: str client: APIClient store: ESPHomeStorage state: dict[type[EntityState], dict[int, EntityState]] = field(default_factory=dict) @@ -127,14 +128,16 @@ class RuntimeEntryData: @property def name(self) -> str: """Return the name of the device.""" - return self.device_info.name if self.device_info else self.entry_id + device_info = self.device_info + return (device_info and device_info.name) or self.title @property def friendly_name(self) -> str: """Return the friendly name of the device.""" - if self.device_info and self.device_info.friendly_name: - return self.device_info.friendly_name - return self.name + device_info = self.device_info + return (device_info and device_info.friendly_name) or self.name.title().replace( + "_", " " + ) @property def signal_device_updated(self) -> str: @@ -303,6 +306,7 @@ def async_update_state(self, state: EntityState) -> None: current_state_by_type = self.state[state_type] current_state = current_state_by_type.get(key, _SENTINEL) subscription_key = (state_type, key) + debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG) if ( current_state == state and subscription_key not in stale_state @@ -314,19 +318,21 @@ def async_update_state(self, state: EntityState) -> None: and (cast(SensorInfo, entity_info)).force_update ) ): + if debug_enabled: + _LOGGER.debug( + "%s: ignoring duplicate update with key %s: %s", + self.name, + key, + state, + ) + return + if debug_enabled: _LOGGER.debug( - "%s: ignoring duplicate update with key %s: %s", + "%s: dispatching update with key %s: %s", self.name, key, state, ) - return - _LOGGER.debug( - "%s: dispatching update with key %s: %s", - self.name, - key, - state, - ) stale_state.discard(subscription_key) current_state_by_type[key] = state if subscription := self.state_subscriptions.get(subscription_key): @@ -367,8 +373,8 @@ async def async_load_from_store(self) -> tuple[list[EntityInfo], list[UserServic async def async_save_to_store(self) -> None: """Generate dynamic data to store and save it to the filesystem.""" - if self.device_info is None: - raise ValueError("device_info is not set yet") + if TYPE_CHECKING: + assert self.device_info is not None store_data: StoreData = { "device_info": self.device_info.to_dict(), "services": [], @@ -377,9 +383,10 @@ async def async_save_to_store(self) -> None: for info_type, infos in self.info.items(): comp_type = INFO_TO_COMPONENT_TYPE[info_type] store_data[comp_type] = [info.to_dict() for info in infos.values()] # type: ignore[literal-required] - for service in self.services.values(): - store_data["services"].append(service.to_dict()) + store_data["services"] = [ + service.to_dict() for service in self.services.values() + ] if store_data == self._storage_contents: return diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 4741eaaa6fb42b..345be0c4b6d7f4 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from aioesphomeapi import ( APIClient, @@ -395,9 +395,7 @@ async def on_connect(self) -> None: ) ) - self.device_id = _async_setup_device_registry( - hass, entry, entry_data.device_info - ) + self.device_id = _async_setup_device_registry(hass, entry, entry_data) entry_data.async_update_device_state(hass) entity_infos, services = await cli.list_entities_services() @@ -515,9 +513,12 @@ async def async_start(self) -> None: @callback def _async_setup_device_registry( - hass: HomeAssistant, entry: ConfigEntry, device_info: EsphomeDeviceInfo + hass: HomeAssistant, entry: ConfigEntry, entry_data: RuntimeEntryData ) -> str: """Set up device registry feature for a particular config entry.""" + device_info = entry_data.device_info + if TYPE_CHECKING: + assert device_info is not None sw_version = device_info.esphome_version if device_info.compilation_time: sw_version += f" ({device_info.compilation_time})" @@ -544,7 +545,7 @@ def _async_setup_device_registry( config_entry_id=entry.entry_id, configuration_url=configuration_url, connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}, - name=device_info.friendly_name or device_info.name, + name=entry_data.friendly_name, manufacturer=manufacturer, model=model, sw_version=sw_version, diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index e809089da11a66..f373c2fdb17e3a 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -211,13 +211,13 @@ async def _mock_generic_device_entry( mock_device = MockESPHomeDevice(entry) - device_info = DeviceInfo( - name="test", - friendly_name="Test", - mac_address="11:22:33:44:55:aa", - esphome_version="1.0.0", - **mock_device_info, - ) + default_device_info = { + "name": "test", + "friendly_name": "Test", + "esphome_version": "1.0.0", + "mac_address": "11:22:33:44:55:aa", + } + device_info = DeviceInfo(**(default_device_info | mock_device_info)) async def _subscribe_states(callback: Callable[[EntityState], None]) -> None: """Subscribe to state.""" diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index e268d065e21c0e..e55d45832759bf 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -184,3 +184,38 @@ async def test_deep_sleep_device( state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_ON + + +async def test_esphome_device_without_friendly_name( + hass: HomeAssistant, + mock_client: APIClient, + hass_storage: dict[str, Any], + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test a device without friendly_name set.""" + entity_info = [ + BinarySensorInfo( + object_id="mybinary_sensor", + key=1, + name="my binary_sensor", + unique_id="my_binary_sensor", + ), + ] + states = [ + BinarySensorState(key=1, state=True, missing_state=False), + BinarySensorState(key=2, state=True, missing_state=False), + ] + user_service = [] + await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + device_info={"friendly_name": None}, + ) + state = hass.states.get("binary_sensor.test_mybinary_sensor") + assert state is not None + assert state.state == STATE_ON diff --git a/tests/components/esphome/test_sensor.py b/tests/components/esphome/test_sensor.py index 6c034e674ee783..83661a58280055 100644 --- a/tests/components/esphome/test_sensor.py +++ b/tests/components/esphome/test_sensor.py @@ -1,30 +1,45 @@ """Test ESPHome sensors.""" +from collections.abc import Awaitable, Callable +import logging import math from aioesphomeapi import ( APIClient, EntityCategory as ESPHomeEntityCategory, + EntityInfo, + EntityState, LastResetType, SensorInfo, SensorState, SensorStateClass as ESPHomeSensorStateClass, TextSensorInfo, TextSensorState, + UserService, ) from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass -from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, 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 +from .conftest import MockESPHomeDevice + async def test_generic_numeric_sensor( hass: HomeAssistant, mock_client: APIClient, - mock_generic_device_entry, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], ) -> None: """Test a generic sensor entity.""" + logging.getLogger("homeassistant.components.esphome").setLevel(logging.DEBUG) entity_info = [ SensorInfo( object_id="mysensor", @@ -35,7 +50,7 @@ async def test_generic_numeric_sensor( ] states = [SensorState(key=1, state=50)] user_service = [] - await mock_generic_device_entry( + mock_device = await mock_esphome_device( mock_client=mock_client, entity_info=entity_info, user_service=user_service, @@ -45,6 +60,34 @@ async def test_generic_numeric_sensor( assert state is not None assert state.state == "50" + # Test updating state + mock_device.set_state(SensorState(key=1, state=60)) + await hass.async_block_till_done() + state = hass.states.get("sensor.test_mysensor") + assert state is not None + assert state.state == "60" + + # Test sending the same state again + mock_device.set_state(SensorState(key=1, state=60)) + await hass.async_block_till_done() + state = hass.states.get("sensor.test_mysensor") + assert state is not None + assert state.state == "60" + + # Test we can still update after the same state + mock_device.set_state(SensorState(key=1, state=70)) + await hass.async_block_till_done() + state = hass.states.get("sensor.test_mysensor") + assert state is not None + assert state.state == "70" + + # Test invalid data from the underlying api does not crash us + mock_device.set_state(SensorState(key=1, state=object())) + await hass.async_block_till_done() + state = hass.states.get("sensor.test_mysensor") + assert state is not None + assert state.state == "70" + async def test_generic_numeric_sensor_with_entity_category_and_icon( hass: HomeAssistant, From 61532475f95fa404ffeba06d16e17ae78cd0e685 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Jul 2023 03:49:45 -0500 Subject: [PATCH 0796/1009] Cleanup sensor unit conversion code (#97074) --- homeassistant/components/sensor/__init__.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 4d76c803da627f..cbdaa24ec8348c 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -602,14 +602,11 @@ def state(self) -> Any: else: numerical_value = value - if ( - native_unit_of_measurement != unit_of_measurement - and device_class in UNIT_CONVERTERS + if native_unit_of_measurement != unit_of_measurement and ( + converter := UNIT_CONVERTERS.get(device_class) ): # Unit conversion needed - converter = UNIT_CONVERTERS[device_class] - - converted_numerical_value = UNIT_CONVERTERS[device_class].convert( + converted_numerical_value = converter.convert( float(numerical_value), native_unit_of_measurement, unit_of_measurement, From d4cdb0453fdc3559f5d0e714e200bcba2f6f699a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Jul 2023 03:54:25 -0500 Subject: [PATCH 0797/1009] Guard expensive debug formatting with calls with isEnabledFor (#97073) --- homeassistant/components/deconz/config_flow.py | 10 +++++++--- homeassistant/components/dlna_dmr/config_flow.py | 3 ++- homeassistant/components/dlna_dms/config_flow.py | 3 ++- homeassistant/components/onvif/config_flow.py | 11 +++++++---- tests/components/deconz/test_config_flow.py | 3 +++ tests/components/dlna_dmr/test_config_flow.py | 4 ++++ tests/components/dlna_dms/test_config_flow.py | 4 ++++ tests/components/onvif/test_config_flow.py | 5 +++++ 8 files changed, 34 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index acbf5089f96273..8eda93c2d4633f 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -4,6 +4,7 @@ import asyncio from collections.abc import Mapping +import logging from pprint import pformat from typing import Any, cast from urllib.parse import urlparse @@ -106,7 +107,8 @@ async def async_step_user( except (asyncio.TimeoutError, ResponseError): self.bridges = [] - LOGGER.debug("Discovered deCONZ gateways %s", pformat(self.bridges)) + if LOGGER.isEnabledFor(logging.DEBUG): + LOGGER.debug("Discovered deCONZ gateways %s", pformat(self.bridges)) if self.bridges: hosts = [] @@ -215,7 +217,8 @@ async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered deCONZ bridge.""" - LOGGER.debug("deCONZ SSDP discovery %s", pformat(discovery_info)) + if LOGGER.isEnabledFor(logging.DEBUG): + LOGGER.debug("deCONZ SSDP discovery %s", pformat(discovery_info)) self.bridge_id = normalize_bridge_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]) parsed_url = urlparse(discovery_info.ssdp_location) @@ -248,7 +251,8 @@ async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResu This flow is triggered by the discovery component. """ - LOGGER.debug("deCONZ HASSIO discovery %s", pformat(discovery_info.config)) + if LOGGER.isEnabledFor(logging.DEBUG): + LOGGER.debug("deCONZ HASSIO discovery %s", pformat(discovery_info.config)) self.bridge_id = normalize_bridge_id(discovery_info.config[CONF_SERIAL]) await self.async_set_unique_id(self.bridge_id) diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index bcd402e6a63e64..1ad29c72c269b6 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -126,7 +126,8 @@ async def async_step_manual(self, user_input: FlowInput = None) -> FlowResult: async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a flow initialized by SSDP discovery.""" - LOGGER.debug("async_step_ssdp: discovery_info %s", pformat(discovery_info)) + if LOGGER.isEnabledFor(logging.DEBUG): + LOGGER.debug("async_step_ssdp: discovery_info %s", pformat(discovery_info)) await self._async_set_info_from_discovery(discovery_info) diff --git a/homeassistant/components/dlna_dms/config_flow.py b/homeassistant/components/dlna_dms/config_flow.py index 8cb34be927f581..e147055df0530a 100644 --- a/homeassistant/components/dlna_dms/config_flow.py +++ b/homeassistant/components/dlna_dms/config_flow.py @@ -67,7 +67,8 @@ async def async_step_user( async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a flow initialized by SSDP discovery.""" - LOGGER.debug("async_step_ssdp: discovery_info %s", pformat(discovery_info)) + if LOGGER.isEnabledFor(logging.DEBUG): + LOGGER.debug("async_step_ssdp: discovery_info %s", pformat(discovery_info)) await self._async_parse_discovery(discovery_info) diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index 842fe4298cfc53..e0342c5f0d408c 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Mapping +import logging from pprint import pformat from typing import Any from urllib.parse import urlparse @@ -218,7 +219,8 @@ async def async_step_device(self, user_input=None): if not configured: self.devices.append(device) - LOGGER.debug("Discovered ONVIF devices %s", pformat(self.devices)) + if LOGGER.isEnabledFor(logging.DEBUG): + LOGGER.debug("Discovered ONVIF devices %s", pformat(self.devices)) if self.devices: devices = {CONF_MANUAL_INPUT: CONF_MANUAL_INPUT} @@ -274,9 +276,10 @@ async def async_setup_profiles( self, configure_unique_id: bool = True ) -> tuple[dict[str, str], dict[str, str]]: """Fetch ONVIF device profiles.""" - LOGGER.debug( - "Fetching profiles from ONVIF device %s", pformat(self.onvif_config) - ) + if LOGGER.isEnabledFor(logging.DEBUG): + LOGGER.debug( + "Fetching profiles from ONVIF device %s", pformat(self.onvif_config) + ) device = get_device( self.hass, diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index d0c51e39987ae0..1211d4dfa46e99 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for deCONZ config flow.""" import asyncio +import logging from unittest.mock import patch import pydeconz @@ -42,6 +43,7 @@ async def test_flow_discovered_bridges( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test that config flow works for discovered bridges.""" + logging.getLogger("homeassistant.components.deconz").setLevel(logging.DEBUG) aioclient_mock.get( pydeconz.utils.URL_DISCOVER, json=[ @@ -142,6 +144,7 @@ async def test_flow_manual_configuration( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test that config flow works with manual configuration after no discovered bridges.""" + logging.getLogger("homeassistant.components.deconz").setLevel(logging.DEBUG) aioclient_mock.get( pydeconz.utils.URL_DISCOVER, json=[], diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index c3251cd31a2cae..43e60638ba94a7 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -3,6 +3,7 @@ from collections.abc import Iterable import dataclasses +import logging from unittest.mock import Mock, patch from async_upnp_client.client import UpnpDevice @@ -286,6 +287,9 @@ async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) - async def test_ssdp_flow_success(hass: HomeAssistant) -> None: """Test that SSDP discovery with an available device works.""" + logging.getLogger("homeassistant.components.dlna_dmr.config_flow").setLevel( + logging.DEBUG + ) result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, diff --git a/tests/components/dlna_dms/test_config_flow.py b/tests/components/dlna_dms/test_config_flow.py index 1d6ac0eaf80262..c8c2998458f797 100644 --- a/tests/components/dlna_dms/test_config_flow.py +++ b/tests/components/dlna_dms/test_config_flow.py @@ -3,6 +3,7 @@ from collections.abc import Iterable import dataclasses +import logging from typing import Final from unittest.mock import Mock, patch @@ -125,6 +126,9 @@ async def test_user_flow_no_devices( async def test_ssdp_flow_success(hass: HomeAssistant) -> None: """Test that SSDP discovery with an available device works.""" + logging.getLogger("homeassistant.components.dlna_dms.config_flow").setLevel( + logging.DEBUG + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index 00fc77076e8408..6133a382855eaf 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -1,4 +1,5 @@ """Test ONVIF config flow.""" +import logging from unittest.mock import MagicMock, patch import pytest @@ -103,6 +104,7 @@ def setup_mock_discovery( async def test_flow_discovered_devices(hass: HomeAssistant) -> None: """Test that config flow works for discovered devices.""" + logging.getLogger("homeassistant.components.onvif").setLevel(logging.DEBUG) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -172,6 +174,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input( hass: HomeAssistant, ) -> None: """Test that config flow discovery ignores configured devices.""" + logging.getLogger("homeassistant.components.onvif").setLevel(logging.DEBUG) await setup_onvif_integration(hass) result = await hass.config_entries.flow.async_init( @@ -241,6 +244,7 @@ async def test_flow_discovered_no_device(hass: HomeAssistant) -> None: async def test_flow_discovery_ignore_existing_and_abort(hass: HomeAssistant) -> None: """Test that config flow discovery ignores setup devices.""" + logging.getLogger("homeassistant.components.onvif").setLevel(logging.DEBUG) await setup_onvif_integration(hass) await setup_onvif_integration( hass, @@ -298,6 +302,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass: HomeAssistant) -> async def test_flow_manual_entry(hass: HomeAssistant) -> None: """Test that config flow works for discovered devices.""" + logging.getLogger("homeassistant.components.onvif").setLevel(logging.DEBUG) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) From 2365e4c1592e569710ea226490529f8f21189195 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 23 Jul 2023 10:59:29 +0200 Subject: [PATCH 0798/1009] Disable Spotify controls when no active session (#96914) --- homeassistant/components/spotify/media_player.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 952e6c606c2cf3..de48e8fae20a4a 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -139,11 +139,11 @@ def __init__( @property def supported_features(self) -> MediaPlayerEntityFeature: """Return the supported features.""" - if self._restricted_device: + if self.data.current_user["product"] != "premium": + return MediaPlayerEntityFeature(0) + if self._restricted_device or not self._currently_playing: return MediaPlayerEntityFeature.SELECT_SOURCE - if self.data.current_user["product"] == "premium": - return SUPPORT_SPOTIFY - return MediaPlayerEntityFeature(0) + return SUPPORT_SPOTIFY @property def state(self) -> MediaPlayerState: From 35f21dcf9c3bbe7ef43d2a13fbea09089bd62a70 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Sun, 23 Jul 2023 10:10:18 +0100 Subject: [PATCH 0799/1009] Add repair hint to deprecate generic camera yaml config (#96923) --- homeassistant/components/generic/camera.py | 6 ----- .../components/generic/config_flow.py | 25 ++++++++++++++++++- tests/components/generic/test_config_flow.py | 11 ++++++-- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 234795e9014c31..c171c95e659fb8 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -80,12 +80,6 @@ async def async_setup_platform( ) -> None: """Set up a generic IP Camera.""" - _LOGGER.warning( - "Loading generic IP camera via configuration.yaml is deprecated, " - "it will be automatically imported. Once you have confirmed correct " - "operation, please remove 'generic' (IP camera) section(s) from " - "configuration.yaml" - ) image = config.get(CONF_STILL_IMAGE_URL) stream = config.get(CONF_STREAM_SOURCE) config_new = { diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 34fc571327111c..ec94d4c227c152 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -40,11 +40,12 @@ HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import FlowResult, UnknownFlow from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template as template_helper from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.util import slugify from .camera import GenericCamera, generate_auth @@ -380,6 +381,28 @@ async def async_step_user_confirm_still( async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: """Handle config import from yaml.""" + + _LOGGER.warning( + "Loading generic IP camera via configuration.yaml is deprecated, " + "it will be automatically imported. Once you have confirmed correct " + "operation, please remove 'generic' (IP camera) section(s) from " + "configuration.yaml" + ) + + async_create_issue( + self.hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Generic IP Camera", + }, + ) # abort if we've already got this one. if self.check_for_existing(import_config): return self.async_abort(reason="already_exists") diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index e7668bdc3ffe26..54a9c5c0796368 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -34,8 +34,8 @@ CONF_VERIFY_SSL, HTTP_BASIC_AUTHENTICATION, ) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import entity_registry as er, issue_registry as ir from tests.common import MockConfigEntry from tests.typing import ClientSessionGenerator @@ -769,6 +769,13 @@ async def test_import(hass: HomeAssistant, fakeimg_png) -> None: assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Yaml Defined Name" await hass.async_block_till_done() + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, "deprecated_yaml_generic" + ) + assert issue.translation_key == "deprecated_yaml" + # Any name defined in yaml should end up as the entity id. assert hass.states.get("camera.yaml_defined_name") assert result2["type"] == data_entry_flow.FlowResultType.ABORT From 672313c8abe1dcbb8ff20dcb4065feb29d9d8530 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Sun, 23 Jul 2023 13:11:05 +0200 Subject: [PATCH 0800/1009] Add support for MiScale V1 (#97081) --- .../components/xiaomi_ble/manifest.json | 6 +- homeassistant/generated/bluetooth.py | 5 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/xiaomi_ble/__init__.py | 18 +++++- tests/components/xiaomi_ble/test_sensor.py | 62 ++++++++++++++++--- 6 files changed, 81 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 683a5dab9dd848..e2b327c6823eb5 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -6,6 +6,10 @@ "connectable": false, "service_data_uuid": "0000181b-0000-1000-8000-00805f9b34fb" }, + { + "connectable": false, + "service_data_uuid": "0000181d-0000-1000-8000-00805f9b34fb" + }, { "connectable": false, "service_data_uuid": "0000fd50-0000-1000-8000-00805f9b34fb" @@ -20,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "iot_class": "local_push", - "requirements": ["xiaomi-ble==0.19.1"] + "requirements": ["xiaomi-ble==0.20.0"] } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index b99b621c614240..7b0aa78d69e06f 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -525,6 +525,11 @@ "domain": "xiaomi_ble", "service_data_uuid": "0000181b-0000-1000-8000-00805f9b34fb", }, + { + "connectable": False, + "domain": "xiaomi_ble", + "service_data_uuid": "0000181d-0000-1000-8000-00805f9b34fb", + }, { "connectable": False, "domain": "xiaomi_ble", diff --git a/requirements_all.txt b/requirements_all.txt index e31b5a32d7003d..0429a807259bef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2687,7 +2687,7 @@ wyoming==1.1.0 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.19.1 +xiaomi-ble==0.20.0 # homeassistant.components.knx xknx==2.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00f14b65a1303d..e07523e1892ca0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1969,7 +1969,7 @@ wyoming==1.1.0 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.19.1 +xiaomi-ble==0.20.0 # homeassistant.components.knx xknx==2.11.1 diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py index 879ab4f7bc4aa2..197745b70f14ec 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -105,6 +105,22 @@ connectable=False, ) +MISCALE_V1_SERVICE_INFO = BluetoothServiceInfoBleak( + name="MISCA", + address="50:FB:19:1B:B5:DC", + device=generate_ble_device("00:00:00:00:00:00", None), + rssi=-60, + manufacturer_data={}, + service_data={ + "0000181d-0000-1000-8000-00805f9b34fb": b"\x22\x9e\x43\xe5\x07\x04\x0b\x10\x13\x01" + }, + service_uuids=["0000181d-0000-1000-8000-00805f9b34fb"], + source="local", + advertisement=generate_advertisement_data(local_name="Not it"), + time=0, + connectable=False, +) + MISCALE_V2_SERVICE_INFO = BluetoothServiceInfoBleak( name="MIBFS", address="50:FB:19:1B:B5:DC", @@ -112,7 +128,7 @@ rssi=-60, manufacturer_data={}, service_data={ - "0000181b-0000-1000-8000-00805f9b34fb": b"\x02\xa6\xe7\x07\x07\x07\x0b\x1f\x1d\x1f\x02\xfa-" + "0000181b-0000-1000-8000-00805f9b34fb": b"\x02&\xb2\x07\x05\x04\x0f\x02\x01\xac\x01\x86B" }, service_uuids=["0000181b-0000-1000-8000-00805f9b34fb"], source="local", diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 40d89a8214d2af..fff8d9b20f11fb 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -6,6 +6,7 @@ from . import ( HHCCJCY10_SERVICE_INFO, + MISCALE_V1_SERVICE_INFO, MISCALE_V2_SERVICE_INFO, MMC_T201_1_SERVICE_INFO, make_advertisement, @@ -513,6 +514,48 @@ async def test_hhccjcy10_uuid(hass: HomeAssistant) -> None: await hass.async_block_till_done() +async def test_miscale_v1_uuid(hass: HomeAssistant) -> None: + """Test MiScale V1 UUID. + + This device uses a different UUID compared to the other Xiaomi sensors. + """ + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="50:FB:19:1B:B5:DC", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + inject_bluetooth_service_info_bleak(hass, MISCALE_V1_SERVICE_INFO) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + mass_non_stabilized_sensor = hass.states.get( + "sensor.mi_smart_scale_b5dc_mass_non_stabilized" + ) + mass_non_stabilized_sensor_attr = mass_non_stabilized_sensor.attributes + assert mass_non_stabilized_sensor.state == "86.55" + assert ( + mass_non_stabilized_sensor_attr[ATTR_FRIENDLY_NAME] + == "Mi Smart Scale (B5DC) Mass Non Stabilized" + ) + assert mass_non_stabilized_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "kg" + assert mass_non_stabilized_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + mass_sensor = hass.states.get("sensor.mi_smart_scale_b5dc_mass") + mass_sensor_attr = mass_sensor.attributes + assert mass_sensor.state == "86.55" + assert mass_sensor_attr[ATTR_FRIENDLY_NAME] == "Mi Smart Scale (B5DC) Mass" + assert mass_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "kg" + assert mass_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + async def test_miscale_v2_uuid(hass: HomeAssistant) -> None: """Test MiScale V2 UUID. @@ -533,35 +576,34 @@ async def test_miscale_v2_uuid(hass: HomeAssistant) -> None: assert len(hass.states.async_all()) == 3 mass_non_stabilized_sensor = hass.states.get( - "sensor.mi_body_composition_scale_2_b5dc_mass_non_stabilized" + "sensor.mi_body_composition_scale_b5dc_mass_non_stabilized" ) mass_non_stabilized_sensor_attr = mass_non_stabilized_sensor.attributes - assert mass_non_stabilized_sensor.state == "58.85" + assert mass_non_stabilized_sensor.state == "85.15" assert ( mass_non_stabilized_sensor_attr[ATTR_FRIENDLY_NAME] - == "Mi Body Composition Scale 2 (B5DC) Mass Non Stabilized" + == "Mi Body Composition Scale (B5DC) Mass Non Stabilized" ) assert mass_non_stabilized_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "kg" assert mass_non_stabilized_sensor_attr[ATTR_STATE_CLASS] == "measurement" - mass_sensor = hass.states.get("sensor.mi_body_composition_scale_2_b5dc_mass") + mass_sensor = hass.states.get("sensor.mi_body_composition_scale_b5dc_mass") mass_sensor_attr = mass_sensor.attributes - assert mass_sensor.state == "58.85" + assert mass_sensor.state == "85.15" assert ( - mass_sensor_attr[ATTR_FRIENDLY_NAME] - == "Mi Body Composition Scale 2 (B5DC) Mass" + mass_sensor_attr[ATTR_FRIENDLY_NAME] == "Mi Body Composition Scale (B5DC) Mass" ) assert mass_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "kg" assert mass_sensor_attr[ATTR_STATE_CLASS] == "measurement" impedance_sensor = hass.states.get( - "sensor.mi_body_composition_scale_2_b5dc_impedance" + "sensor.mi_body_composition_scale_b5dc_impedance" ) impedance_sensor_attr = impedance_sensor.attributes - assert impedance_sensor.state == "543" + assert impedance_sensor.state == "428" assert ( impedance_sensor_attr[ATTR_FRIENDLY_NAME] - == "Mi Body Composition Scale 2 (B5DC) Impedance" + == "Mi Body Composition Scale (B5DC) Impedance" ) assert impedance_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "ohm" assert impedance_sensor_attr[ATTR_STATE_CLASS] == "measurement" From 33f2453f334a170fe874cb005cb9f4bac2525862 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 23 Jul 2023 14:01:27 +0200 Subject: [PATCH 0801/1009] Add entity translations for ld2410 BLE (#95709) --- .../components/ld2410_ble/binary_sensor.py | 6 +- homeassistant/components/ld2410_ble/sensor.py | 29 +++---- .../components/ld2410_ble/strings.json | 79 +++++++++++++++++++ 3 files changed, 92 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/ld2410_ble/binary_sensor.py b/homeassistant/components/ld2410_ble/binary_sensor.py index ab3c8ddea0bd13..59580d5725ed31 100644 --- a/homeassistant/components/ld2410_ble/binary_sensor.py +++ b/homeassistant/components/ld2410_ble/binary_sensor.py @@ -21,14 +21,10 @@ BinarySensorEntityDescription( key="is_moving", device_class=BinarySensorDeviceClass.MOTION, - has_entity_name=True, - name="Motion", ), BinarySensorEntityDescription( key="is_static", device_class=BinarySensorDeviceClass.OCCUPANCY, - has_entity_name=True, - name="Occupancy", ), ) @@ -51,6 +47,8 @@ class LD2410BLEBinarySensor( ): """Moving/static sensor for LD2410BLE.""" + _attr_has_entity_name = True + def __init__( self, coordinator: LD2410BLECoordinator, diff --git a/homeassistant/components/ld2410_ble/sensor.py b/homeassistant/components/ld2410_ble/sensor.py index 6d0e8e4feb9278..806832e9fca9fc 100644 --- a/homeassistant/components/ld2410_ble/sensor.py +++ b/homeassistant/components/ld2410_ble/sensor.py @@ -21,84 +21,76 @@ MOVING_TARGET_DISTANCE_DESCRIPTION = SensorEntityDescription( key="moving_target_distance", + translation_key="moving_target_distance", device_class=SensorDeviceClass.DISTANCE, entity_registry_enabled_default=False, entity_registry_visible_default=True, - has_entity_name=True, - name="Moving Target Distance", native_unit_of_measurement=UnitOfLength.CENTIMETERS, state_class=SensorStateClass.MEASUREMENT, ) STATIC_TARGET_DISTANCE_DESCRIPTION = SensorEntityDescription( key="static_target_distance", + translation_key="static_target_distance", device_class=SensorDeviceClass.DISTANCE, entity_registry_enabled_default=False, entity_registry_visible_default=True, - has_entity_name=True, - name="Static Target Distance", native_unit_of_measurement=UnitOfLength.CENTIMETERS, state_class=SensorStateClass.MEASUREMENT, ) DETECTION_DISTANCE_DESCRIPTION = SensorEntityDescription( key="detection_distance", + translation_key="detection_distance", device_class=SensorDeviceClass.DISTANCE, entity_registry_enabled_default=False, entity_registry_visible_default=True, - has_entity_name=True, - name="Detection Distance", native_unit_of_measurement=UnitOfLength.CENTIMETERS, state_class=SensorStateClass.MEASUREMENT, ) MOVING_TARGET_ENERGY_DESCRIPTION = SensorEntityDescription( key="moving_target_energy", + translation_key="moving_target_energy", device_class=None, entity_registry_enabled_default=False, entity_registry_visible_default=True, - has_entity_name=True, - name="Moving Target Energy", native_unit_of_measurement="Target Energy", state_class=SensorStateClass.MEASUREMENT, ) STATIC_TARGET_ENERGY_DESCRIPTION = SensorEntityDescription( key="static_target_energy", + translation_key="static_target_energy", device_class=None, entity_registry_enabled_default=False, entity_registry_visible_default=True, - has_entity_name=True, - name="Static Target Energy", native_unit_of_measurement="Target Energy", state_class=SensorStateClass.MEASUREMENT, ) MAX_MOTION_GATES_DESCRIPTION = SensorEntityDescription( key="max_motion_gates", + translation_key="max_motion_gates", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - has_entity_name=True, - name="Max Motion Gates", native_unit_of_measurement="Gates", ) MAX_STATIC_GATES_DESCRIPTION = SensorEntityDescription( key="max_static_gates", + translation_key="max_static_gates", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - has_entity_name=True, - name="Max Static Gates", native_unit_of_measurement="Gates", ) MOTION_ENERGY_GATES = [ SensorEntityDescription( key=f"motion_energy_gate_{i}", + translation_key=f"motion_energy_gate_{i}", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - has_entity_name=True, - name=f"Motion Energy Gate {i}", native_unit_of_measurement="Target Energy", ) for i in range(0, 9) @@ -107,10 +99,9 @@ STATIC_ENERGY_GATES = [ SensorEntityDescription( key=f"static_energy_gate_{i}", + translation_key=f"static_energy_gate_{i}", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - has_entity_name=True, - name=f"Static Energy Gate {i}", native_unit_of_measurement="Target Energy", ) for i in range(0, 9) @@ -152,6 +143,8 @@ async def async_setup_entry( class LD2410BLESensor(CoordinatorEntity[LD2410BLECoordinator], SensorEntity): """Generic sensor for LD2410BLE.""" + _attr_has_entity_name = True + def __init__( self, coordinator: LD2410BLECoordinator, diff --git a/homeassistant/components/ld2410_ble/strings.json b/homeassistant/components/ld2410_ble/strings.json index e2be7e6deff147..7e919675426b8c 100644 --- a/homeassistant/components/ld2410_ble/strings.json +++ b/homeassistant/components/ld2410_ble/strings.json @@ -18,5 +18,84 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } + }, + "entity": { + "sensor": { + "moving_target_distance": { + "name": "Moving target distance" + }, + "static_target_distance": { + "name": "Static target distance" + }, + "detection_distance": { + "name": "Detection distance" + }, + "moving_target_energy": { + "name": "Moving target energy" + }, + "static_target_energy": { + "name": "Static target energy" + }, + "max_motion_gates": { + "name": "Max motion gates" + }, + "max_static_gates": { + "name": "Max static gates" + }, + "motion_energy_gate_0": { + "name": "Motion energy gate 0" + }, + "motion_energy_gate_1": { + "name": "Motion energy gate 1" + }, + "motion_energy_gate_2": { + "name": "Motion energy gate 2" + }, + "motion_energy_gate_3": { + "name": "Motion energy gate 3" + }, + "motion_energy_gate_4": { + "name": "Motion energy gate 4" + }, + "motion_energy_gate_5": { + "name": "Motion energy gate 5" + }, + "motion_energy_gate_6": { + "name": "Motion energy gate 6" + }, + "motion_energy_gate_7": { + "name": "Motion energy gate 7" + }, + "motion_energy_gate_8": { + "name": "Motion energy gate 8" + }, + "static_energy_gate_0": { + "name": "Static energy gate 0" + }, + "static_energy_gate_1": { + "name": "Static energy gate 1" + }, + "static_energy_gate_2": { + "name": "Static energy gate 2" + }, + "static_energy_gate_3": { + "name": "Static energy gate 3" + }, + "static_energy_gate_4": { + "name": "Static energy gate 4" + }, + "static_energy_gate_5": { + "name": "Static energy gate 5" + }, + "static_energy_gate_6": { + "name": "Static energy gate 6" + }, + "static_energy_gate_7": { + "name": "Static energy gate 7" + }, + "static_energy_gate_8": { + "name": "Static energy gate 8" + } + } } } From 995c4d8ac1589138a143810405353493c38d95a2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 23 Jul 2023 14:20:57 +0200 Subject: [PATCH 0802/1009] Add missing translations for power binary sensor device class (#97084) --- homeassistant/components/binary_sensor/strings.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index 185482d62e3e43..b86c013f104e8c 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -237,6 +237,13 @@ "on": "Plugged in" } }, + "power": { + "name": "Power", + "state": { + "off": "[%key:component::binary_sensor::entity_component::gas::state::off%]", + "on": "[%key:component::binary_sensor::entity_component::gas::state::on%]" + } + }, "presence": { "name": "Presence", "state": { From 26152adb234f502fabff9a043b66c8d85ad1c932 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 23 Jul 2023 14:32:25 +0200 Subject: [PATCH 0803/1009] Add entity translations to Tado (#96226) --- .../components/tado/binary_sensor.py | 14 ++----- homeassistant/components/tado/climate.py | 2 +- homeassistant/components/tado/entity.py | 3 ++ homeassistant/components/tado/sensor.py | 23 +++++------- homeassistant/components/tado/strings.json | 37 +++++++++++++++++++ homeassistant/components/tado/water_heater.py | 7 +--- tests/components/tado/test_binary_sensor.py | 10 ++--- 7 files changed, 60 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py index 24d62d76026980..c5222112c0281a 100644 --- a/homeassistant/components/tado/binary_sensor.py +++ b/homeassistant/components/tado/binary_sensor.py @@ -50,31 +50,28 @@ class TadoBinarySensorEntityDescription( BATTERY_STATE_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription( key="battery state", - name="Battery state", state_fn=lambda data: data["batteryState"] == "LOW", device_class=BinarySensorDeviceClass.BATTERY, ) CONNECTION_STATE_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription( key="connection state", - name="Connection state", + translation_key="connection_state", state_fn=lambda data: data.get("connectionState", {}).get("value", False), device_class=BinarySensorDeviceClass.CONNECTIVITY, ) POWER_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription( key="power", - name="Power", state_fn=lambda data: data.power == "ON", device_class=BinarySensorDeviceClass.POWER, ) LINK_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription( key="link", - name="Link", state_fn=lambda data: data.link == "ONLINE", device_class=BinarySensorDeviceClass.CONNECTIVITY, ) OVERLAY_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription( key="overlay", - name="Overlay", + translation_key="overlay", state_fn=lambda data: data.overlay_active, attributes_fn=lambda data: {"termination": data.overlay_termination_type} if data.overlay_active @@ -83,14 +80,13 @@ class TadoBinarySensorEntityDescription( ) OPEN_WINDOW_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription( key="open window", - name="Open window", state_fn=lambda data: bool(data.open_window or data.open_window_detected), attributes_fn=lambda data: data.open_window_attr, device_class=BinarySensorDeviceClass.WINDOW, ) EARLY_START_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription( key="early start", - name="Early start", + translation_key="early_start", state_fn=lambda data: data.preparation, device_class=BinarySensorDeviceClass.POWER, ) @@ -173,8 +169,6 @@ class TadoDeviceBinarySensor(TadoDeviceEntity, BinarySensorEntity): entity_description: TadoBinarySensorEntityDescription - _attr_has_entity_name = True - def __init__( self, tado, device_info, entity_description: TadoBinarySensorEntityDescription ) -> None: @@ -227,8 +221,6 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity): entity_description: TadoBinarySensorEntityDescription - _attr_has_entity_name = True - def __init__( self, tado, diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 2b8bc4060d67f3..36a2ab671c9163 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -218,6 +218,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): """Representation of a Tado climate entity.""" _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_name = None def __init__( self, @@ -244,7 +245,6 @@ def __init__( self.zone_type = zone_type self._attr_unique_id = f"{zone_type} {zone_id} {tado.home_id}" - self._attr_name = zone_name self._attr_temperature_unit = UnitOfTemperature.CELSIUS self._attr_translation_key = DOMAIN diff --git a/homeassistant/components/tado/entity.py b/homeassistant/components/tado/entity.py index c825bafc4b95cd..5e3065bfb53959 100644 --- a/homeassistant/components/tado/entity.py +++ b/homeassistant/components/tado/entity.py @@ -8,6 +8,7 @@ class TadoDeviceEntity(Entity): """Base implementation for Tado device.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__(self, device_info): """Initialize a Tado device.""" @@ -34,6 +35,7 @@ class TadoHomeEntity(Entity): """Base implementation for Tado home.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__(self, tado): """Initialize a Tado home.""" @@ -56,6 +58,7 @@ def device_info(self) -> DeviceInfo: class TadoZoneEntity(Entity): """Base implementation for Tado zone.""" + _attr_has_entity_name = True _attr_should_poll = False def __init__(self, zone_name, home_id, zone_id): diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 7742f6b0dca3cd..f7ba1682e18578 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -55,7 +55,7 @@ class TadoSensorEntityDescription( HOME_SENSORS = [ TadoSensorEntityDescription( key="outdoor temperature", - name="Outdoor temperature", + translation_key="outdoor_temperature", state_fn=lambda data: data["outsideTemperature"]["celsius"], attributes_fn=lambda data: { "time": data["outsideTemperature"]["timestamp"], @@ -67,7 +67,7 @@ class TadoSensorEntityDescription( ), TadoSensorEntityDescription( key="solar percentage", - name="Solar percentage", + translation_key="solar_percentage", state_fn=lambda data: data["solarIntensity"]["percentage"], attributes_fn=lambda data: { "time": data["solarIntensity"]["timestamp"], @@ -78,28 +78,28 @@ class TadoSensorEntityDescription( ), TadoSensorEntityDescription( key="weather condition", - name="Weather condition", + translation_key="weather_condition", state_fn=lambda data: format_condition(data["weatherState"]["value"]), attributes_fn=lambda data: {"time": data["weatherState"]["timestamp"]}, data_category=SENSOR_DATA_CATEGORY_WEATHER, ), TadoSensorEntityDescription( key="tado mode", - name="Tado mode", + translation_key="tado_mode", # pylint: disable=unnecessary-lambda state_fn=lambda data: get_tado_mode(data), data_category=SENSOR_DATA_CATEGORY_GEOFENCE, ), TadoSensorEntityDescription( key="geofencing mode", - name="Geofencing mode", + translation_key="geofencing_mode", # pylint: disable=unnecessary-lambda state_fn=lambda data: get_geofencing_mode(data), data_category=SENSOR_DATA_CATEGORY_GEOFENCE, ), TadoSensorEntityDescription( key="automatic geofencing", - name="Automatic geofencing", + translation_key="automatic_geofencing", # pylint: disable=unnecessary-lambda state_fn=lambda data: get_automatic_geofencing(data), data_category=SENSOR_DATA_CATEGORY_GEOFENCE, @@ -108,7 +108,6 @@ class TadoSensorEntityDescription( TEMPERATURE_ENTITY_DESCRIPTION = TadoSensorEntityDescription( key="temperature", - name="Temperature", state_fn=lambda data: data.current_temp, attributes_fn=lambda data: { "time": data.current_temp_timestamp, @@ -120,7 +119,6 @@ class TadoSensorEntityDescription( ) HUMIDITY_ENTITY_DESCRIPTION = TadoSensorEntityDescription( key="humidity", - name="Humidity", state_fn=lambda data: data.current_humidity, attributes_fn=lambda data: {"time": data.current_humidity_timestamp}, native_unit_of_measurement=PERCENTAGE, @@ -129,12 +127,12 @@ class TadoSensorEntityDescription( ) TADO_MODE_ENTITY_DESCRIPTION = TadoSensorEntityDescription( key="tado mode", - name="Tado mode", + translation_key="tado_mode", state_fn=lambda data: data.tado_mode, ) HEATING_ENTITY_DESCRIPTION = TadoSensorEntityDescription( key="heating", - name="Heating", + translation_key="heating", state_fn=lambda data: data.heating_power_percentage, attributes_fn=lambda data: {"time": data.heating_power_timestamp}, native_unit_of_measurement=PERCENTAGE, @@ -142,6 +140,7 @@ class TadoSensorEntityDescription( ) AC_ENTITY_DESCRIPTION = TadoSensorEntityDescription( key="ac", + translation_key="ac", name="AC", state_fn=lambda data: data.ac_power, attributes_fn=lambda data: {"time": data.ac_power_timestamp}, @@ -244,8 +243,6 @@ class TadoHomeSensor(TadoHomeEntity, SensorEntity): entity_description: TadoSensorEntityDescription - _attr_has_entity_name = True - def __init__(self, tado, entity_description: TadoSensorEntityDescription) -> None: """Initialize of the Tado Sensor.""" self.entity_description = entity_description @@ -298,8 +295,6 @@ class TadoZoneSensor(TadoZoneEntity, SensorEntity): entity_description: TadoSensorEntityDescription - _attr_has_entity_name = True - def __init__( self, tado, diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index 70ff38b10be719..9858b7aa51b629 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -31,6 +31,17 @@ } }, "entity": { + "binary_sensor": { + "connection_state": { + "name": "Connection state" + }, + "overlay": { + "name": "Overlay" + }, + "early_start": { + "name": "Early start" + } + }, "climate": { "tado": { "state_attributes": { @@ -41,6 +52,32 @@ } } } + }, + "sensor": { + "outdoor_temperature": { + "name": "Outdoor temperature" + }, + "solar_percentage": { + "name": "Solar percentage" + }, + "weather_condition": { + "name": "Weather condition" + }, + "tado_mode": { + "name": "Tado mode" + }, + "geofencing_mode": { + "name": "Geofencing mode" + }, + "automatic_geofencing": { + "name": "Automatic geofencing" + }, + "heating": { + "name": "Heating" + }, + "ac": { + "name": "AC" + } } }, "services": { diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index f7a1dcd0966799..6d17c85c9811e5 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -119,6 +119,8 @@ def create_water_heater_entity(tado, name: str, zone_id: int, zone: str): class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): """Representation of a Tado water heater.""" + _attr_name = None + def __init__( self, tado, @@ -166,11 +168,6 @@ async def async_added_to_hass(self) -> None: ) self._async_update_data() - @property - def name(self): - """Return the name of the entity.""" - return self.zone_name - @property def unique_id(self): """Return the unique id.""" diff --git a/tests/components/tado/test_binary_sensor.py b/tests/components/tado/test_binary_sensor.py index 9226543abef7f4..1e2f53efeb51a8 100644 --- a/tests/components/tado/test_binary_sensor.py +++ b/tests/components/tado/test_binary_sensor.py @@ -13,13 +13,13 @@ async def test_air_con_create_binary_sensors(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.air_conditioning_power") assert state.state == STATE_ON - state = hass.states.get("binary_sensor.air_conditioning_link") + state = hass.states.get("binary_sensor.air_conditioning_connectivity") assert state.state == STATE_ON state = hass.states.get("binary_sensor.air_conditioning_overlay") assert state.state == STATE_ON - state = hass.states.get("binary_sensor.air_conditioning_open_window") + state = hass.states.get("binary_sensor.air_conditioning_window") assert state.state == STATE_OFF @@ -31,7 +31,7 @@ async def test_heater_create_binary_sensors(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.baseboard_heater_power") assert state.state == STATE_ON - state = hass.states.get("binary_sensor.baseboard_heater_link") + state = hass.states.get("binary_sensor.baseboard_heater_connectivity") assert state.state == STATE_ON state = hass.states.get("binary_sensor.baseboard_heater_early_start") @@ -40,7 +40,7 @@ async def test_heater_create_binary_sensors(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.baseboard_heater_overlay") assert state.state == STATE_ON - state = hass.states.get("binary_sensor.baseboard_heater_open_window") + state = hass.states.get("binary_sensor.baseboard_heater_window") assert state.state == STATE_OFF @@ -49,7 +49,7 @@ async def test_water_heater_create_binary_sensors(hass: HomeAssistant) -> None: await async_init_integration(hass) - state = hass.states.get("binary_sensor.water_heater_link") + state = hass.states.get("binary_sensor.water_heater_connectivity") assert state.state == STATE_ON state = hass.states.get("binary_sensor.water_heater_overlay") From 1b8e03bb6627ef1b9a23574cbfd65fdba1b56479 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 23 Jul 2023 14:42:14 +0200 Subject: [PATCH 0804/1009] Add MQTT event entity platform (#96876) Co-authored-by: Franck Nijhof --- homeassistant/components/mqtt/__init__.py | 4 +- .../components/mqtt/abbreviations.py | 1 + .../components/mqtt/config_integration.py | 5 + homeassistant/components/mqtt/const.py | 2 + homeassistant/components/mqtt/discovery.py | 1 + homeassistant/components/mqtt/event.py | 221 ++++++ tests/components/mqtt/test_event.py | 673 ++++++++++++++++++ 7 files changed, 905 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/mqtt/event.py create mode 100644 tests/components/mqtt/test_event.py diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 405eb86e6ec255..9ec6447b32cd1d 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -25,7 +25,7 @@ ) from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import TemplateError, Unauthorized -from homeassistant.helpers import config_validation as cv, event, template +from homeassistant.helpers import config_validation as cv, event as ev, template from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import async_get_platforms @@ -340,7 +340,7 @@ async def finish_dump(_: datetime) -> None: unsub() await hass.async_add_executor_job(write_dump) - event.async_call_later(hass, call.data["duration"], finish_dump) + ev.async_call_later(hass, call.data["duration"], finish_dump) hass.services.async_register( DOMAIN, diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index a5360090bb9e9d..cc0f37ea145bc6 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -60,6 +60,7 @@ "ent_pic": "entity_picture", "err_t": "error_topic", "err_tpl": "error_template", + "evt_typ": "event_types", "fanspd_t": "fan_speed_topic", "fanspd_tpl": "fan_speed_template", "fanspd_lst": "fan_speed_list", diff --git a/homeassistant/components/mqtt/config_integration.py b/homeassistant/components/mqtt/config_integration.py index ef2c771218ada7..cd4470ef22d197 100644 --- a/homeassistant/components/mqtt/config_integration.py +++ b/homeassistant/components/mqtt/config_integration.py @@ -22,6 +22,7 @@ climate as climate_platform, cover as cover_platform, device_tracker as device_tracker_platform, + event as event_platform, fan as fan_platform, humidifier as humidifier_platform, image as image_platform, @@ -82,6 +83,10 @@ cv.ensure_list, [device_tracker_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), + Platform.EVENT.value: vol.All( + cv.ensure_list, + [event_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] + ), Platform.FAN.value: vol.All( cv.ensure_list, [fan_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index d09a2bb8cb6e17..fb1989069af1df 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -112,6 +112,7 @@ Platform.CAMERA, Platform.CLIMATE, Platform.DEVICE_TRACKER, + Platform.EVENT, Platform.COVER, Platform.FAN, Platform.HUMIDIFIER, @@ -138,6 +139,7 @@ Platform.CLIMATE, Platform.COVER, Platform.DEVICE_TRACKER, + Platform.EVENT, Platform.FAN, Platform.HUMIDIFIER, Platform.IMAGE, diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 70e5ac9e5356b8..8e563a48cdd3cd 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -52,6 +52,7 @@ "cover", "device_automation", "device_tracker", + "event", "fan", "humidifier", "image", diff --git a/homeassistant/components/mqtt/event.py b/homeassistant/components/mqtt/event.py new file mode 100644 index 00000000000000..5a94ec754c0482 --- /dev/null +++ b/homeassistant/components/mqtt/event.py @@ -0,0 +1,221 @@ +"""Support for MQTT events.""" +from __future__ import annotations + +from collections.abc import Callable +import functools +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant.components import event +from homeassistant.components.event import ( + ENTITY_ID_FORMAT, + EventDeviceClass, + EventEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_NAME, + CONF_VALUE_TEMPLATE, +) +from homeassistant.core import HomeAssistant, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads_object + +from . import subscription +from .config import MQTT_RO_SCHEMA +from .const import ( + CONF_ENCODING, + CONF_QOS, + CONF_STATE_TOPIC, + PAYLOAD_EMPTY_JSON, + PAYLOAD_NONE, +) +from .debug_info import log_messages +from .mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_setup_entry_helper, +) +from .models import ( + MqttValueTemplate, + PayloadSentinel, + ReceiveMessage, + ReceivePayloadType, +) +from .util import get_mqtt_data + +_LOGGER = logging.getLogger(__name__) + +CONF_EVENT_TYPES = "event_types" + +MQTT_EVENT_ATTRIBUTES_BLOCKED = frozenset( + { + event.ATTR_EVENT_TYPE, + event.ATTR_EVENT_TYPES, + } +) + +DEFAULT_NAME = "MQTT Event" +DEFAULT_FORCE_UPDATE = False +DEVICE_CLASS_SCHEMA = vol.All(vol.Lower, vol.Coerce(EventDeviceClass)) + +_PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend( + { + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASS_SCHEMA, + vol.Optional(CONF_NAME): vol.Any(None, cv.string), + vol.Required(CONF_EVENT_TYPES): vol.All(cv.ensure_list, [cv.string]), + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + +PLATFORM_SCHEMA_MODERN = vol.All( + _PLATFORM_SCHEMA_BASE, +) + +DISCOVERY_SCHEMA = vol.All( + _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up MQTT event through YAML and through MQTT discovery.""" + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry + ) + await async_setup_entry_helper(hass, event.DOMAIN, setup, DISCOVERY_SCHEMA) + + +async def _async_setup_entity( + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, +) -> None: + """Set up MQTT event.""" + async_add_entities([MqttEvent(hass, config, config_entry, discovery_data)]) + + +class MqttEvent(MqttEntity, EventEntity): + """Representation of an event that can be updated using MQTT.""" + + _default_name = DEFAULT_NAME + _entity_id_format = ENTITY_ID_FORMAT + _attributes_extra_blocked = MQTT_EVENT_ATTRIBUTES_BLOCKED + _template: Callable[[ReceivePayloadType, PayloadSentinel], ReceivePayloadType] + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: + """Initialize the sensor.""" + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) + + @staticmethod + def config_schema() -> vol.Schema: + """Return the config schema.""" + return DISCOVERY_SCHEMA + + def _setup_from_config(self, config: ConfigType) -> None: + """(Re)Setup the entity.""" + self._attr_device_class = config.get(CONF_DEVICE_CLASS) + self._attr_event_types = config[CONF_EVENT_TYPES] + self._template = MqttValueTemplate( + self._config.get(CONF_VALUE_TEMPLATE), entity=self + ).async_render_with_possible_json_value + + def _prepare_subscribe_topics(self) -> None: + """(Re)Subscribe to topics.""" + topics: dict[str, dict[str, Any]] = {} + + @callback + @log_messages(self.hass, self.entity_id) + def message_received(msg: ReceiveMessage) -> None: + """Handle new MQTT messages.""" + event_attributes: dict[str, Any] = {} + event_type: str + payload = self._template(msg.payload, PayloadSentinel.DEFAULT) + if ( + not payload + or payload is PayloadSentinel.DEFAULT + or payload == PAYLOAD_NONE + or payload == PAYLOAD_EMPTY_JSON + ): + _LOGGER.debug( + "Ignoring empty payload '%s' after rendering for topic %s", + payload, + msg.topic, + ) + return + try: + event_attributes = json_loads_object(payload) + event_type = str(event_attributes.pop(event.ATTR_EVENT_TYPE)) + _LOGGER.debug( + ( + "JSON event data detected after processing payload '%s' on" + " topic %s, type %s, attributes %s" + ), + payload, + msg.topic, + event_type, + event_attributes, + ) + except KeyError: + _LOGGER.warning( + ( + "`event_type` missing in JSON event payload, " + " '%s' on topic %s" + ), + payload, + msg.topic, + ) + return + except JSON_DECODE_EXCEPTIONS: + _LOGGER.warning( + ( + "No valid JSON event payload detected, " + "value after processing payload" + " '%s' on topic %s" + ), + payload, + msg.topic, + ) + return + try: + self._trigger_event(event_type, event_attributes) + except ValueError: + _LOGGER.warning( + "Invalid event type %s for %s received on topic %s, payload %s", + event_type, + self.entity_id, + msg.topic, + payload, + ) + return + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + + topics["state_topic"] = { + "topic": self._config[CONF_STATE_TOPIC], + "msg_callback": message_received, + "qos": self._config[CONF_QOS], + "encoding": self._config[CONF_ENCODING] or None, + } + + self._sub_state = subscription.async_prepare_subscribe_topics( + self.hass, self._sub_state, topics + ) + + async def _subscribe_topics(self) -> None: + """(Re)Subscribe to topics.""" + await subscription.async_subscribe_topics(self.hass, self._sub_state) diff --git a/tests/components/mqtt/test_event.py b/tests/components/mqtt/test_event.py new file mode 100644 index 00000000000000..bc7b8b43523b5d --- /dev/null +++ b/tests/components/mqtt/test_event.py @@ -0,0 +1,673 @@ +"""The tests for the MQTT event platform.""" +import copy +import json +from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.components import event, mqtt +from homeassistant.components.mqtt.event import MQTT_EVENT_ATTRIBUTES_BLOCKED +from homeassistant.const import ( + STATE_UNKNOWN, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .test_common import ( + help_test_availability_when_connection_lost, + help_test_availability_without_topic, + help_test_custom_availability_payload, + help_test_default_availability_list_payload, + help_test_default_availability_list_payload_all, + help_test_default_availability_list_payload_any, + help_test_default_availability_list_single, + help_test_default_availability_payload, + help_test_discovery_broken, + help_test_discovery_removal, + help_test_discovery_update_attr, + help_test_discovery_update_availability, + help_test_entity_category, + help_test_entity_debug_info, + help_test_entity_debug_info_message, + help_test_entity_debug_info_remove, + help_test_entity_debug_info_update_entity_id, + help_test_entity_device_info_remove, + help_test_entity_device_info_update, + help_test_entity_device_info_with_connection, + help_test_entity_device_info_with_identifier, + help_test_entity_disabled_by_default, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, + help_test_entity_name, + help_test_reloadable, + help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_unique_id, + help_test_unload_config_entry_with_platform, + help_test_update_with_json_attrs_bad_json, + help_test_update_with_json_attrs_not_dict, +) + +from tests.common import ( + async_fire_mqtt_message, +) +from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient + +DEFAULT_CONFIG = { + mqtt.DOMAIN: { + event.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "event_types": ["press"], + } + } +} + + +@pytest.fixture(autouse=True) +def event_platform_only(): + """Only setup the event platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.EVENT]): + yield + + +@pytest.mark.freeze_time("2023-08-01 00:00:00+00:00") +@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +async def test_setting_event_value_via_mqtt_message( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the an MQTT event with attributes.""" + await mqtt_mock_entry() + + async_fire_mqtt_message( + hass, "test-topic", '{"event_type": "press", "duration": "short" }' + ) + state = hass.states.get("event.test") + + assert state.state == "2023-08-01T00:00:00.000+00:00" + assert state.attributes.get("duration") == "short" + + +@pytest.mark.freeze_time("2023-08-01 00:00:00+00:00") +@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +@pytest.mark.parametrize( + ("message", "log"), + [ + ( + '{"event_type": "press", "duration": "short" ', + "No valid JSON event payload detected", + ), + ('{"event_type": "invalid", "duration": "short" }', "Invalid event type"), + ('{"event_type": 2, "duration": "short" }', "Invalid event type"), + ('{"event_type": null, "duration": "short" }', "Invalid event type"), + ( + '{"event": "press", "duration": "short" }', + "`event_type` missing in JSON event payload", + ), + ], +) +async def test_setting_event_value_with_invalid_payload( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, + message: str, + log: str, +) -> None: + """Test the an MQTT event with attributes.""" + await mqtt_mock_entry() + + async_fire_mqtt_message(hass, "test-topic", message) + state = hass.states.get("event.test") + + assert state is not None + assert state.state == STATE_UNKNOWN + assert log in caplog.text + + +@pytest.mark.freeze_time("2023-08-01 00:00:00+00:00") +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + event.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "event_types": ["press"], + "value_template": '{"event_type": "press", "val": "{{ value_json.val | is_defined }}", "par": "{{ value_json.par }}"}', + } + } + } + ], +) +async def test_setting_event_value_via_mqtt_json_message_and_default_current_state( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + freezer: FrozenDateTimeFactory, +) -> None: + """Test processing an event via MQTT with fall back to current state.""" + await mqtt_mock_entry() + + async_fire_mqtt_message( + hass, "test-topic", '{ "val": "valcontent", "par": "parcontent" }' + ) + state = hass.states.get("event.test") + + assert state.state == "2023-08-01T00:00:00.000+00:00" + assert state.attributes.get("val") == "valcontent" + assert state.attributes.get("par") == "parcontent" + + freezer.move_to("2023-08-01 00:00:10+00:00") + + async_fire_mqtt_message(hass, "test-topic", '{ "par": "invalidcontent" }') + state = hass.states.get("event.test") + + assert state.state == "2023-08-01T00:00:00.000+00:00" + assert state.attributes.get("val") == "valcontent" + assert state.attributes.get("par") == "parcontent" + + +@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +async def test_availability_when_connection_lost( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability after MQTT disconnection.""" + await help_test_availability_when_connection_lost( + hass, mqtt_mock_entry, event.DOMAIN + ) + + +@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +async def test_availability_without_topic( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_payload( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_list_payload( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability by default payload with defined topic.""" + await help_test_default_availability_list_payload( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_list_payload_all( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability by default payload with defined topic.""" + await help_test_default_availability_list_payload_all( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_list_payload_any( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability by default payload with defined topic.""" + await help_test_default_availability_list_payload_any( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_list_single( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test availability list and availability_topic are mutually exclusive.""" + await help_test_default_availability_list_single( + hass, caplog, event.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_custom_availability_payload( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability by custom payload with defined topic.""" + await help_test_custom_availability_payload( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_discovery_update_availability( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability discovery update.""" + await help_test_discovery_update_availability( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + event.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "event_types": ["press"], + "device_class": "foobarnotreal", + } + } + } + ], +) +async def test_invalid_device_class( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test device_class option with invalid value.""" + with pytest.raises(AssertionError): + await mqtt_mock_entry() + assert ( + "Invalid config for [mqtt]: expected EventDeviceClass or one of" in caplog.text + ) + + +async def test_setting_attribute_via_mqtt_json_message( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, + mqtt_mock_entry, + event.DOMAIN, + DEFAULT_CONFIG, + MQTT_EVENT_ATTRIBUTES_BLOCKED, + ) + + +async def test_setting_attribute_with_template( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_with_template( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_not_dict( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_not_dict( + hass, + mqtt_mock_entry, + caplog, + event.DOMAIN, + DEFAULT_CONFIG, + ) + + +async def test_update_with_json_attrs_bad_json( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_bad_json( + hass, + mqtt_mock_entry, + caplog, + event.DOMAIN, + DEFAULT_CONFIG, + ) + + +async def test_discovery_update_attr( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update of discovered MQTTAttributes.""" + await help_test_discovery_update_attr( + hass, + mqtt_mock_entry, + caplog, + event.DOMAIN, + DEFAULT_CONFIG, + ) + + +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + event.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "event_types": ["press"], + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "event_types": ["press"], + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } + } + ], +) +async def test_unique_id( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test unique id option only creates one event per unique_id.""" + await help_test_unique_id(hass, mqtt_mock_entry, event.DOMAIN) + + +async def test_discovery_removal_event( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test removal of discovered event.""" + data = '{ "name": "test", "state_topic": "test_topic", "event_types": ["press"] }' + await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, event.DOMAIN, data) + + +async def test_discovery_update_event_template( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update of discovered mqtt event template.""" + await mqtt_mock_entry() + config = {"name": "test", "state_topic": "test_topic", "event_types": ["press"]} + config1 = copy.deepcopy(config) + config2 = copy.deepcopy(config) + config1["name"] = "Beer" + config2["name"] = "Milk" + config1["state_topic"] = "event/state1" + config2["state_topic"] = "event/state1" + config1[ + "value_template" + ] = '{"event_type": "press", "val": "{{ value_json.val | int }}"}' + config2[ + "value_template" + ] = '{"event_type": "press", "val": "{{ value_json.val | int * 2 }}"}' + + async_fire_mqtt_message(hass, "homeassistant/event/bla/config", json.dumps(config1)) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, "event/state1", '{"val":100}') + await hass.async_block_till_done() + state = hass.states.get("event.beer") + assert state is not None + assert state.attributes.get("val") == "100" + + async_fire_mqtt_message(hass, "homeassistant/event/bla/config", json.dumps(config2)) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, "event/state1", '{"val":100}') + await hass.async_block_till_done() + state = hass.states.get("event.beer") + assert state is not None + assert state.attributes.get("val") == "200" + + +@pytest.mark.no_fail_on_log_exception +async def test_discovery_broken( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling of bad discovery message.""" + data1 = '{ "name": "Beer", "state_topic": "test_topic#", "event_types": ["press"] }' + data2 = '{ "name": "Milk", "state_topic": "test_topic", "event_types": ["press"] }' + await help_test_discovery_broken( + hass, mqtt_mock_entry, caplog, event.DOMAIN, data1, data2 + ) + + +async def test_entity_device_info_with_connection( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT event device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_with_identifier( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT event device registry integration.""" + await help_test_entity_device_info_with_identifier( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_update( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test device registry update.""" + await help_test_entity_device_info_update( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_remove( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test device registry remove.""" + await help_test_entity_device_info_remove( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_subscriptions( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT subscriptions are managed when entity_id is updated.""" + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_discovery_update( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT discovery update when entity_id is updated.""" + await help_test_entity_id_update_discovery_update( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_with_hub( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT event device registry integration.""" + await mqtt_mock_entry() + registry = dr.async_get(hass) + hub = registry.async_get_or_create( + config_entry_id="123", + connections=set(), + identifiers={("mqtt", "hub-id")}, + manufacturer="manufacturer", + model="hub", + ) + + data = json.dumps( + { + "name": "Test 1", + "state_topic": "test-topic", + "event_types": ["press"], + "device": {"identifiers": ["helloworld"], "via_device": "hub-id"}, + "unique_id": "veryunique", + } + ) + async_fire_mqtt_message(hass, "homeassistant/event/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device(identifiers={("mqtt", "helloworld")}) + assert device is not None + assert device.via_device_id == hub.id + + +async def test_entity_debug_info( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT event debug info.""" + await help_test_entity_debug_info( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_message( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG, None + ) + + +async def test_entity_debug_info_remove( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT event debug info.""" + await help_test_entity_debug_info_remove( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_update_entity_id( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT event debug info.""" + await help_test_entity_debug_info_update_entity_id( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_disabled_by_default( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test entity disabled by default.""" + await help_test_entity_disabled_by_default( + hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG + ) + + +@pytest.mark.no_fail_on_log_exception +async def test_entity_category( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test entity category.""" + await help_test_entity_category(hass, mqtt_mock_entry, event.DOMAIN, DEFAULT_CONFIG) + + +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + event.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "event_types": ["press"], + "value_template": '{ "event_type": "press", "val": \ + {% if state_attr(entity_id, "friendly_name") == "test" %} \ + "{{ value | int + 1 }}" \ + {% else %} \ + "{{ value }}" \ + {% endif %}}', + } + } + } + ], +) +async def test_value_template_with_entity_id( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the access to attributes in value_template via the entity_id.""" + await mqtt_mock_entry() + + async_fire_mqtt_message(hass, "test-topic", "100") + state = hass.states.get("event.test") + + assert state.attributes.get("val") == "101" + + +async def test_reloadable( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test reloading the MQTT platform.""" + domain = event.DOMAIN + config = DEFAULT_CONFIG + await help_test_reloadable(hass, mqtt_client_mock, domain, config) + + +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) +async def test_setup_manual_entity_from_yaml( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test setup manual configured MQTT entity.""" + await mqtt_mock_entry() + platform = event.DOMAIN + assert hass.states.get(f"{platform}.test") + + +async def test_unload_entry( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test unloading the config entry.""" + domain = event.DOMAIN + config = DEFAULT_CONFIG + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry, domain, config + ) + + +@pytest.mark.parametrize( + ("expected_friendly_name", "device_class"), + [("test", None), ("Doorbell", "doorbell"), ("Motion", "motion")], +) +async def test_entity_name( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + expected_friendly_name: str | None, + device_class: str | None, +) -> None: + """Test the entity name setup.""" + domain = event.DOMAIN + config = DEFAULT_CONFIG + await help_test_entity_name( + hass, mqtt_mock_entry, domain, config, expected_friendly_name, device_class + ) From e5747d3f4c2057b2aaaa904d099faaa501039a68 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sun, 23 Jul 2023 16:42:54 +0200 Subject: [PATCH 0805/1009] Bump python-kasa to 0.5.3 (#97088) --- 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 c0e85f3dc58344..c33106d13cc375 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -161,5 +161,5 @@ "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa[speedups]==0.5.2"] + "requirements": ["python-kasa[speedups]==0.5.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0429a807259bef..729cd3b478a735 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2105,7 +2105,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.5.2 +python-kasa[speedups]==0.5.3 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e07523e1892ca0..0620b82e14b899 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1546,7 +1546,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.5.2 +python-kasa[speedups]==0.5.3 # homeassistant.components.matter python-matter-server==3.6.3 From 1552319e944d165f59ddd287a4da065883e80822 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 23 Jul 2023 17:56:58 +0200 Subject: [PATCH 0806/1009] Add Axis camera sources to diagnostics (#97063) --- homeassistant/components/axis/camera.py | 43 ++++++++++++++------ homeassistant/components/axis/device.py | 2 + homeassistant/components/axis/diagnostics.py | 2 +- tests/components/axis/test_diagnostics.py | 5 +++ 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index c593c4fa419911..53e2c3c9fe5226 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -35,10 +35,16 @@ class AxisCamera(AxisEntity, MjpegCamera): _attr_supported_features = CameraEntityFeature.STREAM + _still_image_url: str + _mjpeg_url: str + _stream_source: str + def __init__(self, device: AxisNetworkDevice) -> None: """Initialize Axis Communications camera component.""" AxisEntity.__init__(self, device) + self._generate_sources() + MjpegCamera.__init__( self, username=device.username, @@ -46,41 +52,52 @@ def __init__(self, device: AxisNetworkDevice) -> None: mjpeg_url=self.mjpeg_source, still_image_url=self.image_source, authentication=HTTP_DIGEST_AUTHENTICATION, + unique_id=f"{device.unique_id}-camera", ) - self._attr_unique_id = f"{device.unique_id}-camera" - async def async_added_to_hass(self) -> None: """Subscribe camera events.""" self.async_on_remove( async_dispatcher_connect( - self.hass, self.device.signal_new_address, self._new_address + self.hass, self.device.signal_new_address, self._generate_sources ) ) await super().async_added_to_hass() - def _new_address(self) -> None: - """Set new device address for video stream.""" - self._mjpeg_url = self.mjpeg_source - self._still_image_url = self.image_source + def _generate_sources(self) -> None: + """Generate sources. + + Additionally used when device change IP address. + """ + image_options = self.generate_options(skip_stream_profile=True) + self._still_image_url = f"http://{self.device.host}:{self.device.port}/axis-cgi/jpg/image.cgi{image_options}" + + mjpeg_options = self.generate_options() + self._mjpeg_url = f"http://{self.device.host}:{self.device.port}/axis-cgi/mjpg/video.cgi{mjpeg_options}" + + stream_options = self.generate_options(add_video_codec_h264=True) + self._stream_source = f"rtsp://{self.device.username}:{self.device.password}@{self.device.host}/axis-media/media.amp{stream_options}" + + self.device.additional_diagnostics["camera_sources"] = { + "Image": self._still_image_url, + "MJPEG": self._mjpeg_url, + "Stream": f"rtsp://user:pass@{self.device.host}/axis-media/media.amp{stream_options}", + } @property def image_source(self) -> str: """Return still image URL for device.""" - options = self.generate_options(skip_stream_profile=True) - return f"http://{self.device.host}:{self.device.port}/axis-cgi/jpg/image.cgi{options}" + return self._still_image_url @property def mjpeg_source(self) -> str: """Return mjpeg URL for device.""" - options = self.generate_options() - return f"http://{self.device.host}:{self.device.port}/axis-cgi/mjpg/video.cgi{options}" + return self._mjpeg_url async def stream_source(self) -> str: """Return the stream source.""" - options = self.generate_options(add_video_codec_h264=True) - return f"rtsp://{self.device.username}:{self.device.password}@{self.device.host}/axis-media/media.amp{options}" + return self._stream_source def generate_options( self, skip_stream_profile: bool = False, add_video_codec_h264: bool = False diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index f53e69fba9fc12..8f3c8b9a8b6b4f 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -62,6 +62,8 @@ def __init__( self.fw_version = api.vapix.firmware_version self.product_type = api.vapix.product_type + self.additional_diagnostics: dict[str, Any] = {} + @property def host(self): """Return the host address of this device.""" diff --git a/homeassistant/components/axis/diagnostics.py b/homeassistant/components/axis/diagnostics.py index 277f24513dee35..20dfedd717b346 100644 --- a/homeassistant/components/axis/diagnostics.py +++ b/homeassistant/components/axis/diagnostics.py @@ -21,7 +21,7 @@ async def async_get_config_entry_diagnostics( ) -> dict[str, Any]: """Return diagnostics for a config entry.""" device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.entry_id] - diag: dict[str, Any] = {} + diag: dict[str, Any] = device.additional_diagnostics.copy() diag["config"] = async_redact_data(config_entry.as_dict(), REDACT_CONFIG) diff --git a/tests/components/axis/test_diagnostics.py b/tests/components/axis/test_diagnostics.py index df5d071ddbe881..a76aa40ebc8df8 100644 --- a/tests/components/axis/test_diagnostics.py +++ b/tests/components/axis/test_diagnostics.py @@ -38,6 +38,11 @@ async def test_entry_diagnostics( "unique_id": REDACTED, "disabled_by": None, }, + "camera_sources": { + "Image": "http://1.2.3.4:80/axis-cgi/jpg/image.cgi", + "MJPEG": "http://1.2.3.4:80/axis-cgi/mjpg/video.cgi", + "Stream": "rtsp://user:pass@1.2.3.4/axis-media/media.amp?videocodec=h264", + }, "api_discovery": [ { "id": "api-discovery", From 38111141f954ff1912d8e150c3e3e5b5b3e8cb88 Mon Sep 17 00:00:00 2001 From: Miguel Camba Date: Sun, 23 Jul 2023 18:49:10 +0200 Subject: [PATCH 0807/1009] Add new device class: PH (potential hydrogen) (#95928) --- homeassistant/components/number/const.py | 7 +++++++ homeassistant/components/number/strings.json | 3 +++ homeassistant/components/scrape/strings.json | 1 + homeassistant/components/sensor/const.py | 8 ++++++++ homeassistant/components/sensor/device_condition.py | 3 +++ homeassistant/components/sensor/device_trigger.py | 3 +++ homeassistant/components/sensor/strings.json | 5 +++++ homeassistant/components/sql/strings.json | 1 + tests/components/sensor/test_init.py | 1 + 9 files changed, 32 insertions(+) diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 849581b6f9fd14..461139a15eab2b 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -216,6 +216,12 @@ class NumberDeviceClass(StrEnum): Unit of measurement: `µg/m³` """ + PH = "ph" + """Potential hidrogen (acidity/alkalinity). + + Unit of measurement: Unitless + """ + PM1 = "pm1" """Particulate matter <= 1 μm. @@ -422,6 +428,7 @@ class NumberMode(StrEnum): NumberDeviceClass.NITROGEN_MONOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, NumberDeviceClass.NITROUS_OXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, NumberDeviceClass.OZONE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + NumberDeviceClass.PH: {None}, NumberDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, NumberDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, NumberDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, diff --git a/homeassistant/components/number/strings.json b/homeassistant/components/number/strings.json index e954a55b280edd..2d72cdbf203d58 100644 --- a/homeassistant/components/number/strings.json +++ b/homeassistant/components/number/strings.json @@ -91,6 +91,9 @@ "ozone": { "name": "[%key:component::sensor::entity_component::ozone::name%]" }, + "ph": { + "name": "[%key:component::sensor::entity_component::ph::name%]" + }, "pm1": { "name": "[%key:component::sensor::entity_component::pm1::name%]" }, diff --git a/homeassistant/components/scrape/strings.json b/homeassistant/components/scrape/strings.json index e5ed8613fc48fb..4301bb7d5a0d65 100644 --- a/homeassistant/components/scrape/strings.json +++ b/homeassistant/components/scrape/strings.json @@ -155,6 +155,7 @@ "nitrogen_monoxide": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]", "nitrous_oxide": "[%key:component::sensor::entity_component::nitrous_oxide::name%]", "ozone": "[%key:component::sensor::entity_component::ozone::name%]", + "ph": "[%key:component::sensor::entity_component::ph::name%]", "pm1": "[%key:component::sensor::entity_component::pm1::name%]", "pm10": "[%key:component::sensor::entity_component::pm10::name%]", "pm25": "[%key:component::sensor::entity_component::pm25::name%]", diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 2c6883e4a71df5..13c9293daa71da 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -247,6 +247,12 @@ class SensorDeviceClass(StrEnum): Unit of measurement: `µg/m³` """ + PH = "ph" + """Potential hidrogen (acidity/alkalinity). + + Unit of measurement: Unitless + """ + PM1 = "pm1" """Particulate matter <= 1 μm. @@ -509,6 +515,7 @@ class SensorStateClass(StrEnum): SensorDeviceClass.NITROGEN_MONOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, SensorDeviceClass.NITROUS_OXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, SensorDeviceClass.OZONE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + SensorDeviceClass.PH: {None}, SensorDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, SensorDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, SensorDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, @@ -576,6 +583,7 @@ class SensorStateClass(StrEnum): SensorDeviceClass.NITROGEN_MONOXIDE: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.NITROUS_OXIDE: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.OZONE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.PH: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.PM1: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.PM10: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.PM25: {SensorStateClass.MEASUREMENT}, diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 7d6c57de2963e4..b12cdb570eb1f7 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -57,6 +57,7 @@ CONF_IS_NITROGEN_MONOXIDE = "is_nitrogen_monoxide" CONF_IS_NITROUS_OXIDE = "is_nitrous_oxide" CONF_IS_OZONE = "is_ozone" +CONF_IS_PH = "is_ph" CONF_IS_PM1 = "is_pm1" CONF_IS_PM10 = "is_pm10" CONF_IS_PM25 = "is_pm25" @@ -107,6 +108,7 @@ SensorDeviceClass.OZONE: [{CONF_TYPE: CONF_IS_OZONE}], SensorDeviceClass.POWER: [{CONF_TYPE: CONF_IS_POWER}], SensorDeviceClass.POWER_FACTOR: [{CONF_TYPE: CONF_IS_POWER_FACTOR}], + SensorDeviceClass.PH: [{CONF_TYPE: CONF_IS_PH}], SensorDeviceClass.PM1: [{CONF_TYPE: CONF_IS_PM1}], SensorDeviceClass.PM10: [{CONF_TYPE: CONF_IS_PM10}], SensorDeviceClass.PM25: [{CONF_TYPE: CONF_IS_PM25}], @@ -167,6 +169,7 @@ CONF_IS_OZONE, CONF_IS_POWER, CONF_IS_POWER_FACTOR, + CONF_IS_PH, CONF_IS_PM1, CONF_IS_PM10, CONF_IS_PM25, diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 1bb41eb2d30b72..1c0da89692b8e0 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -56,6 +56,7 @@ CONF_NITROGEN_MONOXIDE = "nitrogen_monoxide" CONF_NITROUS_OXIDE = "nitrous_oxide" CONF_OZONE = "ozone" +CONF_PH = "ph" CONF_PM1 = "pm1" CONF_PM10 = "pm10" CONF_PM25 = "pm25" @@ -104,6 +105,7 @@ SensorDeviceClass.NITROGEN_MONOXIDE: [{CONF_TYPE: CONF_NITROGEN_MONOXIDE}], SensorDeviceClass.NITROUS_OXIDE: [{CONF_TYPE: CONF_NITROUS_OXIDE}], SensorDeviceClass.OZONE: [{CONF_TYPE: CONF_OZONE}], + SensorDeviceClass.PH: [{CONF_TYPE: CONF_PH}], SensorDeviceClass.PM1: [{CONF_TYPE: CONF_PM1}], SensorDeviceClass.PM10: [{CONF_TYPE: CONF_PM10}], SensorDeviceClass.PM25: [{CONF_TYPE: CONF_PM25}], @@ -165,6 +167,7 @@ CONF_NITROGEN_MONOXIDE, CONF_NITROUS_OXIDE, CONF_OZONE, + CONF_PH, CONF_PM1, CONF_PM10, CONF_PM25, diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index c4c1f81109d49a..1db5e4c8cfd8a1 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -25,6 +25,7 @@ "is_nitrogen_monoxide": "Current {entity_name} nitrogen monoxide concentration level", "is_nitrous_oxide": "Current {entity_name} nitrous oxide concentration level", "is_ozone": "Current {entity_name} ozone concentration level", + "is_ph": "Current {entity_name} pH level", "is_pm1": "Current {entity_name} PM1 concentration level", "is_pm10": "Current {entity_name} PM10 concentration level", "is_pm25": "Current {entity_name} PM2.5 concentration level", @@ -72,6 +73,7 @@ "nitrogen_monoxide": "{entity_name} nitrogen monoxide concentration changes", "nitrous_oxide": "{entity_name} nitrous oxide concentration changes", "ozone": "{entity_name} ozone concentration changes", + "ph": "{entity_name} pH level changes", "pm1": "{entity_name} PM1 concentration changes", "pm10": "{entity_name} PM10 concentration changes", "pm25": "{entity_name} PM2.5 concentration changes", @@ -198,6 +200,9 @@ "ozone": { "name": "Ozone" }, + "ph": { + "name": "pH" + }, "pm1": { "name": "PM1" }, diff --git a/homeassistant/components/sql/strings.json b/homeassistant/components/sql/strings.json index 74c165e9d20635..9ac8bd220277e7 100644 --- a/homeassistant/components/sql/strings.json +++ b/homeassistant/components/sql/strings.json @@ -93,6 +93,7 @@ "nitrogen_monoxide": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]", "nitrous_oxide": "[%key:component::sensor::entity_component::nitrous_oxide::name%]", "ozone": "[%key:component::sensor::entity_component::ozone::name%]", + "ph": "[%key:component::sensor::entity_component::ph::name%]", "pm1": "[%key:component::sensor::entity_component::pm1::name%]", "pm10": "[%key:component::sensor::entity_component::pm10::name%]", "pm25": "[%key:component::sensor::entity_component::pm25::name%]", diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index b5d425029d08fc..c5406a85fc0ac0 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1799,6 +1799,7 @@ async def test_non_numeric_device_class_with_unit_of_measurement( SensorDeviceClass.NITROGEN_MONOXIDE, SensorDeviceClass.NITROUS_OXIDE, SensorDeviceClass.OZONE, + SensorDeviceClass.PH, SensorDeviceClass.PM1, SensorDeviceClass.PM10, SensorDeviceClass.PM25, From 5158461dec2ccd2ea5c2cb33f34177dd08efbca9 Mon Sep 17 00:00:00 2001 From: Luke Date: Sun, 23 Jul 2023 11:02:16 -0600 Subject: [PATCH 0808/1009] Add Number platform to Roborock (#94209) --- homeassistant/components/roborock/const.py | 8 +- homeassistant/components/roborock/device.py | 8 +- homeassistant/components/roborock/number.py | 121 ++++++++++++++++++ .../components/roborock/strings.json | 5 + tests/components/roborock/test_number.py | 38 ++++++ 5 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/roborock/number.py create mode 100644 tests/components/roborock/test_number.py diff --git a/homeassistant/components/roborock/const.py b/homeassistant/components/roborock/const.py index 287229c9fd1d74..e16ab3d91ae6f7 100644 --- a/homeassistant/components/roborock/const.py +++ b/homeassistant/components/roborock/const.py @@ -6,4 +6,10 @@ CONF_BASE_URL = "base_url" CONF_USER_DATA = "user_data" -PLATFORMS = [Platform.VACUUM, Platform.SELECT, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.VACUUM, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, + Platform.NUMBER, +] diff --git a/homeassistant/components/roborock/device.py b/homeassistant/components/roborock/device.py index 90ca13c5146ebe..86d578d852a6f4 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -31,7 +31,7 @@ def __init__( @property def api(self) -> RoborockLocalClient: - """Returns the api.""" + """Return the Api.""" return self._api def get_cache(self, attribute: CacheableAttribute) -> AttributeCache: @@ -39,7 +39,9 @@ def get_cache(self, attribute: CacheableAttribute) -> AttributeCache: return self._api.cache.get(attribute) async def send( - self, command: RoborockCommand, params: dict[str, Any] | list[Any] | None = None + self, + command: RoborockCommand, + params: dict[str, Any] | list[Any] | int | None = None, ) -> dict: """Send a command to a vacuum cleaner.""" try: @@ -87,7 +89,7 @@ def _device_status(self) -> Status: async def send( self, command: RoborockCommand, - params: dict[str, Any] | list[Any] | None = None, + params: dict[str, Any] | list[Any] | int | None = None, ) -> dict: """Overloads normal send command but refreshes coordinator.""" res = await super().send(command, params) diff --git a/homeassistant/components/roborock/number.py b/homeassistant/components/roborock/number.py new file mode 100644 index 00000000000000..4eaf1464f899eb --- /dev/null +++ b/homeassistant/components/roborock/number.py @@ -0,0 +1,121 @@ +"""Support for Roborock number.""" +import asyncio +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +import logging +from typing import Any + +from roborock.api import AttributeCache +from roborock.command_cache import CacheableAttribute +from roborock.exceptions import RoborockException + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify + +from .const import DOMAIN +from .coordinator import RoborockDataUpdateCoordinator +from .device import RoborockEntity + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class RoborockNumberDescriptionMixin: + """Define an entity description mixin for button entities.""" + + # Gets the status of the switch + cache_key: CacheableAttribute + # Sets the status of the switch + update_value: Callable[[AttributeCache, float], Coroutine[Any, Any, dict]] + + +@dataclass +class RoborockNumberDescription( + NumberEntityDescription, RoborockNumberDescriptionMixin +): + """Class to describe an Roborock number entity.""" + + +NUMBER_DESCRIPTIONS: list[RoborockNumberDescription] = [ + RoborockNumberDescription( + key="volume", + translation_key="volume", + icon="mdi:volume-source", + native_min_value=0, + native_max_value=100, + native_unit_of_measurement=PERCENTAGE, + cache_key=CacheableAttribute.sound_volume, + entity_category=EntityCategory.CONFIG, + update_value=lambda cache, value: cache.update_value([int(value)]), + ) +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Roborock number platform.""" + coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ + config_entry.entry_id + ] + possible_entities: list[ + tuple[RoborockDataUpdateCoordinator, RoborockNumberDescription] + ] = [ + (coordinator, description) + for coordinator in coordinators.values() + for description in NUMBER_DESCRIPTIONS + ] + # We need to check if this function is supported by the device. + results = await asyncio.gather( + *( + coordinator.api.cache.get(description.cache_key).async_value() + for coordinator, description in possible_entities + ), + return_exceptions=True, + ) + valid_entities: list[RoborockNumberEntity] = [] + for (coordinator, description), result in zip(possible_entities, results): + if result is None or isinstance(result, RoborockException): + _LOGGER.debug("Not adding entity because of %s", result) + else: + valid_entities.append( + RoborockNumberEntity( + f"{description.key}_{slugify(coordinator.roborock_device_info.device.duid)}", + coordinator, + description, + ) + ) + async_add_entities(valid_entities) + + +class RoborockNumberEntity(RoborockEntity, NumberEntity): + """A class to let you set options on a Roborock vacuum where the potential options are fixed.""" + + entity_description: RoborockNumberDescription + + def __init__( + self, + unique_id: str, + coordinator: RoborockDataUpdateCoordinator, + entity_description: RoborockNumberDescription, + ) -> None: + """Create a number entity.""" + self.entity_description = entity_description + super().__init__(unique_id, coordinator.device_info, coordinator.api) + + @property + def native_value(self) -> float | None: + """Get native value.""" + return self.get_cache(self.entity_description.cache_key).value + + async def async_set_native_value(self, value: float) -> None: + """Set number value.""" + await self.entity_description.update_value( + self.get_cache(self.entity_description.cache_key), value + ) diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 3b3e6221895895..3989f08505b46f 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -27,6 +27,11 @@ } }, "entity": { + "number": { + "volume": { + "name": "Volume" + } + }, "sensor": { "cleaning_area": { "name": "Cleaning area" diff --git a/tests/components/roborock/test_number.py b/tests/components/roborock/test_number.py new file mode 100644 index 00000000000000..b660bfc296983a --- /dev/null +++ b/tests/components/roborock/test_number.py @@ -0,0 +1,38 @@ +"""Test Roborock Number platform.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.number import ATTR_VALUE, SERVICE_SET_VALUE +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("entity_id", "value"), + [ + ("number.roborock_s7_maxv_volume", 3.0), + ], +) +async def test_update_success( + hass: HomeAssistant, + bypass_api_fixture, + setup_entry: MockConfigEntry, + entity_id: str, + value: float, +) -> None: + """Test allowed changing values for number 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: + await hass.services.async_call( + "number", + SERVICE_SET_VALUE, + service_data={ATTR_VALUE: value}, + blocking=True, + target={"entity_id": entity_id}, + ) + assert mock_send_message.assert_called_once From dd6cd0096aeb15523cfbdbff88f8c6a54c487d00 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 23 Jul 2023 20:09:16 +0200 Subject: [PATCH 0809/1009] Improve code coverage for LastFM (#97012) * Improve code coverage for LastFM * Revert introduced bug --- tests/components/lastfm/conftest.py | 25 +++++++++++++++++++++ tests/components/lastfm/test_config_flow.py | 24 +++++++++++--------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/tests/components/lastfm/conftest.py b/tests/components/lastfm/conftest.py index 8b8548ad1f9fa9..c7cada9ba0a370 100644 --- a/tests/components/lastfm/conftest.py +++ b/tests/components/lastfm/conftest.py @@ -36,6 +36,20 @@ def mock_config_entry() -> MockConfigEntry: ) +@pytest.fixture(name="imported_config_entry") +def mock_imported_config_entry() -> MockConfigEntry: + """Create LastFM entry in Home Assistant.""" + return MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + CONF_API_KEY: API_KEY, + CONF_MAIN_USER: None, + CONF_USERS: [USERNAME_1, USERNAME_2], + }, + ) + + @pytest.fixture(name="setup_integration") async def mock_setup_integration( hass: HomeAssistant, @@ -54,6 +68,17 @@ async def func(mock_config_entry: MockConfigEntry, mock_user: MockUser) -> None: @pytest.fixture(name="default_user") def mock_default_user() -> MockUser: """Return default mock user.""" + return MockUser( + now_playing_result=Track("artist", "title", MockNetwork("lastfm")), + top_tracks=[Track("artist", "title", MockNetwork("lastfm"))], + recent_tracks=[Track("artist", "title", MockNetwork("lastfm"))], + friends=[MockUser()], + ) + + +@pytest.fixture(name="default_user_no_friends") +def mock_default_user_no_friends() -> MockUser: + """Return default mock user without friends.""" return MockUser( now_playing_result=Track("artist", "title", MockNetwork("lastfm")), top_tracks=[Track("artist", "title", MockNetwork("lastfm"))], diff --git a/tests/components/lastfm/test_config_flow.py b/tests/components/lastfm/test_config_flow.py index ce28638c3f3167..07e96afaced4d9 100644 --- a/tests/components/lastfm/test_config_flow.py +++ b/tests/components/lastfm/test_config_flow.py @@ -139,10 +139,12 @@ async def test_flow_friends_invalid_username( async def test_flow_friends_no_friends( - hass: HomeAssistant, default_user: MockUser + hass: HomeAssistant, default_user_no_friends: MockUser ) -> None: """Test options is empty when user has no friends.""" - with patch("pylast.User", return_value=default_user), patch_setup_entry(): + with patch( + "pylast.User", return_value=default_user_no_friends + ), patch_setup_entry(): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -177,11 +179,11 @@ async def test_import_flow_success(hass: HomeAssistant, default_user: MockUser) async def test_import_flow_already_exist( hass: HomeAssistant, setup_integration: ComponentSetup, - config_entry: MockConfigEntry, + imported_config_entry: MockConfigEntry, default_user: MockUser, ) -> None: """Test import of yaml already exist.""" - await setup_integration(config_entry, default_user) + await setup_integration(imported_config_entry, default_user) with patch("pylast.User", return_value=default_user): result = await hass.config_entries.flow.async_init( @@ -275,12 +277,12 @@ async def test_options_flow_incorrect_username( async def test_options_flow_from_import( hass: HomeAssistant, setup_integration: ComponentSetup, - config_entry: MockConfigEntry, - default_user: MockUser, + imported_config_entry: MockConfigEntry, + default_user_no_friends: MockUser, ) -> None: """Test updating options gained from import.""" - await setup_integration(config_entry, default_user) - with patch("pylast.User", return_value=default_user): + await setup_integration(imported_config_entry, default_user_no_friends) + with patch("pylast.User", return_value=default_user_no_friends): entry = hass.config_entries.async_entries(DOMAIN)[0] result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() @@ -294,11 +296,11 @@ async def test_options_flow_without_friends( hass: HomeAssistant, setup_integration: ComponentSetup, config_entry: MockConfigEntry, - default_user: MockUser, + default_user_no_friends: MockUser, ) -> None: """Test updating options for someone without friends.""" - await setup_integration(config_entry, default_user) - with patch("pylast.User", return_value=default_user): + await setup_integration(config_entry, default_user_no_friends) + with patch("pylast.User", return_value=default_user_no_friends): entry = hass.config_entries.async_entries(DOMAIN)[0] result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() From 54044161c33da670e328eedeada3cbf79a5e363e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 23 Jul 2023 20:11:26 +0200 Subject: [PATCH 0810/1009] Add entity translations to Renson (#96040) --- homeassistant/components/renson/sensor.py | 78 ++++++------- homeassistant/components/renson/strings.json | 112 +++++++++++++++++++ 2 files changed, 152 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/renson/sensor.py b/homeassistant/components/renson/sensor.py index 9817951b094a05..c8a355a0f7c86a 100644 --- a/homeassistant/components/renson/sensor.py +++ b/homeassistant/components/renson/sensor.py @@ -50,6 +50,16 @@ from .const import DOMAIN from .entity import RensonEntity +OPTIONS_MAPPING = { + "Off": "off", + "Level1": "level1", + "Level2": "level2", + "Level3": "level3", + "Level4": "level4", + "Breeze": "breeze", + "Holiday": "holiday", +} + @dataclass class RensonSensorEntityDescriptionMixin: @@ -63,13 +73,13 @@ class RensonSensorEntityDescriptionMixin: class RensonSensorEntityDescription( SensorEntityDescription, RensonSensorEntityDescriptionMixin ): - """Description of sensor.""" + """Description of a Renson sensor.""" SENSORS: tuple[RensonSensorEntityDescription, ...] = ( RensonSensorEntityDescription( key="CO2_QUALITY_FIELD", - name="CO2 quality category", + translation_key="co2_quality_category", field=CO2_QUALITY_FIELD, raw_format=False, device_class=SensorDeviceClass.ENUM, @@ -77,7 +87,7 @@ class RensonSensorEntityDescription( ), RensonSensorEntityDescription( key="AIR_QUALITY_FIELD", - name="Air quality category", + translation_key="air_quality_category", field=AIR_QUALITY_FIELD, raw_format=False, device_class=SensorDeviceClass.ENUM, @@ -85,7 +95,6 @@ class RensonSensorEntityDescription( ), RensonSensorEntityDescription( key="CO2_FIELD", - name="CO2 quality", field=CO2_FIELD, raw_format=True, state_class=SensorStateClass.MEASUREMENT, @@ -94,7 +103,7 @@ class RensonSensorEntityDescription( ), RensonSensorEntityDescription( key="AIR_FIELD", - name="Air quality", + translation_key="air_quality", field=AIR_QUALITY_FIELD, state_class=SensorStateClass.MEASUREMENT, raw_format=True, @@ -102,15 +111,15 @@ class RensonSensorEntityDescription( ), RensonSensorEntityDescription( key="CURRENT_LEVEL_FIELD", - name="Ventilation level", + translation_key="ventilation_level", field=CURRENT_LEVEL_FIELD, raw_format=False, device_class=SensorDeviceClass.ENUM, - options=["Off", "Level1", "Level2", "Level3", "Level4", "Breeze", "Holiday"], + options=["off", "level1", "level2", "level3", "level4", "breeze", "holiday"], ), RensonSensorEntityDescription( key="CURRENT_AIRFLOW_EXTRACT_FIELD", - name="Total airflow out", + translation_key="total_airflow_out", field=CURRENT_AIRFLOW_EXTRACT_FIELD, raw_format=False, state_class=SensorStateClass.MEASUREMENT, @@ -118,7 +127,7 @@ class RensonSensorEntityDescription( ), RensonSensorEntityDescription( key="CURRENT_AIRFLOW_INGOING_FIELD", - name="Total airflow in", + translation_key="total_airflow_in", field=CURRENT_AIRFLOW_INGOING_FIELD, raw_format=False, state_class=SensorStateClass.MEASUREMENT, @@ -126,7 +135,7 @@ class RensonSensorEntityDescription( ), RensonSensorEntityDescription( key="OUTDOOR_TEMP_FIELD", - name="Outdoor air temperature", + translation_key="outdoor_air_temperature", field=OUTDOOR_TEMP_FIELD, raw_format=False, device_class=SensorDeviceClass.TEMPERATURE, @@ -135,7 +144,7 @@ class RensonSensorEntityDescription( ), RensonSensorEntityDescription( key="INDOOR_TEMP_FIELD", - name="Extract air temperature", + translation_key="extract_air_temperature", field=INDOOR_TEMP_FIELD, raw_format=False, device_class=SensorDeviceClass.TEMPERATURE, @@ -144,7 +153,7 @@ class RensonSensorEntityDescription( ), RensonSensorEntityDescription( key="FILTER_REMAIN_FIELD", - name="Filter change", + translation_key="filter_change", field=FILTER_REMAIN_FIELD, raw_format=False, device_class=SensorDeviceClass.DURATION, @@ -153,7 +162,6 @@ class RensonSensorEntityDescription( ), RensonSensorEntityDescription( key="HUMIDITY_FIELD", - name="Relative humidity", field=HUMIDITY_FIELD, raw_format=False, device_class=SensorDeviceClass.HUMIDITY, @@ -162,15 +170,15 @@ class RensonSensorEntityDescription( ), RensonSensorEntityDescription( key="MANUAL_LEVEL_FIELD", - name="Manual level", + translation_key="manual_level", field=MANUAL_LEVEL_FIELD, raw_format=False, device_class=SensorDeviceClass.ENUM, - options=["Off", "Level1", "Level2", "Level3", "Level4", "Breeze", "Holiday"], + options=["off", "level1", "level2", "level3", "level4", "breeze", "holiday"], ), RensonSensorEntityDescription( key="BREEZE_TEMPERATURE_FIELD", - name="Breeze temperature", + translation_key="breeze_temperature", field=BREEZE_TEMPERATURE_FIELD, raw_format=False, device_class=SensorDeviceClass.TEMPERATURE, @@ -179,58 +187,48 @@ class RensonSensorEntityDescription( ), RensonSensorEntityDescription( key="BREEZE_LEVEL_FIELD", - name="Breeze level", + translation_key="breeze_level", field=BREEZE_LEVEL_FIELD, raw_format=False, entity_registry_enabled_default=False, device_class=SensorDeviceClass.ENUM, - options=["Off", "Level1", "Level2", "Level3", "Level4", "Breeze"], + options=["off", "level1", "level2", "level3", "level4", "breeze"], ), RensonSensorEntityDescription( key="DAYTIME_FIELD", - name="Start day time", + translation_key="start_day_time", field=DAYTIME_FIELD, raw_format=False, entity_registry_enabled_default=False, ), RensonSensorEntityDescription( key="NIGHTTIME_FIELD", - name="Start night time", + translation_key="start_night_time", field=NIGHTTIME_FIELD, raw_format=False, entity_registry_enabled_default=False, ), RensonSensorEntityDescription( key="DAY_POLLUTION_FIELD", - name="Day pollution level", + translation_key="day_pollution_level", field=DAY_POLLUTION_FIELD, raw_format=False, entity_registry_enabled_default=False, device_class=SensorDeviceClass.ENUM, - options=[ - "Level1", - "Level2", - "Level3", - "Level4", - ], + options=["level1", "level2", "level3", "level4"], ), RensonSensorEntityDescription( key="NIGHT_POLLUTION_FIELD", - name="Night pollution level", + translation_key="co2_quality_category", field=NIGHT_POLLUTION_FIELD, raw_format=False, entity_registry_enabled_default=False, device_class=SensorDeviceClass.ENUM, - options=[ - "Level1", - "Level2", - "Level3", - "Level4", - ], + options=["level1", "level2", "level3", "level4"], ), RensonSensorEntityDescription( key="CO2_THRESHOLD_FIELD", - name="CO2 threshold", + translation_key="co2_threshold", field=CO2_THRESHOLD_FIELD, raw_format=False, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, @@ -238,7 +236,7 @@ class RensonSensorEntityDescription( ), RensonSensorEntityDescription( key="CO2_HYSTERESIS_FIELD", - name="CO2 hysteresis", + translation_key="co2_hysteresis", field=CO2_HYSTERESIS_FIELD, raw_format=False, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, @@ -246,7 +244,7 @@ class RensonSensorEntityDescription( ), RensonSensorEntityDescription( key="BYPASS_TEMPERATURE_FIELD", - name="Bypass activation temperature", + translation_key="bypass_activation_temperature", field=BYPASS_TEMPERATURE_FIELD, raw_format=False, device_class=SensorDeviceClass.TEMPERATURE, @@ -255,7 +253,7 @@ class RensonSensorEntityDescription( ), RensonSensorEntityDescription( key="BYPASS_LEVEL_FIELD", - name="Bypass level", + translation_key="bypass_level", field=BYPASS_LEVEL_FIELD, raw_format=False, device_class=SensorDeviceClass.POWER_FACTOR, @@ -292,6 +290,10 @@ def _handle_coordinator_update(self) -> None: if self.raw_format: self._attr_native_value = value + elif self.entity_description.device_class == SensorDeviceClass.ENUM: + self._attr_native_value = OPTIONS_MAPPING.get( + self.api.parse_value(value, self.data_type), None + ) else: self._attr_native_value = self.api.parse_value(value, self.data_type) diff --git a/homeassistant/components/renson/strings.json b/homeassistant/components/renson/strings.json index 16c5de158a9ae1..06636c9d503889 100644 --- a/homeassistant/components/renson/strings.json +++ b/homeassistant/components/renson/strings.json @@ -11,5 +11,117 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" } + }, + "entity": { + "sensor": { + "co2_quality_category": { + "name": "CO2 quality category", + "state": { + "good": "Good", + "bad": "Bad", + "poor": "Poor" + } + }, + "air_quality_category": { + "name": "Air quality category", + "state": { + "good": "[%key:component::renson::entity::sensor::co2_quality_category::state::good%]", + "bad": "[%key:component::renson::entity::sensor::co2_quality_category::state::bad%]", + "poor": "[%key:component::renson::entity::sensor::co2_quality_category::state::poor%]" + } + }, + "air_quality": { + "name": "Air quality" + }, + "ventilation_level": { + "name": "Ventilation level", + "state": { + "off": "[%key:common::state::off%]", + "level1": "Level 1", + "level2": "Level 2", + "level3": "Level 3", + "level4": "Level 4", + "breeze": "Breeze", + "holiday": "Holiday" + } + }, + "total_airflow_out": { + "name": "Total airflow out" + }, + "total_airflow_in": { + "name": "Total airflow in" + }, + "outdoor_air_temperature": { + "name": "Outdoor air temperature" + }, + "extract_air_temperature": { + "name": "Extract air temperature" + }, + "filter_change": { + "name": "Filter change" + }, + "manual_level": { + "name": "Manual level", + "state": { + "off": "[%key:common::state::off%]", + "level1": "[%key:component::renson::entity::sensor::ventilation_level::state::level1%]", + "level2": "[%key:component::renson::entity::sensor::ventilation_level::state::level2%]", + "level3": "[%key:component::renson::entity::sensor::ventilation_level::state::level3%]", + "level4": "[%key:component::renson::entity::sensor::ventilation_level::state::level4%]", + "breeze": "[%key:component::renson::entity::sensor::ventilation_level::state::breeze%]", + "holiday": "[%key:component::renson::entity::sensor::ventilation_level::state::holiday%]" + } + }, + "breeze_temperature": { + "name": "Breeze temperature" + }, + "breeze_level": { + "name": "Breeze level", + "state": { + "off": "[%key:common::state::off%]", + "level1": "[%key:component::renson::entity::sensor::ventilation_level::state::level1%]", + "level2": "[%key:component::renson::entity::sensor::ventilation_level::state::level2%]", + "level3": "[%key:component::renson::entity::sensor::ventilation_level::state::level3%]", + "level4": "[%key:component::renson::entity::sensor::ventilation_level::state::level4%]", + "breeze": "[%key:component::renson::entity::sensor::ventilation_level::state::breeze%]" + } + }, + "start_day_time": { + "name": "Start day time" + }, + "start_night_time": { + "name": "Start night time" + }, + "day_pollution_level": { + "name": "Day pollution level", + "state": { + "level1": "[%key:component::renson::entity::sensor::ventilation_level::state::level1%]", + "level2": "[%key:component::renson::entity::sensor::ventilation_level::state::level2%]", + "level3": "[%key:component::renson::entity::sensor::ventilation_level::state::level3%]", + "level4": "[%key:component::renson::entity::sensor::ventilation_level::state::level4%]" + } + }, + "night_pollution_level": { + "name": "Night pollution level", + "state": { + "level1": "[%key:component::renson::entity::sensor::ventilation_level::state::level1%]", + "level2": "[%key:component::renson::entity::sensor::ventilation_level::state::level2%]", + "level3": "[%key:component::renson::entity::sensor::ventilation_level::state::level3%]", + "level4": "[%key:component::renson::entity::sensor::ventilation_level::state::level4%]" + } + }, + "co2_threshold": { + "name": "CO2 threshold" + }, + "co2_hysteresis": { + "name": "CO2 hysteresis" + }, + "bypass_activation_temperature": { + "name": "Bypass activation temperature" + }, + "bypass_level": { + "name": "Bypass level" + } + } } } From 3183ce7608740ced18119bffc6d884d2d3712b17 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 23 Jul 2023 20:16:46 +0200 Subject: [PATCH 0811/1009] Add doorbell event support to alexa (#97092) --- homeassistant/components/alexa/entities.py | 21 ++++++++ .../components/alexa/state_report.py | 7 ++- tests/components/alexa/test_smart_home.py | 51 +++++++++++++++---- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 6ed071b8b9ee64..9a805b43c4f7bb 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -14,6 +14,7 @@ camera, climate, cover, + event, fan, group, humidifier, @@ -527,6 +528,26 @@ def interfaces(self) -> Generator[AlexaCapability, None, None]: yield Alexa(self.entity) +@ENTITY_ADAPTERS.register(event.DOMAIN) +class EventCapabilities(AlexaEntity): + """Class to represent doorbel event capabilities.""" + + def default_display_categories(self) -> list[str] | None: + """Return the display categories for this entity.""" + attrs = self.entity.attributes + device_class: event.EventDeviceClass | None = attrs.get(ATTR_DEVICE_CLASS) + if device_class == event.EventDeviceClass.DOORBELL: + return [DisplayCategory.DOORBELL] + return None + + def interfaces(self) -> Generator[AlexaCapability, None, None]: + """Yield the supported interfaces.""" + if self.default_display_categories() is not None: + yield AlexaDoorbellEventSource(self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.entity) + + @ENTITY_ADAPTERS.register(light.DOMAIN) class LightCapabilities(AlexaEntity): """Class to represent Light capabilities.""" diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index ebab3bcee8ce1d..04bb561560f75e 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -10,6 +10,7 @@ import aiohttp import async_timeout +from homeassistant.components import event from homeassistant.const import MATCH_ALL, STATE_ON from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -91,8 +92,10 @@ async def async_entity_state_listener( return if should_doorbell: - if new_state.state == STATE_ON and ( - old_state is None or old_state.state != STATE_ON + if ( + new_state.domain == event.DOMAIN + or new_state.state == STATE_ON + and (old_state is None or old_state.state != STATE_ON) ): await async_send_doorbell_event_message( hass, smart_home_config, alexa_changed_entity diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index a2dcdedd4706e6..477e7884e4fe91 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1,4 +1,5 @@ """Test for smart home alexa support.""" +from typing import Any from unittest.mock import patch import pytest @@ -2136,18 +2137,48 @@ async def test_forced_motion_sensor(hass: HomeAssistant) -> None: properties.assert_equal("Alexa.EndpointHealth", "connectivity", {"value": "OK"}) -async def test_doorbell_sensor(hass: HomeAssistant) -> None: - """Test doorbell sensor discovery.""" - device = ( - "binary_sensor.test_doorbell", - "off", - {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, - ) +@pytest.mark.parametrize( + ("device", "endpoint_id", "friendly_name", "display_category"), + [ + ( + ( + "binary_sensor.test_doorbell", + "off", + {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, + ), + "binary_sensor#test_doorbell", + "Test Doorbell Sensor", + "DOORBELL", + ), + ( + ( + "event.test_doorbell", + None, + { + "friendly_name": "Test Doorbell Event", + "event_types": ["press"], + "device_class": "doorbell", + }, + ), + "event#test_doorbell", + "Test Doorbell Event", + "DOORBELL", + ), + ], +) +async def test_doorbell_event( + hass: HomeAssistant, + device: tuple[str, str, dict[str, Any]], + endpoint_id: str, + friendly_name: str, + display_category: str, +) -> None: + """Test doorbell event/sensor discovery.""" appliance = await discovery_test(device, hass) - assert appliance["endpointId"] == "binary_sensor#test_doorbell" - assert appliance["displayCategories"][0] == "DOORBELL" - assert appliance["friendlyName"] == "Test Doorbell Sensor" + assert appliance["endpointId"] == endpoint_id + assert appliance["displayCategories"][0] == display_category + assert appliance["friendlyName"] == friendly_name capabilities = assert_endpoint_capabilities( appliance, "Alexa.DoorbellEventSource", "Alexa.EndpointHealth", "Alexa" From bfbdebd0f75507b44847dfeb5b05b33c720d6bf8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 23 Jul 2023 20:21:57 +0200 Subject: [PATCH 0812/1009] Add entity translations to uPnP (#96763) --- .../components/upnp/binary_sensor.py | 2 +- homeassistant/components/upnp/entity.py | 2 +- homeassistant/components/upnp/sensor.py | 22 +++++----- homeassistant/components/upnp/strings.json | 42 +++++++++++++++++++ tests/components/upnp/test_sensor.py | 24 +++++------ 5 files changed, 67 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/upnp/binary_sensor.py b/homeassistant/components/upnp/binary_sensor.py index 030b0aa322ea7b..0ab8962077b9a5 100644 --- a/homeassistant/components/upnp/binary_sensor.py +++ b/homeassistant/components/upnp/binary_sensor.py @@ -28,7 +28,7 @@ class UpnpBinarySensorEntityDescription( SENSOR_DESCRIPTIONS: tuple[UpnpBinarySensorEntityDescription, ...] = ( UpnpBinarySensorEntityDescription( key=WAN_STATUS, - name="wan status", + translation_key="wan_status", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, ), diff --git a/homeassistant/components/upnp/entity.py b/homeassistant/components/upnp/entity.py index cd39609d9d517a..a3d7709a5d5d79 100644 --- a/homeassistant/components/upnp/entity.py +++ b/homeassistant/components/upnp/entity.py @@ -25,6 +25,7 @@ class UpnpEntity(CoordinatorEntity[UpnpDataUpdateCoordinator]): """Base class for UPnP/IGD entities.""" entity_description: UpnpEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -35,7 +36,6 @@ def __init__( super().__init__(coordinator) self._device = coordinator.device self.entity_description = entity_description - self._attr_name = f"{coordinator.device.name} {entity_description.name}" self._attr_unique_id = f"{coordinator.device.original_udn}_{entity_description.unique_id or entity_description.key}" self._attr_device_info = DeviceInfo( connections=coordinator.device_entry.connections, diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 6f0fe340f304a6..46d748f6939e5b 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -49,7 +49,7 @@ class UpnpSensorEntityDescription(UpnpEntityDescription, SensorEntityDescription SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( UpnpSensorEntityDescription( key=BYTES_RECEIVED, - name=f"{UnitOfInformation.BYTES} received", + translation_key="data_received", icon="mdi:server-network", device_class=SensorDeviceClass.DATA_SIZE, native_unit_of_measurement=UnitOfInformation.BYTES, @@ -59,7 +59,7 @@ class UpnpSensorEntityDescription(UpnpEntityDescription, SensorEntityDescription ), UpnpSensorEntityDescription( key=BYTES_SENT, - name=f"{UnitOfInformation.BYTES} sent", + translation_key="data_sent", icon="mdi:server-network", device_class=SensorDeviceClass.DATA_SIZE, native_unit_of_measurement=UnitOfInformation.BYTES, @@ -69,7 +69,7 @@ class UpnpSensorEntityDescription(UpnpEntityDescription, SensorEntityDescription ), UpnpSensorEntityDescription( key=PACKETS_RECEIVED, - name=f"{DATA_PACKETS} received", + translation_key="packets_received", icon="mdi:server-network", native_unit_of_measurement=DATA_PACKETS, entity_registry_enabled_default=False, @@ -78,7 +78,7 @@ class UpnpSensorEntityDescription(UpnpEntityDescription, SensorEntityDescription ), UpnpSensorEntityDescription( key=PACKETS_SENT, - name=f"{DATA_PACKETS} sent", + translation_key="packets_sent", icon="mdi:server-network", native_unit_of_measurement=DATA_PACKETS, entity_registry_enabled_default=False, @@ -87,13 +87,13 @@ class UpnpSensorEntityDescription(UpnpEntityDescription, SensorEntityDescription ), UpnpSensorEntityDescription( key=ROUTER_IP, - name="External IP", + translation_key="external_ip", icon="mdi:server-network", entity_category=EntityCategory.DIAGNOSTIC, ), UpnpSensorEntityDescription( key=ROUTER_UPTIME, - name="Uptime", + translation_key="uptime", icon="mdi:server-network", native_unit_of_measurement=UnitOfTime.SECONDS, entity_registry_enabled_default=False, @@ -102,16 +102,16 @@ class UpnpSensorEntityDescription(UpnpEntityDescription, SensorEntityDescription ), UpnpSensorEntityDescription( key=WAN_STATUS, - name="wan status", + translation_key="wan_status", icon="mdi:server-network", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), UpnpSensorEntityDescription( key=BYTES_RECEIVED, + translation_key="download_speed", value_key=KIBIBYTES_PER_SEC_RECEIVED, unique_id="KiB/sec_received", - name=f"{UnitOfDataRate.KIBIBYTES_PER_SECOND} received", icon="mdi:server-network", device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.KIBIBYTES_PER_SECOND, @@ -120,9 +120,9 @@ class UpnpSensorEntityDescription(UpnpEntityDescription, SensorEntityDescription ), UpnpSensorEntityDescription( key=BYTES_SENT, + translation_key="upload_speed", value_key=KIBIBYTES_PER_SEC_SENT, unique_id="KiB/sec_sent", - name=f"{UnitOfDataRate.KIBIBYTES_PER_SECOND} sent", icon="mdi:server-network", device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.KIBIBYTES_PER_SECOND, @@ -131,9 +131,9 @@ class UpnpSensorEntityDescription(UpnpEntityDescription, SensorEntityDescription ), UpnpSensorEntityDescription( key=PACKETS_RECEIVED, + translation_key="packet_download_speed", value_key=PACKETS_PER_SEC_RECEIVED, unique_id="packets/sec_received", - name=f"{DATA_RATE_PACKETS_PER_SECOND} received", icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_PACKETS_PER_SECOND, entity_registry_enabled_default=False, @@ -142,9 +142,9 @@ class UpnpSensorEntityDescription(UpnpEntityDescription, SensorEntityDescription ), UpnpSensorEntityDescription( key=PACKETS_SENT, + translation_key="packet_upload_speed", value_key=PACKETS_PER_SEC_SENT, unique_id="packets/sec_sent", - name=f"{DATA_RATE_PACKETS_PER_SECOND} sent", icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_PACKETS_PER_SECOND, entity_registry_enabled_default=False, diff --git a/homeassistant/components/upnp/strings.json b/homeassistant/components/upnp/strings.json index ea052f0b45a3b9..7ce1798c3511ec 100644 --- a/homeassistant/components/upnp/strings.json +++ b/homeassistant/components/upnp/strings.json @@ -25,5 +25,47 @@ } } } + }, + "entity": { + "binary_sensor": { + "wan_status": { + "name": "[%key:component::upnp::entity::sensor::wan_status::name%]" + } + }, + "sensor": { + "data_received": { + "name": "Data received" + }, + "data_sent": { + "name": "Data sent" + }, + "packets_received": { + "name": "Packets received" + }, + "packets_sent": { + "name": "Packets sent" + }, + "external_ip": { + "name": "External IP" + }, + "uptime": { + "name": "Uptime" + }, + "packet_download_speed": { + "name": "Packet download speed" + }, + "packet_upload_speed": { + "name": "Packet upload speed" + }, + "download_speed": { + "name": "Download speed" + }, + "upload_speed": { + "name": "Upload speed" + }, + "wan_status": { + "name": "WAN status" + } + } } } diff --git a/tests/components/upnp/test_sensor.py b/tests/components/upnp/test_sensor.py index f29d7ac9276c3f..7dfbb144b01cad 100644 --- a/tests/components/upnp/test_sensor.py +++ b/tests/components/upnp/test_sensor.py @@ -16,16 +16,16 @@ async def test_upnp_sensors( ) -> None: """Test sensors.""" # First poll. - assert hass.states.get("sensor.mock_name_b_received").state == "0" - assert hass.states.get("sensor.mock_name_b_sent").state == "0" + assert hass.states.get("sensor.mock_name_data_received").state == "0" + assert hass.states.get("sensor.mock_name_data_sent").state == "0" assert hass.states.get("sensor.mock_name_packets_received").state == "0" assert hass.states.get("sensor.mock_name_packets_sent").state == "0" assert hass.states.get("sensor.mock_name_external_ip").state == "8.9.10.11" assert hass.states.get("sensor.mock_name_wan_status").state == "Connected" - assert hass.states.get("sensor.mock_name_kib_s_received").state == "unknown" - assert hass.states.get("sensor.mock_name_kib_s_sent").state == "unknown" - assert hass.states.get("sensor.mock_name_packets_s_received").state == "unknown" - assert hass.states.get("sensor.mock_name_packets_s_sent").state == "unknown" + assert hass.states.get("sensor.mock_name_download_speed").state == "unknown" + assert hass.states.get("sensor.mock_name_upload_speed").state == "unknown" + assert hass.states.get("sensor.mock_name_packet_download_speed").state == "unknown" + assert hass.states.get("sensor.mock_name_packet_upload_speed").state == "unknown" # Second poll. mock_igd_device: IgdDevice = mock_config_entry.igd_device @@ -51,13 +51,13 @@ async def test_upnp_sensors( async_fire_time_changed(hass, now + timedelta(seconds=DEFAULT_SCAN_INTERVAL)) await hass.async_block_till_done() - assert hass.states.get("sensor.mock_name_b_received").state == "10240" - assert hass.states.get("sensor.mock_name_b_sent").state == "20480" + assert hass.states.get("sensor.mock_name_data_received").state == "10240" + assert hass.states.get("sensor.mock_name_data_sent").state == "20480" assert hass.states.get("sensor.mock_name_packets_received").state == "30" assert hass.states.get("sensor.mock_name_packets_sent").state == "40" assert hass.states.get("sensor.mock_name_external_ip").state == "" assert hass.states.get("sensor.mock_name_wan_status").state == "Disconnected" - assert hass.states.get("sensor.mock_name_kib_s_received").state == "10.0" - assert hass.states.get("sensor.mock_name_kib_s_sent").state == "20.0" - assert hass.states.get("sensor.mock_name_packets_s_received").state == "30.0" - assert hass.states.get("sensor.mock_name_packets_s_sent").state == "40.0" + assert hass.states.get("sensor.mock_name_download_speed").state == "10.0" + assert hass.states.get("sensor.mock_name_upload_speed").state == "20.0" + assert hass.states.get("sensor.mock_name_packet_download_speed").state == "30.0" + assert hass.states.get("sensor.mock_name_packet_upload_speed").state == "40.0" From 7ed66706b9b2e3cf582a2ad9ce35179455123440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emil=20Ahlb=C3=A4ck?= Date: Sun, 23 Jul 2023 20:26:07 +0200 Subject: [PATCH 0813/1009] Add "enqueue" parameter to spotify integration (#90687) Co-authored-by: Franck Nijhof --- .../components/spotify/media_player.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index de48e8fae20a4a..41d27b686725f2 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -12,7 +12,9 @@ from yarl import URL from homeassistant.components.media_player import ( + ATTR_MEDIA_ENQUEUE, BrowseMedia, + MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, @@ -336,6 +338,10 @@ def play_media( """Play media.""" media_type = media_type.removeprefix(MEDIA_PLAYER_PREFIX) + enqueue: MediaPlayerEnqueue = kwargs.get( + ATTR_MEDIA_ENQUEUE, MediaPlayerEnqueue.REPLACE + ) + kwargs = {} # Spotify can't handle URI's with query strings or anchors @@ -357,6 +363,17 @@ def play_media( ): kwargs["device_id"] = self.data.devices.data[0].get("id") + if enqueue == MediaPlayerEnqueue.ADD: + if media_type not in { + MediaType.TRACK, + MediaType.EPISODE, + MediaType.MUSIC, + }: + raise ValueError( + f"Media type {media_type} is not supported when enqueue is ADD" + ) + return self.data.client.add_to_queue(media_id, kwargs.get("device_id")) + self.data.client.start_playback(**kwargs) @spotify_exception_handler From dc3d0fc7a7d9667ff3c9c289a42f83eb2ffce729 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Jul 2023 13:27:09 -0500 Subject: [PATCH 0814/1009] Bump flux_led to 1.0.1 (#97094) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 224d98d92bf00b..689f984722dd6d 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -54,5 +54,5 @@ "iot_class": "local_push", "loggers": ["flux_led"], "quality_scale": "platinum", - "requirements": ["flux-led==1.0.0"] + "requirements": ["flux-led==1.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 729cd3b478a735..af9ecf757f5e8b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -794,7 +794,7 @@ fjaraskupan==2.2.0 flipr-api==1.5.0 # homeassistant.components.flux_led -flux-led==1.0.0 +flux-led==1.0.1 # homeassistant.components.homekit # homeassistant.components.recorder diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0620b82e14b899..e87fbf33bd772a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -622,7 +622,7 @@ fjaraskupan==2.2.0 flipr-api==1.5.0 # homeassistant.components.flux_led -flux-led==1.0.0 +flux-led==1.0.1 # homeassistant.components.homekit # homeassistant.components.recorder From fab3c5b849082c8fbe66f4af507fd7acbdc77bf1 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 23 Jul 2023 20:30:15 +0200 Subject: [PATCH 0815/1009] Fix imap cleanup error on abort (#97097) --- homeassistant/components/imap/coordinator.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index c3cd21e6b2dda0..b644c300979c54 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -298,7 +298,8 @@ async def _cleanup(self, log_error: bool = False) -> None: except (AioImapException, asyncio.TimeoutError): if log_error: _LOGGER.debug("Error while cleaning up imap connection") - self.imap_client = None + finally: + self.imap_client = None async def shutdown(self, *_: Any) -> None: """Close resources.""" @@ -370,7 +371,6 @@ async def async_start(self) -> None: 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() @@ -412,9 +412,6 @@ async def _async_wait_push_loop(self) -> None: 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)", @@ -423,9 +420,6 @@ async def _async_wait_push_loop(self) -> None: ) await self._cleanup() await asyncio.sleep(BACKOFF_TIME) - finally: - if cleanup: - await self._cleanup() async def shutdown(self, *_: Any) -> None: """Close resources.""" From 910c897ceb4b067c58e37eab5c00add4b9ffb505 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 23 Jul 2023 20:34:47 +0200 Subject: [PATCH 0816/1009] Fix typo hidrogen to hydrogen (#97096) --- homeassistant/components/number/const.py | 2 +- homeassistant/components/sensor/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 461139a15eab2b..3d7dba15b0ec6c 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -217,7 +217,7 @@ class NumberDeviceClass(StrEnum): """ PH = "ph" - """Potential hidrogen (acidity/alkalinity). + """Potential hydrogen (acidity/alkalinity). Unit of measurement: Unitless """ diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 13c9293daa71da..6e4f355f852d78 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -248,7 +248,7 @@ class SensorDeviceClass(StrEnum): """ PH = "ph" - """Potential hidrogen (acidity/alkalinity). + """Potential hydrogen (acidity/alkalinity). Unit of measurement: Unitless """ From c61c6474ddec0e2bf6ff13ae24ba209bb9b3095d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 23 Jul 2023 19:33:47 +0000 Subject: [PATCH 0817/1009] Add frequency and N current sensors for Shelly Pro 3EM (#97082) --- homeassistant/components/shelly/sensor.py | 41 +++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 3e9dfaad923aa0..c4fc4b66f373ec 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -27,6 +27,7 @@ UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, + UnitOfFrequency, UnitOfPower, UnitOfTemperature, ) @@ -496,6 +497,16 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), + "n_current": RpcSensorDescription( + key="em", + sub_key="n_current", + name="Phase N current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + available=lambda status: status["n_current"] is not None, + entity_registry_enabled_default=False, + ), "total_current": RpcSensorDescription( key="em", sub_key="total_current", @@ -610,6 +621,36 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, ), + "a_freq": RpcSensorDescription( + key="em", + sub_key="a_freq", + name="Phase A frequency", + native_unit_of_measurement=UnitOfFrequency.HERTZ, + suggested_display_precision=0, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + "b_freq": RpcSensorDescription( + key="em", + sub_key="b_freq", + name="Phase B frequency", + native_unit_of_measurement=UnitOfFrequency.HERTZ, + suggested_display_precision=0, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + "c_freq": RpcSensorDescription( + key="em", + sub_key="c_freq", + name="Phase C frequency", + native_unit_of_measurement=UnitOfFrequency.HERTZ, + suggested_display_precision=0, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), "illuminance": RpcSensorDescription( key="illuminance", sub_key="lux", From 61f3f38c99834971446cbcedebac4368392af364 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 23 Jul 2023 21:34:32 +0200 Subject: [PATCH 0818/1009] State attributes translation for Sensibo (#85239) --- homeassistant/components/sensibo/strings.json | 200 +++++++++++++++++- 1 file changed, 191 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json index 6946b21761c37a..38ae94d4fa30c8 100644 --- a/homeassistant/components/sensibo/strings.json +++ b/homeassistant/components/sensibo/strings.json @@ -71,7 +71,7 @@ "horizontalswing": { "name": "Horizontal swing", "state": { - "stopped": "Stopped", + "stopped": "[%key:common::state::off%]", "fixedleft": "Fixed left", "fixedcenterleft": "Fixed center left", "fixedcenter": "Fixed center", @@ -83,7 +83,7 @@ } }, "light": { - "name": "Light", + "name": "[%key:component::light::title%]", "state": { "on": "[%key:common::state::on%]", "dim": "Dim", @@ -115,17 +115,179 @@ "name": "Temperature feels like" }, "climate_react_low": { - "name": "Climate React low temperature threshold" + "name": "Climate React low temperature threshold", + "state_attributes": { + "fanlevel": { + "name": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::name%]", + "state": { + "auto": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::auto%]", + "high": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::high%]", + "low": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::low%]", + "medium": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::medium%]", + "medium_high": "Medium high", + "quiet": "Quiet" + } + }, + "horizontalswing": { + "name": "Horizontal swing", + "state": { + "stopped": "[%key:common::state::off%]", + "fixedleft": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedleft%]", + "fixedcenterleft": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedcenterleft%]", + "fixedcenter": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedcenter%]", + "fixedcenterright": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedcenterright%]", + "fixedright": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedright%]", + "fixedleftright": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedleftright%]", + "rangecenter": "[%key:component::sensibo::entity::select::horizontalswing::state::rangecenter%]", + "rangefull": "[%key:component::sensibo::entity::select::horizontalswing::state::rangefull%]" + } + }, + "light": { + "name": "[%key:component::light::title%]", + "state": { + "on": "[%key:common::state::on%]", + "dim": "[%key:component::sensibo::entity::select::light::state::dim%]", + "off": "[%key:common::state::off%]" + } + }, + "mode": { + "name": "Mode", + "state": { + "off": "[%key:common::state::off%]", + "heat": "[%key:component::climate::entity_component::_::state::heat%]", + "cool": "[%key:component::climate::entity_component::_::state::cool%]", + "heat_cool": "[%key:component::climate::entity_component::_::state::heat_cool%]", + "auto": "[%key:component::climate::entity_component::_::state::auto%]", + "dry": "[%key:component::climate::entity_component::_::state::dry%]", + "fan_only": "[%key:component::climate::entity_component::_::state::fan_only%]" + } + }, + "on": { + "name": "[%key:common::state::on%]", + "state": { + "true": "[%key:common::state::on%]", + "false": "[%key:common::state::off%]" + } + }, + "swing": { + "name": "[%key:component::climate::entity_component::_::state_attributes::swing_mode::name%]", + "state": { + "both": "[%key:component::climate::entity_component::_::state_attributes::swing_mode::state::both%]", + "fixedbottom": "Fixed bottom", + "fixedmiddle": "Fixed middle", + "fixedmiddlebottom": "Fixed middle bottom", + "fixedmiddletop": "Fixed middle top", + "fixedtop": "Fixed top", + "horizontal": "Horizontal", + "rangebottom": "Range bottom", + "rangefull": "Range full", + "rangemiddle": "Range middle", + "rangetop": "Range top", + "stopped": "[%key:common::state::off%]" + } + }, + "targettemperature": { + "name": "[%key:component::climate::entity_component::_::state_attributes::temperature::name%]" + }, + "temperatureunit": { + "name": "Temperature unit", + "state": { + "c": "Celsius", + "f": "Fahrenheit" + } + } + } }, "climate_react_high": { - "name": "Climate React high temperature threshold" + "name": "Climate React high temperature threshold", + "state_attributes": { + "fanlevel": { + "name": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::name%]", + "state": { + "auto": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::auto%]", + "high": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::high%]", + "low": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::low%]", + "medium": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::medium%]", + "medium_high": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::fanlevel::state::medium_high%]", + "quiet": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::fanlevel::state::quiet%]" + } + }, + "horizontalswing": { + "name": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::name%]", + "state": { + "stopped": "[%key:common::state::off%]", + "fixedleft": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedleft%]", + "fixedcenterleft": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedcenterleft%]", + "fixedcenter": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedcenter%]", + "fixedcenterright": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedcenterright%]", + "fixedright": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedright%]", + "fixedleftright": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedleftright%]", + "rangecenter": "[%key:component::sensibo::entity::select::horizontalswing::state::rangecenter%]", + "rangefull": "[%key:component::sensibo::entity::select::horizontalswing::state::rangefull%]" + } + }, + "light": { + "name": "[%key:component::light::title%]", + "state": { + "on": "[%key:common::state::on%]", + "dim": "[%key:component::sensibo::entity::select::light::state::dim%]", + "off": "[%key:common::state::off%]" + } + }, + "mode": { + "name": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::mode::name%]", + "state": { + "off": "[%key:common::state::off%]", + "heat": "[%key:component::climate::entity_component::_::state::heat%]", + "cool": "[%key:component::climate::entity_component::_::state::cool%]", + "heat_cool": "[%key:component::climate::entity_component::_::state::heat_cool%]", + "auto": "[%key:component::climate::entity_component::_::state::auto%]", + "dry": "[%key:component::climate::entity_component::_::state::dry%]", + "fan_only": "[%key:component::climate::entity_component::_::state::fan_only%]" + } + }, + "on": { + "name": "[%key:common::state::on%]", + "state": { + "true": "[%key:common::state::on%]", + "false": "[%key:common::state::off%]" + } + }, + "swing": { + "name": "[%key:component::climate::entity_component::_::state_attributes::swing_mode::name%]", + "state": { + "both": "[%key:component::climate::entity_component::_::state_attributes::swing_mode::state::both%]", + "fixedbottom": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::swing::state::fixedbottom%]", + "fixedmiddle": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::swing::state::fixedmiddle%]", + "fixedmiddlebottom": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::swing::state::fixedmiddlebottom%]", + "fixedmiddletop": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::swing::state::fixedmiddletop%]", + "fixedtop": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::swing::state::fixedtop%]", + "horizontal": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::swing::state::horizontal%]", + "rangebottom": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::swing::state::rangebottom%]", + "rangefull": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::swing::state::rangefull%]", + "rangemiddle": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::swing::state::rangemiddle%]", + "rangetop": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::swing::state::rangetop%]", + "stopped": "[%key:common::state::off%]" + } + }, + "targettemperature": { + "name": "[%key:component::climate::entity_component::_::state_attributes::temperature::name%]" + }, + "temperatureunit": { + "name": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::temperatureunit::name%]", + "state": { + "c": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::temperatureunit::state::c%]", + "f": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::temperatureunit::state::f%]" + } + } + } }, "smart_type": { "name": "Climate React type", "state": { - "temperature": "Temperature", - "feelslike": "Feels like", - "humidity": "Humidity" + "temperature": "[%key:component::sensor::entity_component::temperature::name%]", + "feelslike": "[%key:component::sensibo::entity::switch::climate_react_switch::state_attributes::type::state::feelslike%]", + "humidity": "[%key:component::sensor::entity_component::humidity::name%]" } }, "airq_tvoc": { @@ -143,10 +305,30 @@ }, "switch": { "timer_on_switch": { - "name": "Timer" + "name": "Timer", + "state_attributes": { + "id": { "name": "Id" }, + "turn_on": { + "name": "Turns on", + "state": { + "true": "[%key:common::state::on%]", + "false": "[%key:common::state::off%]" + } + } + } }, "climate_react_switch": { - "name": "Climate React" + "name": "Climate React", + "state_attributes": { + "type": { + "name": "Type", + "state": { + "feelslike": "Feels like", + "temperature": "[%key:component::sensor::entity_component::temperature::name%]", + "humidity": "[%key:component::sensor::entity_component::humidity::name%]" + } + } + } }, "pure_boost_switch": { "name": "Pure Boost" From 860a37aa65d863112cac2fbe4599f8d7d9c793d1 Mon Sep 17 00:00:00 2001 From: Antoni Czaplicki <56671347+Antoni-Czaplicki@users.noreply.github.com> Date: Sun, 23 Jul 2023 21:40:56 +0200 Subject: [PATCH 0819/1009] Fix vulcan integration (#91401) --- homeassistant/components/vulcan/calendar.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vulcan/calendar.py b/homeassistant/components/vulcan/calendar.py index debf1f4ea0dfeb..791ae9ee7c4603 100644 --- a/homeassistant/components/vulcan/calendar.py +++ b/homeassistant/components/vulcan/calendar.py @@ -3,6 +3,7 @@ from datetime import date, datetime, timedelta import logging +from zoneinfo import ZoneInfo from aiohttp import ClientConnectorError from vulcan import UnauthorizedCertificateException @@ -107,8 +108,12 @@ async def async_get_events( event_list = [] for item in events: event = CalendarEvent( - start=datetime.combine(item["date"], item["time"].from_), - end=datetime.combine(item["date"], item["time"].to), + start=datetime.combine(item["date"], item["time"].from_).astimezone( + ZoneInfo("Europe/Warsaw") + ), + end=datetime.combine(item["date"], item["time"].to).astimezone( + ZoneInfo("Europe/Warsaw") + ), summary=item["lesson"], location=item["room"], description=item["teacher"], @@ -156,8 +161,12 @@ async def async_update(self) -> None: ), ) self._event = CalendarEvent( - start=datetime.combine(new_event["date"], new_event["time"].from_), - end=datetime.combine(new_event["date"], new_event["time"].to), + start=datetime.combine( + new_event["date"], new_event["time"].from_ + ).astimezone(ZoneInfo("Europe/Warsaw")), + end=datetime.combine(new_event["date"], new_event["time"].to).astimezone( + ZoneInfo("Europe/Warsaw") + ), summary=new_event["lesson"], location=new_event["room"], description=new_event["teacher"], From bdd253328d01a9ea001c703d6ab9ffd40f527264 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 23 Jul 2023 21:51:54 +0200 Subject: [PATCH 0820/1009] Add generic Event class (#97071) --- .../components/bayesian/binary_sensor.py | 13 +++-- homeassistant/components/bthome/logbook.py | 12 ++-- homeassistant/helpers/event.py | 55 ++++++++++++------- homeassistant/helpers/typing.py | 11 +++- 4 files changed, 58 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 06baef1bd0e0c7..43411e9ec0daa2 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -33,6 +33,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( + EventStateChangedData, TrackTemplate, TrackTemplateResult, TrackTemplateResultInfo, @@ -41,7 +42,7 @@ ) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.template import Template, result_as_boolean -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType from . import DOMAIN, PLATFORMS from .const import ( @@ -231,16 +232,20 @@ async def async_added_to_hass(self) -> None: """ @callback - def async_threshold_sensor_state_listener(event: Event) -> None: + def async_threshold_sensor_state_listener( + event: EventType[EventStateChangedData], + ) -> None: """Handle sensor state changes. When a state changes, we must update our list of current observations, then calculate the new probability. """ - entity: str = event.data[CONF_ENTITY_ID] + entity_id = event.data["entity_id"] - self.current_observations.update(self._record_entity_observations(entity)) + self.current_observations.update( + self._record_entity_observations(entity_id) + ) self.async_set_context(event.context) self._recalculate_and_write_state() diff --git a/homeassistant/components/bthome/logbook.py b/homeassistant/components/bthome/logbook.py index 703ad671799f82..4111777375ddd0 100644 --- a/homeassistant/components/bthome/logbook.py +++ b/homeassistant/components/bthome/logbook.py @@ -2,11 +2,11 @@ from __future__ import annotations from collections.abc import Callable -from typing import TYPE_CHECKING, cast from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import async_get +from homeassistant.helpers.typing import EventType from .const import ( BTHOME_BLE_EVENT, @@ -18,17 +18,17 @@ @callback def async_describe_events( hass: HomeAssistant, - async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None], + async_describe_event: Callable[ + [str, str, Callable[[EventType[BTHomeBleEvent]], dict[str, str]]], None + ], ) -> None: """Describe logbook events.""" dr = async_get(hass) @callback - def async_describe_bthome_event(event: Event) -> dict[str, str]: + def async_describe_bthome_event(event: EventType[BTHomeBleEvent]) -> dict[str, str]: """Describe bthome logbook event.""" data = event.data - if TYPE_CHECKING: - data = cast(BTHomeBleEvent, data) # type: ignore[assignment] device = dr.async_get(data["device_id"]) name = device and device.name or f'BTHome {data["address"]}' if properties := data["event_properties"]: diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index b7254c5c347538..004a71fa8103af 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -10,7 +10,7 @@ import logging from random import randint import time -from typing import Any, Concatenate, ParamSpec, cast +from typing import Any, Concatenate, ParamSpec, TypedDict, cast import attr @@ -41,7 +41,7 @@ from .ratelimit import KeyedRateLimit from .sun import get_astral_event_next from .template import RenderInfo, Template, result_as_boolean -from .typing import TemplateVarsType +from .typing import EventType, TemplateVarsType TRACK_STATE_CHANGE_CALLBACKS = "track_state_change_callbacks" TRACK_STATE_CHANGE_LISTENER = "track_state_change_listener" @@ -117,6 +117,14 @@ class TrackTemplateResult: result: Any +class EventStateChangedData(TypedDict): + """EventStateChanged data.""" + + entity_id: str + old_state: State | None + new_state: State | None + + def threaded_listener_factory( async_factory: Callable[Concatenate[HomeAssistant, _P], Any] ) -> Callable[Concatenate[HomeAssistant, _P], CALLBACK_TYPE]: @@ -183,36 +191,38 @@ def async_track_state_change( job = HassJob(action, f"track state change {entity_ids} {from_state} {to_state}") @callback - def state_change_filter(event: Event) -> bool: + def state_change_filter(event: EventType[EventStateChangedData]) -> bool: """Handle specific state changes.""" if from_state is not None: - if (old_state := event.data.get("old_state")) is not None: - old_state = old_state.state + old_state_str: str | None = None + if (old_state := event.data["old_state"]) is not None: + old_state_str = old_state.state - if not match_from_state(old_state): + if not match_from_state(old_state_str): return False if to_state is not None: - if (new_state := event.data.get("new_state")) is not None: - new_state = new_state.state + new_state_str: str | None = None + if (new_state := event.data["new_state"]) is not None: + new_state_str = new_state.state - if not match_to_state(new_state): + if not match_to_state(new_state_str): return False return True @callback - def state_change_dispatcher(event: Event) -> None: + def state_change_dispatcher(event: EventType[EventStateChangedData]) -> None: """Handle specific state changes.""" hass.async_run_hass_job( job, event.data["entity_id"], - event.data.get("old_state"), + event.data["old_state"], event.data["new_state"], ) @callback - def state_change_listener(event: Event) -> None: + def state_change_listener(event: EventType[EventStateChangedData]) -> None: """Handle specific state changes.""" if not state_change_filter(event): return @@ -231,7 +241,7 @@ def state_change_listener(event: Event) -> None: return async_track_state_change_event(hass, entity_ids, state_change_listener) return hass.bus.async_listen( - EVENT_STATE_CHANGED, state_change_dispatcher, event_filter=state_change_filter + EVENT_STATE_CHANGED, state_change_dispatcher, event_filter=state_change_filter # type: ignore[arg-type] ) @@ -242,7 +252,7 @@ def state_change_listener(event: Event) -> None: def async_track_state_change_event( hass: HomeAssistant, entity_ids: str | Iterable[str], - action: Callable[[Event], Any], + action: Callable[[EventType[EventStateChangedData]], Any], ) -> CALLBACK_TYPE: """Track specific state change events indexed by entity_id. @@ -263,8 +273,8 @@ def async_track_state_change_event( @callback def _async_dispatch_entity_id_event( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event], Any]]], - event: Event, + callbacks: dict[str, list[HassJob[[EventType[EventStateChangedData]], Any]]], + event: EventType[EventStateChangedData], ) -> None: """Dispatch to listeners.""" if not (callbacks_list := callbacks.get(event.data["entity_id"])): @@ -282,7 +292,9 @@ def _async_dispatch_entity_id_event( @callback def _async_state_change_filter( - hass: HomeAssistant, callbacks: dict[str, list[HassJob[[Event], Any]]], event: Event + hass: HomeAssistant, + callbacks: dict[str, list[HassJob[[EventType[EventStateChangedData]], Any]]], + event: EventType[EventStateChangedData], ) -> bool: """Filter state changes by entity_id.""" return event.data["entity_id"] in callbacks @@ -292,7 +304,7 @@ def _async_state_change_filter( def _async_track_state_change_event( hass: HomeAssistant, entity_ids: str | Iterable[str], - action: Callable[[Event], Any], + action: Callable[[EventType[EventStateChangedData]], Any], ) -> CALLBACK_TYPE: """async_track_state_change_event without lowercasing.""" return _async_track_event( @@ -301,9 +313,10 @@ def _async_track_state_change_event( TRACK_STATE_CHANGE_CALLBACKS, TRACK_STATE_CHANGE_LISTENER, EVENT_STATE_CHANGED, - _async_dispatch_entity_id_event, - _async_state_change_filter, - action, + # Remove type ignores when _async_track_event uses EventType + _async_dispatch_entity_id_event, # type: ignore[arg-type] + _async_state_change_filter, # type: ignore[arg-type] + action, # type: ignore[arg-type] ) diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 5a76fd262a818f..9e3f9de34fa041 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -1,10 +1,12 @@ """Typing Helpers for Home Assistant.""" from collections.abc import Mapping from enum import Enum -from typing import Any +from typing import Any, Generic, TypeVar import homeassistant.core +_DataT = TypeVar("_DataT") + GPSType = tuple[float, float] ConfigType = dict[str, Any] ContextType = homeassistant.core.Context @@ -32,5 +34,10 @@ class UndefinedType(Enum): # that may rely on them. # In due time they will be removed. HomeAssistantType = homeassistant.core.HomeAssistant -EventType = homeassistant.core.Event ServiceCallType = homeassistant.core.ServiceCall + + +class EventType(homeassistant.core.Event, Generic[_DataT]): + """Generic Event class to better type data.""" + + data: _DataT # type: ignore[assignment] From 86708b5590ebf79ba00895c2ae4f245b43ceee2c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 23 Jul 2023 22:00:26 +0200 Subject: [PATCH 0821/1009] Update ruff to v0.0.280 (#97102) --- .pre-commit-config.yaml | 2 +- .../components/assist_pipeline/websocket_api.py | 2 +- .../components/bluesound/media_player.py | 2 +- homeassistant/components/calendar/__init__.py | 2 +- homeassistant/components/flipr/binary_sensor.py | 6 +++--- homeassistant/components/freedompro/switch.py | 2 +- .../homematicip_cloud/generic_entity.py | 4 ++-- homeassistant/components/motioneye/__init__.py | 5 +---- homeassistant/components/netatmo/camera.py | 4 +--- homeassistant/components/plex/services.py | 4 ++-- homeassistant/components/rachio/binary_sensor.py | 5 +---- homeassistant/components/recorder/migration.py | 16 ++-------------- .../components/thermoworks_smoke/sensor.py | 4 +--- homeassistant/components/wemo/wemo_device.py | 2 +- requirements_test_pre_commit.txt | 2 +- tests/components/mqtt/test_discovery.py | 4 +--- tests/components/python_script/test_init.py | 8 ++------ tests/components/starline/test_config_flow.py | 4 +--- tests/components/tellduslive/test_config_flow.py | 2 +- tests/components/zha/zha_devices_list.py | 5 ----- 20 files changed, 25 insertions(+), 60 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9db1f2ae2e7e2d..d1cae2b0fadd5f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.277 + rev: v0.0.280 hooks: - id: ruff args: diff --git a/homeassistant/components/assist_pipeline/websocket_api.py b/homeassistant/components/assist_pipeline/websocket_api.py index ea3aacf43a4124..4e6d44a8868d38 100644 --- a/homeassistant/components/assist_pipeline/websocket_api.py +++ b/homeassistant/components/assist_pipeline/websocket_api.py @@ -111,7 +111,7 @@ async def websocket_run( if start_stage == PipelineStage.STT: # Audio pipeline that will receive audio as binary websocket messages - audio_queue: "asyncio.Queue[bytes]" = asyncio.Queue() + audio_queue: asyncio.Queue[bytes] = asyncio.Queue() incoming_sample_rate = msg["input"]["sample_rate"] async def stt_stream() -> AsyncGenerator[bytes, None]: diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 69e115470ad013..91984cf6247ded 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -694,7 +694,7 @@ def source_list(self): for source in [ x for x in self._services_items - if x["type"] == "LocalMusic" or x["type"] == "RadioService" + if x["type"] in ("LocalMusic", "RadioService") ]: sources.append(source["title"]) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index d56b2b0ddfae67..c85f0d2bff1d0f 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -455,7 +455,7 @@ def extract_offset(summary: str, offset_prefix: str) -> tuple[str, datetime.time if search and search.group(1): time = search.group(1) if ":" not in time: - if time[0] == "+" or time[0] == "-": + if time[0] in ("+", "-"): time = f"{time[0]}0:{time[1:]}" else: time = f"0:{time}" diff --git a/homeassistant/components/flipr/binary_sensor.py b/homeassistant/components/flipr/binary_sensor.py index 76385167d385dc..0597145c2da1dc 100644 --- a/homeassistant/components/flipr/binary_sensor.py +++ b/homeassistant/components/flipr/binary_sensor.py @@ -47,7 +47,7 @@ class FliprBinarySensor(FliprEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the binary sensor is on in case of a Problem is detected.""" - return ( - self.coordinator.data[self.entity_description.key] == "TooLow" - or self.coordinator.data[self.entity_description.key] == "TooHigh" + return self.coordinator.data[self.entity_description.key] in ( + "TooLow", + "TooHigh", ) diff --git a/homeassistant/components/freedompro/switch.py b/homeassistant/components/freedompro/switch.py index 97f0a968cff4f2..7313be1920c989 100644 --- a/homeassistant/components/freedompro/switch.py +++ b/homeassistant/components/freedompro/switch.py @@ -26,7 +26,7 @@ async def async_setup_entry( async_add_entities( Device(hass, api_key, device, coordinator) for device in coordinator.data - if device["type"] == "switch" or device["type"] == "outlet" + if device["type"] in ("switch", "outlet") ) diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index 7a6e7c18e1375d..199cbacfa15325 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -161,10 +161,10 @@ def async_remove_from_registries(self) -> None: if device_id in device_registry.devices: # This will also remove associated entities from entity registry. device_registry.async_remove_device(device_id) - else: + else: # noqa: PLR5501 # Remove from entity registry. # Only relevant for entities that do not belong to a device. - if entity_id := self.registry_entry.entity_id: # noqa: PLR5501 + if entity_id := self.registry_entry.entity_id: entity_registry = er.async_get(self.hass) if entity_id in entity_registry.entities: entity_registry.async_remove(entity_id) diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index b936497cfc67ae..2876a4d49a105d 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -169,10 +169,7 @@ def async_generate_motioneye_webhook( ) -> str | None: """Generate the full local URL for a webhook_id.""" try: - return "{}{}".format( - get_url(hass, allow_cloud=False), - async_generate_path(webhook_id), - ) + return f"{get_url(hass, allow_cloud=False)}{async_generate_path(webhook_id)}" except NoURLAvailableError: _LOGGER.warning( "Unable to get Home Assistant URL. Have you set the internal and/or " diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 01c459acaea41f..7fab99a6f393a8 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -200,9 +200,7 @@ async def stream_source(self) -> str: await self._camera.async_update_camera_urls() if self._camera.local_url: - return "{}/live/files/{}/index.m3u8".format( - self._camera.local_url, self._quality - ) + return f"{self._camera.local_url}/live/files/{self._quality}/index.m3u8" return f"{self._camera.vpn_url}/live/files/{self._quality}/index.m3u8" @callback diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index 10d005d1043f06..39d41369a4bbd3 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -143,9 +143,9 @@ def process_plex_payload( content = plex_url.path server_id = plex_url.host plex_server = get_plex_server(hass, plex_server_id=server_id) - else: + else: # noqa: PLR5501 # Handle legacy payloads without server_id in URL host position - if plex_url.host == "search": # noqa: PLR5501 + if plex_url.host == "search": content = {} else: content = int(plex_url.host) # type: ignore[arg-type] diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index 294931b7538534..f1c515d37f7995 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -109,10 +109,7 @@ def icon(self) -> str: @callback def _async_handle_update(self, *args, **kwargs) -> None: """Handle an update to the state of this sensor.""" - if ( - args[0][0][KEY_SUBTYPE] == SUBTYPE_ONLINE - or args[0][0][KEY_SUBTYPE] == SUBTYPE_COLD_REBOOT - ): + if args[0][0][KEY_SUBTYPE] in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT): self._state = True elif args[0][0][KEY_SUBTYPE] == SUBTYPE_OFFLINE: self._state = False diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 33d8c7b5e67bc7..8fe1d0482e98b6 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -423,13 +423,7 @@ def _add_columns( with session_scope(session=session_maker()) as session: try: connection = session.connection() - connection.execute( - text( - "ALTER TABLE {table} {column_def}".format( - table=table_name, column_def=column_def - ) - ) - ) + connection.execute(text(f"ALTER TABLE {table_name} {column_def}")) except (InternalError, OperationalError, ProgrammingError) as err: raise_if_exception_missing_str(err, ["already exists", "duplicate"]) _LOGGER.warning( @@ -498,13 +492,7 @@ def _modify_columns( with session_scope(session=session_maker()) as session: try: connection = session.connection() - connection.execute( - text( - "ALTER TABLE {table} {column_def}".format( - table=table_name, column_def=column_def - ) - ) - ) + connection.execute(text(f"ALTER TABLE {table_name} {column_def}")) except (InternalError, OperationalError): _LOGGER.exception( "Could not modify column %s in table %s", column_def, table_name diff --git a/homeassistant/components/thermoworks_smoke/sensor.py b/homeassistant/components/thermoworks_smoke/sensor.py index fbbfdef6f02a4c..4b4878fa1c8811 100644 --- a/homeassistant/components/thermoworks_smoke/sensor.py +++ b/homeassistant/components/thermoworks_smoke/sensor.py @@ -111,9 +111,7 @@ def __init__(self, sensor_type, serial, mgr): self.type = sensor_type self.serial = serial self.mgr = mgr - self._attr_name = "{name} {sensor}".format( - name=mgr.name(serial), sensor=SENSOR_TYPES[sensor_type] - ) + self._attr_name = f"{mgr.name(serial)} {SENSOR_TYPES[sensor_type]}" self._attr_native_unit_of_measurement = UnitOfTemperature.FAHRENHEIT self._attr_unique_id = f"{serial}-{sensor_type}" self._attr_device_class = SensorDeviceClass.TEMPERATURE diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py index abb8aa186c9093..c85bc9fd47329b 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -35,7 +35,7 @@ _LOGGER = logging.getLogger(__name__) # Literal values must match options.error keys from strings.json. -ErrorStringKey = Literal["long_press_requires_subscription"] +ErrorStringKey = Literal["long_press_requires_subscription"] # noqa: F821 # Literal values must match options.step.init.data keys from strings.json. OptionsFieldKey = Literal["enable_subscription", "enable_long_press"] diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 28b51fcb447b9b..e91cbe1ff6271a 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -2,5 +2,5 @@ black==23.7.0 codespell==2.2.2 -ruff==0.0.277 +ruff==0.0.280 yamllint==1.32.0 diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index d3b8a145df7d8c..f51d469bde76b8 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1304,9 +1304,7 @@ async def test_missing_discover_abbreviations( and match[0] not in ABBREVIATIONS_WHITE_LIST ): missing.append( - "{}: no abbreviation for {} ({})".format( - fil, match[1], match[0] - ) + f"{fil}: no abbreviation for {match[1]} ({match[0]})" ) assert not missing diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index 767b0bca7427af..9326869b272974 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -357,9 +357,7 @@ async def test_service_descriptions(hass: HomeAssistant) -> None: " example: 'This is a test of python_script.hello'" ) services_yaml1 = { - "{}/{}/services.yaml".format( - hass.config.config_dir, FOLDER - ): service_descriptions1 + f"{hass.config.config_dir}/{FOLDER}/services.yaml": service_descriptions1 } with patch( @@ -408,9 +406,7 @@ async def test_service_descriptions(hass: HomeAssistant) -> None: " example: 'This is a test of python_script.hello2'" ) services_yaml2 = { - "{}/{}/services.yaml".format( - hass.config.config_dir, FOLDER - ): service_descriptions2 + f"{hass.config.config_dir}/{FOLDER}/services.yaml": service_descriptions2 } with patch( diff --git a/tests/components/starline/test_config_flow.py b/tests/components/starline/test_config_flow.py index c659ca5c585e93..4277f01037f144 100644 --- a/tests/components/starline/test_config_flow.py +++ b/tests/components/starline/test_config_flow.py @@ -37,9 +37,7 @@ async def test_flow_works(hass: HomeAssistant) -> None: cookies={"slnet": TEST_APP_SLNET}, ) mock.get( - "https://developer.starline.ru/json/v2/user/{}/user_info".format( - TEST_APP_UID - ), + f"https://developer.starline.ru/json/v2/user/{TEST_APP_UID}/user_info", text='{"code": 200, "devices": [{"device_id": "123", "imei": "123", "alias": "123", "battery": "123", "ctemp": "123", "etemp": "123", "fw_version": "123", "gsm_lvl": "123", "phone": "123", "status": "1", "ts_activity": "123", "typename": "123", "balance": {}, "car_state": {}, "car_alr_state": {}, "functions": [], "position": {}}], "shared_devices": []}', ) diff --git a/tests/components/tellduslive/test_config_flow.py b/tests/components/tellduslive/test_config_flow.py index 0eaadae4931738..de284bb8c16a47 100644 --- a/tests/components/tellduslive/test_config_flow.py +++ b/tests/components/tellduslive/test_config_flow.py @@ -261,4 +261,4 @@ async def test_discovery_already_configured( flow.context = {"source": SOURCE_DISCOVERY} with pytest.raises(data_entry_flow.AbortFlow): - result = await flow.async_step_discovery(["some-host", ""]) + await flow.async_step_discovery(["some-host", ""]) diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 4ccf7323148b60..bba5ee124ba5b7 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -3013,11 +3013,6 @@ DEV_SIG_ENT_MAP_CLASS: "Illuminance", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_illuminance", }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_battery", - }, ("sensor", "00:11:22:33:44:55:66:77-1-2"): { DEV_SIG_CLUSTER_HANDLERS: ["device_temperature"], DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", From 8abf8726c6e670b471eef2a1783ec87c149c814e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 23 Jul 2023 22:27:03 +0200 Subject: [PATCH 0822/1009] Update Home Assistant base image to 2023.07.0 (#97103) --- build.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.yaml b/build.yaml index a181e9d154872b..882fa31f121c0a 100644 --- a/build.yaml +++ b/build.yaml @@ -1,10 +1,10 @@ image: ghcr.io/home-assistant/{arch}-homeassistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.06.1 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.06.1 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.06.1 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.06.1 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.06.1 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.07.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.07.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.07.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.07.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.07.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io From 9f551c0469cfad3168e75eb9aedb8bfc59862e86 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sun, 23 Jul 2023 22:38:21 +0200 Subject: [PATCH 0823/1009] Bump async-upnp-client to 0.34.1 (#97105) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 322cd1e4d2bc54..350ea692338d1d 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "iot_class": "local_push", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.33.2", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.34.1", "getmac==0.8.2"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 227a343a7a49ae..9aabc3cea5e984 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dms", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["async-upnp-client==0.33.2"], + "requirements": ["async-upnp-client==0.34.1"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 9d00282d8daa9b..d32e71c71c05bc 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -39,7 +39,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.6.0", "wakeonlan==2.1.0", - "async-upnp-client==0.33.2" + "async-upnp-client==0.34.1" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index caae5801b21aa7..61b6b05d9d6873 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_push", "loggers": ["async_upnp_client"], "quality_scale": "internal", - "requirements": ["async-upnp-client==0.33.2"] + "requirements": ["async-upnp-client==0.34.1"] } diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 8112726607e211..4b4f0358bb9680 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -8,7 +8,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.33.2", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.34.1", "getmac==0.8.2"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index cf1bafe24fb225..2f66bf836ea4da 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -17,7 +17,7 @@ "iot_class": "local_push", "loggers": ["async_upnp_client", "yeelight"], "quality_scale": "platinum", - "requirements": ["yeelight==0.7.11", "async-upnp-client==0.33.2"], + "requirements": ["yeelight==0.7.11", "async-upnp-client==0.34.1"], "zeroconf": [ { "type": "_miio._udp.local.", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index aa0aceb7365950..5b8853bafb4dd1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -3,7 +3,7 @@ aiohttp-cors==0.7.0 aiohttp==3.8.5 astral==2.2 async-timeout==4.0.2 -async-upnp-client==0.33.2 +async-upnp-client==0.34.1 atomicwrites-homeassistant==1.4.1 attrs==22.2.0 awesomeversion==22.9.0 diff --git a/requirements_all.txt b/requirements_all.txt index af9ecf757f5e8b..ba451aca05cce7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -440,7 +440,7 @@ asterisk-mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.33.2 +async-upnp-client==0.34.1 # homeassistant.components.esphome async_interrupt==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e87fbf33bd772a..77ed4016645fd9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ arcam-fmj==1.4.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.33.2 +async-upnp-client==0.34.1 # homeassistant.components.esphome async_interrupt==1.1.1 From 38e3e20f746909491b842160782492040fe2e831 Mon Sep 17 00:00:00 2001 From: Jeef Date: Sun, 23 Jul 2023 15:11:07 -0600 Subject: [PATCH 0824/1009] Add Low Battery binary_sensor to Flume (#94914) Co-authored-by: Franck Nijhof --- homeassistant/components/flume/binary_sensor.py | 7 +++++++ homeassistant/components/flume/const.py | 1 + 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/flume/binary_sensor.py b/homeassistant/components/flume/binary_sensor.py index 453e259bf4668e..c912c3419d7b41 100644 --- a/homeassistant/components/flume/binary_sensor.py +++ b/homeassistant/components/flume/binary_sensor.py @@ -25,6 +25,7 @@ KEY_DEVICE_TYPE, NOTIFICATION_HIGH_FLOW, NOTIFICATION_LEAK_DETECTED, + NOTIFICATION_LOW_BATTERY, ) from .coordinator import ( FlumeDeviceConnectionUpdateCoordinator, @@ -67,6 +68,12 @@ class FlumeBinarySensorEntityDescription( event_rule=NOTIFICATION_HIGH_FLOW, icon="mdi:waves", ), + FlumeBinarySensorEntityDescription( + key="low_battery", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + event_rule=NOTIFICATION_LOW_BATTERY, + ), ) diff --git a/homeassistant/components/flume/const.py b/homeassistant/components/flume/const.py index 1889cca8fa50f4..9e932cce4ddfc9 100644 --- a/homeassistant/components/flume/const.py +++ b/homeassistant/components/flume/const.py @@ -47,3 +47,4 @@ BRIDGE_NOTIFICATION_KEY = "connected" BRIDGE_NOTIFICATION_RULE = "Bridge Disconnection" NOTIFICATION_LEAK_DETECTED = "Flume Smart Leak Alert" +NOTIFICATION_LOW_BATTERY = "Low Battery" From 30058297cf80db2cf22caf1a52df8b02df97a011 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 23 Jul 2023 23:19:24 +0200 Subject: [PATCH 0825/1009] Migrate backported StrEnum to built-in StrEnum (#97101) --- homeassistant/backports/enum.py | 39 ++++++------------- .../components/alarm_control_panel/const.py | 4 +- .../components/assist_pipeline/pipeline.py | 2 +- .../components/assist_pipeline/vad.py | 3 +- .../components/binary_sensor/__init__.py | 2 +- homeassistant/components/braviatv/const.py | 3 +- homeassistant/components/button/__init__.py | 2 +- homeassistant/components/camera/const.py | 3 +- homeassistant/components/climate/const.py | 4 +- homeassistant/components/cover/__init__.py | 3 +- .../components/device_tracker/const.py | 3 +- homeassistant/components/diagnostics/const.py | 2 +- homeassistant/components/dlna_dms/dms.py | 2 +- homeassistant/components/event/__init__.py | 2 +- homeassistant/components/fritz/const.py | 2 +- .../fritzbox_callmonitor/config_flow.py | 2 +- .../components/fritzbox_callmonitor/const.py | 2 +- .../components/fritzbox_callmonitor/sensor.py | 2 +- homeassistant/components/hassio/const.py | 2 +- .../components/humidifier/__init__.py | 2 +- homeassistant/components/humidifier/const.py | 4 +- .../components/image_processing/__init__.py | 2 +- homeassistant/components/light/__init__.py | 3 +- homeassistant/components/logger/helpers.py | 2 +- .../components/media_player/__init__.py | 2 +- .../components/media_player/const.py | 4 +- homeassistant/components/mqtt/models.py | 2 +- homeassistant/components/number/const.py | 2 +- .../persistent_notification/__init__.py | 2 +- homeassistant/components/qnap_qsw/entity.py | 2 +- homeassistant/components/rainmachine/util.py | 2 +- homeassistant/components/recorder/const.py | 3 +- homeassistant/components/sensor/const.py | 2 +- homeassistant/components/shelly/const.py | 3 +- .../components/spotify/browse_media.py | 2 +- homeassistant/components/stookwijzer/const.py | 3 +- homeassistant/components/switch/__init__.py | 2 +- homeassistant/components/switchbot/const.py | 4 +- homeassistant/components/text/__init__.py | 2 +- .../components/tuya/alarm_control_panel.py | 3 +- homeassistant/components/tuya/const.py | 2 +- homeassistant/components/update/__init__.py | 2 +- homeassistant/components/wallbox/const.py | 2 +- homeassistant/components/withings/common.py | 3 +- homeassistant/components/withings/const.py | 2 +- .../components/zwave_js/discovery.py | 2 +- homeassistant/components/zwave_me/const.py | 3 +- homeassistant/config_entries.py | 3 +- homeassistant/const.py | 3 +- homeassistant/core.py | 5 +-- homeassistant/data_entry_flow.py | 2 +- homeassistant/helpers/config_validation.py | 3 +- homeassistant/helpers/device_registry.py | 2 +- homeassistant/helpers/entity_registry.py | 2 +- homeassistant/helpers/issue_registry.py | 2 +- homeassistant/helpers/selector.py | 3 +- homeassistant/util/ssl.py | 3 +- pylint/plugins/hass_imports.py | 6 +++ tests/backports/__init__.py | 1 - tests/backports/test_enum.py | 35 ----------------- tests/components/esphome/test_enum_mapper.py | 3 +- .../components/samsungtv/test_media_player.py | 2 +- tests/components/utility_meter/test_sensor.py | 2 +- tests/util/test_enum.py | 3 +- 64 files changed, 83 insertions(+), 150 deletions(-) delete mode 100644 tests/backports/__init__.py delete mode 100644 tests/backports/test_enum.py diff --git a/homeassistant/backports/enum.py b/homeassistant/backports/enum.py index 33cafe3b1dd2f7..871244b4567568 100644 --- a/homeassistant/backports/enum.py +++ b/homeassistant/backports/enum.py @@ -1,32 +1,15 @@ -"""Enum backports from standard lib.""" -from __future__ import annotations - -from enum import Enum -from typing import Any, Self - +"""Enum backports from standard lib. -class StrEnum(str, Enum): - """Partial backport of Python 3.11's StrEnum for our basic use cases.""" +This file contained the backport of the StrEnum of Python 3.11. - value: str - - def __new__(cls, value: str, *args: Any, **kwargs: Any) -> Self: - """Create a new StrEnum instance.""" - if not isinstance(value, str): - raise TypeError(f"{value!r} is not a string") - return super().__new__(cls, value, *args, **kwargs) - - def __str__(self) -> str: - """Return self.value.""" - return self.value +Since we have dropped support for Python 3.10, we can remove this backport. +This file is kept for now to avoid breaking custom components that might +import it. +""" +from __future__ import annotations - @staticmethod - def _generate_next_value_( - name: str, start: int, count: int, last_values: list[Any] - ) -> Any: - """Make `auto()` explicitly unsupported. +from enum import StrEnum - We may revisit this when it's very clear that Python 3.11's - `StrEnum.auto()` behavior will no longer change. - """ - raise TypeError("auto() is not supported by this implementation") +__all__ = [ + "StrEnum", +] diff --git a/homeassistant/components/alarm_control_panel/const.py b/homeassistant/components/alarm_control_panel/const.py index e6e628f834d5aa..f14a1ce66e0743 100644 --- a/homeassistant/components/alarm_control_panel/const.py +++ b/homeassistant/components/alarm_control_panel/const.py @@ -1,9 +1,7 @@ """Provides the constants needed for component.""" -from enum import IntFlag +from enum import IntFlag, StrEnum from typing import Final -from homeassistant.backports.enum import StrEnum - DOMAIN: Final = "alarm_control_panel" ATTR_CHANGED_BY: Final = "changed_by" diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 891fc639feee15..1be9ddbb14fb8e 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -4,12 +4,12 @@ import asyncio from collections.abc import AsyncIterable, Callable, Iterable from dataclasses import asdict, dataclass, field +from enum import StrEnum import logging from typing import Any, cast import voluptuous as vol -from homeassistant.backports.enum import StrEnum from homeassistant.components import conversation, media_source, stt, tts, websocket_api from homeassistant.components.tts.media_source import ( generate_media_source_id as tts_generate_media_source_id, diff --git a/homeassistant/components/assist_pipeline/vad.py b/homeassistant/components/assist_pipeline/vad.py index a737490f22f18a..cb19811d6501db 100644 --- a/homeassistant/components/assist_pipeline/vad.py +++ b/homeassistant/components/assist_pipeline/vad.py @@ -2,11 +2,10 @@ from __future__ import annotations from dataclasses import dataclass, field +from enum import StrEnum import webrtcvad -from homeassistant.backports.enum import StrEnum - _SAMPLE_RATE = 16000 diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 1c2d6d779fba47..79e20c6f571492 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -3,12 +3,12 @@ from dataclasses import dataclass from datetime import timedelta +from enum import StrEnum import logging from typing import Literal, final import voluptuous as vol -from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/braviatv/const.py b/homeassistant/components/braviatv/const.py index 5925a97422a577..34b621802f9eba 100644 --- a/homeassistant/components/braviatv/const.py +++ b/homeassistant/components/braviatv/const.py @@ -1,10 +1,9 @@ """Constants for Bravia TV integration.""" from __future__ import annotations +from enum import StrEnum from typing import Final -from homeassistant.backports.enum import StrEnum - ATTR_CID: Final = "cid" ATTR_MAC: Final = "macAddr" ATTR_MANUFACTURER: Final = "Sony" diff --git a/homeassistant/components/button/__init__.py b/homeassistant/components/button/__init__.py index 0e2790a2e85b4a..901acdcdec1989 100644 --- a/homeassistant/components/button/__init__.py +++ b/homeassistant/components/button/__init__.py @@ -3,12 +3,12 @@ from dataclasses import dataclass from datetime import datetime, timedelta +from enum import StrEnum import logging from typing import final import voluptuous as vol -from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 diff --git a/homeassistant/components/camera/const.py b/homeassistant/components/camera/const.py index ab5832e48ab49e..f745f60b51ace6 100644 --- a/homeassistant/components/camera/const.py +++ b/homeassistant/components/camera/const.py @@ -1,8 +1,7 @@ """Constants for Camera component.""" +from enum import StrEnum from typing import Final -from homeassistant.backports.enum import StrEnum - DOMAIN: Final = "camera" DATA_CAMERA_PREFS: Final = "camera_prefs" diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index 41d4646aeae718..23c76c151d76bc 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -1,8 +1,6 @@ """Provides the constants needed for component.""" -from enum import IntFlag - -from homeassistant.backports.enum import StrEnum +from enum import IntFlag, StrEnum class HVACMode(StrEnum): diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index a3965552b16246..354b972e2b78fb 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -4,14 +4,13 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta -from enum import IntFlag +from enum import IntFlag, StrEnum import functools as ft import logging from typing import Any, ParamSpec, TypeVar, final import voluptuous as vol -from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( SERVICE_CLOSE_COVER, diff --git a/homeassistant/components/device_tracker/const.py b/homeassistant/components/device_tracker/const.py index ad68472d9b0d61..3a0b0afd7c9fbf 100644 --- a/homeassistant/components/device_tracker/const.py +++ b/homeassistant/components/device_tracker/const.py @@ -2,11 +2,10 @@ from __future__ import annotations from datetime import timedelta +from enum import StrEnum import logging from typing import Final -from homeassistant.backports.enum import StrEnum - LOGGER: Final = logging.getLogger(__package__) DOMAIN: Final = "device_tracker" diff --git a/homeassistant/components/diagnostics/const.py b/homeassistant/components/diagnostics/const.py index 0d07abde2bd5bc..20f97be1eb121e 100644 --- a/homeassistant/components/diagnostics/const.py +++ b/homeassistant/components/diagnostics/const.py @@ -1,5 +1,5 @@ """Constants for the Diagnostics integration.""" -from homeassistant.backports.enum import StrEnum +from enum import StrEnum DOMAIN = "diagnostics" diff --git a/homeassistant/components/dlna_dms/dms.py b/homeassistant/components/dlna_dms/dms.py index 8fc55830c63425..6352d98da3cced 100644 --- a/homeassistant/components/dlna_dms/dms.py +++ b/homeassistant/components/dlna_dms/dms.py @@ -4,6 +4,7 @@ import asyncio from collections.abc import Callable, Coroutine from dataclasses import dataclass +from enum import StrEnum import functools from typing import Any, TypeVar, cast @@ -15,7 +16,6 @@ from async_upnp_client.profiles.dlna import ContentDirectoryErrorCode, DmsDevice from didl_lite import didl_lite -from homeassistant.backports.enum import StrEnum from homeassistant.backports.functools import cached_property from homeassistant.components import ssdp from homeassistant.components.media_player import BrowseError, MediaClass diff --git a/homeassistant/components/event/__init__.py b/homeassistant/components/event/__init__.py index a98a3fa6c3f522..98dd6036bc9586 100644 --- a/homeassistant/components/event/__init__.py +++ b/homeassistant/components/event/__init__.py @@ -3,10 +3,10 @@ from dataclasses import asdict, dataclass from datetime import datetime, timedelta +from enum import StrEnum import logging from typing import Any, Self, final -from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index 1ce21081f9c8b5..16015ec58374e4 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -1,5 +1,6 @@ """Constants for the FRITZ!Box Tools integration.""" +from enum import StrEnum from typing import Literal from fritzconnection.core.exceptions import ( @@ -13,7 +14,6 @@ FritzServiceError, ) -from homeassistant.backports.enum import StrEnum from homeassistant.const import Platform diff --git a/homeassistant/components/fritzbox_callmonitor/config_flow.py b/homeassistant/components/fritzbox_callmonitor/config_flow.py index f7ce25c2ebe3ae..5065aa65b4d272 100644 --- a/homeassistant/components/fritzbox_callmonitor/config_flow.py +++ b/homeassistant/components/fritzbox_callmonitor/config_flow.py @@ -1,6 +1,7 @@ """Config flow for fritzbox_callmonitor.""" from __future__ import annotations +from enum import StrEnum from typing import Any, cast from fritzconnection import FritzConnection @@ -9,7 +10,6 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.backports.enum import StrEnum from homeassistant.const import ( CONF_HOST, CONF_NAME, diff --git a/homeassistant/components/fritzbox_callmonitor/const.py b/homeassistant/components/fritzbox_callmonitor/const.py index 4f224660ae9f62..75050374e52394 100644 --- a/homeassistant/components/fritzbox_callmonitor/const.py +++ b/homeassistant/components/fritzbox_callmonitor/const.py @@ -1,7 +1,7 @@ """Constants for the AVM Fritz!Box call monitor integration.""" +from enum import StrEnum from typing import Final -from homeassistant.backports.enum import StrEnum from homeassistant.const import Platform diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index ed2be40f30fb5e..adf6bd3a35a41d 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -3,6 +3,7 @@ from collections.abc import Mapping from datetime import datetime, timedelta +from enum import StrEnum import logging import queue from threading import Event as ThreadingEvent, Thread @@ -11,7 +12,6 @@ from fritzconnection.core.fritzmonitor import FritzMonitor -from homeassistant.backports.enum import StrEnum from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 2bc314f169a492..0735f2645cc716 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -1,5 +1,5 @@ """Hass.io const variables.""" -from homeassistant.backports.enum import StrEnum +from enum import StrEnum DOMAIN = "hassio" diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index 79effa6f0c2d9d..a525c626f143a5 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -3,12 +3,12 @@ from dataclasses import dataclass from datetime import timedelta +from enum import StrEnum import logging from typing import Any, final import voluptuous as vol -from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_MODE, diff --git a/homeassistant/components/humidifier/const.py b/homeassistant/components/humidifier/const.py index 35601cf2b1f04e..09c0714cbebc2a 100644 --- a/homeassistant/components/humidifier/const.py +++ b/homeassistant/components/humidifier/const.py @@ -1,7 +1,5 @@ """Provides the constants needed for component.""" -from enum import IntFlag - -from homeassistant.backports.enum import StrEnum +from enum import IntFlag, StrEnum MODE_NORMAL = "normal" MODE_ECO = "eco" diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 733a1344538a11..7640925451ac40 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -4,12 +4,12 @@ import asyncio from dataclasses import dataclass from datetime import timedelta +from enum import StrEnum import logging from typing import Any, Final, TypedDict, final import voluptuous as vol -from homeassistant.backports.enum import StrEnum from homeassistant.components.camera import Image from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 0f49ab605a7086..f7f0150bdd202e 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -5,14 +5,13 @@ import csv import dataclasses from datetime import timedelta -from enum import IntFlag +from enum import IntFlag, StrEnum import logging import os from typing import Any, Self, cast, final import voluptuous as vol -from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( SERVICE_TOGGLE, diff --git a/homeassistant/components/logger/helpers.py b/homeassistant/components/logger/helpers.py index dcd4348a561848..49996408a1d69c 100644 --- a/homeassistant/components/logger/helpers.py +++ b/homeassistant/components/logger/helpers.py @@ -5,10 +5,10 @@ from collections.abc import Mapping import contextlib from dataclasses import asdict, dataclass +from enum import StrEnum import logging from typing import Any, cast -from homeassistant.backports.enum import StrEnum from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 36512620e516fd..39b67477f9783b 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -7,6 +7,7 @@ from contextlib import suppress from dataclasses import dataclass import datetime as dt +from enum import StrEnum import functools as ft import hashlib from http import HTTPStatus @@ -22,7 +23,6 @@ import voluptuous as vol from yarl import URL -from homeassistant.backports.enum import StrEnum from homeassistant.components import websocket_api from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.websocket_api import ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 9ad7b983c7face..2c6097501531e5 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -1,7 +1,5 @@ """Provides the constants needed for component.""" -from enum import IntFlag - -from homeassistant.backports.enum import StrEnum +from enum import IntFlag, StrEnum # How long our auth signature on the content should be valid for CONTENT_AUTH_EXPIRY_TIME = 3600 * 24 diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index fb11400a312e9b..5a966a4455c914 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -7,12 +7,12 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass, field import datetime as dt +from enum import StrEnum import logging from typing import TYPE_CHECKING, Any, TypedDict import attr -from homeassistant.backports.enum import StrEnum from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import template diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 3d7dba15b0ec6c..9248d3f9e575a0 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -1,11 +1,11 @@ """Provides the constants needed for the component.""" from __future__ import annotations +from enum import StrEnum from typing import Final import voluptuous as vol -from homeassistant.backports.enum import StrEnum from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 581720c2730346..c9e8e3703dbd48 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -3,12 +3,12 @@ from collections.abc import Callable, Mapping from datetime import datetime +from enum import StrEnum import logging from typing import Any, Final, TypedDict import voluptuous as vol -from homeassistant.backports.enum import StrEnum from homeassistant.components import websocket_api from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv, singleton diff --git a/homeassistant/components/qnap_qsw/entity.py b/homeassistant/components/qnap_qsw/entity.py index 288c184984d48f..38e45457462760 100644 --- a/homeassistant/components/qnap_qsw/entity.py +++ b/homeassistant/components/qnap_qsw/entity.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +from enum import StrEnum from typing import Any from aioqsw.const import ( @@ -14,7 +15,6 @@ QSD_SYSTEM_BOARD, ) -from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL from homeassistant.core import callback diff --git a/homeassistant/components/rainmachine/util.py b/homeassistant/components/rainmachine/util.py index d4131fdb022cca..61ef1be500ab20 100644 --- a/homeassistant/components/rainmachine/util.py +++ b/homeassistant/components/rainmachine/util.py @@ -4,9 +4,9 @@ from collections.abc import Awaitable, Callable, Iterable from dataclasses import dataclass from datetime import timedelta +from enum import StrEnum from typing import Any -from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index ec5c5c984b50e3..fc7683db90127d 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -1,6 +1,7 @@ """Recorder constants.""" -from homeassistant.backports.enum import StrEnum +from enum import StrEnum + from homeassistant.const import ATTR_ATTRIBUTION, ATTR_RESTORED, ATTR_SUPPORTED_FEATURES from homeassistant.helpers.json import ( # noqa: F401 pylint: disable=unused-import JSON_DUMP, diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 6e4f355f852d78..139725ee1ab132 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -1,11 +1,11 @@ """Constants for sensor.""" from __future__ import annotations +from enum import StrEnum from typing import Final import voluptuous as vol -from homeassistant.backports.enum import StrEnum from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 608798976ba8f0..cc82f0ad700432 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -1,14 +1,13 @@ """Constants for the Shelly integration.""" from __future__ import annotations +from enum import StrEnum from logging import Logger, getLogger import re from typing import Final from awesomeversion import AwesomeVersion -from homeassistant.backports.enum import StrEnum - DOMAIN: Final = "shelly" LOGGER: Logger = getLogger(__package__) diff --git a/homeassistant/components/spotify/browse_media.py b/homeassistant/components/spotify/browse_media.py index e6a1f16eedeb52..162369fd27dda2 100644 --- a/homeassistant/components/spotify/browse_media.py +++ b/homeassistant/components/spotify/browse_media.py @@ -1,6 +1,7 @@ """Support for Spotify media browsing.""" from __future__ import annotations +from enum import StrEnum from functools import partial import logging from typing import Any @@ -8,7 +9,6 @@ from spotipy import Spotify import yarl -from homeassistant.backports.enum import StrEnum from homeassistant.components.media_player import ( BrowseError, BrowseMedia, diff --git a/homeassistant/components/stookwijzer/const.py b/homeassistant/components/stookwijzer/const.py index cdd5ac2a567edb..1a125da6a6bf7a 100644 --- a/homeassistant/components/stookwijzer/const.py +++ b/homeassistant/components/stookwijzer/const.py @@ -1,9 +1,8 @@ """Constants for the Stookwijzer integration.""" +from enum import StrEnum import logging from typing import Final -from homeassistant.backports.enum import StrEnum - DOMAIN: Final = "stookwijzer" LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 6eb2a275e1864d..bf3c3424142e04 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -3,11 +3,11 @@ from dataclasses import dataclass from datetime import timedelta +from enum import StrEnum import logging import voluptuous as vol -from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( SERVICE_TOGGLE, diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index 17e95486298579..0f7d1407fc5dde 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -1,7 +1,7 @@ """Constants for the switchbot integration.""" -from switchbot import SwitchbotModel +from enum import StrEnum -from homeassistant.backports.enum import StrEnum +from switchbot import SwitchbotModel DOMAIN = "switchbot" MANUFACTURER = "switchbot" diff --git a/homeassistant/components/text/__init__.py b/homeassistant/components/text/__init__.py index f07a672afbd195..4182b177bf6db0 100644 --- a/homeassistant/components/text/__init__.py +++ b/homeassistant/components/text/__init__.py @@ -3,13 +3,13 @@ from dataclasses import asdict, dataclass from datetime import timedelta +from enum import StrEnum import logging import re from typing import Any, final import voluptuous as vol -from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import MAX_LENGTH_STATE_STATE from homeassistant.core import HomeAssistant, ServiceCall diff --git a/homeassistant/components/tuya/alarm_control_panel.py b/homeassistant/components/tuya/alarm_control_panel.py index c2c9c207c02b0e..cd92e62b864f8e 100644 --- a/homeassistant/components/tuya/alarm_control_panel.py +++ b/homeassistant/components/tuya/alarm_control_panel.py @@ -1,9 +1,10 @@ """Support for Tuya Alarm.""" from __future__ import annotations +from enum import StrEnum + from tuya_iot import TuyaDevice, TuyaDeviceManager -from homeassistant.backports.enum import StrEnum from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntity, AlarmControlPanelEntityDescription, diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 20dc724deb9db3..acf9f8bbd2cef7 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -3,11 +3,11 @@ from collections.abc import Callable from dataclasses import dataclass, field +from enum import StrEnum import logging from tuya_iot import TuyaCloudOpenAPIEndpoint -from homeassistant.backports.enum import StrEnum from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index 13ab6d38e8a85b..b9d016295367ed 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from datetime import timedelta +from enum import StrEnum from functools import lru_cache import logging from typing import Any, Final, final @@ -10,7 +11,6 @@ from awesomeversion import AwesomeVersion, AwesomeVersionCompareException import voluptuous as vol -from homeassistant.backports.enum import StrEnum from homeassistant.components import websocket_api from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index a6f92541a10b42..9bab8232dabe0a 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -1,5 +1,5 @@ """Constants for the Wallbox integration.""" -from homeassistant.backports.enum import StrEnum +from enum import StrEnum DOMAIN = "wallbox" diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index da43ae973cddb4..9282e3977c1e80 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -6,7 +6,7 @@ from dataclasses import dataclass import datetime from datetime import timedelta -from enum import IntEnum +from enum import IntEnum, StrEnum from http import HTTPStatus import logging import re @@ -27,7 +27,6 @@ query_measure_groups, ) -from homeassistant.backports.enum import StrEnum from homeassistant.components import webhook from homeassistant.components.application_credentials import AuthImplementation from homeassistant.components.http import HomeAssistantView diff --git a/homeassistant/components/withings/const.py b/homeassistant/components/withings/const.py index 1193b6f612a5fd..02d8977c604ac7 100644 --- a/homeassistant/components/withings/const.py +++ b/homeassistant/components/withings/const.py @@ -1,5 +1,5 @@ """Constants used by the Withings component.""" -from homeassistant.backports.enum import StrEnum +from enum import StrEnum CONF_PROFILES = "profiles" CONF_USE_WEBHOOK = "use_webhook" diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 947e5157a8af38..9569ba97167e73 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -3,6 +3,7 @@ from collections.abc import Generator from dataclasses import asdict, dataclass, field +from enum import StrEnum from typing import TYPE_CHECKING, Any, cast from awesomeversion import AwesomeVersion @@ -47,7 +48,6 @@ Value as ZwaveValue, ) -from homeassistant.backports.enum import StrEnum from homeassistant.const import EntityCategory, Platform from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceEntry diff --git a/homeassistant/components/zwave_me/const.py b/homeassistant/components/zwave_me/const.py index 84d49ff7b9d751..1ec4f8d1601a97 100644 --- a/homeassistant/components/zwave_me/const.py +++ b/homeassistant/components/zwave_me/const.py @@ -1,5 +1,6 @@ """Constants for ZWaveMe.""" -from homeassistant.backports.enum import StrEnum +from enum import StrEnum + from homeassistant.const import Platform # Base component constants diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 6fa80406e61b79..15fcb9a50de624 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -6,7 +6,7 @@ from collections.abc import Callable, Coroutine, Generator, Iterable, Mapping from contextvars import ContextVar from copy import deepcopy -from enum import Enum +from enum import Enum, StrEnum import functools import logging from random import randint @@ -14,7 +14,6 @@ from typing import TYPE_CHECKING, Any, Self, TypeVar, cast from . import data_entry_flow, loader -from .backports.enum import StrEnum from .components import persistent_notification from .const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, Platform from .core import CALLBACK_TYPE, CoreState, Event, HassJob, HomeAssistant, callback diff --git a/homeassistant/const.py b/homeassistant/const.py index 85f0f4eee15169..513d72555a5be8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,10 +1,9 @@ """Constants used by Home Assistant components.""" from __future__ import annotations +from enum import StrEnum from typing import Final -from .backports.enum import StrEnum - APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 8 diff --git a/homeassistant/core.py b/homeassistant/core.py index 8bb30f5d57df02..3673f9acba5af8 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -43,7 +43,6 @@ import yarl from . import block_async_io, loader, util -from .backports.enum import StrEnum from .const import ( ATTR_DOMAIN, ATTR_FRIENDLY_NAME, @@ -133,7 +132,7 @@ ServiceResponse = JsonObjectType | None -class ConfigSource(StrEnum): +class ConfigSource(enum.StrEnum): """Source of core configuration.""" DEFAULT = "default" @@ -1669,7 +1668,7 @@ def async_set( ) -class SupportsResponse(StrEnum): +class SupportsResponse(enum.StrEnum): """Service call response configuration.""" NONE = "none" diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index c0a5860529e521..e0408a24b2e8d2 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -5,13 +5,13 @@ from collections.abc import Callable, Iterable, Mapping import copy from dataclasses import dataclass +from enum import StrEnum import logging from types import MappingProxyType from typing import Any, Required, TypedDict import voluptuous as vol -from .backports.enum import StrEnum from .core import HomeAssistant, callback from .exceptions import HomeAssistantError from .helpers.frame import report diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 8d0ee78eca7831..122fd752a84366 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -9,7 +9,7 @@ time as time_sys, timedelta, ) -from enum import Enum +from enum import Enum, StrEnum import inspect import logging from numbers import Number @@ -25,7 +25,6 @@ import voluptuous as vol import voluptuous_serialize -from homeassistant.backports.enum import StrEnum from homeassistant.const import ( ATTR_AREA_ID, ATTR_DEVICE_ID, diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index a59313ed8863cc..c65e87a2119c69 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -3,6 +3,7 @@ from collections import UserDict from collections.abc import Coroutine, ValuesView +from enum import StrEnum import logging import time from typing import TYPE_CHECKING, Any, TypeVar, cast @@ -10,7 +11,6 @@ import attr -from homeassistant.backports.enum import StrEnum from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index cabac2617c2939..5fc4609d8129e3 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -12,6 +12,7 @@ from collections import UserDict from collections.abc import Callable, Iterable, Mapping, ValuesView from datetime import datetime, timedelta +from enum import StrEnum import logging import time from typing import TYPE_CHECKING, Any, TypeVar, cast @@ -19,7 +20,6 @@ import attr import voluptuous as vol -from homeassistant.backports.enum import StrEnum from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, diff --git a/homeassistant/helpers/issue_registry.py b/homeassistant/helpers/issue_registry.py index afe2d98ed0b211..9bd6ebffadb4a0 100644 --- a/homeassistant/helpers/issue_registry.py +++ b/homeassistant/helpers/issue_registry.py @@ -3,12 +3,12 @@ import dataclasses from datetime import datetime +from enum import StrEnum import functools as ft from typing import Any, cast from awesomeversion import AwesomeVersion, AwesomeVersionStrategy -from homeassistant.backports.enum import StrEnum from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant, callback from homeassistant.util.async_ import run_callback_threadsafe diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index b97f781eaf369d..8ec8d5eac3ee17 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -2,14 +2,13 @@ from __future__ import annotations from collections.abc import Callable, Mapping, Sequence -from enum import IntFlag +from enum import IntFlag, StrEnum from functools import cache from typing import Any, Generic, Literal, Required, TypedDict, TypeVar, cast from uuid import UUID import voluptuous as vol -from homeassistant.backports.enum import StrEnum from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.util import decorator diff --git a/homeassistant/util/ssl.py b/homeassistant/util/ssl.py index 664d6f15650e32..84585d7a8c7012 100644 --- a/homeassistant/util/ssl.py +++ b/homeassistant/util/ssl.py @@ -1,13 +1,12 @@ """Helper to create SSL contexts.""" import contextlib +from enum import StrEnum from functools import cache from os import environ import ssl import certifi -from homeassistant.backports.enum import StrEnum - class SSLCipherList(StrEnum): """SSL cipher lists.""" diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index be44c4256ce676..8b3aea61ff4a1e 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -18,6 +18,12 @@ class ObsoleteImportMatch: _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { + "homeassistant.backports.enum": [ + ObsoleteImportMatch( + reason="We can now use the Python 3.11 provided enum.StrEnum instead", + constant=re.compile(r"^StrEnum$"), + ), + ], "homeassistant.components.alarm_control_panel": [ ObsoleteImportMatch( reason="replaced by AlarmControlPanelEntityFeature enum", diff --git a/tests/backports/__init__.py b/tests/backports/__init__.py deleted file mode 100644 index 3f701810a5d4a0..00000000000000 --- a/tests/backports/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The tests for the backports.""" diff --git a/tests/backports/test_enum.py b/tests/backports/test_enum.py deleted file mode 100644 index 06b876eac8d03c..00000000000000 --- a/tests/backports/test_enum.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Test Home Assistant enum utils.""" - -from enum import auto - -import pytest - -from homeassistant.backports.enum import StrEnum - - -def test_strenum() -> None: - """Test StrEnum.""" - - class TestEnum(StrEnum): - Test = "test" - - assert str(TestEnum.Test) == "test" - assert TestEnum.Test == "test" - assert TestEnum("test") is TestEnum.Test - assert TestEnum(TestEnum.Test) is TestEnum.Test - - with pytest.raises(ValueError): - TestEnum(42) - - with pytest.raises(ValueError): - TestEnum("str but unknown") - - with pytest.raises(TypeError): - - class FailEnum(StrEnum): - Test = 42 - - with pytest.raises(TypeError): - - class FailEnum2(StrEnum): - Test = auto() diff --git a/tests/components/esphome/test_enum_mapper.py b/tests/components/esphome/test_enum_mapper.py index 52b81bb383644e..a9ee52425928f1 100644 --- a/tests/components/esphome/test_enum_mapper.py +++ b/tests/components/esphome/test_enum_mapper.py @@ -1,8 +1,9 @@ """Test ESPHome enum mapper.""" +from enum import StrEnum + from aioesphomeapi import APIIntEnum -from homeassistant.backports.enum import StrEnum from homeassistant.components.esphome.enum_mapper import EsphomeEnumMapper diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 3d3077a1c6ac1a..674dea752a0644 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -694,7 +694,7 @@ async def test_device_class(hass: HomeAssistant) -> None: """Test for device_class property.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) state = hass.states.get(ENTITY_ID) - assert state.attributes[ATTR_DEVICE_CLASS] is MediaPlayerDeviceClass.TV.value + assert state.attributes[ATTR_DEVICE_CLASS] == MediaPlayerDeviceClass.TV @pytest.mark.usefixtures("rest_api") diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 5cb9e594cb211c..3d2d95fd26f3af 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -489,7 +489,7 @@ async def test_device_class( state = hass.states.get("sensor.energy_meter") assert state is not None assert state.state == "0" - assert state.attributes.get(ATTR_DEVICE_CLASS) is SensorDeviceClass.ENERGY.value + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR diff --git a/tests/util/test_enum.py b/tests/util/test_enum.py index 61e8471b9d84e1..e975960bbe0e68 100644 --- a/tests/util/test_enum.py +++ b/tests/util/test_enum.py @@ -1,10 +1,9 @@ """Test enum helpers.""" -from enum import Enum, IntEnum, IntFlag +from enum import Enum, IntEnum, IntFlag, StrEnum from typing import Any import pytest -from homeassistant.backports.enum import StrEnum from homeassistant.util.enum import try_parse_enum From 54d7ba72ee55912ba395f1588c875ea042c627d5 Mon Sep 17 00:00:00 2001 From: rale Date: Sun, 23 Jul 2023 16:20:29 -0500 Subject: [PATCH 0826/1009] Add second led control for carro smart fan (#94195) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/light.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 5c2663d251c407..b4396f617cda39 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -350,6 +350,11 @@ class TuyaLightEntityDescription(LightEntityDescription): brightness=DPCode.BRIGHT_VALUE, color_temp=DPCode.TEMP_VALUE, ), + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED, + translation_key="light_2", + brightness=DPCode.BRIGHT_VALUE_1, + ), ), } From 69d7b035e063049ef302c5c281775d477a597124 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 23 Jul 2023 23:22:04 +0200 Subject: [PATCH 0827/1009] Use EventType for more helper methods (#97107) --- homeassistant/helpers/device_registry.py | 11 +++- homeassistant/helpers/entity_registry.py | 12 +++- homeassistant/helpers/event.py | 84 +++++++++++++++++------- 3 files changed, 80 insertions(+), 27 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index c65e87a2119c69..45a4459b5d3ed8 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -6,10 +6,11 @@ from enum import StrEnum import logging import time -from typing import TYPE_CHECKING, Any, TypeVar, cast +from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar, cast from urllib.parse import urlparse import attr +from typing_extensions import NotRequired from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback @@ -96,6 +97,14 @@ class DeviceEntryDisabler(StrEnum): DEVICE_INFO_KEYS = set.union(*(itm for itm in DEVICE_INFO_TYPES.values())) +class EventDeviceRegistryUpdatedData(TypedDict): + """EventDeviceRegistryUpdated data.""" + + action: Literal["create", "remove", "update"] + device_id: str + changes: NotRequired[dict[str, Any]] + + class DeviceEntryType(StrEnum): """Device entry type.""" diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 5fc4609d8129e3..248db9d5180793 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -15,9 +15,10 @@ from enum import StrEnum import logging import time -from typing import TYPE_CHECKING, Any, TypeVar, cast +from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar, cast import attr +from typing_extensions import NotRequired import voluptuous as vol from homeassistant.const import ( @@ -107,6 +108,15 @@ class RegistryEntryHider(StrEnum): USER = "user" +class EventEntityRegistryUpdatedData(TypedDict): + """EventEntityRegistryUpdated data.""" + + action: Literal["create", "remove", "update"] + entity_id: str + changes: NotRequired[dict[str, Any]] + old_entity_id: NotRequired[str] + + EntityOptionsType = Mapping[str, Mapping[str, Any]] ReadOnlyEntityOptionsType = ReadOnlyDict[str, Mapping[str, Any]] diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 004a71fa8103af..830b610011108d 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Coroutine, Iterable, Sequence +from collections.abc import Callable, Coroutine, Iterable, Mapping, Sequence import copy from dataclasses import dataclass from datetime import datetime, timedelta @@ -10,7 +10,7 @@ import logging from random import randint import time -from typing import Any, Concatenate, ParamSpec, TypedDict, cast +from typing import Any, Concatenate, ParamSpec, TypedDict, TypeVar, cast import attr @@ -36,8 +36,14 @@ from homeassistant.util import dt as dt_util from homeassistant.util.async_ import run_callback_threadsafe -from .device_registry import EVENT_DEVICE_REGISTRY_UPDATED -from .entity_registry import EVENT_ENTITY_REGISTRY_UPDATED +from .device_registry import ( + EVENT_DEVICE_REGISTRY_UPDATED, + EventDeviceRegistryUpdatedData, +) +from .entity_registry import ( + EVENT_ENTITY_REGISTRY_UPDATED, + EventEntityRegistryUpdatedData, +) from .ratelimit import KeyedRateLimit from .sun import get_astral_event_next from .template import RenderInfo, Template, result_as_boolean @@ -67,6 +73,7 @@ RANDOM_MICROSECOND_MIN = 50000 RANDOM_MICROSECOND_MAX = 500000 +_TypedDictT = TypeVar("_TypedDictT", bound=Mapping[str, Any]) _P = ParamSpec("_P") @@ -313,10 +320,9 @@ def _async_track_state_change_event( TRACK_STATE_CHANGE_CALLBACKS, TRACK_STATE_CHANGE_LISTENER, EVENT_STATE_CHANGED, - # Remove type ignores when _async_track_event uses EventType - _async_dispatch_entity_id_event, # type: ignore[arg-type] - _async_state_change_filter, # type: ignore[arg-type] - action, # type: ignore[arg-type] + _async_dispatch_entity_id_event, + _async_state_change_filter, + action, ) @@ -351,12 +357,22 @@ def _async_track_event( listeners_key: str, event_type: str, dispatcher_callable: Callable[ - [HomeAssistant, dict[str, list[HassJob[[Event], Any]]], Event], None + [ + HomeAssistant, + dict[str, list[HassJob[[EventType[_TypedDictT]], Any]]], + EventType[_TypedDictT], + ], + None, ], filter_callable: Callable[ - [HomeAssistant, dict[str, list[HassJob[[Event], Any]]], Event], bool + [ + HomeAssistant, + dict[str, list[HassJob[[EventType[_TypedDictT]], Any]]], + EventType[_TypedDictT], + ], + bool, ], - action: Callable[[Event], None], + action: Callable[[EventType[_TypedDictT]], None], ) -> CALLBACK_TYPE: """Track an event by a specific key.""" if not keys: @@ -367,9 +383,9 @@ def _async_track_event( hass_data = hass.data - callbacks: dict[str, list[HassJob[[Event], Any]]] | None = hass_data.get( - callbacks_key - ) + callbacks: dict[ + str, list[HassJob[[EventType[_TypedDictT]], Any]] + ] | None = hass_data.get(callbacks_key) if not callbacks: callbacks = hass_data[callbacks_key] = {} @@ -395,8 +411,10 @@ def _async_track_event( @callback def _async_dispatch_old_entity_id_or_entity_id_event( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event], Any]]], - event: Event, + callbacks: dict[ + str, list[HassJob[[EventType[EventEntityRegistryUpdatedData]], Any]] + ], + event: EventType[EventEntityRegistryUpdatedData], ) -> None: """Dispatch to listeners.""" if not ( @@ -418,7 +436,11 @@ def _async_dispatch_old_entity_id_or_entity_id_event( @callback def _async_entity_registry_updated_filter( - hass: HomeAssistant, callbacks: dict[str, list[HassJob[[Event], Any]]], event: Event + hass: HomeAssistant, + callbacks: dict[ + str, list[HassJob[[EventType[EventEntityRegistryUpdatedData]], Any]] + ], + event: EventType[EventEntityRegistryUpdatedData], ) -> bool: """Filter entity registry updates by entity_id.""" return event.data.get("old_entity_id", event.data["entity_id"]) in callbacks @@ -451,7 +473,11 @@ def async_track_entity_registry_updated_event( @callback def _async_device_registry_updated_filter( - hass: HomeAssistant, callbacks: dict[str, list[HassJob[[Event], Any]]], event: Event + hass: HomeAssistant, + callbacks: dict[ + str, list[HassJob[[EventType[EventDeviceRegistryUpdatedData]], Any]] + ], + event: EventType[EventDeviceRegistryUpdatedData], ) -> bool: """Filter device registry updates by device_id.""" return event.data["device_id"] in callbacks @@ -460,8 +486,10 @@ def _async_device_registry_updated_filter( @callback def _async_dispatch_device_id_event( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[Event], Any]]], - event: Event, + callbacks: dict[ + str, list[HassJob[[EventType[EventDeviceRegistryUpdatedData]], Any]] + ], + event: EventType[EventDeviceRegistryUpdatedData], ) -> None: """Dispatch to listeners.""" if not (callbacks_list := callbacks.get(event.data["device_id"])): @@ -501,7 +529,9 @@ def async_track_device_registry_updated_event( @callback def _async_dispatch_domain_event( - hass: HomeAssistant, callbacks: dict[str, list[HassJob[[Event], Any]]], event: Event + hass: HomeAssistant, + callbacks: dict[str, list[HassJob[[EventType[EventStateChangedData]], Any]]], + event: EventType[EventStateChangedData], ) -> None: """Dispatch domain event listeners.""" domain = split_entity_id(event.data["entity_id"])[0] @@ -516,10 +546,12 @@ def _async_dispatch_domain_event( @callback def _async_domain_added_filter( - hass: HomeAssistant, callbacks: dict[str, list[HassJob[[Event], Any]]], event: Event + hass: HomeAssistant, + callbacks: dict[str, list[HassJob[[EventType[EventStateChangedData]], Any]]], + event: EventType[EventStateChangedData], ) -> bool: """Filter state changes by entity_id.""" - return event.data.get("old_state") is None and ( + return event.data["old_state"] is None and ( MATCH_ALL in callbacks or split_entity_id(event.data["entity_id"])[0] in callbacks ) @@ -558,10 +590,12 @@ def _async_track_state_added_domain( @callback def _async_domain_removed_filter( - hass: HomeAssistant, callbacks: dict[str, list[HassJob[[Event], Any]]], event: Event + hass: HomeAssistant, + callbacks: dict[str, list[HassJob[[EventType[EventStateChangedData]], Any]]], + event: EventType[EventStateChangedData], ) -> bool: """Filter state changes by entity_id.""" - return event.data.get("new_state") is None and ( + return event.data["new_state"] is None and ( MATCH_ALL in callbacks or split_entity_id(event.data["entity_id"])[0] in callbacks ) From 5e88ca23b33bf965f2e6d43fdedb6ea7033021d4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 23 Jul 2023 23:30:37 +0200 Subject: [PATCH 0828/1009] Remove the use of StateType from AccuWeather (#97109) --- homeassistant/components/accuweather/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 5a85b4a4c3876e..9eca5e772b04e0 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -25,7 +25,6 @@ ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import AccuWeatherDataUpdateCoordinator @@ -50,7 +49,7 @@ class AccuWeatherSensorDescriptionMixin: """Mixin for AccuWeather sensor.""" - value_fn: Callable[[dict[str, Any]], StateType] + value_fn: Callable[[dict[str, Any]], str | int | float | None] @dataclass @@ -59,7 +58,7 @@ class AccuWeatherSensorDescription( ): """Class describing AccuWeather sensor entities.""" - attr_fn: Callable[[dict[str, Any]], dict[str, StateType]] = lambda _: {} + attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {} FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( @@ -428,7 +427,7 @@ def __init__( self.forecast_day = forecast_day @property - def native_value(self) -> StateType: + def native_value(self) -> str | int | float | None: """Return the state.""" return self.entity_description.value_fn(self._sensor_data) From 6ad34a7f761bde74259744cd9fe5d5173c3e9a0a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 23 Jul 2023 23:51:50 +0200 Subject: [PATCH 0829/1009] Update pipdeptree to 2.11.0 (#97098) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 5972f9809c0e67..bf71ed4d255e29 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -16,7 +16,7 @@ pre-commit==3.3.3 pydantic==1.10.11 pylint==2.17.4 pylint-per-file-ignores==1.2.1 -pipdeptree==2.10.2 +pipdeptree==2.11.0 pytest-asyncio==0.21.0 pytest-aiohttp==1.0.4 pytest-cov==4.1.0 From 051929984dca26385113c7376a2951eed27ef4e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Jul 2023 17:13:48 -0500 Subject: [PATCH 0830/1009] Bump yeelight to 0.7.12 (#97112) --- homeassistant/components/yeelight/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 2f66bf836ea4da..7f5a67f42207c5 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -17,7 +17,7 @@ "iot_class": "local_push", "loggers": ["async_upnp_client", "yeelight"], "quality_scale": "platinum", - "requirements": ["yeelight==0.7.11", "async-upnp-client==0.34.1"], + "requirements": ["yeelight==0.7.12", "async-upnp-client==0.34.1"], "zeroconf": [ { "type": "_miio._udp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index ba451aca05cce7..8b81bb8410372e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2717,7 +2717,7 @@ yalexs-ble==2.2.3 yalexs==1.5.1 # homeassistant.components.yeelight -yeelight==0.7.11 +yeelight==0.7.12 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77ed4016645fd9..ba9cd91180fccf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1996,7 +1996,7 @@ yalexs-ble==2.2.3 yalexs==1.5.1 # homeassistant.components.yeelight -yeelight==0.7.11 +yeelight==0.7.12 # homeassistant.components.yolink yolink-api==0.2.9 From 2618bfc073da3ae3747f3ab6f5a81b0c64370f23 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jul 2023 01:10:03 +0200 Subject: [PATCH 0831/1009] Use EventType for state changed [core] (#97115) --- homeassistant/components/group/__init__.py | 14 ++++--- .../components/group/binary_sensor.py | 13 ++++-- homeassistant/components/group/cover.py | 18 ++++++--- homeassistant/components/group/fan.py | 18 ++++++--- homeassistant/components/group/light.py | 13 ++++-- homeassistant/components/group/lock.py | 13 ++++-- .../components/group/media_player.py | 13 +++--- homeassistant/components/group/sensor.py | 18 +++++++-- homeassistant/components/group/switch.py | 13 ++++-- .../components/history/websocket_api.py | 10 ++--- homeassistant/components/logbook/helpers.py | 16 +++++--- homeassistant/components/switch/light.py | 13 ++++-- homeassistant/components/zone/trigger.py | 17 ++++---- tests/helpers/test_event.py | 40 ++++++++++--------- 14 files changed, 145 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 4bdabdf9c96287..33df9822ac2baf 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -28,7 +28,6 @@ ) from homeassistant.core import ( CALLBACK_TYPE, - Event, HomeAssistant, ServiceCall, State, @@ -38,13 +37,16 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er, start from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) from homeassistant.helpers.integration_platform import ( async_process_integration_platform_for_component, async_process_integration_platforms, ) from homeassistant.helpers.reload import async_reload_integration_platforms -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, EventType from homeassistant.loader import bind_hass from .const import CONF_HIDE_MEMBERS @@ -737,7 +739,9 @@ async def async_will_remove_from_hass(self) -> None: """Handle removal from Home Assistant.""" self._async_stop() - async def _async_state_changed_listener(self, event: Event) -> None: + async def _async_state_changed_listener( + self, event: EventType[EventStateChangedData] + ) -> None: """Respond to a member state changing. This method must be run in the event loop. @@ -748,7 +752,7 @@ async def _async_state_changed_listener(self, event: Event) -> None: self.async_set_context(event.context) - if (new_state := event.data.get("new_state")) is None: + if (new_state := event.data["new_state"]) is None: # The state was removed from the state machine self._reset_tracked_state() diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index 112b111bdca5a4..7415ee8c60d33e 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -21,11 +21,14 @@ STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType from . import GroupEntity @@ -114,7 +117,9 @@ async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback - def async_state_changed_listener(event: Event) -> None: + def async_state_changed_listener( + event: EventType[EventStateChangedData], + ) -> None: """Handle child updates.""" self.async_set_context(event.context) self.async_defer_or_update_ha_state() diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index 38928302eb1184..784ac9a94af2ee 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -38,11 +38,14 @@ STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType from . import GroupEntity from .util import attribute_equal, reduce_attribute @@ -126,10 +129,13 @@ def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> Non self._attr_unique_id = unique_id @callback - def _update_supported_features_event(self, event: Event) -> None: + def _update_supported_features_event( + self, event: EventType[EventStateChangedData] + ) -> None: self.async_set_context(event.context) - if (entity := event.data.get("entity_id")) is not None: - self.async_update_supported_features(entity, event.data.get("new_state")) + self.async_update_supported_features( + event.data["entity_id"], event.data["new_state"] + ) @callback def async_update_supported_features( diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index 0c4c59d2454584..1fcb859f9269ab 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -35,11 +35,14 @@ STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType from . import GroupEntity from .util import ( @@ -142,10 +145,13 @@ def oscillating(self) -> bool | None: return self._oscillating @callback - def _update_supported_features_event(self, event: Event) -> None: + def _update_supported_features_event( + self, event: EventType[EventStateChangedData] + ) -> None: self.async_set_context(event.context) - if (entity := event.data.get("entity_id")) is not None: - self.async_update_supported_features(entity, event.data.get("new_state")) + self.async_update_supported_features( + event.data["entity_id"], event.data["new_state"] + ) @callback def async_update_supported_features( diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 33d240a9a4d185..e0f7974631b98b 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -44,11 +44,14 @@ STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType from . import GroupEntity from .util import find_state_attributes, mean_tuple, reduce_attribute @@ -154,7 +157,9 @@ async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback - def async_state_changed_listener(event: Event) -> None: + def async_state_changed_listener( + event: EventType[EventStateChangedData], + ) -> None: """Handle child updates.""" self.async_set_context(event.context) self.async_defer_or_update_ha_state() diff --git a/homeassistant/components/group/lock.py b/homeassistant/components/group/lock.py index 07d08c7851d5ad..233d1155c53fbd 100644 --- a/homeassistant/components/group/lock.py +++ b/homeassistant/components/group/lock.py @@ -28,11 +28,14 @@ STATE_UNKNOWN, STATE_UNLOCKING, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType from . import GroupEntity @@ -115,7 +118,9 @@ async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback - def async_state_changed_listener(event: Event) -> None: + def async_state_changed_listener( + event: EventType[EventStateChangedData], + ) -> None: """Handle child updates.""" self.async_set_context(event.context) self.async_defer_or_update_ha_state() diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index ad375630beae5a..f0d076ec130265 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -44,11 +44,14 @@ STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType KEY_ANNOUNCE = "announce" KEY_CLEAR_PLAYLIST = "clear_playlist" @@ -130,11 +133,11 @@ def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> Non } @callback - def async_on_state_change(self, event: Event) -> None: + def async_on_state_change(self, event: EventType[EventStateChangedData]) -> None: """Update supported features and state when a new state is received.""" self.async_set_context(event.context) self.async_update_supported_features( - event.data.get("entity_id"), event.data.get("new_state") # type: ignore[arg-type] + event.data["entity_id"], event.data["new_state"] ) self.async_update_state() diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py index 4c6e8dccc1ebde..d62447d99478e3 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -33,11 +33,19 @@ STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import ( + ConfigType, + DiscoveryInfoType, + EventType, + StateType, +) from . import GroupEntity from .const import CONF_IGNORE_NON_NUMERIC @@ -299,7 +307,9 @@ async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback - def async_state_changed_listener(event: Event) -> None: + def async_state_changed_listener( + event: EventType[EventStateChangedData], + ) -> None: """Handle child updates.""" self.async_set_context(event.context) self.async_defer_or_update_ha_state() diff --git a/homeassistant/components/group/switch.py b/homeassistant/components/group/switch.py index 4b6b959ba17a79..f62c805ba1d744 100644 --- a/homeassistant/components/group/switch.py +++ b/homeassistant/components/group/switch.py @@ -19,11 +19,14 @@ STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType from . import GroupEntity @@ -113,7 +116,9 @@ async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback - def async_state_changed_listener(event: Event) -> None: + def async_state_changed_listener( + event: EventType[EventStateChangedData], + ) -> None: """Handle child updates.""" self.async_set_context(event.context) self.async_defer_or_update_ha_state() diff --git a/homeassistant/components/history/websocket_api.py b/homeassistant/components/history/websocket_api.py index 93a5d272965257..24ec07b6a87813 100644 --- a/homeassistant/components/history/websocket_api.py +++ b/homeassistant/components/history/websocket_api.py @@ -30,10 +30,12 @@ valid_entity_id, ) from homeassistant.helpers.event import ( + EventStateChangedData, async_track_point_in_utc_time, async_track_state_change_event, ) from homeassistant.helpers.json import JSON_DUMP +from homeassistant.helpers.typing import EventType import homeassistant.util.dt as dt_util from .const import EVENT_COALESCE_TIME, MAX_PENDING_HISTORY_STATES @@ -373,14 +375,12 @@ def _async_subscribe_events( assert is_callback(target), "target must be a callback" @callback - def _forward_state_events_filtered(event: Event) -> None: + def _forward_state_events_filtered(event: EventType[EventStateChangedData]) -> None: """Filter state events and forward them.""" - if (new_state := event.data.get("new_state")) is None or ( - old_state := event.data.get("old_state") + if (new_state := event.data["new_state"]) is None or ( + old_state := event.data["old_state"] ) is None: return - assert isinstance(new_state, State) - assert isinstance(old_state, State) if ( (significant_changes_only or minimal_response) and new_state.state == old_state.state diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index 3a1ec971b54e26..c2ea9823535818 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -23,7 +23,11 @@ split_entity_id, ) from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import EventType from .const import ALWAYS_CONTINUOUS_DOMAINS, AUTOMATION_EVENTS, BUILT_IN_EVENTS, DOMAIN from .models import LogbookConfig @@ -184,11 +188,11 @@ def async_subscribe_events( return @callback - def _forward_state_events_filtered(event: Event) -> None: - if event.data.get("old_state") is None or event.data.get("new_state") is None: + def _forward_state_events_filtered(event: EventType[EventStateChangedData]) -> None: + if (old_state := event.data["old_state"]) is None or ( + new_state := event.data["new_state"] + ) is None: return - new_state: State = event.data["new_state"] - old_state: State = event.data["old_state"] if _is_state_filtered(ent_reg, new_state, old_state) or ( entities_filter and not entities_filter(new_state.entity_id) ): @@ -207,7 +211,7 @@ def _forward_state_events_filtered(event: Event) -> None: subscriptions.append( hass.bus.async_listen( EVENT_STATE_CHANGED, - _forward_state_events_filtered, + _forward_state_events_filtered, # type: ignore[arg-type] run_immediately=True, ) ) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index fd2a5afff1c652..ffd345cea3bfea 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -15,12 +15,15 @@ STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType from .const import DOMAIN as SWITCH_DOMAIN @@ -93,7 +96,9 @@ async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback - def async_state_changed_listener(event: Event | None = None) -> None: + def async_state_changed_listener( + event: EventType[EventStateChangedData] | None = None, + ) -> None: """Handle child updates.""" if ( state := self.hass.states.get(self._switch_entity_id) diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index 54a2784467df7d..9412c612ca2906 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -14,10 +14,8 @@ ) from homeassistant.core import ( CALLBACK_TYPE, - Event, HassJob, HomeAssistant, - State, callback, ) from homeassistant.helpers import ( @@ -26,9 +24,12 @@ entity_registry as er, location, ) -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, EventType EVENT_ENTER = "enter" EVENT_LEAVE = "leave" @@ -78,11 +79,11 @@ async def async_attach_trigger( job = HassJob(action) @callback - def zone_automation_listener(zone_event: Event) -> None: + def zone_automation_listener(zone_event: EventType[EventStateChangedData]) -> None: """Listen for state changes and calls action.""" - entity = zone_event.data.get("entity_id") - from_s: State | None = zone_event.data.get("old_state") - to_s: State | None = zone_event.data.get("new_state") + entity = zone_event.data["entity_id"] + from_s = zone_event.data["old_state"] + to_s = zone_event.data["new_state"] if ( from_s diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 3740a6b177a25e..434957dc1319bc 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -21,6 +21,7 @@ from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.helpers.event import ( + EventStateChangedData, TrackStates, TrackTemplate, TrackTemplateResult, @@ -45,6 +46,7 @@ track_point_in_utc_time, ) from homeassistant.helpers.template import Template, result_as_boolean +from homeassistant.helpers.typing import EventType from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -434,21 +436,21 @@ async def test_async_track_state_change_event(hass: HomeAssistant) -> None: multiple_entity_id_tracker = [] @ha.callback - def single_run_callback(event): - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") + def single_run_callback(event: EventType[EventStateChangedData]) -> None: + old_state = event.data["old_state"] + new_state = event.data["new_state"] single_entity_id_tracker.append((old_state, new_state)) @ha.callback - def multiple_run_callback(event): - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") + def multiple_run_callback(event: EventType[EventStateChangedData]) -> None: + old_state = event.data["old_state"] + new_state = event.data["new_state"] multiple_entity_id_tracker.append((old_state, new_state)) @ha.callback - def callback_that_throws(event): + def callback_that_throws(event: EventType[EventStateChangedData]) -> None: raise ValueError unsub_single = async_track_state_change_event( @@ -4302,16 +4304,16 @@ async def test_track_state_change_event_chain_multple_entity( tracker_unsub = [] @ha.callback - def chained_single_run_callback(event): - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") + def chained_single_run_callback(event: EventType[EventStateChangedData]) -> None: + old_state = event.data["old_state"] + new_state = event.data["new_state"] chained_tracker_called.append((old_state, new_state)) @ha.callback - def single_run_callback(event): - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") + def single_run_callback(event: EventType[EventStateChangedData]) -> None: + old_state = event.data["old_state"] + new_state = event.data["new_state"] tracker_called.append((old_state, new_state)) @@ -4356,16 +4358,16 @@ async def test_track_state_change_event_chain_single_entity( tracker_unsub = [] @ha.callback - def chained_single_run_callback(event): - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") + def chained_single_run_callback(event: EventType[EventStateChangedData]) -> None: + old_state = event.data["old_state"] + new_state = event.data["new_state"] chained_tracker_called.append((old_state, new_state)) @ha.callback - def single_run_callback(event): - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") + def single_run_callback(event: EventType[EventStateChangedData]) -> None: + old_state = event.data["old_state"] + new_state = event.data["new_state"] tracker_called.append((old_state, new_state)) From 34dcd984402b13ff5fd6d1b34242d2bcf342066b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Jul 2023 18:17:46 -0500 Subject: [PATCH 0832/1009] Only construct enum __or__ once in emulated_hue (#97114) --- homeassistant/components/emulated_hue/hue_api.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index f779f5d8e9462d..654d0bce13b784 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -110,6 +110,13 @@ {"error": {"address": "/", "description": "unauthorized user", "type": "1"}} ] +DIMMABLE_SUPPORT_FEATURES = ( + CoverEntityFeature.SET_POSITION + | FanEntityFeature.SET_SPEED + | MediaPlayerEntityFeature.VOLUME_SET + | ClimateEntityFeature.TARGET_TEMPERATURE +) + class HueUnauthorizedUser(HomeAssistantView): """Handle requests to find the emulated hue bridge.""" @@ -801,12 +808,9 @@ def state_to_json(config: Config, state: State) -> dict[str, Any]: HUE_API_STATE_BRI: state_dict[STATE_BRIGHTNESS], } ) - elif entity_features & ( - CoverEntityFeature.SET_POSITION - | FanEntityFeature.SET_SPEED - | MediaPlayerEntityFeature.VOLUME_SET - | ClimateEntityFeature.TARGET_TEMPERATURE - ) or light.brightness_supported(color_modes): + elif entity_features & DIMMABLE_SUPPORT_FEATURES or light.brightness_supported( + color_modes + ): # Dimmable light (Zigbee Device ID: 0x0100) # Supports groups, scenes, on/off and dimming retval["type"] = "Dimmable light" From f8c3aa7beccd9adf0583d66b089b8baa2e153f4a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 24 Jul 2023 01:20:23 +0200 Subject: [PATCH 0833/1009] Remove the use of StateType from Demo (#97111) --- homeassistant/components/demo/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index 26689582faef03..a1f7504762a600 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -25,7 +25,6 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import StateType from . import DOMAIN @@ -149,11 +148,11 @@ def __init__( self, unique_id: str, device_name: str | None, - state: StateType, + state: float | int | str | None, device_class: SensorDeviceClass, state_class: SensorStateClass | None, unit_of_measurement: str | None, - battery: StateType, + battery: int | None, options: list[str] | None = None, translation_key: str | None = None, ) -> None: @@ -189,7 +188,7 @@ def __init__( device_class: SensorDeviceClass, state_class: SensorStateClass | None, unit_of_measurement: str | None, - battery: StateType, + battery: int | None, suggested_entity_id: str, ) -> None: """Initialize the sensor.""" From 235b98da8ab7c379e19218fce2aea12edcaf9ec4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jul 2023 01:32:29 +0200 Subject: [PATCH 0834/1009] Use EventType for remaining event helper methods (#97121) --- homeassistant/helpers/event.py | 74 +++++++++++++++++----------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 830b610011108d..12cf58eaa2b476 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -10,12 +10,11 @@ import logging from random import randint import time -from typing import Any, Concatenate, ParamSpec, TypedDict, TypeVar, cast +from typing import Any, Concatenate, ParamSpec, TypedDict, TypeVar import attr from homeassistant.const import ( - ATTR_ENTITY_ID, EVENT_CORE_CONFIG_UPDATE, EVENT_STATE_CHANGED, MATCH_ALL, @@ -24,7 +23,6 @@ ) from homeassistant.core import ( CALLBACK_TYPE, - Event, HassJob, HomeAssistant, State, @@ -331,13 +329,13 @@ def _remove_empty_listener() -> None: """Remove a listener that does nothing.""" -@callback +@callback # type: ignore[arg-type] # mypy bug? def _remove_listener( hass: HomeAssistant, listeners_key: str, keys: Iterable[str], - job: HassJob[[Event], Any], - callbacks: dict[str, list[HassJob[[Event], Any]]], + job: HassJob[[EventType[_TypedDictT]], Any], + callbacks: dict[str, list[HassJob[[EventType[_TypedDictT]], Any]]], ) -> None: """Remove listener.""" for key in keys: @@ -451,7 +449,7 @@ def _async_entity_registry_updated_filter( def async_track_entity_registry_updated_event( hass: HomeAssistant, entity_ids: str | Iterable[str], - action: Callable[[Event], Any], + action: Callable[[EventType[EventEntityRegistryUpdatedData]], Any], ) -> CALLBACK_TYPE: """Track specific entity registry updated events indexed by entity_id. @@ -509,7 +507,7 @@ def _async_dispatch_device_id_event( def async_track_device_registry_updated_event( hass: HomeAssistant, device_ids: str | Iterable[str], - action: Callable[[Event], Any], + action: Callable[[EventType[EventDeviceRegistryUpdatedData]], Any], ) -> CALLBACK_TYPE: """Track specific device registry updated events indexed by device_id. @@ -561,7 +559,7 @@ def _async_domain_added_filter( def async_track_state_added_domain( hass: HomeAssistant, domains: str | Iterable[str], - action: Callable[[Event], Any], + action: Callable[[EventType[EventStateChangedData]], Any], ) -> CALLBACK_TYPE: """Track state change events when an entity is added to domains.""" if not (domains := _async_string_to_lower_list(domains)): @@ -573,7 +571,7 @@ def async_track_state_added_domain( def _async_track_state_added_domain( hass: HomeAssistant, domains: str | Iterable[str], - action: Callable[[Event], Any], + action: Callable[[EventType[EventStateChangedData]], Any], ) -> CALLBACK_TYPE: """Track state change events when an entity is added to domains.""" return _async_track_event( @@ -605,7 +603,7 @@ def _async_domain_removed_filter( def async_track_state_removed_domain( hass: HomeAssistant, domains: str | Iterable[str], - action: Callable[[Event], Any], + action: Callable[[EventType[EventStateChangedData]], Any], ) -> CALLBACK_TYPE: """Track state change events when an entity is removed from domains.""" return _async_track_event( @@ -635,7 +633,7 @@ def __init__( self, hass: HomeAssistant, track_states: TrackStates, - action: Callable[[Event], Any], + action: Callable[[EventType[EventStateChangedData]], Any], ) -> None: """Handle removal / refresh of tracker init.""" self.hass = hass @@ -739,7 +737,7 @@ def _setup_entities_listener(self, domains: set[str], entities: set[str]) -> Non ) @callback - def _state_added(self, event: Event) -> None: + def _state_added(self, event: EventType[EventStateChangedData]) -> None: self._cancel_listener(_ENTITIES_LISTENER) self._setup_entities_listener( self._last_track_states.domains, self._last_track_states.entities @@ -758,7 +756,7 @@ def _setup_domains_listener(self, domains: set[str]) -> None: @callback def _setup_all_listener(self) -> None: self._listeners[_ALL_LISTENER] = self.hass.bus.async_listen( - EVENT_STATE_CHANGED, self._action + EVENT_STATE_CHANGED, self._action # type: ignore[arg-type] ) @@ -767,7 +765,7 @@ def _setup_all_listener(self) -> None: def async_track_state_change_filtered( hass: HomeAssistant, track_states: TrackStates, - action: Callable[[Event], Any], + action: Callable[[EventType[EventStateChangedData]], Any], ) -> _TrackStateChangeFiltered: """Track state changes with a TrackStates filter that can be updated. @@ -841,7 +839,8 @@ def async_track_template( @callback def _template_changed_listener( - event: Event | None, updates: list[TrackTemplateResult] + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], ) -> None: """Check if condition is correct and run action.""" track_result = updates.pop() @@ -867,9 +866,9 @@ def _template_changed_listener( hass.async_run_hass_job( job, - event and event.data.get("entity_id"), - event and event.data.get("old_state"), - event and event.data.get("new_state"), + event and event.data["entity_id"], + event and event.data["old_state"], + event and event.data["new_state"], ) info = async_track_template_result( @@ -889,7 +888,9 @@ def __init__( self, hass: HomeAssistant, track_templates: Sequence[TrackTemplate], - action: Callable[[Event | None, list[TrackTemplateResult]], None], + action: Callable[ + [EventType[EventStateChangedData] | None, list[TrackTemplateResult]], None + ], has_super_template: bool = False, ) -> None: """Handle removal / refresh of tracker init.""" @@ -1026,7 +1027,7 @@ def _render_template_if_ready( self, track_template_: TrackTemplate, now: datetime, - event: Event | None, + event: EventType[EventStateChangedData] | None, ) -> bool | TrackTemplateResult: """Re-render the template if conditions match. @@ -1097,7 +1098,7 @@ def _super_template_as_boolean(result: bool | str | TemplateError) -> bool: @callback def _refresh( self, - event: Event | None, + event: EventType[EventStateChangedData] | None, track_templates: Iterable[TrackTemplate] | None = None, replayed: bool | None = False, ) -> None: @@ -1205,7 +1206,7 @@ def _apply_update( TrackTemplateResultListener = Callable[ [ - Event | None, + EventType[EventStateChangedData] | None, list[TrackTemplateResult], ], None, @@ -1315,11 +1316,11 @@ def state_for_listener(now: Any) -> None: hass.async_run_hass_job(job) @callback - def state_for_cancel_listener(event: Event) -> None: + def state_for_cancel_listener(event: EventType[EventStateChangedData]) -> None: """Fire on changes and cancel for listener if changed.""" - entity: str = event.data["entity_id"] - from_state: State | None = event.data.get("old_state") - to_state: State | None = event.data.get("new_state") + entity = event.data["entity_id"] + from_state = event.data["old_state"] + to_state = event.data["new_state"] if not async_check_same_func(entity, from_state, to_state): clear_listener() @@ -1330,7 +1331,7 @@ def state_for_cancel_listener(event: Event) -> None: if entity_ids == MATCH_ALL: async_remove_state_for_cancel = hass.bus.async_listen( - EVENT_STATE_CHANGED, state_for_cancel_listener + EVENT_STATE_CHANGED, state_for_cancel_listener # type: ignore[arg-type] ) else: async_remove_state_for_cancel = async_track_state_change_event( @@ -1761,17 +1762,16 @@ def _render_infos_to_track_states(render_infos: Iterable[RenderInfo]) -> TrackSt @callback -def _event_triggers_rerender(event: Event, info: RenderInfo) -> bool: +def _event_triggers_rerender( + event: EventType[EventStateChangedData], info: RenderInfo +) -> bool: """Determine if a template should be re-rendered from an event.""" - entity_id = cast(str, event.data.get(ATTR_ENTITY_ID)) + entity_id = event.data["entity_id"] if info.filter(entity_id): return True - if ( - event.data.get("new_state") is not None - and event.data.get("old_state") is not None - ): + if event.data["new_state"] is not None and event.data["old_state"] is not None: return False return bool(info.filter_lifecycle(entity_id)) @@ -1779,12 +1779,14 @@ def _event_triggers_rerender(event: Event, info: RenderInfo) -> bool: @callback def _rate_limit_for_event( - event: Event, info: RenderInfo, track_template_: TrackTemplate + event: EventType[EventStateChangedData], + info: RenderInfo, + track_template_: TrackTemplate, ) -> timedelta | None: """Determine the rate limit for an event.""" # Specifically referenced entities are excluded # from the rate limit - if event.data.get(ATTR_ENTITY_ID) in info.entities: + if event.data["entity_id"] in info.entities: return None if track_template_.rate_limit is not None: From 19b0a6e7f64cc2927b404168305c7981de441299 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Jul 2023 20:47:29 -0500 Subject: [PATCH 0835/1009] Relax typing on cached_property to accept subclasses (#95407) --- homeassistant/backports/functools.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/backports/functools.py b/homeassistant/backports/functools.py index ddcbab7dfc0d5c..83d66a39f71fcb 100644 --- a/homeassistant/backports/functools.py +++ b/homeassistant/backports/functools.py @@ -6,22 +6,21 @@ from typing import Any, Generic, Self, TypeVar, overload _T = TypeVar("_T") -_R = TypeVar("_R") -class cached_property(Generic[_T, _R]): # pylint: disable=invalid-name +class cached_property(Generic[_T]): # pylint: disable=invalid-name """Backport of Python 3.12's cached_property. Includes https://github.com/python/cpython/pull/101890/files """ - def __init__(self, func: Callable[[_T], _R]) -> None: + def __init__(self, func: Callable[[Any], _T]) -> None: """Initialize.""" - self.func = func - self.attrname: Any = None + self.func: Callable[[Any], _T] = func + self.attrname: str | None = None self.__doc__ = func.__doc__ - def __set_name__(self, owner: type[_T], name: str) -> None: + def __set_name__(self, owner: type[Any], name: str) -> None: """Set name.""" if self.attrname is None: self.attrname = name @@ -32,14 +31,16 @@ def __set_name__(self, owner: type[_T], name: str) -> None: ) @overload - def __get__(self, instance: None, owner: type[_T]) -> Self: + def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... @overload - def __get__(self, instance: _T, owner: type[_T]) -> _R: + def __get__(self, instance: Any, owner: type[Any] | None = None) -> _T: ... - def __get__(self, instance: _T | None, owner: type[_T] | None = None) -> _R | Self: + def __get__( + self, instance: Any | None, owner: type[Any] | None = None + ) -> _T | Self: """Get.""" if instance is None: return self From 40382f0caa3a2f34bdd63e3ebe8f93a140f6342f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jul 2023 01:00:25 -0500 Subject: [PATCH 0836/1009] Bump zeroconf to 0.71.3 (#97119) --- 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 473e08f5c8095b..87435d8e2c1482 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.71.0"] + "requirements": ["zeroconf==0.71.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5b8853bafb4dd1..087fe4297ec2d6 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.71.0 +zeroconf==0.71.3 # 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 8b81bb8410372e..731d3c750a84bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2738,7 +2738,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.71.0 +zeroconf==0.71.3 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba9cd91180fccf..cf55bdda53c758 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2008,7 +2008,7 @@ youless-api==1.0.1 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.71.0 +zeroconf==0.71.3 # homeassistant.components.zeversolar zeversolar==0.3.1 From 5b73bd2f8e35248cd4613d580247f97675bdbb41 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jul 2023 08:01:50 +0200 Subject: [PATCH 0837/1009] Use EventType for state changed [h-m] (#97117) --- .../homeassistant/triggers/state.py | 12 ++++----- .../components/homeassistant/triggers/time.py | 15 ++++++----- .../components/homekit/accessories.py | 25 +++++++++++++------ .../components/homekit/type_cameras.py | 20 +++++++++------ .../components/homekit/type_covers.py | 14 ++++++++--- .../components/homekit/type_humidifiers.py | 16 ++++++++---- .../components/integration/sensor.py | 15 ++++++----- homeassistant/components/knx/expose.py | 17 ++++++++----- .../manual_mqtt/alarm_control_panel.py | 9 ++++--- homeassistant/components/min_max/sensor.py | 18 ++++++++----- 10 files changed, 104 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 7fc780d7976c82..ce2d5e647436b3 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -10,7 +10,6 @@ from homeassistant.const import CONF_ATTRIBUTE, CONF_FOR, CONF_PLATFORM, MATCH_ALL from homeassistant.core import ( CALLBACK_TYPE, - Event, HassJob, HomeAssistant, State, @@ -22,12 +21,13 @@ template, ) from homeassistant.helpers.event import ( + EventStateChangedData, async_track_same_state, async_track_state_change_event, process_state_match, ) from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, EventType _LOGGER = logging.getLogger(__name__) @@ -129,11 +129,11 @@ async def async_attach_trigger( _variables = trigger_info["variables"] or {} @callback - def state_automation_listener(event: Event): + def state_automation_listener(event: EventType[EventStateChangedData]): """Listen for state changes and calls action.""" - entity: str = event.data["entity_id"] - from_s: State | None = event.data.get("old_state") - to_s: State | None = event.data.get("new_state") + entity = event.data["entity_id"] + from_s = event.data["old_state"] + to_s = event.data["new_state"] if from_s is None: old_value = None diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index a29cb5ff6da8bc..5b3cd8590a77ff 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -12,15 +12,16 @@ STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import ( + EventStateChangedData, async_track_point_in_time, async_track_state_change_event, async_track_time_change, ) from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, EventType import homeassistant.util.dt as dt_util _TIME_TRIGGER_SCHEMA = vol.Any( @@ -48,7 +49,7 @@ async def async_attach_trigger( """Listen for state changes based on configuration.""" trigger_data = trigger_info["trigger_data"] entities: dict[str, CALLBACK_TYPE] = {} - removes = [] + removes: list[CALLBACK_TYPE] = [] job = HassJob(action, f"time trigger {trigger_info}") @callback @@ -68,12 +69,12 @@ def time_automation_listener(description, now, *, entity_id=None): ) @callback - def update_entity_trigger_event(event): + def update_entity_trigger_event(event: EventType[EventStateChangedData]) -> None: """update_entity_trigger from the event.""" return update_entity_trigger(event.data["entity_id"], event.data["new_state"]) @callback - def update_entity_trigger(entity_id, new_state=None): + def update_entity_trigger(entity_id: str, new_state: State | None = None) -> None: """Update the entity trigger for the entity_id.""" # If a listener was already set up for entity, remove it. if remove := entities.pop(entity_id, None): @@ -83,6 +84,8 @@ def update_entity_trigger(entity_id, new_state=None): if not new_state: return + trigger_dt: datetime | None + # Check state of entity. If valid, set up a listener. if new_state.domain == "input_datetime": if has_date := new_state.attributes["has_date"]: @@ -155,7 +158,7 @@ def update_entity_trigger(entity_id, new_state=None): if remove: entities[entity_id] = remove - to_track = [] + to_track: list[str] = [] for at_time in config[CONF_AT]: if isinstance(at_time, str): diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 00168ef3898fff..f88047795ca59b 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -41,13 +41,16 @@ from homeassistant.core import ( CALLBACK_TYPE, Context, - Event, HomeAssistant, State, callback as ha_callback, split_entity_id, ) -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import EventType from homeassistant.util.decorator import Registry from .const import ( @@ -450,9 +453,11 @@ async def run(self) -> None: self.async_update_battery(battery_state, battery_charging_state) @ha_callback - def async_update_event_state_callback(self, event: Event) -> None: + def async_update_event_state_callback( + self, event: EventType[EventStateChangedData] + ) -> None: """Handle state change event listener callback.""" - self.async_update_state_callback(event.data.get("new_state")) + self.async_update_state_callback(event.data["new_state"]) @ha_callback def async_update_state_callback(self, new_state: State | None) -> None: @@ -477,9 +482,11 @@ def async_update_state_callback(self, new_state: State | None) -> None: self.async_update_state(new_state) @ha_callback - def async_update_linked_battery_callback(self, event: Event) -> None: + def async_update_linked_battery_callback( + self, event: EventType[EventStateChangedData] + ) -> None: """Handle linked battery sensor state change listener callback.""" - if (new_state := event.data.get("new_state")) is None: + if (new_state := event.data["new_state"]) is None: return if self.linked_battery_charging_sensor: battery_charging_state = None @@ -488,9 +495,11 @@ def async_update_linked_battery_callback(self, event: Event) -> None: self.async_update_battery(new_state.state, battery_charging_state) @ha_callback - def async_update_linked_battery_charging_callback(self, event: Event) -> None: + def async_update_linked_battery_charging_callback( + self, event: EventType[EventStateChangedData] + ) -> None: """Handle linked battery charging sensor state change listener callback.""" - if (new_state := event.data.get("new_state")) is None: + if (new_state := event.data["new_state"]) is None: return self.async_update_battery(None, new_state.state == STATE_ON) diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 3bc2b1ed6ae411..62d27245a1c951 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -14,11 +14,13 @@ from homeassistant.components import camera from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.const import STATE_ON -from homeassistant.core import Event, callback +from homeassistant.core import State, callback from homeassistant.helpers.event import ( + EventStateChangedData, async_track_state_change_event, async_track_time_interval, ) +from homeassistant.helpers.typing import EventType from .accessories import TYPES, HomeAccessory from .const import ( @@ -266,13 +268,15 @@ async def run(self): await super().run() @callback - def _async_update_motion_state_event(self, event: Event) -> None: + def _async_update_motion_state_event( + self, event: EventType[EventStateChangedData] + ) -> None: """Handle state change event listener callback.""" if not state_changed_event_is_same_state(event): - self._async_update_motion_state(event.data.get("new_state")) + self._async_update_motion_state(event.data["new_state"]) @callback - def _async_update_motion_state(self, new_state): + def _async_update_motion_state(self, new_state: State | None) -> None: """Handle link motion sensor state change to update HomeKit value.""" if not new_state: return @@ -290,13 +294,15 @@ def _async_update_motion_state(self, new_state): ) @callback - def _async_update_doorbell_state_event(self, event: Event) -> None: + def _async_update_doorbell_state_event( + self, event: EventType[EventStateChangedData] + ) -> None: """Handle state change event listener callback.""" if not state_changed_event_is_same_state(event): - self._async_update_doorbell_state(event.data.get("new_state")) + self._async_update_doorbell_state(event.data["new_state"]) @callback - def _async_update_doorbell_state(self, new_state): + def _async_update_doorbell_state(self, new_state: State | None) -> None: """Handle link doorbell sensor state change to update HomeKit value.""" if not new_state: return diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 05feb580572c45..ea0a5054ffdc38 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -31,7 +31,11 @@ STATE_OPENING, ) from homeassistant.core import State, callback -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import EventType from .accessories import TYPES, HomeAccessory from .const import ( @@ -135,12 +139,14 @@ async def run(self): await super().run() @callback - def _async_update_obstruction_event(self, event): + def _async_update_obstruction_event( + self, event: EventType[EventStateChangedData] + ) -> None: """Handle state change event listener callback.""" - self._async_update_obstruction_state(event.data.get("new_state")) + self._async_update_obstruction_state(event.data["new_state"]) @callback - def _async_update_obstruction_state(self, new_state): + def _async_update_obstruction_state(self, new_state: State | None) -> None: """Handle linked obstruction sensor state change to update HomeKit value.""" if not new_state: return diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py index 33c35908cd11d3..f9f572a096c099 100644 --- a/homeassistant/components/homekit/type_humidifiers.py +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -21,8 +21,12 @@ SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import callback -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.core import State, callback +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import EventType from .accessories import TYPES, HomeAccessory from .const import ( @@ -157,12 +161,14 @@ async def run(self): await super().run() @callback - def async_update_current_humidity_event(self, event): + def async_update_current_humidity_event( + self, event: EventType[EventStateChangedData] + ) -> None: """Handle state change event listener callback.""" - self._async_update_current_humidity(event.data.get("new_state")) + self._async_update_current_humidity(event.data["new_state"]) @callback - def _async_update_current_humidity(self, new_state): + def _async_update_current_humidity(self, new_state: State | None) -> None: """Handle linked humidity sensor state change to update HomeKit value.""" if new_state is None: _LOGGER.error( diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index e4ae3cde88302a..5ce64de9b3313a 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -26,7 +26,7 @@ STATE_UNKNOWN, UnitOfTime, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -34,8 +34,11 @@ ) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType from .const import ( CONF_ROUND_DIGITS, @@ -290,10 +293,10 @@ async def async_added_to_hass(self) -> None: self._unit_of_measurement = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @callback - def calc_integration(event: Event) -> None: + def calc_integration(event: EventType[EventStateChangedData]) -> None: """Handle the sensor state changes.""" - old_state: State | None = event.data.get("old_state") - new_state: State | None = event.data.get("new_state") + old_state = event.data["old_state"] + new_state = event.data["new_state"] # We may want to update our state before an early return, # based on the source sensor's unit_of_measurement diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py index 308fc4eacd173d..e14ee501d7b949 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -17,9 +17,12 @@ STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, State, callback -from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, StateType +from homeassistant.core import HomeAssistant, State, callback +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import ConfigType, EventType, StateType from .const import CONF_RESPOND_TO_READ, KNX_ADDRESS from .schema import ExposeSchema @@ -145,12 +148,14 @@ def _get_expose_value(self, state: State | None) -> bool | int | float | str | N return str(value)[:14] return value - async def _async_entity_changed(self, event: Event) -> None: + async def _async_entity_changed( + self, event: EventType[EventStateChangedData] + ) -> None: """Handle entity change.""" - new_state = event.data.get("new_state") + new_state = event.data["new_state"] if (new_value := self._get_expose_value(new_state)) is None: return - old_state = event.data.get("old_state") + old_state = event.data["old_state"] # don't use default value for comparison on first state change (old_state is None) old_value = self._get_expose_value(old_state) if old_state is not None else None # don't send same value sequentially diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index adb251bd71a6e0..69cd1ef3d11e80 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -33,10 +33,11 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( + EventStateChangedData, async_track_point_in_time, async_track_state_change_event, ) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -481,9 +482,11 @@ async def message_received(msg): self.hass, self._command_topic, message_received, self._qos ) - async def _async_state_changed_listener(self, event): + async def _async_state_changed_listener( + self, event: EventType[EventStateChangedData] + ) -> None: """Publish state change to MQTT.""" - if (new_state := event.data.get("new_state")) is None: + if (new_state := event.data["new_state"]) is None: return await mqtt.async_publish( self.hass, self._state_topic, new_state.state, self._qos, True diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index d1ea9695322973..cc26a684a8db7b 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -22,14 +22,18 @@ STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ( ConfigType, DiscoveryInfoType, + EventType, StateType, ) @@ -253,7 +257,9 @@ async def async_added_to_hass(self) -> None: # Replay current state of source entities for entity_id in self._entity_ids: state = self.hass.states.get(entity_id) - state_event = Event("", {"entity_id": entity_id, "new_state": state}) + state_event: EventType[EventStateChangedData] = EventType( + "", {"entity_id": entity_id, "new_state": state, "old_state": None} + ) self._async_min_max_sensor_state_listener(state_event, update_state=False) self._calc_values() @@ -286,11 +292,11 @@ def extra_state_attributes(self) -> dict[str, Any] | None: @callback def _async_min_max_sensor_state_listener( - self, event: Event, update_state: bool = True + self, event: EventType[EventStateChangedData], update_state: bool = True ) -> None: """Handle the sensor state changes.""" - new_state: State | None = event.data.get("new_state") - entity: str = event.data["entity_id"] + new_state = event.data["new_state"] + entity = event.data["entity_id"] if ( new_state is None From 0cc396b8635b1448c9653fc02e6cebcddcb72b44 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jul 2023 08:04:13 +0200 Subject: [PATCH 0838/1009] Use EventType for state changed [a-h] (#97116) --- homeassistant/components/alert/__init__.py | 13 ++++++---- .../components/compensation/sensor.py | 17 ++++++++----- homeassistant/components/derivative/sensor.py | 17 +++++++------ .../components/emulated_hue/hue_api.py | 8 ++++-- homeassistant/components/esphome/manager.py | 14 ++++++++--- homeassistant/components/filter/sensor.py | 20 +++++++++++---- .../components/generic_thermostat/climate.py | 25 +++++++++++++------ .../components/history_stats/coordinator.py | 12 ++++++--- 8 files changed, 85 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index d7d495b55bfc9b..9b3fb0f29c854b 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -25,16 +25,17 @@ STATE_OFF, STATE_ON, ) -from homeassistant.core import Event, HassJob, HomeAssistant +from homeassistant.core import HassJob, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import ( + EventStateChangedData, async_track_point_in_time, async_track_state_change_event, ) from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, EventType from homeassistant.util.dt import now from .const import ( @@ -196,11 +197,13 @@ def state(self) -> str: return STATE_ON return STATE_IDLE - async def watched_entity_change(self, event: Event) -> None: + async def watched_entity_change( + self, event: EventType[EventStateChangedData] + ) -> None: """Determine if the alert should start or stop.""" - if (to_state := event.data.get("new_state")) is None: + if (to_state := event.data["new_state"]) is None: return - LOGGER.debug("Watched entity (%s) has changed", event.data.get("entity_id")) + LOGGER.debug("Watched entity (%s) has changed", event.data["entity_id"]) if to_state.state == self._alert_state and not self._firing: await self.begin_alerting() if to_state.state != self._alert_state and self._firing: diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index 4d6ff95b8107f8..6abc5d3d5d0265 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -17,10 +17,13 @@ CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType from .const import ( CONF_COMPENSATION, @@ -124,10 +127,12 @@ def extra_state_attributes(self) -> dict[str, Any]: return ret @callback - def _async_compensation_sensor_state_listener(self, event: Event) -> None: + def _async_compensation_sensor_state_listener( + self, event: EventType[EventStateChangedData] + ) -> None: """Handle sensor state changes.""" new_state: State | None - if (new_state := event.data.get("new_state")) is None: + if (new_state := event.data["new_state"]) is None: return if self.native_unit_of_measurement is None and self._source_attribute is None: @@ -140,7 +145,7 @@ def _async_compensation_sensor_state_listener(self, event: Event) -> None: else: value = None if new_state.state == STATE_UNKNOWN else new_state.state try: - x_value = float(value) + x_value = float(value) # type: ignore[arg-type] if self._minimum is not None and x_value <= self._minimum[0]: y_value = self._minimum[1] elif self._maximum is not None and x_value >= self._maximum[0]: diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index af04da27406c28..de9f06a0e889b0 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -18,7 +18,7 @@ STATE_UNKNOWN, UnitOfTime, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -26,8 +26,11 @@ ) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType from .const import ( CONF_ROUND_DIGITS, @@ -210,14 +213,12 @@ async def async_added_to_hass(self) -> None: _LOGGER.warning("Could not restore last state: %s", err) @callback - def calc_derivative(event: Event) -> None: + def calc_derivative(event: EventType[EventStateChangedData]) -> None: """Handle the sensor state changes.""" - old_state: State | None - new_state: State | None if ( - (old_state := event.data.get("old_state")) is None + (old_state := event.data["old_state"]) is None or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) - or (new_state := event.data.get("new_state")) is None + or (new_state := event.data["new_state"]) is None or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) ): return diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 654d0bce13b784..f0a54ba0ea9682 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -63,7 +63,11 @@ STATE_UNAVAILABLE, ) from homeassistant.core import State -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import EventType from homeassistant.util.json import json_loads from homeassistant.util.network import is_local @@ -888,7 +892,7 @@ async def wait_for_state_change_or_timeout( ev = asyncio.Event() @core.callback - def _async_event_changed(event: core.Event) -> None: + def _async_event_changed(event: EventType[EventStateChangedData]) -> None: ev.set() unsub = async_track_state_change_event(hass, [entity_id], _async_event_changed) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 345be0c4b6d7f4..71dc02acf02695 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -34,7 +34,10 @@ import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) from homeassistant.helpers.issue_registry import ( IssueSeverity, async_create_issue, @@ -42,6 +45,7 @@ ) from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.template import Template +from homeassistant.helpers.typing import EventType from .bluetooth import async_connect_scanner from .const import ( @@ -270,11 +274,13 @@ def async_on_state_subscription( """Subscribe and forward states for requested entities.""" hass = self.hass - async def send_home_assistant_state_event(event: Event) -> None: + async def send_home_assistant_state_event( + event: EventType[EventStateChangedData], + ) -> None: """Forward Home Assistant states updates to ESPHome.""" event_data = event.data - new_state: State | None = event_data.get("new_state") - old_state: State | None = event_data.get("old_state") + new_state = event_data["new_state"] + old_state = event_data["old_state"] if new_state is None or old_state is None: return diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index a1470baa4d2fcb..c240d04ec1a766 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -34,13 +34,21 @@ STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import HomeAssistant, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.start import async_at_started -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import ( + ConfigType, + DiscoveryInfoType, + EventType, + StateType, +) from homeassistant.util.decorator import Registry import homeassistant.util.dt as dt_util @@ -217,10 +225,12 @@ def __init__( self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_id} @callback - def _update_filter_sensor_state_event(self, event: Event) -> None: + def _update_filter_sensor_state_event( + self, event: EventType[EventStateChangedData] + ) -> None: """Handle device state changes.""" _LOGGER.debug("Update filter on event: %s", event) - self._update_filter_sensor_state(event.data.get("new_state")) + self._update_filter_sensor_state(event.data["new_state"]) @callback def _update_filter_sensor_state( diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index e3eed8866c8d58..d3d807471274af 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -37,18 +37,25 @@ STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import DOMAIN as HA_DOMAIN, CoreState, HomeAssistant, callback +from homeassistant.core import ( + DOMAIN as HA_DOMAIN, + CoreState, + HomeAssistant, + State, + callback, +) from homeassistant.exceptions import ConditionError from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( + EventStateChangedData, async_track_state_change_event, async_track_time_interval, ) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType from . import DOMAIN, PLATFORMS @@ -395,9 +402,11 @@ def max_temp(self): # Get default temp from super class return super().max_temp - async def _async_sensor_changed(self, event): + async def _async_sensor_changed( + self, event: EventType[EventStateChangedData] + ) -> None: """Handle temperature changes.""" - new_state = event.data.get("new_state") + new_state = event.data["new_state"] if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): return @@ -418,10 +427,10 @@ async def _check_switch_initial_state(self): await self._async_heater_turn_off() @callback - def _async_switch_changed(self, event): + def _async_switch_changed(self, event: EventType[EventStateChangedData]) -> None: """Handle heater switch state changes.""" - new_state = event.data.get("new_state") - old_state = event.data.get("old_state") + new_state = event.data["new_state"] + old_state = event.data["old_state"] if new_state is None: return if old_state is None: @@ -429,7 +438,7 @@ def _async_switch_changed(self, event): self.async_write_ha_state() @callback - def _async_update_temp(self, state): + def _async_update_temp(self, state: State) -> None: """Update thermostat with latest state from sensor.""" try: cur_temp = float(state.state) diff --git a/homeassistant/components/history_stats/coordinator.py b/homeassistant/components/history_stats/coordinator.py index 7d44da9f5f662b..6d4d6e55fa9758 100644 --- a/homeassistant/components/history_stats/coordinator.py +++ b/homeassistant/components/history_stats/coordinator.py @@ -5,10 +5,14 @@ import logging from typing import Any -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import TemplateError -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) from homeassistant.helpers.start import async_at_start +from homeassistant.helpers.typing import EventType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .data import HistoryStats, HistoryStatsState @@ -82,7 +86,9 @@ def _async_add_events_listener(self, *_: Any) -> None: self.hass, [self._history_stats.entity_id], self._async_update_from_event ) - async def _async_update_from_event(self, event: Event) -> None: + async def _async_update_from_event( + self, event: EventType[EventStateChangedData] + ) -> None: """Process an update from an event.""" self.async_set_updated_data(await self._history_stats.async_update(event)) From 8c870a5683107409f9dd02e6365977199d500764 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jul 2023 08:07:07 +0200 Subject: [PATCH 0839/1009] Use EventType for state changed [m-z] (#97118) --- .../components/mold_indicator/sensor.py | 27 ++++++++++++------- homeassistant/components/plant/__init__.py | 16 ++++++----- homeassistant/components/statistics/sensor.py | 15 ++++++++--- .../components/switch_as_x/entity.py | 10 +++++-- .../components/threshold/binary_sensor.py | 13 ++++++--- .../components/trend/binary_sensor.py | 15 +++++++---- .../components/universal/media_player.py | 7 +++-- .../components/utility_meter/sensor.py | 19 +++++++------ homeassistant/components/zha/entity.py | 12 ++++++--- 9 files changed, 90 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index ee3ab9817ea655..ce3844475c5e67 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -16,11 +16,14 @@ STATE_UNKNOWN, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType from homeassistant.util.unit_conversion import TemperatureConverter from homeassistant.util.unit_system import METRIC_SYSTEM @@ -117,11 +120,13 @@ async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback - def mold_indicator_sensors_state_listener(event): + def mold_indicator_sensors_state_listener( + event: EventType[EventStateChangedData], + ) -> None: """Handle for state changes for dependent sensors.""" - new_state = event.data.get("new_state") - old_state = event.data.get("old_state") - entity = event.data.get("entity_id") + new_state = event.data["new_state"] + old_state = event.data["old_state"] + entity = event.data["entity_id"] _LOGGER.debug( "Sensor state change for %s that had old state %s and new state %s", entity, @@ -173,7 +178,9 @@ def mold_indicator_startup(event): EVENT_HOMEASSISTANT_START, mold_indicator_startup ) - def _update_sensor(self, entity, old_state, new_state): + def _update_sensor( + self, entity: str, old_state: State | None, new_state: State | None + ) -> bool: """Update information based on new sensor states.""" _LOGGER.debug("Sensor update for %s", entity) if new_state is None: @@ -194,7 +201,7 @@ def _update_sensor(self, entity, old_state, new_state): return True @staticmethod - def _update_temp_sensor(state): + def _update_temp_sensor(state: State) -> float | None: """Parse temperature sensor value.""" _LOGGER.debug("Updating temp sensor with value %s", state.state) @@ -235,7 +242,7 @@ def _update_temp_sensor(state): return None @staticmethod - def _update_hum_sensor(state): + def _update_hum_sensor(state: State) -> float | None: """Parse humidity sensor value.""" _LOGGER.debug("Updating humidity sensor with value %s", state.state) diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index e385156c6d1a2b..ed88e50b932e19 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -19,13 +19,16 @@ STATE_UNKNOWN, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import ConfigType, EventType from homeassistant.util import dt as dt_util from .const import ( @@ -176,15 +179,16 @@ def __init__(self, name, config): self._brightness_history = DailyHistory(self._conf_check_days) @callback - def _state_changed_event(self, event): + def _state_changed_event(self, event: EventType[EventStateChangedData]): """Sensor state change event.""" - self.state_changed(event.data.get("entity_id"), event.data.get("new_state")) + self.state_changed(event.data["entity_id"], event.data["new_state"]) @callback - def state_changed(self, entity_id, new_state): + def state_changed(self, entity_id: str, new_state: State | None) -> None: """Update the sensor status.""" if new_state is None: return + value: str | float value = new_state.state _LOGGER.debug("Received callback from %s with value %s", entity_id, value) if value == STATE_UNKNOWN: diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 078eb59fe723e9..e86a4741080aff 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -32,7 +32,6 @@ ) from homeassistant.core import ( CALLBACK_TYPE, - Event, HomeAssistant, State, callback, @@ -41,12 +40,18 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( + EventStateChangedData, async_track_point_in_utc_time, async_track_state_change_event, ) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.start import async_at_start -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import ( + ConfigType, + DiscoveryInfoType, + EventType, + StateType, +) from homeassistant.util import dt as dt_util from homeassistant.util.enum import try_parse_enum @@ -308,9 +313,11 @@ async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback - def async_stats_sensor_state_listener(event: Event) -> None: + def async_stats_sensor_state_listener( + event: EventType[EventStateChangedData], + ) -> None: """Handle the sensor state changes.""" - if (new_state := event.data.get("new_state")) is None: + if (new_state := event.data["new_state"]) is None: return self._add_state_to_queue(new_state) self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/switch_as_x/entity.py b/homeassistant/components/switch_as_x/entity.py index a73271bdc835dd..36f8a651f06d03 100644 --- a/homeassistant/components/switch_as_x/entity.py +++ b/homeassistant/components/switch_as_x/entity.py @@ -15,7 +15,11 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import DeviceInfo, Entity, ToggleEntity -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import EventType from .const import DOMAIN as SWITCH_AS_X_DOMAIN @@ -77,7 +81,9 @@ async def async_added_to_hass(self) -> None: """Register callbacks and copy the wrapped entity's custom name if set.""" @callback - def _async_state_changed_listener(event: Event | None = None) -> None: + def _async_state_changed_listener( + event: EventType[EventStateChangedData] | None = None, + ) -> None: """Handle child updates.""" self.async_state_changed_listener(event) self.async_write_ha_state() diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index 09f928303bf383..a6621c096c3fdc 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -21,7 +21,7 @@ STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -29,8 +29,11 @@ ) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType from .const import CONF_HYSTERESIS, CONF_LOWER, CONF_UPPER @@ -210,7 +213,9 @@ def _update_sensor_state() -> None: self._update_state() @callback - def async_threshold_sensor_state_listener(event: Event) -> None: + def async_threshold_sensor_state_listener( + event: EventType[EventStateChangedData], + ) -> None: """Handle sensor state changes.""" _update_sensor_state() self.async_write_ha_state() diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index e43032a580f890..020f7903060821 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -29,9 +29,12 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType from homeassistant.util.dt import utcnow from . import PLATFORMS @@ -174,9 +177,11 @@ async def async_added_to_hass(self) -> None: """Complete device setup after being added to hass.""" @callback - def trend_sensor_state_listener(event): + def trend_sensor_state_listener( + event: EventType[EventStateChangedData], + ) -> None: """Handle state changes on the observed device.""" - if (new_state := event.data.get("new_state")) is None: + if (new_state := event.data["new_state"]) is None: return try: if self._attribute: @@ -184,7 +189,7 @@ def trend_sensor_state_listener(event): else: state = new_state.state if state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - sample = (new_state.last_updated.timestamp(), float(state)) + sample = (new_state.last_updated.timestamp(), float(state)) # type: ignore[arg-type] self.samples.append(sample) self.async_schedule_update_ha_state(True) except (ValueError, TypeError) as ex: diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 68ce8e9b96c945..94034cdffe54ab 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -85,13 +85,14 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( + EventStateChangedData, TrackTemplate, async_track_state_change_event, async_track_template_result, ) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.service import async_call_from_config -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType ATTR_ACTIVE_CHILD = "active_child" @@ -183,7 +184,9 @@ async def async_added_to_hass(self) -> None: """Subscribe to children and template state changes.""" @callback - def _async_on_dependency_update(event): + def _async_on_dependency_update( + event: EventType[EventStateChangedData], + ) -> None: """Update ha state when dependencies update.""" self.async_set_context(event.context) self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index db03a1ccf2e1d9..7301158d6c6cbd 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -26,7 +26,7 @@ STATE_UNKNOWN, UnitOfEnergy, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import ( device_registry as dr, entity_platform, @@ -36,12 +36,13 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( + EventStateChangedData, async_track_point_in_time, async_track_state_change_event, ) from homeassistant.helpers.start import async_at_started from homeassistant.helpers.template import is_number -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType from homeassistant.util import slugify import homeassistant.util.dt as dt_util @@ -451,7 +452,7 @@ def calculate_adjustment( return None @callback - def async_reading(self, event: Event): + def async_reading(self, event: EventType[EventStateChangedData]) -> None: """Handle the sensor state changes.""" if ( source_state := self.hass.states.get(self._sensor_source_id) @@ -462,8 +463,10 @@ def async_reading(self, event: Event): self._attr_available = True - old_state: State | None = event.data.get("old_state") - new_state: State = event.data.get("new_state") # type: ignore[assignment] # a state change event always has a new state + old_state = event.data["old_state"] + new_state = event.data["new_state"] + if new_state is None: + return # First check if the new_state is valid (see discussion in PR #88446) if (new_state_val := self._validate_state(new_state)) is None: @@ -492,14 +495,14 @@ def async_reading(self, event: Event): self.async_write_ha_state() @callback - def async_tariff_change(self, event): + def async_tariff_change(self, event: EventType[EventStateChangedData]) -> None: """Handle tariff changes.""" - if (new_state := event.data.get("new_state")) is None: + if (new_state := event.data["new_state"]) is None: return self._change_status(new_state.state) - def _change_status(self, tariff): + def _change_status(self, tariff: str) -> None: if self._tariff == tariff: self._collecting = async_track_state_change_event( self.hass, [self._sensor_source_id], self.async_reading diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 43f487f61d4e9d..7f34629400f15b 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any, Self from homeassistant.const import ATTR_NAME -from homeassistant.core import CALLBACK_TYPE, Event, callback +from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers import entity from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE @@ -16,8 +16,12 @@ async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import EventType from .core.const import ( ATTR_MANUFACTURER, @@ -319,7 +323,9 @@ def send_removed_signal(): self.async_on_remove(send_removed_signal) @callback - def async_state_changed_listener(self, event: Event): + def async_state_changed_listener( + self, event: EventType[EventStateChangedData] + ) -> None: """Handle child updates.""" # Delay to ensure that we get updates from all members before updating the group assert self._change_listener_debouncer From 797a9c1eadb33c3a69d2539222d9dc1996334f2a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jul 2023 09:11:41 +0200 Subject: [PATCH 0840/1009] Improve `async_track_state_added/removed_domain` callback typing (#97126) --- .../components/conversation/default_agent.py | 8 +++-- homeassistant/components/dhcp/__init__.py | 11 +++--- .../components/emulated_hue/config.py | 7 ++-- homeassistant/components/zone/__init__.py | 14 +++++--- tests/helpers/test_event.py | 36 +++++++++---------- 5 files changed, 44 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 336d6287f18913..b0a3702b5c9a52 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -32,7 +32,11 @@ template, translation, ) -from homeassistant.helpers.event import async_track_state_added_domain +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_added_domain, +) +from homeassistant.helpers.typing import EventType from homeassistant.util.json import JsonObjectType, json_loads_object from .agent import AbstractConversationAgent, ConversationInput, ConversationResult @@ -95,7 +99,7 @@ def async_setup(hass: core.HomeAssistant) -> None: async_should_expose(hass, DOMAIN, entity_id) @core.callback - def async_entity_state_listener(event: core.Event) -> None: + def async_entity_state_listener(event: EventType[EventStateChangedData]) -> None: """Set expose flag on new entities.""" async_should_expose(hass, DOMAIN, event.data["entity_id"]) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 9f9ec48f347929..b3cfd1b65f2404 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -51,10 +51,11 @@ ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import ( + EventStateChangedData, async_track_state_added_domain, async_track_time_interval, ) -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, EventType from homeassistant.loader import DHCPMatcher, async_get_dhcp from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.network import is_invalid, is_link_local, is_loopback @@ -317,14 +318,16 @@ async def async_start(self) -> None: self._async_process_device_state(state) @callback - def _async_process_device_event(self, event: Event) -> None: + def _async_process_device_event( + self, event: EventType[EventStateChangedData] + ) -> None: """Process a device tracker state change event.""" self._async_process_device_state(event.data["new_state"]) @callback - def _async_process_device_state(self, state: State) -> None: + def _async_process_device_state(self, state: State | None) -> None: """Process a device tracker state.""" - if state.state != STATE_HOME: + if state is None or state.state != STATE_HOME: return attributes = state.attributes diff --git a/homeassistant/components/emulated_hue/config.py b/homeassistant/components/emulated_hue/config.py index 1de6ec98520ad9..104e05605cb29d 100644 --- a/homeassistant/components/emulated_hue/config.py +++ b/homeassistant/components/emulated_hue/config.py @@ -15,13 +15,14 @@ script, ) from homeassistant.const import CONF_ENTITIES, CONF_TYPE -from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id +from homeassistant.core import HomeAssistant, State, callback, split_entity_id from homeassistant.helpers import storage from homeassistant.helpers.event import ( + EventStateChangedData, async_track_state_added_domain, async_track_state_removed_domain, ) -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, EventType SUPPORTED_DOMAINS = { climate.DOMAIN, @@ -222,7 +223,7 @@ def get_exposed_states(self) -> list[State]: return states @callback - def _clear_exposed_cache(self, event: Event) -> None: + def _clear_exposed_cache(self, event: EventType[EventStateChangedData]) -> None: """Clear the cache of exposed states.""" self.get_exposed_states.cache_clear() # pylint: disable=no-member diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 8d04987d4fa1d0..77c225d72ecf43 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -37,7 +37,7 @@ service, storage, ) -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, EventType from homeassistant.loader import bind_hass from homeassistant.util.location import distance @@ -155,15 +155,19 @@ def async_setup_track_zone_entity_ids(hass: HomeAssistant) -> None: hass.data[ZONE_ENTITY_IDS] = zone_entity_ids @callback - def _async_add_zone_entity_id(event_: Event) -> None: + def _async_add_zone_entity_id( + event_: EventType[event.EventStateChangedData], + ) -> None: """Add zone entity ID.""" - zone_entity_ids.append(event_.data[ATTR_ENTITY_ID]) + zone_entity_ids.append(event_.data["entity_id"]) zone_entity_ids.sort() @callback - def _async_remove_zone_entity_id(event_: Event) -> None: + def _async_remove_zone_entity_id( + event_: EventType[event.EventStateChangedData], + ) -> None: """Remove zone entity ID.""" - zone_entity_ids.remove(event_.data[ATTR_ENTITY_ID]) + zone_entity_ids.remove(event_.data["entity_id"]) event.async_track_state_added_domain(hass, DOMAIN, _async_add_zone_entity_id) event.async_track_state_removed_domain(hass, DOMAIN, _async_remove_zone_entity_id) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 434957dc1319bc..ee33e20173c769 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -544,16 +544,16 @@ async def test_async_track_state_added_domain(hass: HomeAssistant) -> None: multiple_entity_id_tracker = [] @ha.callback - def single_run_callback(event): - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") + def single_run_callback(event: EventType[EventStateChangedData]): + old_state = event.data["old_state"] + new_state = event.data["new_state"] single_entity_id_tracker.append((old_state, new_state)) @ha.callback - def multiple_run_callback(event): - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") + def multiple_run_callback(event: EventType[EventStateChangedData]): + old_state = event.data["old_state"] + new_state = event.data["new_state"] multiple_entity_id_tracker.append((old_state, new_state)) @@ -656,16 +656,16 @@ async def test_async_track_state_removed_domain(hass: HomeAssistant) -> None: multiple_entity_id_tracker = [] @ha.callback - def single_run_callback(event): - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") + def single_run_callback(event: EventType[EventStateChangedData]): + old_state = event.data["old_state"] + new_state = event.data["new_state"] single_entity_id_tracker.append((old_state, new_state)) @ha.callback - def multiple_run_callback(event): - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") + def multiple_run_callback(event: EventType[EventStateChangedData]): + old_state = event.data["old_state"] + new_state = event.data["new_state"] multiple_entity_id_tracker.append((old_state, new_state)) @@ -738,16 +738,16 @@ async def test_async_track_state_removed_domain_match_all(hass: HomeAssistant) - match_all_entity_id_tracker = [] @ha.callback - def single_run_callback(event): - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") + def single_run_callback(event: EventType[EventStateChangedData]): + old_state = event.data["old_state"] + new_state = event.data["new_state"] single_entity_id_tracker.append((old_state, new_state)) @ha.callback - def match_all_run_callback(event): - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") + def match_all_run_callback(event: EventType[EventStateChangedData]): + old_state = event.data["old_state"] + new_state = event.data["new_state"] match_all_entity_id_tracker.append((old_state, new_state)) From 84220e92ea5bc185a071a01e25d0eadcadc7e113 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 24 Jul 2023 03:12:21 -0400 Subject: [PATCH 0841/1009] Wrap internal ZHA exceptions in `HomeAssistantError`s (#97033) --- .../zha/core/cluster_handlers/__init__.py | 36 ++++++++++++++++--- tests/components/zha/test_cluster_handlers.py | 35 ++++++++++++++++++ tests/components/zha/test_cover.py | 11 +++--- 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/core/cluster_handlers/__init__.py b/homeassistant/components/zha/core/cluster_handlers/__init__.py index dcf8f2a525e2d0..6c05ce2fe4f966 100644 --- a/homeassistant/components/zha/core/cluster_handlers/__init__.py +++ b/homeassistant/components/zha/core/cluster_handlers/__init__.py @@ -2,10 +2,11 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable, Callable, Coroutine from enum import Enum -from functools import partialmethod +import functools import logging -from typing import TYPE_CHECKING, Any, TypedDict +from typing import TYPE_CHECKING, Any, ParamSpec, TypedDict import zigpy.exceptions import zigpy.util @@ -19,6 +20,7 @@ from homeassistant.const import ATTR_COMMAND from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_send from ..const import ( @@ -45,8 +47,34 @@ from ..endpoint import Endpoint _LOGGER = logging.getLogger(__name__) +RETRYABLE_REQUEST_DECORATOR = zigpy.util.retryable_request(tries=3) -retry_request = zigpy.util.retryable_request(tries=3) + +_P = ParamSpec("_P") +_FuncType = Callable[_P, Awaitable[Any]] +_ReturnFuncType = Callable[_P, Coroutine[Any, Any, Any]] + + +def retry_request(func: _FuncType[_P]) -> _ReturnFuncType[_P]: + """Send a request with retries and wrap expected zigpy exceptions.""" + + @functools.wraps(func) + async def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> Any: + try: + return await RETRYABLE_REQUEST_DECORATOR(func)(*args, **kwargs) + except asyncio.TimeoutError as exc: + raise HomeAssistantError( + "Failed to send request: device did not respond" + ) from exc + except zigpy.exceptions.ZigbeeException as exc: + message = "Failed to send request" + + if str(exc): + message = f"{message}: {exc}" + + raise HomeAssistantError(message) from exc + + return wrapper class AttrReportConfig(TypedDict, total=True): @@ -471,7 +499,7 @@ async def _get_attributes( rest = rest[ZHA_CLUSTER_HANDLER_READS_PER_REQ:] return result - get_attributes = partialmethod(_get_attributes, False) + get_attributes = functools.partialmethod(_get_attributes, False) def log(self, level, msg, *args, **kwargs): """Log a message.""" diff --git a/tests/components/zha/test_cluster_handlers.py b/tests/components/zha/test_cluster_handlers.py index 1897383b6c4fa2..7e0e8eaab85c7d 100644 --- a/tests/components/zha/test_cluster_handlers.py +++ b/tests/components/zha/test_cluster_handlers.py @@ -22,6 +22,7 @@ from homeassistant.components.zha.core.endpoint import Endpoint import homeassistant.components.zha.core.registries as registries from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .common import get_zha_gateway, make_zcl_header from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE @@ -831,3 +832,37 @@ class TestZigbeeClusterHandler(cluster_handlers.ClusterHandler): zha_endpoint.add_all_cluster_handlers() assert "missing_attr" in caplog.text + + +# parametrize side effects: +@pytest.mark.parametrize( + ("side_effect", "expected_error"), + [ + (zigpy.exceptions.ZigbeeException(), "Failed to send request"), + ( + zigpy.exceptions.ZigbeeException("Zigbee exception"), + "Failed to send request: Zigbee exception", + ), + (asyncio.TimeoutError(), "Failed to send request: device did not respond"), + ], +) +async def test_retry_request( + side_effect: Exception | None, expected_error: str | None +) -> None: + """Test the `retry_request` decorator's handling of zigpy-internal exceptions.""" + + async def func(arg1: int, arg2: int) -> int: + assert arg1 == 1 + assert arg2 == 2 + + raise side_effect + + func = mock.AsyncMock(wraps=func) + decorated_func = cluster_handlers.retry_request(func) + + with pytest.raises(HomeAssistantError) as exc: + await decorated_func(1, arg2=2) + + assert func.await_count == 3 + assert isinstance(exc.value, HomeAssistantError) + assert str(exc.value) == expected_error diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index d10034184874bf..7c4198bd881372 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -26,6 +26,7 @@ Platform, ) from homeassistant.core import CoreState, HomeAssistant, State +from homeassistant.exceptions import HomeAssistantError from .common import ( async_enable_traffic, @@ -236,7 +237,7 @@ async def test_shade( # close from UI command fails with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(HomeAssistantError): await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER, @@ -261,7 +262,7 @@ async def test_shade( assert ATTR_CURRENT_POSITION not in hass.states.get(entity_id).attributes await send_attributes_report(hass, cluster_level, {0: 0}) with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(HomeAssistantError): await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, @@ -285,7 +286,7 @@ async def test_shade( # set position UI command fails with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(HomeAssistantError): await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, @@ -326,7 +327,7 @@ async def test_shade( # test cover stop with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(HomeAssistantError): await hass.services.async_call( COVER_DOMAIN, SERVICE_STOP_COVER, @@ -395,7 +396,7 @@ async def test_keen_vent( p2 = patch.object(cluster_level, "request", return_value=[4, 0]) with p1, p2: - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(HomeAssistantError): await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, From 062434532240ed5596b32eef7fa50ca5e8ddd26b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jul 2023 09:14:10 +0200 Subject: [PATCH 0842/1009] Improve `async_track_entity_registry_updated_event` callback typing (#97124) --- homeassistant/components/mqtt/mixins.py | 3 ++- .../components/switch_as_x/__init__.py | 7 +++++-- homeassistant/helpers/entity.py | 6 ++++-- homeassistant/helpers/entity_registry.py | 21 +++++++++++++++---- homeassistant/helpers/event.py | 2 +- 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 54dea780dabaa4..0a2ee68f7c4256 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -54,6 +54,7 @@ UNDEFINED, ConfigType, DiscoveryInfoType, + EventType, UndefinedType, ) from homeassistant.util.json import json_loads @@ -616,7 +617,7 @@ async def async_remove_discovery_payload( async def async_clear_discovery_topic_if_entity_removed( hass: HomeAssistant, discovery_data: DiscoveryInfoType, - event: Event, + event: EventType[er.EventEntityRegistryUpdatedData], ) -> None: """Clear the discovery topic if the entity is removed.""" if event.data["action"] == "remove": diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index ef64a86c6e8a7e..e2ad91e990ecae 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -8,9 +8,10 @@ from homeassistant.components.homeassistant import exposed_entities from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.event import async_track_entity_registry_updated_event +from homeassistant.helpers.typing import EventType from .const import CONF_TARGET_DOMAIN from .light import LightSwitch @@ -55,7 +56,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False - async def async_registry_updated(event: Event) -> None: + async def async_registry_updated( + event: EventType[er.EventEntityRegistryUpdatedData], + ) -> None: """Handle entity registry update.""" data = event.data if data["action"] == "remove": diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 55dc69540fdfb4..a720c1831d7e0c 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -45,7 +45,7 @@ async_track_device_registry_updated_event, async_track_entity_registry_updated_event, ) -from .typing import UNDEFINED, StateType, UndefinedType +from .typing import UNDEFINED, EventType, StateType, UndefinedType if TYPE_CHECKING: from .entity_platform import EntityPlatform @@ -1097,7 +1097,9 @@ async def async_internal_will_remove_from_hass(self) -> None: if self.platform: self.hass.data[DATA_ENTITY_SOURCE].pop(self.entity_id) - async def _async_registry_updated(self, event: Event) -> None: + async def _async_registry_updated( + self, event: EventType[er.EventEntityRegistryUpdatedData] + ) -> None: """Handle entity registry update.""" data = event.data if data["action"] == "remove": diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 248db9d5180793..a46dd3c3a52477 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -108,15 +108,28 @@ class RegistryEntryHider(StrEnum): USER = "user" -class EventEntityRegistryUpdatedData(TypedDict): - """EventEntityRegistryUpdated data.""" +class _EventEntityRegistryUpdatedData_CreateRemove(TypedDict): + """EventEntityRegistryUpdated data for action type 'create' and 'remove'.""" - action: Literal["create", "remove", "update"] + action: Literal["create", "remove"] entity_id: str - changes: NotRequired[dict[str, Any]] + + +class _EventEntityRegistryUpdatedData_Update(TypedDict): + """EventEntityRegistryUpdated data for action type 'update'.""" + + action: Literal["update"] + entity_id: str + changes: dict[str, Any] # Required with action == "update" old_entity_id: NotRequired[str] +EventEntityRegistryUpdatedData = ( + _EventEntityRegistryUpdatedData_CreateRemove + | _EventEntityRegistryUpdatedData_Update +) + + EntityOptionsType = Mapping[str, Mapping[str, Any]] ReadOnlyEntityOptionsType = ReadOnlyDict[str, Mapping[str, Any]] diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 12cf58eaa2b476..b31efac92bceea 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -416,7 +416,7 @@ def _async_dispatch_old_entity_id_or_entity_id_event( ) -> None: """Dispatch to listeners.""" if not ( - callbacks_list := callbacks.get( + callbacks_list := callbacks.get( # type: ignore[call-overload] # mypy bug? event.data.get("old_entity_id", event.data["entity_id"]) ) ): From daa76bbab6e9b28dccc51edf329f28f0cda34b01 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 24 Jul 2023 09:39:48 +0200 Subject: [PATCH 0843/1009] Migrate Yeelight to has entity naming (#96836) --- .../components/yeelight/binary_sensor.py | 7 ++--- homeassistant/components/yeelight/entity.py | 1 + homeassistant/components/yeelight/light.py | 29 +++++++++---------- .../components/yeelight/strings.json | 15 ++++++++++ 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index f78b4e1401da79..88779e03b6c635 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -28,6 +28,8 @@ async def async_setup_entry( class YeelightNightlightModeSensor(YeelightEntity, BinarySensorEntity): """Representation of a Yeelight nightlight mode sensor.""" + _attr_translation_key = "nightlight" + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" self.async_on_remove( @@ -44,11 +46,6 @@ def unique_id(self) -> str: """Return a unique ID.""" return f"{self._unique_id}-nightlight_sensor" - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._device.name} nightlight" - @property def is_on(self): """Return true if nightlight mode is on.""" diff --git a/homeassistant/components/yeelight/entity.py b/homeassistant/components/yeelight/entity.py index 53211115dd6bc4..9422ec9980d122 100644 --- a/homeassistant/components/yeelight/entity.py +++ b/homeassistant/components/yeelight/entity.py @@ -12,6 +12,7 @@ class YeelightEntity(Entity): """Represents single Yeelight entity.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__(self, device: YeelightDevice, entry: ConfigEntry) -> None: """Initialize the entity.""" diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 9ac457b79e96c1..35739b0f596245 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -472,11 +472,6 @@ def color_temp(self) -> int | None: self._color_temp = kelvin_to_mired(int(temp_in_k)) return self._color_temp - @property - def name(self) -> str: - """Return the name of the device if any.""" - return self.device.name - @property def is_on(self) -> bool: """Return true if device is on.""" @@ -892,6 +887,7 @@ def _predefined_effects(self) -> list[str]: class YeelightWhiteTempLightSupport(YeelightGenericLight): """Representation of a White temp Yeelight light.""" + _attr_name = None _attr_color_mode = ColorMode.COLOR_TEMP _attr_supported_color_modes = {ColorMode.COLOR_TEMP} @@ -943,6 +939,8 @@ class YeelightColorLightWithNightlightSwitch( It represents case when nightlight switch is set to light. """ + _attr_name = None + @property def is_on(self) -> bool: """Return true if device is on.""" @@ -954,6 +952,8 @@ class YeelightWhiteTempWithoutNightlightSwitch( ): """White temp light, when nightlight switch is not set to light.""" + _attr_name = None + class YeelightWithNightLight( YeelightNightLightSupport, YeelightWhiteTempLightSupport, YeelightGenericLight @@ -963,6 +963,8 @@ class YeelightWithNightLight( It represents case when nightlight switch is set to light. """ + _attr_name = None + @property def is_on(self) -> bool: """Return true if device is on.""" @@ -975,6 +977,7 @@ class YeelightNightLightMode(YeelightGenericLight): _attr_color_mode = ColorMode.BRIGHTNESS _attr_icon = "mdi:weather-night" _attr_supported_color_modes = {ColorMode.BRIGHTNESS} + _attr_translation_key = "nightlight" @property def unique_id(self) -> str: @@ -982,11 +985,6 @@ def unique_id(self) -> str: unique = super().unique_id return f"{unique}-nightlight" - @property - def name(self) -> str: - """Return the name of the device if any.""" - return f"{self.device.name} Nightlight" - @property def is_on(self) -> bool: """Return true if device is on.""" @@ -1030,6 +1028,8 @@ class YeelightWithAmbientWithoutNightlight(YeelightWhiteTempWithoutNightlightSwi And nightlight switch type is none. """ + _attr_name = None + @property def _power_property(self) -> str: return "main_power" @@ -1041,6 +1041,8 @@ class YeelightWithAmbientAndNightlight(YeelightWithNightLight): And nightlight switch type is set to light. """ + _attr_name = None + @property def _power_property(self) -> str: return "main_power" @@ -1049,6 +1051,8 @@ def _power_property(self) -> str: class YeelightAmbientLight(YeelightColorLightWithoutNightlightSwitch): """Representation of a Yeelight ambient light.""" + _attr_translation_key = "ambilight" + PROPERTIES_MAPPING = {"color_mode": "bg_lmode"} def __init__(self, *args, **kwargs): @@ -1065,11 +1069,6 @@ def unique_id(self) -> str: unique = super().unique_id return f"{unique}-ambilight" - @property - def name(self) -> str: - """Return the name of the device if any.""" - return f"{self.device.name} Ambilight" - @property def _brightness_property(self) -> str: return "bright" diff --git a/homeassistant/components/yeelight/strings.json b/homeassistant/components/yeelight/strings.json index 03a93bd9a5bfe4..ab22f42dae3883 100644 --- a/homeassistant/components/yeelight/strings.json +++ b/homeassistant/components/yeelight/strings.json @@ -38,6 +38,21 @@ } } }, + "entity": { + "binary_sensor": { + "nightlight": { + "name": "[%key:component::yeelight::entity::light::nightlight::name%]" + } + }, + "light": { + "nightlight": { + "name": "Nightlight" + }, + "ambilight": { + "name": "Ambilight" + } + } + }, "services": { "set_mode": { "name": "Set mode", From 3371c41bda0b20f73534a891bd1cf89f4b65fa02 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jul 2023 09:42:01 +0200 Subject: [PATCH 0844/1009] Improve `async_track_device_registry_updated_event` callback typing (#97125) --- homeassistant/components/mqtt/mixins.py | 19 +++++++++++++------ homeassistant/helpers/device_registry.py | 22 +++++++++++++++++----- homeassistant/helpers/entity.py | 6 ++++-- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 0a2ee68f7c4256..ee7095bb3bc26b 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -34,7 +34,10 @@ device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.device_registry import ( + DeviceEntry, + EventDeviceRegistryUpdatedData, +) from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -720,7 +723,9 @@ async def async_discovery_update( ) return - async def _async_device_removed(self, event: Event) -> None: + async def _async_device_removed( + self, event: EventType[EventDeviceRegistryUpdatedData] + ) -> None: """Handle the manual removal of a device.""" if self._skip_device_removal or not async_removed_from_device( self.hass, event, cast(str, self._device_id), self._config_entry_id @@ -1178,14 +1183,16 @@ def update_device( @callback def async_removed_from_device( - hass: HomeAssistant, event: Event, mqtt_device_id: str, config_entry_id: str + hass: HomeAssistant, + event: EventType[EventDeviceRegistryUpdatedData], + mqtt_device_id: str, + config_entry_id: str, ) -> bool: """Check if the passed event indicates MQTT was removed from a device.""" - action: str = event.data["action"] - if action not in ("remove", "update"): + if event.data["action"] not in ("remove", "update"): return False - if action == "update": + if event.data["action"] == "update": if "config_entries" not in event.data["changes"]: return False device_registry = dr.async_get(hass) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 45a4459b5d3ed8..f1eed86f10c001 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -10,7 +10,6 @@ from urllib.parse import urlparse import attr -from typing_extensions import NotRequired from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback @@ -97,12 +96,25 @@ class DeviceEntryDisabler(StrEnum): DEVICE_INFO_KEYS = set.union(*(itm for itm in DEVICE_INFO_TYPES.values())) -class EventDeviceRegistryUpdatedData(TypedDict): - """EventDeviceRegistryUpdated data.""" +class _EventDeviceRegistryUpdatedData_CreateRemove(TypedDict): + """EventDeviceRegistryUpdated data for action type 'create' and 'remove'.""" - action: Literal["create", "remove", "update"] + action: Literal["create", "remove"] device_id: str - changes: NotRequired[dict[str, Any]] + + +class _EventDeviceRegistryUpdatedData_Update(TypedDict): + """EventDeviceRegistryUpdated data for action type 'update'.""" + + action: Literal["update"] + device_id: str + changes: dict[str, Any] + + +EventDeviceRegistryUpdatedData = ( + _EventDeviceRegistryUpdatedData_CreateRemove + | _EventDeviceRegistryUpdatedData_Update +) class DeviceEntryType(StrEnum): diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index a720c1831d7e0c..acb5568ccb0552 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -40,7 +40,7 @@ from homeassistant.util import dt as dt_util, ensure_unique_string, slugify from . import device_registry as dr, entity_registry as er -from .device_registry import DeviceEntryType +from .device_registry import DeviceEntryType, EventDeviceRegistryUpdatedData from .event import ( async_track_device_registry_updated_event, async_track_entity_registry_updated_event, @@ -1146,7 +1146,9 @@ def _async_unsubscribe_device_updates(self) -> None: self._unsub_device_updates = None @callback - def _async_device_registry_updated(self, event: Event) -> None: + def _async_device_registry_updated( + self, event: EventType[EventDeviceRegistryUpdatedData] + ) -> None: """Handle device registry update.""" data = event.data From c0da6b822ea93efe5118892b730d594092aea345 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jul 2023 10:34:16 +0200 Subject: [PATCH 0845/1009] Fix ruff (#97131) --- homeassistant/components/mqtt/mixins.py | 2 +- homeassistant/helpers/entity.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index ee7095bb3bc26b..70b681ffbb2f60 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -28,7 +28,7 @@ CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index acb5568ccb0552..8e07897c84f3d6 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -34,7 +34,7 @@ STATE_UNKNOWN, EntityCategory, ) -from homeassistant.core import CALLBACK_TYPE, Context, Event, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, NoEntitySpecifiedError from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util, ensure_unique_string, slugify From 582499a2601babe436a24b7637142fec25f54a00 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jul 2023 12:42:17 +0200 Subject: [PATCH 0846/1009] Improve `async_track_template_result` callback typing (#97135) --- .../components/bayesian/binary_sensor.py | 13 +- homeassistant/components/template/trigger.py | 21 +- .../components/universal/media_player.py | 6 +- .../components/websocket_api/commands.py | 5 +- homeassistant/helpers/event.py | 6 +- homeassistant/helpers/template_entity.py | 14 +- tests/helpers/test_event.py | 235 ++++++++++++++---- 7 files changed, 228 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 43411e9ec0daa2..49965a38b7736a 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -27,7 +27,7 @@ STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConditionError, TemplateError from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv @@ -259,14 +259,13 @@ def async_threshold_sensor_state_listener( @callback def _async_template_result_changed( - event: Event | None, updates: list[TrackTemplateResult] + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], ) -> None: track_template_result = updates.pop() template = track_template_result.template result = track_template_result.result - entity: str | None = ( - None if event is None else event.data.get(CONF_ENTITY_ID) - ) + entity_id = None if event is None else event.data["entity_id"] if isinstance(result, TemplateError): _LOGGER.error( "TemplateError('%s') while processing template '%s' in entity '%s'", @@ -283,8 +282,8 @@ def _async_template_result_changed( observation.observed = observed # in some cases a template may update because of the absence of an entity - if entity is not None: - observation.entity_id = entity + if entity_id is not None: + observation.entity_id = entity_id self.current_observations[observation.id] = observation diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 0cc53d5fb2db82..113da3aa3eeafc 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -1,5 +1,7 @@ """Offer template automation rules.""" +from datetime import timedelta import logging +from typing import Any import voluptuous as vol @@ -8,13 +10,15 @@ from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.event import ( + EventStateChangedData, TrackTemplate, + TrackTemplateResult, async_call_later, async_track_template_result, ) from homeassistant.helpers.template import Template, result_as_boolean from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, EventType _LOGGER = logging.getLogger(__name__) @@ -59,7 +63,10 @@ async def async_attach_trigger( ) @callback - def template_listener(event, updates): + def template_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: """Listen for state changes and calls action.""" nonlocal delay_cancel, armed result = updates.pop().result @@ -88,9 +95,9 @@ def template_listener(event, updates): # Fire! armed = False - entity_id = event and event.data.get("entity_id") - from_s = event and event.data.get("old_state") - to_s = event and event.data.get("new_state") + entity_id = event and event.data["entity_id"] + from_s = event and event.data["old_state"] + to_s = event and event.data["new_state"] if entity_id is not None: description = f"{entity_id} via template" @@ -110,7 +117,7 @@ def template_listener(event, updates): } @callback - def call_action(*_): + def call_action(*_: Any) -> None: """Call action with right context.""" nonlocal trigger_variables hass.async_run_hass_job( @@ -124,7 +131,7 @@ def call_action(*_): return try: - period = cv.positive_time_period( + period: timedelta = cv.positive_time_period( template.render_complex(time_delta, {"trigger": template_variables}) ) except (exceptions.TemplateError, vol.Invalid) as ex: diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 94034cdffe54ab..c221a10284a9fc 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -87,6 +87,7 @@ from homeassistant.helpers.event import ( EventStateChangedData, TrackTemplate, + TrackTemplateResult, async_track_state_change_event, async_track_template_result, ) @@ -192,7 +193,10 @@ def _async_on_dependency_update( self.async_schedule_update_ha_state(True) @callback - def _async_on_template_update(event, updates): + def _async_on_template_update( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: """Update state when template state changes.""" for data in updates: template = data.template diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index ea00de33390aaf..bbcbfa6ecb8e37 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -26,6 +26,7 @@ from homeassistant.helpers import config_validation as cv, entity, template from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import ( + EventStateChangedData, TrackTemplate, TrackTemplateResult, async_track_template_result, @@ -37,6 +38,7 @@ json_dumps, ) from homeassistant.helpers.service import async_get_all_descriptions +from homeassistant.helpers.typing import EventType from homeassistant.loader import ( Integration, IntegrationNotFound, @@ -535,7 +537,8 @@ async def handle_render_template( @callback def _template_listener( - event: Event | None, updates: list[TrackTemplateResult] + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], ) -> None: nonlocal info track_template_result = updates.pop() diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index b31efac92bceea..e615a6422f08b0 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -888,9 +888,7 @@ def __init__( self, hass: HomeAssistant, track_templates: Sequence[TrackTemplate], - action: Callable[ - [EventType[EventStateChangedData] | None, list[TrackTemplateResult]], None - ], + action: TrackTemplateResultListener, has_super_template: bool = False, ) -> None: """Handle removal / refresh of tracker init.""" @@ -1209,7 +1207,7 @@ def _apply_update( EventType[EventStateChangedData] | None, list[TrackTemplateResult], ], - None, + Coroutine[Any, Any, None] | None, ] """Type for the listener for template results. diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py index fcd98a778312d8..e60c58456d9953 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -15,7 +15,6 @@ SensorEntity, ) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, ATTR_ICON, @@ -33,7 +32,12 @@ from . import config_validation as cv from .entity import Entity -from .event import TrackTemplate, TrackTemplateResult, async_track_template_result +from .event import ( + EventStateChangedData, + TrackTemplate, + TrackTemplateResult, + async_track_template_result, +) from .script import Script, _VarsType from .template import ( Template, @@ -42,7 +46,7 @@ render_complex, result_as_boolean, ) -from .typing import ConfigType +from .typing import ConfigType, EventType _LOGGER = logging.getLogger(__name__) @@ -327,14 +331,14 @@ def add_template_attribute( @callback def _handle_results( self, - event: Event | None, + event: EventType[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: """Call back the results to the attributes.""" if event: self.async_set_context(event.context) - entity_id = event and event.data.get(ATTR_ENTITY_ID) + entity_id = event and event.data["entity_id"] if entity_id and entity_id == self.entity_id: self._self_ref_update_count += 1 diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index ee33e20173c769..3c81977c3939e2 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -965,7 +965,10 @@ async def test_track_template_result(hass: HomeAssistant) -> None: "{{(states.sensor.test.state|int) + test }}", hass ) - def specific_run_callback(event, updates): + def specific_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: track_result = updates.pop() specific_runs.append(int(track_result.result)) @@ -974,7 +977,10 @@ def specific_run_callback(event, updates): ) @ha.callback - def wildcard_run_callback(event, updates): + def wildcard_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: track_result = updates.pop() wildcard_runs.append( (int(track_result.last_result or 0), int(track_result.result)) @@ -984,7 +990,10 @@ def wildcard_run_callback(event, updates): hass, [TrackTemplate(template_condition, None)], wildcard_run_callback ) - async def wildercard_run_callback(event, updates): + async def wildercard_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: track_result = updates.pop() wildercard_runs.append( (int(track_result.last_result or 0), int(track_result.result)) @@ -1051,7 +1060,10 @@ async def test_track_template_result_none(hass: HomeAssistant) -> None: "{{(state_attr('sensor.test', 'battery')|int(default=0)) + test }}", hass ) - def specific_run_callback(event, updates): + def specific_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: track_result = updates.pop() result = int(track_result.result) if track_result.result is not None else None specific_runs.append(result) @@ -1061,7 +1073,10 @@ def specific_run_callback(event, updates): ) @ha.callback - def wildcard_run_callback(event, updates): + def wildcard_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: track_result = updates.pop() last_result = ( int(track_result.last_result) @@ -1075,7 +1090,10 @@ def wildcard_run_callback(event, updates): hass, [TrackTemplate(template_condition, None)], wildcard_run_callback ) - async def wildercard_run_callback(event, updates): + async def wildercard_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: track_result = updates.pop() last_result = ( int(track_result.last_result) @@ -1122,7 +1140,10 @@ async def test_track_template_result_super_template(hass: HomeAssistant) -> None "{{(states.sensor.test.state|int) + test }}", hass ) - def specific_run_callback(event, updates): + def specific_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: for track_result in updates: if track_result.template is template_condition: specific_runs.append(int(track_result.result)) @@ -1140,7 +1161,10 @@ def specific_run_callback(event, updates): ) @ha.callback - def wildcard_run_callback(event, updates): + def wildcard_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: for track_result in updates: if track_result.template is template_condition: wildcard_runs.append( @@ -1159,7 +1183,10 @@ def wildcard_run_callback(event, updates): has_super_template=True, ) - async def wildercard_run_callback(event, updates): + async def wildercard_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: for track_result in updates: if track_result.template is template_condition_var: wildercard_runs.append( @@ -1272,7 +1299,10 @@ async def test_track_template_result_super_template_initially_false( hass.states.async_set("sensor.test", "unavailable") await hass.async_block_till_done() - def specific_run_callback(event, updates): + def specific_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: for track_result in updates: if track_result.template is template_condition: specific_runs.append(int(track_result.result)) @@ -1290,7 +1320,10 @@ def specific_run_callback(event, updates): ) @ha.callback - def wildcard_run_callback(event, updates): + def wildcard_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: for track_result in updates: if track_result.template is template_condition: wildcard_runs.append( @@ -1309,7 +1342,10 @@ def wildcard_run_callback(event, updates): has_super_template=True, ) - async def wildercard_run_callback(event, updates): + async def wildercard_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: for track_result in updates: if track_result.template is template_condition_var: wildercard_runs.append( @@ -1434,7 +1470,10 @@ def _super_template_as_boolean(result): return result_as_boolean(result) - def specific_run_callback(event, updates): + def specific_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: for track_result in updates: if track_result.template is template_condition: specific_runs.append(int(track_result.result)) @@ -1454,7 +1493,10 @@ def specific_run_callback(event, updates): ) @ha.callback - def wildcard_run_callback(event, updates): + def wildcard_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: for track_result in updates: if track_result.template is template_condition: wildcard_runs.append( @@ -1475,7 +1517,10 @@ def wildcard_run_callback(event, updates): has_super_template=True, ) - async def wildercard_run_callback(event, updates): + async def wildercard_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: for track_result in updates: if track_result.template is template_condition_var: wildercard_runs.append( @@ -1580,7 +1625,10 @@ def _super_template_as_boolean(result): return result_as_boolean(result) - def specific_run_callback(event, updates): + def specific_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: for track_result in updates: if track_result.template is template_condition: specific_runs.append(int(track_result.result)) @@ -1600,7 +1648,10 @@ def specific_run_callback(event, updates): ) @ha.callback - def wildcard_run_callback(event, updates): + def wildcard_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: for track_result in updates: if track_result.template is template_condition: wildcard_runs.append( @@ -1621,7 +1672,10 @@ def wildcard_run_callback(event, updates): has_super_template=True, ) - async def wildercard_run_callback(event, updates): + async def wildercard_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: for track_result in updates: if track_result.template is template_condition_var: wildercard_runs.append( @@ -1701,7 +1755,10 @@ async def test_track_template_result_complex(hass: HomeAssistant) -> None: """ template_complex = Template(template_complex_str, hass) - def specific_run_callback(event, updates): + def specific_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: specific_runs.append(updates.pop().result) hass.states.async_set("light.one", "on") @@ -1854,7 +1911,10 @@ async def test_track_template_result_with_wildcard(hass: HomeAssistant) -> None: """ template_complex = Template(template_complex_str, hass) - def specific_run_callback(event, updates): + def specific_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: specific_runs.append(updates.pop().result) hass.states.async_set("cover.office_drapes", "closed") @@ -1906,7 +1966,10 @@ async def test_track_template_result_with_group(hass: HomeAssistant) -> None: """ template_complex = Template(template_complex_str, hass) - def specific_run_callback(event, updates): + def specific_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: specific_runs.append(updates.pop().result) info = async_track_template_result( @@ -1963,7 +2026,10 @@ async def test_track_template_result_and_conditional(hass: HomeAssistant) -> Non template = Template(template_str, hass) - def specific_run_callback(event, updates): + def specific_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: specific_runs.append(updates.pop().result) info = async_track_template_result( @@ -2028,7 +2094,10 @@ async def test_track_template_result_and_conditional_upper_case( template = Template(template_str, hass) - def specific_run_callback(event, updates): + def specific_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: specific_runs.append(updates.pop().result) info = async_track_template_result( @@ -2087,7 +2156,10 @@ async def test_track_template_result_iterator(hass: HomeAssistant) -> None: iterator_runs = [] @ha.callback - def iterator_callback(event, updates): + def iterator_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: iterator_runs.append(updates.pop().result) async_track_template_result( @@ -2120,7 +2192,10 @@ def iterator_callback(event, updates): filter_runs = [] @ha.callback - def filter_callback(event, updates): + def filter_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: filter_runs.append(updates.pop().result) info = async_track_template_result( @@ -2170,7 +2245,10 @@ async def test_track_template_result_errors( not_exist_runs = [] @ha.callback - def syntax_error_listener(event, updates): + def syntax_error_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: track_result = updates.pop() syntax_error_runs.append( ( @@ -2190,7 +2268,10 @@ def syntax_error_listener(event, updates): assert "TemplateSyntaxError" in caplog.text @ha.callback - def not_exist_runs_error_listener(event, updates): + def not_exist_runs_error_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: template_track = updates.pop() not_exist_runs.append( ( @@ -2255,7 +2336,10 @@ async def test_track_template_result_transient_errors( sometimes_error_runs = [] @ha.callback - def sometimes_error_listener(event, updates): + def sometimes_error_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: track_result = updates.pop() sometimes_error_runs.append( ( @@ -2300,7 +2384,10 @@ async def test_static_string(hass: HomeAssistant) -> None: refresh_runs = [] @ha.callback - def refresh_listener(event, updates): + def refresh_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: refresh_runs.append(updates.pop().result) info = async_track_template_result( @@ -2320,7 +2407,10 @@ async def test_track_template_rate_limit(hass: HomeAssistant) -> None: refresh_runs = [] @ha.callback - def refresh_listener(event, updates): + def refresh_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: refresh_runs.append(updates.pop().result) info = async_track_template_result( @@ -2379,7 +2469,10 @@ async def test_track_template_rate_limit_super(hass: HomeAssistant) -> None: refresh_runs = [] @ha.callback - def refresh_listener(event, updates): + def refresh_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: for track_result in updates: if track_result.template is template_refresh: refresh_runs.append(track_result.result) @@ -2452,7 +2545,10 @@ async def test_track_template_rate_limit_super_2(hass: HomeAssistant) -> None: refresh_runs = [] @ha.callback - def refresh_listener(event, updates): + def refresh_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: for track_result in updates: if track_result.template is template_refresh: refresh_runs.append(track_result.result) @@ -2521,7 +2617,10 @@ async def test_track_template_rate_limit_super_3(hass: HomeAssistant) -> None: refresh_runs = [] @ha.callback - def refresh_listener(event, updates): + def refresh_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: for track_result in updates: if track_result.template is template_refresh: refresh_runs.append(track_result.result) @@ -2592,7 +2691,10 @@ async def test_track_template_rate_limit_suppress_listener(hass: HomeAssistant) refresh_runs = [] @ha.callback - def refresh_listener(event, updates): + def refresh_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: refresh_runs.append(updates.pop().result) info = async_track_template_result( @@ -2689,7 +2791,10 @@ async def test_track_template_rate_limit_five(hass: HomeAssistant) -> None: refresh_runs = [] @ha.callback - def refresh_listener(event, updates): + def refresh_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: refresh_runs.append(updates.pop().result) info = async_track_template_result( @@ -2725,7 +2830,10 @@ async def test_track_template_has_default_rate_limit(hass: HomeAssistant) -> Non refresh_runs = [] @ha.callback - def refresh_listener(event, updates): + def refresh_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: refresh_runs.append(updates.pop().result) info = async_track_template_result( @@ -2766,7 +2874,10 @@ async def test_track_template_unavailable_states_has_default_rate_limit( refresh_runs = [] @ha.callback - def refresh_listener(event, updates): + def refresh_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: refresh_runs.append(updates.pop().result) info = async_track_template_result( @@ -2807,7 +2918,10 @@ async def test_specifically_referenced_entity_is_not_rate_limited( refresh_runs = [] @ha.callback - def refresh_listener(event, updates): + def refresh_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: refresh_runs.append(updates.pop().result) info = async_track_template_result( @@ -2850,7 +2964,10 @@ async def test_track_two_templates_with_different_rate_limits( } @ha.callback - def refresh_listener(event, updates): + def refresh_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: for update in updates: refresh_runs[update.template].append(update.result) @@ -2911,7 +3028,10 @@ async def test_string(hass: HomeAssistant) -> None: refresh_runs = [] @ha.callback - def refresh_listener(event, updates): + def refresh_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: refresh_runs.append(updates.pop().result) info = async_track_template_result( @@ -2931,7 +3051,10 @@ async def test_track_template_result_refresh_cancel(hass: HomeAssistant) -> None refresh_runs = [] @ha.callback - def refresh_listener(event, updates): + def refresh_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: refresh_runs.append(updates.pop().result) info = async_track_template_result( @@ -2993,7 +3116,10 @@ async def test_async_track_template_result_multiple_templates( refresh_runs = [] @ha.callback - def refresh_listener(event, updates): + def refresh_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: refresh_runs.append(updates) async_track_template_result( @@ -3054,7 +3180,10 @@ async def test_async_track_template_result_multiple_templates_mixing_domain( refresh_runs = [] @ha.callback - def refresh_listener(event, updates): + def refresh_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: refresh_runs.append(updates) async_track_template_result( @@ -3139,7 +3268,10 @@ async def test_track_template_with_time(hass: HomeAssistant) -> None: specific_runs = [] template_complex = Template("{{ states.switch.test.state and now() }}", hass) - def specific_run_callback(event, updates): + def specific_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: specific_runs.append(updates.pop().result) info = async_track_template_result( @@ -3169,7 +3301,10 @@ async def test_track_template_with_time_default(hass: HomeAssistant) -> None: specific_runs = [] template_complex = Template("{{ now() }}", hass) - def specific_run_callback(event, updates): + def specific_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: specific_runs.append(updates.pop().result) info = async_track_template_result( @@ -3218,7 +3353,10 @@ async def test_track_template_with_time_that_leaves_scope(hass: HomeAssistant) - hass, ) - def specific_run_callback(event, updates): + def specific_run_callback( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: specific_runs.append(updates.pop().result) info = async_track_template_result( @@ -3283,7 +3421,10 @@ async def test_async_track_template_result_multiple_templates_mixing_listeners( refresh_runs = [] @ha.callback - def refresh_listener(event, updates): + def refresh_listener( + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: refresh_runs.append(updates) now = dt_util.utcnow() From 4161f53beaaabee7f863b57df87b0ee31f42ba60 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jul 2023 12:42:29 +0200 Subject: [PATCH 0847/1009] Improve `async_track_state_change_filtered` callback typing (#97134) --- .../components/geo_location/trigger.py | 17 ++++++++++------- homeassistant/components/zone/__init__.py | 9 +++++---- tests/helpers/test_event.py | 14 +++++++------- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index 24632e784547a1..5527f5ec9f1e2b 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -9,7 +9,6 @@ from homeassistant.const import CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE from homeassistant.core import ( CALLBACK_TYPE, - Event, HassJob, HomeAssistant, State, @@ -17,9 +16,13 @@ ) from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.config_validation import entity_domain -from homeassistant.helpers.event import TrackStates, async_track_state_change_filtered +from homeassistant.helpers.event import ( + EventStateChangedData, + TrackStates, + async_track_state_change_filtered, +) from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, EventType from . import DOMAIN @@ -60,11 +63,11 @@ async def async_attach_trigger( job = HassJob(action) @callback - def state_change_listener(event: Event) -> None: + def state_change_listener(event: EventType[EventStateChangedData]) -> None: """Handle specific state changes.""" # Skip if the event's source does not match the trigger's source. - from_state = event.data.get("old_state") - to_state = event.data.get("new_state") + from_state = event.data["old_state"] + to_state = event.data["new_state"] if not source_match(from_state, source) and not source_match(to_state, source): return @@ -96,7 +99,7 @@ def state_change_listener(event: Event) -> None: **trigger_data, "platform": "geo_location", "source": source, - "entity_id": event.data.get("entity_id"), + "entity_id": event.data["entity_id"], "from_state": from_state, "to_state": to_state, "zone": zone_state, diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 77c225d72ecf43..bfc9c2fce092a0 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -11,7 +11,6 @@ from homeassistant import config_entries from homeassistant.const import ( ATTR_EDITABLE, - ATTR_ENTITY_ID, ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_PERSONS, @@ -378,10 +377,12 @@ async def async_update_config(self, config: ConfigType) -> None: self.async_write_ha_state() @callback - def _person_state_change_listener(self, evt: Event) -> None: - person_entity_id = evt.data[ATTR_ENTITY_ID] + def _person_state_change_listener( + self, evt: EventType[event.EventStateChangedData] + ) -> None: + person_entity_id = evt.data["entity_id"] cur_count = len(self._persons_in_zone) - if self._state_is_in_zone(evt.data.get("new_state")): + if self._state_is_in_zone(evt.data["new_state"]): self._persons_in_zone.add(person_entity_id) elif person_entity_id in self._persons_in_zone: self._persons_in_zone.remove(person_entity_id) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 3c81977c3939e2..2b77da09778d20 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -300,21 +300,21 @@ async def test_async_track_state_change_filtered(hass: HomeAssistant) -> None: multiple_entity_id_tracker = [] @ha.callback - def single_run_callback(event): - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") + def single_run_callback(event: EventType[EventStateChangedData]) -> None: + old_state = event.data["old_state"] + new_state = event.data["new_state"] single_entity_id_tracker.append((old_state, new_state)) @ha.callback - def multiple_run_callback(event): - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") + def multiple_run_callback(event: EventType[EventStateChangedData]) -> None: + old_state = event.data["old_state"] + new_state = event.data["new_state"] multiple_entity_id_tracker.append((old_state, new_state)) @ha.callback - def callback_that_throws(event): + def callback_that_throws(event: EventType[EventStateChangedData]) -> None: raise ValueError track_single = async_track_state_change_filtered( From 995c29e0523e67c87bb85601cece278ff807e5bb Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 24 Jul 2023 13:18:38 +0200 Subject: [PATCH 0848/1009] Cleanup EventType typing (#97136) --- homeassistant/components/history_stats/data.py | 11 +++++++---- .../components/homeassistant/triggers/state.py | 2 +- homeassistant/components/homekit/util.py | 10 ++++++---- homeassistant/components/plant/__init__.py | 2 +- homeassistant/components/purpleair/config_flow.py | 12 +++++++++--- homeassistant/components/switch_as_x/cover.py | 8 ++++++-- homeassistant/components/switch_as_x/entity.py | 10 +++++++--- homeassistant/components/switch_as_x/lock.py | 8 ++++++-- homeassistant/helpers/template_entity.py | 4 ++-- tests/helpers/test_event.py | 12 ++++++------ 10 files changed, 51 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index af27766f5143d1..69e56ba0333f35 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -5,8 +5,10 @@ import datetime from homeassistant.components.recorder import get_instance, history -from homeassistant.core import Event, HomeAssistant, State +from homeassistant.core import HomeAssistant, State +from homeassistant.helpers.event import EventStateChangedData from homeassistant.helpers.template import Template +from homeassistant.helpers.typing import EventType import homeassistant.util.dt as dt_util from .helpers import async_calculate_period, floored_timestamp @@ -55,7 +57,9 @@ def __init__( self._start = start self._end = end - async def async_update(self, event: Event | None) -> HistoryStatsState: + async def async_update( + self, event: EventType[EventStateChangedData] | None + ) -> HistoryStatsState: """Update the stats at a given time.""" # Get previous values of start and end previous_period_start, previous_period_end = self._period @@ -104,8 +108,7 @@ async def async_update(self, event: Event | None) -> HistoryStatsState: ) ): new_data = False - if event and event.data["new_state"] is not None: - new_state: State = event.data["new_state"] + if event and (new_state := event.data["new_state"]) is not None: if ( current_period_start_timestamp <= floored_timestamp(new_state.last_changed) diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index ce2d5e647436b3..eec66a560a5a09 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -129,7 +129,7 @@ async def async_attach_trigger( _variables = trigger_info["variables"] or {} @callback - def state_automation_listener(event: EventType[EventStateChangedData]): + def state_automation_listener(event: EventType[EventStateChangedData]) -> None: """Listen for state changes and calls action.""" entity = event.data["entity_id"] from_s = event.data["old_state"] diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 0e3bcbfee86070..8287c2b7845b58 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -37,9 +37,11 @@ CONF_TYPE, UnitOfTemperature, ) -from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id +from homeassistant.core import HomeAssistant, State, callback, split_entity_id import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import EventStateChangedData from homeassistant.helpers.storage import STORAGE_DIR +from homeassistant.helpers.typing import EventType from homeassistant.util.unit_conversion import TemperatureConverter from .const import ( @@ -619,9 +621,9 @@ def state_needs_accessory_mode(state: State) -> bool: ) -def state_changed_event_is_same_state(event: Event) -> bool: +def state_changed_event_is_same_state(event: EventType[EventStateChangedData]) -> bool: """Check if a state changed event is the same state.""" event_data = event.data - old_state: State | None = event_data.get("old_state") - new_state: State | None = event_data.get("new_state") + old_state = event_data["old_state"] + new_state = event_data["new_state"] return bool(new_state and old_state and new_state.state == old_state.state) diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index ed88e50b932e19..28cdece0b02aea 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -179,7 +179,7 @@ def __init__(self, name, config): self._brightness_history = DailyHistory(self._conf_check_days) @callback - def _state_changed_event(self, event: EventType[EventStateChangedData]): + def _state_changed_event(self, event: EventType[EventStateChangedData]) -> None: """Sensor state change event.""" self.state_changed(event.data["entity_id"], event.data["new_state"]) diff --git a/homeassistant/components/purpleair/config_flow.py b/homeassistant/components/purpleair/config_flow.py index c7988c02e6ae0a..3daa6f96fdfbc6 100644 --- a/homeassistant/components/purpleair/config_flow.py +++ b/homeassistant/components/purpleair/config_flow.py @@ -15,7 +15,7 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import ( aiohttp_client, @@ -23,13 +23,17 @@ device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, SelectSelectorConfig, SelectSelectorMode, ) +from homeassistant.helpers.typing import EventType from .const import CONF_SENSOR_INDICES, CONF_SHOW_ON_MAP, DOMAIN, LOGGER @@ -420,7 +424,9 @@ async def async_step_remove_sensor( device_entities_removed_event = asyncio.Event() @callback - def async_device_entity_state_changed(_: Event) -> None: + def async_device_entity_state_changed( + _: EventType[EventStateChangedData], + ) -> None: """Listen and respond when all device entities are removed.""" if all( self.hass.states.get(entity_entry.entity_id) is None diff --git a/homeassistant/components/switch_as_x/cover.py b/homeassistant/components/switch_as_x/cover.py index 7df3b177217366..b7fe0fbf36439e 100644 --- a/homeassistant/components/switch_as_x/cover.py +++ b/homeassistant/components/switch_as_x/cover.py @@ -17,9 +17,11 @@ SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import EventStateChangedData +from homeassistant.helpers.typing import EventType from .entity import BaseEntity @@ -74,7 +76,9 @@ async def async_close_cover(self, **kwargs: Any) -> None: ) @callback - def async_state_changed_listener(self, event: Event | None = None) -> None: + def async_state_changed_listener( + self, event: EventType[EventStateChangedData] | None = None + ) -> None: """Handle child updates.""" super().async_state_changed_listener(event) if ( diff --git a/homeassistant/components/switch_as_x/entity.py b/homeassistant/components/switch_as_x/entity.py index 36f8a651f06d03..3718c4ebe998c0 100644 --- a/homeassistant/components/switch_as_x/entity.py +++ b/homeassistant/components/switch_as_x/entity.py @@ -12,7 +12,7 @@ STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import DeviceInfo, Entity, ToggleEntity from homeassistant.helpers.event import ( @@ -67,7 +67,9 @@ def __init__( ) @callback - def async_state_changed_listener(self, event: Event | None = None) -> None: + def async_state_changed_listener( + self, event: EventType[EventStateChangedData] | None = None + ) -> None: """Handle child updates.""" if ( state := self.hass.states.get(self._switch_entity_id) @@ -163,7 +165,9 @@ async def async_turn_off(self, **kwargs: Any) -> None: ) @callback - def async_state_changed_listener(self, event: Event | None = None) -> None: + def async_state_changed_listener( + self, event: EventType[EventStateChangedData] | None = None + ) -> None: """Handle child updates.""" super().async_state_changed_listener(event) if ( diff --git a/homeassistant/components/switch_as_x/lock.py b/homeassistant/components/switch_as_x/lock.py index 9778caf8e600ae..9e7606865a11ce 100644 --- a/homeassistant/components/switch_as_x/lock.py +++ b/homeassistant/components/switch_as_x/lock.py @@ -13,9 +13,11 @@ SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import EventStateChangedData +from homeassistant.helpers.typing import EventType from .entity import BaseEntity @@ -68,7 +70,9 @@ async def async_unlock(self, **kwargs: Any) -> None: ) @callback - def async_state_changed_listener(self, event: Event | None = None) -> None: + def async_state_changed_listener( + self, event: EventType[EventStateChangedData] | None = None + ) -> None: """Handle child updates.""" super().async_state_changed_listener(event) if ( diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py index e60c58456d9953..b7be7c2c9a6281 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -26,7 +26,7 @@ EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, ) -from homeassistant.core import Context, CoreState, Event, HomeAssistant, State, callback +from homeassistant.core import Context, CoreState, HomeAssistant, State, callback from homeassistant.exceptions import TemplateError from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads @@ -131,7 +131,7 @@ def _default_update(self, result: str | TemplateError) -> None: @callback def handle_result( self, - event: Event | None, + event: EventType[EventStateChangedData] | None, template: Template, last_result: str | None | TemplateError, result: str | TemplateError, diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 2b77da09778d20..9436226b335809 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -544,14 +544,14 @@ async def test_async_track_state_added_domain(hass: HomeAssistant) -> None: multiple_entity_id_tracker = [] @ha.callback - def single_run_callback(event: EventType[EventStateChangedData]): + def single_run_callback(event: EventType[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] single_entity_id_tracker.append((old_state, new_state)) @ha.callback - def multiple_run_callback(event: EventType[EventStateChangedData]): + def multiple_run_callback(event: EventType[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] @@ -656,14 +656,14 @@ async def test_async_track_state_removed_domain(hass: HomeAssistant) -> None: multiple_entity_id_tracker = [] @ha.callback - def single_run_callback(event: EventType[EventStateChangedData]): + def single_run_callback(event: EventType[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] single_entity_id_tracker.append((old_state, new_state)) @ha.callback - def multiple_run_callback(event: EventType[EventStateChangedData]): + def multiple_run_callback(event: EventType[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] @@ -738,14 +738,14 @@ async def test_async_track_state_removed_domain_match_all(hass: HomeAssistant) - match_all_entity_id_tracker = [] @ha.callback - def single_run_callback(event: EventType[EventStateChangedData]): + def single_run_callback(event: EventType[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] single_entity_id_tracker.append((old_state, new_state)) @ha.callback - def match_all_run_callback(event: EventType[EventStateChangedData]): + def match_all_run_callback(event: EventType[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] From 755b0f9120fd3773bafe7ba48bc2d0be07aab649 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 24 Jul 2023 14:16:29 +0200 Subject: [PATCH 0849/1009] Update xknx to 2.11.2 - fix DPT 9 small negative values (#97137) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 30e239a65a9778..a915d886138f0e 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -11,7 +11,7 @@ "loggers": ["xknx", "xknxproject"], "quality_scale": "platinum", "requirements": [ - "xknx==2.11.1", + "xknx==2.11.2", "xknxproject==3.2.0", "knx-frontend==2023.6.23.191712" ] diff --git a/requirements_all.txt b/requirements_all.txt index 731d3c750a84bb..9c0659fac39b90 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2690,7 +2690,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.20.0 # homeassistant.components.knx -xknx==2.11.1 +xknx==2.11.2 # homeassistant.components.knx xknxproject==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf55bdda53c758..e6f4644715991f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1972,7 +1972,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.20.0 # homeassistant.components.knx -xknx==2.11.1 +xknx==2.11.2 # homeassistant.components.knx xknxproject==3.2.0 From 14524b985bc6a3a8695f081da144f84698224310 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 24 Jul 2023 14:18:39 +0200 Subject: [PATCH 0850/1009] Handle Matter Nullable as None (#97133) --- homeassistant/components/matter/entity.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index 0457cfaa8107b4..a30939912253c8 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -7,7 +7,7 @@ import logging from typing import TYPE_CHECKING, Any, cast -from chip.clusters.Objects import ClusterAttributeDescriptor +from chip.clusters.Objects import ClusterAttributeDescriptor, NullValue from matter_server.common.helpers.util import create_attribute_path from matter_server.common.models import EventType, ServerInfoMessage @@ -122,10 +122,13 @@ def _update_from_device(self) -> None: @callback def get_matter_attribute_value( - self, attribute: type[ClusterAttributeDescriptor] + self, attribute: type[ClusterAttributeDescriptor], null_as_none: bool = True ) -> Any: """Get current value for given attribute.""" - return self._endpoint.get_attribute_value(None, attribute) + value = self._endpoint.get_attribute_value(None, attribute) + if null_as_none and value == NullValue: + return None + return value @callback def get_matter_attribute_path( From 0c4e3411891e1dec4dc937979749abfccef11f77 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 24 Jul 2023 14:22:09 +0200 Subject: [PATCH 0851/1009] Fix typos in Radio Browser comment and docstring (#97138) --- homeassistant/components/radio_browser/media_source.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/radio_browser/media_source.py b/homeassistant/components/radio_browser/media_source.py index e49f670d37168f..dffbdc42dbec46 100644 --- a/homeassistant/components/radio_browser/media_source.py +++ b/homeassistant/components/radio_browser/media_source.py @@ -28,7 +28,7 @@ async def async_get_media_source(hass: HomeAssistant) -> RadioMediaSource: """Set up Radio Browser media source.""" - # Radio browser support only a single config entry + # Radio browser supports only a single config entry entry = hass.config_entries.async_entries(DOMAIN)[0] return RadioMediaSource(hass, entry) @@ -40,7 +40,7 @@ class RadioMediaSource(MediaSource): name = "Radio Browser" def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: - """Initialize CameraMediaSource.""" + """Initialize RadioMediaSource.""" super().__init__(DOMAIN) self.hass = hass self.entry = entry From b655b9d530e936fc59e63b4443c3ed9fe9bf5914 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 24 Jul 2023 15:57:02 +0200 Subject: [PATCH 0852/1009] Allow for translating service examples (#97141) --- homeassistant/helpers/service.py | 4 ++++ script/hassfest/translations.py | 1 + tests/helpers/test_service.py | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 5470a94896da57..74823dea95335d 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -666,6 +666,10 @@ async def async_get_all_descriptions( f"component.{domain}.services.{service_name}.fields.{field_name}.description" ): field_schema["description"] = desc + if example := translations.get( + f"component.{domain}.services.{service_name}.fields.{field_name}.example" + ): + field_schema["example"] = example if "target" in yaml_description: description["target"] = yaml_description["target"] diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 597b8e1ae1f62e..1754c166ef7b72 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -334,6 +334,7 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: { vol.Required("name"): str, vol.Required("description"): translation_value_validator, + vol.Optional("example"): translation_value_validator, }, slug_validator=translation_key_validator, ), diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 7348e1bf3e22d0..56ee3f74140e9b 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -573,6 +573,7 @@ async def async_get_translations( f"{translation_key_prefix}.description": "Translated description", f"{translation_key_prefix}.fields.level.name": "Field name", f"{translation_key_prefix}.fields.level.description": "Field description", + f"{translation_key_prefix}.fields.level.example": "Field example", } with patch( @@ -599,6 +600,10 @@ async def async_get_translations( ] == "Field description" ) + assert ( + descriptions[logger.DOMAIN]["set_default_level"]["fields"]["level"]["example"] + == "Field example" + ) hass.services.async_register(logger.DOMAIN, "new_service", lambda x: None, None) service.async_set_service_schema( From 57c640c83cbeed97e4ccecac0d32b0a7ac59daf0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jul 2023 09:58:26 -0500 Subject: [PATCH 0853/1009] Reduce attribute lookups in climate needed to write state (#97145) --- homeassistant/components/climate/__init__.py | 58 +++++++++----------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index e62cb1143b5d80..907ff84491bb1f 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -239,11 +239,12 @@ class ClimateEntity(Entity): @property def state(self) -> str | None: """Return the current state.""" - if self.hvac_mode is None: + hvac_mode = self.hvac_mode + if hvac_mode is None: return None - if not isinstance(self.hvac_mode, HVACMode): - return HVACMode(self.hvac_mode).value - return self.hvac_mode.value + if not isinstance(hvac_mode, HVACMode): + return HVACMode(hvac_mode).value + return hvac_mode.value @property def precision(self) -> float: @@ -258,18 +259,18 @@ def precision(self) -> float: def capability_attributes(self) -> dict[str, Any] | None: """Return the capability attributes.""" supported_features = self.supported_features + temperature_unit = self.temperature_unit + precision = self.precision + hass = self.hass + data: dict[str, Any] = { ATTR_HVAC_MODES: self.hvac_modes, - ATTR_MIN_TEMP: show_temp( - self.hass, self.min_temp, self.temperature_unit, self.precision - ), - ATTR_MAX_TEMP: show_temp( - self.hass, self.max_temp, self.temperature_unit, self.precision - ), + ATTR_MIN_TEMP: show_temp(hass, self.min_temp, temperature_unit, precision), + ATTR_MAX_TEMP: show_temp(hass, self.max_temp, temperature_unit, precision), } - if self.target_temperature_step: - data[ATTR_TARGET_TEMP_STEP] = self.target_temperature_step + if target_temperature_step := self.target_temperature_step: + data[ATTR_TARGET_TEMP_STEP] = target_temperature_step if supported_features & ClimateEntityFeature.TARGET_HUMIDITY: data[ATTR_MIN_HUMIDITY] = self.min_humidity @@ -291,39 +292,34 @@ def capability_attributes(self) -> dict[str, Any] | None: def state_attributes(self) -> dict[str, Any]: """Return the optional state attributes.""" supported_features = self.supported_features + temperature_unit = self.temperature_unit + precision = self.precision + hass = self.hass + data: dict[str, str | float | None] = { ATTR_CURRENT_TEMPERATURE: show_temp( - self.hass, - self.current_temperature, - self.temperature_unit, - self.precision, + hass, self.current_temperature, temperature_unit, precision ), } if supported_features & ClimateEntityFeature.TARGET_TEMPERATURE: data[ATTR_TEMPERATURE] = show_temp( - self.hass, + hass, self.target_temperature, - self.temperature_unit, - self.precision, + temperature_unit, + precision, ) if supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE: data[ATTR_TARGET_TEMP_HIGH] = show_temp( - self.hass, - self.target_temperature_high, - self.temperature_unit, - self.precision, + hass, self.target_temperature_high, temperature_unit, precision ) data[ATTR_TARGET_TEMP_LOW] = show_temp( - self.hass, - self.target_temperature_low, - self.temperature_unit, - self.precision, + hass, self.target_temperature_low, temperature_unit, precision ) - if self.current_humidity is not None: - data[ATTR_CURRENT_HUMIDITY] = self.current_humidity + if (current_humidity := self.current_humidity) is not None: + data[ATTR_CURRENT_HUMIDITY] = current_humidity if supported_features & ClimateEntityFeature.TARGET_HUMIDITY: data[ATTR_HUMIDITY] = self.target_humidity @@ -331,8 +327,8 @@ def state_attributes(self) -> dict[str, Any]: if supported_features & ClimateEntityFeature.FAN_MODE: data[ATTR_FAN_MODE] = self.fan_mode - if self.hvac_action: - data[ATTR_HVAC_ACTION] = self.hvac_action + if hvac_action := self.hvac_action: + data[ATTR_HVAC_ACTION] = hvac_action if supported_features & ClimateEntityFeature.PRESET_MODE: data[ATTR_PRESET_MODE] = self.preset_mode From 2220396c418019b36e37ef6c86c774fb43f73ddb Mon Sep 17 00:00:00 2001 From: Nerdix <70015952+N3rdix@users.noreply.github.com> Date: Mon, 24 Jul 2023 16:59:39 +0200 Subject: [PATCH 0854/1009] Enable long-term statistics for Fast.com sensor (#97139) --- homeassistant/components/fastdotcom/sensor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py index b3d5f66ae8c887..b20b0213835080 100644 --- a/homeassistant/components/fastdotcom/sensor.py +++ b/homeassistant/components/fastdotcom/sensor.py @@ -3,7 +3,11 @@ from typing import Any -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) from homeassistant.const import UnitOfDataRate from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -31,6 +35,7 @@ class SpeedtestSensor(RestoreEntity, SensorEntity): _attr_name = "Fast.com Download" _attr_device_class = SensorDeviceClass.DATA_RATE _attr_native_unit_of_measurement = UnitOfDataRate.MEGABITS_PER_SECOND + _attr_state_class = SensorStateClass.MEASUREMENT _attr_icon = "mdi:speedometer" _attr_should_poll = False From 6b980eb0a7b66c3054ab138c9b907d830835cceb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 24 Jul 2023 18:35:26 +0200 Subject: [PATCH 0855/1009] Migrate frontend services to support translations (#96342) Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- .../components/frontend/services.yaml | 16 ++-------- .../components/frontend/strings.json | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/frontend/strings.json diff --git a/homeassistant/components/frontend/services.yaml b/homeassistant/components/frontend/services.yaml index 0cc88baf32f811..8e6820fb5bb216 100644 --- a/homeassistant/components/frontend/services.yaml +++ b/homeassistant/components/frontend/services.yaml @@ -1,29 +1,19 @@ # Describes the format for available frontend services set_theme: - name: Set theme - description: Set a theme unless the client selected per-device theme. fields: name: - name: Theme - description: Name of a predefined theme required: true example: "default" selector: theme: include_default: true mode: - name: Mode - description: The mode the theme is for. default: "light" selector: select: options: - - label: "Dark" - value: "dark" - - label: "Light" - value: "light" - + - "dark" + - "light" + translation_key: mode reload_themes: - name: Reload themes - description: Reload themes from YAML configuration. diff --git a/homeassistant/components/frontend/strings.json b/homeassistant/components/frontend/strings.json new file mode 100644 index 00000000000000..b5fdeb612c4fea --- /dev/null +++ b/homeassistant/components/frontend/strings.json @@ -0,0 +1,30 @@ +{ + "services": { + "set_theme": { + "name": "Set the default theme", + "description": "Sets the default theme Home Assistant uses. Can be overridden by a user.", + "fields": { + "name": { + "name": "Theme", + "description": "Name of a theme." + }, + "mode": { + "name": "Mode", + "description": "Theme mode." + } + } + }, + "reload_themes": { + "name": "Reload themes", + "description": "Reloads themes from the YAML-configuration." + } + }, + "selector": { + "mode": { + "options": { + "dark": "Dark", + "light": "Light" + } + } + } +} From 2c42a319a27bf3b00f09702f447a307a4b89cce0 Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 24 Jul 2023 10:37:37 -0600 Subject: [PATCH 0856/1009] Add Fallback to cloud api for Roborock (#96147) Co-authored-by: Franck Nijhof --- homeassistant/components/roborock/__init__.py | 16 ++++++++-------- .../components/roborock/coordinator.py | 18 ++++++++++++++++++ homeassistant/components/roborock/device.py | 9 ++++----- homeassistant/components/roborock/switch.py | 14 +++++--------- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index 1a308f9dff94a2..b310b2bb2ba977 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -36,24 +36,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } product_info = {product.id: product for product in home_data.products} # Create a mqtt_client, which is needed to get the networking information of the device for local connection and in the future, get the map. - mqtt_clients = [ - RoborockMqttClient( + mqtt_clients = { + device.duid: RoborockMqttClient( user_data, DeviceData(device, product_info[device.product_id].model) ) for device in device_map.values() - ] + } network_results = await asyncio.gather( - *(mqtt_client.get_networking() for mqtt_client in mqtt_clients) + *(mqtt_client.get_networking() for mqtt_client in mqtt_clients.values()) ) network_info = { device.duid: result for device, result in zip(device_map.values(), network_results) if result is not None } - await asyncio.gather( - *(mqtt_client.async_disconnect() for mqtt_client in mqtt_clients), - return_exceptions=True, - ) if not network_info: raise ConfigEntryNotReady( "Could not get network information about your devices" @@ -65,7 +61,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device, network_info[device_id], product_info[device.product_id], + mqtt_clients[device.duid], ) + await asyncio.gather( + *(coordinator.verify_api() for coordinator in coordinator_map.values()) + ) # If one device update fails - we still want to set up other devices await asyncio.gather( *( diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index ba9571a95f5d4a..6ba6f3915ec16b 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging +from roborock.cloud_api import RoborockMqttClient from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, NetworkInfo from roborock.exceptions import RoborockException from roborock.local_api import RoborockLocalClient @@ -30,6 +31,7 @@ def __init__( device: HomeDataDevice, device_networking: NetworkInfo, product_info: HomeDataProduct, + cloud_api: RoborockMqttClient | None = None, ) -> None: """Initialize.""" super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) @@ -41,6 +43,7 @@ def __init__( ) device_data = DeviceData(device, product_info.model, device_networking.ip) self.api = RoborockLocalClient(device_data) + self.cloud_api = cloud_api self.device_info = DeviceInfo( name=self.roborock_device_info.device.name, identifiers={(DOMAIN, self.roborock_device_info.device.duid)}, @@ -49,6 +52,21 @@ def __init__( sw_version=self.roborock_device_info.device.fv, ) + async def verify_api(self) -> None: + """Verify that the api is reachable. If it is not, switch clients.""" + try: + await self.api.ping() + except RoborockException: + if isinstance(self.api, RoborockLocalClient): + _LOGGER.warning( + "Using the cloud API for device %s. This is not recommended as it can lead to rate limiting. We recommend making your vacuum accessible by your Home Assistant instance", + self.roborock_device_info.device.duid, + ) + # We use the cloud api if the local api fails to connect. + self.api = self.cloud_api + # Right now this should never be called if the cloud api is the primary api, + # but in the future if it is, a new else should be added. + async def release(self) -> None: """Disconnect from API.""" await self.api.async_disconnect() diff --git a/homeassistant/components/roborock/device.py b/homeassistant/components/roborock/device.py index 86d578d852a6f4..c40e47ada99706 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -2,11 +2,10 @@ from typing import Any -from roborock.api import AttributeCache +from roborock.api import AttributeCache, RoborockClient from roborock.command_cache import CacheableAttribute from roborock.containers import Status from roborock.exceptions import RoborockException -from roborock.local_api import RoborockLocalClient from roborock.roborock_typing import RoborockCommand from homeassistant.exceptions import HomeAssistantError @@ -22,7 +21,7 @@ class RoborockEntity(Entity): _attr_has_entity_name = True def __init__( - self, unique_id: str, device_info: DeviceInfo, api: RoborockLocalClient + self, unique_id: str, device_info: DeviceInfo, api: RoborockClient ) -> None: """Initialize the coordinated Roborock Device.""" self._attr_unique_id = unique_id @@ -30,8 +29,8 @@ def __init__( self._api = api @property - def api(self) -> RoborockLocalClient: - """Return the Api.""" + def api(self) -> RoborockClient: + """Returns the api.""" return self._api def get_cache(self, attribute: CacheableAttribute) -> AttributeCache: diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py index a0b3d5be29597a..312753ced01d60 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -9,13 +9,11 @@ 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 @@ -121,9 +119,8 @@ async def async_setup_entry( valid_entities.append( RoborockSwitch( f"{description.key}_{slugify(coordinator.roborock_device_info.device.duid)}", - coordinator.device_info, + coordinator, description, - coordinator.api, ) ) async_add_entities(valid_entities) @@ -137,13 +134,12 @@ class RoborockSwitch(RoborockEntity, SwitchEntity): def __init__( self, unique_id: str, - device_info: DeviceInfo, - description: RoborockSwitchDescription, - api: RoborockLocalClient, + coordinator: RoborockDataUpdateCoordinator, + entity_description: RoborockSwitchDescription, ) -> None: """Initialize the entity.""" - super().__init__(unique_id, device_info, api) - self.entity_description = description + self.entity_description = entity_description + super().__init__(unique_id, coordinator.device_info, coordinator.api) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" From 36ad24ce01de8c4ecff000b4e914f5be46142a50 Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Mon, 24 Jul 2023 12:42:08 -0400 Subject: [PATCH 0857/1009] Add name and default name to device info of APCUPSD sensors (#94415) --- homeassistant/components/apcupsd/__init__.py | 26 ++++++---- .../components/apcupsd/binary_sensor.py | 10 +--- homeassistant/components/apcupsd/sensor.py | 9 +--- tests/components/apcupsd/__init__.py | 1 + tests/components/apcupsd/test_config_flow.py | 2 +- tests/components/apcupsd/test_init.py | 48 +++++++++++++++++++ 6 files changed, 69 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index 3fb8bf00b8abc6..bfe6fe6c80c407 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -11,6 +11,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -79,16 +80,6 @@ def model(self) -> str | None: return self.status[model_key] return None - @property - def sw_version(self) -> str | None: - """Return the software version of the APCUPSd, if available.""" - return self.status.get("VERSION") - - @property - def hw_version(self) -> str | None: - """Return the firmware version of the UPS, if available.""" - return self.status.get("FIRMWARE") - @property def serial_no(self) -> str | None: """Return the unique serial number of the UPS, if available.""" @@ -99,6 +90,21 @@ def statflag(self) -> str | None: """Return the STATFLAG indicating the status of the UPS, if available.""" return self.status.get("STATFLAG") + @property + def device_info(self) -> DeviceInfo | None: + """Return the DeviceInfo of this APC UPS for the sensors, if serial number is available.""" + if self.serial_no is None: + return None + + return DeviceInfo( + identifiers={(DOMAIN, self.serial_no)}, + model=self.model, + manufacturer="APC", + name=self.name if self.name is not None else "APC UPS", + hw_version=self.status.get("FIRMWARE"), + sw_version=self.status.get("VERSION"), + ) + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self, **kwargs: Any) -> None: """Fetch the latest status from APCUPSd. diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index d45ad561d8dda8..bac8d18d58bcdd 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -9,7 +9,6 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN, VALUE_ONLINE, APCUPSdData @@ -53,13 +52,8 @@ def __init__( # Set up unique id and device info if serial number is available. if (serial_no := data_service.serial_no) is not None: self._attr_unique_id = f"{serial_no}_{description.key}" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, serial_no)}, - model=data_service.model, - manufacturer="APC", - hw_version=data_service.hw_version, - sw_version=data_service.sw_version, - ) + self._attr_device_info = data_service.device_info + self.entity_description = description self._data_service = data_service diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 8b7034357dfda5..745be7e2d63776 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -21,7 +21,6 @@ UnitOfTime, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN, APCUPSdData @@ -496,13 +495,7 @@ def __init__( # Set up unique id and device info if serial number is available. if (serial_no := data_service.serial_no) is not None: self._attr_unique_id = f"{serial_no}_{description.key}" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, serial_no)}, - model=data_service.model, - manufacturer="APC", - hw_version=data_service.hw_version, - sw_version=data_service.sw_version, - ) + self._attr_device_info = data_service.device_info self.entity_description = description self._data_service = data_service diff --git a/tests/components/apcupsd/__init__.py b/tests/components/apcupsd/__init__.py index f5c3f573030414..b8a83f950d0a04 100644 --- a/tests/components/apcupsd/__init__.py +++ b/tests/components/apcupsd/__init__.py @@ -20,6 +20,7 @@ ("CABLE", "USB Cable"), ("DRIVER", "USB UPS Driver"), ("UPSMODE", "Stand Alone"), + ("UPSNAME", "MyUPS"), ("MODEL", "Back-UPS ES 600"), ("STATUS", "ONLINE"), ("LINEV", "124.0 Volts"), diff --git a/tests/components/apcupsd/test_config_flow.py b/tests/components/apcupsd/test_config_flow.py index a9ef4328e865cd..6ac7992f4043dd 100644 --- a/tests/components/apcupsd/test_config_flow.py +++ b/tests/components/apcupsd/test_config_flow.py @@ -124,7 +124,7 @@ async def test_flow_works(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == MOCK_STATUS["MODEL"] + assert result["title"] == MOCK_STATUS["UPSNAME"] assert result["data"] == CONF_DATA mock_setup.assert_called_once() diff --git a/tests/components/apcupsd/test_init.py b/tests/components/apcupsd/test_init.py index 6e00a382e79f28..8c29edabbc1241 100644 --- a/tests/components/apcupsd/test_init.py +++ b/tests/components/apcupsd/test_init.py @@ -8,6 +8,7 @@ from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from . import CONF_DATA, MOCK_MINIMAL_STATUS, MOCK_STATUS, async_init_integration @@ -28,6 +29,53 @@ async def test_async_setup_entry(hass: HomeAssistant, status: OrderedDict) -> No assert state.state == "on" +@pytest.mark.parametrize( + "status", + ( + # We should not create device entries if SERIALNO is not reported. + MOCK_MINIMAL_STATUS, + # We should set the device name to be the friendly UPSNAME field if available. + MOCK_MINIMAL_STATUS | {"SERIALNO": "XXXX", "UPSNAME": "MyUPS"}, + # Otherwise, we should fall back to default device name --- "APC UPS". + MOCK_MINIMAL_STATUS | {"SERIALNO": "XXXX"}, + # We should create all fields of the device entry if they are available. + MOCK_STATUS, + ), +) +async def test_device_entry(hass: HomeAssistant, status: OrderedDict) -> None: + """Test successful setup of device entries.""" + await async_init_integration(hass, status=status) + + # Verify device info is properly set up. + device_entries = dr.async_get(hass) + + if "SERIALNO" not in status: + assert len(device_entries.devices) == 0 + return + + assert len(device_entries.devices) == 1 + entry = device_entries.async_get_device({(DOMAIN, status["SERIALNO"])}) + assert entry is not None + # Specify the mapping between field name and the expected fields in device entry. + fields = { + "UPSNAME": entry.name, + "MODEL": entry.model, + "VERSION": entry.sw_version, + "FIRMWARE": entry.hw_version, + } + + for field, entry_value in fields.items(): + if field in status: + assert entry_value == status[field] + elif field == "UPSNAME": + # Even if UPSNAME is not available, we must fall back to default "APC UPS". + assert entry_value == "APC UPS" + else: + assert entry_value is None + + assert entry.manufacturer == "APC" + + async def test_multiple_integrations(hass: HomeAssistant) -> None: """Test successful setup for multiple entries.""" # Load two integrations from two mock hosts. From 549fef08ad8a0d57d52449fed3f042c1feb0791f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 24 Jul 2023 18:46:54 +0200 Subject: [PATCH 0858/1009] Make Codespell skip snapshot tests (#97150) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d1cae2b0fadd5f..5e24796323c736 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: codespell args: - --ignore-words-list=additionals,alle,alot,ba,bre,bund,currenty,datas,dof,dur,ether,farenheit,falsy,fo,haa,hass,hist,iam,iff,iif,incomfort,ines,ist,lightsensor,mut,nam,nd,pres,pullrequests,referer,resset,rime,ser,serie,sur,te,technik,ue,uint,unsecure,visability,wan,wanna,withing,zar - - --skip="./.*,*.csv,*.json" + - --skip="./.*,*.csv,*.json,*.ambr" - --quiet-level=2 exclude_types: [csv, json] exclude: ^tests/fixtures/|homeassistant/generated/ From 35aae949d0f22efd52483bc2ea79670efbd26ddb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jul 2023 11:48:09 -0500 Subject: [PATCH 0859/1009] Add initial test coverage for ESPHome manager (#97147) --- tests/components/esphome/test_manager.py | 120 +++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 tests/components/esphome/test_manager.py diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py new file mode 100644 index 00000000000000..7a487f3a385ad1 --- /dev/null +++ b/tests/components/esphome/test_manager.py @@ -0,0 +1,120 @@ +"""Test ESPHome manager.""" +from collections.abc import Awaitable, Callable + +from aioesphomeapi import ( + APIClient, + EntityInfo, + EntityState, + UserService, +) + +from homeassistant.components.esphome.const import DOMAIN, STABLE_BLE_VERSION_STR +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from .conftest import MockESPHomeDevice + +from tests.common import MockConfigEntry + + +async def test_esphome_device_with_old_bluetooth( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test a device with old bluetooth creates an issue.""" + entity_info = [] + states = [] + user_service = [] + await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + device_info={"bluetooth_proxy_feature_flags": 1, "esphome_version": "2023.3.0"}, + ) + await hass.async_block_till_done() + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + "esphome", "ble_firmware_outdated-11:22:33:44:55:aa" + ) + assert ( + issue.learn_more_url + == f"https://esphome.io/changelog/{STABLE_BLE_VERSION_STR}.html" + ) + + +async def test_esphome_device_with_password( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test a device with legacy password creates an issue.""" + entity_info = [] + states = [] + user_service = [] + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "test.local", + CONF_PORT: 6053, + CONF_PASSWORD: "has", + }, + ) + entry.add_to_hass(hass) + await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + device_info={"bluetooth_proxy_feature_flags": 0, "esphome_version": "2023.3.0"}, + entry=entry, + ) + await hass.async_block_till_done() + issue_registry = ir.async_get(hass) + assert ( + issue_registry.async_get_issue( + "esphome", "api_password_deprecated-11:22:33:44:55:aa" + ) + is not None + ) + + +async def test_esphome_device_with_current_bluetooth( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test a device with recent bluetooth does not create an issue.""" + entity_info = [] + states = [] + user_service = [] + await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + device_info={ + "bluetooth_proxy_feature_flags": 1, + "esphome_version": STABLE_BLE_VERSION_STR, + }, + ) + await hass.async_block_till_done() + issue_registry = ir.async_get(hass) + assert ( + issue_registry.async_get_issue( + "esphome", "ble_firmware_outdated-11:22:33:44:55:aa" + ) + is None + ) From 31d6b615b44afa9ad87cd0adf7412a93b769157a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jul 2023 12:11:28 -0500 Subject: [PATCH 0860/1009] Bump home-assistant-bluetooth to 1.10.1 (#97153) --- 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 087fe4297ec2d6..e1a90290c1077e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ fnv-hash-fast==0.3.1 ha-av==10.1.0 hass-nabucasa==0.69.0 hassil==1.0.6 -home-assistant-bluetooth==1.10.0 +home-assistant-bluetooth==1.10.1 home-assistant-frontend==20230705.1 home-assistant-intents==2023.6.28 httpx==0.24.1 diff --git a/pyproject.toml b/pyproject.toml index 1df65353855901..7826de94f9d559 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.24.1", - "home-assistant-bluetooth==1.10.0", + "home-assistant-bluetooth==1.10.1", "ifaddr==0.2.0", "Jinja2==3.1.2", "lru-dict==1.2.0", diff --git a/requirements.txt b/requirements.txt index d4445c953692d2..aee5d454e2338b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ bcrypt==4.0.1 certifi>=2021.5.30 ciso8601==2.3.0 httpx==0.24.1 -home-assistant-bluetooth==1.10.0 +home-assistant-bluetooth==1.10.1 ifaddr==0.2.0 Jinja2==3.1.2 lru-dict==1.2.0 From 2bd6b519fa9bf04df178ec8d700b2b30a1f2353b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 24 Jul 2023 19:29:24 +0200 Subject: [PATCH 0861/1009] Remove unused words from codespell check (#97152) Co-authored-by: Franck Nijhof --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5e24796323c736..b7b351b755f2f1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: hooks: - id: codespell args: - - --ignore-words-list=additionals,alle,alot,ba,bre,bund,currenty,datas,dof,dur,ether,farenheit,falsy,fo,haa,hass,hist,iam,iff,iif,incomfort,ines,ist,lightsensor,mut,nam,nd,pres,pullrequests,referer,resset,rime,ser,serie,sur,te,technik,ue,uint,unsecure,visability,wan,wanna,withing,zar + - --ignore-words-list=additionals,alle,alot,bund,currenty,datas,farenheit,falsy,fo,haa,hass,iif,incomfort,ines,ist,nam,nd,pres,pullrequests,resset,rime,ser,serie,te,technik,ue,unsecure,withing,zar - --skip="./.*,*.csv,*.json,*.ambr" - --quiet-level=2 exclude_types: [csv, json] From e96bff16740d6104ca9249bc7837ed9109c74e5f Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 24 Jul 2023 19:31:25 +0200 Subject: [PATCH 0862/1009] Add alternative key names for Discovergy voltage sensors (#97155) --- homeassistant/components/discovergy/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/discovergy/sensor.py b/homeassistant/components/discovergy/sensor.py index 3f4069752f2ed0..fe6ed408298f60 100644 --- a/homeassistant/components/discovergy/sensor.py +++ b/homeassistant/components/discovergy/sensor.py @@ -101,6 +101,7 @@ class DiscovergySensorEntityDescription(DiscovergyMixin, SensorEntityDescription device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + alternative_keys=["voltage1"], ), DiscovergySensorEntityDescription( key="phase2Voltage", @@ -110,6 +111,7 @@ class DiscovergySensorEntityDescription(DiscovergyMixin, SensorEntityDescription device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + alternative_keys=["voltage2"], ), DiscovergySensorEntityDescription( key="phase3Voltage", @@ -119,6 +121,7 @@ class DiscovergySensorEntityDescription(DiscovergyMixin, SensorEntityDescription device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + alternative_keys=["voltage3"], ), # energy sensors DiscovergySensorEntityDescription( From fe66c3414b2ba68b6b54060df772318671bd38d0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 24 Jul 2023 19:39:46 +0200 Subject: [PATCH 0863/1009] Implement data coordinator for LastFM (#96942) Co-authored-by: G Johansson --- homeassistant/components/lastfm/__init__.py | 7 +- .../components/lastfm/coordinator.py | 89 +++++++++++++++ homeassistant/components/lastfm/sensor.py | 101 ++++++++++-------- tests/components/lastfm/__init__.py | 2 +- .../lastfm/snapshots/test_sensor.ambr | 17 +-- tests/components/lastfm/test_sensor.py | 4 +- 6 files changed, 158 insertions(+), 62 deletions(-) create mode 100644 homeassistant/components/lastfm/coordinator.py diff --git a/homeassistant/components/lastfm/__init__.py b/homeassistant/components/lastfm/__init__.py index fc26dd85ea3229..72dcf08a2d085d 100644 --- a/homeassistant/components/lastfm/__init__.py +++ b/homeassistant/components/lastfm/__init__.py @@ -4,12 +4,17 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import PLATFORMS +from .const import DOMAIN, PLATFORMS +from .coordinator import LastFMDataUpdateCoordinator async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up lastfm from a config entry.""" + coordinator = LastFMDataUpdateCoordinator(hass) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/lastfm/coordinator.py b/homeassistant/components/lastfm/coordinator.py new file mode 100644 index 00000000000000..533f9ec3b090a4 --- /dev/null +++ b/homeassistant/components/lastfm/coordinator.py @@ -0,0 +1,89 @@ +"""DataUpdateCoordinator for the LastFM integration.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta + +from pylast import LastFMNetwork, PyLastError, Track + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_USERS, + DOMAIN, + LOGGER, +) + + +def format_track(track: Track | None) -> str | None: + """Format the track.""" + if track is None: + return None + return f"{track.artist} - {track.title}" + + +@dataclass +class LastFMUserData: + """Data holder for LastFM data.""" + + play_count: int + image: str + now_playing: str | None + top_track: str | None + last_track: str | None + + +class LastFMDataUpdateCoordinator(DataUpdateCoordinator[dict[str, LastFMUserData]]): + """A LastFM Data Update Coordinator.""" + + config_entry: ConfigEntry + _client: LastFMNetwork + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the LastFM data coordinator.""" + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=30), + ) + self._client = LastFMNetwork(api_key=self.config_entry.options[CONF_API_KEY]) + + async def _async_update_data(self) -> dict[str, LastFMUserData]: + res = {} + for username in self.config_entry.options[CONF_USERS]: + data = await self.hass.async_add_executor_job(self._get_user_data, username) + if data is not None: + res[username] = data + if not res: + raise UpdateFailed + return res + + def _get_user_data(self, username: str) -> LastFMUserData | None: + user = self._client.get_user(username) + try: + play_count = user.get_playcount() + image = user.get_image() + now_playing = format_track(user.get_now_playing()) + top_tracks = user.get_top_tracks(limit=1) + last_tracks = user.get_recent_tracks(limit=1) + except PyLastError as exc: + if self.last_update_success: + LOGGER.error("LastFM update for %s failed: %r", username, exc) + return None + top_track = None + if len(top_tracks) > 0: + top_track = format_track(top_tracks[0].item) + last_track = None + if len(last_tracks) > 0: + last_track = format_track(last_tracks[0].track) + return LastFMUserData( + play_count, + image, + now_playing, + top_track, + last_track, + ) diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index c51868394dead9..116a081338701a 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -2,8 +2,8 @@ from __future__ import annotations import hashlib +from typing import Any -from pylast import LastFMNetwork, PyLastError, Track, User import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -16,6 +16,9 @@ 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.helpers.update_coordinator import ( + CoordinatorEntity, +) from .const import ( ATTR_LAST_PLAYED, @@ -24,9 +27,9 @@ CONF_USERS, DEFAULT_NAME, DOMAIN, - LOGGER, STATE_NOT_SCROBBLING, ) +from .coordinator import LastFMDataUpdateCoordinator, LastFMUserData PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -36,11 +39,6 @@ ) -def format_track(track: Track) -> str: - """Format the track.""" - return f"{track.artist} - {track.title}" - - async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -78,61 +76,76 @@ async def async_setup_entry( ) -> None: """Initialize the entries.""" - lastfm_api = LastFMNetwork(api_key=entry.options[CONF_API_KEY]) + coordinator: LastFMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( ( - LastFmSensor(lastfm_api.get_user(user), entry.entry_id) - for user in entry.options[CONF_USERS] + LastFmSensor(coordinator, username, entry.entry_id) + for username in entry.options[CONF_USERS] ), - True, ) -class LastFmSensor(SensorEntity): +class LastFmSensor(CoordinatorEntity[LastFMDataUpdateCoordinator], SensorEntity): """A class for the Last.fm account.""" _attr_attribution = "Data provided by Last.fm" _attr_icon = "mdi:radio-fm" - def __init__(self, user: User, entry_id: str) -> None: + def __init__( + self, + coordinator: LastFMDataUpdateCoordinator, + username: str, + entry_id: str, + ) -> None: """Initialize the sensor.""" - self._user = user - self._attr_unique_id = hashlib.sha256(user.name.encode("utf-8")).hexdigest() - self._attr_name = user.name + super().__init__(coordinator) + self._username = username + self._attr_unique_id = hashlib.sha256(username.encode("utf-8")).hexdigest() + self._attr_name = username self._attr_device_info = DeviceInfo( configuration_url="https://www.last.fm", entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, f"{entry_id}_{self._attr_unique_id}")}, manufacturer=DEFAULT_NAME, - name=f"{DEFAULT_NAME} {user.name}", + name=f"{DEFAULT_NAME} {username}", ) - def update(self) -> None: - """Update device state.""" - self._attr_native_value = STATE_NOT_SCROBBLING - try: - play_count = self._user.get_playcount() - self._attr_entity_picture = self._user.get_image() - now_playing = self._user.get_now_playing() - top_tracks = self._user.get_top_tracks(limit=1) - last_tracks = self._user.get_recent_tracks(limit=1) - except PyLastError as exc: - self._attr_available = False - LOGGER.error("Failed to load LastFM user `%s`: %r", self._user.name, exc) - return - self._attr_available = True - if now_playing: - self._attr_native_value = format_track(now_playing) - self._attr_extra_state_attributes = { + @property + def user_data(self) -> LastFMUserData | None: + """Returns the user from the coordinator.""" + return self.coordinator.data.get(self._username) + + @property + def available(self) -> bool: + """If user not found in coordinator, entity is unavailable.""" + return super().available and self.user_data is not None + + @property + def entity_picture(self) -> str | None: + """Return user avatar.""" + if self.user_data and self.user_data.image is not None: + return self.user_data.image + return None + + @property + def native_value(self) -> str: + """Return value of sensor.""" + if self.user_data and self.user_data.now_playing is not None: + return self.user_data.now_playing + return STATE_NOT_SCROBBLING + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return state attributes.""" + play_count = None + last_track = None + top_track = None + if self.user_data: + play_count = self.user_data.play_count + last_track = self.user_data.last_track + top_track = self.user_data.top_track + return { ATTR_PLAY_COUNT: play_count, - ATTR_LAST_PLAYED: None, - ATTR_TOP_PLAYED: None, + ATTR_LAST_PLAYED: last_track, + ATTR_TOP_PLAYED: top_track, } - if len(last_tracks) > 0: - self._attr_extra_state_attributes[ATTR_LAST_PLAYED] = format_track( - last_tracks[0].track - ) - if len(top_tracks) > 0: - self._attr_extra_state_attributes[ATTR_TOP_PLAYED] = format_track( - top_tracks[0].item - ) diff --git a/tests/components/lastfm/__init__.py b/tests/components/lastfm/__init__.py index dde914d51cc182..7e6bb6500b2325 100644 --- a/tests/components/lastfm/__init__.py +++ b/tests/components/lastfm/__init__.py @@ -75,7 +75,7 @@ def get_playcount(self) -> int: def get_image(self) -> str: """Get mock image.""" - return "" + return "image" def get_recent_tracks(self, limit: int) -> list[MockLastTrack]: """Get mock recent tracks.""" diff --git a/tests/components/lastfm/snapshots/test_sensor.ambr b/tests/components/lastfm/snapshots/test_sensor.ambr index a28e085c1040eb..e64cf6b2629172 100644 --- a/tests/components/lastfm/snapshots/test_sensor.ambr +++ b/tests/components/lastfm/snapshots/test_sensor.ambr @@ -3,7 +3,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Last.fm', - 'entity_picture': '', + 'entity_picture': 'image', 'friendly_name': 'testaccount1', 'icon': 'mdi:radio-fm', 'last_played': 'artist - title', @@ -21,7 +21,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Last.fm', - 'entity_picture': '', + 'entity_picture': 'image', 'friendly_name': 'testaccount1', 'icon': 'mdi:radio-fm', 'last_played': None, @@ -36,16 +36,5 @@ }) # --- # name: test_sensors[not_found_user] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by Last.fm', - 'friendly_name': 'testaccount1', - 'icon': 'mdi:radio-fm', - }), - 'context': , - 'entity_id': 'sensor.testaccount1', - 'last_changed': , - 'last_updated': , - 'state': 'unavailable', - }) + None # --- diff --git a/tests/components/lastfm/test_sensor.py b/tests/components/lastfm/test_sensor.py index ab9358be1d3da7..049f2a74250d1a 100644 --- a/tests/components/lastfm/test_sensor.py +++ b/tests/components/lastfm/test_sensor.py @@ -14,7 +14,7 @@ from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component -from . import API_KEY, USERNAME_1 +from . import API_KEY, USERNAME_1, MockUser from .conftest import ComponentSetup from tests.common import MockConfigEntry @@ -28,7 +28,7 @@ async def test_legacy_migration(hass: HomeAssistant) -> None: """Test migration from yaml to config flow.""" - with patch("pylast.User", return_value=None): + with patch("pylast.User", return_value=MockUser()): assert await async_setup_component(hass, Platform.SENSOR, LEGACY_CONFIG) await hass.async_block_till_done() entries = hass.config_entries.async_entries(DOMAIN) From 0d79903f909f61bf6094af08ebf5838b76c4177e Mon Sep 17 00:00:00 2001 From: Oliver <10700296+ol-iver@users.noreply.github.com> Date: Mon, 24 Jul 2023 19:41:41 +0200 Subject: [PATCH 0864/1009] Fix denonavr netaudio telnet event (#97159) --- homeassistant/components/denonavr/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index eab4c1df3a60a3..67368596439e80 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -256,7 +256,7 @@ async def _telnet_callback(self, zone, event, parameter) -> None: return # Some updates trigger multiple events like one for artist and one for title for one change # We skip every event except the last one - if event == "NS" and not parameter.startswith("E4"): + if event == "NSE" and not parameter.startswith("4"): return if event == "TA" and not parameter.startwith("ANNAME"): return From 6e50576db2e0109509b3c954b5e674db1b026a50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jul 2023 12:48:50 -0500 Subject: [PATCH 0865/1009] Bump zeroconf to 0.71.4 (#97156) --- 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 87435d8e2c1482..92daffc6c8bcb2 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.71.3"] + "requirements": ["zeroconf==0.71.4"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e1a90290c1077e..0dca45102b17a7 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.71.3 +zeroconf==0.71.4 # 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 9c0659fac39b90..dff3628abb4ec0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2738,7 +2738,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.71.3 +zeroconf==0.71.4 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6f4644715991f..dfdc70104963d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2008,7 +2008,7 @@ youless-api==1.0.1 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.71.3 +zeroconf==0.71.4 # homeassistant.components.zeversolar zeversolar==0.3.1 From 593960c7046c332b6191acaf10766194d3694f46 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jul 2023 12:49:24 -0500 Subject: [PATCH 0866/1009] Bump bluetooth deps (#97157) --- homeassistant/components/bluetooth/manifest.json | 4 ++-- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/ld2410_ble/manifest.json | 2 +- homeassistant/components/led_ble/manifest.json | 2 +- homeassistant/package_constraints.txt | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index cbe4bf9069cc2c..781e784fe6a137 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -18,7 +18,7 @@ "bleak-retry-connector==3.1.0", "bluetooth-adapters==0.16.0", "bluetooth-auto-recovery==1.2.1", - "bluetooth-data-tools==1.6.0", - "dbus-fast==1.86.0" + "bluetooth-data-tools==1.6.1", + "dbus-fast==1.87.1" ] } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 33c43936544430..b9b235ab41e717 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "requirements": [ "async_interrupt==1.1.1", "aioesphomeapi==15.1.14", - "bluetooth-data-tools==1.6.0", + "bluetooth-data-tools==1.6.1", "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 a161c3ecde1915..5ee0102ce17908 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.6.0", "ld2410-ble==0.1.1"] + "requirements": ["bluetooth-data-tools==1.6.1", "ld2410-ble==0.1.1"] } diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 51acbe8c7d9e5b..3e34176771ca0f 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.6.0", "led-ble==1.0.0"] + "requirements": ["bluetooth-data-tools==1.6.1", "led-ble==1.0.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0dca45102b17a7..2e1b00fc053ce5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,11 +12,11 @@ bleak-retry-connector==3.1.0 bleak==0.20.2 bluetooth-adapters==0.16.0 bluetooth-auto-recovery==1.2.1 -bluetooth-data-tools==1.6.0 +bluetooth-data-tools==1.6.1 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.2 -dbus-fast==1.86.0 +dbus-fast==1.87.1 fnv-hash-fast==0.3.1 ha-av==10.1.0 hass-nabucasa==0.69.0 diff --git a/requirements_all.txt b/requirements_all.txt index dff3628abb4ec0..b15c4f97c886db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -537,7 +537,7 @@ bluetooth-auto-recovery==1.2.1 # homeassistant.components.esphome # homeassistant.components.ld2410_ble # homeassistant.components.led_ble -bluetooth-data-tools==1.6.0 +bluetooth-data-tools==1.6.1 # homeassistant.components.bond bond-async==0.2.1 @@ -629,7 +629,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.86.0 +dbus-fast==1.87.1 # homeassistant.components.debugpy debugpy==1.6.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dfdc70104963d1..a0b49ab41f0ded 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -451,7 +451,7 @@ bluetooth-auto-recovery==1.2.1 # homeassistant.components.esphome # homeassistant.components.ld2410_ble # homeassistant.components.led_ble -bluetooth-data-tools==1.6.0 +bluetooth-data-tools==1.6.1 # homeassistant.components.bond bond-async==0.2.1 @@ -512,7 +512,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.86.0 +dbus-fast==1.87.1 # homeassistant.components.debugpy debugpy==1.6.7 From 17e757af36417e730582db4b455489ac285e5137 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 24 Jul 2023 17:53:39 +0000 Subject: [PATCH 0867/1009] Add sensors for Shelly Plus PM Mini (#97163) --- homeassistant/components/shelly/sensor.py | 50 +++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index c4fc4b66f373ec..8c98eb6473c3fe 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -337,6 +337,14 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), + "power_pm1": RpcSensorDescription( + key="pm1", + sub_key="apower", + name="Power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), "a_act_power": RpcSensorDescription( key="em", sub_key="a_act_power", @@ -433,6 +441,17 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), + "voltage_pm1": RpcSensorDescription( + key="pm1", + sub_key="voltage", + name="Voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + value=lambda status, _: None if status is None else float(status), + suggested_display_precision=1, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), "a_voltage": RpcSensorDescription( key="em", sub_key="a_voltage", @@ -470,6 +489,16 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), + "current_pm1": RpcSensorDescription( + key="pm1", + sub_key="current", + name="Current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + value=lambda status, _: None if status is None else float(status), + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), "a_current": RpcSensorDescription( key="em", sub_key="a_current", @@ -527,6 +556,17 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), + "energy_pm1": RpcSensorDescription( + key="pm1", + sub_key="aenergy", + name="Energy", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + value=lambda status, _: status["total"], + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), "total_act": RpcSensorDescription( key="emdata", sub_key="total_act", @@ -621,6 +661,16 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, ), + "freq_pm1": RpcSensorDescription( + key="pm1", + sub_key="freq", + name="Frequency", + native_unit_of_measurement=UnitOfFrequency.HERTZ, + suggested_display_precision=0, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), "a_freq": RpcSensorDescription( key="em", sub_key="a_freq", From 345df715d60968c250381607ccff55f77be3de42 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Mon, 24 Jul 2023 19:53:58 +0200 Subject: [PATCH 0868/1009] Change AsusWRT entities unique id (#97066) Migrate AsusWRT entities unique id --- homeassistant/components/asuswrt/router.py | 53 ++++++++++-- homeassistant/components/asuswrt/sensor.py | 12 +-- tests/components/asuswrt/test_sensor.py | 95 +++++++++++++--------- 3 files changed, 105 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index c782a8f0f3b036..e0143d49259b72 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.util import dt as dt_util +from homeassistant.util import dt as dt_util, slugify from .bridge import AsusWrtBridge, WrtDevice from .const import ( @@ -39,7 +39,6 @@ ) CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP] -DEFAULT_NAME = "Asuswrt" SCAN_INTERVAL = timedelta(seconds=30) @@ -179,6 +178,44 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: self.hass, dict(self._entry.data), self._options ) + def _migrate_entities_unique_id(self) -> None: + """Migrate router entities to new unique id format.""" + _ENTITY_MIGRATION_ID = { + "sensor_connected_device": "Devices Connected", + "sensor_rx_bytes": "Download", + "sensor_tx_bytes": "Upload", + "sensor_rx_rates": "Download Speed", + "sensor_tx_rates": "Upload Speed", + "sensor_load_avg1": "Load Avg (1m)", + "sensor_load_avg5": "Load Avg (5m)", + "sensor_load_avg15": "Load Avg (15m)", + "2.4GHz": "2.4GHz Temperature", + "5.0GHz": "5GHz Temperature", + "CPU": "CPU Temperature", + } + + entity_reg = er.async_get(self.hass) + router_entries = er.async_entries_for_config_entry( + entity_reg, self._entry.entry_id + ) + + migrate_entities: dict[str, str] = {} + for entry in router_entries: + if entry.domain == TRACKER_DOMAIN: + continue + old_unique_id = entry.unique_id + if not old_unique_id.startswith(DOMAIN): + continue + for new_id, old_id in _ENTITY_MIGRATION_ID.items(): + if old_unique_id.endswith(old_id): + migrate_entities[entry.entity_id] = slugify( + f"{self.unique_id}_{new_id}" + ) + break + + for entity_id, unique_id in migrate_entities.items(): + entity_reg.async_update_entity(entity_id, new_unique_id=unique_id) + async def setup(self) -> None: """Set up a AsusWrt router.""" try: @@ -215,6 +252,9 @@ async def setup(self) -> None: self._devices[device_mac] = AsusWrtDevInfo(device_mac, entry.original_name) + # Migrate entities to new unique id format + self._migrate_entities_unique_id() + # Update devices await self.update_devices() @@ -364,14 +404,9 @@ def host(self) -> str: return self._api.host @property - def unique_id(self) -> str | None: + def unique_id(self) -> str: """Return router unique id.""" - return self._entry.unique_id - - @property - def name(self) -> str: - """Return router name.""" - return self.host if self.unique_id else DEFAULT_NAME + return self._entry.unique_id or self._entry.entry_id @property def devices(self) -> dict[str, AsusWrtDevInfo]: diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index accd1eba59bece..7f54bc29393270 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -22,6 +22,7 @@ CoordinatorEntity, DataUpdateCoordinator, ) +from homeassistant.util import slugify from .const import ( DATA_ASUSWRT, @@ -182,6 +183,9 @@ async def async_setup_entry( class AsusWrtSensor(CoordinatorEntity, SensorEntity): """Representation of a AsusWrt sensor.""" + entity_description: AsusWrtSensorEntityDescription + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, @@ -190,13 +194,9 @@ def __init__( ) -> None: """Initialize a AsusWrt sensor.""" super().__init__(coordinator) - self.entity_description: AsusWrtSensorEntityDescription = description + self.entity_description = description - self._attr_name = f"{router.name} {description.name}" - if router.unique_id: - self._attr_unique_id = f"{DOMAIN} {router.unique_id} {description.name}" - else: - self._attr_unique_id = f"{DOMAIN} {self.name}" + self._attr_unique_id = slugify(f"{router.unique_id}_{description.key}") self._attr_device_info = router.device_info self._attr_extra_state_attributes = {"hostname": router.host} diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index c28d71c1a293e6..2d7bda491a8a61 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -9,9 +9,13 @@ from homeassistant.components.asuswrt.const import ( CONF_INTERFACE, DOMAIN, + MODE_ROUTER, PROTOCOL_TELNET, + SENSORS_BYTES, + SENSORS_LOAD_AVG, + SENSORS_RATES, + SENSORS_TEMPERATURES, ) -from homeassistant.components.asuswrt.router import DEFAULT_NAME from homeassistant.components.device_tracker import CONF_CONSIDER_HOME from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( @@ -43,7 +47,7 @@ CONF_PROTOCOL: PROTOCOL_TELNET, CONF_USERNAME: "user", CONF_PASSWORD: "pwd", - CONF_MODE: "router", + CONF_MODE: MODE_ROUTER, } MAC_ADDR = "a1:b2:c3:d4:e5:f6" @@ -57,26 +61,8 @@ MOCK_MAC_3 = "A3:B3:C3:D3:E3:F3" MOCK_MAC_4 = "A4:B4:C4:D4:E4:F4" -SENSORS_DEFAULT = [ - "Download Speed", - "Download", - "Upload Speed", - "Upload", -] - -SENSORS_LOADAVG = [ - "Load Avg (1m)", - "Load Avg (5m)", - "Load Avg (15m)", -] - -SENSORS_TEMP = [ - "2.4GHz Temperature", - "5GHz Temperature", - "CPU Temperature", -] - -SENSORS_ALL = [*SENSORS_DEFAULT, *SENSORS_LOADAVG, *SENSORS_TEMP] +SENSORS_DEFAULT = [*SENSORS_BYTES, *SENSORS_RATES] +SENSORS_ALL = [*SENSORS_DEFAULT, *SENSORS_LOAD_AVG, *SENSORS_TEMPERATURES] PATCH_SETUP_ENTRY = patch( "homeassistant.components.asuswrt.async_setup_entry", @@ -105,7 +91,7 @@ def mock_available_temps_fixture(): @pytest.fixture(name="create_device_registry_devices") -def create_device_registry_devices_fixture(hass): +def create_device_registry_devices_fixture(hass: HomeAssistant): """Create device registry devices so the device tracker entities are enabled when added.""" dev_reg = dr.async_get(hass) config_entry = MockConfigEntry(domain="something_else") @@ -182,7 +168,7 @@ def mock_controller_connect_sens_fail(): yield service_mock -def _setup_entry(hass, config, sensors, unique_id=None): +def _setup_entry(hass: HomeAssistant, config, sensors, unique_id=None): """Create mock config entry with enabled sensors.""" entity_reg = er.async_get(hass) @@ -195,16 +181,17 @@ def _setup_entry(hass, config, sensors, unique_id=None): ) # init variable - obj_prefix = slugify(HOST if unique_id else DEFAULT_NAME) + obj_prefix = slugify(HOST) sensor_prefix = f"{sensor.DOMAIN}.{obj_prefix}" + unique_id_prefix = slugify(unique_id or config_entry.entry_id) # Pre-enable the status sensor - for sensor_name in sensors: - sensor_id = slugify(sensor_name) + for sensor_key in sensors: + sensor_id = slugify(sensor_key) entity_reg.async_get_or_create( sensor.DOMAIN, DOMAIN, - f"{DOMAIN} {unique_id or DEFAULT_NAME} {sensor_name}", + f"{unique_id_prefix}_{sensor_id}", suggested_object_id=f"{obj_prefix}_{sensor_id}", config_entry=config_entry, disabled_by=None, @@ -255,10 +242,10 @@ async def test_sensors( assert hass.states.get(f"{device_tracker.DOMAIN}.test").state == STATE_HOME assert hass.states.get(f"{device_tracker.DOMAIN}.testtwo").state == STATE_HOME - assert hass.states.get(f"{sensor_prefix}_download_speed").state == "160.0" - assert hass.states.get(f"{sensor_prefix}_download").state == "60.0" - assert hass.states.get(f"{sensor_prefix}_upload_speed").state == "80.0" - assert hass.states.get(f"{sensor_prefix}_upload").state == "50.0" + assert hass.states.get(f"{sensor_prefix}_sensor_rx_rates").state == "160.0" + assert hass.states.get(f"{sensor_prefix}_sensor_rx_bytes").state == "60.0" + assert hass.states.get(f"{sensor_prefix}_sensor_tx_rates").state == "80.0" + assert hass.states.get(f"{sensor_prefix}_sensor_tx_bytes").state == "50.0" assert hass.states.get(f"{sensor_prefix}_devices_connected").state == "2" # remove first tracked device @@ -296,7 +283,7 @@ async def test_loadavg_sensors( connect, ) -> None: """Test creating an AsusWRT load average sensors.""" - config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA, SENSORS_LOADAVG) + config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA, SENSORS_LOAD_AVG) config_entry.add_to_hass(hass) # initial devices setup @@ -306,9 +293,9 @@ async def test_loadavg_sensors( await hass.async_block_till_done() # assert temperature sensor available - assert hass.states.get(f"{sensor_prefix}_load_avg_1m").state == "1.1" - assert hass.states.get(f"{sensor_prefix}_load_avg_5m").state == "1.2" - assert hass.states.get(f"{sensor_prefix}_load_avg_15m").state == "1.3" + assert hass.states.get(f"{sensor_prefix}_sensor_load_avg1").state == "1.1" + assert hass.states.get(f"{sensor_prefix}_sensor_load_avg5").state == "1.2" + assert hass.states.get(f"{sensor_prefix}_sensor_load_avg15").state == "1.3" async def test_temperature_sensors( @@ -316,7 +303,7 @@ async def test_temperature_sensors( connect, ) -> None: """Test creating a AsusWRT temperature sensors.""" - config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA, SENSORS_TEMP) + config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA, SENSORS_TEMPERATURES) config_entry.add_to_hass(hass) # initial devices setup @@ -326,9 +313,9 @@ async def test_temperature_sensors( await hass.async_block_till_done() # assert temperature sensor available - assert hass.states.get(f"{sensor_prefix}_2_4ghz_temperature").state == "40.0" - assert not hass.states.get(f"{sensor_prefix}_5ghz_temperature") - assert hass.states.get(f"{sensor_prefix}_cpu_temperature").state == "71.2" + assert hass.states.get(f"{sensor_prefix}_2_4ghz").state == "40.0" + assert not hass.states.get(f"{sensor_prefix}_5_0ghz") + assert hass.states.get(f"{sensor_prefix}_cpu").state == "71.2" @pytest.mark.parametrize( @@ -396,3 +383,31 @@ async def test_options_reload(hass: HomeAssistant, connect) -> None: assert setup_entry_call.called assert config_entry.state is ConfigEntryState.LOADED + + +async def test_unique_id_migration(hass: HomeAssistant, connect) -> None: + """Test AsusWRT entities unique id format migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=CONFIG_DATA, + unique_id=MAC_ADDR, + ) + config_entry.add_to_hass(hass) + + entity_reg = er.async_get(hass) + obj_entity_id = slugify(f"{HOST} Upload") + entity_reg.async_get_or_create( + sensor.DOMAIN, + DOMAIN, + f"{DOMAIN} {MAC_ADDR} Upload", + suggested_object_id=obj_entity_id, + config_entry=config_entry, + disabled_by=None, + ) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + migr_entity = entity_reg.async_get(f"{sensor.DOMAIN}.{obj_entity_id}") + assert migr_entity is not None + assert migr_entity.unique_id == slugify(f"{MAC_ADDR}_sensor_tx_bytes") From 2cfc11d4b990912956a6ae17839d5eda94512a62 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Mon, 24 Jul 2023 19:58:11 +0200 Subject: [PATCH 0869/1009] Limit AndroidTV screencap calls (#96485) --- .../components/androidtv/media_player.py | 54 +++++++++----- tests/components/androidtv/patchers.py | 4 + .../components/androidtv/test_media_player.py | 73 +++++++++++++++---- 3 files changed, 99 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 8f5f3bdfe56bc2..4f927f242df301 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -2,8 +2,9 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine -from datetime import datetime +from datetime import timedelta import functools +import hashlib import logging from typing import Any, Concatenate, ParamSpec, TypeVar @@ -35,6 +36,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import Throttle from . import ADB_PYTHON_EXCEPTIONS, ADB_TCP_EXCEPTIONS, get_androidtv_mac from .const import ( @@ -65,6 +67,8 @@ ATTR_HDMI_INPUT = "hdmi_input" ATTR_LOCAL_PATH = "local_path" +MIN_TIME_BETWEEN_SCREENCAPS = timedelta(seconds=60) + SERVICE_ADB_COMMAND = "adb_command" SERVICE_DOWNLOAD = "download" SERVICE_LEARN_SENDEVENT = "learn_sendevent" @@ -228,6 +232,9 @@ def __init__( self._entry_id = entry_id self._entry_data = entry_data + self._media_image: tuple[bytes | None, str | None] = None, None + self._attr_media_image_hash = None + info = aftv.device_properties model = info.get(ATTR_MODEL) self._attr_device_info = DeviceInfo( @@ -304,34 +311,39 @@ async def async_added_to_hass(self) -> None: ) ) - @property - def media_image_hash(self) -> str | None: - """Hash value for media image.""" - return f"{datetime.now().timestamp()}" if self._screencap else None - @adb_decorator() async def _adb_screencap(self) -> bytes | None: """Take a screen capture from the device.""" return await self.aftv.adb_screencap() - async def async_get_media_image(self) -> tuple[bytes | None, str | None]: - """Fetch current playing image.""" + async def _async_get_screencap(self, prev_app_id: str | None = None) -> None: + """Take a screen capture from the device when enabled.""" if ( not self._screencap or self.state in {MediaPlayerState.OFF, None} or not self.available ): - return None, None - - media_data = await self._adb_screencap() - if media_data: - return media_data, "image/png" - - # If an exception occurred and the device is no longer available, write the state - if not self.available: - self.async_write_ha_state() + self._media_image = None, None + self._attr_media_image_hash = None + else: + force: bool = prev_app_id is not None + if force: + force = prev_app_id != self._attr_app_id + await self._adb_get_screencap(no_throttle=force) + + @Throttle(MIN_TIME_BETWEEN_SCREENCAPS) + async def _adb_get_screencap(self, **kwargs) -> None: + """Take a screen capture from the device every 60 seconds.""" + if media_data := await self._adb_screencap(): + self._media_image = media_data, "image/png" + self._attr_media_image_hash = hashlib.sha256(media_data).hexdigest()[:16] + else: + self._media_image = None, None + self._attr_media_image_hash = None - return None, None + async def async_get_media_image(self) -> tuple[bytes | None, str | None]: + """Fetch current playing image.""" + return self._media_image @adb_decorator() async def async_media_play(self) -> None: @@ -485,6 +497,7 @@ async def async_update(self) -> None: if not self.available: return + prev_app_id = self._attr_app_id # Get the updated state and attributes. ( state, @@ -514,6 +527,8 @@ async def async_update(self) -> None: else: self._attr_source_list = None + await self._async_get_screencap(prev_app_id) + @adb_decorator() async def async_media_stop(self) -> None: """Send stop command.""" @@ -575,6 +590,7 @@ async def async_update(self) -> None: if not self.available: return + prev_app_id = self._attr_app_id # Get the `state`, `current_app`, `running_apps` and `hdmi_input`. ( state, @@ -601,6 +617,8 @@ async def async_update(self) -> None: else: self._attr_source_list = None + await self._async_get_screencap(prev_app_id) + @adb_decorator() async def async_media_stop(self) -> None: """Send stop (back) command.""" diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index f0fca5aae90362..aae99b34438b61 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -185,6 +185,10 @@ def isfile(filepath): return filepath.endswith("adbkey") +PATCH_SCREENCAP = patch( + "androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap", + return_value=b"image", +) PATCH_SETUP_ENTRY = patch( "homeassistant.components.androidtv.async_setup_entry", return_value=True, diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index c7083626e15bac..847bc5c7d2fac5 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -1,4 +1,5 @@ """The tests for the androidtv platform.""" +from datetime import timedelta import logging from typing import Any from unittest.mock import Mock, patch @@ -70,10 +71,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util import slugify +from homeassistant.util.dt import utcnow from . import patchers -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed from tests.typing import ClientSessionGenerator HOST = "127.0.0.1" @@ -263,7 +265,7 @@ async def test_reconnect( caplog.set_level(logging.DEBUG) with patchers.patch_connect(True)[patch_key], patchers.patch_shell( SHELL_RESPONSE_STANDBY - )[patch_key]: + )[patch_key], patchers.PATCH_SCREENCAP: await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) @@ -751,7 +753,9 @@ async def test_update_lock_not_acquired(hass: HomeAssistant) -> None: assert state is not None assert state.state == STATE_OFF - with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: + with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[ + patch_key + ], patchers.PATCH_SCREENCAP: await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None @@ -890,8 +894,11 @@ async def test_get_image_http( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - with patchers.patch_shell("11")[patch_key]: + with patchers.patch_shell("11")[ + patch_key + ], patchers.PATCH_SCREENCAP as patch_screen_cap: await async_update_entity(hass, entity_id) + patch_screen_cap.assert_called() media_player_name = "media_player." + slugify( CONFIG_ANDROID_DEFAULT[TEST_ENTITY_NAME] @@ -901,21 +908,53 @@ async def test_get_image_http( client = await hass_client_no_auth() - with patch( - "androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap", return_value=b"image" + resp = await client.get(state.attributes["entity_picture"]) + content = await resp.read() + assert content == b"image" + + next_update = utcnow() + timedelta(seconds=30) + with patchers.patch_shell("11")[ + patch_key + ], patchers.PATCH_SCREENCAP as patch_screen_cap, patch( + "homeassistant.util.utcnow", return_value=next_update ): - resp = await client.get(state.attributes["entity_picture"]) - content = await resp.read() + async_fire_time_changed(hass, next_update, True) + await hass.async_block_till_done() + patch_screen_cap.assert_not_called() - assert content == b"image" + next_update = utcnow() + timedelta(seconds=60) + with patchers.patch_shell("11")[ + patch_key + ], patchers.PATCH_SCREENCAP as patch_screen_cap, patch( + "homeassistant.util.utcnow", return_value=next_update + ): + async_fire_time_changed(hass, next_update, True) + await hass.async_block_till_done() + patch_screen_cap.assert_called() - with patch( + +async def test_get_image_http_fail(hass: HomeAssistant) -> None: + """Test taking a screen capture fail.""" + + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROID_DEFAULT) + config_entry.add_to_hass(hass) + + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + with patchers.patch_shell("11")[patch_key], patch( "androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap", side_effect=ConnectionResetError, ): - resp = await client.get(state.attributes["entity_picture"]) + await async_update_entity(hass, entity_id) # The device is unavailable, but getting the media image did not cause an exception + media_player_name = "media_player." + slugify( + CONFIG_ANDROID_DEFAULT[TEST_ENTITY_NAME] + ) state = hass.states.get(media_player_name) assert state is not None assert state.state == STATE_UNAVAILABLE @@ -986,7 +1025,9 @@ async def test_services_androidtv(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: + with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[ + patch_key + ], patchers.PATCH_SCREENCAP: await _test_service( hass, entity_id, SERVICE_MEDIA_NEXT_TRACK, "media_next_track" ) @@ -1034,7 +1075,9 @@ async def test_services_firetv(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: + with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[ + patch_key + ], patchers.PATCH_SCREENCAP: await _test_service(hass, entity_id, SERVICE_MEDIA_STOP, "back") await _test_service(hass, entity_id, SERVICE_TURN_OFF, "adb_shell") await _test_service(hass, entity_id, SERVICE_TURN_ON, "adb_shell") @@ -1050,7 +1093,9 @@ async def test_volume_mute(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: + with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[ + patch_key + ], patchers.PATCH_SCREENCAP: service_data = {ATTR_ENTITY_ID: entity_id, ATTR_MEDIA_VOLUME_MUTED: True} with patch( "androidtv.androidtv.androidtv_async.AndroidTVAsync.mute_volume", From d0722e2312ee79d15319b50863f8b5abe94d0479 Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 24 Jul 2023 11:00:51 -0700 Subject: [PATCH 0870/1009] Android TV Remote: Add option to disable IME (#95765) --- .../components/androidtv_remote/__init__.py | 10 +++- .../androidtv_remote/config_flow.py | 44 +++++++++++++-- .../components/androidtv_remote/const.py | 3 + .../components/androidtv_remote/helpers.py | 11 +++- .../components/androidtv_remote/strings.json | 9 +++ .../androidtv_remote/test_config_flow.py | 56 +++++++++++++++++++ 6 files changed, 126 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/androidtv_remote/__init__.py b/homeassistant/components/androidtv_remote/__init__.py index 9299b1ed0b0b4b..4c58f82b8e75b3 100644 --- a/homeassistant/components/androidtv_remote/__init__.py +++ b/homeassistant/components/androidtv_remote/__init__.py @@ -18,7 +18,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import DOMAIN -from .helpers import create_api +from .helpers import create_api, get_enable_ime _LOGGER = logging.getLogger(__name__) @@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Android TV Remote from a config entry.""" - api = create_api(hass, entry.data[CONF_HOST]) + api = create_api(hass, entry.data[CONF_HOST], get_enable_ime(entry)) @callback def is_available_updated(is_available: bool) -> None: @@ -76,6 +76,7 @@ def on_hass_stop(event) -> None: entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) ) + entry.async_on_unload(entry.add_update_listener(update_listener)) return True @@ -87,3 +88,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api.disconnect() return unload_ok + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/androidtv_remote/config_flow.py b/homeassistant/components/androidtv_remote/config_flow.py index f7e1078d3fa68c..b8399fd7ba2e8b 100644 --- a/homeassistant/components/androidtv_remote/config_flow.py +++ b/homeassistant/components/androidtv_remote/config_flow.py @@ -15,11 +15,12 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac -from .const import DOMAIN -from .helpers import create_api +from .const import CONF_ENABLE_IME, DOMAIN +from .helpers import create_api, get_enable_ime STEP_USER_DATA_SCHEMA = vol.Schema( { @@ -55,7 +56,7 @@ async def async_step_user( if user_input is not None: self.host = user_input["host"] assert self.host - api = create_api(self.hass, self.host) + api = create_api(self.hass, self.host, enable_ime=False) try: self.name, self.mac = await api.async_get_name_and_mac() assert self.mac @@ -75,7 +76,7 @@ async def async_step_user( async def _async_start_pair(self) -> FlowResult: """Start pairing with the Android TV. Navigate to the pair flow to enter the PIN shown on screen.""" assert self.host - self.api = create_api(self.hass, self.host) + self.api = create_api(self.hass, self.host, enable_ime=False) await self.api.async_generate_cert_if_missing() await self.api.async_start_pairing() return await self.async_step_pair() @@ -186,3 +187,38 @@ async def async_step_reauth_confirm( description_placeholders={CONF_NAME: self.name}, errors=errors, ) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Create the options flow.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Android TV Remote options flow.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + CONF_ENABLE_IME, + default=get_enable_ime(self.config_entry), + ): bool, + } + ), + ) diff --git a/homeassistant/components/androidtv_remote/const.py b/homeassistant/components/androidtv_remote/const.py index 82f494b81aad03..44d7098adc188c 100644 --- a/homeassistant/components/androidtv_remote/const.py +++ b/homeassistant/components/androidtv_remote/const.py @@ -4,3 +4,6 @@ from typing import Final DOMAIN: Final = "androidtv_remote" + +CONF_ENABLE_IME: Final = "enable_ime" +CONF_ENABLE_IME_DEFAULT_VALUE: Final = True diff --git a/homeassistant/components/androidtv_remote/helpers.py b/homeassistant/components/androidtv_remote/helpers.py index 0bc1f1b904f14a..41b056269f2421 100644 --- a/homeassistant/components/androidtv_remote/helpers.py +++ b/homeassistant/components/androidtv_remote/helpers.py @@ -3,11 +3,14 @@ from androidtvremote2 import AndroidTVRemote +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.storage import STORAGE_DIR +from .const import CONF_ENABLE_IME, CONF_ENABLE_IME_DEFAULT_VALUE -def create_api(hass: HomeAssistant, host: str) -> AndroidTVRemote: + +def create_api(hass: HomeAssistant, host: str, enable_ime: bool) -> AndroidTVRemote: """Create an AndroidTVRemote instance.""" return AndroidTVRemote( client_name="Home Assistant", @@ -15,4 +18,10 @@ def create_api(hass: HomeAssistant, host: str) -> AndroidTVRemote: keyfile=hass.config.path(STORAGE_DIR, "androidtv_remote_key.pem"), host=host, loop=hass.loop, + enable_ime=enable_ime, ) + + +def get_enable_ime(entry: ConfigEntry) -> bool: + """Get value of enable_ime option or its default value.""" + return entry.options.get(CONF_ENABLE_IME, CONF_ENABLE_IME_DEFAULT_VALUE) diff --git a/homeassistant/components/androidtv_remote/strings.json b/homeassistant/components/androidtv_remote/strings.json index 983c604370b863..dbbf6a2d383668 100644 --- a/homeassistant/components/androidtv_remote/strings.json +++ b/homeassistant/components/androidtv_remote/strings.json @@ -34,5 +34,14 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "options": { + "step": { + "init": { + "data": { + "enable_ime": "Enable IME. Needed for getting the current app. Disable for devices that show 'Use keyboard on mobile device screen' instead of the on screen keyboard." + } + } + } } } diff --git a/tests/components/androidtv_remote/test_config_flow.py b/tests/components/androidtv_remote/test_config_flow.py index ec368081a95d72..4e0067152e7678 100644 --- a/tests/components/androidtv_remote/test_config_flow.py +++ b/tests/components/androidtv_remote/test_config_flow.py @@ -857,3 +857,59 @@ async def test_reauth_flow_cannot_connect( await hass.async_block_till_done() assert len(mock_unload_entry.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_options_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock +) -> None: + """Test options flow.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_api.disconnect.call_count == 0 + assert mock_api.async_connect.call_count == 1 + + # Trigger options flow, first time + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + assert result["type"] == "form" + assert result["step_id"] == "init" + data_schema = result["data_schema"].schema + assert set(data_schema) == {"enable_ime"} + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"enable_ime": False}, + ) + assert result["type"] == "create_entry" + assert mock_config_entry.options == {"enable_ime": False} + await hass.async_block_till_done() + + assert mock_api.disconnect.call_count == 1 + assert mock_api.async_connect.call_count == 2 + + # Trigger options flow, second time, no change, doesn't reload + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"enable_ime": False}, + ) + assert result["type"] == "create_entry" + assert mock_config_entry.options == {"enable_ime": False} + await hass.async_block_till_done() + + assert mock_api.disconnect.call_count == 1 + assert mock_api.async_connect.call_count == 2 + + # Trigger options flow, third time, change, reloads + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"enable_ime": True}, + ) + assert result["type"] == "create_entry" + assert mock_config_entry.options == {"enable_ime": True} + await hass.async_block_till_done() + + assert mock_api.disconnect.call_count == 2 + assert mock_api.async_connect.call_count == 3 From 4c3d9e5205fd5187b5ad97368d0fa64be76ab32e Mon Sep 17 00:00:00 2001 From: Renier Moorcroft <66512715+RenierM26@users.noreply.github.com> Date: Mon, 24 Jul 2023 20:03:31 +0200 Subject: [PATCH 0871/1009] Fix EZVIZ LightEntity occasional ValueError (#95679) --- homeassistant/components/ezviz/light.py | 60 ++++++++++++++----------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/ezviz/light.py b/homeassistant/components/ezviz/light.py index 38007962e4e6f3..9702959649dd1e 100644 --- a/homeassistant/components/ezviz/light.py +++ b/homeassistant/components/ezviz/light.py @@ -8,7 +8,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( @@ -61,22 +61,14 @@ def __init__( ) self._attr_unique_id = f"{serial}_Light" self._attr_name = "Light" - - @property - def brightness(self) -> int | None: - """Return the brightness of this light between 0..255.""" - return round( + self._attr_is_on = self.data["switches"][DeviceSwitchType.ALARM_LIGHT.value] + self._attr_brightness = round( percentage_to_ranged_value( BRIGHTNESS_RANGE, self.coordinator.data[self._serial]["alarm_light_luminance"], ) ) - @property - def is_on(self) -> bool: - """Return the state of the light.""" - return self.data["switches"][DeviceSwitchType.ALARM_LIGHT.value] - async def async_turn_on(self, **kwargs: Any) -> None: """Turn on light.""" try: @@ -85,41 +77,55 @@ async def async_turn_on(self, **kwargs: Any) -> None: BRIGHTNESS_RANGE, kwargs[ATTR_BRIGHTNESS] ) - update_ok = await self.hass.async_add_executor_job( + if await self.hass.async_add_executor_job( self.coordinator.ezviz_client.set_floodlight_brightness, self._serial, data, - ) - else: - update_ok = await self.hass.async_add_executor_job( - self.coordinator.ezviz_client.switch_status, - self._serial, - DeviceSwitchType.ALARM_LIGHT.value, - 1, - ) + ): + self._attr_brightness = kwargs[ATTR_BRIGHTNESS] + + if await self.hass.async_add_executor_job( + self.coordinator.ezviz_client.switch_status, + self._serial, + DeviceSwitchType.ALARM_LIGHT.value, + 1, + ): + self._attr_is_on = True + self.async_write_ha_state() except (HTTPError, PyEzvizError) as err: raise HomeAssistantError( f"Failed to turn on light {self._attr_name}" ) from err - if update_ok: - await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs: Any) -> None: """Turn off light.""" try: - update_ok = await self.hass.async_add_executor_job( + if await self.hass.async_add_executor_job( self.coordinator.ezviz_client.switch_status, self._serial, DeviceSwitchType.ALARM_LIGHT.value, 0, - ) + ): + self._attr_is_on = False + self.async_write_ha_state() except (HTTPError, PyEzvizError) as err: raise HomeAssistantError( f"Failed to turn off light {self._attr_name}" ) from err - if update_ok: - await self.coordinator.async_request_refresh() + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_is_on = self.data["switches"].get(DeviceSwitchType.ALARM_LIGHT.value) + + if isinstance(self.data["alarm_light_luminance"], int): + self._attr_brightness = round( + percentage_to_ranged_value( + BRIGHTNESS_RANGE, + self.data["alarm_light_luminance"], + ) + ) + + super()._handle_coordinator_update() From fb6699b4987456c7fc49f82fd58bb363fd0b0bda Mon Sep 17 00:00:00 2001 From: Jan Stienstra <65826735+j-stienstra@users.noreply.github.com> Date: Mon, 24 Jul 2023 20:13:26 +0200 Subject: [PATCH 0872/1009] Jellyfin: Sort seasons and episodes by index (#92961) --- .../components/jellyfin/media_source.py | 64 +++++++++++++++++-- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index 318798fdc5f2d3..3bbe3e0b18451c 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -185,7 +185,15 @@ async def _build_music_library( async def _build_artists(self, library_id: str) -> list[BrowseMediaSource]: """Return all artists in the music library.""" artists = await self._get_children(library_id, ITEM_TYPE_ARTIST) - artists = sorted(artists, key=lambda k: k[ITEM_KEY_NAME]) + artists = sorted( + artists, + # Sort by whether an artist has an name first, then by name + # This allows for sorting artists with, without and with missing names + key=lambda k: ( + ITEM_KEY_NAME not in k, + k.get(ITEM_KEY_NAME), + ), + ) return [await self._build_artist(artist, False) for artist in artists] async def _build_artist( @@ -216,7 +224,15 @@ async def _build_artist( async def _build_albums(self, parent_id: str) -> list[BrowseMediaSource]: """Return all albums of a single artist as browsable media sources.""" albums = await self._get_children(parent_id, ITEM_TYPE_ALBUM) - albums = sorted(albums, key=lambda k: k[ITEM_KEY_NAME]) + albums = sorted( + albums, + # Sort by whether an album has an name first, then by name + # This allows for sorting albums with, without and with missing names + key=lambda k: ( + ITEM_KEY_NAME not in k, + k.get(ITEM_KEY_NAME), + ), + ) return [await self._build_album(album, False) for album in albums] async def _build_album( @@ -249,9 +265,11 @@ async def _build_tracks(self, album_id: str) -> list[BrowseMediaSource]: tracks = await self._get_children(album_id, ITEM_TYPE_AUDIO) tracks = sorted( tracks, + # Sort by whether a track has an index first, then by index + # This allows for sorting tracks with, without and with missing indices key=lambda k: ( ITEM_KEY_INDEX_NUMBER not in k, - k.get(ITEM_KEY_INDEX_NUMBER, None), + k.get(ITEM_KEY_INDEX_NUMBER), ), ) return [ @@ -306,7 +324,15 @@ async def _build_movie_library( async def _build_movies(self, library_id: str) -> list[BrowseMediaSource]: """Return all movies in the movie library.""" movies = await self._get_children(library_id, ITEM_TYPE_MOVIE) - movies = sorted(movies, key=lambda k: k[ITEM_KEY_NAME]) + movies = sorted( + movies, + # Sort by whether a movies has an name first, then by name + # This allows for sorting moveis with, without and with missing names + key=lambda k: ( + ITEM_KEY_NAME not in k, + k.get(ITEM_KEY_NAME), + ), + ) return [ self._build_movie(movie) for movie in movies @@ -359,7 +385,15 @@ async def _build_tv_library( async def _build_tvshow(self, library_id: str) -> list[BrowseMediaSource]: """Return all series in the tv library.""" series = await self._get_children(library_id, ITEM_TYPE_SERIES) - series = sorted(series, key=lambda k: k[ITEM_KEY_NAME]) + series = sorted( + series, + # Sort by whether a seroes has an name first, then by name + # This allows for sorting series with, without and with missing names + key=lambda k: ( + ITEM_KEY_NAME not in k, + k.get(ITEM_KEY_NAME), + ), + ) return [await self._build_series(serie, False) for serie in series] async def _build_series( @@ -390,7 +424,15 @@ async def _build_series( async def _build_seasons(self, series_id: str) -> list[BrowseMediaSource]: """Return all seasons in the series.""" seasons = await self._get_children(series_id, ITEM_TYPE_SEASON) - seasons = sorted(seasons, key=lambda k: k[ITEM_KEY_NAME]) + seasons = sorted( + seasons, + # Sort by whether a season has an index first, then by index + # This allows for sorting seasons with, without and with missing indices + key=lambda k: ( + ITEM_KEY_INDEX_NUMBER not in k, + k.get(ITEM_KEY_INDEX_NUMBER), + ), + ) return [await self._build_season(season, False) for season in seasons] async def _build_season( @@ -421,7 +463,15 @@ async def _build_season( async def _build_episodes(self, season_id: str) -> list[BrowseMediaSource]: """Return all episode in the season.""" episodes = await self._get_children(season_id, ITEM_TYPE_EPISODE) - episodes = sorted(episodes, key=lambda k: k[ITEM_KEY_NAME]) + episodes = sorted( + episodes, + # Sort by whether an episode has an index first, then by index + # This allows for sorting episodes with, without and with missing indices + key=lambda k: ( + ITEM_KEY_INDEX_NUMBER not in k, + k.get(ITEM_KEY_INDEX_NUMBER), + ), + ) return [ self._build_episode(episode) for episode in episodes From 649568be83542c711b3c9eaf774a02b5acc352f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jul 2023 13:16:29 -0500 Subject: [PATCH 0873/1009] Bump ulid-transform to 0.8.0 (#97162) --- 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 2e1b00fc053ce5..f2f3f482fe8d79 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -47,7 +47,7 @@ requests==2.31.0 scapy==2.5.0 SQLAlchemy==2.0.15 typing-extensions>=4.7.0,<5.0 -ulid-transform==0.7.2 +ulid-transform==0.8.0 voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 diff --git a/pyproject.toml b/pyproject.toml index 7826de94f9d559..a7fd2e24ce53fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ "PyYAML==6.0.1", "requests==2.31.0", "typing-extensions>=4.7.0,<5.0", - "ulid-transform==0.7.2", + "ulid-transform==0.8.0", "voluptuous==0.13.1", "voluptuous-serialize==2.6.0", "yarl==1.9.2", diff --git a/requirements.txt b/requirements.txt index aee5d454e2338b..098cf402e73e6a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ python-slugify==4.0.1 PyYAML==6.0.1 requests==2.31.0 typing-extensions>=4.7.0,<5.0 -ulid-transform==0.7.2 +ulid-transform==0.8.0 voluptuous==0.13.1 voluptuous-serialize==2.6.0 yarl==1.9.2 From 557b6d511bea82537eafc1f8c0806760e0c49102 Mon Sep 17 00:00:00 2001 From: elmurato <1382097+elmurato@users.noreply.github.com> Date: Mon, 24 Jul 2023 20:23:11 +0200 Subject: [PATCH 0874/1009] Improve reading of MOTD and bump mcstatus to 11.0.0 (#95715) * Improve reading of MOTD, bump mcstatus to 10.0.3 and getmac to 0.9.4 * Revert bump of getmac * Bump mcstatus to 11.0.0-rc3. Use new MOTD parser. * Bump mcstatus to 11.0.0 --- .../components/minecraft_server/__init__.py | 34 +++++++---------- .../minecraft_server/config_flow.py | 5 ++- .../components/minecraft_server/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../minecraft_server/test_config_flow.py | 38 ++++++++++++------- 6 files changed, 44 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index 801b27ee9716f2..da897a9767f026 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -6,7 +6,7 @@ import logging from typing import Any -from mcstatus.server import MinecraftServer as MCStatus +from mcstatus.server import JavaServer from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, Platform @@ -69,9 +69,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> class MinecraftServer: """Representation of a Minecraft server.""" - # Private constants - _MAX_RETRIES_STATUS = 3 - def __init__( self, hass: HomeAssistant, unique_id: str, config_data: Mapping[str, Any] ) -> None: @@ -88,16 +85,16 @@ def __init__( self.srv_record_checked = False # 3rd party library instance - self._mc_status = MCStatus(self.host, self.port) + self._server = JavaServer(self.host, self.port) # Data provided by 3rd party library - self.version = None - self.protocol_version = None - self.latency_time = None - self.players_online = None - self.players_max = None + self.version: str | None = None + self.protocol_version: int | None = None + self.latency_time: float | None = None + self.players_online: int | None = None + self.players_max: int | None = None self.players_list: list[str] | None = None - self.motd = None + self.motd: str | None = None # Dispatcher signal name self.signal_name = f"{SIGNAL_NAME_PREFIX}_{self.unique_id}" @@ -133,13 +130,11 @@ async def async_check_connection(self) -> None: # with data extracted out of SRV record. self.host = srv_record[CONF_HOST] self.port = srv_record[CONF_PORT] - self._mc_status = MCStatus(self.host, self.port) + self._server = JavaServer(self.host, self.port) # Ping the server with a status request. try: - await self._hass.async_add_executor_job( - self._mc_status.status, self._MAX_RETRIES_STATUS - ) + await self._server.async_status() self.online = True except OSError as error: _LOGGER.debug( @@ -176,9 +171,7 @@ async def async_update(self, now: datetime | None = None) -> None: async def _async_status_request(self) -> None: """Request server status and update properties.""" try: - status_response = await self._hass.async_add_executor_job( - self._mc_status.status, self._MAX_RETRIES_STATUS - ) + status_response = await self._server.async_status() # Got answer to request, update properties. self.version = status_response.version.name @@ -186,7 +179,8 @@ async def _async_status_request(self) -> None: self.players_online = status_response.players.online self.players_max = status_response.players.max self.latency_time = status_response.latency - self.motd = (status_response.description).get("text") + self.motd = status_response.motd.to_plain() + self.players_list = [] if status_response.players.sample is not None: for player in status_response.players.sample: @@ -244,7 +238,7 @@ def __init__( manufacturer=MANUFACTURER, model=f"Minecraft Server ({self._server.version})", name=self._server.name, - sw_version=self._server.protocol_version, + sw_version=str(self._server.protocol_version), ) self._attr_device_class = device_class self._extra_state_attributes = None diff --git a/homeassistant/components/minecraft_server/config_flow.py b/homeassistant/components/minecraft_server/config_flow.py index 691aea0f75e669..b402b7cfff0800 100644 --- a/homeassistant/components/minecraft_server/config_flow.py +++ b/homeassistant/components/minecraft_server/config_flow.py @@ -8,6 +8,7 @@ from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.data_entry_flow import FlowResult from . import MinecraftServer, helpers from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, DOMAIN @@ -18,7 +19,7 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> FlowResult: """Handle the initial step.""" errors = {} @@ -117,7 +118,7 @@ async def async_step_user(self, user_input=None): # form filled with user_input and eventually with errors otherwise). return self._show_config_form(user_input, errors) - def _show_config_form(self, user_input=None, errors=None): + def _show_config_form(self, user_input=None, errors=None) -> FlowResult: """Show the setup form to the user.""" if user_input is None: user_input = {} diff --git a/homeassistant/components/minecraft_server/manifest.json b/homeassistant/components/minecraft_server/manifest.json index b831e1eae90f9a..27019cb80a8bf4 100644 --- a/homeassistant/components/minecraft_server/manifest.json +++ b/homeassistant/components/minecraft_server/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["dnspython", "mcstatus"], "quality_scale": "silver", - "requirements": ["aiodns==3.0.0", "getmac==0.8.2", "mcstatus==6.0.0"] + "requirements": ["aiodns==3.0.0", "getmac==0.8.2", "mcstatus==11.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index b15c4f97c886db..394bf420796b9a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1175,7 +1175,7 @@ maxcube-api==0.4.3 mbddns==0.1.2 # homeassistant.components.minecraft_server -mcstatus==6.0.0 +mcstatus==11.0.0 # homeassistant.components.meater meater-python==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0b49ab41f0ded..2b84604df48119 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -898,7 +898,7 @@ maxcube-api==0.4.3 mbddns==0.1.2 # homeassistant.components.minecraft_server -mcstatus==6.0.0 +mcstatus==11.0.0 # homeassistant.components.meater meater-python==0.0.8 diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py index 25c0cec441b2e9..ac5ae7dbc6e7bf 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import aiodns -from mcstatus.pinger import PingResponse +from mcstatus.status_response import JavaStatusResponse from homeassistant.components.minecraft_server.const import ( DEFAULT_NAME, @@ -22,7 +22,7 @@ class QueryMock: """Mock for result of aiodns.DNSResolver.query.""" - def __init__(self): + def __init__(self) -> None: """Set up query result mock.""" self.host = "mc.dummyserver.com" self.port = 23456 @@ -31,7 +31,7 @@ def __init__(self): self.ttl = None -STATUS_RESPONSE_RAW = { +JAVA_STATUS_RESPONSE_RAW = { "description": {"text": "Dummy Description"}, "version": {"name": "Dummy Version", "protocol": 123}, "players": { @@ -103,8 +103,10 @@ async def test_same_host(hass: HomeAssistant) -> None: "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, ), patch( - "mcstatus.server.MinecraftServer.status", - return_value=PingResponse(STATUS_RESPONSE_RAW), + "mcstatus.server.JavaServer.async_status", + return_value=JavaStatusResponse( + None, None, None, None, JAVA_STATUS_RESPONSE_RAW, None + ), ): unique_id = "mc.dummyserver.com-25565" config_data = { @@ -158,7 +160,7 @@ async def test_connection_failed(hass: HomeAssistant) -> None: with patch( "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, - ), patch("mcstatus.server.MinecraftServer.status", side_effect=OSError): + ), patch("mcstatus.server.JavaServer.async_status", side_effect=OSError): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) @@ -173,8 +175,10 @@ async def test_connection_succeeded_with_srv_record(hass: HomeAssistant) -> None "aiodns.DNSResolver.query", return_value=SRV_RECORDS, ), patch( - "mcstatus.server.MinecraftServer.status", - return_value=PingResponse(STATUS_RESPONSE_RAW), + "mcstatus.server.JavaServer.async_status", + return_value=JavaStatusResponse( + None, None, None, None, JAVA_STATUS_RESPONSE_RAW, None + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_SRV @@ -192,8 +196,10 @@ async def test_connection_succeeded_with_host(hass: HomeAssistant) -> None: "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, ), patch( - "mcstatus.server.MinecraftServer.status", - return_value=PingResponse(STATUS_RESPONSE_RAW), + "mcstatus.server.JavaServer.async_status", + return_value=JavaStatusResponse( + None, None, None, None, JAVA_STATUS_RESPONSE_RAW, None + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT @@ -211,8 +217,10 @@ async def test_connection_succeeded_with_ip4(hass: HomeAssistant) -> None: "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, ), patch( - "mcstatus.server.MinecraftServer.status", - return_value=PingResponse(STATUS_RESPONSE_RAW), + "mcstatus.server.JavaServer.async_status", + return_value=JavaStatusResponse( + None, None, None, None, JAVA_STATUS_RESPONSE_RAW, None + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV4 @@ -230,8 +238,10 @@ async def test_connection_succeeded_with_ip6(hass: HomeAssistant) -> None: "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, ), patch( - "mcstatus.server.MinecraftServer.status", - return_value=PingResponse(STATUS_RESPONSE_RAW), + "mcstatus.server.JavaServer.async_status", + return_value=JavaStatusResponse( + None, None, None, None, JAVA_STATUS_RESPONSE_RAW, None + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV6 From ba1bf9d39fdf49b149332a1e63f8a9d9b1c132dd Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 24 Jul 2023 20:29:23 +0200 Subject: [PATCH 0875/1009] Add entity translations to AsusWRT (#95125) --- homeassistant/components/asuswrt/sensor.py | 22 +++++------ homeassistant/components/asuswrt/strings.json | 39 ++++++++++++++++++- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 7f54bc29393270..4f9ec0af411c5a 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -50,14 +50,14 @@ class AsusWrtSensorEntityDescription(SensorEntityDescription): CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( AsusWrtSensorEntityDescription( key=SENSORS_CONNECTED_DEVICE[0], - name="Devices Connected", + translation_key="devices_connected", icon="mdi:router-network", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UNIT_DEVICES, ), AsusWrtSensorEntityDescription( key=SENSORS_RATES[0], - name="Download Speed", + translation_key="download_speed", icon="mdi:download-network", device_class=SensorDeviceClass.DATA_RATE, state_class=SensorStateClass.MEASUREMENT, @@ -68,7 +68,7 @@ class AsusWrtSensorEntityDescription(SensorEntityDescription): ), AsusWrtSensorEntityDescription( key=SENSORS_RATES[1], - name="Upload Speed", + translation_key="upload_speed", icon="mdi:upload-network", device_class=SensorDeviceClass.DATA_RATE, state_class=SensorStateClass.MEASUREMENT, @@ -79,7 +79,7 @@ class AsusWrtSensorEntityDescription(SensorEntityDescription): ), AsusWrtSensorEntityDescription( key=SENSORS_BYTES[0], - name="Download", + translation_key="download", icon="mdi:download", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfInformation.GIGABYTES, @@ -90,7 +90,7 @@ class AsusWrtSensorEntityDescription(SensorEntityDescription): ), AsusWrtSensorEntityDescription( key=SENSORS_BYTES[1], - name="Upload", + translation_key="upload", icon="mdi:upload", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfInformation.GIGABYTES, @@ -101,7 +101,7 @@ class AsusWrtSensorEntityDescription(SensorEntityDescription): ), AsusWrtSensorEntityDescription( key=SENSORS_LOAD_AVG[0], - name="Load Avg (1m)", + translation_key="load_avg_1m", icon="mdi:cpu-32-bit", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -110,7 +110,7 @@ class AsusWrtSensorEntityDescription(SensorEntityDescription): ), AsusWrtSensorEntityDescription( key=SENSORS_LOAD_AVG[1], - name="Load Avg (5m)", + translation_key="load_avg_5m", icon="mdi:cpu-32-bit", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -119,7 +119,7 @@ class AsusWrtSensorEntityDescription(SensorEntityDescription): ), AsusWrtSensorEntityDescription( key=SENSORS_LOAD_AVG[2], - name="Load Avg (15m)", + translation_key="load_avg_15m", icon="mdi:cpu-32-bit", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -128,7 +128,7 @@ class AsusWrtSensorEntityDescription(SensorEntityDescription): ), AsusWrtSensorEntityDescription( key=SENSORS_TEMPERATURES[0], - name="2.4GHz Temperature", + translation_key="24ghz_temperature", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -138,7 +138,7 @@ class AsusWrtSensorEntityDescription(SensorEntityDescription): ), AsusWrtSensorEntityDescription( key=SENSORS_TEMPERATURES[1], - name="5GHz Temperature", + translation_key="5ghz_temperature", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -148,7 +148,7 @@ class AsusWrtSensorEntityDescription(SensorEntityDescription): ), AsusWrtSensorEntityDescription( key=SENSORS_TEMPERATURES[2], - name="CPU Temperature", + translation_key="cpu_temperature", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, diff --git a/homeassistant/components/asuswrt/strings.json b/homeassistant/components/asuswrt/strings.json index f6ccb5a7c9cb40..52b9f919434381 100644 --- a/homeassistant/components/asuswrt/strings.json +++ b/homeassistant/components/asuswrt/strings.json @@ -36,11 +36,48 @@ "data": { "consider_home": "Seconds to wait before considering a device away", "track_unknown": "Track unknown / unnamed devices", - "interface": "The interface that you want statistics from (e.g. eth0,eth1 etc)", + "interface": "The interface that you want statistics from (e.g. eth0, eth1 etc)", "dnsmasq": "The location in the router of the dnsmasq.leases files", "require_ip": "Devices must have IP (for access point mode)" } } } + }, + "entity": { + "sensor": { + "devices_connected": { + "name": "Devices connected" + }, + "download_speed": { + "name": "Download speed" + }, + "upload_speed": { + "name": "Upload speed" + }, + "download": { + "name": "Download" + }, + "upload": { + "name": "Upload" + }, + "load_avg_1m": { + "name": "Average load (1m)" + }, + "load_avg_5m": { + "name": "Average load (5m)" + }, + "load_avg_15m": { + "name": "Average load (15m)" + }, + "24ghz_temperature": { + "name": "2.4GHz Temperature" + }, + "5ghz_temperature": { + "name": "5GHz Temperature" + }, + "cpu_temperature": { + "name": "CPU Temperature" + } + } } } From 5cc72814c96071e2e99f4391ac84070366c158d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jul 2023 13:34:46 -0500 Subject: [PATCH 0876/1009] Bump fnv-hash-fast to 0.4.0 (#97160) --- homeassistant/components/homekit/manifest.json | 2 +- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 245dbd0a19e487..19fd0b518b24b7 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -10,7 +10,7 @@ "loggers": ["pyhap"], "requirements": [ "HAP-python==4.7.0", - "fnv-hash-fast==0.3.1", + "fnv-hash-fast==0.4.0", "PyQRCode==1.2.1", "base36==0.1.1" ], diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 2e868542457646..6f919ee50da6ce 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -8,7 +8,7 @@ "quality_scale": "internal", "requirements": [ "SQLAlchemy==2.0.15", - "fnv-hash-fast==0.3.1", + "fnv-hash-fast==0.4.0", "psutil-home-assistant==0.0.1" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f2f3f482fe8d79..abee7ee1d0062c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.2 dbus-fast==1.87.1 -fnv-hash-fast==0.3.1 +fnv-hash-fast==0.4.0 ha-av==10.1.0 hass-nabucasa==0.69.0 hassil==1.0.6 diff --git a/requirements_all.txt b/requirements_all.txt index 394bf420796b9a..2def3fca3bd501 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -798,7 +798,7 @@ flux-led==1.0.1 # homeassistant.components.homekit # homeassistant.components.recorder -fnv-hash-fast==0.3.1 +fnv-hash-fast==0.4.0 # homeassistant.components.foobot foobot-async==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b84604df48119..1264ef4ff358dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -626,7 +626,7 @@ flux-led==1.0.1 # homeassistant.components.homekit # homeassistant.components.recorder -fnv-hash-fast==0.3.1 +fnv-hash-fast==0.4.0 # homeassistant.components.foobot foobot-async==1.0.0 From f8705a8074f32aa84aabfd62a41e15afd6e4120b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jul 2023 13:34:59 -0500 Subject: [PATCH 0877/1009] Bump anyio to 3.7.1 (#97165) --- homeassistant/package_constraints.txt | 2 +- script/gen_requirements_all.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index abee7ee1d0062c..d2ec1c7bdda667 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -100,7 +100,7 @@ regex==2021.8.28 # these requirements are quite loose. As the entire stack has some outstanding issues, and # even newer versions seem to introduce new issues, it's useful for us to pin all these # requirements so we can directly link HA versions to these library versions. -anyio==3.7.0 +anyio==3.7.1 h11==0.14.0 httpcore==0.17.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index f215b649bb2ad3..9302d5477865cd 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -102,7 +102,7 @@ # these requirements are quite loose. As the entire stack has some outstanding issues, and # even newer versions seem to introduce new issues, it's useful for us to pin all these # requirements so we can directly link HA versions to these library versions. -anyio==3.7.0 +anyio==3.7.1 h11==0.14.0 httpcore==0.17.3 From 2dc86364f33411e18e077172fb8a860d559c5a99 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 24 Jul 2023 20:49:40 +0200 Subject: [PATCH 0878/1009] Migrate TPLink to has entity name (#96246) --- homeassistant/components/tplink/entity.py | 3 +- homeassistant/components/tplink/light.py | 1 + homeassistant/components/tplink/sensor.py | 13 ++------- homeassistant/components/tplink/strings.json | 18 ++++++++++++ homeassistant/components/tplink/switch.py | 30 ++++++++++++++++++-- tests/components/tplink/test_switch.py | 4 +-- 6 files changed, 54 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 01e124dea1a462..4bf076a59bc1fc 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -32,13 +32,14 @@ async def _async_wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator]): """Common base class for all coordinated tplink entities.""" + _attr_has_entity_name = True + def __init__( self, device: SmartDevice, coordinator: TPLinkDataUpdateCoordinator ) -> None: """Initialize the switch.""" super().__init__(coordinator) self.device: SmartDevice = device - self._attr_name = self.device.alias self._attr_unique_id = self.device.device_id @property diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index e4f91f282f6b46..db7e6ff355e827 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -162,6 +162,7 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): """Representation of a TPLink Smart Bulb.""" _attr_supported_features = LightEntityFeature.TRANSITION + _attr_name = None device: SmartBulb diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index 7471ed8982b6f6..ba4949434f730a 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -46,6 +46,7 @@ class TPLinkSensorEntityDescription(SensorEntityDescription): ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( TPLinkSensorEntityDescription( key=ATTR_CURRENT_POWER_W, + translation_key="current_consumption", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -55,6 +56,7 @@ class TPLinkSensorEntityDescription(SensorEntityDescription): ), TPLinkSensorEntityDescription( key=ATTR_TOTAL_ENERGY_KWH, + translation_key="total_consumption", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -64,6 +66,7 @@ class TPLinkSensorEntityDescription(SensorEntityDescription): ), TPLinkSensorEntityDescription( key=ATTR_TODAY_ENERGY_KWH, + translation_key="today_consumption", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -75,7 +78,6 @@ class TPLinkSensorEntityDescription(SensorEntityDescription): native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, - name="Voltage", emeter_attr="voltage", precision=1, ), @@ -84,7 +86,6 @@ class TPLinkSensorEntityDescription(SensorEntityDescription): native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, - name="Current", emeter_attr="current", precision=2, ), @@ -155,14 +156,6 @@ def __init__( f"{legacy_device_id(self.device)}_{self.entity_description.key}" ) - @property - def name(self) -> str: - """Return the name of the Smart Plug. - - Overridden to include the description. - """ - return f"{self.device.alias} {self.entity_description.name}" - @property def native_value(self) -> float | None: """Return the sensors state.""" diff --git a/homeassistant/components/tplink/strings.json b/homeassistant/components/tplink/strings.json index 6daa5c9cb1aecd..750d422cd0df2a 100644 --- a/homeassistant/components/tplink/strings.json +++ b/homeassistant/components/tplink/strings.json @@ -25,6 +25,24 @@ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } }, + "entity": { + "sensor": { + "current_consumption": { + "name": "Current consumption" + }, + "total_consumption": { + "name": "Total consumption" + }, + "today_consumption": { + "name": "Today's consumption" + } + }, + "switch": { + "led": { + "name": "LED" + } + } + }, "services": { "sequence_effect": { "name": "Sequence effect", diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index aa0616447ccc39..d82308a2e3299e 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -35,7 +35,7 @@ async def async_setup_entry( # Historically we only add the children if the device is a strip _LOGGER.debug("Initializing strip with %s sockets", len(device.children)) for child in device.children: - entities.append(SmartPlugSwitch(child, coordinator)) + entities.append(SmartPlugSwitchChild(device, coordinator, child)) elif device.is_plug: entities.append(SmartPlugSwitch(device, coordinator)) @@ -49,6 +49,7 @@ class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity): device: SmartPlug + _attr_translation_key = "led" _attr_entity_category = EntityCategory.CONFIG def __init__( @@ -57,7 +58,6 @@ def __init__( """Initialize the LED switch.""" super().__init__(device, coordinator) - self._attr_name = f"{device.alias} LED" self._attr_unique_id = f"{self.device.mac}_led" @property @@ -103,3 +103,29 @@ async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self.device.turn_off() + + +class SmartPlugSwitchChild(SmartPlugSwitch): + """Representation of an individual plug of a TPLink Smart Plug strip.""" + + def __init__( + self, + device: SmartDevice, + coordinator: TPLinkDataUpdateCoordinator, + plug: SmartDevice, + ) -> None: + """Initialize the switch.""" + super().__init__(device, coordinator) + self._plug = plug + self._attr_unique_id = legacy_device_id(plug) + self._attr_name = plug.alias + + @async_refresh_after + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + await self._plug.turn_on() + + @async_refresh_after + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + await self._plug.turn_off() diff --git a/tests/components/tplink/test_switch.py b/tests/components/tplink/test_switch.py index ba4419500f45ae..1e5e03c0f37ba6 100644 --- a/tests/components/tplink/test_switch.py +++ b/tests/components/tplink/test_switch.py @@ -147,7 +147,7 @@ async def test_strip(hass: HomeAssistant) -> None: assert hass.states.get("switch.my_strip") is None for plug_id in range(2): - entity_id = f"switch.plug{plug_id}" + entity_id = f"switch.my_strip_plug{plug_id}" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -176,7 +176,7 @@ async def test_strip_unique_ids(hass: HomeAssistant) -> None: await hass.async_block_till_done() for plug_id in range(2): - entity_id = f"switch.plug{plug_id}" + entity_id = f"switch.my_strip_plug{plug_id}" entity_registry = er.async_get(hass) assert ( entity_registry.async_get(entity_id).unique_id == f"PLUG{plug_id}DEVICEID" From 8ff9f2ddbe95c0398ba674e2bb1c6f8f3669cdef Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 24 Jul 2023 21:12:37 +0200 Subject: [PATCH 0879/1009] Add date platform to KNX (#97154) --- homeassistant/components/knx/__init__.py | 2 + homeassistant/components/knx/const.py | 1 + homeassistant/components/knx/date.py | 100 +++++++++++++++++++++++ homeassistant/components/knx/schema.py | 19 +++++ tests/components/knx/test_date.py | 86 +++++++++++++++++++ 5 files changed, 208 insertions(+) create mode 100644 homeassistant/components/knx/date.py create mode 100644 tests/components/knx/test_date.py diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index c30098f254b59f..f0ee9576cc79bc 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -80,6 +80,7 @@ ButtonSchema, ClimateSchema, CoverSchema, + DateSchema, EventSchema, ExposeSchema, FanSchema, @@ -136,6 +137,7 @@ **ButtonSchema.platform_node(), **ClimateSchema.platform_node(), **CoverSchema.platform_node(), + **DateSchema.platform_node(), **FanSchema.platform_node(), **LightSchema.platform_node(), **NotifySchema.platform_node(), diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index bdc480851c32a6..c96f10736ddd8c 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -127,6 +127,7 @@ class ColorTempModes(Enum): Platform.BUTTON, Platform.CLIMATE, Platform.COVER, + Platform.DATE, Platform.FAN, Platform.LIGHT, Platform.NOTIFY, diff --git a/homeassistant/components/knx/date.py b/homeassistant/components/knx/date.py new file mode 100644 index 00000000000000..1f286d59ecbd89 --- /dev/null +++ b/homeassistant/components/knx/date.py @@ -0,0 +1,100 @@ +"""Support for KNX/IP date.""" +from __future__ import annotations + +from datetime import date as dt_date +import time +from typing import Final + +from xknx import XKNX +from xknx.devices import DateTime as XknxDateTime + +from homeassistant import config_entries +from homeassistant.components.date import DateEntity +from homeassistant.const import ( + CONF_ENTITY_CATEGORY, + CONF_NAME, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType + +from .const import ( + CONF_RESPOND_TO_READ, + CONF_STATE_ADDRESS, + CONF_SYNC_STATE, + DATA_KNX_CONFIG, + DOMAIN, + KNX_ADDRESS, +) +from .knx_entity import KnxEntity + +_DATE_TRANSLATION_FORMAT: Final = "%Y-%m-%d" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up entities for KNX platform.""" + xknx: XKNX = hass.data[DOMAIN].xknx + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.DATE] + + async_add_entities(KNXDate(xknx, entity_config) for entity_config in config) + + +def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTime: + """Return a XKNX DateTime object to be used within XKNX.""" + return XknxDateTime( + xknx, + name=config[CONF_NAME], + broadcast_type="DATE", + localtime=False, + group_address=config[KNX_ADDRESS], + group_address_state=config.get(CONF_STATE_ADDRESS), + respond_to_read=config[CONF_RESPOND_TO_READ], + sync_state=config[CONF_SYNC_STATE], + ) + + +class KNXDate(KnxEntity, DateEntity, RestoreEntity): + """Representation of a KNX date.""" + + _device: XknxDateTime + + def __init__(self, xknx: XKNX, config: ConfigType) -> None: + """Initialize a KNX time.""" + super().__init__(_create_xknx_device(xknx, config)) + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) + self._attr_unique_id = str(self._device.remote_value.group_address) + + async def async_added_to_hass(self) -> None: + """Restore last state.""" + await super().async_added_to_hass() + if ( + not self._device.remote_value.readable + and (last_state := await self.async_get_last_state()) is not None + and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) + ): + self._device.remote_value.value = time.strptime( + last_state.state, _DATE_TRANSLATION_FORMAT + ) + + @property + def native_value(self) -> dt_date | None: + """Return the latest value.""" + if (time_struct := self._device.remote_value.value) is None: + return None + return dt_date( + year=time_struct.tm_year, + month=time_struct.tm_mon, + day=time_struct.tm_mday, + ) + + async def async_set_value(self, value: dt_date) -> None: + """Change the value.""" + await self._device.set(value.timetuple()) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 86bf790a0775cc..40cc2232d8f3bd 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -556,6 +556,25 @@ class CoverSchema(KNXPlatformSchema): ) +class DateSchema(KNXPlatformSchema): + """Voluptuous schema for KNX date.""" + + PLATFORM = Platform.DATE + + DEFAULT_NAME = "KNX Date" + + ENTITY_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean, + vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, + vol.Required(KNX_ADDRESS): ga_list_validator, + vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + } + ) + + class ExposeSchema(KNXPlatformSchema): """Voluptuous schema for KNX exposures.""" diff --git a/tests/components/knx/test_date.py b/tests/components/knx/test_date.py new file mode 100644 index 00000000000000..bfde519f3c0355 --- /dev/null +++ b/tests/components/knx/test_date.py @@ -0,0 +1,86 @@ +"""Test KNX date.""" +from homeassistant.components.date import ATTR_DATE, DOMAIN, SERVICE_SET_VALUE +from homeassistant.components.knx.const import CONF_RESPOND_TO_READ, KNX_ADDRESS +from homeassistant.components.knx.schema import DateSchema +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant, State + +from .conftest import KNXTestKit + +from tests.common import mock_restore_cache + + +async def test_date(hass: HomeAssistant, knx: KNXTestKit) -> None: + """Test KNX date.""" + test_address = "1/1/1" + await knx.setup_integration( + { + DateSchema.PLATFORM: { + CONF_NAME: "test", + KNX_ADDRESS: test_address, + } + } + ) + # set value + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {"entity_id": "date.test", ATTR_DATE: "1999-03-31"}, + blocking=True, + ) + await knx.assert_write( + test_address, + (0x1F, 0x03, 0x63), + ) + state = hass.states.get("date.test") + assert state.state == "1999-03-31" + + # update from KNX + await knx.receive_write( + test_address, + (0x01, 0x02, 0x03), + ) + state = hass.states.get("date.test") + assert state.state == "2003-02-01" + + +async def test_date_restore_and_respond(hass: HomeAssistant, knx: KNXTestKit) -> None: + """Test KNX date with passive_address, restoring state and respond_to_read.""" + test_address = "1/1/1" + test_passive_address = "3/3/3" + + fake_state = State("date.test", "2023-07-24") + mock_restore_cache(hass, (fake_state,)) + + await knx.setup_integration( + { + DateSchema.PLATFORM: { + CONF_NAME: "test", + KNX_ADDRESS: [test_address, test_passive_address], + CONF_RESPOND_TO_READ: True, + } + } + ) + # restored state - doesn't send telegram + state = hass.states.get("date.test") + assert state.state == "2023-07-24" + await knx.assert_telegram_count(0) + + # respond with restored state + await knx.receive_read(test_address) + await knx.assert_response( + test_address, + (0x18, 0x07, 0x17), + ) + + # don't respond to passive address + await knx.receive_read(test_passive_address) + await knx.assert_no_telegram() + + # update from KNX passive address + await knx.receive_write( + test_passive_address, + (0x18, 0x02, 0x18), + ) + state = hass.states.get("date.test") + assert state.state == "2024-02-24" From 28197adebd13de0a64dbb7ed44ab4b9a44511207 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Mon, 24 Jul 2023 21:13:16 +0200 Subject: [PATCH 0880/1009] Add support for sleepy Xiaomi BLE sensors (#97166) --- .../components/xiaomi_ble/__init__.py | 34 ++++- .../components/xiaomi_ble/binary_sensor.py | 25 ++-- homeassistant/components/xiaomi_ble/const.py | 4 +- .../components/xiaomi_ble/coordinator.py | 63 +++++++++ homeassistant/components/xiaomi_ble/sensor.py | 22 +++- tests/components/xiaomi_ble/test_sensor.py | 121 +++++++++++++++++- 6 files changed, 242 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/xiaomi_ble/coordinator.py diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 3930c50c70ce3d..1810d52323cb54 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -12,15 +12,18 @@ BluetoothServiceInfoBleak, async_ble_device_from_address, ) -from homeassistant.components.bluetooth.active_update_processor import ( - ActiveBluetoothProcessorCoordinator, -) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers.device_registry import DeviceRegistry, async_get -from .const import DOMAIN, XIAOMI_BLE_EVENT, XiaomiBleEvent +from .const import ( + CONF_DISCOVERED_EVENT_CLASSES, + DOMAIN, + XIAOMI_BLE_EVENT, + XiaomiBleEvent, +) +from .coordinator import XiaomiActiveBluetoothProcessorCoordinator PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -36,6 +39,10 @@ def process_service_info( ) -> SensorUpdate: """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" update = data.update(service_info) + coordinator: XiaomiActiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + discovered_device_classes = coordinator.discovered_device_classes if update.events: address = service_info.device.address for device_key, event in update.events.items(): @@ -49,6 +56,16 @@ def process_service_info( sw_version=sensor_device_info.sw_version, hw_version=sensor_device_info.hw_version, ) + event_class = event.device_key.key + event_type = event.event_type + + if event_class not in discovered_device_classes: + discovered_device_classes.add(event_class) + hass.config_entries.async_update_entry( + entry, + data=entry.data + | {CONF_DISCOVERED_EVENT_CLASSES: list(discovered_device_classes)}, + ) hass.bus.async_fire( XIAOMI_BLE_EVENT, @@ -56,7 +73,8 @@ def process_service_info( XiaomiBleEvent( device_id=device.id, address=address, - event_type=event.event_type, + event_class=event_class, # ie 'button' + event_type=event_type, # ie 'press' event_properties=event.event_properties, ) ), @@ -121,7 +139,7 @@ async def _async_poll(service_info: BluetoothServiceInfoBleak): device_registry = async_get(hass) coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id - ] = ActiveBluetoothProcessorCoordinator( + ] = XiaomiActiveBluetoothProcessorCoordinator( hass, _LOGGER, address=address, @@ -130,6 +148,10 @@ async def _async_poll(service_info: BluetoothServiceInfoBleak): hass, entry, data, service_info, device_registry ), needs_poll_method=_needs_poll, + device_data=data, + discovered_device_classes=set( + entry.data.get(CONF_DISCOVERED_EVENT_CLASSES, []) + ), poll_method=_async_poll, # We will take advertisements from non-connectable devices # since we will trade the BLEDevice for a connectable one diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py index 3d7bdfd0b4804d..f7c4c87014c97b 100644 --- a/homeassistant/components/xiaomi_ble/binary_sensor.py +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -1,7 +1,6 @@ """Support for Xiaomi binary sensors.""" from __future__ import annotations -from xiaomi_ble import SLEEPY_DEVICE_MODELS from xiaomi_ble.parser import ( BinarySensorDeviceClass as XiaomiBinarySensorDeviceClass, ExtendedBinarySensorDeviceClass, @@ -15,17 +14,18 @@ BinarySensorEntityDescription, ) from homeassistant.components.bluetooth.passive_update_processor import ( - PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, - PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorEntity, ) -from homeassistant.const import ATTR_MODEL from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN +from .coordinator import ( + XiaomiActiveBluetoothProcessorCoordinator, + XiaomiPassiveBluetoothDataProcessor, +) from .device import device_key_to_bluetooth_entity_key BINARY_SENSOR_DESCRIPTIONS = { @@ -108,10 +108,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Xiaomi BLE sensors.""" - coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + coordinator: XiaomiActiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] - processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + processor = XiaomiPassiveBluetoothDataProcessor( + sensor_update_to_bluetooth_data_update + ) entry.async_on_unload( processor.async_add_entities_listener( XiaomiBluetoothSensorEntity, async_add_entities @@ -121,7 +123,7 @@ async def async_setup_entry( class XiaomiBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[bool | None]], + PassiveBluetoothProcessorEntity[XiaomiPassiveBluetoothDataProcessor], BinarySensorEntity, ): """Representation of a Xiaomi binary sensor.""" @@ -134,8 +136,7 @@ def is_on(self) -> bool | None: @property def available(self) -> bool: """Return True if entity is available.""" - if self.device_info and self.device_info[ATTR_MODEL] in SLEEPY_DEVICE_MODELS: - # These devices sleep for an indeterminate amount of time - # so there is no way to track their availability. - return True - return super().available + coordinator: XiaomiActiveBluetoothProcessorCoordinator = ( + self.processor.coordinator + ) + return coordinator.device_data.sleepy_device or super().available diff --git a/homeassistant/components/xiaomi_ble/const.py b/homeassistant/components/xiaomi_ble/const.py index dda6c61d8aa607..1566478bcea689 100644 --- a/homeassistant/components/xiaomi_ble/const.py +++ b/homeassistant/components/xiaomi_ble/const.py @@ -6,6 +6,7 @@ DOMAIN = "xiaomi_ble" +CONF_DISCOVERED_EVENT_CLASSES: Final = "known_events" CONF_EVENT_PROPERTIES: Final = "event_properties" EVENT_PROPERTIES: Final = "event_properties" EVENT_TYPE: Final = "event_type" @@ -17,5 +18,6 @@ class XiaomiBleEvent(TypedDict): device_id: str address: str - event_type: str + event_class: str # ie 'button' + event_type: str # ie 'press' event_properties: dict[str, str | int | float | None] | None diff --git a/homeassistant/components/xiaomi_ble/coordinator.py b/homeassistant/components/xiaomi_ble/coordinator.py new file mode 100644 index 00000000000000..2a4b35f61719e0 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/coordinator.py @@ -0,0 +1,63 @@ +"""The Xiaomi BLE integration.""" +from collections.abc import Callable, Coroutine +from logging import Logger +from typing import Any + +from xiaomi_ble import XiaomiBluetoothDeviceData + +from homeassistant.components.bluetooth import ( + BluetoothScanningMode, + BluetoothServiceInfoBleak, +) +from homeassistant.components.bluetooth.active_update_processor import ( + ActiveBluetoothProcessorCoordinator, +) +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.debounce import Debouncer + + +class XiaomiActiveBluetoothProcessorCoordinator(ActiveBluetoothProcessorCoordinator): + """Define a Xiaomi Bluetooth Active Update Processor Coordinator.""" + + def __init__( + self, + hass: HomeAssistant, + logger: Logger, + *, + address: str, + mode: BluetoothScanningMode, + update_method: Callable[[BluetoothServiceInfoBleak], Any], + needs_poll_method: Callable[[BluetoothServiceInfoBleak, float | None], bool], + device_data: XiaomiBluetoothDeviceData, + discovered_device_classes: set[str], + poll_method: Callable[ + [BluetoothServiceInfoBleak], + Coroutine[Any, Any, Any], + ] + | None = None, + poll_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None, + connectable: bool = True, + ) -> None: + """Initialize the Xiaomi Bluetooth Active Update Processor Coordinator.""" + super().__init__( + hass=hass, + logger=logger, + address=address, + mode=mode, + update_method=update_method, + needs_poll_method=needs_poll_method, + poll_method=poll_method, + poll_debouncer=poll_debouncer, + connectable=connectable, + ) + self.discovered_device_classes = discovered_device_classes + self.device_data = device_data + + +class XiaomiPassiveBluetoothDataProcessor(PassiveBluetoothDataProcessor): + """Define a Xiaomi Bluetooth Passive Update Data Processor.""" + + coordinator: XiaomiActiveBluetoothProcessorCoordinator diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index 84ef91bf5a81a8..f0f0d7fa71e174 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -5,9 +5,7 @@ from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( - PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, - PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import ( @@ -33,6 +31,10 @@ from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN +from .coordinator import ( + XiaomiActiveBluetoothProcessorCoordinator, + XiaomiPassiveBluetoothDataProcessor, +) from .device import device_key_to_bluetooth_entity_key SENSOR_DESCRIPTIONS = { @@ -170,10 +172,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Xiaomi BLE sensors.""" - coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + coordinator: XiaomiActiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] - processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + processor = XiaomiPassiveBluetoothDataProcessor( + sensor_update_to_bluetooth_data_update + ) entry.async_on_unload( processor.async_add_entities_listener( XiaomiBluetoothSensorEntity, async_add_entities @@ -183,7 +187,7 @@ async def async_setup_entry( class XiaomiBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], + PassiveBluetoothProcessorEntity[XiaomiPassiveBluetoothDataProcessor], SensorEntity, ): """Representation of a xiaomi ble sensor.""" @@ -192,3 +196,11 @@ class XiaomiBluetoothSensorEntity( def native_value(self) -> int | float | None: """Return the native value.""" return self.processor.entity_data.get(self.entity_key) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + coordinator: XiaomiActiveBluetoothProcessorCoordinator = ( + self.processor.coordinator + ) + return coordinator.device_data.sleepy_device or super().available diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index fff8d9b20f11fb..7f39228a01270f 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -1,8 +1,20 @@ """Test Xiaomi BLE sensors.""" +from datetime import timedelta +import time +from unittest.mock import patch + +from homeassistant.components.bluetooth import ( + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, +) from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.xiaomi_ble.const import DOMAIN -from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util from . import ( HHCCJCY10_SERVICE_INFO, @@ -12,8 +24,11 @@ make_advertisement, ) -from tests.common import MockConfigEntry -from tests.components.bluetooth import inject_bluetooth_service_info_bleak +from tests.common import MockConfigEntry, async_fire_time_changed +from tests.components.bluetooth import ( + inject_bluetooth_service_info_bleak, + patch_all_discovered_devices, +) async def test_sensors(hass: HomeAssistant) -> None: @@ -610,3 +625,103 @@ async def test_miscale_v2_uuid(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_unavailable(hass: HomeAssistant) -> None: + """Test normal device goes to unavailable after 60 minutes.""" + start_monotonic = time.monotonic() + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="58:2D:34:12:20:89", + data={"bindkey": "a3bfe9853dd85a620debe3620caaa351"}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + "58:2D:34:12:20:89", + b"XXo\x06\x07\x89 \x124-X_\x17m\xd5O\x02\x00\x00/\xa4S\xfa", + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + temp_sensor = hass.states.get("sensor.temperature_humidity_sensor_2089_temperature") + assert temp_sensor.state == "22.6" + + # Fastforward time without BLE advertisements + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([]): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1), + ) + await hass.async_block_till_done() + + temp_sensor = hass.states.get("sensor.temperature_humidity_sensor_2089_temperature") + + # Sleepy devices should keep their state over time + assert temp_sensor.state == STATE_UNAVAILABLE + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_sleepy_device(hass: HomeAssistant) -> None: + """Test normal device goes to unavailable after 60 minutes.""" + start_monotonic = time.monotonic() + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="50:FB:19:1B:B5:DC", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak(hass, MISCALE_V1_SERVICE_INFO) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + mass_non_stabilized_sensor = hass.states.get( + "sensor.mi_smart_scale_b5dc_mass_non_stabilized" + ) + assert mass_non_stabilized_sensor.state == "86.55" + + # Fastforward time without BLE advertisements + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([]): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1), + ) + await hass.async_block_till_done() + + mass_non_stabilized_sensor = hass.states.get( + "sensor.mi_smart_scale_b5dc_mass_non_stabilized" + ) + + # Sleepy devices should keep their state over time + assert mass_non_stabilized_sensor.state == "86.55" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 410b343ae0be03a6c20b8ed2b9a52874dfae64b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Jul 2023 14:48:18 -0500 Subject: [PATCH 0881/1009] Bump dbus-fast to 1.87.2 (#97167) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 781e784fe6a137..9bd0672179ab46 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -19,6 +19,6 @@ "bluetooth-adapters==0.16.0", "bluetooth-auto-recovery==1.2.1", "bluetooth-data-tools==1.6.1", - "dbus-fast==1.87.1" + "dbus-fast==1.87.2" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d2ec1c7bdda667..232b2821209dad 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ bluetooth-data-tools==1.6.1 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.2 -dbus-fast==1.87.1 +dbus-fast==1.87.2 fnv-hash-fast==0.4.0 ha-av==10.1.0 hass-nabucasa==0.69.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2def3fca3bd501..ba62e1d6f0a836 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -629,7 +629,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.87.1 +dbus-fast==1.87.2 # homeassistant.components.debugpy debugpy==1.6.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1264ef4ff358dc..e8e0df2ef32e3a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -512,7 +512,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.87.1 +dbus-fast==1.87.2 # homeassistant.components.debugpy debugpy==1.6.7 From 8a58675be22ccc67ab4d8597d9a14094ed105fbb Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 24 Jul 2023 22:01:45 +0200 Subject: [PATCH 0882/1009] Reolink improve webhook URL error message (#96088) Co-authored-by: Franck Nijhof --- homeassistant/components/reolink/config_flow.py | 8 +++++++- homeassistant/components/reolink/strings.json | 3 ++- tests/components/reolink/test_config_flow.py | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index 75ad26665c3bd8..d24fd8d1f14ba6 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -17,7 +17,7 @@ from homeassistant.helpers.device_registry import format_mac from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DOMAIN -from .exceptions import ReolinkException, UserNotAdmin +from .exceptions import ReolinkException, ReolinkWebhookException, UserNotAdmin from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) @@ -133,6 +133,12 @@ async def async_step_user( except ApiError as err: placeholders["error"] = str(err) errors[CONF_HOST] = "api_error" + except ReolinkWebhookException as err: + placeholders["error"] = str(err) + placeholders[ + "more_info" + ] = "https://www.home-assistant.io/more-info/no-url-available/#configuring-the-instance-url" + errors["base"] = "webhook_exception" except (ReolinkError, ReolinkException) as err: placeholders["error"] = str(err) errors[CONF_HOST] = "cannot_connect" diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 7d8c3a213eb08e..2389c433b20684 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -22,7 +22,8 @@ "cannot_connect": "Failed to connect, check the IP address of the camera", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "not_admin": "User needs to be admin, user ''{username}'' has authorisation level ''{userlevel}''", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "webhook_exception": "Home Assistant URL is not available, go to Settings > System > Network > Home Assistant URL and correct the URLs, see {more_info}" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index 7d25fd62811afc..b6e48cab7b2fff 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -9,6 +9,7 @@ from homeassistant.components import dhcp from homeassistant.components.reolink import const from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL +from homeassistant.components.reolink.exceptions import ReolinkWebhookException from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import format_mac @@ -109,6 +110,20 @@ async def test_config_flow_errors( assert result["step_id"] == "user" assert result["errors"] == {CONF_HOST: "cannot_connect"} + reolink_connect.get_host_data.side_effect = ReolinkWebhookException("Test error") + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_HOST: TEST_HOST, + }, + ) + + assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "webhook_exception"} + reolink_connect.get_host_data.side_effect = json.JSONDecodeError( "test_error", "test", 1 ) From 7c902d5aadf359e2e0246300cf4d7ca39c9c5978 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Mon, 24 Jul 2023 22:19:37 +0200 Subject: [PATCH 0883/1009] Bumb python-homewizard-energy to 2.0.2 (#97169) --- homeassistant/components/homewizard/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index b1bbd8d0945aed..1ca833c6a744b1 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_polling", "loggers": ["homewizard_energy"], "quality_scale": "platinum", - "requirements": ["python-homewizard-energy==2.0.1"], + "requirements": ["python-homewizard-energy==2.0.2"], "zeroconf": ["_hwenergy._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index ba62e1d6f0a836..f601fd7b88f847 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2090,7 +2090,7 @@ python-gc100==1.0.3a0 python-gitlab==1.6.0 # homeassistant.components.homewizard -python-homewizard-energy==2.0.1 +python-homewizard-energy==2.0.2 # homeassistant.components.hp_ilo python-hpilo==4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8e0df2ef32e3a..4b9be3ca91b637 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1537,7 +1537,7 @@ python-ecobee-api==0.2.14 python-fullykiosk==0.0.12 # homeassistant.components.homewizard -python-homewizard-energy==2.0.1 +python-homewizard-energy==2.0.2 # homeassistant.components.izone python-izone==1.2.9 From 9f9602e8a7cb5510ce7869af19f0bcbf3a5db31f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 24 Jul 2023 21:07:57 +0000 Subject: [PATCH 0884/1009] Add frequency sensor for Shelly Plus/Pro xPM devices (#97172) --- homeassistant/components/shelly/sensor.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 8c98eb6473c3fe..b52e176b521483 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -661,6 +661,16 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, ), + "freq": RpcSensorDescription( + key="switch", + sub_key="freq", + name="Frequency", + native_unit_of_measurement=UnitOfFrequency.HERTZ, + suggested_display_precision=0, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), "freq_pm1": RpcSensorDescription( key="pm1", sub_key="freq", From d1e96a356a751f49efca1bab40589e532ac30ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 24 Jul 2023 23:27:33 +0200 Subject: [PATCH 0885/1009] Add Airzone Cloud Aidoo binary sensors (#95607) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit airzone_cloud: add Aidoo binary sensors Signed-off-by: Álvaro Fernández Rojas --- .../components/airzone_cloud/binary_sensor.py | 60 ++++++++++++++++++- .../airzone_cloud/test_binary_sensor.py | 9 +++ tests/components/airzone_cloud/util.py | 1 + 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airzone_cloud/binary_sensor.py b/homeassistant/components/airzone_cloud/binary_sensor.py index 29b550463d0f82..765eec2d28854f 100644 --- a/homeassistant/components/airzone_cloud/binary_sensor.py +++ b/homeassistant/components/airzone_cloud/binary_sensor.py @@ -4,7 +4,14 @@ from dataclasses import dataclass from typing import Any, Final -from aioairzone_cloud.const import AZD_ACTIVE, AZD_PROBLEMS, AZD_WARNINGS, AZD_ZONES +from aioairzone_cloud.const import ( + AZD_ACTIVE, + AZD_AIDOOS, + AZD_ERRORS, + AZD_PROBLEMS, + AZD_WARNINGS, + AZD_ZONES, +) from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -18,7 +25,7 @@ from .const import DOMAIN from .coordinator import AirzoneUpdateCoordinator -from .entity import AirzoneEntity, AirzoneZoneEntity +from .entity import AirzoneAidooEntity, AirzoneEntity, AirzoneZoneEntity @dataclass @@ -28,6 +35,22 @@ class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription): attributes: dict[str, str] | None = None +AIDOO_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = ( + AirzoneBinarySensorEntityDescription( + device_class=BinarySensorDeviceClass.RUNNING, + key=AZD_ACTIVE, + ), + AirzoneBinarySensorEntityDescription( + attributes={ + "errors": AZD_ERRORS, + "warnings": AZD_WARNINGS, + }, + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + key=AZD_PROBLEMS, + ), +) + ZONE_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = ( AirzoneBinarySensorEntityDescription( device_class=BinarySensorDeviceClass.RUNNING, @@ -52,6 +75,18 @@ async def async_setup_entry( binary_sensors: list[AirzoneBinarySensor] = [] + for aidoo_id, aidoo_data in coordinator.data.get(AZD_AIDOOS, {}).items(): + for description in AIDOO_BINARY_SENSOR_TYPES: + if description.key in aidoo_data: + binary_sensors.append( + AirzoneAidooBinarySensor( + coordinator, + description, + aidoo_id, + aidoo_data, + ) + ) + for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items(): for description in ZONE_BINARY_SENSOR_TYPES: if description.key in zone_data: @@ -89,6 +124,27 @@ def _async_update_attrs(self) -> None: } +class AirzoneAidooBinarySensor(AirzoneAidooEntity, AirzoneBinarySensor): + """Define an Airzone Cloud Aidoo binary sensor.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + description: AirzoneBinarySensorEntityDescription, + aidoo_id: str, + aidoo_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator, aidoo_id, aidoo_data) + + self._attr_unique_id = f"{aidoo_id}_{description.key}" + self.entity_description = description + + self._async_update_attrs() + + class AirzoneZoneBinarySensor(AirzoneZoneEntity, AirzoneBinarySensor): """Define an Airzone Cloud Zone binary sensor.""" diff --git a/tests/components/airzone_cloud/test_binary_sensor.py b/tests/components/airzone_cloud/test_binary_sensor.py index 37357bf59da94a..14f7a078156e85 100644 --- a/tests/components/airzone_cloud/test_binary_sensor.py +++ b/tests/components/airzone_cloud/test_binary_sensor.py @@ -11,6 +11,15 @@ async def test_airzone_create_binary_sensors(hass: HomeAssistant) -> None: await async_init_integration(hass) + # Aidoo + state = hass.states.get("binary_sensor.bron_problem") + assert state.state == STATE_OFF + assert state.attributes.get("errors") is None + assert state.attributes.get("warnings") is None + + state = hass.states.get("binary_sensor.bron_running") + assert state.state == STATE_OFF + # Zones state = hass.states.get("binary_sensor.dormitorio_problem") assert state.state == STATE_OFF diff --git a/tests/components/airzone_cloud/util.py b/tests/components/airzone_cloud/util.py index a8cb539bb1d253..0c26755f948631 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -163,6 +163,7 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: if device.get_id() == "aidoo1": return { + API_ACTIVE: False, API_ERRORS: [], API_IS_CONNECTED: True, API_WS_CONNECTED: True, From 99e7b42127bf8570aed2e781caa851fefc4c4285 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 24 Jul 2023 16:52:16 -0500 Subject: [PATCH 0886/1009] Bump hassil and intents (#97174) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index aa2d0c32d16ee9..65b12b64e58a21 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.0.6", "home-assistant-intents==2023.6.28"] + "requirements": ["hassil==1.2.0", "home-assistant-intents==2023.7.24"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 232b2821209dad..031e64453f4e68 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,10 +20,10 @@ dbus-fast==1.87.2 fnv-hash-fast==0.4.0 ha-av==10.1.0 hass-nabucasa==0.69.0 -hassil==1.0.6 +hassil==1.2.0 home-assistant-bluetooth==1.10.1 home-assistant-frontend==20230705.1 -home-assistant-intents==2023.6.28 +home-assistant-intents==2023.7.24 httpx==0.24.1 ifaddr==0.2.0 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index f601fd7b88f847..e36f8fe52faadc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -953,7 +953,7 @@ hass-nabucasa==0.69.0 hass-splunk==0.1.1 # homeassistant.components.conversation -hassil==1.0.6 +hassil==1.2.0 # homeassistant.components.jewish_calendar hdate==0.10.4 @@ -986,7 +986,7 @@ holidays==0.28 home-assistant-frontend==20230705.1 # homeassistant.components.conversation -home-assistant-intents==2023.6.28 +home-assistant-intents==2023.7.24 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b9be3ca91b637..f2e3d281f88fb1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -748,7 +748,7 @@ habitipy==0.2.0 hass-nabucasa==0.69.0 # homeassistant.components.conversation -hassil==1.0.6 +hassil==1.2.0 # homeassistant.components.jewish_calendar hdate==0.10.4 @@ -772,7 +772,7 @@ holidays==0.28 home-assistant-frontend==20230705.1 # homeassistant.components.conversation -home-assistant-intents==2023.6.28 +home-assistant-intents==2023.7.24 # homeassistant.components.home_connect homeconnect==0.7.2 From cce9d938f65f4fe9b4d2e18568ff728f537ea042 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 25 Jul 2023 00:07:43 +0200 Subject: [PATCH 0887/1009] Make setup of Ecovacs async (#96200) * make setup async * apply suggestions --- homeassistant/components/ecovacs/__init__.py | 70 +++++++++++--------- homeassistant/components/ecovacs/vacuum.py | 13 ++-- 2 files changed, 44 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/ecovacs/__init__.py b/homeassistant/components/ecovacs/__init__.py index cd87b175cf9e92..9cb8a8c38d8330 100644 --- a/homeassistant/components/ecovacs/__init__.py +++ b/homeassistant/components/ecovacs/__init__.py @@ -46,54 +46,60 @@ ) -def setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Ecovacs component.""" _LOGGER.debug("Creating new Ecovacs component") - hass.data[ECOVACS_DEVICES] = [] + def get_devices() -> list[VacBot]: + ecovacs_api = EcoVacsAPI( + ECOVACS_API_DEVICEID, + config[DOMAIN].get(CONF_USERNAME), + EcoVacsAPI.md5(config[DOMAIN].get(CONF_PASSWORD)), + config[DOMAIN].get(CONF_COUNTRY), + config[DOMAIN].get(CONF_CONTINENT), + ) + ecovacs_devices = ecovacs_api.devices() + _LOGGER.debug("Ecobot devices: %s", ecovacs_devices) - ecovacs_api = EcoVacsAPI( - ECOVACS_API_DEVICEID, - config[DOMAIN].get(CONF_USERNAME), - EcoVacsAPI.md5(config[DOMAIN].get(CONF_PASSWORD)), - config[DOMAIN].get(CONF_COUNTRY), - config[DOMAIN].get(CONF_CONTINENT), - ) + devices: list[VacBot] = [] + for device in ecovacs_devices: + _LOGGER.info( + "Discovered Ecovacs device on account: %s with nickname %s", + device.get("did"), + device.get("nick"), + ) + vacbot = VacBot( + ecovacs_api.uid, + ecovacs_api.REALM, + ecovacs_api.resource, + ecovacs_api.user_access_token, + device, + config[DOMAIN].get(CONF_CONTINENT).lower(), + monitor=True, + ) - devices = ecovacs_api.devices() - _LOGGER.debug("Ecobot devices: %s", devices) + devices.append(vacbot) + return devices - for device in devices: - _LOGGER.info( - "Discovered Ecovacs device on account: %s with nickname %s", - device.get("did"), - device.get("nick"), - ) - vacbot = VacBot( - ecovacs_api.uid, - ecovacs_api.REALM, - ecovacs_api.resource, - ecovacs_api.user_access_token, - device, - config[DOMAIN].get(CONF_CONTINENT).lower(), - monitor=True, - ) - hass.data[ECOVACS_DEVICES].append(vacbot) + hass.data[ECOVACS_DEVICES] = await hass.async_add_executor_job(get_devices) - def stop(event: object) -> None: + async def async_stop(event: object) -> None: """Shut down open connections to Ecovacs XMPP server.""" - for device in hass.data[ECOVACS_DEVICES]: + devices: list[VacBot] = hass.data[ECOVACS_DEVICES] + for device in devices: _LOGGER.info( "Shutting down connection to Ecovacs device %s", device.vacuum.get("did"), ) - device.disconnect() + await hass.async_add_executor_job(device.disconnect) # Listen for HA stop to disconnect. - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop) if hass.data[ECOVACS_DEVICES]: _LOGGER.debug("Starting vacuum components") - discovery.load_platform(hass, Platform.VACUUM, DOMAIN, {}, config) + hass.async_create_task( + discovery.async_load_platform(hass, Platform.VACUUM, DOMAIN, {}, config) + ) return True diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index ba922a30b84fa6..2ec9a1a3e4a625 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -28,18 +28,20 @@ ATTR_COMPONENT_PREFIX = "component_" -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Ecovacs vacuums.""" vacuums = [] - for device in hass.data[ECOVACS_DEVICES]: + devices: list[sucks.VacBot] = hass.data[ECOVACS_DEVICES] + for device in devices: + await hass.async_add_executor_job(device.connect_and_wait_until_ready) vacuums.append(EcovacsVacuum(device)) _LOGGER.debug("Adding Ecovacs Vacuums to Home Assistant: %s", vacuums) - add_entities(vacuums, True) + async_add_entities(vacuums) class EcovacsVacuum(StateVacuumEntity): @@ -62,15 +64,12 @@ class EcovacsVacuum(StateVacuumEntity): def __init__(self, device: sucks.VacBot) -> None: """Initialize the Ecovacs Vacuum.""" self.device = device - self.device.connect_and_wait_until_ready() vacuum = self.device.vacuum self.error = None self._attr_unique_id = vacuum["did"] self._attr_name = vacuum.get("nick", vacuum["did"]) - _LOGGER.debug("StateVacuum initialized: %s", self.name) - async def async_added_to_hass(self) -> None: """Set up the event listeners now that hass is ready.""" self.device.statusEvents.subscribe(lambda _: self.schedule_update_ha_state()) From 6717e401149a3cd8692111d888a943935cefdb5c Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Tue, 25 Jul 2023 00:20:09 +0200 Subject: [PATCH 0888/1009] Use snapshots in devolo Home Network button tests (#95141) Use snapshots --- .../snapshots/test_button.ambr | 345 ++++++++++++++++++ .../devolo_home_network/test_button.py | 185 +++------- 2 files changed, 389 insertions(+), 141 deletions(-) create mode 100644 tests/components/devolo_home_network/snapshots/test_button.ambr diff --git a/tests/components/devolo_home_network/snapshots/test_button.ambr b/tests/components/devolo_home_network/snapshots/test_button.ambr new file mode 100644 index 00000000000000..a124ef576937cf --- /dev/null +++ b/tests/components/devolo_home_network/snapshots/test_button.ambr @@ -0,0 +1,345 @@ +# serializer version: 1 +# name: test_button[identify_device_with_a_blinking_led-async_identify_device_start] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Identify device with a blinking LED', + 'icon': 'mdi:led-on', + }), + 'context': , + 'entity_id': 'button.mock_title_identify_device_with_a_blinking_led', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_button[identify_device_with_a_blinking_led-async_identify_device_start].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.mock_title_identify_device_with_a_blinking_led', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:led-on', + 'original_name': 'Identify device with a blinking LED', + 'platform': 'devolo_home_network', + 'supported_features': 0, + 'translation_key': 'identify', + 'unique_id': '1234567890_identify', + 'unit_of_measurement': None, + }) +# --- +# name: test_button[identify_device_with_a_blinking_led-plcnet-async_identify_device_start] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Identify device with a blinking LED', + 'icon': 'mdi:led-on', + }), + 'context': , + 'entity_id': 'button.mock_title_identify_device_with_a_blinking_led', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_button[identify_device_with_a_blinking_led-plcnet-async_identify_device_start].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.mock_title_identify_device_with_a_blinking_led', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:led-on', + 'original_name': 'Identify device with a blinking LED', + 'platform': 'devolo_home_network', + 'supported_features': 0, + 'translation_key': 'identify', + 'unique_id': '1234567890_identify', + 'unit_of_measurement': None, + }) +# --- +# name: test_button[restart_device-async_restart] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'Mock Title Restart device', + }), + 'context': , + 'entity_id': 'button.mock_title_restart_device', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_button[restart_device-async_restart].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.mock_title_restart_device', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Restart device', + 'platform': 'devolo_home_network', + 'supported_features': 0, + 'translation_key': 'restart', + 'unique_id': '1234567890_restart', + 'unit_of_measurement': None, + }) +# --- +# name: test_button[restart_device-device-async_restart] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'Mock Title Restart device', + }), + 'context': , + 'entity_id': 'button.mock_title_restart_device', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_button[restart_device-device-async_restart].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.mock_title_restart_device', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Restart device', + 'platform': 'devolo_home_network', + 'supported_features': 0, + 'translation_key': 'restart', + 'unique_id': '1234567890_restart', + 'unit_of_measurement': None, + }) +# --- +# name: test_button[start_plc_pairing-async_pair_device] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Start PLC pairing', + 'icon': 'mdi:plus-network-outline', + }), + 'context': , + 'entity_id': 'button.mock_title_start_plc_pairing', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_button[start_plc_pairing-async_pair_device].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.mock_title_start_plc_pairing', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:plus-network-outline', + 'original_name': 'Start PLC pairing', + 'platform': 'devolo_home_network', + 'supported_features': 0, + 'translation_key': 'pairing', + 'unique_id': '1234567890_pairing', + 'unit_of_measurement': None, + }) +# --- +# name: test_button[start_plc_pairing-plcnet-async_pair_device] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Start PLC pairing', + 'icon': 'mdi:plus-network-outline', + }), + 'context': , + 'entity_id': 'button.mock_title_start_plc_pairing', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_button[start_plc_pairing-plcnet-async_pair_device].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.mock_title_start_plc_pairing', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:plus-network-outline', + 'original_name': 'Start PLC pairing', + 'platform': 'devolo_home_network', + 'supported_features': 0, + 'translation_key': 'pairing', + 'unique_id': '1234567890_pairing', + 'unit_of_measurement': None, + }) +# --- +# name: test_button[start_wps-async_start_wps] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Start WPS', + 'icon': 'mdi:wifi-plus', + }), + 'context': , + 'entity_id': 'button.mock_title_start_wps', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_button[start_wps-async_start_wps].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.mock_title_start_wps', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:wifi-plus', + 'original_name': 'Start WPS', + 'platform': 'devolo_home_network', + 'supported_features': 0, + 'translation_key': 'start_wps', + 'unique_id': '1234567890_start_wps', + 'unit_of_measurement': None, + }) +# --- +# name: test_button[start_wps-device-async_start_wps] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Start WPS', + 'icon': 'mdi:wifi-plus', + }), + 'context': , + 'entity_id': 'button.mock_title_start_wps', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_button[start_wps-device-async_start_wps].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.mock_title_start_wps', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:wifi-plus', + 'original_name': 'Start WPS', + 'platform': 'devolo_home_network', + 'supported_features': 0, + 'translation_key': 'start_wps', + 'unique_id': '1234567890_start_wps', + 'unit_of_measurement': None, + }) +# --- diff --git a/tests/components/devolo_home_network/test_button.py b/tests/components/devolo_home_network/test_button.py index c5681e4a278fb3..4b8521b57986cc 100644 --- a/tests/components/devolo_home_network/test_button.py +++ b/tests/components/devolo_home_network/test_button.py @@ -3,19 +3,18 @@ from devolo_plc_api.exceptions.device import DevicePasswordProtected, DeviceUnavailable import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import ( DOMAIN as PLATFORM, SERVICE_PRESS, - ButtonDeviceClass, ) from homeassistant.components.devolo_home_network.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity import EntityCategory from . import configure_integration from .mock import MockDevice @@ -40,116 +39,50 @@ async def test_button_setup(hass: HomeAssistant) -> None: await hass.config_entries.async_unload(entry.entry_id) +@pytest.mark.parametrize( + ("name", "api_name", "trigger_method"), + [ + [ + "identify_device_with_a_blinking_led", + "plcnet", + "async_identify_device_start", + ], + [ + "start_plc_pairing", + "plcnet", + "async_pair_device", + ], + [ + "restart_device", + "device", + "async_restart", + ], + [ + "start_wps", + "device", + "async_start_wps", + ], + ], +) @pytest.mark.freeze_time("2023-01-13 12:00:00+00:00") -async def test_identify_device( - hass: HomeAssistant, mock_device: MockDevice, entity_registry: er.EntityRegistry -) -> None: - """Test start PLC pairing button.""" - entry = configure_integration(hass) - device_name = entry.title.replace(" ", "_").lower() - state_key = f"{PLATFORM}.{device_name}_identify_device_with_a_blinking_led" - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_UNKNOWN - assert ( - entity_registry.async_get(state_key).entity_category - is EntityCategory.DIAGNOSTIC - ) - - # Emulate button press - await hass.services.async_call( - PLATFORM, - SERVICE_PRESS, - {ATTR_ENTITY_ID: state_key}, - blocking=True, - ) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state.state == "2023-01-13T12:00:00+00:00" - assert mock_device.plcnet.async_identify_device_start.call_count == 1 - - await hass.config_entries.async_unload(entry.entry_id) - - -@pytest.mark.freeze_time("2023-01-13 12:00:00+00:00") -async def test_start_plc_pairing(hass: HomeAssistant, mock_device: MockDevice) -> None: - """Test start PLC pairing button.""" - entry = configure_integration(hass) - device_name = entry.title.replace(" ", "_").lower() - state_key = f"{PLATFORM}.{device_name}_start_plc_pairing" - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_UNKNOWN - - # Emulate button press - await hass.services.async_call( - PLATFORM, - SERVICE_PRESS, - {ATTR_ENTITY_ID: state_key}, - blocking=True, - ) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state.state == "2023-01-13T12:00:00+00:00" - assert mock_device.plcnet.async_pair_device.call_count == 1 - - await hass.config_entries.async_unload(entry.entry_id) - - -@pytest.mark.freeze_time("2023-01-13 12:00:00+00:00") -async def test_restart( - hass: HomeAssistant, mock_device: MockDevice, entity_registry: er.EntityRegistry +async def test_button( + hass: HomeAssistant, + mock_device: MockDevice, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + name: str, + api_name: str, + trigger_method: str, ) -> None: - """Test restart button.""" - entry = configure_integration(hass) - device_name = entry.title.replace(" ", "_").lower() - state_key = f"{PLATFORM}.{device_name}_restart_device" - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_UNKNOWN - assert state.attributes["device_class"] == ButtonDeviceClass.RESTART - assert entity_registry.async_get(state_key).entity_category is EntityCategory.CONFIG - - # Emulate button press - await hass.services.async_call( - PLATFORM, - SERVICE_PRESS, - {ATTR_ENTITY_ID: state_key}, - blocking=True, - ) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state.state == "2023-01-13T12:00:00+00:00" - assert mock_device.device.async_restart.call_count == 1 - - await hass.config_entries.async_unload(entry.entry_id) - - -@pytest.mark.freeze_time("2023-01-13 12:00:00+00:00") -async def test_start_wps(hass: HomeAssistant, mock_device: MockDevice) -> None: - """Test start WPS button.""" + """Test a button.""" entry = configure_integration(hass) device_name = entry.title.replace(" ", "_").lower() - state_key = f"{PLATFORM}.{device_name}_start_wps" - + state_key = f"{PLATFORM}.{device_name}_{name}" await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_UNKNOWN + assert hass.states.get(state_key) == snapshot + assert entity_registry.async_get(state_key) == snapshot # Emulate button press await hass.services.async_call( @@ -162,42 +95,12 @@ async def test_start_wps(hass: HomeAssistant, mock_device: MockDevice) -> None: state = hass.states.get(state_key) assert state.state == "2023-01-13T12:00:00+00:00" - assert mock_device.device.async_start_wps.call_count == 1 - - await hass.config_entries.async_unload(entry.entry_id) - - -@pytest.mark.parametrize( - ("name", "trigger_method"), - [ - ["identify_device_with_a_blinking_led", "async_identify_device_start"], - ["start_plc_pairing", "async_pair_device"], - ["restart_device", "async_restart"], - ["start_wps", "async_start_wps"], - ], -) -async def test_device_failure( - hass: HomeAssistant, - mock_device: MockDevice, - name: str, - trigger_method: str, -) -> None: - """Test device failure.""" - entry = configure_integration(hass) - device_name = entry.title.replace(" ", "_").lower() - state_key = f"{PLATFORM}.{device_name}_{name}" - - setattr(mock_device.device, trigger_method, AsyncMock()) - api = getattr(mock_device.device, trigger_method) - api.side_effect = DeviceUnavailable - setattr(mock_device.plcnet, trigger_method, AsyncMock()) - api = getattr(mock_device.plcnet, trigger_method) - api.side_effect = DeviceUnavailable - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + api = getattr(mock_device, api_name) + assert getattr(api, trigger_method).call_count == 1 - # Emulate button press + # Emulate device failure + setattr(api, trigger_method, AsyncMock()) + getattr(api, trigger_method).side_effect = DeviceUnavailable with pytest.raises(HomeAssistantError): await hass.services.async_call( PLATFORM, From 5ec633a839c013655807da2ebe0cc42fd40f37de Mon Sep 17 00:00:00 2001 From: Renier Moorcroft <66512715+RenierM26@users.noreply.github.com> Date: Tue, 25 Jul 2023 00:31:44 +0200 Subject: [PATCH 0889/1009] Add Ezviz button entities (#93647) * Initial commit * Add button for ptz * coveragerc * Add ptz buttons to PTZ cameras only * Describe support capbility * Improve typing * bump api version. * Match entity naming used throughout * Add translation * Create ir before execution and breaks in version * Fix for translation missing name key. * Change depreciation to 2024.2.0 * Update camera.py * Tiny spelling tweaks --------- Co-authored-by: Franck Nijhof --- .coveragerc | 1 + homeassistant/components/ezviz/__init__.py | 1 + homeassistant/components/ezviz/button.py | 131 ++++++++++++++++++++ homeassistant/components/ezviz/camera.py | 11 ++ homeassistant/components/ezviz/strings.json | 25 ++++ 5 files changed, 169 insertions(+) create mode 100644 homeassistant/components/ezviz/button.py diff --git a/.coveragerc b/.coveragerc index db1914055226c1..1032ac2db0aa9d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -316,6 +316,7 @@ omit = homeassistant/components/ezviz/__init__.py homeassistant/components/ezviz/alarm_control_panel.py homeassistant/components/ezviz/binary_sensor.py + homeassistant/components/ezviz/button.py homeassistant/components/ezviz/camera.py homeassistant/components/ezviz/image.py homeassistant/components/ezviz/light.py diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index 59dfb7c269c832..c007de78130121 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -35,6 +35,7 @@ ATTR_TYPE_CLOUD: [ Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.CAMERA, Platform.IMAGE, Platform.LIGHT, diff --git a/homeassistant/components/ezviz/button.py b/homeassistant/components/ezviz/button.py new file mode 100644 index 00000000000000..1c04de956c6c2f --- /dev/null +++ b/homeassistant/components/ezviz/button.py @@ -0,0 +1,131 @@ +"""Support for EZVIZ button controls.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from pyezviz import EzvizClient +from pyezviz.constants import SupportExt +from pyezviz.exceptions import HTTPError, PyEzvizError + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DATA_COORDINATOR, DOMAIN +from .coordinator import EzvizDataUpdateCoordinator +from .entity import EzvizEntity + +PARALLEL_UPDATES = 1 + + +@dataclass +class EzvizButtonEntityDescriptionMixin: + """Mixin values for EZVIZ button entities.""" + + method: Callable[[EzvizClient, str, str], Any] + supported_ext: str + + +@dataclass +class EzvizButtonEntityDescription( + ButtonEntityDescription, EzvizButtonEntityDescriptionMixin +): + """Describe a EZVIZ Button.""" + + +BUTTON_ENTITIES = ( + EzvizButtonEntityDescription( + key="ptz_up", + translation_key="ptz_up", + icon="mdi:pan", + method=lambda pyezviz_client, serial, run: pyezviz_client.ptz_control( + "UP", serial, run + ), + supported_ext=str(SupportExt.SupportPtz.value), + ), + EzvizButtonEntityDescription( + key="ptz_down", + translation_key="ptz_down", + icon="mdi:pan", + method=lambda pyezviz_client, serial, run: pyezviz_client.ptz_control( + "DOWN", serial, run + ), + supported_ext=str(SupportExt.SupportPtz.value), + ), + EzvizButtonEntityDescription( + key="ptz_left", + translation_key="ptz_left", + icon="mdi:pan", + method=lambda pyezviz_client, serial, run: pyezviz_client.ptz_control( + "LEFT", serial, run + ), + supported_ext=str(SupportExt.SupportPtz.value), + ), + EzvizButtonEntityDescription( + key="ptz_right", + translation_key="ptz_right", + icon="mdi:pan", + method=lambda pyezviz_client, serial, run: pyezviz_client.ptz_control( + "RIGHT", serial, run + ), + supported_ext=str(SupportExt.SupportPtz.value), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up EZVIZ button based on a config entry.""" + coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR + ] + + # Add button entities if supportExt value indicates PTZ capbility. + # Could be missing or "0" for unsupported. + # If present with value of "1" then add button entity. + + async_add_entities( + EzvizButtonEntity(coordinator, camera, entity_description) + for camera in coordinator.data + for capibility, value in coordinator.data[camera]["supportExt"].items() + for entity_description in BUTTON_ENTITIES + if capibility == entity_description.supported_ext + if value == "1" + ) + + +class EzvizButtonEntity(EzvizEntity, ButtonEntity): + """Representation of a EZVIZ button entity.""" + + entity_description: EzvizButtonEntityDescription + _attr_has_entity_name = True + + def __init__( + self, + coordinator: EzvizDataUpdateCoordinator, + serial: str, + description: EzvizButtonEntityDescription, + ) -> None: + """Initialize the button.""" + super().__init__(coordinator, serial) + self._attr_unique_id = f"{serial}_{description.key}" + self.entity_description = description + + def press(self) -> None: + """Execute the button action.""" + try: + self.entity_description.method( + self.coordinator.ezviz_client, self._serial, "START" + ) + self.entity_description.method( + self.coordinator.ezviz_client, self._serial, "STOP" + ) + except (HTTPError, PyEzvizError) as err: + raise HomeAssistantError( + f"Cannot perform PTZ action on {self.name}" + ) from err diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 01e8425c13bbdc..7f03aef1d97b78 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -263,6 +263,17 @@ async def stream_source(self) -> str | None: def perform_ptz(self, direction: str, speed: int) -> None: """Perform a PTZ action on the camera.""" + ir.async_create_issue( + self.hass, + DOMAIN, + "service_depreciation_ptz", + breaks_in_ha_version="2024.2.0", + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="service_depreciation_ptz", + ) + try: self.coordinator.ezviz_client.ptz_control( str(direction).upper(), self._serial, "START", speed diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index 0245edc0e3eb0c..d60c4816d24f88 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -81,6 +81,17 @@ } } } + }, + "service_depreciation_ptz": { + "title": "EZVIZ PTZ service is being removed", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::ezviz::issues::service_depreciation_ptz::title%]", + "description": "EZVIZ PTZ service is deprecated and will be removed.\nTo move the camera, you can instead use the `button.press` service targetting the PTZ* entities.\n\nPlease remove the use of this service from your automations and scripts and select **submit** to close this issue." + } + } + } } }, "entity": { @@ -99,6 +110,20 @@ "name": "Last motion image" } }, + "button": { + "ptz_up": { + "name": "PTZ up" + }, + "ptz_down": { + "name": "PTZ down" + }, + "ptz_left": { + "name": "PTZ left" + }, + "ptz_right": { + "name": "PTZ right" + } + }, "binary_sensor": { "alarm_schedules_enabled": { "name": "Alarm schedules enabled" From c312dcbc4b8b9a69ea52398a72db995fb0526587 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 25 Jul 2023 00:54:19 +0200 Subject: [PATCH 0890/1009] Scrape refactor to ManualTriggerEntity (#96329) --- homeassistant/components/scrape/__init__.py | 6 +- homeassistant/components/scrape/sensor.py | 94 +++++++++++++++------ tests/components/scrape/test_sensor.py | 93 +++++++++++++++++++- 3 files changed, 163 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/scrape/__init__.py b/homeassistant/components/scrape/__init__.py index 8953d9facd0714..bf2ccb16b03198 100644 --- a/homeassistant/components/scrape/__init__.py +++ b/homeassistant/components/scrape/__init__.py @@ -20,7 +20,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.template_entity import TEMPLATE_SENSOR_BASE_SCHEMA +from homeassistant.helpers.template_entity import ( + CONF_AVAILABILITY, + TEMPLATE_SENSOR_BASE_SCHEMA, +) from homeassistant.helpers.typing import ConfigType from .const import CONF_INDEX, CONF_SELECT, DEFAULT_SCAN_INTERVAL, DOMAIN, PLATFORMS @@ -29,6 +32,7 @@ SENSOR_SCHEMA = vol.Schema( { **TEMPLATE_SENSOR_BASE_SCHEMA.schema, + vol.Optional(CONF_AVAILABILITY): cv.template, vol.Optional(CONF_ATTRIBUTE): cv.string, vol.Optional(CONF_INDEX, default=0): cv.positive_int, vol.Required(CONF_SELECT): cv.string, diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 5ddd6c48e433af..a68083856f72b2 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -6,13 +6,20 @@ import voluptuous as vol -from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.sensor import ( + CONF_STATE_CLASS, + SensorDeviceClass, + SensorEntity, +) from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ATTRIBUTE, + CONF_DEVICE_CLASS, + CONF_ICON, CONF_NAME, CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant, callback @@ -20,8 +27,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template from homeassistant.helpers.template_entity import ( + CONF_AVAILABILITY, + CONF_PICTURE, TEMPLATE_SENSOR_BASE_SCHEMA, - TemplateSensor, + ManualTriggerEntity, ) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -53,17 +62,30 @@ async def async_setup_platform( if value_template is not None: value_template.hass = hass + trigger_entity_config = { + CONF_NAME: sensor_config[CONF_NAME], + CONF_DEVICE_CLASS: sensor_config.get(CONF_DEVICE_CLASS), + CONF_UNIQUE_ID: sensor_config.get(CONF_UNIQUE_ID), + } + if available := sensor_config.get(CONF_AVAILABILITY): + trigger_entity_config[CONF_AVAILABILITY] = available + if icon := sensor_config.get(CONF_ICON): + trigger_entity_config[CONF_ICON] = icon + if picture := sensor_config.get(CONF_PICTURE): + trigger_entity_config[CONF_PICTURE] = picture + entities.append( ScrapeSensor( hass, coordinator, - sensor_config, - sensor_config[CONF_NAME], - sensor_config.get(CONF_UNIQUE_ID), + trigger_entity_config, + sensor_config.get(CONF_UNIT_OF_MEASUREMENT), + sensor_config.get(CONF_STATE_CLASS), sensor_config[CONF_SELECT], sensor_config.get(CONF_ATTRIBUTE), sensor_config[CONF_INDEX], value_template, + True, ) ) @@ -84,60 +106,65 @@ async def async_setup_entry( )(sensor) name: str = sensor_config[CONF_NAME] - select: str = sensor_config[CONF_SELECT] - attr: str | None = sensor_config.get(CONF_ATTRIBUTE) - index: int = int(sensor_config[CONF_INDEX]) value_string: str | None = sensor_config.get(CONF_VALUE_TEMPLATE) - unique_id: str = sensor_config[CONF_UNIQUE_ID] value_template: Template | None = ( Template(value_string, hass) if value_string is not None else None ) + + trigger_entity_config = { + CONF_NAME: name, + CONF_DEVICE_CLASS: sensor_config.get(CONF_DEVICE_CLASS), + CONF_UNIQUE_ID: sensor_config[CONF_UNIQUE_ID], + } + entities.append( ScrapeSensor( hass, coordinator, - sensor_config, - name, - unique_id, - select, - attr, - index, + trigger_entity_config, + sensor_config.get(CONF_UNIT_OF_MEASUREMENT), + sensor_config.get(CONF_STATE_CLASS), + sensor_config[CONF_SELECT], + sensor_config.get(CONF_ATTRIBUTE), + sensor_config[CONF_INDEX], value_template, + False, ) ) async_add_entities(entities) -class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], TemplateSensor): +class ScrapeSensor( + CoordinatorEntity[ScrapeCoordinator], ManualTriggerEntity, SensorEntity +): """Representation of a web scrape sensor.""" def __init__( self, hass: HomeAssistant, coordinator: ScrapeCoordinator, - config: ConfigType, - name: str, - unique_id: str | None, + trigger_entity_config: ConfigType, + unit_of_measurement: str | None, + state_class: str | None, select: str, attr: str | None, index: int, value_template: Template | None, + yaml: bool, ) -> None: """Initialize a web scrape sensor.""" CoordinatorEntity.__init__(self, coordinator) - TemplateSensor.__init__( - self, - hass, - config=config, - fallback_name=name, - unique_id=unique_id, - ) + ManualTriggerEntity.__init__(self, hass, trigger_entity_config) + self._attr_name = trigger_entity_config[CONF_NAME].template + self._attr_native_unit_of_measurement = unit_of_measurement + self._attr_state_class = state_class self._select = select self._attr = attr self._index = index self._value_template = value_template + self._attr_native_value = None def _extract_value(self) -> Any: """Parse the html extraction in the executor.""" @@ -164,12 +191,15 @@ def _extract_value(self) -> Any: async def async_added_to_hass(self) -> None: """Ensure the data from the initial update is reflected in the state.""" - await super().async_added_to_hass() + await ManualTriggerEntity.async_added_to_hass(self) + # https://github.com/python/mypy/issues/15097 + await CoordinatorEntity.async_added_to_hass(self) # type: ignore[arg-type] self._async_update_from_rest_data() def _async_update_from_rest_data(self) -> None: """Update state from the rest data.""" value = self._extract_value() + raw_value = value if (template := self._value_template) is not None: value = template.async_render_with_possible_json_value(value, None) @@ -179,11 +209,21 @@ def _async_update_from_rest_data(self) -> None: SensorDeviceClass.TIMESTAMP, }: self._attr_native_value = value + self._process_manual_data(raw_value) return self._attr_native_value = async_parse_date_datetime( value, self.entity_id, self.device_class ) + self._process_manual_data(raw_value) + self.async_write_ha_state() + + @property + def available(self) -> bool: + """Return if entity is available.""" + available1 = CoordinatorEntity.available.fget(self) # type: ignore[attr-defined] + available2 = ManualTriggerEntity.available.fget(self) # type: ignore[attr-defined] + return bool(available1 and available2) @callback def _handle_coordinator_update(self) -> None: diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index 44c264520d6f84..60cde48e5bfba8 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -1,12 +1,16 @@ """The tests for the Scrape sensor platform.""" from __future__ import annotations -from datetime import datetime +from datetime import datetime, timedelta from unittest.mock import patch import pytest -from homeassistant.components.scrape.const import DEFAULT_SCAN_INTERVAL +from homeassistant.components.scrape.const import ( + CONF_INDEX, + CONF_SELECT, + DEFAULT_SCAN_INTERVAL, +) from homeassistant.components.sensor import ( CONF_STATE_CLASS, SensorDeviceClass, @@ -14,6 +18,9 @@ ) from homeassistant.const import ( CONF_DEVICE_CLASS, + CONF_ICON, + CONF_NAME, + CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -21,7 +28,9 @@ ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.template_entity import CONF_AVAILABILITY, CONF_PICTURE from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from . import MockRestData, return_integration_config @@ -469,3 +478,83 @@ async def test_setup_config_entry( entity = entity_reg.async_get("sensor.current_version") assert entity.unique_id == "3699ef88-69e6-11ed-a1eb-0242ac120002" + + +async def test_templates_with_yaml(hass: HomeAssistant) -> None: + """Test the Scrape sensor from yaml config with templates.""" + + hass.states.async_set("sensor.input1", "on") + hass.states.async_set("sensor.input2", "on") + await hass.async_block_till_done() + + config = { + DOMAIN: [ + return_integration_config( + sensors=[ + { + CONF_NAME: "Get values with template", + CONF_SELECT: ".current-version h1", + CONF_INDEX: 0, + CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002", + CONF_ICON: '{% if states("sensor.input1")=="on" %} mdi:on {% else %} mdi:off {% endif %}', + CONF_PICTURE: '{% if states("sensor.input1")=="on" %} /local/picture1.jpg {% else %} /local/picture2.jpg {% endif %}', + CONF_AVAILABILITY: '{{ states("sensor.input2")=="on" }}', + } + ] + ) + ] + } + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.rest.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.get_values_with_template") + assert state.state == "Current Version: 2021.12.10" + assert state.attributes[CONF_ICON] == "mdi:on" + assert state.attributes["entity_picture"] == "/local/picture1.jpg" + + hass.states.async_set("sensor.input1", "off") + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(minutes=10), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.get_values_with_template") + assert state.state == "Current Version: 2021.12.10" + assert state.attributes[CONF_ICON] == "mdi:off" + assert state.attributes["entity_picture"] == "/local/picture2.jpg" + + hass.states.async_set("sensor.input2", "off") + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(minutes=20), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.get_values_with_template") + assert state.state == STATE_UNAVAILABLE + + hass.states.async_set("sensor.input1", "on") + hass.states.async_set("sensor.input2", "on") + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(minutes=30), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.get_values_with_template") + assert state.state == "Current Version: 2021.12.10" + assert state.attributes[CONF_ICON] == "mdi:on" + assert state.attributes["entity_picture"] == "/local/picture1.jpg" From 945fffebcc5a337a80ca00af8e18201b7bca3320 Mon Sep 17 00:00:00 2001 From: Mike Woudenberg Date: Tue, 25 Jul 2023 08:27:18 +0200 Subject: [PATCH 0891/1009] Use get_url to get Home Assistant instance for Loqed webhook (#95761) --- homeassistant/components/loqed/const.py | 1 + homeassistant/components/loqed/coordinator.py | 35 ++++++++++-- homeassistant/components/loqed/manifest.json | 1 + homeassistant/components/loqed/strings.json | 2 +- tests/components/loqed/conftest.py | 29 +++++++++- .../loqed/fixtures/get_all_webhooks.json | 2 +- tests/components/loqed/test_init.py | 56 ++++++++++++++++++- 7 files changed, 112 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/loqed/const.py b/homeassistant/components/loqed/const.py index 6b1c0311a2d049..59011f26566e36 100644 --- a/homeassistant/components/loqed/const.py +++ b/homeassistant/components/loqed/const.py @@ -2,3 +2,4 @@ DOMAIN = "loqed" +CONF_CLOUDHOOK_URL = "cloudhook_url" diff --git a/homeassistant/components/loqed/coordinator.py b/homeassistant/components/loqed/coordinator.py index 507debc02ab9ba..42e0d523abafed 100644 --- a/homeassistant/components/loqed/coordinator.py +++ b/homeassistant/components/loqed/coordinator.py @@ -6,13 +6,13 @@ import async_timeout from loqedAPI import loqed -from homeassistant.components import webhook +from homeassistant.components import cloud, webhook from homeassistant.config_entries import ConfigEntry 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 +from .const import CONF_CLOUDHOOK_URL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -114,7 +114,14 @@ async def ensure_webhooks(self) -> None: webhook.async_register( self.hass, DOMAIN, "Loqed", webhook_id, self._handle_webhook ) - webhook_url = webhook.async_generate_url(self.hass, webhook_id) + + if cloud.async_active_subscription(self.hass): + webhook_url = await async_cloudhook_generate_url(self.hass, self._entry) + else: + webhook_url = webhook.async_generate_url( + self.hass, self._entry.data[CONF_WEBHOOK_ID] + ) + _LOGGER.debug("Webhook URL: %s", webhook_url) webhooks = await self.lock.getWebhooks() @@ -128,18 +135,22 @@ async def ensure_webhooks(self) -> None: webhooks = await self.lock.getWebhooks() webhook_index = next(x["id"] for x in webhooks if x["url"] == webhook_url) - _LOGGER.info("Webhook got index %s", webhook_index) + _LOGGER.debug("Webhook got index %s", webhook_index) async def remove_webhooks(self) -> None: """Remove webhook from LOQED bridge.""" webhook_id = self._entry.data[CONF_WEBHOOK_ID] - webhook_url = webhook.async_generate_url(self.hass, webhook_id) + + if CONF_CLOUDHOOK_URL in self._entry.data: + webhook_url = self._entry.data[CONF_CLOUDHOOK_URL] + else: + webhook_url = webhook.async_generate_url(self.hass, webhook_id) webhook.async_unregister( self.hass, webhook_id, ) - _LOGGER.info("Webhook URL: %s", webhook_url) + _LOGGER.debug("Webhook URL: %s", webhook_url) webhooks = await self.lock.getWebhooks() @@ -149,3 +160,15 @@ async def remove_webhooks(self) -> None: if webhook_index: await self.lock.deleteWebhook(webhook_index) + + +async def async_cloudhook_generate_url(hass: HomeAssistant, entry: ConfigEntry) -> str: + """Generate the full URL for a webhook_id.""" + if CONF_CLOUDHOOK_URL not in entry.data: + webhook_url = await cloud.async_create_cloudhook( + hass, entry.data[CONF_WEBHOOK_ID] + ) + data = {**entry.data, CONF_CLOUDHOOK_URL: webhook_url} + hass.config_entries.async_update_entry(entry, data=data) + return webhook_url + return str(entry.data[CONF_CLOUDHOOK_URL]) diff --git a/homeassistant/components/loqed/manifest.json b/homeassistant/components/loqed/manifest.json index 1000d8f804d643..25d1f15486d9d2 100644 --- a/homeassistant/components/loqed/manifest.json +++ b/homeassistant/components/loqed/manifest.json @@ -1,6 +1,7 @@ { "domain": "loqed", "name": "LOQED Touch Smart Lock", + "after_dependencies": ["cloud"], "codeowners": ["@mikewoudenberg"], "config_flow": true, "dependencies": ["webhook"], diff --git a/homeassistant/components/loqed/strings.json b/homeassistant/components/loqed/strings.json index 3d31194f5a6ed4..59b91fea1959fa 100644 --- a/homeassistant/components/loqed/strings.json +++ b/homeassistant/components/loqed/strings.json @@ -6,7 +6,7 @@ "description": "Login at {config_url} and: \n* Create an API-key by clicking 'Create' \n* Copy the created access token.", "data": { "name": "Name of your lock in the LOQED app.", - "api_key": "[%key:common::config_flow::data::api_key%]" + "api_token": "[%key:common::config_flow::data::api_token%]" } } }, diff --git a/tests/components/loqed/conftest.py b/tests/components/loqed/conftest.py index be57237afdc784..616c0cb05528ab 100644 --- a/tests/components/loqed/conftest.py +++ b/tests/components/loqed/conftest.py @@ -9,6 +9,7 @@ import pytest from homeassistant.components.loqed import DOMAIN +from homeassistant.components.loqed.const import CONF_CLOUDHOOK_URL from homeassistant.const import CONF_API_TOKEN, CONF_NAME, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -39,6 +40,31 @@ def config_entry_fixture() -> MockConfigEntry: ) +@pytest.fixture(name="cloud_config_entry") +def cloud_config_entry_fixture() -> MockConfigEntry: + """Mock config entry.""" + + config = load_fixture("loqed/integration_config.json") + webhooks_fixture = json.loads(load_fixture("loqed/get_all_webhooks.json")) + json_config = json.loads(config) + return MockConfigEntry( + version=1, + domain=DOMAIN, + data={ + "id": "Foo", + "bridge_ip": json_config["bridge_ip"], + "bridge_mdns_hostname": json_config["bridge_mdns_hostname"], + "bridge_key": json_config["bridge_key"], + "lock_key_local_id": int(json_config["lock_key_local_id"]), + "lock_key_key": json_config["lock_key_key"], + CONF_WEBHOOK_ID: "Webhook_id", + CONF_API_TOKEN: "Token", + CONF_NAME: "Home", + CONF_CLOUDHOOK_URL: webhooks_fixture[0]["url"], + }, + ) + + @pytest.fixture(name="lock") def lock_fixture() -> loqed.Lock: """Set up a mock implementation of a Lock.""" @@ -64,9 +90,6 @@ async def integration_fixture( with patch("loqedAPI.loqed.LoqedAPI.async_get_lock", return_value=lock), patch( "loqedAPI.loqed.LoqedAPI.async_get_lock_details", return_value=lock_status - ), patch( - "homeassistant.components.webhook.async_generate_url", - return_value="http://hook_id", ): await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/loqed/fixtures/get_all_webhooks.json b/tests/components/loqed/fixtures/get_all_webhooks.json index cf53fcf56a9241..e42c39b60f59f2 100644 --- a/tests/components/loqed/fixtures/get_all_webhooks.json +++ b/tests/components/loqed/fixtures/get_all_webhooks.json @@ -1,7 +1,7 @@ [ { "id": 1, - "url": "http://hook_id", + "url": "http://10.10.10.10:8123/api/webhook/Webhook_id", "trigger_state_changed_open": 1, "trigger_state_changed_latch": 1, "trigger_state_changed_night_lock": 1, diff --git a/tests/components/loqed/test_init.py b/tests/components/loqed/test_init.py index 960ad9def6b97e..057061f5915982 100644 --- a/tests/components/loqed/test_init.py +++ b/tests/components/loqed/test_init.py @@ -10,6 +10,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant +from homeassistant.helpers.network import get_url from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture @@ -50,14 +51,63 @@ async def test_setup_webhook_in_bridge( with patch("loqedAPI.loqed.LoqedAPI.async_get_lock", return_value=lock), patch( "loqedAPI.loqed.LoqedAPI.async_get_lock_details", return_value=lock_status + ): + await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + lock.registerWebhook.assert_called_with(f"{get_url(hass)}/api/webhook/Webhook_id") + + +async def test_setup_cloudhook_in_bridge( + hass: HomeAssistant, config_entry: MockConfigEntry, lock: loqed.Lock +): + """Test webhook setup in loqed bridge.""" + config: dict[str, Any] = {DOMAIN: {}} + config_entry.add_to_hass(hass) + + lock_status = json.loads(load_fixture("loqed/status_ok.json")) + webhooks_fixture = json.loads(load_fixture("loqed/get_all_webhooks.json")) + lock.getWebhooks = AsyncMock(side_effect=[[], webhooks_fixture]) + + with patch("loqedAPI.loqed.LoqedAPI.async_get_lock", return_value=lock), patch( + "loqedAPI.loqed.LoqedAPI.async_get_lock_details", return_value=lock_status + ), patch( + "homeassistant.components.cloud.async_active_subscription", return_value=True + ), patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value=webhooks_fixture[0]["url"], + ): + await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + lock.registerWebhook.assert_called_with(f"{get_url(hass)}/api/webhook/Webhook_id") + + +async def test_setup_cloudhook_from_entry_in_bridge( + hass: HomeAssistant, cloud_config_entry: MockConfigEntry, lock: loqed.Lock +): + """Test webhook setup in loqed bridge.""" + webhooks_fixture = json.loads(load_fixture("loqed/get_all_webhooks.json")) + + config: dict[str, Any] = {DOMAIN: {}} + cloud_config_entry.add_to_hass(hass) + + lock_status = json.loads(load_fixture("loqed/status_ok.json")) + + lock.getWebhooks = AsyncMock(side_effect=[[], webhooks_fixture]) + + with patch("loqedAPI.loqed.LoqedAPI.async_get_lock", return_value=lock), patch( + "loqedAPI.loqed.LoqedAPI.async_get_lock_details", return_value=lock_status + ), patch( + "homeassistant.components.cloud.async_active_subscription", return_value=True ), patch( - "homeassistant.components.webhook.async_generate_url", - return_value="http://hook_id", + "homeassistant.components.cloud.async_create_cloudhook", + return_value=webhooks_fixture[0]["url"], ): await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() - lock.registerWebhook.assert_called_with("http://hook_id") + lock.registerWebhook.assert_called_with(f"{get_url(hass)}/api/webhook/Webhook_id") async def test_unload_entry(hass, integration: MockConfigEntry, lock: loqed.Lock): From 3bbbd8642fc8b30fcced282a2147a015e3940e7a Mon Sep 17 00:00:00 2001 From: Matrix Date: Tue, 25 Jul 2023 14:30:16 +0800 Subject: [PATCH 0892/1009] Add yolink finger support (#96944) --- homeassistant/components/yolink/__init__.py | 14 ++++++++++- .../components/yolink/coordinator.py | 25 ++++++++++++++++--- homeassistant/components/yolink/cover.py | 13 +++++++--- homeassistant/components/yolink/manifest.json | 2 +- homeassistant/components/yolink/sensor.py | 4 +++ homeassistant/components/yolink/switch.py | 13 +++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 61 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index c10cc8158eae88..c3633800685195 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -121,8 +121,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from err device_coordinators = {} + + # revese mapping + device_pairing_mapping = {} + for device in yolink_home.get_devices(): + if (parent_id := device.get_paired_device_id()) is not None: + device_pairing_mapping[parent_id] = device.device_id + for device in yolink_home.get_devices(): - device_coordinator = YoLinkCoordinator(hass, device) + paried_device: YoLinkDevice | None = None + if ( + paried_device_id := device_pairing_mapping.get(device.device_id) + ) is not None: + paried_device = yolink_home.get_device(paried_device_id) + device_coordinator = YoLinkCoordinator(hass, device, paried_device) try: await device_coordinator.async_config_entry_first_refresh() except ConfigEntryNotReady: diff --git a/homeassistant/components/yolink/coordinator.py b/homeassistant/components/yolink/coordinator.py index f22e416511b6e5..e322961d179b34 100644 --- a/homeassistant/components/yolink/coordinator.py +++ b/homeassistant/components/yolink/coordinator.py @@ -20,7 +20,12 @@ class YoLinkCoordinator(DataUpdateCoordinator[dict]): """YoLink DataUpdateCoordinator.""" - def __init__(self, hass: HomeAssistant, device: YoLinkDevice) -> None: + def __init__( + self, + hass: HomeAssistant, + device: YoLinkDevice, + paired_device: YoLinkDevice | None = None, + ) -> None: """Init YoLink DataUpdateCoordinator. fetch state every 30 minutes base on yolink device heartbeat interval @@ -31,16 +36,30 @@ def __init__(self, hass: HomeAssistant, device: YoLinkDevice) -> None: hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=30) ) self.device = device + self.paired_device = paired_device async def _async_update_data(self) -> dict: """Fetch device state.""" try: async with async_timeout.timeout(10): device_state_resp = await self.device.fetch_state() + device_state = device_state_resp.data.get(ATTR_DEVICE_STATE) + if self.paired_device is not None and device_state is not None: + paried_device_state_resp = await self.paired_device.fetch_state() + paried_device_state = paried_device_state_resp.data.get( + ATTR_DEVICE_STATE + ) + if ( + paried_device_state is not None + and ATTR_DEVICE_STATE in paried_device_state + ): + device_state[ATTR_DEVICE_STATE] = paried_device_state[ + ATTR_DEVICE_STATE + ] except YoLinkAuthFailError as yl_auth_err: raise ConfigEntryAuthFailed from yl_auth_err except YoLinkClientError as yl_client_err: raise UpdateFailed from yl_client_err - if ATTR_DEVICE_STATE in device_state_resp.data: - return device_state_resp.data[ATTR_DEVICE_STATE] + if device_state is not None: + return device_state return {} diff --git a/homeassistant/components/yolink/cover.py b/homeassistant/components/yolink/cover.py index 0d1f1e590b468c..6cc1ea3acd61ea 100644 --- a/homeassistant/components/yolink/cover.py +++ b/homeassistant/components/yolink/cover.py @@ -4,7 +4,7 @@ from typing import Any from yolink.client_request import ClientRequest -from yolink.const import ATTR_GARAGE_DOOR_CONTROLLER +from yolink.const import ATTR_DEVICE_FINGER, ATTR_GARAGE_DOOR_CONTROLLER from homeassistant.components.cover import ( CoverDeviceClass, @@ -30,7 +30,8 @@ async def async_setup_entry( entities = [ YoLinkCoverEntity(config_entry, device_coordinator) for device_coordinator in device_coordinators.values() - if device_coordinator.device.device_type == ATTR_GARAGE_DOOR_CONTROLLER + if device_coordinator.device.device_type + in [ATTR_GARAGE_DOOR_CONTROLLER, ATTR_DEVICE_FINGER] ] async_add_entities(entities) @@ -58,8 +59,12 @@ def update_entity_state(self, state: dict[str, Any]) -> None: """Update HA Entity State.""" if (state_val := state.get("state")) is None: return - self._attr_is_closed = state_val == "closed" - self.async_write_ha_state() + if self.coordinator.paired_device is None: + self._attr_is_closed = None + self.async_write_ha_state() + elif state_val in ["open", "closed"]: + self._attr_is_closed = state_val == "closed" + self.async_write_ha_state() async def toggle_garage_state(self) -> None: """Toggle Garage door state.""" diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index 088ddd114f8fa7..ced0d527c7dbde 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["auth", "application_credentials"], "documentation": "https://www.home-assistant.io/integrations/yolink", "iot_class": "cloud_push", - "requirements": ["yolink-api==0.2.9"] + "requirements": ["yolink-api==0.3.0"] } diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 149bdc0adf89df..e4d0aa38fbee89 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -8,6 +8,7 @@ ATTR_DEVICE_CO_SMOKE_SENSOR, ATTR_DEVICE_DIMMER, ATTR_DEVICE_DOOR_SENSOR, + ATTR_DEVICE_FINGER, ATTR_DEVICE_LEAK_SENSOR, ATTR_DEVICE_LOCK, ATTR_DEVICE_MANIPULATOR, @@ -67,6 +68,7 @@ class YoLinkSensorEntityDescription( SENSOR_DEVICE_TYPE = [ ATTR_DEVICE_DIMMER, ATTR_DEVICE_DOOR_SENSOR, + ATTR_DEVICE_FINGER, ATTR_DEVICE_LEAK_SENSOR, ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_MULTI_OUTLET, @@ -86,6 +88,7 @@ class YoLinkSensorEntityDescription( BATTERY_POWER_SENSOR = [ ATTR_DEVICE_DOOR_SENSOR, + ATTR_DEVICE_FINGER, ATTR_DEVICE_LEAK_SENSOR, ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_POWER_FAILURE_ALARM, @@ -129,6 +132,7 @@ def cvt_volume(val: int | None) -> str | None: state_class=SensorStateClass.MEASUREMENT, value=cvt_battery, exists_fn=lambda device: device.device_type in BATTERY_POWER_SENSOR, + should_update_entity=lambda value: value is not None, ), YoLinkSensorEntityDescription( key="humidity", diff --git a/homeassistant/components/yolink/switch.py b/homeassistant/components/yolink/switch.py index 415c1e9584de41..018fcb84988cca 100644 --- a/homeassistant/components/yolink/switch.py +++ b/homeassistant/components/yolink/switch.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from typing import Any +from yolink.client_request import ClientRequest from yolink.const import ( ATTR_DEVICE_MANIPULATOR, ATTR_DEVICE_MULTI_OUTLET, @@ -160,11 +161,17 @@ def update_entity_state(self, state: dict[str, str | list[str]]) -> None: async def call_state_change(self, state: str) -> None: """Call setState api to change switch state.""" - await self.call_device( - OutletRequestBuilder.set_state_request( + client_request: ClientRequest = None + if self.coordinator.device.device_type in [ + ATTR_DEVICE_OUTLET, + ATTR_DEVICE_MULTI_OUTLET, + ]: + client_request = OutletRequestBuilder.set_state_request( state, self.entity_description.plug_index ) - ) + else: + client_request = ClientRequest("setState", {"state": state}) + await self.call_device(client_request) self._attr_is_on = self._get_state(state, self.entity_description.plug_index) self.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index e36f8fe52faadc..0e2b4467c8b1f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2723,7 +2723,7 @@ yeelight==0.7.12 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.2.9 +yolink-api==0.3.0 # homeassistant.components.youless youless-api==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f2e3d281f88fb1..34faeba0e2aaba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1999,7 +1999,7 @@ yalexs==1.5.1 yeelight==0.7.12 # homeassistant.components.yolink -yolink-api==0.2.9 +yolink-api==0.3.0 # homeassistant.components.youless youless-api==1.0.1 From 024d646526741106a156bb46c4414b0ce111515e Mon Sep 17 00:00:00 2001 From: Meow Date: Tue, 25 Jul 2023 08:33:56 +0200 Subject: [PATCH 0893/1009] Aligned integration manifest files (#97175) --- homeassistant/components/accuweather/manifest.json | 2 +- homeassistant/components/agent_dvr/manifest.json | 2 +- homeassistant/components/atag/manifest.json | 2 +- homeassistant/components/bluetooth_le_tracker/manifest.json | 3 +-- homeassistant/components/co2signal/manifest.json | 2 +- homeassistant/components/device_tracker/manifest.json | 1 - homeassistant/components/elv/manifest.json | 2 +- homeassistant/components/flick_electric/manifest.json | 2 +- homeassistant/components/flume/manifest.json | 2 +- homeassistant/components/fortios/manifest.json | 2 +- homeassistant/components/fully_kiosk/manifest.json | 2 +- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/google_assistant_sdk/manifest.json | 2 +- homeassistant/components/google_mail/manifest.json | 2 +- homeassistant/components/google_sheets/manifest.json | 2 +- homeassistant/components/growatt_server/manifest.json | 2 +- homeassistant/components/homewizard/manifest.json | 1 - homeassistant/components/iaqualink/manifest.json | 2 +- homeassistant/components/ld2410_ble/manifest.json | 2 +- homeassistant/components/led_ble/manifest.json | 2 +- homeassistant/components/lookin/manifest.json | 2 +- homeassistant/components/nina/manifest.json | 1 - homeassistant/components/oasa_telematics/manifest.json | 2 +- homeassistant/components/ombi/manifest.json | 2 +- homeassistant/components/radio_browser/manifest.json | 2 +- homeassistant/components/smarttub/manifest.json | 1 - homeassistant/components/speedtestdotnet/manifest.json | 1 - homeassistant/components/switcher_kis/manifest.json | 2 +- homeassistant/components/system_log/manifest.json | 1 - homeassistant/components/totalconnect/manifest.json | 1 - 30 files changed, 23 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index 658b5d368d056b..3a834261af5eb7 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -3,7 +3,7 @@ "name": "AccuWeather", "codeowners": ["@bieniu"], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/accuweather/", + "documentation": "https://www.home-assistant.io/integrations/accuweather", "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["accuweather"], diff --git a/homeassistant/components/agent_dvr/manifest.json b/homeassistant/components/agent_dvr/manifest.json index 0c9c829631a530..9a6c528c33665d 100644 --- a/homeassistant/components/agent_dvr/manifest.json +++ b/homeassistant/components/agent_dvr/manifest.json @@ -3,7 +3,7 @@ "name": "Agent DVR", "codeowners": ["@ispysoftware"], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/agent_dvr/", + "documentation": "https://www.home-assistant.io/integrations/agent_dvr", "iot_class": "local_polling", "loggers": ["agent"], "requirements": ["agent-py==0.0.23"] diff --git a/homeassistant/components/atag/manifest.json b/homeassistant/components/atag/manifest.json index 2a279840a9e9be..c45d8c4254641a 100644 --- a/homeassistant/components/atag/manifest.json +++ b/homeassistant/components/atag/manifest.json @@ -3,7 +3,7 @@ "name": "Atag", "codeowners": ["@MatsNL"], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/atag/", + "documentation": "https://www.home-assistant.io/integrations/atag", "iot_class": "local_polling", "loggers": ["pyatag"], "requirements": ["pyatag==0.3.5.3"] diff --git a/homeassistant/components/bluetooth_le_tracker/manifest.json b/homeassistant/components/bluetooth_le_tracker/manifest.json index 9c13bcc8c94fd7..79f885cad1895f 100644 --- a/homeassistant/components/bluetooth_le_tracker/manifest.json +++ b/homeassistant/components/bluetooth_le_tracker/manifest.json @@ -4,6 +4,5 @@ "codeowners": [], "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/bluetooth_le_tracker", - "iot_class": "local_push", - "loggers": [] + "iot_class": "local_push" } diff --git a/homeassistant/components/co2signal/manifest.json b/homeassistant/components/co2signal/manifest.json index 0c5e6f4139b21e..a0a3ee71a9cfd6 100644 --- a/homeassistant/components/co2signal/manifest.json +++ b/homeassistant/components/co2signal/manifest.json @@ -3,7 +3,7 @@ "name": "Electricity Maps", "codeowners": [], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/electricity_maps", + "documentation": "https://www.home-assistant.io/integrations/co2signal", "iot_class": "cloud_polling", "loggers": ["CO2Signal"], "requirements": ["CO2Signal==0.4.2"] diff --git a/homeassistant/components/device_tracker/manifest.json b/homeassistant/components/device_tracker/manifest.json index 11c85ebf8721ad..5fde0fc9fa1699 100644 --- a/homeassistant/components/device_tracker/manifest.json +++ b/homeassistant/components/device_tracker/manifest.json @@ -1,7 +1,6 @@ { "domain": "device_tracker", "name": "Device Tracker", - "after_dependencies": [], "codeowners": ["@home-assistant/core"], "dependencies": ["zone"], "documentation": "https://www.home-assistant.io/integrations/device_tracker", diff --git a/homeassistant/components/elv/manifest.json b/homeassistant/components/elv/manifest.json index 92213f39fcec5a..9b71595e58f8f7 100644 --- a/homeassistant/components/elv/manifest.json +++ b/homeassistant/components/elv/manifest.json @@ -2,7 +2,7 @@ "domain": "elv", "name": "ELV PCA", "codeowners": ["@majuss"], - "documentation": "https://www.home-assistant.io/integrations/pca", + "documentation": "https://www.home-assistant.io/integrations/elv", "iot_class": "local_polling", "loggers": ["pypca"], "requirements": ["pypca==0.0.7"] diff --git a/homeassistant/components/flick_electric/manifest.json b/homeassistant/components/flick_electric/manifest.json index a7db00b8f17425..0b1f2677d6abef 100644 --- a/homeassistant/components/flick_electric/manifest.json +++ b/homeassistant/components/flick_electric/manifest.json @@ -3,7 +3,7 @@ "name": "Flick Electric", "codeowners": ["@ZephireNZ"], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/flick_electric/", + "documentation": "https://www.home-assistant.io/integrations/flick_electric", "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["pyflick"], diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index 17a2b0b53be873..953d9791f2f780 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -8,7 +8,7 @@ "hostname": "flume-gw-*" } ], - "documentation": "https://www.home-assistant.io/integrations/flume/", + "documentation": "https://www.home-assistant.io/integrations/flume", "iot_class": "cloud_polling", "loggers": ["pyflume"], "requirements": ["PyFlume==0.6.5"] diff --git a/homeassistant/components/fortios/manifest.json b/homeassistant/components/fortios/manifest.json index a161d48398f36b..93e55071178221 100644 --- a/homeassistant/components/fortios/manifest.json +++ b/homeassistant/components/fortios/manifest.json @@ -2,7 +2,7 @@ "domain": "fortios", "name": "FortiOS", "codeowners": ["@kimfrellsen"], - "documentation": "https://www.home-assistant.io/integrations/fortios/", + "documentation": "https://www.home-assistant.io/integrations/fortios", "iot_class": "local_polling", "loggers": ["fortiosapi", "paramiko"], "requirements": ["fortiosapi==1.0.5"] diff --git a/homeassistant/components/fully_kiosk/manifest.json b/homeassistant/components/fully_kiosk/manifest.json index f313a117c44078..dcd36671fce83f 100644 --- a/homeassistant/components/fully_kiosk/manifest.json +++ b/homeassistant/components/fully_kiosk/manifest.json @@ -8,7 +8,7 @@ "registered_devices": true } ], - "documentation": "https://www.home-assistant.io/integrations/fullykiosk", + "documentation": "https://www.home-assistant.io/integrations/fully_kiosk", "iot_class": "local_polling", "mqtt": ["fully/deviceInfo/+"], "requirements": ["python-fullykiosk==0.0.12"] diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index f4177e8c3004f9..d5329598655abb 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "codeowners": ["@allenporter"], "config_flow": true, "dependencies": ["application_credentials"], - "documentation": "https://www.home-assistant.io/integrations/calendar.google/", + "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], "requirements": ["gcal-sync==4.1.4", "oauth2client==4.1.3"] diff --git a/homeassistant/components/google_assistant_sdk/manifest.json b/homeassistant/components/google_assistant_sdk/manifest.json index 984bbdfe7c1906..d52b7c18c41ce8 100644 --- a/homeassistant/components/google_assistant_sdk/manifest.json +++ b/homeassistant/components/google_assistant_sdk/manifest.json @@ -4,7 +4,7 @@ "codeowners": ["@tronikos"], "config_flow": true, "dependencies": ["application_credentials", "http"], - "documentation": "https://www.home-assistant.io/integrations/google_assistant_sdk/", + "documentation": "https://www.home-assistant.io/integrations/google_assistant_sdk", "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "platinum", diff --git a/homeassistant/components/google_mail/manifest.json b/homeassistant/components/google_mail/manifest.json index 1375ae4539267a..dfc5e279dc5fda 100644 --- a/homeassistant/components/google_mail/manifest.json +++ b/homeassistant/components/google_mail/manifest.json @@ -4,7 +4,7 @@ "codeowners": ["@tkdrob"], "config_flow": true, "dependencies": ["application_credentials"], - "documentation": "https://www.home-assistant.io/integrations/google_mail/", + "documentation": "https://www.home-assistant.io/integrations/google_mail", "integration_type": "service", "iot_class": "cloud_polling", "requirements": ["google-api-python-client==2.71.0"] diff --git a/homeassistant/components/google_sheets/manifest.json b/homeassistant/components/google_sheets/manifest.json index 5b2e5da8902389..6fae364df3b9cb 100644 --- a/homeassistant/components/google_sheets/manifest.json +++ b/homeassistant/components/google_sheets/manifest.json @@ -4,7 +4,7 @@ "codeowners": ["@tkdrob"], "config_flow": true, "dependencies": ["application_credentials"], - "documentation": "https://www.home-assistant.io/integrations/google_sheets/", + "documentation": "https://www.home-assistant.io/integrations/google_sheets", "integration_type": "service", "iot_class": "cloud_polling", "requirements": ["gspread==5.5.0"] diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index 7cdf12ab6bd405..a21c811af47b0d 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -3,7 +3,7 @@ "name": "Growatt", "codeowners": ["@muppet3000"], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/growatt_server/", + "documentation": "https://www.home-assistant.io/integrations/growatt_server", "iot_class": "cloud_polling", "loggers": ["growattServer"], "requirements": ["growattServer==1.3.0"] diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index 1ca833c6a744b1..36b9631c801b32 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -3,7 +3,6 @@ "name": "HomeWizard Energy", "codeowners": ["@DCSBL"], "config_flow": true, - "dependencies": [], "documentation": "https://www.home-assistant.io/integrations/homewizard", "iot_class": "local_polling", "loggers": ["homewizard_energy"], diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index df77d60c1414f5..8834a538be9005 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -3,7 +3,7 @@ "name": "Jandy iAqualink", "codeowners": ["@flz"], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/iaqualink/", + "documentation": "https://www.home-assistant.io/integrations/iaqualink", "iot_class": "cloud_polling", "loggers": ["iaqualink"], "requirements": ["iaqualink==0.5.0", "h2==4.1.0"] diff --git a/homeassistant/components/ld2410_ble/manifest.json b/homeassistant/components/ld2410_ble/manifest.json index 5ee0102ce17908..1a613a8209817a 100644 --- a/homeassistant/components/ld2410_ble/manifest.json +++ b/homeassistant/components/ld2410_ble/manifest.json @@ -17,7 +17,7 @@ "codeowners": ["@930913"], "config_flow": true, "dependencies": ["bluetooth_adapters"], - "documentation": "https://www.home-assistant.io/integrations/ld2410_ble/", + "documentation": "https://www.home-assistant.io/integrations/ld2410_ble", "integration_type": "device", "iot_class": "local_push", "requirements": ["bluetooth-data-tools==1.6.1", "ld2410-ble==0.1.1"] diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 3e34176771ca0f..5a1eef400016c5 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -30,7 +30,7 @@ "codeowners": ["@bdraco"], "config_flow": true, "dependencies": ["bluetooth_adapters"], - "documentation": "https://www.home-assistant.io/integrations/led_ble/", + "documentation": "https://www.home-assistant.io/integrations/led_ble", "iot_class": "local_polling", "requirements": ["bluetooth-data-tools==1.6.1", "led-ble==1.0.0"] } diff --git a/homeassistant/components/lookin/manifest.json b/homeassistant/components/lookin/manifest.json index 232493234bb427..63da470c5cd6bf 100644 --- a/homeassistant/components/lookin/manifest.json +++ b/homeassistant/components/lookin/manifest.json @@ -3,7 +3,7 @@ "name": "LOOKin", "codeowners": ["@ANMalko", "@bdraco"], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/lookin/", + "documentation": "https://www.home-assistant.io/integrations/lookin", "iot_class": "local_push", "loggers": ["aiolookin"], "requirements": ["aiolookin==1.0.0"], diff --git a/homeassistant/components/nina/manifest.json b/homeassistant/components/nina/manifest.json index 0185c727f67df3..d1897b53e046aa 100644 --- a/homeassistant/components/nina/manifest.json +++ b/homeassistant/components/nina/manifest.json @@ -3,7 +3,6 @@ "name": "NINA", "codeowners": ["@DeerMaximum"], "config_flow": true, - "dependencies": [], "documentation": "https://www.home-assistant.io/integrations/nina", "iot_class": "cloud_polling", "loggers": ["pynina"], diff --git a/homeassistant/components/oasa_telematics/manifest.json b/homeassistant/components/oasa_telematics/manifest.json index d50561a33a4acf..d3dbaad98e36da 100644 --- a/homeassistant/components/oasa_telematics/manifest.json +++ b/homeassistant/components/oasa_telematics/manifest.json @@ -2,7 +2,7 @@ "domain": "oasa_telematics", "name": "OASA Telematics", "codeowners": [], - "documentation": "https://www.home-assistant.io/integrations/oasa_telematics/", + "documentation": "https://www.home-assistant.io/integrations/oasa_telematics", "iot_class": "cloud_polling", "loggers": ["oasatelematics"], "requirements": ["oasatelematics==0.3"] diff --git a/homeassistant/components/ombi/manifest.json b/homeassistant/components/ombi/manifest.json index 91df756dafe35c..d9da13d2381846 100644 --- a/homeassistant/components/ombi/manifest.json +++ b/homeassistant/components/ombi/manifest.json @@ -2,7 +2,7 @@ "domain": "ombi", "name": "Ombi", "codeowners": ["@larssont"], - "documentation": "https://www.home-assistant.io/integrations/ombi/", + "documentation": "https://www.home-assistant.io/integrations/ombi", "iot_class": "local_polling", "requirements": ["pyombi==0.1.10"] } diff --git a/homeassistant/components/radio_browser/manifest.json b/homeassistant/components/radio_browser/manifest.json index 3d2ba299628322..035c4bdda45a29 100644 --- a/homeassistant/components/radio_browser/manifest.json +++ b/homeassistant/components/radio_browser/manifest.json @@ -3,7 +3,7 @@ "name": "Radio Browser", "codeowners": ["@frenck"], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/radio", + "documentation": "https://www.home-assistant.io/integrations/radio_browser", "integration_type": "service", "iot_class": "cloud_polling", "requirements": ["radios==0.1.1"] diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 76e79fcf94953b..3b8b727015b5d3 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -3,7 +3,6 @@ "name": "SmartTub", "codeowners": ["@mdz"], "config_flow": true, - "dependencies": [], "documentation": "https://www.home-assistant.io/integrations/smarttub", "iot_class": "cloud_polling", "loggers": ["smarttub"], diff --git a/homeassistant/components/speedtestdotnet/manifest.json b/homeassistant/components/speedtestdotnet/manifest.json index 6cb8e2b7d920c3..79999eb8ad9686 100644 --- a/homeassistant/components/speedtestdotnet/manifest.json +++ b/homeassistant/components/speedtestdotnet/manifest.json @@ -3,7 +3,6 @@ "name": "Speedtest.net", "codeowners": ["@rohankapoorcom", "@engrbm87"], "config_flow": true, - "dependencies": [], "documentation": "https://www.home-assistant.io/integrations/speedtestdotnet", "iot_class": "cloud_polling", "requirements": ["speedtest-cli==2.1.3"] diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 823f2c5463f08f..9accda95912b6f 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -3,7 +3,7 @@ "name": "Switcher", "codeowners": ["@thecode"], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", + "documentation": "https://www.home-assistant.io/integrations/switcher_kis", "iot_class": "local_push", "loggers": ["aioswitcher"], "quality_scale": "platinum", diff --git a/homeassistant/components/system_log/manifest.json b/homeassistant/components/system_log/manifest.json index 5d0fa29b2e8612..e9a24cfe1e1d04 100644 --- a/homeassistant/components/system_log/manifest.json +++ b/homeassistant/components/system_log/manifest.json @@ -2,7 +2,6 @@ "domain": "system_log", "name": "System Log", "codeowners": [], - "dependencies": [], "documentation": "https://www.home-assistant.io/integrations/system_log", "integration_type": "system", "quality_scale": "internal" diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index a81e7518132ae0..183919f05f29cc 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -3,7 +3,6 @@ "name": "Total Connect", "codeowners": ["@austinmroczek"], "config_flow": true, - "dependencies": [], "documentation": "https://www.home-assistant.io/integrations/totalconnect", "iot_class": "cloud_polling", "loggers": ["total_connect_client"], From 0dc5875cbda50430410f51f1ca44ab78345beede Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 25 Jul 2023 09:22:57 +0200 Subject: [PATCH 0894/1009] Bump python-otbr-api to 2.3.0 (#97185) --- homeassistant/components/otbr/manifest.json | 2 +- homeassistant/components/thread/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/otbr/manifest.json b/homeassistant/components/otbr/manifest.json index 94659df8547d03..a8a5ae062f7caf 100644 --- a/homeassistant/components/otbr/manifest.json +++ b/homeassistant/components/otbr/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/otbr", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["python-otbr-api==2.2.0"] + "requirements": ["python-otbr-api==2.3.0"] } diff --git a/homeassistant/components/thread/manifest.json b/homeassistant/components/thread/manifest.json index 0ce54496539637..71dbb786eb5c9a 100644 --- a/homeassistant/components/thread/manifest.json +++ b/homeassistant/components/thread/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://www.home-assistant.io/integrations/thread", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["python-otbr-api==2.2.0", "pyroute2==0.7.5"], + "requirements": ["python-otbr-api==2.3.0", "pyroute2==0.7.5"], "zeroconf": ["_meshcop._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 0e2b4467c8b1f9..731c4f18c45fd2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2130,7 +2130,7 @@ python-opensky==0.0.10 # homeassistant.components.otbr # homeassistant.components.thread -python-otbr-api==2.2.0 +python-otbr-api==2.3.0 # homeassistant.components.picnic python-picnic-api==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 34faeba0e2aaba..426f249c2f3958 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1559,7 +1559,7 @@ python-mystrom==2.2.0 # homeassistant.components.otbr # homeassistant.components.thread -python-otbr-api==2.2.0 +python-otbr-api==2.3.0 # homeassistant.components.picnic python-picnic-api==1.1.0 From f2726527f2c8e0a575720e6352b812dd30b2a232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Moreno?= Date: Tue, 25 Jul 2023 09:55:05 +0200 Subject: [PATCH 0895/1009] Create zwave_js repair issue instead of warning log entry (#95997) Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/climate.py | 23 +++++++++++++--- .../components/zwave_js/strings.json | 11 ++++++++ tests/components/zwave_js/test_climate.py | 27 ++++++++++++++----- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index cb027f32e0a5da..327db05cb00a5e 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -37,6 +37,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.util.unit_conversion import TemperatureConverter from .const import DATA_CLIENT, DOMAIN, LOGGER @@ -502,13 +503,27 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: preset_mode_value = self._hvac_presets.get(preset_mode) if preset_mode_value is None: raise ValueError(f"Received an invalid preset mode: {preset_mode}") - # Dry and Fan preset modes are deprecated as of 2023.8 - # Use Dry and Fan HVAC modes instead + # Dry and Fan preset modes are deprecated as of Home Assistant 2023.8. + # Please use Dry and Fan HVAC modes instead. if preset_mode_value in (ThermostatMode.DRY, ThermostatMode.FAN): LOGGER.warning( - "Dry and Fan preset modes are deprecated and will be removed in a future release. " - "Use the corresponding Dry and Fan HVAC modes instead" + "Dry and Fan preset modes are deprecated and will be removed in Home Assistant 2024.2. " + "Please use the corresponding Dry and Fan HVAC modes instead" ) + async_create_issue( + self.hass, + DOMAIN, + f"dry_fan_presets_deprecation_{self.entity_id}", + breaks_in_ha_version="2024.2.0", + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key="dry_fan_presets_deprecation", + translation_placeholders={ + "entity_id": self.entity_id, + }, + ) + await self._async_set_value(self._current_mode, preset_mode_value) diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 3b86cbdd5a4bf3..934307947d85d5 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -150,6 +150,17 @@ "invalid_server_version": { "title": "Newer version of Z-Wave JS Server needed", "description": "The version of Z-Wave JS Server you are currently running is too old for this version of Home Assistant. Please update the Z-Wave JS Server to the latest version to fix this issue." + }, + "dry_fan_presets_deprecation": { + "title": "Dry and Fan preset modes will be removed: {entity_id}", + "fix_flow": { + "step": { + "confirm": { + "title": "Dry and Fan preset modes will be removed: {entity_id}", + "description": "You are using the Dry or Fan preset modes in your entity `{entity_id}`.\n\nDry and Fan preset modes are deprecated and will be removed. Please update your automations to use the corresponding Dry and Fan **HVAC modes** instead.\n\nClick on SUBMIT below once you have manually fixed this issue." + } + } + } } }, "services": { diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index 753c107c2ee072..23d34c131b8690 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -40,6 +40,7 @@ ATTR_TEMPERATURE, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from .common import ( CLIMATE_AIDOO_HVAC_UNIT_ENTITY, @@ -722,14 +723,14 @@ async def test_thermostat_dry_and_fan_both_hvac_mode_and_preset( ] -async def test_thermostat_warning_when_setting_dry_preset( +async def test_thermostat_raise_repair_issue_and_warning_when_setting_dry_preset( hass: HomeAssistant, client, climate_airzone_aidoo_control_hvac_unit, integration, caplog: pytest.LogCaptureFixture, ) -> None: - """Test warning when setting Dry preset.""" + """Test raise of repair issue and warning when setting Dry preset.""" state = hass.states.get(CLIMATE_AIDOO_HVAC_UNIT_ENTITY) assert state @@ -743,20 +744,27 @@ async def test_thermostat_warning_when_setting_dry_preset( blocking=True, ) + issue_id = f"dry_fan_presets_deprecation_{CLIMATE_AIDOO_HVAC_UNIT_ENTITY}" + issue_registry = ir.async_get(hass) + + assert issue_registry.async_get_issue( + domain=DOMAIN, + issue_id=issue_id, + ) assert ( - "Dry and Fan preset modes are deprecated and will be removed in a future release. Use the corresponding Dry and Fan HVAC modes instead" + "Dry and Fan preset modes are deprecated and will be removed in Home Assistant 2024.2. Please use the corresponding Dry and Fan HVAC modes instead" in caplog.text ) -async def test_thermostat_warning_when_setting_fan_preset( +async def test_thermostat_raise_repair_issue_and_warning_when_setting_fan_preset( hass: HomeAssistant, client, climate_airzone_aidoo_control_hvac_unit, integration, caplog: pytest.LogCaptureFixture, ) -> None: - """Test warning when setting Fan preset.""" + """Test raise of repair issue and warning when setting Fan preset.""" state = hass.states.get(CLIMATE_AIDOO_HVAC_UNIT_ENTITY) assert state @@ -770,7 +778,14 @@ async def test_thermostat_warning_when_setting_fan_preset( blocking=True, ) + issue_id = f"dry_fan_presets_deprecation_{CLIMATE_AIDOO_HVAC_UNIT_ENTITY}" + issue_registry = ir.async_get(hass) + + assert issue_registry.async_get_issue( + domain=DOMAIN, + issue_id=issue_id, + ) assert ( - "Dry and Fan preset modes are deprecated and will be removed in a future release. Use the corresponding Dry and Fan HVAC modes instead" + "Dry and Fan preset modes are deprecated and will be removed in Home Assistant 2024.2. Please use the corresponding Dry and Fan HVAC modes instead" in caplog.text ) From 06f97679ee8b7f675ae53dbd9a7990e1a2993501 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 25 Jul 2023 10:11:48 +0200 Subject: [PATCH 0896/1009] Add WLAN QR code support to UniFi Image platform (#97171) --- homeassistant/components/unifi/config_flow.py | 2 +- homeassistant/components/unifi/const.py | 1 + homeassistant/components/unifi/image.py | 136 ++++++++++++++++++ homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../unifi/snapshots/test_image.ambr | 7 + tests/components/unifi/test_config_flow.py | 11 +- tests/components/unifi/test_controller.py | 6 +- tests/components/unifi/test_image.py | 122 ++++++++++++++++ 10 files changed, 282 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/unifi/image.py create mode 100644 tests/components/unifi/snapshots/test_image.ambr create mode 100644 tests/components/unifi/test_image.py diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index d283b668995a19..12f2d49e416cc3 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -308,7 +308,7 @@ async def async_step_device_tracker( return await self.async_step_client_control() ssids = ( - set(self.controller.api.wlans) + {wlan.name for wlan in self.controller.api.wlans.values()} | { f"{wlan.name}{wlan.name_combine_suffix}" for wlan in self.controller.api.wlans.values() diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index b5cea06c7193bb..e03bd50d4833bc 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -9,6 +9,7 @@ PLATFORMS = [ Platform.DEVICE_TRACKER, + Platform.IMAGE, Platform.SENSOR, Platform.SWITCH, Platform.UPDATE, diff --git a/homeassistant/components/unifi/image.py b/homeassistant/components/unifi/image.py new file mode 100644 index 00000000000000..730720753d4785 --- /dev/null +++ b/homeassistant/components/unifi/image.py @@ -0,0 +1,136 @@ +"""Image platform for UniFi Network integration. + +Support for QR code for guest WLANs. +""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Generic + +import aiounifi +from aiounifi.interfaces.api_handlers import ItemEvent +from aiounifi.interfaces.wlans import Wlans +from aiounifi.models.api import ApiItemT +from aiounifi.models.wlan import Wlan + +from homeassistant.components.image import DOMAIN, ImageEntity, ImageEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.util.dt as dt_util + +from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN +from .controller import UniFiController +from .entity import HandlerT, UnifiEntity, UnifiEntityDescription + + +@callback +def async_wlan_qr_code_image_fn(controller: UniFiController, wlan: Wlan) -> bytes: + """Calculate receiving data transfer value.""" + return controller.api.wlans.generate_wlan_qr_code(wlan) + + +@callback +def async_wlan_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: + """Create device registry entry for WLAN.""" + wlan = api.wlans[obj_id] + return DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, wlan.id)}, + manufacturer=ATTR_MANUFACTURER, + model="UniFi Network", + name=wlan.name, + ) + + +@dataclass +class UnifiImageEntityDescriptionMixin(Generic[HandlerT, ApiItemT]): + """Validate and load entities from different UniFi handlers.""" + + image_fn: Callable[[UniFiController, ApiItemT], bytes] + value_fn: Callable[[ApiItemT], str] + + +@dataclass +class UnifiImageEntityDescription( + ImageEntityDescription, + UnifiEntityDescription[HandlerT, ApiItemT], + UnifiImageEntityDescriptionMixin[HandlerT, ApiItemT], +): + """Class describing UniFi image entity.""" + + +ENTITY_DESCRIPTIONS: tuple[UnifiImageEntityDescription, ...] = ( + UnifiImageEntityDescription[Wlans, Wlan]( + key="WLAN QR Code", + entity_category=EntityCategory.DIAGNOSTIC, + has_entity_name=True, + entity_registry_enabled_default=False, + allowed_fn=lambda controller, obj_id: True, + api_handler_fn=lambda api: api.wlans, + available_fn=lambda controller, _: controller.available, + device_info_fn=async_wlan_device_info_fn, + event_is_on=None, + event_to_subscribe=None, + name_fn=lambda _: "QR Code", + object_fn=lambda api, obj_id: api.wlans[obj_id], + supported_fn=lambda controller, obj_id: True, + unique_id_fn=lambda controller, obj_id: f"qr_code-{obj_id}", + image_fn=async_wlan_qr_code_image_fn, + value_fn=lambda obj: obj.x_passphrase, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up image platform for UniFi Network integration.""" + controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + controller.register_platform_add_entities( + UnifiImageEntity, ENTITY_DESCRIPTIONS, async_add_entities + ) + + +class UnifiImageEntity(UnifiEntity[HandlerT, ApiItemT], ImageEntity): + """Base representation of a UniFi image.""" + + entity_description: UnifiImageEntityDescription[HandlerT, ApiItemT] + _attr_content_type = "image/png" + + current_image: bytes | None = None + previous_value = "" + + def __init__( + self, + obj_id: str, + controller: UniFiController, + description: UnifiEntityDescription[HandlerT, ApiItemT], + ) -> None: + """Initiatlize UniFi Image entity.""" + super().__init__(obj_id, controller, description) + ImageEntity.__init__(self, controller.hass) + + def image(self) -> bytes | None: + """Return bytes of image.""" + if self.current_image is None: + description = self.entity_description + obj = description.object_fn(self.controller.api, self._obj_id) + self.current_image = description.image_fn(self.controller, obj) + return self.current_image + + @callback + def async_update_state(self, event: ItemEvent, obj_id: str) -> None: + """Update entity state.""" + description = self.entity_description + obj = description.object_fn(self.controller.api, self._obj_id) + if (value := description.value_fn(obj)) != self.previous_value: + self.previous_value = value + self.current_image = None + self._attr_image_last_updated = dt_util.utcnow() diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 9bfb01e5a88336..c34d1035158f67 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==49"], + "requirements": ["aiounifi==50"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 731c4f18c45fd2..cde697360aaff1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -357,7 +357,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==49 +aiounifi==50 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 426f249c2f3958..70e07baf8d6424 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==49 +aiounifi==50 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/tests/components/unifi/snapshots/test_image.ambr b/tests/components/unifi/snapshots/test_image.ambr new file mode 100644 index 00000000000000..77b171118a1dfb --- /dev/null +++ b/tests/components/unifi/snapshots/test_image.ambr @@ -0,0 +1,7 @@ +# serializer version: 1 +# name: test_wlan_qr_code + b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x84\x00\x00\x00\x84\x01\x00\x00\x00\x00y?\xbe\n\x00\x00\x00\xcaIDATx\xda\xedV[\n\xc30\x0c\x13\xbb\x80\xef\x7fK\xdd\xc0\x93\x94\xfd\xac\x1fcL\xfbl(\xc4\x04*\xacG\xdcb/\x8b\xb8O\xdeO\x00\xccP\x95\x8b\xe5\x03\xd7\xf5\xcd\x89pF\xcf\x8c \\48\x08\nS\x948\x03p\xfe\x80C\xa8\x9d\x16\xc7P\xabvJ}\xe2\xd7\x84[\xe5W\xfc7\xbbS\xfd\xde\xcfB\xf115\xa2\xe3%\x99\xad\x93\xa0:\xbf6\xbeS\xec\x1a^\xb4\xed\xfb\xb2\xab\xd1\x99\xc9\xcdAjx\x89\x0e\xc5\xea\xf4T\xf9\xee\xe40m58\xb6<\x1b\xab~\xf4\xban\xd7:\xceu\x9e\x05\xc4I\xa6\xbb\xfb%q<7:\xbf\xa2\x90wo\xf5 None: + """Test the update_clients function when no clients are found.""" + await setup_unifi_integration(hass, aioclient_mock, wlans_response=[WLAN]) + assert len(hass.states.async_entity_ids(IMAGE_DOMAIN)) == 0 + + ent_reg = er.async_get(hass) + ent_reg_entry = ent_reg.async_get("image.ssid_1_qr_code") + assert ent_reg_entry.unique_id == "qr_code-012345678910111213141516" + assert ent_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION + assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC + + # Enable entity + ent_reg.async_update_entity(entity_id="image.ssid_1_qr_code", disabled_by=None) + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + # Validate state object + image_state_1 = hass.states.get("image.ssid_1_qr_code") + assert image_state_1.name == "SSID 1 QR Code" + + # Validate image + client = await hass_client() + resp = await client.get("/api/image_proxy/image.ssid_1_qr_code") + assert resp.status == HTTPStatus.OK + body = await resp.read() + assert body == snapshot + + # Update state object - same password - no change to state + mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=WLAN) + await hass.async_block_till_done() + image_state_2 = hass.states.get("image.ssid_1_qr_code") + assert image_state_1.state == image_state_2.state + + # Update state object - changeed password - new state + data = deepcopy(WLAN) + data["x_passphrase"] = "new password" + mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=data) + await hass.async_block_till_done() + image_state_3 = hass.states.get("image.ssid_1_qr_code") + assert image_state_1.state != image_state_3.state + + # Validate image + client = await hass_client() + resp = await client.get("/api/image_proxy/image.ssid_1_qr_code") + assert resp.status == HTTPStatus.OK + body = await resp.read() + assert body == snapshot From 90bf2d3076170a1c4d54f63e38cc51105db158d0 Mon Sep 17 00:00:00 2001 From: elmurato <1382097+elmurato@users.noreply.github.com> Date: Tue, 25 Jul 2023 10:14:01 +0200 Subject: [PATCH 0897/1009] Move Minecraft Server base entity to its own file (#97187) --- .coveragerc | 3 + .../components/minecraft_server/__init__.py | 59 +------------------ .../minecraft_server/binary_sensor.py | 3 +- .../components/minecraft_server/entity.py | 57 ++++++++++++++++++ .../components/minecraft_server/sensor.py | 3 +- 5 files changed, 67 insertions(+), 58 deletions(-) create mode 100644 homeassistant/components/minecraft_server/entity.py diff --git a/.coveragerc b/.coveragerc index 1032ac2db0aa9d..05a86ddebd1165 100644 --- a/.coveragerc +++ b/.coveragerc @@ -706,6 +706,9 @@ omit = homeassistant/components/mill/climate.py homeassistant/components/mill/sensor.py homeassistant/components/minecraft_server/__init__.py + homeassistant/components/minecraft_server/binary_sensor.py + homeassistant/components/minecraft_server/entity.py + homeassistant/components/minecraft_server/sensor.py homeassistant/components/minio/minio_helper.py homeassistant/components/mjpeg/camera.py homeassistant/components/mjpeg/util.py diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index da897a9767f026..aef6c94767fdae 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -10,16 +10,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, Platform -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from . import helpers -from .const import DOMAIN, MANUFACTURER, SCAN_INTERVAL, SIGNAL_NAME_PREFIX +from .const import DOMAIN, SCAN_INTERVAL, SIGNAL_NAME_PREFIX PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -214,52 +210,3 @@ async def _async_status_request(self) -> None: error, ) self._last_status_request_failed = True - - -class MinecraftServerEntity(Entity): - """Representation of a Minecraft Server base entity.""" - - _attr_has_entity_name = True - _attr_should_poll = False - - def __init__( - self, - server: MinecraftServer, - type_name: str, - icon: str, - device_class: str | None, - ) -> None: - """Initialize base entity.""" - self._server = server - self._attr_icon = icon - self._attr_unique_id = f"{self._server.unique_id}-{type_name}" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._server.unique_id)}, - manufacturer=MANUFACTURER, - model=f"Minecraft Server ({self._server.version})", - name=self._server.name, - sw_version=str(self._server.protocol_version), - ) - self._attr_device_class = device_class - self._extra_state_attributes = None - self._disconnect_dispatcher: CALLBACK_TYPE | None = None - - async def async_update(self) -> None: - """Fetch data from the server.""" - raise NotImplementedError() - - async def async_added_to_hass(self) -> None: - """Connect dispatcher to signal from server.""" - self._disconnect_dispatcher = async_dispatcher_connect( - self.hass, self._server.signal_name, self._update_callback - ) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect dispatcher before removal.""" - if self._disconnect_dispatcher: - self._disconnect_dispatcher() - - @callback - def _update_callback(self) -> None: - """Triggers update of properties after receiving signal from server.""" - self.async_schedule_update_ha_state(force_refresh=True) diff --git a/homeassistant/components/minecraft_server/binary_sensor.py b/homeassistant/components/minecraft_server/binary_sensor.py index ecf7d747770345..5c9cb5f42e18b3 100644 --- a/homeassistant/components/minecraft_server/binary_sensor.py +++ b/homeassistant/components/minecraft_server/binary_sensor.py @@ -7,8 +7,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import MinecraftServer, MinecraftServerEntity +from . import MinecraftServer from .const import DOMAIN, ICON_STATUS, NAME_STATUS +from .entity import MinecraftServerEntity async def async_setup_entry( diff --git a/homeassistant/components/minecraft_server/entity.py b/homeassistant/components/minecraft_server/entity.py new file mode 100644 index 00000000000000..02875cb69f2b22 --- /dev/null +++ b/homeassistant/components/minecraft_server/entity.py @@ -0,0 +1,57 @@ +"""Base entity for the Minecraft Server integration.""" + +from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo, Entity + +from . import MinecraftServer +from .const import DOMAIN, MANUFACTURER + + +class MinecraftServerEntity(Entity): + """Representation of a Minecraft Server base entity.""" + + _attr_has_entity_name = True + _attr_should_poll = False + + def __init__( + self, + server: MinecraftServer, + type_name: str, + icon: str, + device_class: str | None, + ) -> None: + """Initialize base entity.""" + self._server = server + self._attr_icon = icon + self._attr_unique_id = f"{self._server.unique_id}-{type_name}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._server.unique_id)}, + manufacturer=MANUFACTURER, + model=f"Minecraft Server ({self._server.version})", + name=self._server.name, + sw_version=str(self._server.protocol_version), + ) + self._attr_device_class = device_class + self._extra_state_attributes = None + self._disconnect_dispatcher: CALLBACK_TYPE | None = None + + async def async_update(self) -> None: + """Fetch data from the server.""" + raise NotImplementedError() + + async def async_added_to_hass(self) -> None: + """Connect dispatcher to signal from server.""" + self._disconnect_dispatcher = async_dispatcher_connect( + self.hass, self._server.signal_name, self._update_callback + ) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect dispatcher before removal.""" + if self._disconnect_dispatcher: + self._disconnect_dispatcher() + + @callback + def _update_callback(self) -> None: + """Triggers update of properties after receiving signal from server.""" + self.async_schedule_update_ha_state(force_refresh=True) diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index 5d056d98dd117a..3a9e4b8f0a0bd3 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import MinecraftServer, MinecraftServerEntity +from . import MinecraftServer from .const import ( ATTR_PLAYERS_LIST, DOMAIN, @@ -26,6 +26,7 @@ UNIT_PLAYERS_MAX, UNIT_PLAYERS_ONLINE, ) +from .entity import MinecraftServerEntity async def async_setup_entry( From 714a04d603d2a4ba1fbf42db286c55d2d2dabf46 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 25 Jul 2023 10:16:05 +0200 Subject: [PATCH 0898/1009] Add service turn_on and turn_off service for water_heater (#94817) --- homeassistant/components/demo/water_heater.py | 9 ++ .../components/melcloud/water_heater.py | 5 +- .../components/water_heater/__init__.py | 23 ++++ .../components/water_heater/services.yaml | 10 ++ .../components/water_heater/strings.json | 8 ++ tests/components/demo/test_water_heater.py | 16 +++ tests/components/water_heater/common.py | 25 +++++ tests/components/water_heater/test_init.py | 102 ++++++++++++++++++ 8 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 tests/components/water_heater/test_init.py diff --git a/homeassistant/components/demo/water_heater.py b/homeassistant/components/demo/water_heater.py index a21f492c439382..0ab175691f8d82 100644 --- a/homeassistant/components/demo/water_heater.py +++ b/homeassistant/components/demo/water_heater.py @@ -15,6 +15,7 @@ SUPPORT_FLAGS_HEATER = ( WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.ON_OFF | WaterHeaterEntityFeature.OPERATION_MODE | WaterHeaterEntityFeature.AWAY_MODE ) @@ -103,3 +104,11 @@ def turn_away_mode_off(self) -> None: """Turn away mode off.""" self._attr_is_away_mode_on = False self.schedule_update_ha_state() + + def turn_on(self, **kwargs: Any) -> None: + """Turn on water heater.""" + self.set_operation_mode("eco") + + def turn_off(self, **kwargs: Any) -> None: + """Turn off water heater.""" + self.set_operation_mode("off") diff --git a/homeassistant/components/melcloud/water_heater.py b/homeassistant/components/melcloud/water_heater.py index 511518279cbfc5..cf4b788480f1f7 100644 --- a/homeassistant/components/melcloud/water_heater.py +++ b/homeassistant/components/melcloud/water_heater.py @@ -44,6 +44,7 @@ class AtwWaterHeater(WaterHeaterEntity): _attr_supported_features = ( WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.ON_OFF | WaterHeaterEntityFeature.OPERATION_MODE ) @@ -72,11 +73,11 @@ def device_info(self): """Return a device description for device registry.""" return self._api.device_info - async def async_turn_on(self) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" await self._device.set({PROPERTY_POWER: True}) - async def async_turn_off(self) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await self._device.set({PROPERTY_POWER: False}) diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 3cd9378cfca85a..b31d1306c55943 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -61,6 +61,7 @@ class WaterHeaterEntityFeature(IntFlag): TARGET_TEMPERATURE = 1 OPERATION_MODE = 2 AWAY_MODE = 4 + ON_OFF = 8 # These SUPPORT_* constants are deprecated as of Home Assistant 2022.5. @@ -116,6 +117,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) await component.async_setup(config) + component.async_register_entity_service( + SERVICE_TURN_ON, {}, "async_turn_on", [WaterHeaterEntityFeature.ON_OFF] + ) + component.async_register_entity_service( + SERVICE_TURN_OFF, {}, "async_turn_off", [WaterHeaterEntityFeature.ON_OFF] + ) component.async_register_entity_service( SERVICE_SET_AWAY_MODE, SET_AWAY_MODE_SCHEMA, async_service_away_mode ) @@ -294,6 +301,22 @@ async def async_set_temperature(self, **kwargs: Any) -> None: ft.partial(self.set_temperature, **kwargs) ) + def turn_on(self, **kwargs: Any) -> None: + """Turn the water heater on.""" + raise NotImplementedError() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the water heater on.""" + await self.hass.async_add_executor_job(ft.partial(self.turn_on, **kwargs)) + + def turn_off(self, **kwargs: Any) -> None: + """Turn the water heater off.""" + raise NotImplementedError() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the water heater off.""" + await self.hass.async_add_executor_job(ft.partial(self.turn_off, **kwargs)) + def set_operation_mode(self, operation_mode: str) -> None: """Set new target operation mode.""" raise NotImplementedError() diff --git a/homeassistant/components/water_heater/services.yaml b/homeassistant/components/water_heater/services.yaml index b42109ee649960..b60cfdd8c48462 100644 --- a/homeassistant/components/water_heater/services.yaml +++ b/homeassistant/components/water_heater/services.yaml @@ -38,3 +38,13 @@ set_operation_mode: example: eco selector: text: + +turn_on: + target: + entity: + domain: water_heater + +turn_off: + target: + entity: + domain: water_heater diff --git a/homeassistant/components/water_heater/strings.json b/homeassistant/components/water_heater/strings.json index a03e93cde41e54..5ddb61d28b0387 100644 --- a/homeassistant/components/water_heater/strings.json +++ b/homeassistant/components/water_heater/strings.json @@ -53,6 +53,14 @@ "description": "[%key:component::water_heater::services::set_temperature::fields::operation_mode::description%]" } } + }, + "turn_on": { + "name": "[%key:common::action::turn_on%]", + "description": "Turns water heater on." + }, + "turn_off": { + "name": "[%key:common::action::turn_off%]", + "description": "Turns water heater off." } } } diff --git a/tests/components/demo/test_water_heater.py b/tests/components/demo/test_water_heater.py index 9e45b4e39bf994..cc91f57d872438 100644 --- a/tests/components/demo/test_water_heater.py +++ b/tests/components/demo/test_water_heater.py @@ -112,3 +112,19 @@ async def test_set_only_target_temp_with_convert(hass: HomeAssistant) -> None: await common.async_set_temperature(hass, 114, ENTITY_WATER_HEATER_CELSIUS) state = hass.states.get(ENTITY_WATER_HEATER_CELSIUS) assert state.attributes.get("temperature") == 114 + + +async def test_turn_on_off(hass: HomeAssistant) -> None: + """Test turn on and off.""" + state = hass.states.get(ENTITY_WATER_HEATER) + assert state.attributes.get("temperature") == 119 + assert state.attributes.get("away_mode") == "off" + assert state.attributes.get("operation_mode") == "eco" + + await common.async_turn_off(hass, ENTITY_WATER_HEATER) + state = hass.states.get(ENTITY_WATER_HEATER) + assert state.attributes.get("operation_mode") == "off" + + await common.async_turn_on(hass, ENTITY_WATER_HEATER) + state = hass.states.get(ENTITY_WATER_HEATER) + assert state.attributes.get("operation_mode") == "eco" diff --git a/tests/components/water_heater/common.py b/tests/components/water_heater/common.py index ece283f4bab65e..0d2d73d17fda1f 100644 --- a/tests/components/water_heater/common.py +++ b/tests/components/water_heater/common.py @@ -11,8 +11,11 @@ SERVICE_SET_AWAY_MODE, SERVICE_SET_OPERATION_MODE, SERVICE_SET_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, ENTITY_MATCH_ALL +from homeassistant.core import HomeAssistant async def async_set_away_mode(hass, away_mode, entity_id=ENTITY_MATCH_ALL): @@ -54,3 +57,25 @@ async def async_set_operation_mode(hass, operation_mode, entity_id=ENTITY_MATCH_ await hass.services.async_call( DOMAIN, SERVICE_SET_OPERATION_MODE, data, blocking=True ) + + +async def async_turn_on(hass: HomeAssistant, entity_id: str = ENTITY_MATCH_ALL) -> None: + """Turn all or specified water_heater devices on.""" + data = {} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) + + +async def async_turn_off( + hass: HomeAssistant, entity_id: str = ENTITY_MATCH_ALL +) -> None: + """Turn all or specified water_heater devices off.""" + data = {} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data, blocking=True) diff --git a/tests/components/water_heater/test_init.py b/tests/components/water_heater/test_init.py new file mode 100644 index 00000000000000..66276f0bc886c5 --- /dev/null +++ b/tests/components/water_heater/test_init.py @@ -0,0 +1,102 @@ +"""The tests for the water heater component.""" +from __future__ import annotations + +from unittest.mock import AsyncMock, MagicMock + +import pytest +import voluptuous as vol + +from homeassistant.components.water_heater import ( + SET_TEMPERATURE_SCHEMA, + WaterHeaterEntity, + WaterHeaterEntityFeature, +) +from homeassistant.core import HomeAssistant + +from tests.common import async_mock_service + + +async def test_set_temp_schema_no_req( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test the set temperature schema with missing required data.""" + domain = "climate" + service = "test_set_temperature" + schema = SET_TEMPERATURE_SCHEMA + calls = async_mock_service(hass, domain, service, schema) + + data = {"hvac_mode": "off", "entity_id": ["climate.test_id"]} + with pytest.raises(vol.Invalid): + await hass.services.async_call(domain, service, data) + await hass.async_block_till_done() + + assert len(calls) == 0 + + +async def test_set_temp_schema( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test the set temperature schema with ok required data.""" + domain = "water_heater" + service = "test_set_temperature" + schema = SET_TEMPERATURE_SCHEMA + calls = async_mock_service(hass, domain, service, schema) + + data = { + "temperature": 20.0, + "operation_mode": "gas", + "entity_id": ["water_heater.test_id"], + } + await hass.services.async_call(domain, service, data) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[-1].data == data + + +class MockWaterHeaterEntity(WaterHeaterEntity): + """Mock water heater device to use in tests.""" + + _attr_operation_list: list[str] = ["off", "heat_pump", "gas"] + _attr_operation = "heat_pump" + _attr_supported_features = WaterHeaterEntityFeature.ON_OFF + + +async def test_sync_turn_on(hass: HomeAssistant) -> None: + """Test if async turn_on calls sync turn_on.""" + water_heater = MockWaterHeaterEntity() + water_heater.hass = hass + + # Test with turn_on method defined + setattr(water_heater, "turn_on", MagicMock()) + await water_heater.async_turn_on() + + # pylint: disable-next=no-member + assert water_heater.turn_on.call_count == 1 + + # Test with async_turn_on method defined + setattr(water_heater, "async_turn_on", AsyncMock()) + await water_heater.async_turn_on() + + # pylint: disable-next=no-member + assert water_heater.async_turn_on.call_count == 1 + + +async def test_sync_turn_off(hass: HomeAssistant) -> None: + """Test if async turn_off calls sync turn_off.""" + water_heater = MockWaterHeaterEntity() + water_heater.hass = hass + + # Test with turn_off method defined + setattr(water_heater, "turn_off", MagicMock()) + await water_heater.async_turn_off() + + # pylint: disable-next=no-member + assert water_heater.turn_off.call_count == 1 + + # Test with async_turn_off method defined + setattr(water_heater, "async_turn_off", AsyncMock()) + await water_heater.async_turn_off() + + # pylint: disable-next=no-member + assert water_heater.async_turn_off.call_count == 1 From 04f6d1848bed237d020eede02be63273b43242a8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 25 Jul 2023 10:18:20 +0200 Subject: [PATCH 0899/1009] Implement YouTube async library (#97072) --- homeassistant/components/youtube/api.py | 31 ++--- .../components/youtube/config_flow.py | 95 +++++--------- .../components/youtube/coordinator.py | 102 +++++---------- homeassistant/components/youtube/entity.py | 2 +- .../components/youtube/manifest.json | 2 +- homeassistant/components/youtube/sensor.py | 14 ++- requirements_all.txt | 4 +- requirements_test_all.txt | 4 +- tests/components/youtube/__init__.py | 118 ++++++------------ tests/components/youtube/conftest.py | 4 +- .../youtube/fixtures/get_channel_2.json | 57 +++++---- .../fixtures/get_no_playlist_items.json | 9 ++ .../youtube/fixtures/thumbnail/default.json | 42 ------- .../youtube/fixtures/thumbnail/high.json | 52 -------- .../youtube/fixtures/thumbnail/medium.json | 47 ------- .../youtube/fixtures/thumbnail/none.json | 36 ------ .../youtube/fixtures/thumbnail/standard.json | 57 --------- .../youtube/snapshots/test_diagnostics.ambr | 4 +- .../youtube/snapshots/test_sensor.ambr | 32 ++++- tests/components/youtube/test_config_flow.py | 67 ++++------ tests/components/youtube/test_sensor.py | 82 ++++++------ 21 files changed, 272 insertions(+), 589 deletions(-) create mode 100644 tests/components/youtube/fixtures/get_no_playlist_items.json delete mode 100644 tests/components/youtube/fixtures/thumbnail/default.json delete mode 100644 tests/components/youtube/fixtures/thumbnail/high.json delete mode 100644 tests/components/youtube/fixtures/thumbnail/medium.json delete mode 100644 tests/components/youtube/fixtures/thumbnail/none.json delete mode 100644 tests/components/youtube/fixtures/thumbnail/standard.json diff --git a/homeassistant/components/youtube/api.py b/homeassistant/components/youtube/api.py index 64abf1a6753be7..f8a9008d9b37b4 100644 --- a/homeassistant/components/youtube/api.py +++ b/homeassistant/components/youtube/api.py @@ -1,16 +1,18 @@ """API for YouTube bound to Home Assistant OAuth.""" -from google.auth.exceptions import RefreshError -from google.oauth2.credentials import Credentials -from googleapiclient.discovery import Resource, build +from youtubeaio.types import AuthScope +from youtubeaio.youtube import YouTube from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.aiohttp_client import async_get_clientsession class AsyncConfigEntryAuth: """Provide Google authentication tied to an OAuth2 based config entry.""" + youtube: YouTube | None = None + def __init__( self, hass: HomeAssistant, @@ -30,19 +32,10 @@ async def check_and_refresh_token(self) -> str: await self.oauth_session.async_ensure_token_valid() return self.access_token - async def get_resource(self) -> Resource: - """Create executor job to get current resource.""" - try: - credentials = Credentials(await self.check_and_refresh_token()) - except RefreshError as ex: - self.oauth_session.config_entry.async_start_reauth(self.oauth_session.hass) - raise ex - return await self.hass.async_add_executor_job(self._get_resource, credentials) - - def _get_resource(self, credentials: Credentials) -> Resource: - """Get current resource.""" - return build( - "youtube", - "v3", - credentials=credentials, - ) + async def get_resource(self) -> YouTube: + """Create resource.""" + token = await self.check_and_refresh_token() + if self.youtube is None: + self.youtube = YouTube(session=async_get_clientsession(self.hass)) + await self.youtube.set_user_authentication(token, [AuthScope.READ_ONLY]) + return self.youtube diff --git a/homeassistant/components/youtube/config_flow.py b/homeassistant/components/youtube/config_flow.py index fa3bc6c8237352..50dee14d61a1d7 100644 --- a/homeassistant/components/youtube/config_flow.py +++ b/homeassistant/components/youtube/config_flow.py @@ -1,21 +1,21 @@ """Config flow for YouTube integration.""" from __future__ import annotations -from collections.abc import AsyncGenerator, Mapping +from collections.abc import Mapping import logging from typing import Any -from google.oauth2.credentials import Credentials -from googleapiclient.discovery import Resource, build -from googleapiclient.errors import HttpError -from googleapiclient.http import HttpRequest import voluptuous as vol +from youtubeaio.helper import first +from youtubeaio.types import AuthScope, ForbiddenError +from youtubeaio.youtube import YouTube from homeassistant.config_entries import ConfigEntry, OptionsFlowWithConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -31,37 +31,6 @@ ) -async def _get_subscriptions(hass: HomeAssistant, resource: Resource) -> AsyncGenerator: - amount_of_subscriptions = 50 - received_amount_of_subscriptions = 0 - next_page_token = None - while received_amount_of_subscriptions < amount_of_subscriptions: - # pylint: disable=no-member - subscription_request: HttpRequest = resource.subscriptions().list( - part="snippet", mine=True, maxResults=50, pageToken=next_page_token - ) - res = await hass.async_add_executor_job(subscription_request.execute) - amount_of_subscriptions = res["pageInfo"]["totalResults"] - if "nextPageToken" in res: - next_page_token = res["nextPageToken"] - for item in res["items"]: - received_amount_of_subscriptions += 1 - yield item - - -async def get_resource(hass: HomeAssistant, token: str) -> Resource: - """Get Youtube resource async.""" - - def _build_resource() -> Resource: - return build( - "youtube", - "v3", - credentials=Credentials(token), - ) - - return await hass.async_add_executor_job(_build_resource) - - class OAuth2FlowHandler( config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN ): @@ -73,6 +42,7 @@ class OAuth2FlowHandler( DOMAIN = DOMAIN reauth_entry: ConfigEntry | None = None + _youtube: YouTube | None = None @staticmethod @callback @@ -112,25 +82,25 @@ async def async_step_reauth_confirm( return self.async_show_form(step_id="reauth_confirm") return await self.async_step_user() + async def get_resource(self, token: str) -> YouTube: + """Get Youtube resource async.""" + if self._youtube is None: + self._youtube = YouTube(session=async_get_clientsession(self.hass)) + await self._youtube.set_user_authentication(token, [AuthScope.READ_ONLY]) + return self._youtube + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Create an entry for the flow, or update existing entry.""" try: - service = await get_resource(self.hass, data[CONF_TOKEN][CONF_ACCESS_TOKEN]) - # pylint: disable=no-member - own_channel_request: HttpRequest = service.channels().list( - part="snippet", mine=True - ) - response = await self.hass.async_add_executor_job( - own_channel_request.execute - ) - if not response["items"]: + youtube = await self.get_resource(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) + own_channel = await first(youtube.get_user_channels()) + if own_channel is None or own_channel.snippet is None: return self.async_abort( reason="no_channel", description_placeholders={"support_url": CHANNEL_CREATION_HELP_URL}, ) - own_channel = response["items"][0] - except HttpError as ex: - error = ex.reason + except ForbiddenError as ex: + error = ex.args[0] return self.async_abort( reason="access_not_configured", description_placeholders={"message": error}, @@ -138,16 +108,16 @@ async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: except Exception as ex: # pylint: disable=broad-except LOGGER.error("Unknown error occurred: %s", ex.args) return self.async_abort(reason="unknown") - self._title = own_channel["snippet"]["title"] + self._title = own_channel.snippet.title self._data = data if not self.reauth_entry: - await self.async_set_unique_id(own_channel["id"]) + await self.async_set_unique_id(own_channel.channel_id) self._abort_if_unique_id_configured() return await self.async_step_channels() - if self.reauth_entry.unique_id == own_channel["id"]: + if self.reauth_entry.unique_id == own_channel.channel_id: self.hass.config_entries.async_update_entry(self.reauth_entry, data=data) await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) return self.async_abort(reason="reauth_successful") @@ -167,15 +137,13 @@ async def async_step_channels( data=self._data, options=user_input, ) - service = await get_resource( - self.hass, self._data[CONF_TOKEN][CONF_ACCESS_TOKEN] - ) + youtube = await self.get_resource(self._data[CONF_TOKEN][CONF_ACCESS_TOKEN]) selectable_channels = [ SelectOptionDict( - value=subscription["snippet"]["resourceId"]["channelId"], - label=subscription["snippet"]["title"], + value=subscription.snippet.channel_id, + label=subscription.snippet.title, ) - async for subscription in _get_subscriptions(self.hass, service) + async for subscription in youtube.get_user_subscriptions() ] return self.async_show_form( step_id="channels", @@ -201,15 +169,16 @@ async def async_step_init( title=self.config_entry.title, data=user_input, ) - service = await get_resource( - self.hass, self.config_entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN] + youtube = YouTube(session=async_get_clientsession(self.hass)) + await youtube.set_user_authentication( + self.config_entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN], [AuthScope.READ_ONLY] ) selectable_channels = [ SelectOptionDict( - value=subscription["snippet"]["resourceId"]["channelId"], - label=subscription["snippet"]["title"], + value=subscription.snippet.channel_id, + label=subscription.snippet.title, ) - async for subscription in _get_subscriptions(self.hass, service) + async for subscription in youtube.get_user_subscriptions() ] return self.async_show_form( step_id="init", diff --git a/homeassistant/components/youtube/coordinator.py b/homeassistant/components/youtube/coordinator.py index 726295448950e5..cb9d1e8214e249 100644 --- a/homeassistant/components/youtube/coordinator.py +++ b/homeassistant/components/youtube/coordinator.py @@ -4,12 +4,13 @@ from datetime import timedelta from typing import Any -from googleapiclient.discovery import Resource -from googleapiclient.http import HttpRequest +from youtubeaio.helper import first +from youtubeaio.types import UnauthorizedError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ICON, ATTR_ID from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import AsyncConfigEntryAuth @@ -27,16 +28,7 @@ ) -def get_upload_playlist_id(channel_id: str) -> str: - """Return the playlist id with the uploads of the channel. - - Replacing the UC in the channel id (UCxxxxxxxxxxxx) with UU is - the way to do it without extra request (UUxxxxxxxxxxxx). - """ - return channel_id.replace("UC", "UU", 1) - - -class YouTubeDataUpdateCoordinator(DataUpdateCoordinator): +class YouTubeDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """A YouTube Data Update Coordinator.""" config_entry: ConfigEntry @@ -52,64 +44,30 @@ def __init__(self, hass: HomeAssistant, auth: AsyncConfigEntryAuth) -> None: ) async def _async_update_data(self) -> dict[str, Any]: - service = await self._auth.get_resource() - channels = await self._get_channels(service) - - return await self.hass.async_add_executor_job( - self._get_channel_data, service, channels - ) - - async def _get_channels(self, service: Resource) -> list[dict[str, Any]]: - data = [] - received_channels = 0 - 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)) - channel_request: HttpRequest = service.channels().list( - part="snippet,statistics", - id=",".join(channels[received_channels:end]), - maxResults=50, - ) - response: dict = await self.hass.async_add_executor_job( - channel_request.execute - ) - data.extend(response["items"]) - received_channels += len(response["items"]) - return data - - def _get_channel_data( - self, service: Resource, channels: list[dict[str, Any]] - ) -> dict[str, Any]: - data: dict[str, Any] = {} - for channel in channels: - playlist_id = get_upload_playlist_id(channel["id"]) - response = ( - service.playlistItems() - .list( - part="snippet,contentDetails", playlistId=playlist_id, maxResults=1 + youtube = await self._auth.get_resource() + res = {} + channel_ids = self.config_entry.options[CONF_CHANNELS] + try: + async for channel in youtube.get_channels(channel_ids): + video = await first( + youtube.get_playlist_items(channel.upload_playlist_id, 1) ) - .execute() - ) - video = response["items"][0] - data[channel["id"]] = { - ATTR_ID: channel["id"], - ATTR_TITLE: channel["snippet"]["title"], - ATTR_ICON: channel["snippet"]["thumbnails"]["high"]["url"], - ATTR_LATEST_VIDEO: { - ATTR_PUBLISHED_AT: video["snippet"]["publishedAt"], - ATTR_TITLE: video["snippet"]["title"], - ATTR_DESCRIPTION: video["snippet"]["description"], - ATTR_THUMBNAIL: self._get_thumbnail(video), - ATTR_VIDEO_ID: video["contentDetails"]["videoId"], - }, - ATTR_SUBSCRIBER_COUNT: int(channel["statistics"]["subscriberCount"]), - } - return data - - def _get_thumbnail(self, video: dict[str, Any]) -> str | None: - thumbnails = video["snippet"]["thumbnails"] - for size in ("standard", "high", "medium", "default"): - if size in thumbnails: - return thumbnails[size]["url"] - return None + latest_video = None + if video: + latest_video = { + ATTR_PUBLISHED_AT: video.snippet.added_at, + ATTR_TITLE: video.snippet.title, + ATTR_DESCRIPTION: video.snippet.description, + ATTR_THUMBNAIL: video.snippet.thumbnails.get_highest_quality().url, + ATTR_VIDEO_ID: video.content_details.video_id, + } + res[channel.channel_id] = { + ATTR_ID: channel.channel_id, + ATTR_TITLE: channel.snippet.title, + ATTR_ICON: channel.snippet.thumbnails.get_highest_quality().url, + ATTR_LATEST_VIDEO: latest_video, + ATTR_SUBSCRIBER_COUNT: channel.statistics.subscriber_count, + } + except UnauthorizedError as err: + raise ConfigEntryAuthFailed from err + return res diff --git a/homeassistant/components/youtube/entity.py b/homeassistant/components/youtube/entity.py index 2f9238dec2674c..46deaf40450fc6 100644 --- a/homeassistant/components/youtube/entity.py +++ b/homeassistant/components/youtube/entity.py @@ -9,7 +9,7 @@ from .coordinator import YouTubeDataUpdateCoordinator -class YouTubeChannelEntity(CoordinatorEntity): +class YouTubeChannelEntity(CoordinatorEntity[YouTubeDataUpdateCoordinator]): """An HA implementation for YouTube entity.""" _attr_has_entity_name = True diff --git a/homeassistant/components/youtube/manifest.json b/homeassistant/components/youtube/manifest.json index fbc02bda0069c6..b37d242fe524ff 100644 --- a/homeassistant/components/youtube/manifest.json +++ b/homeassistant/components/youtube/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/youtube", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["google-api-python-client==2.71.0"] + "requirements": ["youtubeaio==1.1.4"] } diff --git a/homeassistant/components/youtube/sensor.py b/homeassistant/components/youtube/sensor.py index b5d3fc79b396c0..a63b8fb0c0ba34 100644 --- a/homeassistant/components/youtube/sensor.py +++ b/homeassistant/components/youtube/sensor.py @@ -30,9 +30,10 @@ class YouTubeMixin: """Mixin for required keys.""" + available_fn: Callable[[Any], bool] value_fn: Callable[[Any], StateType] entity_picture_fn: Callable[[Any], str | None] - attributes_fn: Callable[[Any], dict[str, Any]] | None + attributes_fn: Callable[[Any], dict[str, Any] | None] | None @dataclass @@ -45,6 +46,7 @@ class YouTubeSensorEntityDescription(SensorEntityDescription, YouTubeMixin): key="latest_upload", translation_key="latest_upload", icon="mdi:youtube", + available_fn=lambda channel: channel[ATTR_LATEST_VIDEO] is not None, value_fn=lambda channel: channel[ATTR_LATEST_VIDEO][ATTR_TITLE], entity_picture_fn=lambda channel: channel[ATTR_LATEST_VIDEO][ATTR_THUMBNAIL], attributes_fn=lambda channel: { @@ -57,6 +59,7 @@ class YouTubeSensorEntityDescription(SensorEntityDescription, YouTubeMixin): translation_key="subscribers", icon="mdi:youtube-subscription", native_unit_of_measurement="subscribers", + available_fn=lambda _: True, value_fn=lambda channel: channel[ATTR_SUBSCRIBER_COUNT], entity_picture_fn=lambda channel: channel[ATTR_ICON], attributes_fn=None, @@ -83,6 +86,13 @@ class YouTubeSensor(YouTubeChannelEntity, SensorEntity): entity_description: YouTubeSensorEntityDescription + @property + def available(self): + """Return if the entity is available.""" + return self.entity_description.available_fn( + self.coordinator.data[self._channel_id] + ) + @property def native_value(self) -> StateType: """Return the value reported by the sensor.""" @@ -91,6 +101,8 @@ def native_value(self) -> StateType: @property def entity_picture(self) -> str | None: """Return the value reported by the sensor.""" + if not self.available: + return None return self.entity_description.entity_picture_fn( self.coordinator.data[self._channel_id] ) diff --git a/requirements_all.txt b/requirements_all.txt index cde697360aaff1..d4836ea1522252 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -873,7 +873,6 @@ goalzero==0.2.2 goodwe==0.2.31 # homeassistant.components.google_mail -# homeassistant.components.youtube google-api-python-client==2.71.0 # homeassistant.components.google_pubsub @@ -2728,6 +2727,9 @@ yolink-api==0.3.0 # homeassistant.components.youless youless-api==1.0.1 +# homeassistant.components.youtube +youtubeaio==1.1.4 + # homeassistant.components.media_extractor yt-dlp==2023.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70e07baf8d6424..6d3a9d3819f679 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -689,7 +689,6 @@ goalzero==0.2.2 goodwe==0.2.31 # homeassistant.components.google_mail -# homeassistant.components.youtube google-api-python-client==2.71.0 # homeassistant.components.google_pubsub @@ -2004,6 +2003,9 @@ yolink-api==0.3.0 # homeassistant.components.youless youless-api==1.0.1 +# homeassistant.components.youtube +youtubeaio==1.1.4 + # homeassistant.components.zamg zamg==0.2.4 diff --git a/tests/components/youtube/__init__.py b/tests/components/youtube/__init__.py index 15a43d7a62fcd3..3c46ff92661c8b 100644 --- a/tests/components/youtube/__init__.py +++ b/tests/components/youtube/__init__.py @@ -1,78 +1,18 @@ """Tests for the YouTube integration.""" -from dataclasses import dataclass +from collections.abc import AsyncGenerator import json -from typing import Any -from tests.common import load_fixture - - -@dataclass -class MockRequest: - """Mock object for a request.""" - - fixture: str - - def execute(self) -> dict[str, Any]: - """Return a fixture.""" - return json.loads(load_fixture(self.fixture)) - - -class MockChannels: - """Mock object for channels.""" - - def __init__(self, fixture: str): - """Initialize mock channels.""" - self._fixture = fixture - - def list( - self, - part: str, - id: str | None = None, - mine: bool | None = None, - maxResults: int | None = None, - ) -> MockRequest: - """Return a fixture.""" - return MockRequest(fixture=self._fixture) - - -class MockPlaylistItems: - """Mock object for playlist items.""" +from youtubeaio.models import YouTubeChannel, YouTubePlaylistItem, YouTubeSubscription +from youtubeaio.types import AuthScope - def __init__(self, fixture: str): - """Initialize mock playlist items.""" - self._fixture = fixture - - def list( - self, - part: str, - playlistId: str, - maxResults: int | None = None, - ) -> MockRequest: - """Return a fixture.""" - return MockRequest(fixture=self._fixture) - - -class MockSubscriptions: - """Mock object for subscriptions.""" - - def __init__(self, fixture: str): - """Initialize mock subscriptions.""" - self._fixture = fixture - - def list( - self, - part: str, - mine: bool, - maxResults: int | None = None, - pageToken: str | None = None, - ) -> MockRequest: - """Return a fixture.""" - return MockRequest(fixture=self._fixture) +from tests.common import load_fixture -class MockService: +class MockYouTube: """Service which returns mock objects.""" + _authenticated = False + def __init__( self, channel_fixture: str = "youtube/get_channel.json", @@ -84,14 +24,36 @@ def __init__( self._playlist_items_fixture = playlist_items_fixture self._subscriptions_fixture = subscriptions_fixture - def channels(self) -> MockChannels: - """Return a mock object.""" - return MockChannels(self._channel_fixture) - - def playlistItems(self) -> MockPlaylistItems: - """Return a mock object.""" - return MockPlaylistItems(self._playlist_items_fixture) - - def subscriptions(self) -> MockSubscriptions: - """Return a mock object.""" - return MockSubscriptions(self._subscriptions_fixture) + async def set_user_authentication( + self, token: str, scopes: list[AuthScope] + ) -> None: + """Authenticate the user.""" + self._authenticated = True + + async def get_user_channels(self) -> AsyncGenerator[YouTubeChannel, None]: + """Get channels for authenticated user.""" + channels = json.loads(load_fixture(self._channel_fixture)) + for item in channels["items"]: + yield YouTubeChannel(**item) + + async def get_channels( + self, channel_ids: list[str] + ) -> AsyncGenerator[YouTubeChannel, None]: + """Get channels.""" + channels = json.loads(load_fixture(self._channel_fixture)) + for item in channels["items"]: + yield YouTubeChannel(**item) + + async def get_playlist_items( + self, playlist_id: str, amount: int + ) -> AsyncGenerator[YouTubePlaylistItem, None]: + """Get channels.""" + channels = json.loads(load_fixture(self._playlist_items_fixture)) + for item in channels["items"]: + yield YouTubePlaylistItem(**item) + + async def get_user_subscriptions(self) -> AsyncGenerator[YouTubeSubscription, None]: + """Get channels for authenticated user.""" + channels = json.loads(load_fixture(self._subscriptions_fixture)) + for item in channels["items"]: + yield YouTubeSubscription(**item) diff --git a/tests/components/youtube/conftest.py b/tests/components/youtube/conftest.py index d87a3c07679c46..a8a333190eecd6 100644 --- a/tests/components/youtube/conftest.py +++ b/tests/components/youtube/conftest.py @@ -15,7 +15,7 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -from tests.components.youtube import MockService +from tests.components.youtube import MockYouTube from tests.test_util.aiohttp import AiohttpClientMocker ComponentSetup = Callable[[], Awaitable[None]] @@ -106,7 +106,7 @@ async def mock_setup_integration( async def func() -> None: with patch( - "homeassistant.components.youtube.api.build", return_value=MockService() + "homeassistant.components.youtube.api.YouTube", return_value=MockYouTube() ): assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/youtube/fixtures/get_channel_2.json b/tests/components/youtube/fixtures/get_channel_2.json index 24e71ad91abd59..f2757b169bb987 100644 --- a/tests/components/youtube/fixtures/get_channel_2.json +++ b/tests/components/youtube/fixtures/get_channel_2.json @@ -1,47 +1,54 @@ { - "kind": "youtube#SubscriptionListResponse", - "etag": "6C9iFE7CzKQqPrEoJlE0H2U27xI", - "nextPageToken": "CAEQAA", + "kind": "youtube#channelListResponse", + "etag": "en7FWhCsHOdM398MU6qRntH03cQ", "pageInfo": { - "totalResults": 525, - "resultsPerPage": 1 + "totalResults": 1, + "resultsPerPage": 5 }, "items": [ { - "kind": "youtube#subscription", - "etag": "4Hr8w5f03mLak3fZID0aXypQRDg", - "id": "l6YW-siEBx2rtBlTJ_ip10UA2t_d09UYkgtJsqbYblE", + "kind": "youtube#channel", + "etag": "PyFk-jpc2-v4mvG_6imAHx3y6TM", + "id": "UCXuqSBlHAE6Xw-yeJA0Tunw", "snippet": { - "publishedAt": "2015-08-09T21:37:44Z", "title": "Linus Tech Tips", - "description": "Linus Tech Tips is a passionate team of \"professionally curious\" experts in consumer technology and video production who aim to educate and entertain.", - "resourceId": { - "kind": "youtube#channel", - "channelId": "UCXuqSBlHAE6Xw-yeJA0Tunw" - }, - "channelId": "UCXuqSBlHAE6Xw-yeJA0Tunw", + "description": "Linus Tech Tips is a passionate team of \"professionally curious\" experts in consumer technology and video production who aim to educate and entertain.\n", + "customUrl": "@linustechtips", + "publishedAt": "2008-11-25T00:46:52Z", "thumbnails": { "default": { - "url": "https://yt3.ggpht.com/Vy6KL7EM_apxPSxF0pPy5w_c87YDTOlBQo3MADDF0Wl51kwxmt9wmRotnt2xQXwlrcyO0Xe56w=s88-c-k-c0x00ffffff-no-rj" + "url": "https://yt3.ggpht.com/Vy6KL7EM_apxPSxF0pPy5w_c87YDTOlBQo3MADDF0Wl51kwxmt9wmRotnt2xQXwlrcyO0Xe56w=s88-c-k-c0x00ffffff-no-rj", + "width": 88, + "height": 88 }, "medium": { - "url": "https://yt3.ggpht.com/Vy6KL7EM_apxPSxF0pPy5w_c87YDTOlBQo3MADDF0Wl51kwxmt9wmRotnt2xQXwlrcyO0Xe56w=s240-c-k-c0x00ffffff-no-rj" + "url": "https://yt3.ggpht.com/Vy6KL7EM_apxPSxF0pPy5w_c87YDTOlBQo3MADDF0Wl51kwxmt9wmRotnt2xQXwlrcyO0Xe56w=s240-c-k-c0x00ffffff-no-rj", + "width": 240, + "height": 240 }, "high": { - "url": "https://yt3.ggpht.com/Vy6KL7EM_apxPSxF0pPy5w_c87YDTOlBQo3MADDF0Wl51kwxmt9wmRotnt2xQXwlrcyO0Xe56w=s800-c-k-c0x00ffffff-no-rj" + "url": "https://yt3.ggpht.com/Vy6KL7EM_apxPSxF0pPy5w_c87YDTOlBQo3MADDF0Wl51kwxmt9wmRotnt2xQXwlrcyO0Xe56w=s800-c-k-c0x00ffffff-no-rj", + "width": 800, + "height": 800 } - } + }, + "localized": { + "title": "Linus Tech Tips", + "description": "Linus Tech Tips is a passionate team of \"professionally curious\" experts in consumer technology and video production who aim to educate and entertain.\n" + }, + "country": "CA" }, "contentDetails": { - "totalItemCount": 6178, - "newItemCount": 0, - "activityType": "all" + "relatedPlaylists": { + "likes": "", + "uploads": "UUXuqSBlHAE6Xw-yeJA0Tunw" + } }, "statistics": { - "viewCount": "214141263", - "subscriberCount": "2290000", + "viewCount": "7190986011", + "subscriberCount": "15600000", "hiddenSubscriberCount": false, - "videoCount": "5798" + "videoCount": "6541" } } ] diff --git a/tests/components/youtube/fixtures/get_no_playlist_items.json b/tests/components/youtube/fixtures/get_no_playlist_items.json new file mode 100644 index 00000000000000..98b9a11737e0e6 --- /dev/null +++ b/tests/components/youtube/fixtures/get_no_playlist_items.json @@ -0,0 +1,9 @@ +{ + "kind": "youtube#playlistItemListResponse", + "etag": "O0Ah8Wd5pUD2Gsv-n0A42RDRcX8", + "items": [], + "pageInfo": { + "totalResults": 0, + "resultsPerPage": 0 + } +} diff --git a/tests/components/youtube/fixtures/thumbnail/default.json b/tests/components/youtube/fixtures/thumbnail/default.json deleted file mode 100644 index 6b5d66d6501cce..00000000000000 --- a/tests/components/youtube/fixtures/thumbnail/default.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "kind": "youtube#playlistItemListResponse", - "etag": "O0Ah8Wd5pUD2Gsv-n0A42RDRcX8", - "nextPageToken": "EAAaBlBUOkNBVQ", - "items": [ - { - "kind": "youtube#playlistItem", - "etag": "qgpoAJRNskzLhD99njC8e2kPB0M", - "id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3Lnd5c3VrRHJNZHFV", - "snippet": { - "publishedAt": "2023-05-11T00:20:46Z", - "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", - "title": "What's new in Google Home in less than 1 minute", - "description": "Discover how your connected devices can do more with Google Home using Matter and Automations at Google I/O 2023.\n\nTo learn more about what's new in Google Home, check out the keynote → https://goo.gle/IO23_homekey\n\nSubscribe to Google Developers → https://goo.gle/developers \n\n#GoogleIO #GoogleHome", - "thumbnails": { - "default": { - "url": "https://i.ytimg.com/vi/wysukDrMdqU/default.jpg", - "width": 120, - "height": 90 - } - }, - "channelTitle": "Google for Developers", - "playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw", - "position": 0, - "resourceId": { - "kind": "youtube#video", - "videoId": "wysukDrMdqU" - }, - "videoOwnerChannelTitle": "Google for Developers", - "videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw" - }, - "contentDetails": { - "videoId": "wysukDrMdqU", - "videoPublishedAt": "2023-05-11T00:20:46Z" - } - } - ], - "pageInfo": { - "totalResults": 5798, - "resultsPerPage": 1 - } -} diff --git a/tests/components/youtube/fixtures/thumbnail/high.json b/tests/components/youtube/fixtures/thumbnail/high.json deleted file mode 100644 index 430ad3715cc3da..00000000000000 --- a/tests/components/youtube/fixtures/thumbnail/high.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "kind": "youtube#playlistItemListResponse", - "etag": "O0Ah8Wd5pUD2Gsv-n0A42RDRcX8", - "nextPageToken": "EAAaBlBUOkNBVQ", - "items": [ - { - "kind": "youtube#playlistItem", - "etag": "qgpoAJRNskzLhD99njC8e2kPB0M", - "id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3Lnd5c3VrRHJNZHFV", - "snippet": { - "publishedAt": "2023-05-11T00:20:46Z", - "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", - "title": "What's new in Google Home in less than 1 minute", - "description": "Discover how your connected devices can do more with Google Home using Matter and Automations at Google I/O 2023.\n\nTo learn more about what's new in Google Home, check out the keynote → https://goo.gle/IO23_homekey\n\nSubscribe to Google Developers → https://goo.gle/developers \n\n#GoogleIO #GoogleHome", - "thumbnails": { - "default": { - "url": "https://i.ytimg.com/vi/wysukDrMdqU/default.jpg", - "width": 120, - "height": 90 - }, - "medium": { - "url": "https://i.ytimg.com/vi/wysukDrMdqU/mqdefault.jpg", - "width": 320, - "height": 180 - }, - "high": { - "url": "https://i.ytimg.com/vi/wysukDrMdqU/hqdefault.jpg", - "width": 480, - "height": 360 - } - }, - "channelTitle": "Google for Developers", - "playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw", - "position": 0, - "resourceId": { - "kind": "youtube#video", - "videoId": "wysukDrMdqU" - }, - "videoOwnerChannelTitle": "Google for Developers", - "videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw" - }, - "contentDetails": { - "videoId": "wysukDrMdqU", - "videoPublishedAt": "2023-05-11T00:20:46Z" - } - } - ], - "pageInfo": { - "totalResults": 5798, - "resultsPerPage": 1 - } -} diff --git a/tests/components/youtube/fixtures/thumbnail/medium.json b/tests/components/youtube/fixtures/thumbnail/medium.json deleted file mode 100644 index 21cb09bd886a5a..00000000000000 --- a/tests/components/youtube/fixtures/thumbnail/medium.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "kind": "youtube#playlistItemListResponse", - "etag": "O0Ah8Wd5pUD2Gsv-n0A42RDRcX8", - "nextPageToken": "EAAaBlBUOkNBVQ", - "items": [ - { - "kind": "youtube#playlistItem", - "etag": "qgpoAJRNskzLhD99njC8e2kPB0M", - "id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3Lnd5c3VrRHJNZHFV", - "snippet": { - "publishedAt": "2023-05-11T00:20:46Z", - "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", - "title": "What's new in Google Home in less than 1 minute", - "description": "Discover how your connected devices can do more with Google Home using Matter and Automations at Google I/O 2023.\n\nTo learn more about what's new in Google Home, check out the keynote → https://goo.gle/IO23_homekey\n\nSubscribe to Google Developers → https://goo.gle/developers \n\n#GoogleIO #GoogleHome", - "thumbnails": { - "default": { - "url": "https://i.ytimg.com/vi/wysukDrMdqU/default.jpg", - "width": 120, - "height": 90 - }, - "medium": { - "url": "https://i.ytimg.com/vi/wysukDrMdqU/mqdefault.jpg", - "width": 320, - "height": 180 - } - }, - "channelTitle": "Google for Developers", - "playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw", - "position": 0, - "resourceId": { - "kind": "youtube#video", - "videoId": "wysukDrMdqU" - }, - "videoOwnerChannelTitle": "Google for Developers", - "videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw" - }, - "contentDetails": { - "videoId": "wysukDrMdqU", - "videoPublishedAt": "2023-05-11T00:20:46Z" - } - } - ], - "pageInfo": { - "totalResults": 5798, - "resultsPerPage": 1 - } -} diff --git a/tests/components/youtube/fixtures/thumbnail/none.json b/tests/components/youtube/fixtures/thumbnail/none.json deleted file mode 100644 index d4c28730cabb04..00000000000000 --- a/tests/components/youtube/fixtures/thumbnail/none.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "kind": "youtube#playlistItemListResponse", - "etag": "O0Ah8Wd5pUD2Gsv-n0A42RDRcX8", - "nextPageToken": "EAAaBlBUOkNBVQ", - "items": [ - { - "kind": "youtube#playlistItem", - "etag": "qgpoAJRNskzLhD99njC8e2kPB0M", - "id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3Lnd5c3VrRHJNZHFV", - "snippet": { - "publishedAt": "2023-05-11T00:20:46Z", - "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", - "title": "What's new in Google Home in less than 1 minute", - "description": "Discover how your connected devices can do more with Google Home using Matter and Automations at Google I/O 2023.\n\nTo learn more about what's new in Google Home, check out the keynote → https://goo.gle/IO23_homekey\n\nSubscribe to Google Developers → https://goo.gle/developers \n\n#GoogleIO #GoogleHome", - "thumbnails": {}, - "channelTitle": "Google for Developers", - "playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw", - "position": 0, - "resourceId": { - "kind": "youtube#video", - "videoId": "wysukDrMdqU" - }, - "videoOwnerChannelTitle": "Google for Developers", - "videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw" - }, - "contentDetails": { - "videoId": "wysukDrMdqU", - "videoPublishedAt": "2023-05-11T00:20:46Z" - } - } - ], - "pageInfo": { - "totalResults": 5798, - "resultsPerPage": 1 - } -} diff --git a/tests/components/youtube/fixtures/thumbnail/standard.json b/tests/components/youtube/fixtures/thumbnail/standard.json deleted file mode 100644 index bdbedfcf4c9a21..00000000000000 --- a/tests/components/youtube/fixtures/thumbnail/standard.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "kind": "youtube#playlistItemListResponse", - "etag": "O0Ah8Wd5pUD2Gsv-n0A42RDRcX8", - "nextPageToken": "EAAaBlBUOkNBVQ", - "items": [ - { - "kind": "youtube#playlistItem", - "etag": "qgpoAJRNskzLhD99njC8e2kPB0M", - "id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3Lnd5c3VrRHJNZHFV", - "snippet": { - "publishedAt": "2023-05-11T00:20:46Z", - "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", - "title": "What's new in Google Home in less than 1 minute", - "description": "Discover how your connected devices can do more with Google Home using Matter and Automations at Google I/O 2023.\n\nTo learn more about what's new in Google Home, check out the keynote → https://goo.gle/IO23_homekey\n\nSubscribe to Google Developers → https://goo.gle/developers \n\n#GoogleIO #GoogleHome", - "thumbnails": { - "default": { - "url": "https://i.ytimg.com/vi/wysukDrMdqU/default.jpg", - "width": 120, - "height": 90 - }, - "medium": { - "url": "https://i.ytimg.com/vi/wysukDrMdqU/mqdefault.jpg", - "width": 320, - "height": 180 - }, - "high": { - "url": "https://i.ytimg.com/vi/wysukDrMdqU/hqdefault.jpg", - "width": 480, - "height": 360 - }, - "standard": { - "url": "https://i.ytimg.com/vi/wysukDrMdqU/sddefault.jpg", - "width": 640, - "height": 480 - } - }, - "channelTitle": "Google for Developers", - "playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw", - "position": 0, - "resourceId": { - "kind": "youtube#video", - "videoId": "wysukDrMdqU" - }, - "videoOwnerChannelTitle": "Google for Developers", - "videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw" - }, - "contentDetails": { - "videoId": "wysukDrMdqU", - "videoPublishedAt": "2023-05-11T00:20:46Z" - } - } - ], - "pageInfo": { - "totalResults": 5798, - "resultsPerPage": 1 - } -} diff --git a/tests/components/youtube/snapshots/test_diagnostics.ambr b/tests/components/youtube/snapshots/test_diagnostics.ambr index 6a41465ac92596..a938cb8daad089 100644 --- a/tests/components/youtube/snapshots/test_diagnostics.ambr +++ b/tests/components/youtube/snapshots/test_diagnostics.ambr @@ -5,8 +5,8 @@ 'icon': 'https://yt3.ggpht.com/fca_HuJ99xUxflWdex0XViC3NfctBFreIl8y4i9z411asnGTWY-Ql3MeH_ybA4kNaOjY7kyA=s800-c-k-c0x00ffffff-no-rj', 'id': 'UC_x5XG1OV2P6uZZ5FSM9Ttw', 'latest_video': dict({ - 'published_at': '2023-05-11T00:20:46Z', - 'thumbnail': 'https://i.ytimg.com/vi/wysukDrMdqU/sddefault.jpg', + 'published_at': '2023-05-11T00:20:46+00:00', + 'thumbnail': 'https://i.ytimg.com/vi/wysukDrMdqU/maxresdefault.jpg', 'title': "What's new in Google Home in less than 1 minute", 'video_id': 'wysukDrMdqU', }), diff --git a/tests/components/youtube/snapshots/test_sensor.ambr b/tests/components/youtube/snapshots/test_sensor.ambr index b643bdeb979903..e3bfa4ec4bdd20 100644 --- a/tests/components/youtube/snapshots/test_sensor.ambr +++ b/tests/components/youtube/snapshots/test_sensor.ambr @@ -2,10 +2,10 @@ # name: test_sensor StateSnapshot({ 'attributes': ReadOnlyDict({ - 'entity_picture': 'https://i.ytimg.com/vi/wysukDrMdqU/sddefault.jpg', + 'entity_picture': 'https://i.ytimg.com/vi/wysukDrMdqU/maxresdefault.jpg', 'friendly_name': 'Google for Developers Latest upload', 'icon': 'mdi:youtube', - 'published_at': '2023-05-11T00:20:46Z', + 'published_at': datetime.datetime(2023, 5, 11, 0, 20, 46, tzinfo=datetime.timezone.utc), 'video_id': 'wysukDrMdqU', }), 'context': , @@ -30,3 +30,31 @@ 'state': '2290000', }) # --- +# name: test_sensor_without_uploaded_video + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Google for Developers Latest upload', + 'icon': 'mdi:youtube', + }), + 'context': , + 'entity_id': 'sensor.google_for_developers_latest_upload', + 'last_changed': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensor_without_uploaded_video.1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'entity_picture': 'https://yt3.ggpht.com/fca_HuJ99xUxflWdex0XViC3NfctBFreIl8y4i9z411asnGTWY-Ql3MeH_ybA4kNaOjY7kyA=s800-c-k-c0x00ffffff-no-rj', + 'friendly_name': 'Google for Developers Subscribers', + 'icon': 'mdi:youtube-subscription', + 'unit_of_measurement': 'subscribers', + }), + 'context': , + 'entity_id': 'sensor.google_for_developers_subscribers', + 'last_changed': , + 'last_updated': , + 'state': '2290000', + }) +# --- diff --git a/tests/components/youtube/test_config_flow.py b/tests/components/youtube/test_config_flow.py index 5b91ff958f80b1..97875004d11cc2 100644 --- a/tests/components/youtube/test_config_flow.py +++ b/tests/components/youtube/test_config_flow.py @@ -1,9 +1,8 @@ """Test the YouTube config flow.""" from unittest.mock import patch -from googleapiclient.errors import HttpError -from httplib2 import Response import pytest +from youtubeaio.types import ForbiddenError from homeassistant import config_entries from homeassistant.components.youtube.const import CONF_CHANNELS, DOMAIN @@ -11,7 +10,7 @@ from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow -from . import MockService +from . import MockYouTube from .conftest import ( CLIENT_ID, GOOGLE_AUTH_URI, @@ -21,7 +20,7 @@ ComponentSetup, ) -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator @@ -58,9 +57,8 @@ async def test_full_flow( with patch( "homeassistant.components.youtube.async_setup_entry", return_value=True ) as mock_setup, patch( - "homeassistant.components.youtube.api.build", return_value=MockService() - ), patch( - "homeassistant.components.youtube.config_flow.build", return_value=MockService() + "homeassistant.components.youtube.config_flow.YouTube", + return_value=MockYouTube(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == FlowResultType.FORM @@ -112,11 +110,11 @@ async def test_flow_abort_without_channel( assert resp.status == 200 assert resp.headers["content-type"] == "text/html; charset=utf-8" - service = MockService(channel_fixture="youtube/get_no_channel.json") + service = MockYouTube(channel_fixture="youtube/get_no_channel.json") with patch( "homeassistant.components.youtube.async_setup_entry", return_value=True - ), patch("homeassistant.components.youtube.api.build", return_value=service), patch( - "homeassistant.components.youtube.config_flow.build", return_value=service + ), patch( + "homeassistant.components.youtube.config_flow.YouTube", return_value=service ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == FlowResultType.ABORT @@ -153,41 +151,29 @@ async def test_flow_http_error( assert resp.headers["content-type"] == "text/html; charset=utf-8" with patch( - "homeassistant.components.youtube.config_flow.build", - side_effect=HttpError( - Response( - { - "vary": "Origin, X-Origin, Referer", - "content-type": "application/json; charset=UTF-8", - "date": "Mon, 15 May 2023 21:25:42 GMT", - "server": "scaffolding on HTTPServer2", - "cache-control": "private", - "x-xss-protection": "0", - "x-frame-options": "SAMEORIGIN", - "x-content-type-options": "nosniff", - "alt-svc": 'h3=":443"; ma=2592000,h3-29=":443"; ma=2592000', - "transfer-encoding": "chunked", - "status": "403", - "content-length": "947", - "-content-encoding": "gzip", - } - ), - b'{"error": {"code": 403,"message": "YouTube Data API v3 has not been used in project 0 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/youtube.googleapis.com/overview?project=0 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.","errors": [ { "message": "YouTube Data API v3 has not been used in project 0 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/youtube.googleapis.com/overview?project=0 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.", "domain": "usageLimits", "reason": "accessNotConfigured", "extendedHelp": "https://console.developers.google.com" }],"status": "PERMISSION_DENIED"\n }\n}\n', + "homeassistant.components.youtube.config_flow.YouTube.get_user_channels", + side_effect=ForbiddenError( + "YouTube Data API v3 has not been used in project 0 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/youtube.googleapis.com/overview?project=0 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry." ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "access_not_configured" - assert ( - result["description_placeholders"]["message"] - == "YouTube Data API v3 has not been used in project 0 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/youtube.googleapis.com/overview?project=0 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry." + assert result["description_placeholders"]["message"] == ( + "YouTube Data API v3 has not been used in project 0 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/youtube.googleapis.com/overview?project=0 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry." ) @pytest.mark.parametrize( ("fixture", "abort_reason", "placeholders", "calls", "access_token"), [ - ("get_channel", "reauth_successful", None, 1, "updated-access-token"), + ( + "get_channel", + "reauth_successful", + None, + 1, + "updated-access-token", + ), ( "get_channel_2", "wrong_account", @@ -254,14 +240,12 @@ async def test_reauth( }, ) + youtube = MockYouTube(channel_fixture=f"youtube/{fixture}.json") with patch( "homeassistant.components.youtube.async_setup_entry", return_value=True ) as mock_setup, patch( - "httplib2.Http.request", - return_value=( - Response({}), - bytes(load_fixture(f"youtube/{fixture}.json"), encoding="UTF-8"), - ), + "homeassistant.components.youtube.config_flow.YouTube", + return_value=youtube, ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -309,7 +293,7 @@ async def test_flow_exception( assert resp.headers["content-type"] == "text/html; charset=utf-8" with patch( - "homeassistant.components.youtube.config_flow.build", side_effect=Exception + "homeassistant.components.youtube.config_flow.YouTube", side_effect=Exception ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == FlowResultType.ABORT @@ -322,7 +306,8 @@ async def test_options_flow( """Test the full options flow.""" await setup_integration() with patch( - "homeassistant.components.youtube.config_flow.build", return_value=MockService() + "homeassistant.components.youtube.config_flow.YouTube", + return_value=MockYouTube(), ): entry = hass.config_entries.async_entries(DOMAIN)[0] result = await hass.config_entries.options.async_init(entry.entry_id) diff --git a/tests/components/youtube/test_sensor.py b/tests/components/youtube/test_sensor.py index f2c5274c4a71c5..7dc368a5860c25 100644 --- a/tests/components/youtube/test_sensor.py +++ b/tests/components/youtube/test_sensor.py @@ -2,17 +2,17 @@ from datetime import timedelta from unittest.mock import patch -from google.auth.exceptions import RefreshError -import pytest from syrupy import SnapshotAssertion +from youtubeaio.types import UnauthorizedError from homeassistant import config_entries -from homeassistant.components.youtube import DOMAIN +from homeassistant.components.youtube.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import MockService -from .conftest import TOKEN, ComponentSetup +from . import MockYouTube +from .conftest import ComponentSetup from tests.common import async_fire_time_changed @@ -30,6 +30,29 @@ async def test_sensor( assert state == snapshot +async def test_sensor_without_uploaded_video( + hass: HomeAssistant, snapshot: SnapshotAssertion, setup_integration: ComponentSetup +) -> None: + """Test sensor when there is no video on the channel.""" + await setup_integration() + + with patch( + "homeassistant.components.youtube.api.AsyncConfigEntryAuth.get_resource", + return_value=MockYouTube( + playlist_items_fixture="youtube/get_no_playlist_items.json" + ), + ): + future = dt_util.utcnow() + timedelta(minutes=15) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.google_for_developers_latest_upload") + assert state == snapshot + + state = hass.states.get("sensor.google_for_developers_subscribers") + assert state == snapshot + + async def test_sensor_updating( hass: HomeAssistant, setup_integration: ComponentSetup ) -> None: @@ -41,8 +64,8 @@ async def test_sensor_updating( assert state.attributes["video_id"] == "wysukDrMdqU" with patch( - "homeassistant.components.youtube.api.build", - return_value=MockService( + "homeassistant.components.youtube.api.AsyncConfigEntryAuth.get_resource", + return_value=MockYouTube( playlist_items_fixture="youtube/get_playlist_items_2.json" ), ): @@ -55,7 +78,7 @@ async def test_sensor_updating( assert state.state == "Google I/O 2023 Developer Keynote in 5 minutes" assert ( state.attributes["entity_picture"] - == "https://i.ytimg.com/vi/hleLlcHwQLM/sddefault.jpg" + == "https://i.ytimg.com/vi/hleLlcHwQLM/maxresdefault.jpg" ) assert state.attributes["video_id"] == "hleLlcHwQLM" @@ -64,9 +87,11 @@ async def test_sensor_reauth_trigger( hass: HomeAssistant, setup_integration: ComponentSetup ) -> None: """Test reauth is triggered after a refresh error.""" - await setup_integration() - - with patch(TOKEN, side_effect=RefreshError): + with patch( + "youtubeaio.youtube.YouTube.get_channels", side_effect=UnauthorizedError + ): + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() future = dt_util.utcnow() + timedelta(minutes=15) async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -78,38 +103,3 @@ async def test_sensor_reauth_trigger( assert flow["step_id"] == "reauth_confirm" assert flow["handler"] == DOMAIN assert flow["context"]["source"] == config_entries.SOURCE_REAUTH - - -@pytest.mark.parametrize( - ("fixture", "url", "has_entity_picture"), - [ - ("standard", "https://i.ytimg.com/vi/wysukDrMdqU/sddefault.jpg", True), - ("high", "https://i.ytimg.com/vi/wysukDrMdqU/hqdefault.jpg", True), - ("medium", "https://i.ytimg.com/vi/wysukDrMdqU/mqdefault.jpg", True), - ("default", "https://i.ytimg.com/vi/wysukDrMdqU/default.jpg", True), - ("none", None, False), - ], -) -async def test_thumbnail( - hass: HomeAssistant, - setup_integration: ComponentSetup, - fixture: str, - url: str | None, - has_entity_picture: bool, -) -> None: - """Test if right thumbnail is selected.""" - await setup_integration() - - with patch( - "homeassistant.components.youtube.api.build", - return_value=MockService( - playlist_items_fixture=f"youtube/thumbnail/{fixture}.json" - ), - ): - future = dt_util.utcnow() + timedelta(minutes=15) - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - state = hass.states.get("sensor.google_for_developers_latest_upload") - assert state - assert ("entity_picture" in state.attributes) is has_entity_picture - assert state.attributes.get("entity_picture") == url From 6ef7c5ece6e317737901c482ccac8f4875bb3c73 Mon Sep 17 00:00:00 2001 From: Michael Arthur Date: Tue, 25 Jul 2023 20:46:53 +1200 Subject: [PATCH 0900/1009] Add electric kiwi integration (#81149) Co-authored-by: Franck Nijhof --- .coveragerc | 5 + .strict-typing | 1 + CODEOWNERS | 2 + .../components/electric_kiwi/__init__.py | 65 ++++++ homeassistant/components/electric_kiwi/api.py | 33 ++++ .../electric_kiwi/application_credentials.py | 38 ++++ .../components/electric_kiwi/config_flow.py | 59 ++++++ .../components/electric_kiwi/const.py | 11 ++ .../components/electric_kiwi/coordinator.py | 81 ++++++++ .../components/electric_kiwi/manifest.json | 11 ++ .../components/electric_kiwi/oauth2.py | 76 +++++++ .../components/electric_kiwi/sensor.py | 113 +++++++++++ .../components/electric_kiwi/strings.json | 36 ++++ .../generated/application_credentials.py | 1 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + mypy.ini | 10 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/electric_kiwi/__init__.py | 1 + tests/components/electric_kiwi/conftest.py | 63 ++++++ .../electric_kiwi/test_config_flow.py | 187 ++++++++++++++++++ 22 files changed, 806 insertions(+) create mode 100644 homeassistant/components/electric_kiwi/__init__.py create mode 100644 homeassistant/components/electric_kiwi/api.py create mode 100644 homeassistant/components/electric_kiwi/application_credentials.py create mode 100644 homeassistant/components/electric_kiwi/config_flow.py create mode 100644 homeassistant/components/electric_kiwi/const.py create mode 100644 homeassistant/components/electric_kiwi/coordinator.py create mode 100644 homeassistant/components/electric_kiwi/manifest.json create mode 100644 homeassistant/components/electric_kiwi/oauth2.py create mode 100644 homeassistant/components/electric_kiwi/sensor.py create mode 100644 homeassistant/components/electric_kiwi/strings.json create mode 100644 tests/components/electric_kiwi/__init__.py create mode 100644 tests/components/electric_kiwi/conftest.py create mode 100644 tests/components/electric_kiwi/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 05a86ddebd1165..30f768e01a40e7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -261,6 +261,11 @@ omit = homeassistant/components/eight_sleep/__init__.py homeassistant/components/eight_sleep/binary_sensor.py homeassistant/components/eight_sleep/sensor.py + homeassistant/components/electric_kiwi/__init__.py + homeassistant/components/electric_kiwi/api.py + homeassistant/components/electric_kiwi/oauth2.py + homeassistant/components/electric_kiwi/sensor.py + homeassistant/components/electric_kiwi/coordinator.py homeassistant/components/eliqonline/sensor.py homeassistant/components/elkm1/__init__.py homeassistant/components/elkm1/alarm_control_panel.py diff --git a/.strict-typing b/.strict-typing index 9818e3d3197dfa..dffeb08e014796 100644 --- a/.strict-typing +++ b/.strict-typing @@ -108,6 +108,7 @@ homeassistant.components.dsmr.* homeassistant.components.dunehd.* homeassistant.components.efergy.* homeassistant.components.electrasmart.* +homeassistant.components.electric_kiwi.* homeassistant.components.elgato.* homeassistant.components.elkm1.* homeassistant.components.emulated_hue.* diff --git a/CODEOWNERS b/CODEOWNERS index 918ad4c23439a8..ef9634e1527d10 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -319,6 +319,8 @@ build.json @home-assistant/supervisor /tests/components/eight_sleep/ @mezz64 @raman325 /homeassistant/components/electrasmart/ @jafar-atili /tests/components/electrasmart/ @jafar-atili +/homeassistant/components/electric_kiwi/ @mikey0000 +/tests/components/electric_kiwi/ @mikey0000 /homeassistant/components/elgato/ @frenck /tests/components/elgato/ @frenck /homeassistant/components/elkm1/ @gwww @bdraco diff --git a/homeassistant/components/electric_kiwi/__init__.py b/homeassistant/components/electric_kiwi/__init__.py new file mode 100644 index 00000000000000..3ae6b1c70cf058 --- /dev/null +++ b/homeassistant/components/electric_kiwi/__init__.py @@ -0,0 +1,65 @@ +"""The Electric Kiwi integration.""" +from __future__ import annotations + +import aiohttp +from electrickiwi_api import ElectricKiwiApi +from electrickiwi_api.exceptions import ApiException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow + +from . import api +from .const import DOMAIN +from .coordinator import ElectricKiwiHOPDataCoordinator + +PLATFORMS: list[Platform] = [ + Platform.SENSOR, +] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Electric Kiwi from a config entry.""" + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + ) + + session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + + try: + await session.async_ensure_token_valid() + except aiohttp.ClientResponseError as err: + if 400 <= err.status < 500: + raise ConfigEntryAuthFailed(err) from err + raise ConfigEntryNotReady from err + except aiohttp.ClientError as err: + raise ConfigEntryNotReady from err + + ek_api = ElectricKiwiApi( + api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session) + ) + hop_coordinator = ElectricKiwiHOPDataCoordinator(hass, ek_api) + + try: + await ek_api.set_active_session() + await hop_coordinator.async_config_entry_first_refresh() + except ApiException as err: + raise ConfigEntryNotReady from err + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = hop_coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/electric_kiwi/api.py b/homeassistant/components/electric_kiwi/api.py new file mode 100644 index 00000000000000..89109f01948189 --- /dev/null +++ b/homeassistant/components/electric_kiwi/api.py @@ -0,0 +1,33 @@ +"""API for Electric Kiwi bound to Home Assistant OAuth.""" + +from __future__ import annotations + +from typing import cast + +from aiohttp import ClientSession +from electrickiwi_api import AbstractAuth + +from homeassistant.helpers import config_entry_oauth2_flow + +from .const import API_BASE_URL + + +class AsyncConfigEntryAuth(AbstractAuth): + """Provide Electric Kiwi authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + websession: ClientSession, + oauth_session: config_entry_oauth2_flow.OAuth2Session, + ) -> None: + """Initialize Electric Kiwi auth.""" + # add host when ready for production "https://api.electrickiwi.co.nz" defaults to dev + super().__init__(websession, API_BASE_URL) + self._oauth_session = oauth_session + + async def async_get_access_token(self) -> str: + """Return a valid access token.""" + if not self._oauth_session.valid_token: + await self._oauth_session.async_ensure_token_valid() + + return cast(str, self._oauth_session.token["access_token"]) diff --git a/homeassistant/components/electric_kiwi/application_credentials.py b/homeassistant/components/electric_kiwi/application_credentials.py new file mode 100644 index 00000000000000..4a3ef8aa1c55ee --- /dev/null +++ b/homeassistant/components/electric_kiwi/application_credentials.py @@ -0,0 +1,38 @@ +"""application_credentials platform the Electric Kiwi integration.""" + +from homeassistant.components.application_credentials import ( + AuthorizationServer, + ClientCredential, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow + +from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN +from .oauth2 import ElectricKiwiLocalOAuth2Implementation + + +async def async_get_auth_implementation( + hass: HomeAssistant, auth_domain: str, credential: ClientCredential +) -> config_entry_oauth2_flow.AbstractOAuth2Implementation: + """Return auth implementation.""" + return ElectricKiwiLocalOAuth2Implementation( + hass, + auth_domain, + credential, + authorization_server=await async_get_authorization_server(hass), + ) + + +async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: + """Return authorization server.""" + return AuthorizationServer( + authorize_url=OAUTH2_AUTHORIZE, + token_url=OAUTH2_TOKEN, + ) + + +async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]: + """Return description placeholders for the credentials dialog.""" + return { + "more_info_url": "https://www.home-assistant.io/integrations/electric_kiwi/" + } diff --git a/homeassistant/components/electric_kiwi/config_flow.py b/homeassistant/components/electric_kiwi/config_flow.py new file mode 100644 index 00000000000000..c2c80aaa4020ac --- /dev/null +++ b/homeassistant/components/electric_kiwi/config_flow.py @@ -0,0 +1,59 @@ +"""Config flow for Electric Kiwi.""" +from __future__ import annotations + +from collections.abc import Mapping +import logging +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_entry_oauth2_flow + +from .const import DOMAIN, SCOPE_VALUES + + +class ElectricKiwiOauth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle Electric Kiwi OAuth2 authentication.""" + + DOMAIN = DOMAIN + + def __init__(self) -> None: + """Set up instance.""" + super().__init__() + self._reauth_entry: ConfigEntry | None = None + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + @property + def extra_authorize_data(self) -> dict[str, Any]: + """Extra data that needs to be appended to the authorize url.""" + return {"scope": SCOPE_VALUES} + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Dialog that informs the user that reauth is required.""" + if user_input is None: + return self.async_show_form(step_id="reauth_confirm") + return await self.async_step_user() + + async def async_oauth_create_entry(self, data: dict) -> FlowResult: + """Create an entry for Electric Kiwi.""" + existing_entry = await self.async_set_unique_id(DOMAIN) + if existing_entry: + self.hass.config_entries.async_update_entry(existing_entry, data=data) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + return await super().async_oauth_create_entry(data) diff --git a/homeassistant/components/electric_kiwi/const.py b/homeassistant/components/electric_kiwi/const.py new file mode 100644 index 00000000000000..907b62471720ba --- /dev/null +++ b/homeassistant/components/electric_kiwi/const.py @@ -0,0 +1,11 @@ +"""Constants for the Electric Kiwi integration.""" + +NAME = "Electric Kiwi" +DOMAIN = "electric_kiwi" +ATTRIBUTION = "Data provided by the Juice Hacker API" + +OAUTH2_AUTHORIZE = "https://welcome.electrickiwi.co.nz/oauth/authorize" +OAUTH2_TOKEN = "https://welcome.electrickiwi.co.nz/oauth/token" +API_BASE_URL = "https://api.electrickiwi.co.nz" + +SCOPE_VALUES = "read_connection_detail read_billing_frequency read_account_running_balance read_consumption_summary read_consumption_averages read_hop_intervals_config read_hop_connection save_hop_connection read_session" diff --git a/homeassistant/components/electric_kiwi/coordinator.py b/homeassistant/components/electric_kiwi/coordinator.py new file mode 100644 index 00000000000000..3e0ba997cd412a --- /dev/null +++ b/homeassistant/components/electric_kiwi/coordinator.py @@ -0,0 +1,81 @@ +"""Electric Kiwi coordinators.""" +from collections import OrderedDict +from datetime import timedelta +import logging + +import async_timeout +from electrickiwi_api import ElectricKiwiApi +from electrickiwi_api.exceptions import ApiException, AuthException +from electrickiwi_api.model import Hop, HopIntervals + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +_LOGGER = logging.getLogger(__name__) + +HOP_SCAN_INTERVAL = timedelta(hours=2) + + +class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]): + """ElectricKiwi Data object.""" + + def __init__(self, hass: HomeAssistant, ek_api: ElectricKiwiApi) -> None: + """Initialize ElectricKiwiAccountDataCoordinator.""" + super().__init__( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="Electric Kiwi HOP Data", + # Polling interval. Will only be polled if there are subscribers. + update_interval=HOP_SCAN_INTERVAL, + ) + self._ek_api = ek_api + self.hop_intervals: HopIntervals | None = None + + def get_hop_options(self) -> dict[str, int]: + """Get the hop interval options for selection.""" + if self.hop_intervals is not None: + return { + f"{v.start_time} - {v.end_time}": k + for k, v in self.hop_intervals.intervals.items() + } + return {} + + async def async_update_hop(self, hop_interval: int) -> Hop: + """Update selected hop and data.""" + try: + self.async_set_updated_data(await self._ek_api.post_hop(hop_interval)) + except AuthException as auth_err: + raise ConfigEntryAuthFailed from auth_err + except ApiException as api_err: + raise UpdateFailed( + f"Error communicating with EK API: {api_err}" + ) from api_err + + return self.data + + async def _async_update_data(self) -> Hop: + """Fetch data from API endpoint. + + filters the intervals to remove ones that are not active + """ + try: + async with async_timeout.timeout(60): + if self.hop_intervals is None: + hop_intervals: HopIntervals = await self._ek_api.get_hop_intervals() + hop_intervals.intervals = OrderedDict( + filter( + lambda pair: pair[1].active == 1, + hop_intervals.intervals.items(), + ) + ) + + self.hop_intervals = hop_intervals + return await self._ek_api.get_hop() + except AuthException as auth_err: + raise ConfigEntryAuthFailed from auth_err + except ApiException as api_err: + raise UpdateFailed( + f"Error communicating with EK API: {api_err}" + ) from api_err diff --git a/homeassistant/components/electric_kiwi/manifest.json b/homeassistant/components/electric_kiwi/manifest.json new file mode 100644 index 00000000000000..8ddb4c1af7c44d --- /dev/null +++ b/homeassistant/components/electric_kiwi/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "electric_kiwi", + "name": "Electric Kiwi", + "codeowners": ["@mikey0000"], + "config_flow": true, + "dependencies": ["application_credentials"], + "documentation": "https://www.home-assistant.io/integrations/electric_kiwi", + "integration_type": "hub", + "iot_class": "cloud_polling", + "requirements": ["electrickiwi-api==0.8.5"] +} diff --git a/homeassistant/components/electric_kiwi/oauth2.py b/homeassistant/components/electric_kiwi/oauth2.py new file mode 100644 index 00000000000000..ce3e473159a568 --- /dev/null +++ b/homeassistant/components/electric_kiwi/oauth2.py @@ -0,0 +1,76 @@ +"""OAuth2 implementations for Toon.""" +from __future__ import annotations + +import base64 +from typing import Any, cast + +from homeassistant.components.application_credentials import ( + AuthImplementation, + AuthorizationServer, + ClientCredential, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import SCOPE_VALUES + + +class ElectricKiwiLocalOAuth2Implementation(AuthImplementation): + """Local OAuth2 implementation for Electric Kiwi.""" + + def __init__( + self, + hass: HomeAssistant, + domain: str, + client_credential: ClientCredential, + authorization_server: AuthorizationServer, + ) -> None: + """Set up Electric Kiwi oauth.""" + super().__init__( + hass=hass, + auth_domain=domain, + credential=client_credential, + authorization_server=authorization_server, + ) + + self._name = client_credential.name + + @property + def extra_authorize_data(self) -> dict[str, Any]: + """Extra data that needs to be appended to the authorize url.""" + return {"scope": SCOPE_VALUES} + + async def async_resolve_external_data(self, external_data: Any) -> dict: + """Initialize local Electric Kiwi auth implementation.""" + data = { + "grant_type": "authorization_code", + "code": external_data["code"], + "redirect_uri": external_data["state"]["redirect_uri"], + } + + return await self._token_request(data) + + async def _async_refresh_token(self, token: dict) -> dict: + """Refresh tokens.""" + data = { + "grant_type": "refresh_token", + "refresh_token": token["refresh_token"], + } + + new_token = await self._token_request(data) + return {**token, **new_token} + + async def _token_request(self, data: dict) -> dict: + """Make a token request.""" + session = async_get_clientsession(self.hass) + client_str = f"{self.client_id}:{self.client_secret}" + client_string_bytes = client_str.encode("ascii") + + base64_bytes = base64.b64encode(client_string_bytes) + base64_client = base64_bytes.decode("ascii") + headers = {"Authorization": f"Basic {base64_client}"} + + resp = await session.post(self.token_url, data=data, headers=headers) + resp.raise_for_status() + resp_json = cast(dict, await resp.json()) + return resp_json diff --git a/homeassistant/components/electric_kiwi/sensor.py b/homeassistant/components/electric_kiwi/sensor.py new file mode 100644 index 00000000000000..4f32f237c00cab --- /dev/null +++ b/homeassistant/components/electric_kiwi/sensor.py @@ -0,0 +1,113 @@ +"""Support for Electric Kiwi sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime, timedelta +import logging + +from electrickiwi_api.model import Hop + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import dt as dt_util + +from .const import ATTRIBUTION, DOMAIN +from .coordinator import ElectricKiwiHOPDataCoordinator + +_LOGGER = logging.getLogger(DOMAIN) + +ATTR_EK_HOP_START = "hop_sensor_start" +ATTR_EK_HOP_END = "hop_sensor_end" + + +@dataclass +class ElectricKiwiHOPRequiredKeysMixin: + """Mixin for required HOP keys.""" + + value_func: Callable[[Hop], datetime] + + +@dataclass +class ElectricKiwiHOPSensorEntityDescription( + SensorEntityDescription, + ElectricKiwiHOPRequiredKeysMixin, +): + """Describes Electric Kiwi HOP sensor entity.""" + + +def _check_and_move_time(hop: Hop, time: str) -> datetime: + """Return the time a day forward if HOP end_time is in the past.""" + date_time = datetime.combine( + datetime.today(), + datetime.strptime(time, "%I:%M %p").time(), + ).astimezone(dt_util.DEFAULT_TIME_ZONE) + + end_time = datetime.combine( + datetime.today(), + datetime.strptime(hop.end.end_time, "%I:%M %p").time(), + ).astimezone(dt_util.DEFAULT_TIME_ZONE) + + if end_time < datetime.now().astimezone(dt_util.DEFAULT_TIME_ZONE): + return date_time + timedelta(days=1) + return date_time + + +HOP_SENSOR_TYPE: tuple[ElectricKiwiHOPSensorEntityDescription, ...] = ( + ElectricKiwiHOPSensorEntityDescription( + key=ATTR_EK_HOP_START, + translation_key="hopfreepowerstart", + device_class=SensorDeviceClass.TIMESTAMP, + value_func=lambda hop: _check_and_move_time(hop, hop.start.start_time), + ), + ElectricKiwiHOPSensorEntityDescription( + key=ATTR_EK_HOP_END, + translation_key="hopfreepowerend", + device_class=SensorDeviceClass.TIMESTAMP, + value_func=lambda hop: _check_and_move_time(hop, hop.end.end_time), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Electric Kiwi Sensor Setup.""" + hop_coordinator: ElectricKiwiHOPDataCoordinator = hass.data[DOMAIN][entry.entry_id] + hop_entities = [ + ElectricKiwiHOPEntity(hop_coordinator, description) + for description in HOP_SENSOR_TYPE + ] + async_add_entities(hop_entities) + + +class ElectricKiwiHOPEntity( + CoordinatorEntity[ElectricKiwiHOPDataCoordinator], SensorEntity +): + """Entity object for Electric Kiwi sensor.""" + + entity_description: ElectricKiwiHOPSensorEntityDescription + _attr_attribution = ATTRIBUTION + + def __init__( + self, + hop_coordinator: ElectricKiwiHOPDataCoordinator, + description: ElectricKiwiHOPSensorEntityDescription, + ) -> None: + """Entity object for Electric Kiwi sensor.""" + super().__init__(hop_coordinator) + + self._attr_unique_id = f"{self.coordinator._ek_api.customer_number}_{self.coordinator._ek_api.connection_id}_{description.key}" + self.entity_description = description + + @property + def native_value(self) -> datetime: + """Return the state of the sensor.""" + return self.entity_description.value_func(self.coordinator.data) diff --git a/homeassistant/components/electric_kiwi/strings.json b/homeassistant/components/electric_kiwi/strings.json new file mode 100644 index 00000000000000..19056180f17148 --- /dev/null +++ b/homeassistant/components/electric_kiwi/strings.json @@ -0,0 +1,36 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Electric Kiwi integration needs to re-authenticate your account" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" + } + }, + "entity": { + "sensor": { + "hopfreepowerstart": { + "name": "Hour of free power start" + }, + "hopfreepowerend": { + "name": "Hour of free power end" + } + } + } +} diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index d1b330b5dbe726..78c98bcc03d30d 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -4,6 +4,7 @@ """ APPLICATION_CREDENTIALS = [ + "electric_kiwi", "geocaching", "google", "google_assistant_sdk", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6d9a132a29a8eb..7283b187ba08f3 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -117,6 +117,7 @@ "efergy", "eight_sleep", "electrasmart", + "electric_kiwi", "elgato", "elkm1", "elmax", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 99566340ccdb65..18e7f1c22e175c 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1328,6 +1328,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "electric_kiwi": { + "name": "Electric Kiwi", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "elgato": { "name": "Elgato", "integrations": { diff --git a/mypy.ini b/mypy.ini index 66568cf5400129..7d1ec19c4d597d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -842,6 +842,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.electric_kiwi.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.elgato.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index d4836ea1522252..f64d8f8e66b922 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -702,6 +702,9 @@ ebusdpy==0.0.17 # homeassistant.components.ecoal_boiler ecoaliface==0.4.0 +# homeassistant.components.electric_kiwi +electrickiwi-api==0.8.5 + # homeassistant.components.elgato elgato==4.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d3a9d3819f679..440d4a22c0c605 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -564,6 +564,9 @@ eagle100==0.1.1 # homeassistant.components.easyenergy easyenergy==0.3.0 +# homeassistant.components.electric_kiwi +electrickiwi-api==0.8.5 + # homeassistant.components.elgato elgato==4.0.1 diff --git a/tests/components/electric_kiwi/__init__.py b/tests/components/electric_kiwi/__init__.py new file mode 100644 index 00000000000000..7f5e08a56b5e6d --- /dev/null +++ b/tests/components/electric_kiwi/__init__.py @@ -0,0 +1 @@ +"""Tests for the Electric Kiwi integration.""" diff --git a/tests/components/electric_kiwi/conftest.py b/tests/components/electric_kiwi/conftest.py new file mode 100644 index 00000000000000..525f5742382973 --- /dev/null +++ b/tests/components/electric_kiwi/conftest.py @@ -0,0 +1,63 @@ +"""Define fixtures for electric kiwi tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.electric_kiwi.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" +REDIRECT_URI = "https://example.com/auth/external/callback" + + +@pytest.fixture(autouse=True) +async def request_setup(current_request_with_host) -> None: + """Request setup.""" + return + + +@pytest.fixture +async def setup_credentials(hass: HomeAssistant) -> None: + """Fixture to setup credentials.""" + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential(CLIENT_ID, CLIENT_SECRET), + ) + + +@pytest.fixture(name="config_entry") +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Create mocked config entry.""" + entry = MockConfigEntry( + title="Electric Kiwi", + domain=DOMAIN, + data={ + "id": "mock_user", + "auth_implementation": DOMAIN, + }, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + return entry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.electric_kiwi.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup diff --git a/tests/components/electric_kiwi/test_config_flow.py b/tests/components/electric_kiwi/test_config_flow.py new file mode 100644 index 00000000000000..51d00722341f53 --- /dev/null +++ b/tests/components/electric_kiwi/test_config_flow.py @@ -0,0 +1,187 @@ +"""Test the Electric Kiwi config flow.""" +from __future__ import annotations + +from http import HTTPStatus +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from homeassistant import config_entries +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.electric_kiwi.const import ( + DOMAIN, + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, + SCOPE_VALUES, +) +from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import config_entry_oauth2_flow + +from .conftest import CLIENT_ID, CLIENT_SECRET, REDIRECT_URI + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker +from tests.typing import ClientSessionGenerator + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +async def test_config_flow_no_credentials(hass: HomeAssistant) -> None: + """Test config flow base case with no credentials registered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "missing_credentials" + + +async def test_full_flow( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + setup_credentials, + mock_setup_entry: AsyncMock, +) -> None: + """Check full flow.""" + await async_import_client_credential( + hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "imported-cred" + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER, "entry_id": DOMAIN} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": REDIRECT_URI, + }, + ) + + URL_SCOPE = SCOPE_VALUES.replace(" ", "+") + + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + f"&redirect_uri={REDIRECT_URI}" + f"&state={state}" + f"&scope={URL_SCOPE}" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_existing_entry( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + setup_credentials: None, + config_entry: MockConfigEntry, +) -> None: + """Check existing entry.""" + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER, "entry_id": DOMAIN} + ) + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": OAUTH2_AUTHORIZE, + }, + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "access_token": "mock-access-token", + "token_type": "bearer", + "expires_in": 3599, + "refresh_token": "mock-refresh_token", + }, + ) + + await hass.config_entries.flow.async_configure(result["flow_id"]) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + +async def test_reauthentication( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + current_request_with_host: None, + aioclient_mock: AiohttpClientMocker, + mock_setup_entry: MagicMock, + config_entry: MockConfigEntry, + setup_credentials: None, +) -> None: + """Test Electric Kiwi reauthentication.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_REAUTH, "entry_id": DOMAIN} + ) + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert "flow_id" in flows[0] + + result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": REDIRECT_URI, + }, + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "access_token": "mock-access-token", + "token_type": "bearer", + "expires_in": 3599, + "refresh_token": "mock-refresh_token", + }, + ) + + await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup_entry.mock_calls) == 1 From 74deb8b011f388c60d25f85028a2fefa1f3d7465 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 25 Jul 2023 11:04:05 +0200 Subject: [PATCH 0901/1009] Add datetime platform to KNX (#97190) --- homeassistant/components/knx/__init__.py | 2 + homeassistant/components/knx/const.py | 1 + homeassistant/components/knx/datetime.py | 103 +++++++++++++++++++++++ homeassistant/components/knx/schema.py | 19 +++++ tests/components/knx/test_datetime.py | 89 ++++++++++++++++++++ 5 files changed, 214 insertions(+) create mode 100644 homeassistant/components/knx/datetime.py create mode 100644 tests/components/knx/test_datetime.py diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index f0ee9576cc79bc..1bb6d9bbdd2dd7 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -81,6 +81,7 @@ ClimateSchema, CoverSchema, DateSchema, + DateTimeSchema, EventSchema, ExposeSchema, FanSchema, @@ -138,6 +139,7 @@ **ClimateSchema.platform_node(), **CoverSchema.platform_node(), **DateSchema.platform_node(), + **DateTimeSchema.platform_node(), **FanSchema.platform_node(), **LightSchema.platform_node(), **NotifySchema.platform_node(), diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index c96f10736ddd8c..519d5d0742d2b0 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -128,6 +128,7 @@ class ColorTempModes(Enum): Platform.CLIMATE, Platform.COVER, Platform.DATE, + Platform.DATETIME, Platform.FAN, Platform.LIGHT, Platform.NOTIFY, diff --git a/homeassistant/components/knx/datetime.py b/homeassistant/components/knx/datetime.py new file mode 100644 index 00000000000000..fc63df04233e0c --- /dev/null +++ b/homeassistant/components/knx/datetime.py @@ -0,0 +1,103 @@ +"""Support for KNX/IP datetime.""" +from __future__ import annotations + +from datetime import datetime + +from xknx import XKNX +from xknx.devices import DateTime as XknxDateTime + +from homeassistant import config_entries +from homeassistant.components.datetime import DateTimeEntity +from homeassistant.const import ( + CONF_ENTITY_CATEGORY, + CONF_NAME, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType +import homeassistant.util.dt as dt_util + +from .const import ( + CONF_RESPOND_TO_READ, + CONF_STATE_ADDRESS, + CONF_SYNC_STATE, + DATA_KNX_CONFIG, + DOMAIN, + KNX_ADDRESS, +) +from .knx_entity import KnxEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up entities for KNX platform.""" + xknx: XKNX = hass.data[DOMAIN].xknx + config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.DATETIME] + + async_add_entities(KNXDateTime(xknx, entity_config) for entity_config in config) + + +def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTime: + """Return a XKNX DateTime object to be used within XKNX.""" + return XknxDateTime( + xknx, + name=config[CONF_NAME], + broadcast_type="DATETIME", + localtime=False, + group_address=config[KNX_ADDRESS], + group_address_state=config.get(CONF_STATE_ADDRESS), + respond_to_read=config[CONF_RESPOND_TO_READ], + sync_state=config[CONF_SYNC_STATE], + ) + + +class KNXDateTime(KnxEntity, DateTimeEntity, RestoreEntity): + """Representation of a KNX datetime.""" + + _device: XknxDateTime + + def __init__(self, xknx: XKNX, config: ConfigType) -> None: + """Initialize a KNX time.""" + super().__init__(_create_xknx_device(xknx, config)) + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) + self._attr_unique_id = str(self._device.remote_value.group_address) + + async def async_added_to_hass(self) -> None: + """Restore last state.""" + await super().async_added_to_hass() + if ( + not self._device.remote_value.readable + and (last_state := await self.async_get_last_state()) is not None + and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) + ): + self._device.remote_value.value = ( + datetime.fromisoformat(last_state.state) + .astimezone(dt_util.DEFAULT_TIME_ZONE) + .timetuple() + ) + + @property + def native_value(self) -> datetime | None: + """Return the latest value.""" + if (time_struct := self._device.remote_value.value) is None: + return None + return datetime( + year=time_struct.tm_year, + month=time_struct.tm_mon, + day=time_struct.tm_mday, + hour=time_struct.tm_hour, + minute=time_struct.tm_min, + second=min(time_struct.tm_sec, 59), # account for leap seconds + tzinfo=dt_util.DEFAULT_TIME_ZONE, + ) + + async def async_set_value(self, value: datetime) -> None: + """Change the value.""" + await self._device.set(value.astimezone(dt_util.DEFAULT_TIME_ZONE).timetuple()) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 40cc2232d8f3bd..8240fbaf3c13f7 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -575,6 +575,25 @@ class DateSchema(KNXPlatformSchema): ) +class DateTimeSchema(KNXPlatformSchema): + """Voluptuous schema for KNX date.""" + + PLATFORM = Platform.DATETIME + + DEFAULT_NAME = "KNX DateTime" + + ENTITY_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean, + vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, + vol.Required(KNX_ADDRESS): ga_list_validator, + vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + } + ) + + class ExposeSchema(KNXPlatformSchema): """Voluptuous schema for KNX exposures.""" diff --git a/tests/components/knx/test_datetime.py b/tests/components/knx/test_datetime.py new file mode 100644 index 00000000000000..f9d9f039367569 --- /dev/null +++ b/tests/components/knx/test_datetime.py @@ -0,0 +1,89 @@ +"""Test KNX date.""" +from homeassistant.components.datetime import ATTR_DATETIME, DOMAIN, SERVICE_SET_VALUE +from homeassistant.components.knx.const import CONF_RESPOND_TO_READ, KNX_ADDRESS +from homeassistant.components.knx.schema import DateTimeSchema +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant, State + +from .conftest import KNXTestKit + +from tests.common import mock_restore_cache + +# KNX DPT 19.001 doesn't provide timezone information so we send local time + + +async def test_datetime(hass: HomeAssistant, knx: KNXTestKit) -> None: + """Test KNX datetime.""" + # default timezone in tests is US/Pacific + test_address = "1/1/1" + await knx.setup_integration( + { + DateTimeSchema.PLATFORM: { + CONF_NAME: "test", + KNX_ADDRESS: test_address, + } + } + ) + # set value + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {"entity_id": "datetime.test", ATTR_DATETIME: "2020-01-02T03:04:05+00:00"}, + blocking=True, + ) + await knx.assert_write( + test_address, + (0x78, 0x01, 0x01, 0x73, 0x04, 0x05, 0x20, 0x80), + ) + state = hass.states.get("datetime.test") + assert state.state == "2020-01-02T03:04:05+00:00" + + # update from KNX + await knx.receive_write( + test_address, + (0x7B, 0x07, 0x19, 0x49, 0x28, 0x08, 0x00, 0x00), + ) + state = hass.states.get("datetime.test") + assert state.state == "2023-07-25T16:40:08+00:00" + + +async def test_date_restore_and_respond(hass: HomeAssistant, knx: KNXTestKit) -> None: + """Test KNX datetime with passive_address, restoring state and respond_to_read.""" + hass.config.set_time_zone("Europe/Vienna") + test_address = "1/1/1" + test_passive_address = "3/3/3" + fake_state = State("datetime.test", "2022-03-03T03:04:05+00:00") + mock_restore_cache(hass, (fake_state,)) + + await knx.setup_integration( + { + DateTimeSchema.PLATFORM: { + CONF_NAME: "test", + KNX_ADDRESS: [test_address, test_passive_address], + CONF_RESPOND_TO_READ: True, + } + } + ) + # restored state - doesn't send telegram + state = hass.states.get("datetime.test") + assert state.state == "2022-03-03T03:04:05+00:00" + await knx.assert_telegram_count(0) + + # respond with restored state + await knx.receive_read(test_address) + await knx.assert_response( + test_address, + (0x7A, 0x03, 0x03, 0x84, 0x04, 0x05, 0x20, 0x80), + ) + + # don't respond to passive address + await knx.receive_read(test_passive_address) + await knx.assert_no_telegram() + + # update from KNX passive address + await knx.receive_write( + test_passive_address, + (0x78, 0x01, 0x01, 0x73, 0x04, 0x05, 0x20, 0x80), + ) + state = hass.states.get("datetime.test") + assert state.state == "2020-01-01T18:04:05+00:00" From fc41f3d25be967f60fa16b0df4e19714976e70ad Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 25 Jul 2023 09:13:52 +0000 Subject: [PATCH 0902/1009] Use device class ENUM for Tractive tracker state sensor (#97191) --- homeassistant/components/tractive/sensor.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index 8f56d1a2e9c52d..b127bf8d1d7f53 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -172,11 +172,18 @@ async def async_added_to_hass(self) -> None: entity_category=EntityCategory.DIAGNOSTIC, ), TractiveSensorEntityDescription( - # Currently, only state operational and not_reporting are used - # More states are available by polling the data key=ATTR_TRACKER_STATE, translation_key="tracker_state", entity_class=TractiveHardwareSensor, + icon="mdi:radar", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.ENUM, + options=[ + "not_reporting", + "operational", + "system_shutdown_user", + "system_startup", + ], ), TractiveSensorEntityDescription( key=ATTR_MINUTES_ACTIVE, From 7f049c5b2077a54923e5cb68f87337d2f5ae9d43 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Tue, 25 Jul 2023 11:16:00 +0200 Subject: [PATCH 0903/1009] Add the Duotecno intergration (#96399) Co-authored-by: Isak Nyberg <36712644+IsakNyberg@users.noreply.github.com> Co-authored-by: Franck Nijhof --- .coveragerc | 3 + CODEOWNERS | 2 + homeassistant/components/duotecno/__init__.py | 37 ++++++++ .../components/duotecno/config_flow.py | 61 +++++++++++++ homeassistant/components/duotecno/const.py | 3 + homeassistant/components/duotecno/entity.py | 36 ++++++++ .../components/duotecno/manifest.json | 9 ++ .../components/duotecno/strings.json | 18 ++++ homeassistant/components/duotecno/switch.py | 50 +++++++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/duotecno/__init__.py | 1 + tests/components/duotecno/conftest.py | 14 +++ tests/components/duotecno/test_config_flow.py | 89 +++++++++++++++++++ 16 files changed, 336 insertions(+) create mode 100644 homeassistant/components/duotecno/__init__.py create mode 100644 homeassistant/components/duotecno/config_flow.py create mode 100644 homeassistant/components/duotecno/const.py create mode 100644 homeassistant/components/duotecno/entity.py create mode 100644 homeassistant/components/duotecno/manifest.json create mode 100644 homeassistant/components/duotecno/strings.json create mode 100644 homeassistant/components/duotecno/switch.py create mode 100644 tests/components/duotecno/__init__.py create mode 100644 tests/components/duotecno/conftest.py create mode 100644 tests/components/duotecno/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 30f768e01a40e7..6bc69125c303d2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -230,6 +230,9 @@ omit = homeassistant/components/dublin_bus_transport/sensor.py homeassistant/components/dunehd/__init__.py homeassistant/components/dunehd/media_player.py + homeassistant/components/duotecno/__init__.py + homeassistant/components/duotecno/entity.py + homeassistant/components/duotecno/switch.py homeassistant/components/dwd_weather_warnings/const.py homeassistant/components/dwd_weather_warnings/coordinator.py homeassistant/components/dwd_weather_warnings/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index ef9634e1527d10..f09785a778160c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -297,6 +297,8 @@ build.json @home-assistant/supervisor /tests/components/dsmr_reader/ @depl0y @glodenox /homeassistant/components/dunehd/ @bieniu /tests/components/dunehd/ @bieniu +/homeassistant/components/duotecno/ @cereal2nd +/tests/components/duotecno/ @cereal2nd /homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 @andarotajo /tests/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 @andarotajo /homeassistant/components/dynalite/ @ziv1234 diff --git a/homeassistant/components/duotecno/__init__.py b/homeassistant/components/duotecno/__init__.py new file mode 100644 index 00000000000000..a1cf1c907a6c58 --- /dev/null +++ b/homeassistant/components/duotecno/__init__.py @@ -0,0 +1,37 @@ +"""The duotecno integration.""" +from __future__ import annotations + +from duotecno.controller import PyDuotecno +from duotecno.exceptions import InvalidPassword, LoadFailure + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SWITCH] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up duotecno from a config entry.""" + + controller = PyDuotecno() + try: + await controller.connect( + entry.data[CONF_HOST], entry.data[CONF_PORT], entry.data[CONF_PASSWORD] + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + except (OSError, InvalidPassword, LoadFailure) as err: + raise ConfigEntryNotReady from err + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = controller + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/duotecno/config_flow.py b/homeassistant/components/duotecno/config_flow.py new file mode 100644 index 00000000000000..37087d4ea1add8 --- /dev/null +++ b/homeassistant/components/duotecno/config_flow.py @@ -0,0 +1,61 @@ +"""Config flow for duotecno integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from duotecno.controller import PyDuotecno +from duotecno.exceptions import InvalidPassword +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_PORT): int, + } +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for duotecno.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + try: + controller = PyDuotecno() + await controller.connect( + user_input[CONF_HOST], + user_input[CONF_PORT], + user_input[CONF_PASSWORD], + True, + ) + except ConnectionError: + errors["base"] = "cannot_connect" + except InvalidPassword: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=f"{user_input[CONF_HOST]}", data=user_input + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/duotecno/const.py b/homeassistant/components/duotecno/const.py new file mode 100644 index 00000000000000..114867b8d95219 --- /dev/null +++ b/homeassistant/components/duotecno/const.py @@ -0,0 +1,3 @@ +"""Constants for the duotecno integration.""" + +DOMAIN = "duotecno" diff --git a/homeassistant/components/duotecno/entity.py b/homeassistant/components/duotecno/entity.py new file mode 100644 index 00000000000000..f1c72aa55c432f --- /dev/null +++ b/homeassistant/components/duotecno/entity.py @@ -0,0 +1,36 @@ +"""Support for Velbus devices.""" +from __future__ import annotations + +from duotecno.unit import BaseUnit + +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import DOMAIN + + +class DuotecnoEntity(Entity): + """Representation of a Duotecno entity.""" + + _attr_should_poll: bool = False + _unit: BaseUnit + + def __init__(self, unit) -> None: + """Initialize a Duotecno entity.""" + self._unit = unit + self._attr_name = unit.get_name() + self._attr_device_info = DeviceInfo( + identifiers={ + (DOMAIN, str(unit.get_node_address())), + }, + manufacturer="Duotecno", + name=unit.get_node_name(), + ) + self._attr_unique_id = f"{unit.get_node_address()}-{unit.get_number()}" + + async def async_added_to_hass(self) -> None: + """When added to hass.""" + self._unit.on_status_update(self._on_update) + + async def _on_update(self) -> None: + """When a unit has an update.""" + self.async_write_ha_state() diff --git a/homeassistant/components/duotecno/manifest.json b/homeassistant/components/duotecno/manifest.json new file mode 100644 index 00000000000000..a630a3dedbd477 --- /dev/null +++ b/homeassistant/components/duotecno/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "duotecno", + "name": "duotecno", + "codeowners": ["@cereal2nd"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/duotecno", + "iot_class": "local_push", + "requirements": ["pyduotecno==2023.7.3"] +} diff --git a/homeassistant/components/duotecno/strings.json b/homeassistant/components/duotecno/strings.json new file mode 100644 index 00000000000000..379291eb626330 --- /dev/null +++ b/homeassistant/components/duotecno/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "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%]" + } + } +} diff --git a/homeassistant/components/duotecno/switch.py b/homeassistant/components/duotecno/switch.py new file mode 100644 index 00000000000000..a9921de85d3e19 --- /dev/null +++ b/homeassistant/components/duotecno/switch.py @@ -0,0 +1,50 @@ +"""Support for Duotecno switches.""" +from typing import Any + +from duotecno.unit import SwitchUnit + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import DuotecnoEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Velbus switch based on config_entry.""" + cntrl = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + DuotecnoSwitch(channel) for channel in cntrl.get_units("SwitchUnit") + ) + + +class DuotecnoSwitch(DuotecnoEntity, SwitchEntity): + """Representation of a switch.""" + + _unit: SwitchUnit + + @property + def is_on(self) -> bool: + """Return true if the switch is on.""" + return self._unit.is_on() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Instruct the switch to turn on.""" + try: + await self._unit.turn_on() + except OSError as err: + raise HomeAssistantError("Transmit for the turn_on packet failed") from err + + async def async_turn_off(self, **kwargs: Any) -> None: + """Instruct the switch to turn off.""" + try: + await self._unit.turn_off() + except OSError as err: + raise HomeAssistantError("Transmit for the turn_off packet failed") from err diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7283b187ba08f3..b4b9c409c6e45c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -106,6 +106,7 @@ "dsmr", "dsmr_reader", "dunehd", + "duotecno", "dwd_weather_warnings", "dynalite", "eafm", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 18e7f1c22e175c..ebe16947a51352 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1220,6 +1220,12 @@ "config_flow": true, "iot_class": "local_polling" }, + "duotecno": { + "name": "duotecno", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_push" + }, "dwd_weather_warnings": { "name": "Deutscher Wetterdienst (DWD) Weather Warnings", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index f64d8f8e66b922..617a266421708c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1646,6 +1646,9 @@ pydrawise==2023.7.0 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 +# homeassistant.components.duotecno +pyduotecno==2023.7.3 + # homeassistant.components.ebox pyebox==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 440d4a22c0c605..650a78954de912 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1219,6 +1219,9 @@ pydiscovergy==1.2.1 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 +# homeassistant.components.duotecno +pyduotecno==2023.7.3 + # homeassistant.components.econet pyeconet==0.1.20 diff --git a/tests/components/duotecno/__init__.py b/tests/components/duotecno/__init__.py new file mode 100644 index 00000000000000..9cb20bcaec6e32 --- /dev/null +++ b/tests/components/duotecno/__init__.py @@ -0,0 +1 @@ +"""Tests for the duotecno integration.""" diff --git a/tests/components/duotecno/conftest.py b/tests/components/duotecno/conftest.py new file mode 100644 index 00000000000000..82c3e0c7f44031 --- /dev/null +++ b/tests/components/duotecno/conftest.py @@ -0,0 +1,14 @@ +"""Common fixtures for the duotecno tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.duotecno.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/duotecno/test_config_flow.py b/tests/components/duotecno/test_config_flow.py new file mode 100644 index 00000000000000..a2dc265ae6eb54 --- /dev/null +++ b/tests/components/duotecno/test_config_flow.py @@ -0,0 +1,89 @@ +"""Test the duotecno config flow.""" +from unittest.mock import AsyncMock, patch + +from duotecno.exceptions import InvalidPassword +import pytest + +from homeassistant import config_entries +from homeassistant.components.duotecno.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "duotecno.controller.PyDuotecno.connect", + return_value=None, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 1234, + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "1.1.1.1" + assert result2["data"] == { + "host": "1.1.1.1", + "port": 1234, + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("test_side_effect", "test_error"), + [ + (InvalidPassword, "invalid_auth"), + (ConnectionError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_invalid(hass: HomeAssistant, test_side_effect, test_error): + """Test all side_effects on the controller.connect via parameters.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("duotecno.controller.PyDuotecno.connect", side_effect=test_side_effect): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 1234, + "password": "test-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": test_error} + + with patch("duotecno.controller.PyDuotecno.connect"): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 1234, + "password": "test-password2", + }, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "1.1.1.1" + assert result2["data"] == { + "host": "1.1.1.1", + "port": 1234, + "password": "test-password2", + } From cd84a188ee13d7cb0f5c00a9d3d7ce715e5d79f2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 25 Jul 2023 09:59:11 +0000 Subject: [PATCH 0904/1009] Improve Tractive sensor names (#97192) * Improve entity names * Rename translation keys --- homeassistant/components/tractive/sensor.py | 4 ++-- homeassistant/components/tractive/strings.json | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index b127bf8d1d7f53..493b627f9b4008 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -187,7 +187,7 @@ async def async_added_to_hass(self) -> None: ), TractiveSensorEntityDescription( key=ATTR_MINUTES_ACTIVE, - translation_key="minutes_active", + translation_key="activity_time", icon="mdi:clock-time-eight-outline", native_unit_of_measurement=UnitOfTime.MINUTES, entity_class=TractiveActivitySensor, @@ -195,7 +195,7 @@ async def async_added_to_hass(self) -> None: ), TractiveSensorEntityDescription( key=ATTR_MINUTES_REST, - translation_key="minutes_rest", + translation_key="rest_time", icon="mdi:clock-time-eight-outline", native_unit_of_measurement=UnitOfTime.MINUTES, entity_class=TractiveWellnessSensor, diff --git a/homeassistant/components/tractive/strings.json b/homeassistant/components/tractive/strings.json index 44b0a497881072..4053d2658f5922 100644 --- a/homeassistant/components/tractive/strings.json +++ b/homeassistant/components/tractive/strings.json @@ -36,8 +36,8 @@ "daily_goal": { "name": "Daily goal" }, - "minutes_active": { - "name": "Minutes active" + "activity_time": { + "name": "Activity time" }, "minutes_day_sleep": { "name": "Day sleep" @@ -45,8 +45,8 @@ "minutes_night_sleep": { "name": "Night sleep" }, - "minutes_rest": { - "name": "Minutes rest" + "rest_time": { + "name": "Rest time" }, "tracker_battery_level": { "name": "Tracker battery" From 5e40fe97fdc0264fbc4e092654af290600d473c6 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 25 Jul 2023 12:14:22 +0200 Subject: [PATCH 0905/1009] Prevent duplicate Matter attribute event subscription (#97194) --- homeassistant/components/matter/entity.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index a30939912253c8..0082370d5ff429 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -78,6 +78,9 @@ async def async_added_to_hass(self) -> None: sub_paths: list[str] = [] for attr_cls in self._entity_info.attributes_to_watch: attr_path = self.get_matter_attribute_path(attr_cls) + if attr_path in sub_paths: + # prevent duplicate subscriptions + continue self._attributes_map[attr_cls] = attr_path sub_paths.append(attr_path) self._unsubscribes.append( From bb0727ab8a8548d97a8963b56781747ccd273b27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jul 2023 05:20:03 -0500 Subject: [PATCH 0906/1009] Bump home-assistant-bluetooth to 1.10.2 (#97193) --- homeassistant/components/bluetooth/update_coordinator.py | 3 +-- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py index ed2bfb5ffac06d..0c41b58c63d771 100644 --- a/homeassistant/components/bluetooth/update_coordinator.py +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -3,7 +3,6 @@ from abc import ABC, abstractmethod import logging -from typing import cast from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback @@ -70,7 +69,7 @@ def name(self) -> str: if service_info := async_last_service_info( self.hass, self.address, self.connectable ): - return cast(str, service_info.name) # for compat this can be a pyobjc + return service_info.name return self._last_name @property diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 031e64453f4e68..c3bd83679ae7a9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ fnv-hash-fast==0.4.0 ha-av==10.1.0 hass-nabucasa==0.69.0 hassil==1.2.0 -home-assistant-bluetooth==1.10.1 +home-assistant-bluetooth==1.10.2 home-assistant-frontend==20230705.1 home-assistant-intents==2023.7.24 httpx==0.24.1 diff --git a/pyproject.toml b/pyproject.toml index a7fd2e24ce53fa..1f179518fd974d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.24.1", - "home-assistant-bluetooth==1.10.1", + "home-assistant-bluetooth==1.10.2", "ifaddr==0.2.0", "Jinja2==3.1.2", "lru-dict==1.2.0", diff --git a/requirements.txt b/requirements.txt index 098cf402e73e6a..9f5023c9a1c063 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ bcrypt==4.0.1 certifi>=2021.5.30 ciso8601==2.3.0 httpx==0.24.1 -home-assistant-bluetooth==1.10.1 +home-assistant-bluetooth==1.10.2 ifaddr==0.2.0 Jinja2==3.1.2 lru-dict==1.2.0 From 6b41c324cc1cb3cc312b8160dd11d1e43db0bbba Mon Sep 17 00:00:00 2001 From: Michael Arthur Date: Tue, 25 Jul 2023 22:42:24 +1200 Subject: [PATCH 0907/1009] Fix broken translation keys (#97202) --- homeassistant/components/electric_kiwi/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/electric_kiwi/sensor.py b/homeassistant/components/electric_kiwi/sensor.py index 4f32f237c00cab..a657b768aa572c 100644 --- a/homeassistant/components/electric_kiwi/sensor.py +++ b/homeassistant/components/electric_kiwi/sensor.py @@ -94,6 +94,7 @@ class ElectricKiwiHOPEntity( """Entity object for Electric Kiwi sensor.""" entity_description: ElectricKiwiHOPSensorEntityDescription + _attr_has_entity_name = True _attr_attribution = ATTRIBUTION def __init__( From 6c43ce69d3189f3b38cd69cc6d7e1a944eb2fe8f Mon Sep 17 00:00:00 2001 From: Luke Date: Tue, 25 Jul 2023 05:29:48 -0600 Subject: [PATCH 0908/1009] Add time platform to Roborock (#94039) --- homeassistant/components/roborock/const.py | 1 + .../components/roborock/strings.json | 8 + homeassistant/components/roborock/time.py | 150 ++++++++++++++++++ tests/components/roborock/test_time.py | 39 +++++ 4 files changed, 198 insertions(+) create mode 100644 homeassistant/components/roborock/time.py create mode 100644 tests/components/roborock/test_time.py diff --git a/homeassistant/components/roborock/const.py b/homeassistant/components/roborock/const.py index e16ab3d91ae6f7..2fc59134d140f8 100644 --- a/homeassistant/components/roborock/const.py +++ b/homeassistant/components/roborock/const.py @@ -11,5 +11,6 @@ Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.TIME, Platform.NUMBER, ] diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 3989f08505b46f..cd629e208e3673 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -154,6 +154,14 @@ "name": "Status indicator light" } }, + "time": { + "dnd_start_time": { + "name": "Do not disturb begin" + }, + "dnd_end_time": { + "name": "Do not disturb end" + } + }, "vacuum": { "roborock": { "state_attributes": { diff --git a/homeassistant/components/roborock/time.py b/homeassistant/components/roborock/time.py new file mode 100644 index 00000000000000..514d147d46918e --- /dev/null +++ b/homeassistant/components/roborock/time.py @@ -0,0 +1,150 @@ +"""Support for Roborock time.""" +import asyncio +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +import datetime +from datetime import time +import logging +from typing import Any + +from roborock.api import AttributeCache +from roborock.command_cache import CacheableAttribute +from roborock.exceptions import RoborockException + +from homeassistant.components.time import TimeEntity, TimeEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify + +from .const import DOMAIN +from .coordinator import RoborockDataUpdateCoordinator +from .device import RoborockEntity + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class RoborockTimeDescriptionMixin: + """Define an entity description mixin for time entities.""" + + # Gets the status of the switch + cache_key: CacheableAttribute + # Sets the status of the switch + update_value: Callable[[AttributeCache, datetime.time], Coroutine[Any, Any, dict]] + # Attribute from cache + get_value: Callable[[AttributeCache], datetime.time] + + +@dataclass +class RoborockTimeDescription(TimeEntityDescription, RoborockTimeDescriptionMixin): + """Class to describe an Roborock time entity.""" + + +TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [ + RoborockTimeDescription( + key="dnd_start_time", + translation_key="dnd_start_time", + icon="mdi:bell-cancel", + cache_key=CacheableAttribute.dnd_timer, + update_value=lambda cache, desired_time: cache.update_value( + [ + desired_time.hour, + desired_time.minute, + cache.value.get("end_hour"), + cache.value.get("end_minute"), + ] + ), + get_value=lambda cache: datetime.time( + hour=cache.value.get("start_hour"), minute=cache.value.get("start_minute") + ), + entity_category=EntityCategory.CONFIG, + ), + RoborockTimeDescription( + key="dnd_end_time", + translation_key="dnd_end_time", + icon="mdi:bell-ring", + cache_key=CacheableAttribute.dnd_timer, + update_value=lambda cache, desired_time: cache.update_value( + [ + cache.value.get("start_hour"), + cache.value.get("start_minute"), + desired_time.hour, + desired_time.minute, + ] + ), + get_value=lambda cache: datetime.time( + hour=cache.value.get("end_hour"), minute=cache.value.get("end_minute") + ), + entity_category=EntityCategory.CONFIG, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Roborock time platform.""" + coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ + config_entry.entry_id + ] + possible_entities: list[ + tuple[RoborockDataUpdateCoordinator, RoborockTimeDescription] + ] = [ + (coordinator, description) + for coordinator in coordinators.values() + for description in TIME_DESCRIPTIONS + ] + # We need to check if this function is supported by the device. + results = await asyncio.gather( + *( + coordinator.api.cache.get(description.cache_key).async_value() + for coordinator, description in possible_entities + ), + return_exceptions=True, + ) + valid_entities: list[RoborockTimeEntity] = [] + for (coordinator, description), result in zip(possible_entities, results): + if result is None or isinstance(result, RoborockException): + _LOGGER.debug("Not adding entity because of %s", result) + else: + valid_entities.append( + RoborockTimeEntity( + f"{description.key}_{slugify(coordinator.roborock_device_info.device.duid)}", + coordinator, + description, + ) + ) + async_add_entities(valid_entities) + + +class RoborockTimeEntity(RoborockEntity, TimeEntity): + """A class to let you set options on a Roborock vacuum where the potential options are fixed.""" + + entity_description: RoborockTimeDescription + + def __init__( + self, + unique_id: str, + coordinator: RoborockDataUpdateCoordinator, + entity_description: RoborockTimeDescription, + ) -> None: + """Create a time entity.""" + self.entity_description = entity_description + super().__init__(unique_id, coordinator.device_info, coordinator.api) + + @property + def native_value(self) -> time | None: + """Return the value reported by the time.""" + return self.entity_description.get_value( + self.get_cache(self.entity_description.cache_key) + ) + + async def async_set_value(self, value: time) -> None: + """Set the time.""" + await self.entity_description.update_value( + self.get_cache(self.entity_description.cache_key), value + ) diff --git a/tests/components/roborock/test_time.py b/tests/components/roborock/test_time.py new file mode 100644 index 00000000000000..6ba996ca23ffb5 --- /dev/null +++ b/tests/components/roborock/test_time.py @@ -0,0 +1,39 @@ +"""Test Roborock Time platform.""" +from datetime import time +from unittest.mock import patch + +import pytest + +from homeassistant.components.time import SERVICE_SET_VALUE +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("entity_id"), + [ + ("time.roborock_s7_maxv_do_not_disturb_begin"), + ("time.roborock_s7_maxv_do_not_disturb_end"), + ], +) +async def test_update_success( + hass: HomeAssistant, + bypass_api_fixture, + setup_entry: MockConfigEntry, + 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: + await hass.services.async_call( + "time", + SERVICE_SET_VALUE, + service_data={"time": time(hour=1, minute=1)}, + blocking=True, + target={"entity_id": entity_id}, + ) + assert mock_send_message.assert_called_once From fb00cd8963a6153ea26e71b343018284ed50c238 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 25 Jul 2023 13:33:02 +0200 Subject: [PATCH 0909/1009] Add turn on/off support for mqtt water_heater (#97197) --- homeassistant/components/mqtt/climate.py | 5 +- homeassistant/components/mqtt/const.py | 2 + homeassistant/components/mqtt/water_heater.py | 25 ++++ tests/components/mqtt/test_water_heater.py | 109 ++++++++++++++++++ 4 files changed, 138 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index f29a114620ace8..f45d2852df0b25 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -64,6 +64,8 @@ CONF_MODE_LIST, CONF_MODE_STATE_TEMPLATE, CONF_MODE_STATE_TOPIC, + CONF_POWER_COMMAND_TEMPLATE, + CONF_POWER_COMMAND_TOPIC, CONF_PRECISION, CONF_QOS, CONF_RETAIN, @@ -113,9 +115,6 @@ # was removed in HA Core 2023.8 CONF_POWER_STATE_TEMPLATE = "power_state_template" CONF_POWER_STATE_TOPIC = "power_state_topic" - -CONF_POWER_COMMAND_TOPIC = "power_command_topic" -CONF_POWER_COMMAND_TEMPLATE = "power_command_template" CONF_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic" CONF_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic" CONF_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template" diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index fb1989069af1df..fcdfeb4bd7df59 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -40,6 +40,8 @@ CONF_MODE_LIST = "modes" CONF_MODE_STATE_TEMPLATE = "mode_state_template" CONF_MODE_STATE_TOPIC = "mode_state_topic" +CONF_POWER_COMMAND_TOPIC = "power_command_topic" +CONF_POWER_COMMAND_TEMPLATE = "power_command_template" CONF_PRECISION = "precision" CONF_TEMP_COMMAND_TEMPLATE = "temperature_command_template" CONF_TEMP_COMMAND_TOPIC = "temperature_command_topic" diff --git a/homeassistant/components/mqtt/water_heater.py b/homeassistant/components/mqtt/water_heater.py index 17e9430dba34ff..08b9d36d850b8c 100644 --- a/homeassistant/components/mqtt/water_heater.py +++ b/homeassistant/components/mqtt/water_heater.py @@ -51,6 +51,8 @@ CONF_MODE_LIST, CONF_MODE_STATE_TEMPLATE, CONF_MODE_STATE_TOPIC, + CONF_POWER_COMMAND_TEMPLATE, + CONF_POWER_COMMAND_TOPIC, CONF_PRECISION, CONF_RETAIN, CONF_TEMP_COMMAND_TEMPLATE, @@ -91,6 +93,7 @@ COMMAND_TEMPLATE_KEYS = { CONF_MODE_COMMAND_TEMPLATE, CONF_TEMP_COMMAND_TEMPLATE, + CONF_POWER_COMMAND_TEMPLATE, } @@ -98,6 +101,7 @@ CONF_CURRENT_TEMP_TOPIC, CONF_MODE_COMMAND_TOPIC, CONF_MODE_STATE_TOPIC, + CONF_POWER_COMMAND_TOPIC, CONF_TEMP_COMMAND_TOPIC, CONF_TEMP_STATE_TOPIC, ) @@ -127,6 +131,8 @@ vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, + vol.Optional(CONF_POWER_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_POWER_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_PRECISION): vol.In( [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] ), @@ -266,6 +272,9 @@ def _setup_from_config(self, config: ConfigType) -> None: ): support |= WaterHeaterEntityFeature.OPERATION_MODE + if self._topic[CONF_POWER_COMMAND_TOPIC] is not None: + support |= WaterHeaterEntityFeature.ON_OFF + self._attr_supported_features = support def _prepare_subscribe_topics(self) -> None: @@ -317,3 +326,19 @@ async def async_set_operation_mode(self, operation_mode: str) -> None: if self._optimistic or self._topic[CONF_MODE_STATE_TOPIC] is None: self._attr_current_operation = operation_mode self.async_write_ha_state() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + if CONF_POWER_COMMAND_TOPIC in self._config: + mqtt_payload = self._command_templates[CONF_POWER_COMMAND_TEMPLATE]( + self._config[CONF_PAYLOAD_ON] + ) + await self._publish(CONF_POWER_COMMAND_TOPIC, mqtt_payload) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + if CONF_POWER_COMMAND_TOPIC in self._config: + mqtt_payload = self._command_templates[CONF_POWER_COMMAND_TEMPLATE]( + self._config[CONF_PAYLOAD_OFF] + ) + await self._publish(CONF_POWER_COMMAND_TOPIC, mqtt_payload) diff --git a/tests/components/mqtt/test_water_heater.py b/tests/components/mqtt/test_water_heater.py index c4f798e05ec2d1..245af5c6918f0d 100644 --- a/tests/components/mqtt/test_water_heater.py +++ b/tests/components/mqtt/test_water_heater.py @@ -257,6 +257,91 @@ async def test_set_operation_optimistic( assert state.state == "performance" +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + water_heater.DOMAIN, + DEFAULT_CONFIG, + ({"power_command_topic": "power-command"},), + ) + ], +) +async def test_set_operation_with_power_command( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test setting of new operation mode with power command enabled.""" + mqtt_mock = await mqtt_mock_entry() + + state = hass.states.get(ENTITY_WATER_HEATER) + assert state.state == "off" + await common.async_set_operation_mode(hass, "electric", ENTITY_WATER_HEATER) + state = hass.states.get(ENTITY_WATER_HEATER) + assert state.state == "electric" + mqtt_mock.async_publish.assert_has_calls([call("mode-topic", "electric", 0, False)]) + mqtt_mock.async_publish.reset_mock() + + await common.async_set_operation_mode(hass, "off", ENTITY_WATER_HEATER) + state = hass.states.get(ENTITY_WATER_HEATER) + assert state.state == "off" + mqtt_mock.async_publish.assert_has_calls([call("mode-topic", "off", 0, False)]) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, ENTITY_WATER_HEATER) + # the water heater is not updated optimistically as this is not supported + mqtt_mock.async_publish.assert_has_calls([call("power-command", "ON", 0, False)]) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, ENTITY_WATER_HEATER) + mqtt_mock.async_publish.assert_has_calls([call("power-command", "OFF", 0, False)]) + mqtt_mock.async_publish.reset_mock() + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + water_heater.DOMAIN, + DEFAULT_CONFIG, + ({"power_command_topic": "power-command", "optimistic": True},), + ) + ], +) +async def test_turn_on_and_off_optimistic_with_power_command( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test setting of turn on/off with power command enabled.""" + mqtt_mock = await mqtt_mock_entry() + + state = hass.states.get(ENTITY_WATER_HEATER) + assert state.state == "off" + await common.async_set_operation_mode(hass, "electric", ENTITY_WATER_HEATER) + state = hass.states.get(ENTITY_WATER_HEATER) + assert state.state == "electric" + mqtt_mock.async_publish.assert_has_calls([call("mode-topic", "electric", 0, False)]) + mqtt_mock.async_publish.reset_mock() + await common.async_set_operation_mode(hass, "off", ENTITY_WATER_HEATER) + state = hass.states.get(ENTITY_WATER_HEATER) + assert state.state == "off" + + await common.async_turn_on(hass, ENTITY_WATER_HEATER) + # the water heater is not updated optimistically as this is not supported + state = hass.states.get(ENTITY_WATER_HEATER) + assert state.state == "off" + mqtt_mock.async_publish.assert_has_calls([call("power-command", "ON", 0, False)]) + mqtt_mock.async_publish.reset_mock() + + await common.async_set_operation_mode(hass, "gas", ENTITY_WATER_HEATER) + state = hass.states.get(ENTITY_WATER_HEATER) + assert state.state == "gas" + await common.async_turn_off(hass, ENTITY_WATER_HEATER) + # the water heater is not updated optimistically as this is not supported + state = hass.states.get(ENTITY_WATER_HEATER) + assert state.state == "gas" + mqtt_mock.async_publish.assert_has_calls([call("power-command", "OFF", 0, False)]) + mqtt_mock.async_publish.reset_mock() + + @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_set_target_temperature( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator @@ -509,9 +594,11 @@ async def test_get_with_templates( "name": "test", "mode_command_topic": "mode-topic", "temperature_command_topic": "temperature-topic", + "power_command_topic": "power-topic", # Create simple templates "mode_command_template": "mode: {{ value }}", "temperature_command_template": "temp: {{ value }}", + "power_command_template": "pwr: {{ value }}", } } } @@ -544,6 +631,14 @@ async def test_set_and_templates( state = hass.states.get(ENTITY_WATER_HEATER) assert state.attributes.get("temperature") == 107 + # Power + await common.async_turn_on(hass, entity_id=ENTITY_WATER_HEATER) + mqtt_mock.async_publish.assert_called_once_with("power-topic", "pwr: ON", 0, False) + mqtt_mock.async_publish.reset_mock() + await common.async_turn_off(hass, entity_id=ENTITY_WATER_HEATER) + mqtt_mock.async_publish.assert_called_once_with("power-topic", "pwr: OFF", 0, False) + mqtt_mock.async_publish.reset_mock() + @pytest.mark.parametrize( "hass_config", @@ -1047,6 +1142,20 @@ async def test_precision_whole( 20.1, "temperature_command_template", ), + ( + water_heater.SERVICE_TURN_ON, + "power_command_topic", + {}, + "ON", + "power_command_template", + ), + ( + water_heater.SERVICE_TURN_OFF, + "power_command_topic", + {}, + "OFF", + "power_command_template", + ), ], ) async def test_publishing_with_custom_encoding( From a0b61a1188123b29e539d9811ae71cba9c1615d8 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 25 Jul 2023 13:58:38 +0200 Subject: [PATCH 0910/1009] Bump pydiscovergy to 2.0.1 (#97186) --- .../components/discovergy/__init__.py | 7 ++- .../components/discovergy/config_flow.py | 5 +-- homeassistant/components/discovergy/const.py | 1 - .../components/discovergy/coordinator.py | 4 +- .../components/discovergy/diagnostics.py | 10 ++--- .../components/discovergy/manifest.json | 2 +- homeassistant/components/discovergy/sensor.py | 6 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/discovergy/conftest.py | 2 +- tests/components/discovergy/const.py | 43 ++++++++++--------- .../components/discovergy/test_config_flow.py | 6 +-- .../components/discovergy/test_diagnostics.py | 25 +++++------ 13 files changed, 54 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/discovergy/__init__.py b/homeassistant/components/discovergy/__init__.py index 54f6fca83d4698..fe1045203d85c0 100644 --- a/homeassistant/components/discovergy/__init__.py +++ b/homeassistant/components/discovergy/__init__.py @@ -14,7 +14,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.httpx_client import get_async_client -from .const import APP_NAME, DOMAIN +from .const import DOMAIN from .coordinator import DiscovergyUpdateCoordinator PLATFORMS = [Platform.SENSOR] @@ -38,7 +38,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api_client=pydiscovergy.Discovergy( email=entry.data[CONF_EMAIL], password=entry.data[CONF_PASSWORD], - app_name=APP_NAME, httpx_client=get_async_client(hass), authentication=BasicAuth(), ), @@ -49,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: # try to get meters from api to check if credentials are still valid and for later use # if no exception is raised everything is fine to go - discovergy_data.meters = await discovergy_data.api_client.get_meters() + discovergy_data.meters = await discovergy_data.api_client.meters() except discovergyError.InvalidLogin as err: raise ConfigEntryAuthFailed("Invalid email or password") from err except Exception as err: # pylint: disable=broad-except @@ -69,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - discovergy_data.coordinators[meter.get_meter_id()] = coordinator + discovergy_data.coordinators[meter.meter_id] = coordinator hass.data[DOMAIN][entry.entry_id] = discovergy_data await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/discovergy/config_flow.py b/homeassistant/components/discovergy/config_flow.py index d6b81ed88375e8..3434b1dd84caa5 100644 --- a/homeassistant/components/discovergy/config_flow.py +++ b/homeassistant/components/discovergy/config_flow.py @@ -16,7 +16,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.httpx_client import get_async_client -from .const import APP_NAME, DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -82,10 +82,9 @@ async def _validate_and_save( await pydiscovergy.Discovergy( email=user_input[CONF_EMAIL], password=user_input[CONF_PASSWORD], - app_name=APP_NAME, httpx_client=get_async_client(self.hass), authentication=BasicAuth(), - ).get_meters() + ).meters() except discovergyError.HTTPError: errors["base"] = "cannot_connect" except discovergyError.InvalidLogin: diff --git a/homeassistant/components/discovergy/const.py b/homeassistant/components/discovergy/const.py index 866e9f11def260..f410eb94bcfc90 100644 --- a/homeassistant/components/discovergy/const.py +++ b/homeassistant/components/discovergy/const.py @@ -3,4 +3,3 @@ DOMAIN = "discovergy" MANUFACTURER = "Discovergy" -APP_NAME = "homeassistant" diff --git a/homeassistant/components/discovergy/coordinator.py b/homeassistant/components/discovergy/coordinator.py index e3b6e91e03f50e..6ee5a4c3e849ca 100644 --- a/homeassistant/components/discovergy/coordinator.py +++ b/homeassistant/components/discovergy/coordinator.py @@ -47,9 +47,7 @@ def __init__( async def _async_update_data(self) -> Reading: """Get last reading for meter.""" try: - return await self.discovergy_client.get_last_reading( - self.meter.get_meter_id() - ) + return await self.discovergy_client.meter_last_reading(self.meter.meter_id) except AccessTokenExpired as err: raise ConfigEntryAuthFailed( f"Auth expired while fetching last reading for meter {self.meter.get_meter_id()}" diff --git a/homeassistant/components/discovergy/diagnostics.py b/homeassistant/components/discovergy/diagnostics.py index 02d5585c1dcfe3..a7c79bf3b13257 100644 --- a/homeassistant/components/discovergy/diagnostics.py +++ b/homeassistant/components/discovergy/diagnostics.py @@ -19,9 +19,9 @@ "serial_number", "full_serial_number", "location", - "fullSerialNumber", - "printedFullSerialNumber", - "administrationNumber", + "full_serial_number", + "printed_full_serial_number", + "administration_number", } @@ -39,8 +39,8 @@ async def async_get_config_entry_diagnostics( flattened_meter.append(async_redact_data(meter.__dict__, TO_REDACT_METER)) # get last reading for meter and make a dict of it - coordinator = data.coordinators[meter.get_meter_id()] - last_readings[meter.get_meter_id()] = coordinator.data.__dict__ + coordinator = data.coordinators[meter.meter_id] + last_readings[meter.meter_id] = coordinator.data.__dict__ return { "entry": async_redact_data(entry.as_dict(), TO_REDACT_CONFIG_ENTRY), diff --git a/homeassistant/components/discovergy/manifest.json b/homeassistant/components/discovergy/manifest.json index c929386e8e85ca..23d7f1ad5bfc58 100644 --- a/homeassistant/components/discovergy/manifest.json +++ b/homeassistant/components/discovergy/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/discovergy", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["pydiscovergy==1.2.1"] + "requirements": ["pydiscovergy==2.0.1"] } diff --git a/homeassistant/components/discovergy/sensor.py b/homeassistant/components/discovergy/sensor.py index fe6ed408298f60..79fc6af1b9a059 100644 --- a/homeassistant/components/discovergy/sensor.py +++ b/homeassistant/components/discovergy/sensor.py @@ -154,8 +154,6 @@ async def async_setup_entry( entities: list[DiscovergySensor] = [] for meter in meters: - meter_id = meter.get_meter_id() - sensors = None if meter.measurement_type == "ELECTRICITY": sensors = ELECTRICITY_SENSORS @@ -167,7 +165,7 @@ async def async_setup_entry( # check if this meter has this data, then add this sensor for key in {description.key, *description.alternative_keys}: coordinator: DiscovergyUpdateCoordinator = data.coordinators[ - meter_id + meter.meter_id ] if key in coordinator.data.values: entities.append( @@ -199,7 +197,7 @@ def __init__( self.entity_description = description self._attr_unique_id = f"{meter.full_serial_number}-{data_key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, meter.get_meter_id())}, + identifiers={(DOMAIN, meter.meter_id)}, name=f"{meter.measurement_type.capitalize()} {meter.location.street} {meter.location.street_number}", model=f"{meter.type} {meter.full_serial_number}", manufacturer=MANUFACTURER, diff --git a/requirements_all.txt b/requirements_all.txt index 617a266421708c..e903c80748097d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1635,7 +1635,7 @@ pydelijn==1.1.0 pydexcom==0.2.3 # homeassistant.components.discovergy -pydiscovergy==1.2.1 +pydiscovergy==2.0.1 # homeassistant.components.doods pydoods==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 650a78954de912..87b42c66399331 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1214,7 +1214,7 @@ pydeconz==113 pydexcom==0.2.3 # homeassistant.components.discovergy -pydiscovergy==1.2.1 +pydiscovergy==2.0.1 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 diff --git a/tests/components/discovergy/conftest.py b/tests/components/discovergy/conftest.py index 313985bd7d2a3d..ea0fe84852fa34 100644 --- a/tests/components/discovergy/conftest.py +++ b/tests/components/discovergy/conftest.py @@ -14,7 +14,7 @@ @pytest.fixture def mock_meters() -> Mock: """Patch libraries.""" - with patch("pydiscovergy.Discovergy.get_meters") as discovergy: + with patch("pydiscovergy.Discovergy.meters") as discovergy: discovergy.side_effect = AsyncMock(return_value=GET_METERS) yield discovergy diff --git a/tests/components/discovergy/const.py b/tests/components/discovergy/const.py index 2205a70830ee6f..5c233d50ba84d6 100644 --- a/tests/components/discovergy/const.py +++ b/tests/components/discovergy/const.py @@ -1,31 +1,34 @@ """Constants for Discovergy integration tests.""" import datetime -from pydiscovergy.models import Meter, Reading +from pydiscovergy.models import Location, Meter, Reading GET_METERS = [ Meter( - meterId="f8d610b7a8cc4e73939fa33b990ded54", - serialNumber="abc123", - fullSerialNumber="abc123", + meter_id="f8d610b7a8cc4e73939fa33b990ded54", + serial_number="abc123", + full_serial_number="abc123", type="TST", - measurementType="ELECTRICITY", - loadProfileType="SLP", - location={ - "city": "Testhause", - "street": "Teststraße", - "streetNumber": "1", - "country": "Germany", + measurement_type="ELECTRICITY", + load_profile_type="SLP", + location=Location( + zip=12345, + city="Testhause", + street="Teststraße", + street_number="1", + country="Germany", + ), + additional={ + "manufacturer_id": "TST", + "printed_full_serial_number": "abc123", + "administration_number": "12345", + "scaling_factor": 1, + "current_scaling_factor": 1, + "voltage_scaling_factor": 1, + "internal_meters": 1, + "first_measurement_time": 1517569090926, + "last_measurement_time": 1678430543742, }, - manufacturerId="TST", - printedFullSerialNumber="abc123", - administrationNumber="12345", - scalingFactor=1, - currentScalingFactor=1, - voltageScalingFactor=1, - internalMeters=1, - firstMeasurementTime=1517569090926, - lastMeasurementTime=1678430543742, ), ] diff --git a/tests/components/discovergy/test_config_flow.py b/tests/components/discovergy/test_config_flow.py index f42a4a983fb757..bc4fd2d9e9df06 100644 --- a/tests/components/discovergy/test_config_flow.py +++ b/tests/components/discovergy/test_config_flow.py @@ -80,7 +80,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: ) with patch( - "pydiscovergy.Discovergy.get_meters", + "pydiscovergy.Discovergy.meters", side_effect=InvalidLogin, ): result2 = await hass.config_entries.flow.async_configure( @@ -101,7 +101,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - with patch("pydiscovergy.Discovergy.get_meters", side_effect=HTTPError): + with patch("pydiscovergy.Discovergy.meters", side_effect=HTTPError): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -120,7 +120,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - with patch("pydiscovergy.Discovergy.get_meters", side_effect=Exception): + with patch("pydiscovergy.Discovergy.meters", side_effect=Exception): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/discovergy/test_diagnostics.py b/tests/components/discovergy/test_diagnostics.py index 1d465dda0e066c..b9da2bb7e6f2d6 100644 --- a/tests/components/discovergy/test_diagnostics.py +++ b/tests/components/discovergy/test_diagnostics.py @@ -16,8 +16,8 @@ async def test_entry_diagnostics( mock_config_entry: MockConfigEntry, ) -> None: """Test config entry diagnostics.""" - with patch("pydiscovergy.Discovergy.get_meters", return_value=GET_METERS), patch( - "pydiscovergy.Discovergy.get_last_reading", return_value=LAST_READING + with patch("pydiscovergy.Discovergy.meters", return_value=GET_METERS), patch( + "pydiscovergy.Discovergy.meter_last_reading", return_value=LAST_READING ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -43,18 +43,15 @@ async def test_entry_diagnostics( assert result["meters"] == [ { "additional": { - "administrationNumber": REDACTED, - "currentScalingFactor": 1, - "firstMeasurementTime": 1517569090926, - "fullSerialNumber": REDACTED, - "internalMeters": 1, - "lastMeasurementTime": 1678430543742, - "loadProfileType": "SLP", - "manufacturerId": "TST", - "printedFullSerialNumber": REDACTED, - "scalingFactor": 1, - "type": "TST", - "voltageScalingFactor": 1, + "administration_number": REDACTED, + "current_scaling_factor": 1, + "first_measurement_time": 1517569090926, + "internal_meters": 1, + "last_measurement_time": 1678430543742, + "manufacturer_id": "TST", + "printed_full_serial_number": REDACTED, + "scaling_factor": 1, + "voltage_scaling_factor": 1, }, "full_serial_number": REDACTED, "load_profile_type": "SLP", From 8d6c4e33064d2b23eccd1b6c18dc621881720dfa Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 25 Jul 2023 14:01:57 +0200 Subject: [PATCH 0911/1009] Add controls to enable and disable a UniFi WLAN (#97204) --- homeassistant/components/unifi/entity.py | 20 ++++- homeassistant/components/unifi/image.py | 27 ++---- homeassistant/components/unifi/switch.py | 30 +++++++ tests/components/unifi/test_switch.py | 105 +++++++++++++++++++++++ 4 files changed, 161 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index 18a132be6a8b29..70b28e34dd0c65 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -18,11 +18,14 @@ from homeassistant.core import callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + DeviceEntryType, +) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription -from .const import ATTR_MANUFACTURER +from .const import ATTR_MANUFACTURER, DOMAIN if TYPE_CHECKING: from .controller import UniFiController @@ -58,6 +61,19 @@ def async_device_device_info_fn(api: aiounifi.Controller, obj_id: str) -> Device ) +@callback +def async_wlan_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: + """Create device registry entry for WLAN.""" + wlan = api.wlans[obj_id] + return DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, wlan.id)}, + manufacturer=ATTR_MANUFACTURER, + model="UniFi WLAN", + name=wlan.name, + ) + + @dataclass class UnifiDescription(Generic[HandlerT, ApiItemT]): """Validate and load entities from different UniFi handlers.""" diff --git a/homeassistant/components/unifi/image.py b/homeassistant/components/unifi/image.py index 730720753d4785..c26f06cb5f2686 100644 --- a/homeassistant/components/unifi/image.py +++ b/homeassistant/components/unifi/image.py @@ -8,24 +8,26 @@ from dataclasses import dataclass from typing import Generic -import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.wlans import Wlans from aiounifi.models.api import ApiItemT from aiounifi.models.wlan import Wlan -from homeassistant.components.image import DOMAIN, ImageEntity, ImageEntityDescription +from homeassistant.components.image import ImageEntity, ImageEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN +from .const import DOMAIN as UNIFI_DOMAIN from .controller import UniFiController -from .entity import HandlerT, UnifiEntity, UnifiEntityDescription +from .entity import ( + HandlerT, + UnifiEntity, + UnifiEntityDescription, + async_wlan_device_info_fn, +) @callback @@ -34,19 +36,6 @@ def async_wlan_qr_code_image_fn(controller: UniFiController, wlan: Wlan) -> byte return controller.api.wlans.generate_wlan_qr_code(wlan) -@callback -def async_wlan_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: - """Create device registry entry for WLAN.""" - wlan = api.wlans[obj_id] - return DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, wlan.id)}, - manufacturer=ATTR_MANUFACTURER, - model="UniFi Network", - name=wlan.name, - ) - - @dataclass class UnifiImageEntityDescriptionMixin(Generic[HandlerT, ApiItemT]): """Validate and load entities from different UniFi handlers.""" diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 846c6d12234210..ca11cdfea30eb6 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -3,6 +3,7 @@ Support for controlling power supply of clients which are powered over Ethernet (POE). Support for controlling network access of clients selected in option flow. Support for controlling deep packet inspection (DPI) restriction groups. +Support for controlling WLAN availability. """ from __future__ import annotations @@ -17,6 +18,7 @@ from aiounifi.interfaces.dpi_restriction_groups import DPIRestrictionGroups from aiounifi.interfaces.outlets import Outlets from aiounifi.interfaces.ports import Ports +from aiounifi.interfaces.wlans import Wlans from aiounifi.models.api import ApiItemT from aiounifi.models.client import Client, ClientBlockRequest from aiounifi.models.device import ( @@ -28,6 +30,7 @@ from aiounifi.models.event import Event, EventKey from aiounifi.models.outlet import Outlet from aiounifi.models.port import Port +from aiounifi.models.wlan import Wlan, WlanEnableRequest from homeassistant.components.switch import ( DOMAIN, @@ -54,6 +57,7 @@ UnifiEntityDescription, async_device_available_fn, async_device_device_info_fn, + async_wlan_device_info_fn, ) CLIENT_BLOCKED = (EventKey.WIRED_CLIENT_BLOCKED, EventKey.WIRELESS_CLIENT_BLOCKED) @@ -137,6 +141,13 @@ async def async_poe_port_control_fn( await api.request(DeviceSetPoePortModeRequest.create(device, int(index), state)) +async def async_wlan_control_fn( + api: aiounifi.Controller, obj_id: str, target: bool +) -> None: + """Control outlet relay.""" + await api.request(WlanEnableRequest.create(obj_id, target)) + + @dataclass class UnifiSwitchEntityDescriptionMixin(Generic[HandlerT, ApiItemT]): """Validate and load entities from different UniFi handlers.""" @@ -233,6 +244,25 @@ class UnifiSwitchEntityDescription( supported_fn=lambda controller, obj_id: controller.api.ports[obj_id].port_poe, unique_id_fn=lambda controller, obj_id: f"{obj_id.split('_', 1)[0]}-poe-{obj_id.split('_', 1)[1]}", ), + UnifiSwitchEntityDescription[Wlans, Wlan]( + key="WLAN control", + device_class=SwitchDeviceClass.SWITCH, + entity_category=EntityCategory.CONFIG, + has_entity_name=True, + icon="mdi:wifi-check", + allowed_fn=lambda controller, obj_id: True, + api_handler_fn=lambda api: api.wlans, + available_fn=lambda controller, _: controller.available, + control_fn=async_wlan_control_fn, + device_info_fn=async_wlan_device_info_fn, + event_is_on=None, + event_to_subscribe=None, + is_on_fn=lambda controller, wlan: wlan.enabled, + name_fn=lambda wlan: None, + object_fn=lambda api, obj_id: api.wlans[obj_id], + supported_fn=lambda controller, obj_id: True, + unique_id_fn=lambda controller, obj_id: f"wlan-{obj_id}", + ), ) diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index f93abc291b8d93..ad5131614afee2 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -580,6 +580,43 @@ } +WLAN = { + "_id": "012345678910111213141516", + "bc_filter_enabled": False, + "bc_filter_list": [], + "dtim_mode": "default", + "dtim_na": 1, + "dtim_ng": 1, + "enabled": True, + "group_rekey": 3600, + "mac_filter_enabled": False, + "mac_filter_list": [], + "mac_filter_policy": "allow", + "minrate_na_advertising_rates": False, + "minrate_na_beacon_rate_kbps": 6000, + "minrate_na_data_rate_kbps": 6000, + "minrate_na_enabled": False, + "minrate_na_mgmt_rate_kbps": 6000, + "minrate_ng_advertising_rates": False, + "minrate_ng_beacon_rate_kbps": 1000, + "minrate_ng_data_rate_kbps": 1000, + "minrate_ng_enabled": False, + "minrate_ng_mgmt_rate_kbps": 1000, + "name": "SSID 1", + "no2ghz_oui": False, + "schedule": [], + "security": "wpapsk", + "site_id": "5a32aa4ee4b0412345678910", + "usergroup_id": "012345678910111213141518", + "wep_idx": 1, + "wlangroup_id": "012345678910111213141519", + "wpa_enc": "ccmp", + "wpa_mode": "wpa2", + "x_iapp_key": "01234567891011121314151617181920", + "x_passphrase": "password", +} + + async def test_no_clients( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: @@ -1230,3 +1267,71 @@ async def test_remove_poe_client_switches( for entry in ent_reg.entities.values() if entry.config_entry_id == config_entry.entry_id ] + + +async def test_wlan_switches( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket +) -> None: + """Test control of UniFi WLAN availability.""" + config_entry = await setup_unifi_integration( + hass, aioclient_mock, wlans_response=[WLAN] + ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 + + ent_reg = er.async_get(hass) + ent_reg_entry = ent_reg.async_get("switch.ssid_1") + assert ent_reg_entry.unique_id == "wlan-012345678910111213141516" + assert ent_reg_entry.entity_category is EntityCategory.CONFIG + + # Validate state object + switch_1 = hass.states.get("switch.ssid_1") + assert switch_1 is not None + assert switch_1.state == STATE_ON + assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.SWITCH + + # Update state object + wlan = deepcopy(WLAN) + wlan["enabled"] = False + mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan) + await hass.async_block_till_done() + assert hass.states.get("switch.ssid_1").state == STATE_OFF + + # Disable WLAN + aioclient_mock.clear_requests() + aioclient_mock.put( + f"https://{controller.host}:1234/api/s/{controller.site}" + + f"/rest/wlanconf/{WLAN['_id']}", + ) + + await hass.services.async_call( + SWITCH_DOMAIN, + "turn_off", + {"entity_id": "switch.ssid_1"}, + blocking=True, + ) + assert aioclient_mock.call_count == 1 + assert aioclient_mock.mock_calls[0][2] == {"enabled": False} + + # Enable WLAN + await hass.services.async_call( + SWITCH_DOMAIN, + "turn_on", + {"entity_id": "switch.ssid_1"}, + blocking=True, + ) + assert aioclient_mock.call_count == 2 + assert aioclient_mock.mock_calls[1][2] == {"enabled": True} + + # Availability signalling + + # Controller disconnects + mock_unifi_websocket(state=WebsocketState.DISCONNECTED) + await hass.async_block_till_done() + assert hass.states.get("switch.ssid_1").state == STATE_UNAVAILABLE + + # Controller reconnects + mock_unifi_websocket(state=WebsocketState.RUNNING) + await hass.async_block_till_done() + assert hass.states.get("switch.ssid_1").state == STATE_OFF From c2f9070f4093e6b0c596b07289fa04ebd1e299d1 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 25 Jul 2023 16:11:37 +0200 Subject: [PATCH 0912/1009] Check before casting to float & add integration type to bsblan (#97210) --- homeassistant/components/bsblan/climate.py | 4 ++++ homeassistant/components/bsblan/manifest.json | 1 + homeassistant/generated/integrations.json | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index dc403611da2e2e..39eab6e7e0a5f1 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -106,6 +106,10 @@ def __init__( @property def current_temperature(self) -> float | None: """Return the current temperature.""" + if self.coordinator.data.current_temperature.value == "---": + # device returns no current temperature + return None + return float(self.coordinator.data.current_temperature.value) @property diff --git a/homeassistant/components/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json index 0e945d13d48439..5abb888513dc82 100644 --- a/homeassistant/components/bsblan/manifest.json +++ b/homeassistant/components/bsblan/manifest.json @@ -4,6 +4,7 @@ "codeowners": ["@liudger"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bsblan", + "integration_type": "device", "iot_class": "local_polling", "loggers": ["bsblan"], "requirements": ["python-bsblan==0.5.11"] diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index ebe16947a51352..6bc96ea15bc070 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -703,7 +703,7 @@ }, "bsblan": { "name": "BSB-Lan", - "integration_type": "hub", + "integration_type": "device", "config_flow": true, "iot_class": "local_polling" }, From 213a1690f3b1ccf9142902811de5728f64c67b19 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jul 2023 12:21:11 -0500 Subject: [PATCH 0913/1009] Bump bleak-retry-connector to 3.1.1 (#97218) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 9bd0672179ab46..cbeab2abec0646 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -15,7 +15,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.20.2", - "bleak-retry-connector==3.1.0", + "bleak-retry-connector==3.1.1", "bluetooth-adapters==0.16.0", "bluetooth-auto-recovery==1.2.1", "bluetooth-data-tools==1.6.1", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c3bd83679ae7a9..13d67cc95f53a1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ atomicwrites-homeassistant==1.4.1 attrs==22.2.0 awesomeversion==22.9.0 bcrypt==4.0.1 -bleak-retry-connector==3.1.0 +bleak-retry-connector==3.1.1 bleak==0.20.2 bluetooth-adapters==0.16.0 bluetooth-auto-recovery==1.2.1 diff --git a/requirements_all.txt b/requirements_all.txt index e903c80748097d..56516f1cbbd1f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -506,7 +506,7 @@ bimmer-connected==0.13.8 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==3.1.0 +bleak-retry-connector==3.1.1 # homeassistant.components.bluetooth bleak==0.20.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 87b42c66399331..a1775c0ef16ed1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -427,7 +427,7 @@ bellows==0.35.8 bimmer-connected==0.13.8 # homeassistant.components.bluetooth -bleak-retry-connector==3.1.0 +bleak-retry-connector==3.1.1 # homeassistant.components.bluetooth bleak==0.20.2 From 6ae79524bd8a50de711a48164da4e919776b95c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Jul 2023 12:30:54 -0500 Subject: [PATCH 0914/1009] Add support for bleak 0.21 (#97212) --- .../components/bluetooth/wrappers.py | 24 +++++++- tests/components/bluetooth/test_init.py | 59 +++++++++++++++++++ 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth/wrappers.py b/homeassistant/components/bluetooth/wrappers.py index 67e401cd40afff..2ae036080f81cc 100644 --- a/homeassistant/components/bluetooth/wrappers.py +++ b/homeassistant/components/bluetooth/wrappers.py @@ -6,13 +6,18 @@ import contextlib from dataclasses import dataclass from functools import partial +import inspect import logging from typing import TYPE_CHECKING, Any, Final from bleak import BleakClient, BleakError from bleak.backends.client import BaseBleakClient, get_platform_client_backend_type from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementDataCallback, BaseBleakScanner +from bleak.backends.scanner import ( + AdvertisementData, + AdvertisementDataCallback, + BaseBleakScanner, +) from bleak_retry_connector import ( NO_RSSI_VALUE, ble_device_description, @@ -58,6 +63,7 @@ def __init__( self._detection_cancel: CALLBACK_TYPE | None = None self._mapped_filters: dict[str, set[str]] = {} self._advertisement_data_callback: AdvertisementDataCallback | None = None + self._background_tasks: set[asyncio.Task] = set() remapped_kwargs = { "detection_callback": detection_callback, "service_uuids": service_uuids or [], @@ -128,12 +134,24 @@ def _setup_detection_callback(self) -> None: """Set up the detection callback.""" if self._advertisement_data_callback is None: return + callback = self._advertisement_data_callback self._cancel_callback() super().register_detection_callback(self._advertisement_data_callback) assert models.MANAGER is not None - assert self._callback is not None + + if not inspect.iscoroutinefunction(callback): + detection_callback = callback + else: + + def detection_callback( + ble_device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + task = asyncio.create_task(callback(ble_device, advertisement_data)) + self._background_tasks.add(task) + task.add_done_callback(self._background_tasks.discard) + self._detection_cancel = models.MANAGER.async_register_bleak_callback( - self._callback, self._mapped_filters + detection_callback, self._mapped_filters ) def __del__(self) -> None: diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 24f1039175b8c0..21fade843f54c5 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -2386,6 +2386,65 @@ def _device_detected( assert len(detected) == 2 +async def test_wrapped_instance_with_service_uuids_with_coro_callback( + hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None +) -> None: + """Test consumers can use the wrapped instance with a service_uuids list as if it was normal BleakScanner. + + Verify that coro callbacks are supported. + """ + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ): + await async_setup_with_default_adapter(hass) + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + detected = [] + + async def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) + + switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand") + switchbot_adv = generate_advertisement_data( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + switchbot_adv_2 = generate_advertisement_data( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = generate_ble_device("11:22:33:44:55:66", "empty") + empty_adv = generate_advertisement_data(local_name="empty") + + assert _get_manager() is not None + scanner = HaBleakScannerWrapper( + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + scanner.register_detection_callback(_device_detected) + + inject_advertisement(hass, switchbot_device, switchbot_adv) + inject_advertisement(hass, switchbot_device, switchbot_adv_2) + + await hass.async_block_till_done() + + assert len(detected) == 2 + + # The UUIDs list we created in the wrapped scanner with should be respected + # and we should not get another callback + inject_advertisement(hass, empty_device, empty_adv) + assert len(detected) == 2 + + async def test_wrapped_instance_with_broken_callbacks( hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None ) -> None: From b4200cb85e5e7466dbc03da75d3eaae7a2e46c45 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 25 Jul 2023 19:44:32 +0200 Subject: [PATCH 0915/1009] Update frontend to 20230725.0 (#97220) --- 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 07c5585833dd20..47e742bdb764a0 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.1"] + "requirements": ["home-assistant-frontend==20230725.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 13d67cc95f53a1..cf576fc1c837a9 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.2.0 home-assistant-bluetooth==1.10.2 -home-assistant-frontend==20230705.1 +home-assistant-frontend==20230725.0 home-assistant-intents==2023.7.24 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 56516f1cbbd1f8..4892fddd5ba32d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -985,7 +985,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230705.1 +home-assistant-frontend==20230725.0 # homeassistant.components.conversation home-assistant-intents==2023.7.24 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1775c0ef16ed1..0820a3026e195d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -771,7 +771,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230705.1 +home-assistant-frontend==20230725.0 # homeassistant.components.conversation home-assistant-intents==2023.7.24 From 585d35712941815c1f0523fe89fe8e1bb1e09d18 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 25 Jul 2023 20:46:04 +0200 Subject: [PATCH 0916/1009] Add config flow to OpenSky (#96912) Co-authored-by: Sander --- CODEOWNERS | 1 + homeassistant/components/opensky/__init__.py | 26 +++ .../components/opensky/config_flow.py | 77 +++++++++ homeassistant/components/opensky/const.py | 4 + .../components/opensky/manifest.json | 1 + homeassistant/components/opensky/sensor.py | 69 ++++++-- homeassistant/components/opensky/strings.json | 16 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- requirements_test_all.txt | 3 + tests/components/opensky/__init__.py | 9 + tests/components/opensky/conftest.py | 50 ++++++ tests/components/opensky/test_config_flow.py | 155 ++++++++++++++++++ tests/components/opensky/test_init.py | 28 ++++ tests/components/opensky/test_sensor.py | 20 +++ 15 files changed, 443 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/opensky/config_flow.py create mode 100644 homeassistant/components/opensky/strings.json create mode 100644 tests/components/opensky/__init__.py create mode 100644 tests/components/opensky/conftest.py create mode 100644 tests/components/opensky/test_config_flow.py create mode 100644 tests/components/opensky/test_init.py create mode 100644 tests/components/opensky/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index f09785a778160c..8a72fadfbd9c40 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -894,6 +894,7 @@ build.json @home-assistant/supervisor /homeassistant/components/openhome/ @bazwilliams /tests/components/openhome/ @bazwilliams /homeassistant/components/opensky/ @joostlek +/tests/components/opensky/ @joostlek /homeassistant/components/opentherm_gw/ @mvn23 /tests/components/opentherm_gw/ @mvn23 /homeassistant/components/openuv/ @bachya diff --git a/homeassistant/components/opensky/__init__.py b/homeassistant/components/opensky/__init__.py index da805999d538aa..197356b2092cf8 100644 --- a/homeassistant/components/opensky/__init__.py +++ b/homeassistant/components/opensky/__init__.py @@ -1 +1,27 @@ """The opensky component.""" +from __future__ import annotations + +from python_opensky import OpenSky + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CLIENT, DOMAIN, PLATFORMS + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up opensky from a config entry.""" + + client = OpenSky(session=async_get_clientsession(hass)) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {CLIENT: client} + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload opensky config entry.""" + + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/opensky/config_flow.py b/homeassistant/components/opensky/config_flow.py new file mode 100644 index 00000000000000..6e3ffb5e2b17bd --- /dev/null +++ b/homeassistant/components/opensky/config_flow.py @@ -0,0 +1,77 @@ +"""Config flow for OpenSky integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + CONF_RADIUS, +) +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType + +from .const import DEFAULT_NAME, DOMAIN +from .sensor import CONF_ALTITUDE, DEFAULT_ALTITUDE + + +class OpenSkyConfigFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow handler for OpenSky.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Initialize user input.""" + if user_input is not None: + return self.async_create_entry( + title=DEFAULT_NAME, + data={ + CONF_LATITUDE: user_input[CONF_LATITUDE], + CONF_LONGITUDE: user_input[CONF_LONGITUDE], + }, + options={ + CONF_RADIUS: user_input[CONF_RADIUS], + CONF_ALTITUDE: user_input[CONF_ALTITUDE], + }, + ) + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required(CONF_RADIUS): vol.Coerce(float), + vol.Required(CONF_LATITUDE): cv.latitude, + vol.Required(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_ALTITUDE): vol.Coerce(float), + } + ), + { + CONF_LATITUDE: self.hass.config.latitude, + CONF_LONGITUDE: self.hass.config.longitude, + CONF_ALTITUDE: DEFAULT_ALTITUDE, + }, + ), + ) + + async def async_step_import(self, import_config: ConfigType) -> FlowResult: + """Import config from yaml.""" + entry_data = { + CONF_LATITUDE: import_config.get(CONF_LATITUDE, self.hass.config.latitude), + CONF_LONGITUDE: import_config.get( + CONF_LONGITUDE, self.hass.config.longitude + ), + } + self._async_abort_entries_match(entry_data) + return self.async_create_entry( + title=import_config.get(CONF_NAME, DEFAULT_NAME), + data=entry_data, + options={ + CONF_RADIUS: import_config[CONF_RADIUS] * 1000, + CONF_ALTITUDE: import_config.get(CONF_ALTITUDE, DEFAULT_ALTITUDE), + }, + ) diff --git a/homeassistant/components/opensky/const.py b/homeassistant/components/opensky/const.py index 7e511ed7d2c5e7..ccea69f8b7f38a 100644 --- a/homeassistant/components/opensky/const.py +++ b/homeassistant/components/opensky/const.py @@ -1,6 +1,10 @@ """OpenSky constants.""" +from homeassistant.const import Platform + +PLATFORMS = [Platform.SENSOR] DEFAULT_NAME = "OpenSky" DOMAIN = "opensky" +CLIENT = "client" CONF_ALTITUDE = "altitude" ATTR_ICAO24 = "icao24" diff --git a/homeassistant/components/opensky/manifest.json b/homeassistant/components/opensky/manifest.json index 6c6d3acb30eb4e..f3fb13589bb30a 100644 --- a/homeassistant/components/opensky/manifest.json +++ b/homeassistant/components/opensky/manifest.json @@ -2,6 +2,7 @@ "domain": "opensky", "name": "OpenSky Network", "codeowners": ["@joostlek"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/opensky", "iot_class": "cloud_polling", "requirements": ["python-opensky==0.0.10"] diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index 0616b774951845..4ef1070d12d093 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, @@ -15,10 +16,10 @@ CONF_NAME, CONF_RADIUS, ) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant import homeassistant.helpers.config_validation as cv 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 ( @@ -26,6 +27,7 @@ ATTR_CALLSIGN, ATTR_ICAO24, ATTR_SENSOR, + CLIENT, CONF_ALTITUDE, DEFAULT_ALTITUDE, DOMAIN, @@ -36,6 +38,7 @@ # OpenSky free user has 400 credits, with 4 credits per API call. 100/24 = ~4 requests per hour SCAN_INTERVAL = timedelta(minutes=15) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_RADIUS): vol.Coerce(float), @@ -47,27 +50,57 @@ ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Open Sky platform.""" - latitude = config.get(CONF_LATITUDE, hass.config.latitude) - longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - radius = config.get(CONF_RADIUS, 0) - bounding_box = OpenSky.get_bounding_box(latitude, longitude, radius * 1000) - session = async_get_clientsession(hass) - opensky = OpenSky(session=session) - add_entities( + """Set up the OpenSky sensor platform from yaml.""" + + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.1.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "OpenSky", + }, + ) + + 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: + """Initialize the entries.""" + + opensky = hass.data[DOMAIN][entry.entry_id][CLIENT] + bounding_box = OpenSky.get_bounding_box( + entry.data[CONF_LATITUDE], + entry.data[CONF_LONGITUDE], + entry.options[CONF_RADIUS], + ) + async_add_entities( [ OpenSkySensor( - hass, - config.get(CONF_NAME, DOMAIN), + entry.title, opensky, bounding_box, - config[CONF_ALTITUDE], + entry.options.get(CONF_ALTITUDE, DEFAULT_ALTITUDE), + entry.entry_id, ) ], True, @@ -83,20 +116,20 @@ class OpenSkySensor(SensorEntity): def __init__( self, - hass: HomeAssistant, name: str, opensky: OpenSky, bounding_box: BoundingBox, altitude: float, + entry_id: str, ) -> None: """Initialize the sensor.""" self._altitude = altitude self._state = 0 - self._hass = hass self._name = name self._previously_tracked: set[str] = set() self._opensky = opensky self._bounding_box = bounding_box + self._attr_unique_id = f"{entry_id}_opensky" @property def name(self) -> str: @@ -133,7 +166,7 @@ def _handle_boundary( ATTR_LATITUDE: latitude, ATTR_ICAO24: icao24, } - self._hass.bus.fire(event, data) + self.hass.bus.fire(event, data) async def async_update(self) -> None: """Update device state.""" diff --git a/homeassistant/components/opensky/strings.json b/homeassistant/components/opensky/strings.json new file mode 100644 index 00000000000000..768ffde155fbdc --- /dev/null +++ b/homeassistant/components/opensky/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "user": { + "description": "Fill in the location to track.", + "data": { + "name": "[%key:common::config_flow::data::api_key%]", + "radius": "Radius", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", + "altitude": "Altitude" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b4b9c409c6e45c..2359ac79e040f7 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -325,6 +325,7 @@ "openexchangerates", "opengarage", "openhome", + "opensky", "opentherm_gw", "openuv", "openweathermap", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 6bc96ea15bc070..938ffa13ab541c 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3976,7 +3976,7 @@ "opensky": { "name": "OpenSky Network", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "cloud_polling" }, "opentherm_gw": { diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0820a3026e195d..d2a9c71103970e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1562,6 +1562,9 @@ python-miio==0.5.12 # homeassistant.components.mystrom python-mystrom==2.2.0 +# homeassistant.components.opensky +python-opensky==0.0.10 + # homeassistant.components.otbr # homeassistant.components.thread python-otbr-api==2.3.0 diff --git a/tests/components/opensky/__init__.py b/tests/components/opensky/__init__.py new file mode 100644 index 00000000000000..f985f068ab10be --- /dev/null +++ b/tests/components/opensky/__init__.py @@ -0,0 +1,9 @@ +"""Opensky tests.""" +from unittest.mock import patch + + +def patch_setup_entry() -> bool: + """Patch interface.""" + return patch( + "homeassistant.components.opensky.async_setup_entry", return_value=True + ) diff --git a/tests/components/opensky/conftest.py b/tests/components/opensky/conftest.py new file mode 100644 index 00000000000000..63e514d0d8fa2f --- /dev/null +++ b/tests/components/opensky/conftest.py @@ -0,0 +1,50 @@ +"""Configure tests for the OpenSky integration.""" +from collections.abc import Awaitable, Callable +from unittest.mock import patch + +import pytest +from python_opensky import StatesResponse + +from homeassistant.components.opensky.const import CONF_ALTITUDE, DOMAIN +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +ComponentSetup = Callable[[MockConfigEntry], Awaitable[None]] + + +@pytest.fixture(name="config_entry") +def mock_config_entry() -> MockConfigEntry: + """Create OpenSky entry in Home Assistant.""" + return MockConfigEntry( + domain=DOMAIN, + title="OpenSky", + data={ + CONF_LATITUDE: 0.0, + CONF_LONGITUDE: 0.0, + }, + options={ + CONF_RADIUS: 10.0, + CONF_ALTITUDE: 0.0, + }, + ) + + +@pytest.fixture(name="setup_integration") +async def mock_setup_integration( + hass: HomeAssistant, +) -> Callable[[MockConfigEntry], Awaitable[None]]: + """Fixture for setting up the component.""" + + async def func(mock_config_entry: MockConfigEntry) -> None: + mock_config_entry.add_to_hass(hass) + with patch( + "python_opensky.OpenSky.get_states", + return_value=StatesResponse(states=[], time=0), + ): + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + return func diff --git a/tests/components/opensky/test_config_flow.py b/tests/components/opensky/test_config_flow.py new file mode 100644 index 00000000000000..e785a5f3a8fbae --- /dev/null +++ b/tests/components/opensky/test_config_flow.py @@ -0,0 +1,155 @@ +"""Test OpenSky config flow.""" +from typing import Any + +import pytest + +from homeassistant.components.opensky.const import ( + CONF_ALTITUDE, + DEFAULT_NAME, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_RADIUS +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import patch_setup_entry + +from tests.common import MockConfigEntry + + +async def test_full_user_flow(hass: HomeAssistant) -> None: + """Test the full user configuration flow.""" + with patch_setup_entry(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_RADIUS: 10, + CONF_LATITUDE: 0.0, + CONF_LONGITUDE: 0.0, + CONF_ALTITUDE: 0, + }, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "OpenSky" + assert result["data"] == { + CONF_LATITUDE: 0.0, + CONF_LONGITUDE: 0.0, + } + assert result["options"] == { + CONF_ALTITUDE: 0.0, + CONF_RADIUS: 10.0, + } + + +@pytest.mark.parametrize( + ("config", "title", "data", "options"), + [ + ( + {CONF_RADIUS: 10.0}, + DEFAULT_NAME, + { + CONF_LATITUDE: 32.87336, + CONF_LONGITUDE: -117.22743, + }, + { + CONF_RADIUS: 10000.0, + CONF_ALTITUDE: 0, + }, + ), + ( + { + CONF_RADIUS: 10.0, + CONF_NAME: "My home", + }, + "My home", + { + CONF_LATITUDE: 32.87336, + CONF_LONGITUDE: -117.22743, + }, + { + CONF_RADIUS: 10000.0, + CONF_ALTITUDE: 0, + }, + ), + ( + { + CONF_RADIUS: 10.0, + CONF_LATITUDE: 10.0, + CONF_LONGITUDE: -100.0, + }, + DEFAULT_NAME, + { + CONF_LATITUDE: 10.0, + CONF_LONGITUDE: -100.0, + }, + { + CONF_RADIUS: 10000.0, + CONF_ALTITUDE: 0, + }, + ), + ( + {CONF_RADIUS: 10.0, CONF_ALTITUDE: 100.0}, + DEFAULT_NAME, + { + CONF_LATITUDE: 32.87336, + CONF_LONGITUDE: -117.22743, + }, + { + CONF_RADIUS: 10000.0, + CONF_ALTITUDE: 100.0, + }, + ), + ], +) +async def test_import_flow( + hass: HomeAssistant, + config: dict[str, Any], + title: str, + data: dict[str, Any], + options: dict[str, Any], +) -> None: + """Test the import flow.""" + with patch_setup_entry(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == title + assert result["options"] == options + assert result["data"] == data + + +async def test_importing_already_exists_flow(hass: HomeAssistant) -> None: + """Test the import flow when same location already exists.""" + MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_NAME, + data={}, + options={ + CONF_LATITUDE: 32.87336, + CONF_LONGITUDE: -117.22743, + CONF_RADIUS: 10.0, + CONF_ALTITUDE: 100.0, + }, + ).add_to_hass(hass) + with patch_setup_entry(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_LATITUDE: 32.87336, + CONF_LONGITUDE: -117.22743, + CONF_RADIUS: 10.0, + CONF_ALTITUDE: 100.0, + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/opensky/test_init.py b/tests/components/opensky/test_init.py new file mode 100644 index 00000000000000..be1c21627f09d0 --- /dev/null +++ b/tests/components/opensky/test_init.py @@ -0,0 +1,28 @@ +"""Test OpenSky component setup process.""" +from __future__ import annotations + +from homeassistant.components.opensky.const import DOMAIN +from homeassistant.core import HomeAssistant + +from .conftest import ComponentSetup + +from tests.common import MockConfigEntry + + +async def test_load_unload_entry( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, +) -> None: + """Test load and unload entry.""" + await setup_integration(config_entry) + entry = hass.config_entries.async_entries(DOMAIN)[0] + + state = hass.states.get("sensor.opensky") + assert state + + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.opensky") + assert not state diff --git a/tests/components/opensky/test_sensor.py b/tests/components/opensky/test_sensor.py new file mode 100644 index 00000000000000..1768efebc78f36 --- /dev/null +++ b/tests/components/opensky/test_sensor.py @@ -0,0 +1,20 @@ +"""OpenSky sensor tests.""" +from homeassistant.components.opensky.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_PLATFORM, CONF_RADIUS, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + +LEGACY_CONFIG = {Platform.SENSOR: [{CONF_PLATFORM: DOMAIN, CONF_RADIUS: 10.0}]} + + +async def test_legacy_migration(hass: HomeAssistant) -> None: + """Test migration from yaml to config flow.""" + assert await async_setup_component(hass, Platform.SENSOR, LEGACY_CONFIG) + await hass.async_block_till_done() + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + issue_registry = ir.async_get(hass) + assert len(issue_registry.issues) == 1 From 234715a8c68d843bf2f4198e90dfe1728fb77678 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 25 Jul 2023 20:48:05 +0200 Subject: [PATCH 0917/1009] Add explicit device naming for Verisure (#97224) --- homeassistant/components/verisure/camera.py | 1 + homeassistant/components/verisure/lock.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index 1f890a22a644cd..90ad926aeb77a3 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -47,6 +47,7 @@ class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera) """Representation of a Verisure camera.""" _attr_has_entity_name = True + _attr_name = None def __init__( self, diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 53646c1e435009..6af64060ab5b3b 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -60,6 +60,7 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt """Representation of a Verisure doorlock.""" _attr_has_entity_name = True + _attr_name = None def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str From c6f21b47a860e503bff368dec332ffe723acb9ac Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 25 Jul 2023 16:23:31 -0400 Subject: [PATCH 0918/1009] Whrilpool add periodic update (#97222) --- homeassistant/components/whirlpool/sensor.py | 7 ++++++- tests/components/whirlpool/test_sensor.py | 11 +++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/whirlpool/sensor.py b/homeassistant/components/whirlpool/sensor.py index 37b16530b0d902..f761badfa2b5d3 100644 --- a/homeassistant/components/whirlpool/sensor.py +++ b/homeassistant/components/whirlpool/sensor.py @@ -70,6 +70,7 @@ ICON_W = "mdi:washing-machine" _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(minutes=5) def washer_state(washer: WasherDryer) -> str | None: @@ -228,7 +229,7 @@ def native_value(self) -> StateType | str: class WasherDryerTimeClass(RestoreSensor): """A timestamp class for the whirlpool/maytag washer account.""" - _attr_should_poll = False + _attr_should_poll = True _attr_has_entity_name = True def __init__( @@ -272,6 +273,10 @@ def available(self) -> bool: """Return True if entity is available.""" return self._wd.get_online() + async def async_update(self) -> None: + """Update status of Whirlpool.""" + await self._wd.fetch_data() + @callback def update_from_latest_data(self) -> None: """Calculate the time stamp for completion.""" diff --git a/tests/components/whirlpool/test_sensor.py b/tests/components/whirlpool/test_sensor.py index be78b0e2df8c1c..4e451f46e9ba0e 100644 --- a/tests/components/whirlpool/test_sensor.py +++ b/tests/components/whirlpool/test_sensor.py @@ -4,13 +4,14 @@ from whirlpool.washerdryer import MachineState +from homeassistant.components.whirlpool.sensor import SCAN_INTERVAL from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.helpers import entity_registry as er -from homeassistant.util.dt import as_timestamp, utc_from_timestamp +from homeassistant.util.dt import as_timestamp, utc_from_timestamp, utcnow from . import init_integration -from tests.common import mock_restore_cache_with_extra_data +from tests.common import async_fire_time_changed, mock_restore_cache_with_extra_data async def update_sensor_state( @@ -132,6 +133,12 @@ async def test_washer_sensor_values( await init_integration(hass) + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + entity_id = "sensor.washer_state" mock_instance = mock_sensor1_api entry = entity_registry.async_get(entity_id) From 66bbe6865eb81d9f0a444d687156a5b0b1b29096 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 25 Jul 2023 22:39:55 +0200 Subject: [PATCH 0919/1009] Bump youtubeaio to 1.1.5 (#97231) --- homeassistant/components/youtube/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/youtube/manifest.json b/homeassistant/components/youtube/manifest.json index b37d242fe524ff..a1a71f6712e382 100644 --- a/homeassistant/components/youtube/manifest.json +++ b/homeassistant/components/youtube/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/youtube", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["youtubeaio==1.1.4"] + "requirements": ["youtubeaio==1.1.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4892fddd5ba32d..92d3ae94fb1494 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2734,7 +2734,7 @@ yolink-api==0.3.0 youless-api==1.0.1 # homeassistant.components.youtube -youtubeaio==1.1.4 +youtubeaio==1.1.5 # homeassistant.components.media_extractor yt-dlp==2023.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d2a9c71103970e..d10f29c5374233 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2013,7 +2013,7 @@ yolink-api==0.3.0 youless-api==1.0.1 # homeassistant.components.youtube -youtubeaio==1.1.4 +youtubeaio==1.1.5 # homeassistant.components.zamg zamg==0.2.4 From c3977b5eb34a547926888bfe12d62406058f52f1 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Wed, 26 Jul 2023 01:01:39 +0200 Subject: [PATCH 0920/1009] Correct AsusWRT device identifier (#97238) --- homeassistant/components/asuswrt/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index e0143d49259b72..8f7229bf5adfb8 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -377,7 +377,7 @@ def update_options(self, new_options: dict[str, Any]) -> bool: def device_info(self) -> DeviceInfo: """Return the device information.""" info = DeviceInfo( - identifiers={(DOMAIN, self.unique_id or "AsusWRT")}, + identifiers={(DOMAIN, self._entry.unique_id or "AsusWRT")}, name=self.host, model=self._api.model or "Asus Router", manufacturer="Asus", From 311c321d06269da0b9c1f52abb7312ee8d4dae4f Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 25 Jul 2023 21:19:03 -0500 Subject: [PATCH 0921/1009] Add HassShoppingListAddItem to default agent (#97232) * Bump hassil and intents package for HassShoppingListAddItem * Remove hard-coded response text * Test adding item to the shopping list * Hook removed import in test for some reason --- .../components/conversation/manifest.json | 2 +- .../components/shopping_list/intent.py | 1 - homeassistant/package_constraints.txt | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- tests/components/conversation/conftest.py | 23 +++++++++++++++++++ .../conversation/test_default_agent.py | 13 +++++++++++ tests/components/shopping_list/test_init.py | 3 ++- 8 files changed, 45 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 65b12b64e58a21..a8f24a335f07c7 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.2.0", "home-assistant-intents==2023.7.24"] + "requirements": ["hassil==1.2.2", "home-assistant-intents==2023.7.25"] } diff --git a/homeassistant/components/shopping_list/intent.py b/homeassistant/components/shopping_list/intent.py index c709322e0b7db0..d6a29eb73f364c 100644 --- a/homeassistant/components/shopping_list/intent.py +++ b/homeassistant/components/shopping_list/intent.py @@ -29,7 +29,6 @@ async def async_handle(self, intent_obj: intent.Intent): await intent_obj.hass.data[DOMAIN].async_add(item) response = intent_obj.create_response() - response.async_set_speech(f"I've added {item} to your shopping list") intent_obj.hass.bus.async_fire(EVENT_SHOPPING_LIST_UPDATED) return response diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cf576fc1c837a9..a9239bcfda8890 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,10 +20,10 @@ dbus-fast==1.87.2 fnv-hash-fast==0.4.0 ha-av==10.1.0 hass-nabucasa==0.69.0 -hassil==1.2.0 +hassil==1.2.2 home-assistant-bluetooth==1.10.2 home-assistant-frontend==20230725.0 -home-assistant-intents==2023.7.24 +home-assistant-intents==2023.7.25 httpx==0.24.1 ifaddr==0.2.0 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 92d3ae94fb1494..a51c0e57a6d7f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -955,7 +955,7 @@ hass-nabucasa==0.69.0 hass-splunk==0.1.1 # homeassistant.components.conversation -hassil==1.2.0 +hassil==1.2.2 # homeassistant.components.jewish_calendar hdate==0.10.4 @@ -988,7 +988,7 @@ holidays==0.28 home-assistant-frontend==20230725.0 # homeassistant.components.conversation -home-assistant-intents==2023.7.24 +home-assistant-intents==2023.7.25 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d10f29c5374233..b2d39c0d7877e8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -750,7 +750,7 @@ habitipy==0.2.0 hass-nabucasa==0.69.0 # homeassistant.components.conversation -hassil==1.2.0 +hassil==1.2.2 # homeassistant.components.jewish_calendar hdate==0.10.4 @@ -774,7 +774,7 @@ holidays==0.28 home-assistant-frontend==20230725.0 # homeassistant.components.conversation -home-assistant-intents==2023.7.24 +home-assistant-intents==2023.7.25 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/tests/components/conversation/conftest.py b/tests/components/conversation/conftest.py index 85d5b5daa910e3..a08823255e9d1d 100644 --- a/tests/components/conversation/conftest.py +++ b/tests/components/conversation/conftest.py @@ -1,8 +1,10 @@ """Conversation test helpers.""" +from unittest.mock import patch import pytest from homeassistant.components import conversation +from homeassistant.components.shopping_list import intent as sl_intent from homeassistant.const import MATCH_ALL from . import MockAgent @@ -28,3 +30,24 @@ def mock_agent_support_all(hass): agent = MockAgent(entry.entry_id, MATCH_ALL) conversation.async_set_agent(hass, entry, agent) return agent + + +@pytest.fixture(autouse=True) +def mock_shopping_list_io(): + """Stub out the persistence.""" + with patch("homeassistant.components.shopping_list.ShoppingData.save"), patch( + "homeassistant.components.shopping_list.ShoppingData.async_load" + ): + yield + + +@pytest.fixture +async def sl_setup(hass): + """Set up the shopping list.""" + + entry = MockConfigEntry(domain="shopping_list") + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + + await sl_intent.async_setup_intents(hass) diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 899fd761d5ebf8..af9af468453ee4 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -265,3 +265,16 @@ async def test_trigger_sentences(hass: HomeAssistant, init_components) -> None: ), sentence assert len(callback.mock_calls) == 0 + + +async def test_shopping_list_add_item( + hass: HomeAssistant, init_components, sl_setup +) -> None: + """Test adding an item to the shopping list through the default agent.""" + result = await conversation.async_converse( + hass, "add apples to my shopping list", None, Context() + ) + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert result.response.speech == { + "plain": {"speech": "Added apples", "extra_data": None} + } diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py index e5f1e30efdb3e3..a28b1ee0cfbbd0 100644 --- a/tests/components/shopping_list/test_init.py +++ b/tests/components/shopping_list/test_init.py @@ -34,7 +34,8 @@ async def test_add_item(hass: HomeAssistant, sl_setup) -> None: hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) - assert response.speech["plain"]["speech"] == "I've added beer to your shopping list" + # Response text is now handled by default conversation agent + assert response.response_type == intent.IntentResponseType.ACTION_DONE async def test_remove_item(hass: HomeAssistant, sl_setup) -> None: From 4a649ff31d7e0570ad8910c4e43f3632f2110da9 Mon Sep 17 00:00:00 2001 From: tronikos Date: Tue, 25 Jul 2023 22:48:06 -0700 Subject: [PATCH 0922/1009] Bump opower==0.0.15 (#97243) --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index e7ebb7b546b0ba..08f25d20efff21 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["recorder"], "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", - "requirements": ["opower==0.0.14"] + "requirements": ["opower==0.0.15"] } diff --git a/requirements_all.txt b/requirements_all.txt index a51c0e57a6d7f7..6b77bbc03b9ece 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1365,7 +1365,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.14 +opower==0.0.15 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2d39c0d7877e8..1320097746a44d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1034,7 +1034,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.14 +opower==0.0.15 # homeassistant.components.oralb oralb-ble==0.17.6 From 89069bb9b80521903b39f923ed0efc197e18ea58 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 26 Jul 2023 08:00:17 +0200 Subject: [PATCH 0923/1009] Add WLAN clients reporting to UniFi Sensor platform (#97234) --- .../components/unifi/device_tracker.py | 2 + homeassistant/components/unifi/entity.py | 8 +- homeassistant/components/unifi/image.py | 1 + homeassistant/components/unifi/sensor.py | 36 +++++ homeassistant/components/unifi/switch.py | 5 + homeassistant/components/unifi/update.py | 1 + tests/components/unifi/test_sensor.py | 124 ++++++++++++++++++ 7 files changed, 175 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 296857e1cfaf22..fcfe71a2858246 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -171,6 +171,7 @@ class UnifiTrackerEntityDescription( is_connected_fn=async_client_is_connected_fn, name_fn=lambda client: client.name or client.hostname, object_fn=lambda api, obj_id: api.clients[obj_id], + should_poll=False, supported_fn=lambda controller, obj_id: True, unique_id_fn=lambda controller, obj_id: f"{obj_id}-{controller.site}", ip_address_fn=lambda api, obj_id: api.clients[obj_id].ip, @@ -190,6 +191,7 @@ class UnifiTrackerEntityDescription( is_connected_fn=lambda ctrlr, obj_id: ctrlr.api.devices[obj_id].state == 1, name_fn=lambda device: device.name or device.model, object_fn=lambda api, obj_id: api.devices[obj_id], + should_poll=False, supported_fn=lambda controller, obj_id: True, unique_id_fn=lambda controller, obj_id: obj_id, ip_address_fn=lambda api, obj_id: api.devices[obj_id].ip, diff --git a/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index 70b28e34dd0c65..54b9cb12157a4c 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -86,6 +86,7 @@ class UnifiDescription(Generic[HandlerT, ApiItemT]): event_to_subscribe: tuple[EventKey, ...] | None name_fn: Callable[[ApiItemT], str | None] object_fn: Callable[[aiounifi.Controller, str], ApiItemT] + should_poll: bool supported_fn: Callable[[UniFiController, str], bool | None] unique_id_fn: Callable[[UniFiController, str], str] @@ -99,8 +100,6 @@ class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]): """Representation of a UniFi entity.""" entity_description: UnifiEntityDescription[HandlerT, ApiItemT] - _attr_should_poll = False - _attr_unique_id: str def __init__( @@ -120,6 +119,7 @@ def __init__( self._attr_available = description.available_fn(controller, obj_id) self._attr_device_info = description.device_info_fn(controller.api, obj_id) + self._attr_should_poll = description.should_poll self._attr_unique_id = description.unique_id_fn(controller, obj_id) obj = description.object_fn(self.controller.api, obj_id) @@ -209,6 +209,10 @@ async def remove_item(self, keys: set) -> None: else: await self.async_remove(force_remove=True) + async def async_update(self) -> None: + """Update state if polling is configured.""" + self.async_update_state(ItemEvent.CHANGED, self._obj_id) + @callback def async_initiate_state(self) -> None: """Initiate entity state. diff --git a/homeassistant/components/unifi/image.py b/homeassistant/components/unifi/image.py index c26f06cb5f2686..25c368880fa17a 100644 --- a/homeassistant/components/unifi/image.py +++ b/homeassistant/components/unifi/image.py @@ -67,6 +67,7 @@ class UnifiImageEntityDescription( event_to_subscribe=None, name_fn=lambda _: "QR Code", object_fn=lambda api, obj_id: api.wlans[obj_id], + should_poll=False, supported_fn=lambda controller, obj_id: True, unique_id_fn=lambda controller, obj_id: f"qr_code-{obj_id}", image_fn=async_wlan_qr_code_image_fn, diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 3682fa0bf6cd0f..8cdc0dcbb71597 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -14,9 +14,11 @@ from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.clients import Clients from aiounifi.interfaces.ports import Ports +from aiounifi.interfaces.wlans import Wlans from aiounifi.models.api import ApiItemT from aiounifi.models.client import Client from aiounifi.models.port import Port +from aiounifi.models.wlan import Wlan from homeassistant.components.sensor import ( SensorDeviceClass, @@ -39,6 +41,7 @@ UnifiEntityDescription, async_device_available_fn, async_device_device_info_fn, + async_wlan_device_info_fn, ) @@ -68,6 +71,18 @@ def async_client_uptime_value_fn( return dt_util.utc_from_timestamp(float(client.uptime)) +@callback +def async_wlan_client_value_fn(controller: UniFiController, wlan: Wlan) -> int: + """Calculate the amount of clients connected to a wlan.""" + return len( + [ + client.mac + for client in controller.api.clients.values() + if client.essid == wlan.name + ] + ) + + @callback def async_client_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: """Create device registry entry for client.""" @@ -109,6 +124,7 @@ class UnifiSensorEntityDescription( event_to_subscribe=None, name_fn=lambda _: "RX", object_fn=lambda api, obj_id: api.clients[obj_id], + should_poll=False, supported_fn=lambda controller, _: controller.option_allow_bandwidth_sensors, unique_id_fn=lambda controller, obj_id: f"rx-{obj_id}", value_fn=async_client_rx_value_fn, @@ -126,6 +142,7 @@ class UnifiSensorEntityDescription( event_to_subscribe=None, name_fn=lambda _: "TX", object_fn=lambda api, obj_id: api.clients[obj_id], + should_poll=False, supported_fn=lambda controller, _: controller.option_allow_bandwidth_sensors, unique_id_fn=lambda controller, obj_id: f"tx-{obj_id}", value_fn=async_client_tx_value_fn, @@ -145,6 +162,7 @@ class UnifiSensorEntityDescription( event_to_subscribe=None, name_fn=lambda port: f"{port.name} PoE Power", object_fn=lambda api, obj_id: api.ports[obj_id], + should_poll=False, supported_fn=lambda controller, obj_id: controller.api.ports[obj_id].port_poe, unique_id_fn=lambda controller, obj_id: f"poe_power-{obj_id}", value_fn=lambda _, obj: obj.poe_power if obj.poe_mode != "off" else "0", @@ -163,10 +181,28 @@ class UnifiSensorEntityDescription( event_to_subscribe=None, name_fn=lambda client: "Uptime", object_fn=lambda api, obj_id: api.clients[obj_id], + should_poll=False, supported_fn=lambda controller, _: controller.option_allow_uptime_sensors, unique_id_fn=lambda controller, obj_id: f"uptime-{obj_id}", value_fn=async_client_uptime_value_fn, ), + UnifiSensorEntityDescription[Wlans, Wlan]( + key="WLAN clients", + entity_category=EntityCategory.DIAGNOSTIC, + has_entity_name=True, + allowed_fn=lambda controller, _: True, + api_handler_fn=lambda api: api.wlans, + available_fn=lambda controller, obj_id: controller.available, + device_info_fn=async_wlan_device_info_fn, + event_is_on=None, + event_to_subscribe=None, + name_fn=lambda client: None, + object_fn=lambda api, obj_id: api.wlans[obj_id], + should_poll=True, + supported_fn=lambda controller, _: True, + unique_id_fn=lambda controller, obj_id: f"wlan_clients-{obj_id}", + value_fn=async_wlan_client_value_fn, + ), ) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index ca11cdfea30eb6..64e3ec2455c15d 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -186,6 +186,7 @@ class UnifiSwitchEntityDescription( name_fn=lambda client: None, object_fn=lambda api, obj_id: api.clients[obj_id], only_event_for_state_change=True, + should_poll=False, supported_fn=lambda controller, obj_id: True, unique_id_fn=lambda controller, obj_id: f"block-{obj_id}", ), @@ -204,6 +205,7 @@ class UnifiSwitchEntityDescription( is_on_fn=async_dpi_group_is_on_fn, name_fn=lambda group: group.name, object_fn=lambda api, obj_id: api.dpi_groups[obj_id], + should_poll=False, supported_fn=lambda c, obj_id: bool(c.api.dpi_groups[obj_id].dpiapp_ids), unique_id_fn=lambda controller, obj_id: obj_id, ), @@ -221,6 +223,7 @@ class UnifiSwitchEntityDescription( is_on_fn=lambda controller, outlet: outlet.relay_state, name_fn=lambda outlet: outlet.name, object_fn=lambda api, obj_id: api.outlets[obj_id], + should_poll=False, supported_fn=lambda c, obj_id: c.api.outlets[obj_id].has_relay, unique_id_fn=lambda controller, obj_id: f"{obj_id.split('_', 1)[0]}-outlet-{obj_id.split('_', 1)[1]}", ), @@ -241,6 +244,7 @@ class UnifiSwitchEntityDescription( is_on_fn=lambda controller, port: port.poe_mode != "off", name_fn=lambda port: f"{port.name} PoE", object_fn=lambda api, obj_id: api.ports[obj_id], + should_poll=False, supported_fn=lambda controller, obj_id: controller.api.ports[obj_id].port_poe, unique_id_fn=lambda controller, obj_id: f"{obj_id.split('_', 1)[0]}-poe-{obj_id.split('_', 1)[1]}", ), @@ -260,6 +264,7 @@ class UnifiSwitchEntityDescription( is_on_fn=lambda controller, wlan: wlan.enabled, name_fn=lambda wlan: None, object_fn=lambda api, obj_id: api.wlans[obj_id], + should_poll=False, supported_fn=lambda controller, obj_id: True, unique_id_fn=lambda controller, obj_id: f"wlan-{obj_id}", ), diff --git a/homeassistant/components/unifi/update.py b/homeassistant/components/unifi/update.py index ea02b144a2f77d..661a9016bdc7a8 100644 --- a/homeassistant/components/unifi/update.py +++ b/homeassistant/components/unifi/update.py @@ -74,6 +74,7 @@ class UnifiUpdateEntityDescription( event_to_subscribe=None, name_fn=lambda device: None, object_fn=lambda api, obj_id: api.devices[obj_id], + should_poll=False, state_fn=lambda api, device: device.state == 4, supported_fn=lambda controller, obj_id: True, unique_id_fn=lambda controller, obj_id: f"device_update-{obj_id}", diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index bf7ba4d53c08bb..d619cd4c3c94a0 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL from homeassistant.helpers.entity_registry import RegistryEntryDisabler import homeassistant.util.dt as dt_util @@ -95,6 +96,42 @@ "version": "4.0.42.10433", } +WLAN = { + "_id": "012345678910111213141516", + "bc_filter_enabled": False, + "bc_filter_list": [], + "dtim_mode": "default", + "dtim_na": 1, + "dtim_ng": 1, + "enabled": True, + "group_rekey": 3600, + "mac_filter_enabled": False, + "mac_filter_list": [], + "mac_filter_policy": "allow", + "minrate_na_advertising_rates": False, + "minrate_na_beacon_rate_kbps": 6000, + "minrate_na_data_rate_kbps": 6000, + "minrate_na_enabled": False, + "minrate_na_mgmt_rate_kbps": 6000, + "minrate_ng_advertising_rates": False, + "minrate_ng_beacon_rate_kbps": 1000, + "minrate_ng_data_rate_kbps": 1000, + "minrate_ng_enabled": False, + "minrate_ng_mgmt_rate_kbps": 1000, + "name": "SSID 1", + "no2ghz_oui": False, + "schedule": [], + "security": "wpapsk", + "site_id": "5a32aa4ee4b0412345678910", + "usergroup_id": "012345678910111213141518", + "wep_idx": 1, + "wlangroup_id": "012345678910111213141519", + "wpa_enc": "ccmp", + "wpa_mode": "wpa2", + "x_iapp_key": "01234567891011121314151617181920", + "x_passphrase": "password", +} + async def test_no_clients( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker @@ -424,3 +461,90 @@ async def test_poe_port_switches( mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() assert hass.states.get("sensor.mock_name_port_1_poe_power") + + +async def test_wlan_client_sensors( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket +) -> None: + """Verify that WLAN client sensors are working as expected.""" + wireless_client_1 = { + "essid": "SSID 1", + "is_wired": False, + "mac": "00:00:00:00:00:01", + "name": "Wireless client", + "oui": "Producer", + "rx_bytes-r": 2345000000, + "tx_bytes-r": 6789000000, + } + wireless_client_2 = { + "essid": "SSID 2", + "is_wired": False, + "mac": "00:00:00:00:00:02", + "name": "Wireless client2", + "oui": "Producer2", + "rx_bytes-r": 2345000000, + "tx_bytes-r": 6789000000, + } + + await setup_unifi_integration( + hass, + aioclient_mock, + clients_response=[wireless_client_1, wireless_client_2], + wlans_response=[WLAN], + ) + + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 + + ent_reg = er.async_get(hass) + ent_reg_entry = ent_reg.async_get("sensor.ssid_1") + assert ent_reg_entry.unique_id == "wlan_clients-012345678910111213141516" + assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC + + # Validate state object + ssid_1 = hass.states.get("sensor.ssid_1") + assert ssid_1 is not None + assert ssid_1.state == "1" + + # Verify state update - increasing number + + wireless_client_1["essid"] = "SSID 1" + wireless_client_2["essid"] = "SSID 1" + + mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client_1) + mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client_2) + await hass.async_block_till_done() + + ssid_1 = hass.states.get("sensor.ssid_1") + assert ssid_1.state == "1" + + async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + ssid_1 = hass.states.get("sensor.ssid_1") + assert ssid_1.state == "2" + + # Verify state update - decreasing number + + wireless_client_1["essid"] = "SSID" + wireless_client_2["essid"] = "SSID" + + mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client_1) + mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client_2) + + async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + ssid_1 = hass.states.get("sensor.ssid_1") + assert ssid_1.state == "0" + + # Availability signalling + + # Controller disconnects + mock_unifi_websocket(state=WebsocketState.DISCONNECTED) + await hass.async_block_till_done() + assert hass.states.get("sensor.ssid_1").state == STATE_UNAVAILABLE + + # Controller reconnects + mock_unifi_websocket(state=WebsocketState.RUNNING) + await hass.async_block_till_done() + assert hass.states.get("sensor.ssid_1").state == "0" From 70b1083c8f353a5c69b6b7100ec6a0d3747a2249 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Jul 2023 01:06:24 -0500 Subject: [PATCH 0924/1009] Bump pyunifiprotect to 4.10.6 (#97240) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 95353e84754935..5f2f58ce98aa88 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -41,7 +41,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==4.10.5", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.10.6", "unifi-discovery==1.1.7"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 6b77bbc03b9ece..9830e646566db6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2191,7 +2191,7 @@ pytrafikverket==0.3.3 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.10.5 +pyunifiprotect==4.10.6 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1320097746a44d..1ed9ff34b10fe7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1611,7 +1611,7 @@ pytrafikverket==0.3.3 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.10.5 +pyunifiprotect==4.10.6 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From d0512d5b26df6ddf0f2bae6439c6bf93f20490aa Mon Sep 17 00:00:00 2001 From: Amos Yuen Date: Tue, 25 Jul 2023 23:09:50 -0700 Subject: [PATCH 0925/1009] Stop rounding history_stats sensor (#97195) --- .../components/history_stats/sensor.py | 3 +- tests/components/history_stats/test_sensor.py | 55 ++++++++++++------- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 5b1242423c74b7..958f46a5e04e22 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -163,6 +163,7 @@ def __init__( self._process_update() if self._type == CONF_TYPE_TIME: self._attr_device_class = SensorDeviceClass.DURATION + self._attr_suggested_display_precision = 2 @callback def _process_update(self) -> None: @@ -173,7 +174,7 @@ def _process_update(self) -> None: return if self._type == CONF_TYPE_TIME: - self._attr_native_value = round(state.seconds_matched / 3600, 2) + self._attr_native_value = state.seconds_matched / 3600 elif self._type == CONF_TYPE_RATIO: self._attr_native_value = pretty_ratio(state.seconds_matched, state.period) elif self._type == CONF_TYPE_COUNT: diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 28e24b587aaa81..ddd11c0d7683d1 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -334,7 +334,7 @@ def _fake_states(*args, **kwargs): await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.5" + assert round(float(hass.states.get("sensor.sensor1").state), 3) == 0.5 assert hass.states.get("sensor.sensor2").state == "0.0" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "50.0" @@ -413,8 +413,8 @@ def _fake_states(*args, **kwargs): await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.83" - assert hass.states.get("sensor.sensor2").state == "0.83" + assert hass.states.get("sensor.sensor1").state == "0.833333333333333" + assert hass.states.get("sensor.sensor2").state == "0.833333333333333" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -769,7 +769,7 @@ def _fake_states(*args, **kwargs): async_fire_time_changed(hass, next_update_time) await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "1.53" + assert hass.states.get("sensor.sensor1").state == "1.53333333333333" end_time = start_time + timedelta(minutes=120) with freeze_time(end_time): @@ -1011,7 +1011,7 @@ def _fake_states(*args, **kwargs): async_fire_time_changed(hass, in_the_window) await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.08" + assert hass.states.get("sensor.sensor1").state == "0.0833333333333333" past_the_window = start_time + timedelta(hours=25) with patch( @@ -1175,8 +1175,8 @@ def _fake_states(*args, **kwargs): await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.83" - assert hass.states.get("sensor.sensor2").state == "0.83" + assert hass.states.get("sensor.sensor1").state == "0.833333333333333" + assert hass.states.get("sensor.sensor2").state == "0.833333333333333" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "41.7" @@ -1188,8 +1188,8 @@ def _fake_states(*args, **kwargs): async_fire_time_changed(hass, past_next_update) await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.83" - assert hass.states.get("sensor.sensor2").state == "0.83" + assert hass.states.get("sensor.sensor1").state == "0.833333333333333" + assert hass.states.get("sensor.sensor2").state == "0.833333333333333" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "41.7" @@ -1269,8 +1269,8 @@ def _fake_states(*args, **kwargs): await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.83" - assert hass.states.get("sensor.sensor2").state == "0.83" + assert hass.states.get("sensor.sensor1").state == "0.833333333333333" + assert hass.states.get("sensor.sensor2").state == "0.833333333333333" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -1282,8 +1282,8 @@ def _fake_states(*args, **kwargs): async_fire_time_changed(hass, past_next_update) await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.83" - assert hass.states.get("sensor.sensor2").state == "0.83" + assert hass.states.get("sensor.sensor1").state == "0.833333333333333" + assert hass.states.get("sensor.sensor2").state == "0.833333333333333" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -1362,8 +1362,8 @@ def _fake_states(*args, **kwargs): await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.83" - assert hass.states.get("sensor.sensor2").state == "0.83" + assert hass.states.get("sensor.sensor1").state == "0.833333333333333" + assert hass.states.get("sensor.sensor2").state == "0.833333333333333" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -1431,10 +1431,16 @@ def _fake_states(*args, **kwargs): await hass.async_block_till_done() await async_update_entity(hass, "sensor.heatpump_compressor_today") await hass.async_block_till_done() - assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83" + assert ( + hass.states.get("sensor.heatpump_compressor_today").state + == "1.83333333333333" + ) async_fire_time_changed(hass, time_200) await hass.async_block_till_done() - assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83" + assert ( + hass.states.get("sensor.heatpump_compressor_today").state + == "1.83333333333333" + ) hass.states.async_set("binary_sensor.heatpump_compressor_state", "off") await hass.async_block_till_done() @@ -1442,14 +1448,20 @@ def _fake_states(*args, **kwargs): with freeze_time(time_400): async_fire_time_changed(hass, time_400) await hass.async_block_till_done() - assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83" + assert ( + hass.states.get("sensor.heatpump_compressor_today").state + == "1.83333333333333" + ) hass.states.async_set("binary_sensor.heatpump_compressor_state", "on") await async_wait_recording_done(hass) time_600 = start_of_today + timedelta(hours=6) with freeze_time(time_600): async_fire_time_changed(hass, time_600) await hass.async_block_till_done() - assert hass.states.get("sensor.heatpump_compressor_today").state == "3.83" + assert ( + hass.states.get("sensor.heatpump_compressor_today").state + == "3.83333333333333" + ) rolled_to_next_day = start_of_today + timedelta(days=1) assert rolled_to_next_day.hour == 0 @@ -1491,7 +1503,10 @@ def _fake_states(*args, **kwargs): with freeze_time(rolled_to_next_day_plus_18): async_fire_time_changed(hass, rolled_to_next_day_plus_18) await hass.async_block_till_done() - assert hass.states.get("sensor.heatpump_compressor_today").state == "16.0" + assert ( + hass.states.get("sensor.heatpump_compressor_today").state + == "16.0002388888929" + ) async def test_device_classes(recorder_mock: Recorder, hass: HomeAssistant) -> None: From c0debaf26e750baeae6392711a1ee25693ff11f6 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 26 Jul 2023 07:20:41 +0100 Subject: [PATCH 0926/1009] Add event entities to homekit_controller (#97140) Co-authored-by: Franck Nijhof --- .../homekit_controller/connection.py | 29 +++ .../components/homekit_controller/const.py | 3 + .../homekit_controller/device_trigger.py | 4 +- .../components/homekit_controller/event.py | 160 +++++++++++++++ .../homekit_controller/strings.json | 24 +++ .../homekit_controller/test_event.py | 183 ++++++++++++++++++ 6 files changed, 401 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/homekit_controller/event.py create mode 100644 tests/components/homekit_controller/test_event.py diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 6ef5917a0fb1c4..d101517e002541 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -95,6 +95,13 @@ def __init__( # A list of callbacks that turn HK service metadata into entities self.listeners: list[AddServiceCb] = [] + # A list of callbacks that turn HK service metadata into triggers + self.trigger_factories: list[AddServiceCb] = [] + + # Track aid/iid pairs so we know if we already handle triggers for a HK + # service. + self._triggers: list[tuple[int, int]] = [] + # A list of callbacks that turn HK characteristics into entities self.char_factories: list[AddCharacteristicCb] = [] @@ -637,11 +644,33 @@ def add_listener(self, add_entities_cb: AddServiceCb) -> None: self.listeners.append(add_entities_cb) self._add_new_entities([add_entities_cb]) + def add_trigger_factory(self, add_triggers_cb: AddServiceCb) -> None: + """Add a callback to run when discovering new triggers for services.""" + self.trigger_factories.append(add_triggers_cb) + self._add_new_triggers([add_triggers_cb]) + + def _add_new_triggers(self, callbacks: list[AddServiceCb]) -> None: + for accessory in self.entity_map.accessories: + aid = accessory.aid + for service in accessory.services: + iid = service.iid + entity_key = (aid, iid) + + if entity_key in self._triggers: + # Don't add the same trigger again + continue + + for add_trigger_cb in callbacks: + if add_trigger_cb(service): + self._triggers.append(entity_key) + break + def add_entities(self) -> None: """Process the entity map and create HA entities.""" self._add_new_entities(self.listeners) self._add_new_entities_for_accessory(self.accessory_factories) self._add_new_entities_for_char(self.char_factories) + self._add_new_triggers(self.trigger_factories) def _add_new_entities(self, callbacks) -> None: for accessory in self.entity_map.accessories: diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 0dfaf6e538cb50..cde9aa732c3f92 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -53,6 +53,9 @@ ServicesTypes.TELEVISION: "media_player", ServicesTypes.VALVE: "switch", ServicesTypes.CAMERA_RTP_STREAM_MANAGEMENT: "camera", + ServicesTypes.DOORBELL: "event", + ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH: "event", + ServicesTypes.SERVICE_LABEL: "event", } CHARACTERISTIC_PLATFORMS = { diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index 229c8aecc00f75..bbc56ddd4a4cac 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -211,7 +211,7 @@ async def async_setup_triggers_for_entry( conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(service): + def async_add_characteristic(service: Service): aid = service.accessory.aid service_type = service.type @@ -238,7 +238,7 @@ def async_add_service(service): return True - conn.add_listener(async_add_service) + conn.add_trigger_factory(async_add_characteristic) @callback diff --git a/homeassistant/components/homekit_controller/event.py b/homeassistant/components/homekit_controller/event.py new file mode 100644 index 00000000000000..9d70127f74af83 --- /dev/null +++ b/homeassistant/components/homekit_controller/event.py @@ -0,0 +1,160 @@ +"""Support for Homekit motion sensors.""" +from __future__ import annotations + +from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.characteristics.const import InputEventValues +from aiohomekit.model.services import Service, ServicesTypes +from aiohomekit.utils import clamp_enum_to_char + +from homeassistant.components.event import ( + EventDeviceClass, + EventEntity, + EventEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import KNOWN_DEVICES +from .connection import HKDevice +from .entity import HomeKitEntity + +INPUT_EVENT_VALUES = { + InputEventValues.SINGLE_PRESS: "single_press", + InputEventValues.DOUBLE_PRESS: "double_press", + InputEventValues.LONG_PRESS: "long_press", +} + + +class HomeKitEventEntity(HomeKitEntity, EventEntity): + """Representation of a Homekit event entity.""" + + _attr_should_poll = False + + def __init__( + self, + connection: HKDevice, + service: Service, + entity_description: EventEntityDescription, + ) -> None: + """Initialise a generic HomeKit event entity.""" + super().__init__( + connection, + { + "aid": service.accessory.aid, + "iid": service.iid, + }, + ) + self._characteristic = service.characteristics_by_type[ + CharacteristicsTypes.INPUT_EVENT + ] + + self.entity_description = entity_description + + # An INPUT_EVENT may support single_press, long_press and double_press. All are optional. So we have to + # clamp InputEventValues for this exact device + self._attr_event_types = [ + INPUT_EVENT_VALUES[v] + for v in clamp_enum_to_char(InputEventValues, self._characteristic) + ] + + def get_characteristic_types(self) -> list[str]: + """Define the homekit characteristics the entity cares about.""" + return [CharacteristicsTypes.INPUT_EVENT] + + async def async_added_to_hass(self) -> None: + """Entity added to hass.""" + await super().async_added_to_hass() + + self.async_on_remove( + self._accessory.async_subscribe( + [(self._aid, self._characteristic.iid)], + self._handle_event, + ) + ) + + @callback + def _handle_event(self): + if self._characteristic.value is None: + # For IP backed devices the characteristic is marked as + # pollable, but always returns None when polled + # Make sure we don't explode if we see that edge case. + return + self._trigger_event(INPUT_EVENT_VALUES[self._characteristic.value]) + self.async_write_ha_state() + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Homekit event.""" + hkid: str = config_entry.data["AccessoryPairingID"] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] + + @callback + def async_add_service(service: Service) -> bool: + entities = [] + + if service.type == ServicesTypes.DOORBELL: + entities.append( + HomeKitEventEntity( + conn, + service, + EventEntityDescription( + key=f"{service.accessory.aid}_{service.iid}", + device_class=EventDeviceClass.DOORBELL, + translation_key="doorbell", + ), + ) + ) + + elif service.type == ServicesTypes.SERVICE_LABEL: + switches = list( + service.accessory.services.filter( + service_type=ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH, + child_service=service, + order_by=[CharacteristicsTypes.SERVICE_LABEL_INDEX], + ) + ) + + for switch in switches: + # The Apple docs say that if we number the buttons ourselves + # We do it in service label index order. `switches` is already in + # that order. + entities.append( + HomeKitEventEntity( + conn, + switch, + EventEntityDescription( + key=f"{service.accessory.aid}_{service.iid}", + device_class=EventDeviceClass.BUTTON, + translation_key="button", + ), + ) + ) + + elif service.type == ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH: + # A stateless switch that has a SERVICE_LABEL_INDEX is part of a group + # And is handled separately + if not service.has(CharacteristicsTypes.SERVICE_LABEL_INDEX): + entities.append( + HomeKitEventEntity( + conn, + service, + EventEntityDescription( + key=f"{service.accessory.aid}_{service.iid}", + device_class=EventDeviceClass.BUTTON, + translation_key="button", + ), + ) + ) + + if entities: + async_add_entities(entities) + return True + + return False + + conn.add_listener(async_add_service) diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index e47ae0fca84246..901378c8cb900b 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -71,6 +71,30 @@ } }, "entity": { + "event": { + "doorbell": { + "state_attributes": { + "event_type": { + "state": { + "double_press": "Double press", + "long_press": "Long press", + "single_press": "Single press" + } + } + } + }, + "button": { + "state_attributes": { + "event_type": { + "state": { + "double_press": "[%key:component::homekit_controller::entity::event::doorbell::state_attributes::event_type::state::double_press%]", + "long_press": "[%key:component::homekit_controller::entity::event::doorbell::state_attributes::event_type::state::long_press%]", + "single_press": "[%key:component::homekit_controller::entity::event::doorbell::state_attributes::event_type::state::single_press%]" + } + } + } + } + }, "select": { "ecobee_mode": { "state": { diff --git a/tests/components/homekit_controller/test_event.py b/tests/components/homekit_controller/test_event.py new file mode 100644 index 00000000000000..9731f429eaf092 --- /dev/null +++ b/tests/components/homekit_controller/test_event.py @@ -0,0 +1,183 @@ +"""Test homekit_controller stateless triggers.""" +from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes + +from homeassistant.components.event import EventDeviceClass +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .common import setup_test_component + + +def create_remote(accessory): + """Define characteristics for a button (that is inn a group).""" + service_label = accessory.add_service(ServicesTypes.SERVICE_LABEL) + + char = service_label.add_char(CharacteristicsTypes.SERVICE_LABEL_NAMESPACE) + char.value = 1 + + for i in range(4): + button = accessory.add_service(ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH) + button.linked.append(service_label) + + char = button.add_char(CharacteristicsTypes.INPUT_EVENT) + char.value = 0 + char.perms = ["pw", "pr", "ev"] + + char = button.add_char(CharacteristicsTypes.NAME) + char.value = f"Button {i + 1}" + + char = button.add_char(CharacteristicsTypes.SERVICE_LABEL_INDEX) + char.value = i + + battery = accessory.add_service(ServicesTypes.BATTERY_SERVICE) + battery.add_char(CharacteristicsTypes.BATTERY_LEVEL) + + +def create_button(accessory): + """Define a button (that is not in a group).""" + button = accessory.add_service(ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH) + + char = button.add_char(CharacteristicsTypes.INPUT_EVENT) + char.value = 0 + char.perms = ["pw", "pr", "ev"] + + char = button.add_char(CharacteristicsTypes.NAME) + char.value = "Button 1" + + battery = accessory.add_service(ServicesTypes.BATTERY_SERVICE) + battery.add_char(CharacteristicsTypes.BATTERY_LEVEL) + + +def create_doorbell(accessory): + """Define a button (that is not in a group).""" + button = accessory.add_service(ServicesTypes.DOORBELL) + + char = button.add_char(CharacteristicsTypes.INPUT_EVENT) + char.value = 0 + char.perms = ["pw", "pr", "ev"] + + char = button.add_char(CharacteristicsTypes.NAME) + char.value = "Doorbell" + + battery = accessory.add_service(ServicesTypes.BATTERY_SERVICE) + battery.add_char(CharacteristicsTypes.BATTERY_LEVEL) + + +async def test_remote(hass: HomeAssistant, utcnow) -> None: + """Test that remote is supported.""" + helper = await setup_test_component(hass, create_remote) + + entities = [ + ("event.testdevice_button_1", "Button 1"), + ("event.testdevice_button_2", "Button 2"), + ("event.testdevice_button_3", "Button 3"), + ("event.testdevice_button_4", "Button 4"), + ] + + entity_registry = er.async_get(hass) + + for entity_id, service in entities: + button = entity_registry.async_get(entity_id) + + assert button.original_device_class == EventDeviceClass.BUTTON + assert button.capabilities["event_types"] == [ + "single_press", + "double_press", + "long_press", + ] + + helper.pairing.testing.update_named_service( + service, {CharacteristicsTypes.INPUT_EVENT: 0} + ) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes["event_type"] == "single_press" + + helper.pairing.testing.update_named_service( + service, {CharacteristicsTypes.INPUT_EVENT: 1} + ) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes["event_type"] == "double_press" + + helper.pairing.testing.update_named_service( + service, {CharacteristicsTypes.INPUT_EVENT: 2} + ) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes["event_type"] == "long_press" + + +async def test_button(hass: HomeAssistant, utcnow) -> None: + """Test that a button is correctly enumerated.""" + helper = await setup_test_component(hass, create_button) + entity_id = "event.testdevice_button_1" + + entity_registry = er.async_get(hass) + button = entity_registry.async_get(entity_id) + + assert button.original_device_class == EventDeviceClass.BUTTON + assert button.capabilities["event_types"] == [ + "single_press", + "double_press", + "long_press", + ] + + helper.pairing.testing.update_named_service( + "Button 1", {CharacteristicsTypes.INPUT_EVENT: 0} + ) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes["event_type"] == "single_press" + + helper.pairing.testing.update_named_service( + "Button 1", {CharacteristicsTypes.INPUT_EVENT: 1} + ) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes["event_type"] == "double_press" + + helper.pairing.testing.update_named_service( + "Button 1", {CharacteristicsTypes.INPUT_EVENT: 2} + ) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes["event_type"] == "long_press" + + +async def test_doorbell(hass: HomeAssistant, utcnow) -> None: + """Test that doorbell service is handled.""" + helper = await setup_test_component(hass, create_doorbell) + entity_id = "event.testdevice_doorbell" + + entity_registry = er.async_get(hass) + doorbell = entity_registry.async_get(entity_id) + + assert doorbell.original_device_class == EventDeviceClass.DOORBELL + assert doorbell.capabilities["event_types"] == [ + "single_press", + "double_press", + "long_press", + ] + + helper.pairing.testing.update_named_service( + "Doorbell", {CharacteristicsTypes.INPUT_EVENT: 0} + ) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes["event_type"] == "single_press" + + helper.pairing.testing.update_named_service( + "Doorbell", {CharacteristicsTypes.INPUT_EVENT: 1} + ) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes["event_type"] == "double_press" + + helper.pairing.testing.update_named_service( + "Doorbell", {CharacteristicsTypes.INPUT_EVENT: 2} + ) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes["event_type"] == "long_press" From b4a46b981717bb79dd3294cbb4f69df7a8f0a37f Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Wed, 26 Jul 2023 09:07:47 +0200 Subject: [PATCH 0927/1009] Codeowner update for cert-expiry (#97246) --- CODEOWNERS | 4 ++-- homeassistant/components/cert_expiry/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 8a72fadfbd9c40..7f925f698097d7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -195,8 +195,8 @@ build.json @home-assistant/supervisor /tests/components/camera/ @home-assistant/core /homeassistant/components/cast/ @emontnemery /tests/components/cast/ @emontnemery -/homeassistant/components/cert_expiry/ @Cereal2nd @jjlawren -/tests/components/cert_expiry/ @Cereal2nd @jjlawren +/homeassistant/components/cert_expiry/ @jjlawren +/tests/components/cert_expiry/ @jjlawren /homeassistant/components/circuit/ @braam /homeassistant/components/cisco_ios/ @fbradyirl /homeassistant/components/cisco_mobility_express/ @fbradyirl diff --git a/homeassistant/components/cert_expiry/manifest.json b/homeassistant/components/cert_expiry/manifest.json index 5125f69d03a5d9..df135b65bbeac3 100644 --- a/homeassistant/components/cert_expiry/manifest.json +++ b/homeassistant/components/cert_expiry/manifest.json @@ -1,7 +1,7 @@ { "domain": "cert_expiry", "name": "Certificate Expiry", - "codeowners": ["@Cereal2nd", "@jjlawren"], + "codeowners": ["@jjlawren"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cert_expiry", "iot_class": "cloud_polling" From 5caa1969c55a629eac0a6d76a64cf7caa842d1cf Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 26 Jul 2023 09:12:39 +0200 Subject: [PATCH 0928/1009] Add Pegel Online integration (#97028) --- CODEOWNERS | 2 + .../components/pegel_online/__init__.py | 49 ++++ .../components/pegel_online/config_flow.py | 134 +++++++++++ .../components/pegel_online/const.py | 9 + .../components/pegel_online/coordinator.py | 40 ++++ .../components/pegel_online/entity.py | 31 +++ .../components/pegel_online/manifest.json | 11 + .../components/pegel_online/model.py | 11 + .../components/pegel_online/sensor.py | 89 ++++++++ .../components/pegel_online/strings.json | 34 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/pegel_online/__init__.py | 40 ++++ .../pegel_online/test_config_flow.py | 209 ++++++++++++++++++ tests/components/pegel_online/test_init.py | 63 ++++++ tests/components/pegel_online/test_sensor.py | 53 +++++ 18 files changed, 788 insertions(+) create mode 100644 homeassistant/components/pegel_online/__init__.py create mode 100644 homeassistant/components/pegel_online/config_flow.py create mode 100644 homeassistant/components/pegel_online/const.py create mode 100644 homeassistant/components/pegel_online/coordinator.py create mode 100644 homeassistant/components/pegel_online/entity.py create mode 100644 homeassistant/components/pegel_online/manifest.json create mode 100644 homeassistant/components/pegel_online/model.py create mode 100644 homeassistant/components/pegel_online/sensor.py create mode 100644 homeassistant/components/pegel_online/strings.json create mode 100644 tests/components/pegel_online/__init__.py create mode 100644 tests/components/pegel_online/test_config_flow.py create mode 100644 tests/components/pegel_online/test_init.py create mode 100644 tests/components/pegel_online/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 7f925f698097d7..10acd5dd65a6d1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -922,6 +922,8 @@ build.json @home-assistant/supervisor /tests/components/panel_iframe/ @home-assistant/frontend /homeassistant/components/peco/ @IceBotYT /tests/components/peco/ @IceBotYT +/homeassistant/components/pegel_online/ @mib1185 +/tests/components/pegel_online/ @mib1185 /homeassistant/components/persistent_notification/ @home-assistant/core /tests/components/persistent_notification/ @home-assistant/core /homeassistant/components/philips_js/ @elupus diff --git a/homeassistant/components/pegel_online/__init__.py b/homeassistant/components/pegel_online/__init__.py new file mode 100644 index 00000000000000..a2767cb749bbdc --- /dev/null +++ b/homeassistant/components/pegel_online/__init__.py @@ -0,0 +1,49 @@ +"""The PEGELONLINE component.""" +from __future__ import annotations + +import logging + +from aiopegelonline import PegelOnline + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ( + CONF_STATION, + DOMAIN, +) +from .coordinator import PegelOnlineDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up PEGELONLINE entry.""" + station_uuid = entry.data[CONF_STATION] + + _LOGGER.debug("Setting up station with uuid %s", station_uuid) + + api = PegelOnline(async_get_clientsession(hass)) + station = await api.async_get_station_details(station_uuid) + + coordinator = PegelOnlineDataUpdateCoordinator(hass, entry.title, api, station) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload PEGELONLINE entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/pegel_online/config_flow.py b/homeassistant/components/pegel_online/config_flow.py new file mode 100644 index 00000000000000..a72e450e2e572b --- /dev/null +++ b/homeassistant/components/pegel_online/config_flow.py @@ -0,0 +1,134 @@ +"""Config flow for PEGELONLINE.""" +from __future__ import annotations + +from typing import Any + +from aiopegelonline import CONNECT_ERRORS, PegelOnline +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LOCATION, + CONF_LONGITUDE, + CONF_RADIUS, + UnitOfLength, +) +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.selector import ( + LocationSelector, + NumberSelector, + NumberSelectorConfig, + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, +) + +from .const import CONF_STATION, DEFAULT_RADIUS, DOMAIN + + +class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + + def __init__(self) -> None: + """Init the FlowHandler.""" + super().__init__() + self._data: dict[str, Any] = {} + self._stations: dict[str, str] = {} + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if not user_input: + return self._show_form_user() + + api = PegelOnline(async_get_clientsession(self.hass)) + try: + stations = await api.async_get_nearby_stations( + user_input[CONF_LOCATION][CONF_LATITUDE], + user_input[CONF_LOCATION][CONF_LONGITUDE], + user_input[CONF_RADIUS], + ) + except CONNECT_ERRORS: + return self._show_form_user(user_input, errors={"base": "cannot_connect"}) + + if len(stations) == 0: + return self._show_form_user(user_input, errors={CONF_RADIUS: "no_stations"}) + + for uuid, station in stations.items(): + self._stations[uuid] = f"{station.name} {station.water_name}" + + self._data = user_input + + return await self.async_step_select_station() + + async def async_step_select_station( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the step select_station of a flow initialized by the user.""" + if not user_input: + stations = [ + SelectOptionDict(value=k, label=v) for k, v in self._stations.items() + ] + return self.async_show_form( + step_id="select_station", + description_placeholders={"stations_count": str(len(self._stations))}, + data_schema=vol.Schema( + { + vol.Required(CONF_STATION): SelectSelector( + SelectSelectorConfig( + options=stations, mode=SelectSelectorMode.DROPDOWN + ) + ) + } + ), + ) + + await self.async_set_unique_id(user_input[CONF_STATION]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=self._stations[user_input[CONF_STATION]], + data=user_input, + ) + + def _show_form_user( + self, + user_input: dict[str, Any] | None = None, + errors: dict[str, Any] | None = None, + ) -> FlowResult: + if user_input is None: + user_input = {} + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_LOCATION, + default=user_input.get( + CONF_LOCATION, + { + "latitude": self.hass.config.latitude, + "longitude": self.hass.config.longitude, + }, + ), + ): LocationSelector(), + vol.Required( + CONF_RADIUS, default=user_input.get(CONF_RADIUS, DEFAULT_RADIUS) + ): NumberSelector( + NumberSelectorConfig( + min=1, + max=100, + step=1, + unit_of_measurement=UnitOfLength.KILOMETERS, + ), + ), + } + ), + errors=errors, + ) diff --git a/homeassistant/components/pegel_online/const.py b/homeassistant/components/pegel_online/const.py new file mode 100644 index 00000000000000..1e6c26a057b9dd --- /dev/null +++ b/homeassistant/components/pegel_online/const.py @@ -0,0 +1,9 @@ +"""Constants for PEGELONLINE.""" +from datetime import timedelta + +DOMAIN = "pegel_online" + +DEFAULT_RADIUS = "25" +CONF_STATION = "station" + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) diff --git a/homeassistant/components/pegel_online/coordinator.py b/homeassistant/components/pegel_online/coordinator.py new file mode 100644 index 00000000000000..995953c5e36eda --- /dev/null +++ b/homeassistant/components/pegel_online/coordinator.py @@ -0,0 +1,40 @@ +"""DataUpdateCoordinator for pegel_online.""" +import logging + +from aiopegelonline import CONNECT_ERRORS, PegelOnline, Station + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import MIN_TIME_BETWEEN_UPDATES +from .model import PegelOnlineData + +_LOGGER = logging.getLogger(__name__) + + +class PegelOnlineDataUpdateCoordinator(DataUpdateCoordinator[PegelOnlineData]): + """DataUpdateCoordinator for the pegel_online integration.""" + + def __init__( + self, hass: HomeAssistant, name: str, api: PegelOnline, station: Station + ) -> None: + """Initialize the PegelOnlineDataUpdateCoordinator.""" + self.api = api + self.station = station + super().__init__( + hass, + _LOGGER, + name=name, + update_interval=MIN_TIME_BETWEEN_UPDATES, + ) + + async def _async_update_data(self) -> PegelOnlineData: + """Fetch data from API endpoint.""" + try: + current_measurement = await self.api.async_get_station_measurement( + self.station.uuid + ) + except CONNECT_ERRORS as err: + raise UpdateFailed(f"Failed to communicate with API: {err}") from err + + return {"current_measurement": current_measurement} diff --git a/homeassistant/components/pegel_online/entity.py b/homeassistant/components/pegel_online/entity.py new file mode 100644 index 00000000000000..118392c6a698ec --- /dev/null +++ b/homeassistant/components/pegel_online/entity.py @@ -0,0 +1,31 @@ +"""The PEGELONLINE base entity.""" +from __future__ import annotations + +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import PegelOnlineDataUpdateCoordinator + + +class PegelOnlineEntity(CoordinatorEntity): + """Representation of a PEGELONLINE entity.""" + + _attr_has_entity_name = True + _attr_available = True + + def __init__(self, coordinator: PegelOnlineDataUpdateCoordinator) -> None: + """Initialize a PEGELONLINE entity.""" + super().__init__(coordinator) + self.station = coordinator.station + self._attr_extra_state_attributes = {} + + @property + def device_info(self) -> DeviceInfo: + """Return the device information of the entity.""" + return DeviceInfo( + identifiers={(DOMAIN, self.station.uuid)}, + name=f"{self.station.name} {self.station.water_name}", + manufacturer=self.station.agency, + configuration_url=self.station.base_data_url, + ) diff --git a/homeassistant/components/pegel_online/manifest.json b/homeassistant/components/pegel_online/manifest.json new file mode 100644 index 00000000000000..a51954496cdd18 --- /dev/null +++ b/homeassistant/components/pegel_online/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "pegel_online", + "name": "PEGELONLINE", + "codeowners": ["@mib1185"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/pegel_online", + "integration_type": "service", + "iot_class": "cloud_polling", + "loggers": ["aiopegelonline"], + "requirements": ["aiopegelonline==0.0.5"] +} diff --git a/homeassistant/components/pegel_online/model.py b/homeassistant/components/pegel_online/model.py new file mode 100644 index 00000000000000..c1760d3261b6a8 --- /dev/null +++ b/homeassistant/components/pegel_online/model.py @@ -0,0 +1,11 @@ +"""Models for PEGELONLINE.""" + +from typing import TypedDict + +from aiopegelonline import CurrentMeasurement + + +class PegelOnlineData(TypedDict): + """TypedDict for PEGELONLINE Coordinator Data.""" + + current_measurement: CurrentMeasurement diff --git a/homeassistant/components/pegel_online/sensor.py b/homeassistant/components/pegel_online/sensor.py new file mode 100644 index 00000000000000..7d48635781bc7d --- /dev/null +++ b/homeassistant/components/pegel_online/sensor.py @@ -0,0 +1,89 @@ +"""PEGELONLINE sensor entities.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import PegelOnlineDataUpdateCoordinator +from .entity import PegelOnlineEntity +from .model import PegelOnlineData + + +@dataclass +class PegelOnlineRequiredKeysMixin: + """Mixin for required keys.""" + + fn_native_unit: Callable[[PegelOnlineData], str] + fn_native_value: Callable[[PegelOnlineData], float] + + +@dataclass +class PegelOnlineSensorEntityDescription( + SensorEntityDescription, PegelOnlineRequiredKeysMixin +): + """PEGELONLINE sensor entity description.""" + + +SENSORS: tuple[PegelOnlineSensorEntityDescription, ...] = ( + PegelOnlineSensorEntityDescription( + key="current_measurement", + translation_key="current_measurement", + state_class=SensorStateClass.MEASUREMENT, + fn_native_unit=lambda data: data["current_measurement"].uom, + fn_native_value=lambda data: data["current_measurement"].value, + icon="mdi:waves-arrow-up", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the PEGELONLINE sensor.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + [PegelOnlineSensor(coordinator, description) for description in SENSORS] + ) + + +class PegelOnlineSensor(PegelOnlineEntity, SensorEntity): + """Representation of a PEGELONLINE sensor.""" + + entity_description: PegelOnlineSensorEntityDescription + + def __init__( + self, + coordinator: PegelOnlineDataUpdateCoordinator, + description: PegelOnlineSensorEntityDescription, + ) -> None: + """Initialize a PEGELONLINE sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{self.station.uuid}_{description.key}" + self._attr_native_unit_of_measurement = self.entity_description.fn_native_unit( + coordinator.data + ) + + if self.station.latitude and self.station.longitude: + self._attr_extra_state_attributes.update( + { + ATTR_LATITUDE: self.station.latitude, + ATTR_LONGITUDE: self.station.longitude, + } + ) + + @property + def native_value(self) -> float: + """Return the state of the device.""" + return self.entity_description.fn_native_value(self.coordinator.data) diff --git a/homeassistant/components/pegel_online/strings.json b/homeassistant/components/pegel_online/strings.json new file mode 100644 index 00000000000000..71ec95f825cd32 --- /dev/null +++ b/homeassistant/components/pegel_online/strings.json @@ -0,0 +1,34 @@ +{ + "config": { + "step": { + "user": { + "description": "Select the area, where you want to search for water measuring stations", + "data": { + "location": "[%key:common::config_flow::data::location%]", + "radius": "Search radius (in km)" + } + }, + "select_station": { + "title": "Select the measuring station to add", + "description": "Found {stations_count} stations in radius", + "data": { + "station": "Station" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "no_stations": "Could not find any station in range." + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + }, + "entity": { + "sensor": { + "current_measurement": { + "name": "Water level" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2359ac79e040f7..10221d1d58989a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -338,6 +338,7 @@ "p1_monitor", "panasonic_viera", "peco", + "pegel_online", "philips_js", "pi_hole", "picnic", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 938ffa13ab541c..85138b82c821dc 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4136,6 +4136,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "pegel_online": { + "name": "PEGELONLINE", + "integration_type": "service", + "config_flow": true, + "iot_class": "cloud_polling" + }, "pencom": { "name": "Pencom", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 9830e646566db6..d40375adfc0487 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -303,6 +303,9 @@ aiooncue==0.3.5 # homeassistant.components.openexchangerates aioopenexchangerates==0.4.0 +# homeassistant.components.pegel_online +aiopegelonline==0.0.5 + # homeassistant.components.acmeda aiopulse==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ed9ff34b10fe7..ddb0f2f221a2f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -278,6 +278,9 @@ aiooncue==0.3.5 # homeassistant.components.openexchangerates aioopenexchangerates==0.4.0 +# homeassistant.components.pegel_online +aiopegelonline==0.0.5 + # homeassistant.components.acmeda aiopulse==0.4.3 diff --git a/tests/components/pegel_online/__init__.py b/tests/components/pegel_online/__init__.py new file mode 100644 index 00000000000000..ac3f9bda7dd765 --- /dev/null +++ b/tests/components/pegel_online/__init__.py @@ -0,0 +1,40 @@ +"""Tests for Pegel Online component.""" + + +class PegelOnlineMock: + """Class mock of PegelOnline.""" + + def __init__( + self, + nearby_stations=None, + station_details=None, + station_measurement=None, + side_effect=None, + ) -> None: + """Init the mock.""" + self.nearby_stations = nearby_stations + self.station_details = station_details + self.station_measurement = station_measurement + self.side_effect = side_effect + + async def async_get_nearby_stations(self, *args): + """Mock async_get_nearby_stations.""" + if self.side_effect: + raise self.side_effect + return self.nearby_stations + + async def async_get_station_details(self, *args): + """Mock async_get_station_details.""" + if self.side_effect: + raise self.side_effect + return self.station_details + + async def async_get_station_measurement(self, *args): + """Mock async_get_station_measurement.""" + if self.side_effect: + raise self.side_effect + return self.station_measurement + + def override_side_effect(self, side_effect): + """Override the side_effect.""" + self.side_effect = side_effect diff --git a/tests/components/pegel_online/test_config_flow.py b/tests/components/pegel_online/test_config_flow.py new file mode 100644 index 00000000000000..ffc2f88d5a8e72 --- /dev/null +++ b/tests/components/pegel_online/test_config_flow.py @@ -0,0 +1,209 @@ +"""Tests for Pegel Online config flow.""" +from unittest.mock import patch + +from aiohttp.client_exceptions import ClientError +from aiopegelonline import Station + +from homeassistant.components.pegel_online.const import ( + CONF_STATION, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LOCATION, + CONF_LONGITUDE, + CONF_RADIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import PegelOnlineMock + +from tests.common import MockConfigEntry + +MOCK_USER_DATA_STEP1 = { + CONF_LOCATION: {CONF_LATITUDE: 51.0, CONF_LONGITUDE: 13.0}, + CONF_RADIUS: 25, +} + +MOCK_USER_DATA_STEP2 = {CONF_STATION: "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8"} + +MOCK_CONFIG_ENTRY_DATA = {CONF_STATION: "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8"} + +MOCK_NEARBY_STATIONS = { + "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8": Station( + { + "uuid": "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", + "number": "501060", + "shortname": "DRESDEN", + "longname": "DRESDEN", + "km": 55.63, + "agency": "STANDORT DRESDEN", + "longitude": 13.738831783620384, + "latitude": 51.054459765598125, + "water": {"shortname": "ELBE", "longname": "ELBE"}, + } + ), + "85d686f1-xxxx-xxxx-xxxx-3207b50901a7": Station( + { + "uuid": "85d686f1-xxxx-xxxx-xxxx-3207b50901a7", + "number": "501060", + "shortname": "MEISSEN", + "longname": "MEISSEN", + "km": 82.2, + "agency": "STANDORT DRESDEN", + "longitude": 13.475467710324812, + "latitude": 51.16440557554545, + "water": {"shortname": "ELBE", "longname": "ELBE"}, + } + ), +} + + +async def test_user(hass: HomeAssistant) -> None: + """Test starting a flow by user.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.pegel_online.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.pegel_online.config_flow.PegelOnline", + ) as pegelonline: + pegelonline.return_value = PegelOnlineMock(nearby_stations=MOCK_NEARBY_STATIONS) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA_STEP1 + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "select_station" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA_STEP2 + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"][CONF_STATION] == "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8" + assert result["title"] == "DRESDEN ELBE" + + await hass.async_block_till_done() + + assert mock_setup_entry.called + + +async def test_user_already_configured(hass: HomeAssistant) -> None: + """Test starting a flow by user with an already configured statioon.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG_ENTRY_DATA, + unique_id=MOCK_CONFIG_ENTRY_DATA[CONF_STATION], + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.pegel_online.config_flow.PegelOnline", + ) as pegelonline: + pegelonline.return_value = PegelOnlineMock(nearby_stations=MOCK_NEARBY_STATIONS) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA_STEP1 + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "select_station" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA_STEP2 + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_connection_error(hass: HomeAssistant) -> None: + """Test connection error during user flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.pegel_online.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.pegel_online.config_flow.PegelOnline", + ) as pegelonline: + # connection issue during setup + pegelonline.return_value = PegelOnlineMock(side_effect=ClientError) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA_STEP1 + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "cannot_connect" + + # connection issue solved + pegelonline.return_value = PegelOnlineMock(nearby_stations=MOCK_NEARBY_STATIONS) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA_STEP1 + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "select_station" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA_STEP2 + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"][CONF_STATION] == "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8" + assert result["title"] == "DRESDEN ELBE" + + await hass.async_block_till_done() + + assert mock_setup_entry.called + + +async def test_user_no_stations(hass: HomeAssistant) -> None: + """Test starting a flow by user which does not find any station.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.pegel_online.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.pegel_online.config_flow.PegelOnline", + ) as pegelonline: + # no stations found + pegelonline.return_value = PegelOnlineMock(nearby_stations={}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA_STEP1 + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"][CONF_RADIUS] == "no_stations" + + # stations found, go ahead + pegelonline.return_value = PegelOnlineMock(nearby_stations=MOCK_NEARBY_STATIONS) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA_STEP1 + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "select_station" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA_STEP2 + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"][CONF_STATION] == "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8" + assert result["title"] == "DRESDEN ELBE" + + await hass.async_block_till_done() + + assert mock_setup_entry.called diff --git a/tests/components/pegel_online/test_init.py b/tests/components/pegel_online/test_init.py new file mode 100644 index 00000000000000..93ade37331544f --- /dev/null +++ b/tests/components/pegel_online/test_init.py @@ -0,0 +1,63 @@ +"""Test pegel_online component.""" +from unittest.mock import patch + +from aiohttp.client_exceptions import ClientError +from aiopegelonline import CurrentMeasurement, Station + +from homeassistant.components.pegel_online.const import ( + CONF_STATION, + DOMAIN, + MIN_TIME_BETWEEN_UPDATES, +) +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.util import utcnow + +from . import PegelOnlineMock + +from tests.common import MockConfigEntry, async_fire_time_changed + +MOCK_CONFIG_ENTRY_DATA = {CONF_STATION: "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8"} + +MOCK_STATION_DETAILS = Station( + { + "uuid": "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", + "number": "501060", + "shortname": "DRESDEN", + "longname": "DRESDEN", + "km": 55.63, + "agency": "STANDORT DRESDEN", + "longitude": 13.738831783620384, + "latitude": 51.054459765598125, + "water": {"shortname": "ELBE", "longname": "ELBE"}, + } +) +MOCK_STATION_MEASUREMENT = CurrentMeasurement("cm", 56) + + +async def test_update_error(hass: HomeAssistant) -> None: + """Tests error during update entity.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG_ENTRY_DATA, + unique_id=MOCK_CONFIG_ENTRY_DATA[CONF_STATION], + ) + entry.add_to_hass(hass) + with patch("homeassistant.components.pegel_online.PegelOnline") as pegelonline: + pegelonline.return_value = PegelOnlineMock( + station_details=MOCK_STATION_DETAILS, + station_measurement=MOCK_STATION_MEASUREMENT, + ) + assert await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + state = hass.states.get("sensor.dresden_elbe_water_level") + assert state + + pegelonline().override_side_effect(ClientError) + async_fire_time_changed(hass, utcnow() + MIN_TIME_BETWEEN_UPDATES) + await hass.async_block_till_done() + + state = hass.states.get("sensor.dresden_elbe_water_level") + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/pegel_online/test_sensor.py b/tests/components/pegel_online/test_sensor.py new file mode 100644 index 00000000000000..216ca3427c57e7 --- /dev/null +++ b/tests/components/pegel_online/test_sensor.py @@ -0,0 +1,53 @@ +"""Test pegel_online component.""" +from unittest.mock import patch + +from aiopegelonline import CurrentMeasurement, Station + +from homeassistant.components.pegel_online.const import CONF_STATION, DOMAIN +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import HomeAssistant + +from . import PegelOnlineMock + +from tests.common import MockConfigEntry + +MOCK_CONFIG_ENTRY_DATA = {CONF_STATION: "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8"} + +MOCK_STATION_DETAILS = Station( + { + "uuid": "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", + "number": "501060", + "shortname": "DRESDEN", + "longname": "DRESDEN", + "km": 55.63, + "agency": "STANDORT DRESDEN", + "longitude": 13.738831783620384, + "latitude": 51.054459765598125, + "water": {"shortname": "ELBE", "longname": "ELBE"}, + } +) +MOCK_STATION_MEASUREMENT = CurrentMeasurement("cm", 56) + + +async def test_sensor(hass: HomeAssistant) -> None: + """Tests sensor entity.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG_ENTRY_DATA, + unique_id=MOCK_CONFIG_ENTRY_DATA[CONF_STATION], + ) + entry.add_to_hass(hass) + with patch("homeassistant.components.pegel_online.PegelOnline") as pegelonline: + pegelonline.return_value = PegelOnlineMock( + station_details=MOCK_STATION_DETAILS, + station_measurement=MOCK_STATION_MEASUREMENT, + ) + assert await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + state = hass.states.get("sensor.dresden_elbe_water_level") + assert state.name == "DRESDEN ELBE Water level" + assert state.state == "56" + assert state.attributes[ATTR_LATITUDE] == 51.054459765598125 + assert state.attributes[ATTR_LONGITUDE] == 13.738831783620384 From aad281db187aab69a5f57b83434fd569f8406ff3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Jul 2023 04:14:18 -0400 Subject: [PATCH 0929/1009] Add service to OpenAI to Generate an image (#97018) Co-authored-by: Franck Nijhof --- .../openai_conversation/__init__.py | 71 +++++++++++++++++-- .../openai_conversation/services.yaml | 22 ++++++ .../openai_conversation/strings.json | 21 ++++++ homeassistant/helpers/selector.py | 2 +- .../openai_conversation/test_init.py | 68 ++++++++++++++++++ 5 files changed, 176 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/openai_conversation/services.yaml diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py index efa81c7b73c3fc..9f4c30d91baf4f 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -7,13 +7,24 @@ import openai from openai import error +import voluptuous as vol from homeassistant.components import conversation from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, MATCH_ALL -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady, TemplateError -from homeassistant.helpers import intent, template +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, +) +from homeassistant.exceptions import ( + ConfigEntryNotReady, + HomeAssistantError, + TemplateError, +) +from homeassistant.helpers import config_validation as cv, intent, selector, template +from homeassistant.helpers.typing import ConfigType from homeassistant.util import ulid from .const import ( @@ -27,18 +38,61 @@ DEFAULT_PROMPT, DEFAULT_TEMPERATURE, DEFAULT_TOP_P, + DOMAIN, ) _LOGGER = logging.getLogger(__name__) +SERVICE_GENERATE_IMAGE = "generate_image" + +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up OpenAI Conversation.""" + + async def render_image(call: ServiceCall) -> ServiceResponse: + """Render an image with dall-e.""" + try: + response = await openai.Image.acreate( + api_key=hass.data[DOMAIN][call.data["config_entry"]], + prompt=call.data["prompt"], + n=1, + size=f'{call.data["size"]}x{call.data["size"]}', + ) + except error.OpenAIError as err: + raise HomeAssistantError(f"Error generating image: {err}") from err + + return response["data"][0] + + hass.services.async_register( + DOMAIN, + SERVICE_GENERATE_IMAGE, + render_image, + schema=vol.Schema( + { + vol.Required("config_entry"): selector.ConfigEntrySelector( + { + "integration": DOMAIN, + } + ), + vol.Required("prompt"): cv.string, + vol.Optional("size", default="512"): vol.In(("256", "512", "1024")), + } + ), + supports_response=SupportsResponse.ONLY, + ) + return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OpenAI Conversation from a config entry.""" - openai.api_key = entry.data[CONF_API_KEY] - try: await hass.async_add_executor_job( - partial(openai.Engine.list, request_timeout=10) + partial( + openai.Engine.list, + api_key=entry.data[CONF_API_KEY], + request_timeout=10, + ) ) except error.AuthenticationError as err: _LOGGER.error("Invalid API key: %s", err) @@ -46,13 +100,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except error.OpenAIError as err: raise ConfigEntryNotReady(err) from err + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.data[CONF_API_KEY] + conversation.async_set_agent(hass, entry, OpenAIAgent(hass, entry)) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload OpenAI.""" - openai.api_key = None + hass.data[DOMAIN].pop(entry.entry_id) conversation.async_unset_agent(hass, entry) return True @@ -106,6 +162,7 @@ async def async_process( try: result = await openai.ChatCompletion.acreate( + api_key=self.entry.data[CONF_API_KEY], model=model, messages=messages, max_tokens=max_tokens, diff --git a/homeassistant/components/openai_conversation/services.yaml b/homeassistant/components/openai_conversation/services.yaml new file mode 100644 index 00000000000000..81818fb3e71f13 --- /dev/null +++ b/homeassistant/components/openai_conversation/services.yaml @@ -0,0 +1,22 @@ +generate_image: + fields: + config_entry: + required: true + selector: + config_entry: + integration: openai_conversation + prompt: + required: true + selector: + text: + multiline: true + size: + required: true + example: "512" + default: "512" + selector: + select: + options: + - "256" + - "512" + - "1024" diff --git a/homeassistant/components/openai_conversation/strings.json b/homeassistant/components/openai_conversation/strings.json index 9583e759bd2b2c..542fe06dd5687b 100644 --- a/homeassistant/components/openai_conversation/strings.json +++ b/homeassistant/components/openai_conversation/strings.json @@ -25,5 +25,26 @@ } } } + }, + "services": { + "generate_image": { + "name": "Generate image", + "description": "Turn a prompt into an image", + "fields": { + "config_entry": { + "name": "Config Entry", + "description": "The config entry to use for this service" + }, + "prompt": { + "name": "Prompt", + "description": "The text to turn into an image", + "example": "A photo of a dog" + }, + "size": { + "name": "Size", + "description": "The size of the image to generate" + } + } + } } } diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 8ec8d5eac3ee17..08975c5c881e28 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -539,7 +539,7 @@ class ConversationAgentSelectorConfig(TypedDict, total=False): @SELECTORS.register("conversation_agent") -class COnversationAgentSelector(Selector[ConversationAgentSelectorConfig]): +class ConversationAgentSelector(Selector[ConversationAgentSelectorConfig]): """Selector for a conversation agent.""" selector_type = "conversation_agent" diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index fe23bbac56c813..1b9f81f60c046d 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -2,10 +2,12 @@ from unittest.mock import patch from openai import error +import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components import conversation from homeassistant.core import Context, HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import area_registry as ar, device_registry as dr, intent from tests.common import MockConfigEntry @@ -158,3 +160,69 @@ async def test_conversation_agent( mock_config_entry.entry_id ) assert agent.supported_languages == "*" + + +@pytest.mark.parametrize( + ("service_data", "expected_args"), + [ + ( + {"prompt": "Picture of a dog"}, + {"prompt": "Picture of a dog", "size": "512x512"}, + ), + ( + {"prompt": "Picture of a dog", "size": "256"}, + {"prompt": "Picture of a dog", "size": "256x256"}, + ), + ( + {"prompt": "Picture of a dog", "size": "1024"}, + {"prompt": "Picture of a dog", "size": "1024x1024"}, + ), + ], +) +async def test_generate_image_service( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_init_component, + service_data, + expected_args, +) -> None: + """Test generate image service.""" + service_data["config_entry"] = mock_config_entry.entry_id + expected_args["api_key"] = mock_config_entry.data["api_key"] + expected_args["n"] = 1 + + with patch( + "openai.Image.acreate", return_value={"data": [{"url": "A"}]} + ) as mock_create: + response = await hass.services.async_call( + "openai_conversation", + "generate_image", + service_data, + blocking=True, + return_response=True, + ) + + assert response == {"url": "A"} + assert len(mock_create.mock_calls) == 1 + assert mock_create.mock_calls[0][2] == expected_args + + +@pytest.mark.usefixtures("mock_init_component") +async def test_generate_image_service_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test generate image service handles errors.""" + with patch( + "openai.Image.acreate", side_effect=error.ServiceUnavailableError("Reason") + ), pytest.raises(HomeAssistantError, match="Error generating image: Reason"): + await hass.services.async_call( + "openai_conversation", + "generate_image", + { + "config_entry": mock_config_entry.entry_id, + "prompt": "Image of an epic fail", + }, + blocking=True, + return_response=True, + ) From 1a25b17c27a1fff88750eadb92a14db82056295e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jul 2023 11:36:51 +0200 Subject: [PATCH 0930/1009] Fix pegel_online generic typing (#97252) --- homeassistant/components/pegel_online/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/pegel_online/entity.py b/homeassistant/components/pegel_online/entity.py index 118392c6a698ec..c8a01623c7d1fb 100644 --- a/homeassistant/components/pegel_online/entity.py +++ b/homeassistant/components/pegel_online/entity.py @@ -8,7 +8,7 @@ from .coordinator import PegelOnlineDataUpdateCoordinator -class PegelOnlineEntity(CoordinatorEntity): +class PegelOnlineEntity(CoordinatorEntity[PegelOnlineDataUpdateCoordinator]): """Representation of a PEGELONLINE entity.""" _attr_has_entity_name = True From ae33670b33b07eaa757191e1feecc22e71ee3f16 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 26 Jul 2023 11:37:13 +0200 Subject: [PATCH 0931/1009] Add guard for missing xy color support in Matter light platform (#97251) --- homeassistant/components/matter/light.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 02919baa8f1a4b..52a6b4162fed56 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -337,10 +337,16 @@ def _update_from_device(self) -> None: # set current values if self.supports_color: - self._attr_color_mode = self._get_color_mode() - if self._attr_color_mode == ColorMode.HS: + self._attr_color_mode = color_mode = self._get_color_mode() + if ( + ColorMode.HS in self._attr_supported_color_modes + and color_mode == ColorMode.HS + ): self._attr_hs_color = self._get_hs_color() - else: + elif ( + ColorMode.XY in self._attr_supported_color_modes + and color_mode == ColorMode.XY + ): self._attr_xy_color = self._get_xy_color() if self.supports_color_temperature: From 5ec816568950c0601d84c137c97c4d93316f43f2 Mon Sep 17 00:00:00 2001 From: tronikos Date: Wed, 26 Jul 2023 02:39:50 -0700 Subject: [PATCH 0932/1009] Add virtual integrations supported by opower (#97250) --- .../atlanticcityelectric/__init__.py | 1 + .../atlanticcityelectric/manifest.json | 6 +++ homeassistant/components/bge/__init__.py | 1 + homeassistant/components/bge/manifest.json | 6 +++ homeassistant/components/comed/__init__.py | 1 + homeassistant/components/comed/manifest.json | 6 +++ homeassistant/components/delmarva/__init__.py | 1 + .../components/delmarva/manifest.json | 6 +++ homeassistant/components/evergy/__init__.py | 1 + homeassistant/components/evergy/manifest.json | 6 +++ .../components/peco_opower/__init__.py | 1 + .../components/peco_opower/manifest.json | 6 +++ homeassistant/components/pepco/__init__.py | 1 + homeassistant/components/pepco/manifest.json | 6 +++ homeassistant/components/pge/__init__.py | 1 + homeassistant/components/pge/manifest.json | 6 +++ homeassistant/components/pse/__init__.py | 1 + homeassistant/components/pse/manifest.json | 6 +++ homeassistant/generated/integrations.json | 45 +++++++++++++++++++ 19 files changed, 108 insertions(+) create mode 100644 homeassistant/components/atlanticcityelectric/__init__.py create mode 100644 homeassistant/components/atlanticcityelectric/manifest.json create mode 100644 homeassistant/components/bge/__init__.py create mode 100644 homeassistant/components/bge/manifest.json create mode 100644 homeassistant/components/comed/__init__.py create mode 100644 homeassistant/components/comed/manifest.json create mode 100644 homeassistant/components/delmarva/__init__.py create mode 100644 homeassistant/components/delmarva/manifest.json create mode 100644 homeassistant/components/evergy/__init__.py create mode 100644 homeassistant/components/evergy/manifest.json create mode 100644 homeassistant/components/peco_opower/__init__.py create mode 100644 homeassistant/components/peco_opower/manifest.json create mode 100644 homeassistant/components/pepco/__init__.py create mode 100644 homeassistant/components/pepco/manifest.json create mode 100644 homeassistant/components/pge/__init__.py create mode 100644 homeassistant/components/pge/manifest.json create mode 100644 homeassistant/components/pse/__init__.py create mode 100644 homeassistant/components/pse/manifest.json diff --git a/homeassistant/components/atlanticcityelectric/__init__.py b/homeassistant/components/atlanticcityelectric/__init__.py new file mode 100644 index 00000000000000..2a6ada2bf0514e --- /dev/null +++ b/homeassistant/components/atlanticcityelectric/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Atlantic City Electric.""" diff --git a/homeassistant/components/atlanticcityelectric/manifest.json b/homeassistant/components/atlanticcityelectric/manifest.json new file mode 100644 index 00000000000000..e6055d66462f21 --- /dev/null +++ b/homeassistant/components/atlanticcityelectric/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "atlanticcityelectric", + "name": "Atlantic City Electric", + "integration_type": "virtual", + "supported_by": "opower" +} diff --git a/homeassistant/components/bge/__init__.py b/homeassistant/components/bge/__init__.py new file mode 100644 index 00000000000000..a9bb8803f090b7 --- /dev/null +++ b/homeassistant/components/bge/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Baltimore Gas and Electric (BGE).""" diff --git a/homeassistant/components/bge/manifest.json b/homeassistant/components/bge/manifest.json new file mode 100644 index 00000000000000..7cce2b5cf1adb9 --- /dev/null +++ b/homeassistant/components/bge/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "bge", + "name": "Baltimore Gas and Electric (BGE)", + "integration_type": "virtual", + "supported_by": "opower" +} diff --git a/homeassistant/components/comed/__init__.py b/homeassistant/components/comed/__init__.py new file mode 100644 index 00000000000000..6808e129f875c3 --- /dev/null +++ b/homeassistant/components/comed/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Commonwealth Edison (ComEd).""" diff --git a/homeassistant/components/comed/manifest.json b/homeassistant/components/comed/manifest.json new file mode 100644 index 00000000000000..355328481c37f6 --- /dev/null +++ b/homeassistant/components/comed/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "comed", + "name": "Commonwealth Edison (ComEd)", + "integration_type": "virtual", + "supported_by": "opower" +} diff --git a/homeassistant/components/delmarva/__init__.py b/homeassistant/components/delmarva/__init__.py new file mode 100644 index 00000000000000..2af337b64a49a1 --- /dev/null +++ b/homeassistant/components/delmarva/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Delmarva Power.""" diff --git a/homeassistant/components/delmarva/manifest.json b/homeassistant/components/delmarva/manifest.json new file mode 100644 index 00000000000000..7f0de5c464a2c0 --- /dev/null +++ b/homeassistant/components/delmarva/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "delmarva", + "name": "Delmarva Power", + "integration_type": "virtual", + "supported_by": "opower" +} diff --git a/homeassistant/components/evergy/__init__.py b/homeassistant/components/evergy/__init__.py new file mode 100644 index 00000000000000..cf1018dccef787 --- /dev/null +++ b/homeassistant/components/evergy/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Evergy.""" diff --git a/homeassistant/components/evergy/manifest.json b/homeassistant/components/evergy/manifest.json new file mode 100644 index 00000000000000..a54dfca196d875 --- /dev/null +++ b/homeassistant/components/evergy/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "evergy", + "name": "Evergy", + "integration_type": "virtual", + "supported_by": "opower" +} diff --git a/homeassistant/components/peco_opower/__init__.py b/homeassistant/components/peco_opower/__init__.py new file mode 100644 index 00000000000000..a0d26cf7b136ac --- /dev/null +++ b/homeassistant/components/peco_opower/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: PECO Energy Company (PECO).""" diff --git a/homeassistant/components/peco_opower/manifest.json b/homeassistant/components/peco_opower/manifest.json new file mode 100644 index 00000000000000..e0c58729ce5f77 --- /dev/null +++ b/homeassistant/components/peco_opower/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "peco_opower", + "name": "PECO Energy Company (PECO)", + "integration_type": "virtual", + "supported_by": "opower" +} diff --git a/homeassistant/components/pepco/__init__.py b/homeassistant/components/pepco/__init__.py new file mode 100644 index 00000000000000..2ffcd22ade11b9 --- /dev/null +++ b/homeassistant/components/pepco/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Potomac Electric Power Company (Pepco).""" diff --git a/homeassistant/components/pepco/manifest.json b/homeassistant/components/pepco/manifest.json new file mode 100644 index 00000000000000..97a837399d0b09 --- /dev/null +++ b/homeassistant/components/pepco/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "pepco", + "name": "Potomac Electric Power Company (Pepco)", + "integration_type": "virtual", + "supported_by": "opower" +} diff --git a/homeassistant/components/pge/__init__.py b/homeassistant/components/pge/__init__.py new file mode 100644 index 00000000000000..e4402a7a3c2719 --- /dev/null +++ b/homeassistant/components/pge/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Pacific Gas & Electric (PG&E).""" diff --git a/homeassistant/components/pge/manifest.json b/homeassistant/components/pge/manifest.json new file mode 100644 index 00000000000000..4c1fa71a4b8b5b --- /dev/null +++ b/homeassistant/components/pge/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "pge", + "name": "Pacific Gas & Electric (PG&E)", + "integration_type": "virtual", + "supported_by": "opower" +} diff --git a/homeassistant/components/pse/__init__.py b/homeassistant/components/pse/__init__.py new file mode 100644 index 00000000000000..5af296c9bef8c9 --- /dev/null +++ b/homeassistant/components/pse/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Puget Sound Energy (PSE).""" diff --git a/homeassistant/components/pse/manifest.json b/homeassistant/components/pse/manifest.json new file mode 100644 index 00000000000000..5df86ac39a2eb4 --- /dev/null +++ b/homeassistant/components/pse/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "pse", + "name": "Puget Sound Energy (PSE)", + "integration_type": "virtual", + "supported_by": "opower" +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 85138b82c821dc..a3a8c334c11b91 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -456,6 +456,11 @@ "config_flow": false, "iot_class": "local_polling" }, + "atlanticcityelectric": { + "name": "Atlantic City Electric", + "integration_type": "virtual", + "supported_by": "opower" + }, "atome": { "name": "Atome Linky", "integration_type": "hub", @@ -555,6 +560,11 @@ "config_flow": false, "iot_class": "local_polling" }, + "bge": { + "name": "Baltimore Gas and Electric (BGE)", + "integration_type": "virtual", + "supported_by": "opower" + }, "bitcoin": { "name": "Bitcoin", "integration_type": "hub", @@ -862,6 +872,11 @@ "integration_type": "hub", "config_flow": false }, + "comed": { + "name": "Commonwealth Edison (ComEd)", + "integration_type": "virtual", + "supported_by": "opower" + }, "comed_hourly_pricing": { "name": "ComEd Hourly Pricing", "integration_type": "hub", @@ -991,6 +1006,11 @@ "config_flow": false, "iot_class": "cloud_polling" }, + "delmarva": { + "name": "Delmarva Power", + "integration_type": "virtual", + "supported_by": "opower" + }, "deluge": { "name": "Deluge", "integration_type": "service", @@ -1554,6 +1574,11 @@ } } }, + "evergy": { + "name": "Evergy", + "integration_type": "virtual", + "supported_by": "opower" + }, "everlights": { "name": "EverLights", "integration_type": "hub", @@ -4136,6 +4161,11 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "peco_opower": { + "name": "PECO Energy Company (PECO)", + "integration_type": "virtual", + "supported_by": "opower" + }, "pegel_online": { "name": "PEGELONLINE", "integration_type": "service", @@ -4148,6 +4178,16 @@ "config_flow": false, "iot_class": "local_polling" }, + "pepco": { + "name": "Potomac Electric Power Company (Pepco)", + "integration_type": "virtual", + "supported_by": "opower" + }, + "pge": { + "name": "Pacific Gas & Electric (PG&E)", + "integration_type": "virtual", + "supported_by": "opower" + }, "philips": { "name": "Philips", "integrations": { @@ -4321,6 +4361,11 @@ "config_flow": true, "iot_class": "local_polling" }, + "pse": { + "name": "Puget Sound Energy (PSE)", + "integration_type": "virtual", + "supported_by": "opower" + }, "pulseaudio_loopback": { "name": "PulseAudio Loopback", "integration_type": "hub", From d7af1e2d5dae9e0b727a7e7a29a1e1d47ff82517 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Wed, 26 Jul 2023 11:45:55 +0200 Subject: [PATCH 0933/1009] Add duotecno covers (#97205) --- .coveragerc | 1 + homeassistant/components/duotecno/__init__.py | 2 +- homeassistant/components/duotecno/cover.py | 85 +++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/duotecno/cover.py diff --git a/.coveragerc b/.coveragerc index 6bc69125c303d2..fb1869b2489577 100644 --- a/.coveragerc +++ b/.coveragerc @@ -233,6 +233,7 @@ omit = homeassistant/components/duotecno/__init__.py homeassistant/components/duotecno/entity.py homeassistant/components/duotecno/switch.py + homeassistant/components/duotecno/cover.py homeassistant/components/dwd_weather_warnings/const.py homeassistant/components/dwd_weather_warnings/coordinator.py homeassistant/components/dwd_weather_warnings/sensor.py diff --git a/homeassistant/components/duotecno/__init__.py b/homeassistant/components/duotecno/__init__.py index a1cf1c907a6c58..668a38dae5b3fb 100644 --- a/homeassistant/components/duotecno/__init__.py +++ b/homeassistant/components/duotecno/__init__.py @@ -11,7 +11,7 @@ from .const import DOMAIN -PLATFORMS: list[Platform] = [Platform.SWITCH] +PLATFORMS: list[Platform] = [Platform.SWITCH, Platform.COVER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/duotecno/cover.py b/homeassistant/components/duotecno/cover.py new file mode 100644 index 00000000000000..13e3df8fc0a103 --- /dev/null +++ b/homeassistant/components/duotecno/cover.py @@ -0,0 +1,85 @@ +"""Support for Velbus covers.""" +from __future__ import annotations + +from typing import Any + +from duotecno.unit import DuoswitchUnit + +from homeassistant.components.cover import ( + CoverEntity, + CoverEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import DuotecnoEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the duoswitch endities.""" + cntrl = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + DuotecnoCover(channel) for channel in cntrl.get_units("DuoSwitchUnit") + ) + + +class DuotecnoCover(DuotecnoEntity, CoverEntity): + """Representation a Velbus cover.""" + + _unit: DuoswitchUnit + + def __init__(self, unit: DuoswitchUnit) -> None: + """Initialize the cover.""" + super().__init__(unit) + self._attr_supported_features = ( + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP + ) + + @property + def is_closed(self) -> bool | None: + """Return if the cover is closed.""" + return self._unit.is_closed() + + @property + def is_opening(self) -> bool: + """Return if the cover is opening.""" + return self._unit.is_opening() + + @property + def is_closing(self) -> bool: + """Return if the cover is closing.""" + return self._unit.is_closing() + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + try: + await self._unit.open() + except OSError as err: + raise HomeAssistantError( + "Transmit for the open_cover packet failed" + ) from err + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover.""" + try: + await self._unit.close() + except OSError as err: + raise HomeAssistantError( + "Transmit for the close_cover packet failed" + ) from err + + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop the cover.""" + try: + await self._unit.stop() + except OSError as err: + raise HomeAssistantError( + "Transmit for the stop_cover packet failed" + ) from err From fd44bef39b03cb37f87e278c6a0742a7e106e6b5 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 26 Jul 2023 12:19:23 +0200 Subject: [PATCH 0934/1009] Add Event platform to Matter (#97219) --- homeassistant/components/matter/discovery.py | 2 + homeassistant/components/matter/event.py | 135 ++++++++++++++++++ homeassistant/components/matter/manifest.json | 2 +- homeassistant/components/matter/strings.json | 17 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../fixtures/nodes/generic-switch-multi.json | 117 +++++++++++++++ .../matter/fixtures/nodes/generic-switch.json | 81 +++++++++++ tests/components/matter/test_event.py | 128 +++++++++++++++++ 9 files changed, 483 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/matter/event.py create mode 100644 tests/components/matter/fixtures/nodes/generic-switch-multi.json create mode 100644 tests/components/matter/fixtures/nodes/generic-switch.json create mode 100644 tests/components/matter/test_event.py diff --git a/homeassistant/components/matter/discovery.py b/homeassistant/components/matter/discovery.py index 0b4bacf00ca343..c971bf8465ed13 100644 --- a/homeassistant/components/matter/discovery.py +++ b/homeassistant/components/matter/discovery.py @@ -12,6 +12,7 @@ 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 .event import DISCOVERY_SCHEMAS as EVENT_SCHEMAS from .light import DISCOVERY_SCHEMAS as LIGHT_SCHEMAS from .lock import DISCOVERY_SCHEMAS as LOCK_SCHEMAS from .models import MatterDiscoverySchema, MatterEntityInfo @@ -22,6 +23,7 @@ Platform.BINARY_SENSOR: BINARY_SENSOR_SCHEMAS, Platform.CLIMATE: CLIMATE_SENSOR_SCHEMAS, Platform.COVER: COVER_SCHEMAS, + Platform.EVENT: EVENT_SCHEMAS, Platform.LIGHT: LIGHT_SCHEMAS, Platform.LOCK: LOCK_SCHEMAS, Platform.SENSOR: SENSOR_SCHEMAS, diff --git a/homeassistant/components/matter/event.py b/homeassistant/components/matter/event.py new file mode 100644 index 00000000000000..3a1faa6dcbef84 --- /dev/null +++ b/homeassistant/components/matter/event.py @@ -0,0 +1,135 @@ +"""Matter event entities from Node events.""" +from __future__ import annotations + +from typing import Any + +from chip.clusters import Objects as clusters +from matter_server.client.models import device_types +from matter_server.common.models import EventType, MatterNodeEvent + +from homeassistant.components.event import ( + EventDeviceClass, + EventEntity, + EventEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +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 + +SwitchFeature = clusters.Switch.Bitmaps.SwitchFeature + +EVENT_TYPES_MAP = { + # mapping from raw event id's to translation keys + 0: "switch_latched", # clusters.Switch.Events.SwitchLatched + 1: "initial_press", # clusters.Switch.Events.InitialPress + 2: "long_press", # clusters.Switch.Events.LongPress + 3: "short_release", # clusters.Switch.Events.ShortRelease + 4: "long_release", # clusters.Switch.Events.LongRelease + 5: "multi_press_ongoing", # clusters.Switch.Events.MultiPressOngoing + 6: "multi_press_complete", # clusters.Switch.Events.MultiPressComplete +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Matter switches from Config Entry.""" + matter = get_matter(hass) + matter.register_platform_handler(Platform.EVENT, async_add_entities) + + +class MatterEventEntity(MatterEntity, EventEntity): + """Representation of a Matter Event entity.""" + + _attr_translation_key = "push" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the entity.""" + super().__init__(*args, **kwargs) + # fill the event types based on the features the switch supports + event_types: list[str] = [] + feature_map = int( + self.get_matter_attribute_value(clusters.Switch.Attributes.FeatureMap) + ) + if feature_map & SwitchFeature.kLatchingSwitch: + event_types.append("switch_latched") + if feature_map & SwitchFeature.kMomentarySwitch: + event_types.append("initial_press") + if feature_map & SwitchFeature.kMomentarySwitchRelease: + event_types.append("short_release") + if feature_map & SwitchFeature.kMomentarySwitchLongPress: + event_types.append("long_press_ongoing") + event_types.append("long_release") + if feature_map & SwitchFeature.kMomentarySwitchMultiPress: + event_types.append("multi_press_ongoing") + event_types.append("multi_press_complete") + self._attr_event_types = event_types + # the optional label attribute could be used to identify multiple buttons + # e.g. in case of a dimmer switch with 4 buttons, each button + # will have its own name, prefixed by the device name. + if labels := self.get_matter_attribute_value( + clusters.FixedLabel.Attributes.LabelList + ): + for label in labels: + if label.label == "Label": + label_value: str = label.value + # in the case the label is only the label id, prettify it a bit + if label_value.isnumeric(): + self._attr_name = f"Button {label_value}" + else: + self._attr_name = label_value + break + + async def async_added_to_hass(self) -> None: + """Handle being added to Home Assistant.""" + await super().async_added_to_hass() + + # subscribe to NodeEvent events + self._unsubscribes.append( + self.matter_client.subscribe_events( + callback=self._on_matter_node_event, + event_filter=EventType.NODE_EVENT, + node_filter=self._endpoint.node.node_id, + ) + ) + + def _update_from_device(self) -> None: + """Call when Node attribute(s) changed.""" + + @callback + def _on_matter_node_event( + self, event: EventType, data: MatterNodeEvent + ) -> None: # noqa: F821 + """Call on NodeEvent.""" + if data.endpoint_id != self._endpoint.endpoint_id: + return + self._trigger_event(EVENT_TYPES_MAP[data.event_id], data.data) + self.async_write_ha_state() + + +# Discovery schema(s) to map Matter Attributes to HA entities +DISCOVERY_SCHEMAS = [ + MatterDiscoverySchema( + platform=Platform.EVENT, + entity_description=EventEntityDescription( + key="GenericSwitch", device_class=EventDeviceClass.BUTTON, name=None + ), + entity_class=MatterEventEntity, + required_attributes=( + clusters.Switch.Attributes.CurrentPosition, + clusters.Switch.Attributes.FeatureMap, + ), + device_type=(device_types.GenericSwitch,), + optional_attributes=( + clusters.Switch.Attributes.NumberOfPositions, + clusters.FixedLabel.Attributes.LabelList, + ), + ), +] diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 85434407a10ffb..2237f0ade98121 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.3"] + "requirements": ["python-matter-server==3.7.0"] } diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index 61f1ca9180a28b..bfdba33327be89 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -45,6 +45,23 @@ } }, "entity": { + "event": { + "push": { + "state_attributes": { + "event_type": { + "state": { + "switch_latched": "Switch latched", + "initial_press": "Initial press", + "long_press": "Lomng press", + "short_release": "Short release", + "long_release": "Long release", + "multi_press_ongoing": "Multi press ongoing", + "multi_press_complete": "Multi press complete" + } + } + } + } + }, "sensor": { "flow": { "name": "Flow" diff --git a/requirements_all.txt b/requirements_all.txt index d40375adfc0487..1db78e0c0994f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2119,7 +2119,7 @@ python-kasa[speedups]==0.5.3 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==3.6.3 +python-matter-server==3.7.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ddb0f2f221a2f2..4701dafe91db29 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1557,7 +1557,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.5.3 # homeassistant.components.matter -python-matter-server==3.6.3 +python-matter-server==3.7.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/tests/components/matter/fixtures/nodes/generic-switch-multi.json b/tests/components/matter/fixtures/nodes/generic-switch-multi.json new file mode 100644 index 00000000000000..15c93825307260 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/generic-switch-multi.json @@ -0,0 +1,117 @@ +{ + "node_id": 1, + "date_commissioned": "2023-07-06T11:13:20.917394", + "last_interview": "2023-07-06T11:13:20.917401", + "interview_version": 2, + "attributes": { + "0/29/0": [ + { + "deviceType": 22, + "revision": 1 + } + ], + "0/29/1": [ + 4, 29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 53, 54, 55, 59, 60, 62, 63, + 64, 65 + ], + "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/40/0": 1, + "0/40/1": "Nabu Casa", + "0/40/2": 65521, + "0/40/3": "Mock GenericSwitch", + "0/40/4": 32768, + "0/40/5": "Mock Generic Switch", + "0/40/6": "XX", + "0/40/7": 0, + "0/40/8": "v1.0", + "0/40/9": 1, + "0/40/10": "prerelease", + "0/40/11": "20230707", + "0/40/12": "", + "0/40/13": "", + "0/40/14": "", + "0/40/15": "TEST_SN", + "0/40/16": false, + "0/40/17": true, + "0/40/18": "mock-generic-switch", + "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, 13, 14, 15, 16, 17, 18, 19, + 65528, 65529, 65531, 65532, 65533 + ], + "1/3/65529": [0, 64], + "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/29/0": [ + { + "deviceType": 15, + "revision": 1 + } + ], + "1/29/1": [3, 29, 59], + "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/59/65529": [], + "1/59/0": 2, + "1/59/65533": 1, + "1/59/1": 0, + "1/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/59/65532": 14, + "1/59/65528": [], + "1/64/0": [ + { + "label": "Label", + "value": "1" + } + ], + + "2/3/65529": [0, 64], + "2/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "2/29/0": [ + { + "deviceType": 15, + "revision": 1 + } + ], + "2/29/1": [3, 29, 59], + "2/29/2": [], + "2/29/3": [], + "2/29/65532": 0, + "2/29/65533": 1, + "2/29/65528": [], + "2/29/65529": [], + "2/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "2/59/65529": [], + "2/59/0": 2, + "2/59/65533": 1, + "2/59/1": 0, + "2/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "2/59/65532": 14, + "2/59/65528": [], + "2/64/0": [ + { + "label": "Label", + "value": "Fancy Button" + } + ] + }, + "available": true, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/fixtures/nodes/generic-switch.json b/tests/components/matter/fixtures/nodes/generic-switch.json new file mode 100644 index 00000000000000..30763c88e5ba0f --- /dev/null +++ b/tests/components/matter/fixtures/nodes/generic-switch.json @@ -0,0 +1,81 @@ +{ + "node_id": 1, + "date_commissioned": "2023-07-06T11:13:20.917394", + "last_interview": "2023-07-06T11:13:20.917401", + "interview_version": 2, + "attributes": { + "0/29/0": [ + { + "deviceType": 22, + "revision": 1 + } + ], + "0/29/1": [ + 4, 29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 53, 54, 55, 59, 60, 62, 63, + 64, 65 + ], + "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/40/0": 1, + "0/40/1": "Nabu Casa", + "0/40/2": 65521, + "0/40/3": "Mock GenericSwitch", + "0/40/4": 32768, + "0/40/5": "Mock Generic Switch", + "0/40/6": "XX", + "0/40/7": 0, + "0/40/8": "v1.0", + "0/40/9": 1, + "0/40/10": "prerelease", + "0/40/11": "20230707", + "0/40/12": "", + "0/40/13": "", + "0/40/14": "", + "0/40/15": "TEST_SN", + "0/40/16": false, + "0/40/17": true, + "0/40/18": "mock-generic-switch", + "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, 13, 14, 15, 16, 17, 18, 19, + 65528, 65529, 65531, 65532, 65533 + ], + "1/3/65529": [0, 64], + "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/29/0": [ + { + "deviceType": 15, + "revision": 1 + } + ], + "1/29/1": [3, 29, 59], + "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/59/65529": [], + "1/59/0": 2, + "1/59/65533": 1, + "1/59/1": 0, + "1/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/59/65532": 30, + "1/59/65528": [] + }, + "available": true, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/test_event.py b/tests/components/matter/test_event.py new file mode 100644 index 00000000000000..911dd0fe389d61 --- /dev/null +++ b/tests/components/matter/test_event.py @@ -0,0 +1,128 @@ +"""Test Matter Event entities.""" +from unittest.mock import MagicMock + +from matter_server.client.models.node import MatterNode +from matter_server.common.models import EventType, MatterNodeEvent +import pytest + +from homeassistant.components.event import ( + ATTR_EVENT_TYPE, + ATTR_EVENT_TYPES, +) +from homeassistant.core import HomeAssistant + +from .common import ( + setup_integration_with_node_fixture, + trigger_subscription_callback, +) + + +@pytest.fixture(name="generic_switch_node") +async def switch_node_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a GenericSwitch node.""" + return await setup_integration_with_node_fixture( + hass, "generic-switch", matter_client + ) + + +@pytest.fixture(name="generic_switch_multi_node") +async def multi_switch_node_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a GenericSwitch node with multiple buttons.""" + return await setup_integration_with_node_fixture( + hass, "generic-switch-multi", matter_client + ) + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_generic_switch_node( + hass: HomeAssistant, + matter_client: MagicMock, + generic_switch_node: MatterNode, +) -> None: + """Test event entity for a GenericSwitch node.""" + state = hass.states.get("event.mock_generic_switch") + assert state + assert state.state == "unknown" + # the switch endpoint has no label so the entity name should be the device itself + assert state.name == "Mock Generic Switch" + # check event_types from featuremap 30 + assert state.attributes[ATTR_EVENT_TYPES] == [ + "initial_press", + "short_release", + "long_press_ongoing", + "long_release", + "multi_press_ongoing", + "multi_press_complete", + ] + # trigger firing a new event from the device + await trigger_subscription_callback( + hass, + matter_client, + EventType.NODE_EVENT, + MatterNodeEvent( + node_id=generic_switch_node.node_id, + endpoint_id=1, + cluster_id=59, + event_id=1, + event_number=0, + priority=1, + timestamp=0, + timestamp_type=0, + data=None, + ), + ) + state = hass.states.get("event.mock_generic_switch") + assert state.attributes[ATTR_EVENT_TYPE] == "initial_press" + # trigger firing a multi press event + await trigger_subscription_callback( + hass, + matter_client, + EventType.NODE_EVENT, + MatterNodeEvent( + node_id=generic_switch_node.node_id, + endpoint_id=1, + cluster_id=59, + event_id=5, + event_number=0, + priority=1, + timestamp=0, + timestamp_type=0, + data={"NewPosition": 3}, + ), + ) + state = hass.states.get("event.mock_generic_switch") + assert state.attributes[ATTR_EVENT_TYPE] == "multi_press_ongoing" + assert state.attributes["NewPosition"] == 3 + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_generic_switch_multi_node( + hass: HomeAssistant, + matter_client: MagicMock, + generic_switch_multi_node: MatterNode, +) -> None: + """Test event entity for a GenericSwitch node with multiple buttons.""" + state_button_1 = hass.states.get("event.mock_generic_switch_button_1") + assert state_button_1 + assert state_button_1.state == "unknown" + # name should be 'DeviceName Button 1' due to the label set to just '1' + assert state_button_1.name == "Mock Generic Switch Button 1" + # check event_types from featuremap 14 + assert state_button_1.attributes[ATTR_EVENT_TYPES] == [ + "initial_press", + "short_release", + "long_press_ongoing", + "long_release", + ] + # check button 2 + state_button_1 = hass.states.get("event.mock_generic_switch_fancy_button") + assert state_button_1 + assert state_button_1.state == "unknown" + # name should be 'DeviceName Fancy Button' due to the label set to 'Fancy Button' + assert state_button_1.name == "Mock Generic Switch Fancy Button" From db491c86c34b022e4451c73e8ae83bedd7f9ff4d Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 26 Jul 2023 07:52:26 -0400 Subject: [PATCH 0935/1009] Bump whirlpool-sixth-sense to 0.18.4 (#97255) --- homeassistant/components/whirlpool/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/whirlpool/manifest.json b/homeassistant/components/whirlpool/manifest.json index 4b54f9746a029e..4c3ce680323224 100644 --- a/homeassistant/components/whirlpool/manifest.json +++ b/homeassistant/components/whirlpool/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["whirlpool"], - "requirements": ["whirlpool-sixth-sense==0.18.3"] + "requirements": ["whirlpool-sixth-sense==0.18.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1db78e0c0994f4..edd1edd7c2ff5e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2668,7 +2668,7 @@ webexteamssdk==1.1.1 webrtcvad==2.0.10 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.18.3 +whirlpool-sixth-sense==0.18.4 # homeassistant.components.whois whois==0.9.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4701dafe91db29..a07488a189866f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1956,7 +1956,7 @@ watchdog==2.3.1 webrtcvad==2.0.10 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.18.3 +whirlpool-sixth-sense==0.18.4 # homeassistant.components.whois whois==0.9.27 From d233438e1aa080d85f9b303af2b2c7556d7fdf35 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 26 Jul 2023 15:09:15 +0200 Subject: [PATCH 0936/1009] Handle UpdateFailed for YouTube (#97233) --- .../components/youtube/coordinator.py | 6 ++- homeassistant/components/youtube/sensor.py | 4 +- tests/components/youtube/__init__.py | 9 +++- tests/components/youtube/conftest.py | 12 ++--- tests/components/youtube/test_sensor.py | 47 +++++++++++++++---- 5 files changed, 56 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/youtube/coordinator.py b/homeassistant/components/youtube/coordinator.py index cb9d1e8214e249..07420233baf81d 100644 --- a/homeassistant/components/youtube/coordinator.py +++ b/homeassistant/components/youtube/coordinator.py @@ -5,13 +5,13 @@ from typing import Any from youtubeaio.helper import first -from youtubeaio.types import UnauthorizedError +from youtubeaio.types import UnauthorizedError, YouTubeBackendError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ICON, ATTR_ID from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from . import AsyncConfigEntryAuth from .const import ( @@ -70,4 +70,6 @@ async def _async_update_data(self) -> dict[str, Any]: } except UnauthorizedError as err: raise ConfigEntryAuthFailed from err + except YouTubeBackendError as err: + raise UpdateFailed("Couldn't connect to YouTube") from err return res diff --git a/homeassistant/components/youtube/sensor.py b/homeassistant/components/youtube/sensor.py index a63b8fb0c0ba34..99cd3ecf095836 100644 --- a/homeassistant/components/youtube/sensor.py +++ b/homeassistant/components/youtube/sensor.py @@ -87,9 +87,9 @@ class YouTubeSensor(YouTubeChannelEntity, SensorEntity): entity_description: YouTubeSensorEntityDescription @property - def available(self): + def available(self) -> bool: """Return if the entity is available.""" - return self.entity_description.available_fn( + return super().available and self.entity_description.available_fn( self.coordinator.data[self._channel_id] ) diff --git a/tests/components/youtube/__init__.py b/tests/components/youtube/__init__.py index 3c46ff92661c8b..665f5f3a76274b 100644 --- a/tests/components/youtube/__init__.py +++ b/tests/components/youtube/__init__.py @@ -11,7 +11,7 @@ class MockYouTube: """Service which returns mock objects.""" - _authenticated = False + _thrown_error: Exception | None = None def __init__( self, @@ -28,7 +28,6 @@ async def set_user_authentication( self, token: str, scopes: list[AuthScope] ) -> None: """Authenticate the user.""" - self._authenticated = True async def get_user_channels(self) -> AsyncGenerator[YouTubeChannel, None]: """Get channels for authenticated user.""" @@ -40,6 +39,8 @@ async def get_channels( self, channel_ids: list[str] ) -> AsyncGenerator[YouTubeChannel, None]: """Get channels.""" + if self._thrown_error is not None: + raise self._thrown_error channels = json.loads(load_fixture(self._channel_fixture)) for item in channels["items"]: yield YouTubeChannel(**item) @@ -57,3 +58,7 @@ async def get_user_subscriptions(self) -> AsyncGenerator[YouTubeSubscription, No channels = json.loads(load_fixture(self._subscriptions_fixture)) for item in channels["items"]: yield YouTubeSubscription(**item) + + def set_thrown_exception(self, exception: Exception) -> None: + """Set thrown exception for testing purposes.""" + self._thrown_error = exception diff --git a/tests/components/youtube/conftest.py b/tests/components/youtube/conftest.py index a8a333190eecd6..8b6ce5d00a23a4 100644 --- a/tests/components/youtube/conftest.py +++ b/tests/components/youtube/conftest.py @@ -18,7 +18,7 @@ from tests.components.youtube import MockYouTube from tests.test_util.aiohttp import AiohttpClientMocker -ComponentSetup = Callable[[], Awaitable[None]] +ComponentSetup = Callable[[], Awaitable[MockYouTube]] CLIENT_ID = "1234" CLIENT_SECRET = "5678" @@ -92,7 +92,7 @@ def mock_connection(aioclient_mock: AiohttpClientMocker) -> None: @pytest.fixture(name="setup_integration") async def mock_setup_integration( hass: HomeAssistant, config_entry: MockConfigEntry -) -> Callable[[], Coroutine[Any, Any, None]]: +) -> Callable[[], Coroutine[Any, Any, MockYouTube]]: """Fixture for setting up the component.""" config_entry.add_to_hass(hass) @@ -104,11 +104,11 @@ async def mock_setup_integration( DOMAIN, ) - async def func() -> None: - with patch( - "homeassistant.components.youtube.api.YouTube", return_value=MockYouTube() - ): + async def func() -> MockYouTube: + mock = MockYouTube() + with patch("homeassistant.components.youtube.api.YouTube", return_value=mock): assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() + return mock return func diff --git a/tests/components/youtube/test_sensor.py b/tests/components/youtube/test_sensor.py index 7dc368a5860c25..9f0b63bc062c3f 100644 --- a/tests/components/youtube/test_sensor.py +++ b/tests/components/youtube/test_sensor.py @@ -3,12 +3,11 @@ from unittest.mock import patch from syrupy import SnapshotAssertion -from youtubeaio.types import UnauthorizedError +from youtubeaio.types import UnauthorizedError, YouTubeBackendError from homeassistant import config_entries from homeassistant.components.youtube.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from . import MockYouTube @@ -87,14 +86,18 @@ async def test_sensor_reauth_trigger( hass: HomeAssistant, setup_integration: ComponentSetup ) -> None: """Test reauth is triggered after a refresh error.""" - with patch( - "youtubeaio.youtube.YouTube.get_channels", side_effect=UnauthorizedError - ): - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - future = dt_util.utcnow() + timedelta(minutes=15) - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + mock = await setup_integration() + + state = hass.states.get("sensor.google_for_developers_latest_upload") + assert state.state == "What's new in Google Home in less than 1 minute" + + state = hass.states.get("sensor.google_for_developers_subscribers") + assert state.state == "2290000" + + mock.set_thrown_exception(UnauthorizedError()) + future = dt_util.utcnow() + timedelta(minutes=15) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() @@ -103,3 +106,27 @@ async def test_sensor_reauth_trigger( assert flow["step_id"] == "reauth_confirm" assert flow["handler"] == DOMAIN assert flow["context"]["source"] == config_entries.SOURCE_REAUTH + + +async def test_sensor_unavailable( + hass: HomeAssistant, setup_integration: ComponentSetup +) -> None: + """Test update failed.""" + mock = await setup_integration() + + state = hass.states.get("sensor.google_for_developers_latest_upload") + assert state.state == "What's new in Google Home in less than 1 minute" + + state = hass.states.get("sensor.google_for_developers_subscribers") + assert state.state == "2290000" + + mock.set_thrown_exception(YouTubeBackendError()) + future = dt_util.utcnow() + timedelta(minutes=15) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.google_for_developers_latest_upload") + assert state.state == "unavailable" + + state = hass.states.get("sensor.google_for_developers_subscribers") + assert state.state == "unavailable" From 2ae059d4fcd59e1dbcdf9b723d81a75dfa48f973 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 26 Jul 2023 16:42:01 +0200 Subject: [PATCH 0937/1009] Add Event platform/entity to Hue integration (#97256) Co-authored-by: Franck Nijhof --- homeassistant/components/hue/bridge.py | 1 + homeassistant/components/hue/const.py | 28 ++++ homeassistant/components/hue/event.py | 133 ++++++++++++++++++ homeassistant/components/hue/strings.json | 28 ++++ .../components/hue/v2/device_trigger.py | 35 ++--- tests/components/hue/const.py | 18 +++ .../components/hue/fixtures/v2_resources.json | 4 + tests/components/hue/test_bridge.py | 11 +- .../components/hue/test_device_trigger_v2.py | 1 - tests/components/hue/test_event.py | 100 +++++++++++++ 10 files changed, 330 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/hue/event.py create mode 100644 tests/components/hue/test_event.py diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index c39fbed180ca69..0e1688221b342f 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -29,6 +29,7 @@ PLATFORMS_v1 = [Platform.BINARY_SENSOR, Platform.LIGHT, Platform.SENSOR] PLATFORMS_v2 = [ Platform.BINARY_SENSOR, + Platform.EVENT, Platform.LIGHT, Platform.SCENE, Platform.SENSOR, diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py index 798148b92c0012..d7d254b64a83d1 100644 --- a/homeassistant/components/hue/const.py +++ b/homeassistant/components/hue/const.py @@ -1,4 +1,9 @@ """Constants for the Hue component.""" +from aiohue.v2.models.button import ButtonEvent +from aiohue.v2.models.relative_rotary import ( + RelativeRotaryAction, + RelativeRotaryDirection, +) DOMAIN = "hue" @@ -33,3 +38,26 @@ # How long to wait to actually do the refresh after requesting it. # We wait some time so if we control multiple lights, we batch requests. REQUEST_REFRESH_DELAY = 0.3 + + +# V2 API SPECIFIC CONSTANTS ################## + +DEFAULT_BUTTON_EVENT_TYPES = ( + # I have never ever seen the `DOUBLE_SHORT_RELEASE` + # or `DOUBLE_SHORT_RELEASE` events so leave them out here + ButtonEvent.INITIAL_PRESS, + ButtonEvent.REPEAT, + ButtonEvent.SHORT_RELEASE, + ButtonEvent.LONG_RELEASE, +) + +DEFAULT_ROTARY_EVENT_TYPES = (RelativeRotaryAction.START, RelativeRotaryAction.REPEAT) +DEFAULT_ROTARY_EVENT_SUBTYPES = ( + RelativeRotaryDirection.CLOCK_WISE, + RelativeRotaryDirection.COUNTER_CLOCK_WISE, +) + +DEVICE_SPECIFIC_EVENT_TYPES = { + # device specific overrides of specific supported button events + "Hue tap switch": (ButtonEvent.INITIAL_PRESS,), +} diff --git a/homeassistant/components/hue/event.py b/homeassistant/components/hue/event.py new file mode 100644 index 00000000000000..8e34f7a22bfb8f --- /dev/null +++ b/homeassistant/components/hue/event.py @@ -0,0 +1,133 @@ +"""Hue event entities from Button resources.""" +from __future__ import annotations + +from typing import Any + +from aiohue.v2 import HueBridgeV2 +from aiohue.v2.controllers.events import EventType +from aiohue.v2.models.button import Button +from aiohue.v2.models.relative_rotary import ( + RelativeRotary, + RelativeRotaryDirection, +) + +from homeassistant.components.event import ( + EventDeviceClass, + EventEntity, + EventEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .bridge import HueBridge +from .const import DEFAULT_BUTTON_EVENT_TYPES, DEVICE_SPECIFIC_EVENT_TYPES, DOMAIN +from .v2.entity import HueBaseEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up event platform from Hue button resources.""" + bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + api: HueBridgeV2 = bridge.api + + if bridge.api_version == 1: + # should not happen, but just in case + raise NotImplementedError("Event support is only available for V2 bridges") + + # add entities for all button and relative rotary resources + @callback + def async_add_entity( + event_type: EventType, + resource: Button | RelativeRotary, + ) -> None: + """Add entity from Hue resource.""" + if isinstance(resource, RelativeRotary): + async_add_entities( + [HueRotaryEventEntity(bridge, api.sensors.relative_rotary, resource)] + ) + else: + async_add_entities( + [HueButtonEventEntity(bridge, api.sensors.button, resource)] + ) + + for controller in (api.sensors.button, api.sensors.relative_rotary): + # add all current items in controller + for item in controller: + async_add_entity(EventType.RESOURCE_ADDED, item) + + # register listener for new items only + config_entry.async_on_unload( + controller.subscribe( + async_add_entity, event_filter=EventType.RESOURCE_ADDED + ) + ) + + +class HueButtonEventEntity(HueBaseEntity, EventEntity): + """Representation of a Hue Event entity from a button resource.""" + + entity_description = EventEntityDescription( + key="button", + device_class=EventDeviceClass.BUTTON, + translation_key="button", + ) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the entity.""" + super().__init__(*args, **kwargs) + # fill the event types based on the features the switch supports + hue_dev_id = self.controller.get_device(self.resource.id).id + model_id = self.bridge.api.devices[hue_dev_id].product_data.product_name + event_types: list[str] = [] + for event_type in DEVICE_SPECIFIC_EVENT_TYPES.get( + model_id, DEFAULT_BUTTON_EVENT_TYPES + ): + event_types.append(event_type.value) + self._attr_event_types = event_types + + @property + def name(self) -> str: + """Return name for the entity.""" + return f"{super().name} {self.resource.metadata.control_id}" + + @callback + def _handle_event(self, event_type: EventType, resource: Button) -> None: + """Handle status event for this resource (or it's parent).""" + if event_type == EventType.RESOURCE_UPDATED and resource.id == self.resource.id: + self._trigger_event(resource.button.last_event.value) + self.async_write_ha_state() + return + super()._handle_event(event_type, resource) + + +class HueRotaryEventEntity(HueBaseEntity, EventEntity): + """Representation of a Hue Event entity from a RelativeRotary resource.""" + + entity_description = EventEntityDescription( + key="rotary", + device_class=EventDeviceClass.BUTTON, + translation_key="rotary", + event_types=[ + RelativeRotaryDirection.CLOCK_WISE.value, + RelativeRotaryDirection.COUNTER_CLOCK_WISE.value, + ], + ) + + @callback + def _handle_event(self, event_type: EventType, resource: RelativeRotary) -> None: + """Handle status event for this resource (or it's parent).""" + if event_type == EventType.RESOURCE_UPDATED and resource.id == self.resource.id: + event_key = resource.relative_rotary.last_event.rotation.direction.value + event_data = { + "duration": resource.relative_rotary.last_event.rotation.duration, + "steps": resource.relative_rotary.last_event.rotation.steps, + "action": resource.relative_rotary.last_event.action.value, + } + self._trigger_event(event_key, event_data) + self.async_write_ha_state() + return + super()._handle_event(event_type, resource) diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index aef5dba19869ba..a6920293ac1c80 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -67,6 +67,34 @@ "start": "[%key:component::hue::device_automation::trigger_type::initial_press%]" } }, + "entity": { + "event": { + "button": { + "state_attributes": { + "event_type": { + "state": { + "initial_press": "Initial press", + "repeat": "Repeat", + "short_release": "Short press", + "long_release": "Long press", + "double_short_release": "Double press" + } + } + } + }, + "rotary": { + "name": "Rotary", + "state_attributes": { + "event_type": { + "state": { + "clock_wise": "Clockwise", + "counter_clock_wise": "Counter clockwise" + } + } + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py index 466b593b56aa25..a3027736661ae0 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -3,11 +3,6 @@ from typing import TYPE_CHECKING, Any -from aiohue.v2.models.button import ButtonEvent -from aiohue.v2.models.relative_rotary import ( - RelativeRotaryAction, - RelativeRotaryDirection, -) from aiohue.v2.models.resource import ResourceTypes import voluptuous as vol @@ -24,7 +19,15 @@ from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.typing import ConfigType -from ..const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN +from ..const import ( + ATTR_HUE_EVENT, + CONF_SUBTYPE, + DEFAULT_BUTTON_EVENT_TYPES, + DEFAULT_ROTARY_EVENT_SUBTYPES, + DEFAULT_ROTARY_EVENT_TYPES, + DEVICE_SPECIFIC_EVENT_TYPES, + DOMAIN, +) if TYPE_CHECKING: from aiohue.v2 import HueBridgeV2 @@ -41,26 +44,6 @@ } ) -DEFAULT_BUTTON_EVENT_TYPES = ( - # all except `DOUBLE_SHORT_RELEASE` - ButtonEvent.INITIAL_PRESS, - ButtonEvent.REPEAT, - ButtonEvent.SHORT_RELEASE, - ButtonEvent.LONG_PRESS, - ButtonEvent.LONG_RELEASE, -) - -DEFAULT_ROTARY_EVENT_TYPES = (RelativeRotaryAction.START, RelativeRotaryAction.REPEAT) -DEFAULT_ROTARY_EVENT_SUBTYPES = ( - RelativeRotaryDirection.CLOCK_WISE, - RelativeRotaryDirection.COUNTER_CLOCK_WISE, -) - -DEVICE_SPECIFIC_EVENT_TYPES = { - # device specific overrides of specific supported button events - "Hue tap switch": (ButtonEvent.INITIAL_PRESS,), -} - async def async_validate_trigger_config( bridge: HueBridge, diff --git a/tests/components/hue/const.py b/tests/components/hue/const.py index 01b9c7f84b839f..415fe1324b7ae0 100644 --- a/tests/components/hue/const.py +++ b/tests/components/hue/const.py @@ -18,6 +18,7 @@ {"rid": "fake_zigbee_connectivity_id_1", "rtype": "zigbee_connectivity"}, {"rid": "fake_temperature_sensor_id_1", "rtype": "temperature"}, {"rid": "fake_motion_sensor_id_1", "rtype": "motion"}, + {"rid": "fake_relative_rotary", "rtype": "relative_rotary"}, ], "type": "device", } @@ -95,3 +96,20 @@ "auto_dynamic": False, "type": "scene", } + +FAKE_ROTARY = { + "id": "fake_relative_rotary", + "id_v1": "/sensors/1", + "owner": {"rid": "fake_device_id_1", "rtype": "device"}, + "relative_rotary": { + "last_event": { + "action": "start", + "rotation": { + "direction": "clock_wise", + "steps": 0, + "duration": 0, + }, + } + }, + "type": "relative_rotary", +} diff --git a/tests/components/hue/fixtures/v2_resources.json b/tests/components/hue/fixtures/v2_resources.json index a7ad7ec1a00e6e..371975e12a506b 100644 --- a/tests/components/hue/fixtures/v2_resources.json +++ b/tests/components/hue/fixtures/v2_resources.json @@ -475,6 +475,10 @@ { "rid": "db50a5d9-8cc7-486f-be06-c0b8f0d26c69", "rtype": "zigbee_connectivity" + }, + { + "rid": "2f029c7b-868b-49e9-aa01-a0bbc595990d", + "rtype": "relative_rotary" } ], "type": "device" diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index ef309849faa534..28aa8626c42b41 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -51,9 +51,16 @@ async def test_bridge_setup_v2(hass: HomeAssistant, mock_api_v2) -> None: assert hue_bridge.api is mock_api_v2 assert isinstance(hue_bridge.api, HueBridgeV2) assert hue_bridge.api_version == 2 - assert len(mock_forward.mock_calls) == 5 + assert len(mock_forward.mock_calls) == 6 forward_entries = {c[1][1] for c in mock_forward.mock_calls} - assert forward_entries == {"light", "binary_sensor", "sensor", "switch", "scene"} + assert forward_entries == { + "light", + "binary_sensor", + "event", + "sensor", + "switch", + "scene", + } async def test_bridge_setup_invalid_api_key(hass: HomeAssistant) -> None: diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index ab400c53ee4278..bfc0b612c1f66a 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -92,7 +92,6 @@ async def test_get_triggers( } for event_type in ( ButtonEvent.INITIAL_PRESS, - ButtonEvent.LONG_PRESS, ButtonEvent.LONG_RELEASE, ButtonEvent.REPEAT, ButtonEvent.SHORT_RELEASE, diff --git a/tests/components/hue/test_event.py b/tests/components/hue/test_event.py new file mode 100644 index 00000000000000..e3f50318f616a6 --- /dev/null +++ b/tests/components/hue/test_event.py @@ -0,0 +1,100 @@ +"""Philips Hue Event platform tests for V2 bridge/api.""" +from homeassistant.components.event import ( + ATTR_EVENT_TYPE, + ATTR_EVENT_TYPES, +) +from homeassistant.core import HomeAssistant + +from .conftest import setup_platform +from .const import FAKE_DEVICE, FAKE_ROTARY, FAKE_ZIGBEE_CONNECTIVITY + + +async def test_event( + hass: HomeAssistant, mock_bridge_v2, v2_resources_test_data +) -> None: + """Test event entity for Hue integration.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + await setup_platform(hass, mock_bridge_v2, "event") + # 7 entities should be created from test data + assert len(hass.states.async_all()) == 7 + + # pick one of the remote buttons + state = hass.states.get("event.hue_dimmer_switch_with_4_controls_button_1") + assert state + assert state.state == "unknown" + assert state.name == "Hue Dimmer switch with 4 controls Button 1" + # check event_types + assert state.attributes[ATTR_EVENT_TYPES] == [ + "initial_press", + "repeat", + "short_release", + "long_release", + ] + # trigger firing 'initial_press' event from the device + btn_event = { + "button": {"last_event": "initial_press"}, + "id": "f92aa267-1387-4f02-9950-210fb7ca1f5a", + "metadata": {"control_id": 1}, + "type": "button", + } + mock_bridge_v2.api.emit_event("update", btn_event) + await hass.async_block_till_done() + state = hass.states.get("event.hue_dimmer_switch_with_4_controls_button_1") + assert state.attributes[ATTR_EVENT_TYPE] == "initial_press" + # trigger firing 'long_release' event from the device + btn_event = { + "button": {"last_event": "long_release"}, + "id": "f92aa267-1387-4f02-9950-210fb7ca1f5a", + "metadata": {"control_id": 1}, + "type": "button", + } + mock_bridge_v2.api.emit_event("update", btn_event) + await hass.async_block_till_done() + state = hass.states.get("event.hue_dimmer_switch_with_4_controls_button_1") + assert state.attributes[ATTR_EVENT_TYPE] == "long_release" + + +async def test_sensor_add_update(hass: HomeAssistant, mock_bridge_v2) -> None: + """Test Event entity for newly added Relative Rotary resource.""" + await mock_bridge_v2.api.load_test_data([FAKE_DEVICE, FAKE_ZIGBEE_CONNECTIVITY]) + await setup_platform(hass, mock_bridge_v2, "event") + + test_entity_id = "event.hue_mocked_device_relative_rotary" + + # verify entity does not exist before we start + assert hass.states.get(test_entity_id) is None + + # Add new fake relative_rotary entity by emitting event + mock_bridge_v2.api.emit_event("add", FAKE_ROTARY) + await hass.async_block_till_done() + + # the entity should now be available + state = hass.states.get(test_entity_id) + assert state is not None + assert state.state == "unknown" + assert state.name == "Hue mocked device Relative Rotary" + # check event_types + assert state.attributes[ATTR_EVENT_TYPES] == ["clock_wise", "counter_clock_wise"] + + # test update of entity works on incoming event + btn_event = { + "id": "fake_relative_rotary", + "relative_rotary": { + "last_event": { + "action": "repeat", + "rotation": { + "direction": "counter_clock_wise", + "steps": 60, + "duration": 400, + }, + } + }, + "type": "relative_rotary", + } + mock_bridge_v2.api.emit_event("update", btn_event) + await hass.async_block_till_done() + state = hass.states.get(test_entity_id) + assert state.attributes[ATTR_EVENT_TYPE] == "counter_clock_wise" + assert state.attributes["action"] == "repeat" + assert state.attributes["steps"] == 60 + assert state.attributes["duration"] == 400 From 94870f05eee47d35d914c6f607b608c41be8e6c8 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 26 Jul 2023 16:43:02 +0200 Subject: [PATCH 0938/1009] Fix invalid ColorMode on (some) 3rd party Hue Color lights (#97263) --- homeassistant/components/hue/v2/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index f2c1571fda2e5a..957aa4a7806481 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -123,7 +123,7 @@ def color_mode(self) -> ColorMode: """Return the color mode of the light.""" if color_temp := self.resource.color_temperature: # Hue lights return `mired_valid` to indicate CT is active - if color_temp.mirek_valid and color_temp.mirek is not None: + if color_temp.mirek is not None: return ColorMode.COLOR_TEMP if self.resource.supports_color: return ColorMode.XY From 6200fd381e3af95b49f86941e84c282ece665d14 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jul 2023 16:47:34 +0200 Subject: [PATCH 0939/1009] Bumped version to 2023.8.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 513d72555a5be8..c965cf78528e8d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 1f179518fd974d..a15355dc9cf538 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.8.0.dev0" +version = "2023.8.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From e31a4610f7ece736b995b999537656940a048ece Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 27 Jul 2023 08:54:20 +0200 Subject: [PATCH 0940/1009] Fix authlib version constraint required by point (#97228) --- homeassistant/components/point/__init__.py | 5 ++--- homeassistant/package_constraints.txt | 4 ---- script/gen_requirements_all.py | 4 ---- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 6600a8240a0dba..627736f605d3a0 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -97,9 +97,8 @@ async def token_saver(token, **kwargs): token_saver=token_saver, ) try: - # pylint: disable-next=fixme - # TODO Remove authlib constraint when refactoring this code - await session.ensure_active_token() + # the call to user() implicitly calls ensure_active_token() in authlib + await session.user() except ConnectTimeout as err: _LOGGER.debug("Connection Timeout") raise ConfigEntryNotReady from err diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a9239bcfda8890..3ce056e1a4663e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -121,10 +121,6 @@ python-socketio>=4.6.0,<5.0 # https://github.com/home-assistant/core/pull/67046 multidict>=6.0.2 -# Required for compatibility with point integration - ensure_active_token -# https://github.com/home-assistant/core/pull/68176 -authlib<1.0 - # Version 2.0 added typing, prevent accidental fallbacks backoff>=2.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 9302d5477865cd..02d528c33e2cb1 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -123,10 +123,6 @@ # https://github.com/home-assistant/core/pull/67046 multidict>=6.0.2 -# Required for compatibility with point integration - ensure_active_token -# https://github.com/home-assistant/core/pull/68176 -authlib<1.0 - # Version 2.0 added typing, prevent accidental fallbacks backoff>=2.0 From b2adb4edbe7444fb6779360ff46844932b827595 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 27 Jul 2023 13:30:42 -0500 Subject: [PATCH 0941/1009] Add wildcards to sentence triggers (#97236) Co-authored-by: Franck Nijhof --- .../components/conversation/__init__.py | 6 +- .../components/conversation/default_agent.py | 44 ++++++++++--- .../components/conversation/manifest.json | 2 +- .../components/conversation/trigger.py | 21 ++++++- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../conversation/snapshots/test_init.ambr | 23 +++++-- .../conversation/test_default_agent.py | 3 +- tests/components/conversation/test_trigger.py | 61 +++++++++++++++++++ 10 files changed, 147 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 30ecf16bb373d8..29dd56c11ec054 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -322,7 +322,11 @@ async def websocket_hass_agent_debug( "intent": { "name": result.intent.name, }, - "entities": { + "slots": { # direct access to values + entity_key: entity.value + for entity_key, entity in result.entities.items() + }, + "details": { entity_key: { "name": entity.name, "value": entity.value, diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index b0a3702b5c9a52..04aafc8a99d6a2 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -11,7 +11,14 @@ import re from typing import IO, Any -from hassil.intents import Intents, ResponseType, SlotList, TextSlotList +from hassil.expression import Expression, ListReference, Sequence +from hassil.intents import ( + Intents, + ResponseType, + SlotList, + TextSlotList, + WildcardSlotList, +) from hassil.recognize import RecognizeResult, recognize_all from hassil.util import merge_dict from home_assistant_intents import get_domains_and_languages, get_intents @@ -48,7 +55,7 @@ REGEX_TYPE = type(re.compile("")) TRIGGER_CALLBACK_TYPE = Callable[ # pylint: disable=invalid-name - [str], Awaitable[str | None] + [str, RecognizeResult], Awaitable[str | None] ] @@ -657,6 +664,17 @@ def _rebuild_trigger_intents(self) -> None: } self._trigger_intents = Intents.from_dict(intents_dict) + + # Assume slot list references are wildcards + wildcard_names: set[str] = set() + for trigger_intent in self._trigger_intents.intents.values(): + for intent_data in trigger_intent.data: + for sentence in intent_data.sentences: + _collect_list_references(sentence, wildcard_names) + + for wildcard_name in wildcard_names: + self._trigger_intents.slot_lists[wildcard_name] = WildcardSlotList() + _LOGGER.debug("Rebuilt trigger intents: %s", intents_dict) def _unregister_trigger(self, trigger_data: TriggerData) -> None: @@ -682,14 +700,14 @@ async def _match_triggers(self, sentence: str) -> ConversationResult | None: assert self._trigger_intents is not None - matched_triggers: set[int] = set() + matched_triggers: dict[int, RecognizeResult] = {} for result in recognize_all(sentence, self._trigger_intents): trigger_id = int(result.intent.name) if trigger_id in matched_triggers: # Already matched a sentence from this trigger break - matched_triggers.add(trigger_id) + matched_triggers[trigger_id] = result if not matched_triggers: # Sentence did not match any trigger sentences @@ -699,14 +717,14 @@ async def _match_triggers(self, sentence: str) -> ConversationResult | None: "'%s' matched %s trigger(s): %s", sentence, len(matched_triggers), - matched_triggers, + list(matched_triggers), ) # Gather callback responses in parallel trigger_responses = await asyncio.gather( *( - self._trigger_sentences[trigger_id].callback(sentence) - for trigger_id in matched_triggers + self._trigger_sentences[trigger_id].callback(sentence, result) + for trigger_id, result in matched_triggers.items() ) ) @@ -733,3 +751,15 @@ def _make_error_result( response.async_set_error(error_code, response_text) return ConversationResult(response, conversation_id) + + +def _collect_list_references(expression: Expression, list_names: set[str]) -> None: + """Collect list reference names recursively.""" + if isinstance(expression, Sequence): + seq: Sequence = expression + for item in seq.items: + _collect_list_references(item, list_names) + elif isinstance(expression, ListReference): + # {list} + list_ref: ListReference = expression + list_names.add(list_ref.slot_name) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index a8f24a335f07c7..1eb58e96ff913c 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.2.2", "home-assistant-intents==2023.7.25"] + "requirements": ["hassil==1.2.5", "home-assistant-intents==2023.7.25"] } diff --git a/homeassistant/components/conversation/trigger.py b/homeassistant/components/conversation/trigger.py index b64b74c5fa69ba..71ddb5c1237852 100644 --- a/homeassistant/components/conversation/trigger.py +++ b/homeassistant/components/conversation/trigger.py @@ -3,7 +3,7 @@ from typing import Any -from hassil.recognize import PUNCTUATION +from hassil.recognize import PUNCTUATION, RecognizeResult import voluptuous as vol from homeassistant.const import CONF_COMMAND, CONF_PLATFORM @@ -49,12 +49,29 @@ async def async_attach_trigger( job = HassJob(action) @callback - async def call_action(sentence: str) -> str | None: + async def call_action(sentence: str, result: RecognizeResult) -> str | None: """Call action with right context.""" + + # Add slot values as extra trigger data + details = { + entity_name: { + "name": entity_name, + "text": entity.text.strip(), # remove whitespace + "value": entity.value.strip() + if isinstance(entity.value, str) + else entity.value, + } + for entity_name, entity in result.entities.items() + } + trigger_input: dict[str, Any] = { # Satisfy type checker **trigger_data, "platform": DOMAIN, "sentence": sentence, + "details": details, + "slots": { # direct access to values + entity_name: entity["value"] for entity_name, entity in details.items() + }, } # Wait for the automation to complete diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3ce056e1a4663e..3210d76964e29c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,7 @@ dbus-fast==1.87.2 fnv-hash-fast==0.4.0 ha-av==10.1.0 hass-nabucasa==0.69.0 -hassil==1.2.2 +hassil==1.2.5 home-assistant-bluetooth==1.10.2 home-assistant-frontend==20230725.0 home-assistant-intents==2023.7.25 diff --git a/requirements_all.txt b/requirements_all.txt index edd1edd7c2ff5e..2be19a3052c47d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -958,7 +958,7 @@ hass-nabucasa==0.69.0 hass-splunk==0.1.1 # homeassistant.components.conversation -hassil==1.2.2 +hassil==1.2.5 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a07488a189866f..220c21bada3d28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -753,7 +753,7 @@ habitipy==0.2.0 hass-nabucasa==0.69.0 # homeassistant.components.conversation -hassil==1.2.2 +hassil==1.2.5 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index 8ef0cef52f9117..f9fe284bcb07e4 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -372,7 +372,7 @@ dict({ 'results': list([ dict({ - 'entities': dict({ + 'details': dict({ 'name': dict({ 'name': 'name', 'text': 'my cool light', @@ -382,6 +382,9 @@ 'intent': dict({ 'name': 'HassTurnOn', }), + 'slots': dict({ + 'name': 'my cool light', + }), 'targets': dict({ 'light.kitchen': dict({ 'matched': True, @@ -389,7 +392,7 @@ }), }), dict({ - 'entities': dict({ + 'details': dict({ 'name': dict({ 'name': 'name', 'text': 'my cool light', @@ -399,6 +402,9 @@ 'intent': dict({ 'name': 'HassTurnOff', }), + 'slots': dict({ + 'name': 'my cool light', + }), 'targets': dict({ 'light.kitchen': dict({ 'matched': True, @@ -406,7 +412,7 @@ }), }), dict({ - 'entities': dict({ + 'details': dict({ 'area': dict({ 'name': 'area', 'text': 'kitchen', @@ -421,6 +427,10 @@ 'intent': dict({ 'name': 'HassTurnOn', }), + 'slots': dict({ + 'area': 'kitchen', + 'domain': 'light', + }), 'targets': dict({ 'light.kitchen': dict({ 'matched': True, @@ -428,7 +438,7 @@ }), }), dict({ - 'entities': dict({ + 'details': dict({ 'area': dict({ 'name': 'area', 'text': 'kitchen', @@ -448,6 +458,11 @@ 'intent': dict({ 'name': 'HassGetState', }), + 'slots': dict({ + 'area': 'kitchen', + 'domain': 'light', + 'state': 'on', + }), 'targets': dict({ 'light.kitchen': dict({ 'matched': False, diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index af9af468453ee4..c3c2e621260e9a 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -246,7 +246,8 @@ async def test_trigger_sentences(hass: HomeAssistant, init_components) -> None: for sentence in test_sentences: callback.reset_mock() result = await conversation.async_converse(hass, sentence, None, Context()) - callback.assert_called_once_with(sentence) + assert callback.call_count == 1 + assert callback.call_args[0][0] == sentence assert ( result.response.response_type == intent.IntentResponseType.ACTION_DONE ), sentence diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py index 522162fa457df9..3f4dd9e3a7e271 100644 --- a/tests/components/conversation/test_trigger.py +++ b/tests/components/conversation/test_trigger.py @@ -61,6 +61,8 @@ async def test_if_fires_on_event(hass: HomeAssistant, calls, setup_comp) -> None "idx": "0", "platform": "conversation", "sentence": "Ha ha ha", + "slots": {}, + "details": {}, } @@ -103,6 +105,8 @@ async def test_same_trigger_multiple_sentences( "idx": "0", "platform": "conversation", "sentence": "hello", + "slots": {}, + "details": {}, } @@ -188,3 +192,60 @@ async def test_fails_on_punctuation(hass: HomeAssistant, command: str) -> None: }, ], ) + + +async def test_wildcards(hass: HomeAssistant, calls, setup_comp) -> None: + """Test wildcards in trigger sentences.""" + assert await async_setup_component( + hass, + "automation", + { + "automation": { + "trigger": { + "platform": "conversation", + "command": [ + "play {album} by {artist}", + ], + }, + "action": { + "service": "test.automation", + "data_template": {"data": "{{ trigger }}"}, + }, + } + }, + ) + + await hass.services.async_call( + "conversation", + "process", + { + "text": "play the white album by the beatles", + }, + blocking=True, + ) + + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["data"] == { + "alias": None, + "id": "0", + "idx": "0", + "platform": "conversation", + "sentence": "play the white album by the beatles", + "slots": { + "album": "the white album", + "artist": "the beatles", + }, + "details": { + "album": { + "name": "album", + "text": "the white album", + "value": "the white album", + }, + "artist": { + "name": "artist", + "text": "the beatles", + "value": "the beatles", + }, + }, + } From 20df37c1323d649b046c03ede2856791cd047746 Mon Sep 17 00:00:00 2001 From: "J.P. Krauss" Date: Wed, 26 Jul 2023 12:30:25 -0700 Subject: [PATCH 0942/1009] Improve AirNow Configuration Error Handling (#97267) * Fix config flow error handling when no data is returned by AirNow API * Add test for PyAirNow EmptyResponseError * Typo Fix --- homeassistant/components/airnow/config_flow.py | 4 +++- homeassistant/components/airnow/strings.json | 2 +- tests/components/airnow/test_config_flow.py | 13 ++++++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airnow/config_flow.py b/homeassistant/components/airnow/config_flow.py index 39dbef4864746c..67bce66e1673ab 100644 --- a/homeassistant/components/airnow/config_flow.py +++ b/homeassistant/components/airnow/config_flow.py @@ -2,7 +2,7 @@ import logging from pyairnow import WebServiceAPI -from pyairnow.errors import AirNowError, InvalidKeyError +from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError import voluptuous as vol from homeassistant import config_entries, core, exceptions @@ -35,6 +35,8 @@ async def validate_input(hass: core.HomeAssistant, data): raise InvalidAuth from exc except AirNowError as exc: raise CannotConnect from exc + except EmptyResponseError as exc: + raise InvalidLocation from exc if not test_data: raise InvalidLocation diff --git a/homeassistant/components/airnow/strings.json b/homeassistant/components/airnow/strings.json index aed12596176cc6..072f0988c19eae 100644 --- a/homeassistant/components/airnow/strings.json +++ b/homeassistant/components/airnow/strings.json @@ -14,7 +14,7 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "invalid_location": "No results found for that location", + "invalid_location": "No results found for that location, try changing the location or station radius.", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { diff --git a/tests/components/airnow/test_config_flow.py b/tests/components/airnow/test_config_flow.py index efa462ee4e6bd0..5fda5f532a3da5 100644 --- a/tests/components/airnow/test_config_flow.py +++ b/tests/components/airnow/test_config_flow.py @@ -1,7 +1,7 @@ """Test the AirNow config flow.""" from unittest.mock import AsyncMock -from pyairnow.errors import AirNowError, InvalidKeyError +from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError import pytest from homeassistant import config_entries, data_entry_flow @@ -55,6 +55,17 @@ async def test_form_cannot_connect(hass: HomeAssistant, config, setup_airnow) -> assert result2["errors"] == {"base": "cannot_connect"} +@pytest.mark.parametrize("mock_api_get", [AsyncMock(side_effect=EmptyResponseError)]) +async def test_form_empty_result(hass: HomeAssistant, config, setup_airnow) -> None: + """Test we handle empty response error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_location"} + + @pytest.mark.parametrize("mock_api_get", [AsyncMock(side_effect=RuntimeError)]) async def test_form_unexpected(hass: HomeAssistant, config, setup_airnow) -> None: """Test we handle an unexpected error.""" From 73076fe94dc1a5b17f7c998c50a74d8112182c61 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jul 2023 21:22:22 +0200 Subject: [PATCH 0943/1009] Fix zodiac import flow/issue (#97282) --- homeassistant/components/zodiac/__init__.py | 38 ++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/zodiac/__init__.py b/homeassistant/components/zodiac/__init__.py index 81d5b5bdc219bf..48d1d8aa7aa3ea 100644 --- a/homeassistant/components/zodiac/__init__.py +++ b/homeassistant/components/zodiac/__init__.py @@ -17,27 +17,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the zodiac component.""" + if DOMAIN in config: + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.1.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Zodiac", + }, + ) - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.1.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Zodiac", - }, - ) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) ) - ) return True From d9beeac6750f5dc77e67df7817785204cc55ad0f Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 27 Jul 2023 09:21:30 +0200 Subject: [PATCH 0944/1009] Bump aioslimproto to 2.3.3 (#97283) --- 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 1ef87e8493354e..b221db96262587 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.3.2"] + "requirements": ["aioslimproto==2.3.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2be19a3052c47d..049199b38906ad 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.3.2 +aioslimproto==2.3.3 # homeassistant.components.steamist aiosteamist==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 220c21bada3d28..67ab0c73b796e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -320,7 +320,7 @@ aioshelly==5.4.0 aioskybell==22.7.0 # homeassistant.components.slimproto -aioslimproto==2.3.2 +aioslimproto==2.3.3 # homeassistant.components.steamist aiosteamist==0.3.2 From fc6ff69564a07a5270b914ac525fd35f4a3ffd29 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 26 Jul 2023 22:03:38 +0200 Subject: [PATCH 0945/1009] Rename key of water level sensor in PEGELONLINE (#97289) --- homeassistant/components/pegel_online/coordinator.py | 4 ++-- homeassistant/components/pegel_online/model.py | 2 +- homeassistant/components/pegel_online/sensor.py | 8 ++++---- homeassistant/components/pegel_online/strings.json | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/pegel_online/coordinator.py b/homeassistant/components/pegel_online/coordinator.py index 995953c5e36eda..8fab3ce36ae8f7 100644 --- a/homeassistant/components/pegel_online/coordinator.py +++ b/homeassistant/components/pegel_online/coordinator.py @@ -31,10 +31,10 @@ def __init__( async def _async_update_data(self) -> PegelOnlineData: """Fetch data from API endpoint.""" try: - current_measurement = await self.api.async_get_station_measurement( + water_level = await self.api.async_get_station_measurement( self.station.uuid ) except CONNECT_ERRORS as err: raise UpdateFailed(f"Failed to communicate with API: {err}") from err - return {"current_measurement": current_measurement} + return {"water_level": water_level} diff --git a/homeassistant/components/pegel_online/model.py b/homeassistant/components/pegel_online/model.py index c1760d3261b6a8..c8dac75bcf28d3 100644 --- a/homeassistant/components/pegel_online/model.py +++ b/homeassistant/components/pegel_online/model.py @@ -8,4 +8,4 @@ class PegelOnlineData(TypedDict): """TypedDict for PEGELONLINE Coordinator Data.""" - current_measurement: CurrentMeasurement + water_level: CurrentMeasurement diff --git a/homeassistant/components/pegel_online/sensor.py b/homeassistant/components/pegel_online/sensor.py index 7d48635781bc7d..14ec0c2d03228b 100644 --- a/homeassistant/components/pegel_online/sensor.py +++ b/homeassistant/components/pegel_online/sensor.py @@ -37,11 +37,11 @@ class PegelOnlineSensorEntityDescription( SENSORS: tuple[PegelOnlineSensorEntityDescription, ...] = ( PegelOnlineSensorEntityDescription( - key="current_measurement", - translation_key="current_measurement", + key="water_level", + translation_key="water_level", state_class=SensorStateClass.MEASUREMENT, - fn_native_unit=lambda data: data["current_measurement"].uom, - fn_native_value=lambda data: data["current_measurement"].value, + fn_native_unit=lambda data: data["water_level"].uom, + fn_native_value=lambda data: data["water_level"].value, icon="mdi:waves-arrow-up", ), ) diff --git a/homeassistant/components/pegel_online/strings.json b/homeassistant/components/pegel_online/strings.json index 71ec95f825cd32..930e349f9c3dec 100644 --- a/homeassistant/components/pegel_online/strings.json +++ b/homeassistant/components/pegel_online/strings.json @@ -26,7 +26,7 @@ }, "entity": { "sensor": { - "current_measurement": { + "water_level": { "name": "Water level" } } From c48f1b78997077674a044cabec22a3f663e9a331 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 26 Jul 2023 23:12:01 +0200 Subject: [PATCH 0946/1009] Weather remove forecast deprecation (#97292) --- homeassistant/components/weather/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 89bd601fdae815..f0c32f2d8cce8f 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -272,8 +272,6 @@ def __init_subclass__(cls, **kwargs: Any) -> None: "visibility_unit", "_attr_precipitation_unit", "precipitation_unit", - "_attr_forecast", - "forecast", ) ): if _reported is False: From 1b664e6a0b66e6b962b12308dfa98ebde607e36f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 Jul 2023 09:22:22 +0200 Subject: [PATCH 0947/1009] Fix implicit use of device name in TPLink switch (#97293) --- homeassistant/components/tplink/switch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index d82308a2e3299e..6c843246663208 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -84,6 +84,8 @@ def is_on(self) -> bool: class SmartPlugSwitch(CoordinatedTPLinkEntity, SwitchEntity): """Representation of a TPLink Smart Plug switch.""" + _attr_name = None + def __init__( self, device: SmartDevice, From aee6e0e6ebab52ad8115ab0baa433c86d3f15ba4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jul 2023 02:17:27 -0500 Subject: [PATCH 0948/1009] Fix dumping lru stats in the profiler (#97303) --- homeassistant/components/profiler/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index ba5f25a1c02408..8c5c206ae9ff33 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -45,7 +45,6 @@ "StatesMetaManager", "StateAttributesManager", "StatisticsMetaManager", - "DomainData", "IntegrationMatcher", ) From c925e1826bb812f2131ac70e5c40b0ac42086302 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 27 Jul 2023 09:23:23 +0200 Subject: [PATCH 0949/1009] Set mqtt entity name to `null` when it is a duplicate of the device name (#97304) --- homeassistant/components/mqtt/mixins.py | 9 +++++++++ tests/components/mqtt/test_mixins.py | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 70b681ffbb2f60..9f0849a4d4c74b 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -1135,7 +1135,16 @@ def _set_entity_name(self, config: ConfigType) -> None: "MQTT device information always needs to include a name, got %s, " "if device information is shared between multiple entities, the device " "name must be included in each entity's device configuration", + config, ) + elif config[CONF_DEVICE][CONF_NAME] == entity_name: + _LOGGER.warning( + "MQTT device name is equal to entity name in your config %s, " + "this is not expected. Please correct your configuration. " + "The entity name will be set to `null`", + config, + ) + self._attr_name = None def _setup_common_attributes_from_config(self, config: ConfigType) -> None: """(Re)Setup the common attributes for the entity.""" diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py index 5a30a3a65de052..23367d7829f216 100644 --- a/tests/components/mqtt/test_mixins.py +++ b/tests/components/mqtt/test_mixins.py @@ -212,6 +212,26 @@ def test_callback(event) -> None: None, True, ), + ( # entity_name_and_device_name_the_sane + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "Hello world", + "state_topic": "test-topic", + "unique_id": "veryunique", + "device_class": "humidity", + "device": { + "identifiers": ["helloworld"], + "name": "Hello world", + }, + } + } + }, + "sensor.hello_world", + "Hello world", + "Hello world", + False, + ), ], ids=[ "default_entity_name_without_device_name", @@ -222,6 +242,7 @@ def test_callback(event) -> None: "name_set_no_device_name_set", "none_entity_name_with_device_name", "none_entity_name_without_device_name", + "entity_name_and_device_name_the_sane", ], ) @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) From d6dba4b42b8ad5493010fccde241b5f09f534a0a Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 27 Jul 2023 03:13:49 -0400 Subject: [PATCH 0950/1009] bump python-roborock to 0.30.2 (#97306) --- 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 5f6aa63ce2f469..d26116a7818125 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.1"] + "requirements": ["python-roborock==0.30.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 049199b38906ad..2f9b876a2a17f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2150,7 +2150,7 @@ python-qbittorrent==0.4.3 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.30.1 +python-roborock==0.30.2 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67ab0c73b796e9..3742431f646a66 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1579,7 +1579,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.3 # homeassistant.components.roborock -python-roborock==0.30.1 +python-roborock==0.30.2 # homeassistant.components.smarttub python-smarttub==0.0.33 From 4eb37172a8f78181a921cd04837081834cb99e9d Mon Sep 17 00:00:00 2001 From: Markus Becker Date: Thu, 27 Jul 2023 08:58:52 +0200 Subject: [PATCH 0951/1009] Fix typo Lomng -> Long (#97315) --- homeassistant/components/matter/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index bfdba33327be89..c68b38bbb8ce3d 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -52,7 +52,7 @@ "state": { "switch_latched": "Switch latched", "initial_press": "Initial press", - "long_press": "Lomng press", + "long_press": "Long press", "short_release": "Short release", "long_release": "Long release", "multi_press_ongoing": "Multi press ongoing", From 52ce21f3b681343803b55d52e76f9d27b6eca85d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 27 Jul 2023 09:24:32 +0200 Subject: [PATCH 0952/1009] Fix sql entities not loading (#97316) --- homeassistant/components/sql/sensor.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index cbdef90f623274..0c8e90b8895a15 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -84,7 +84,11 @@ async def async_setup_platform( if value_template is not None: value_template.hass = hass - trigger_entity_config = {CONF_NAME: name, CONF_DEVICE_CLASS: device_class} + trigger_entity_config = { + CONF_NAME: name, + CONF_DEVICE_CLASS: device_class, + CONF_UNIQUE_ID: unique_id, + } if availability: trigger_entity_config[CONF_AVAILABILITY] = availability if icon: @@ -132,7 +136,11 @@ async def async_setup_entry( value_template.hass = hass name_template = Template(name, hass) - trigger_entity_config = {CONF_NAME: name_template, CONF_DEVICE_CLASS: device_class} + trigger_entity_config = { + CONF_NAME: name_template, + CONF_DEVICE_CLASS: device_class, + CONF_UNIQUE_ID: entry.entry_id, + } await async_setup_sensor( hass, @@ -269,7 +277,6 @@ async def async_setup_sensor( column_name, unit, value_template, - unique_id, yaml, state_class, use_database_executor, @@ -322,7 +329,6 @@ def __init__( column: str, unit: str | None, value_template: Template | None, - unique_id: str | None, yaml: bool, state_class: SensorStateClass | None, use_database_executor: bool, @@ -336,14 +342,16 @@ def __init__( self._column_name = column self.sessionmaker = sessmaker self._attr_extra_state_attributes = {} - self._attr_unique_id = unique_id self._use_database_executor = use_database_executor self._lambda_stmt = _generate_lambda_stmt(query) + self._attr_name = ( + None if not yaml else trigger_entity_config[CONF_NAME].template + ) self._attr_has_entity_name = not yaml - if not yaml and unique_id: + if not yaml and trigger_entity_config.get(CONF_UNIQUE_ID): self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, unique_id)}, + identifiers={(DOMAIN, trigger_entity_config[CONF_UNIQUE_ID])}, manufacturer="SQL", name=trigger_entity_config[CONF_NAME].template, ) From 216383437508f3813321390640da3fd2246a6224 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 Jul 2023 18:57:01 +0200 Subject: [PATCH 0953/1009] Fix DeviceInfo configuration_url validation (#97319) --- homeassistant/helpers/device_registry.py | 41 ++++++++------ homeassistant/helpers/entity.py | 3 +- tests/helpers/test_device_registry.py | 68 ++++++++++++++++++++++-- tests/helpers/test_entity_platform.py | 5 -- 4 files changed, 92 insertions(+), 25 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index f1eed86f10c001..5764f65957e772 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -10,6 +10,7 @@ from urllib.parse import urlparse import attr +from yarl import URL from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback @@ -48,6 +49,8 @@ RUNTIME_ONLY_ATTRS = {"suggested_area"} +CONFIGURATION_URL_SCHEMES = {"http", "https", "homeassistant"} + class DeviceEntryDisabler(StrEnum): """What disabled a device entry.""" @@ -168,28 +171,36 @@ def _validate_device_info( ), ) - if (config_url := device_info.get("configuration_url")) is not None: - if type(config_url) is not str or urlparse(config_url).scheme not in [ - "http", - "https", - "homeassistant", - ]: - raise DeviceInfoError( - config_entry.domain if config_entry else "unknown", - device_info, - f"invalid configuration_url '{config_url}'", - ) - return device_info_type +def _validate_configuration_url(value: Any) -> str | None: + """Validate and convert configuration_url.""" + if value is None: + return None + if ( + isinstance(value, URL) + and (value.scheme not in CONFIGURATION_URL_SCHEMES or not value.host) + ) or ( + (parsed_url := urlparse(str(value))) + and ( + parsed_url.scheme not in CONFIGURATION_URL_SCHEMES + or not parsed_url.hostname + ) + ): + raise ValueError(f"invalid configuration_url '{value}'") + return str(value) + + @attr.s(slots=True, frozen=True) class DeviceEntry: """Device Registry Entry.""" area_id: str | None = attr.ib(default=None) config_entries: set[str] = attr.ib(converter=set, factory=set) - configuration_url: str | None = attr.ib(default=None) + configuration_url: str | URL | None = attr.ib( + converter=_validate_configuration_url, default=None + ) connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set) disabled_by: DeviceEntryDisabler | None = attr.ib(default=None) entry_type: DeviceEntryType | None = attr.ib(default=None) @@ -453,7 +464,7 @@ def async_get_or_create( self, *, config_entry_id: str, - configuration_url: str | None | UndefinedType = UNDEFINED, + configuration_url: str | URL | None | UndefinedType = UNDEFINED, connections: set[tuple[str, str]] | None | UndefinedType = UNDEFINED, default_manufacturer: str | None | UndefinedType = UNDEFINED, default_model: str | None | UndefinedType = UNDEFINED, @@ -582,7 +593,7 @@ def async_update_device( *, add_config_entry_id: str | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, - configuration_url: str | None | UndefinedType = UNDEFINED, + configuration_url: str | URL | None | UndefinedType = UNDEFINED, disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED, entry_type: DeviceEntryType | None | UndefinedType = UNDEFINED, hw_version: str | None | UndefinedType = UNDEFINED, diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 8e07897c84f3d6..7d240cc0320a76 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -15,6 +15,7 @@ from typing import TYPE_CHECKING, Any, Final, Literal, TypedDict, TypeVar, final import voluptuous as vol +from yarl import URL from homeassistant.backports.functools import cached_property from homeassistant.config import DATA_CUSTOMIZE @@ -177,7 +178,7 @@ def get_unit_of_measurement(hass: HomeAssistant, entity_id: str) -> str | None: class DeviceInfo(TypedDict, total=False): """Entity device information for device registry.""" - configuration_url: str | None + configuration_url: str | URL | None connections: set[tuple[str, str]] default_manufacturer: str default_model: str diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 3e59b08cfa8fff..0210d7ba75d692 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1,9 +1,11 @@ """Tests for the Device Registry.""" +from contextlib import nullcontext import time from typing import Any from unittest.mock import patch import pytest +from yarl import URL from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STARTED @@ -171,7 +173,7 @@ async def test_loading_from_storage( { "area_id": "12345A", "config_entries": ["1234"], - "configuration_url": "configuration_url", + "configuration_url": "https://example.com/config", "connections": [["Zigbee", "01.23.45.67.89"]], "disabled_by": dr.DeviceEntryDisabler.USER, "entry_type": dr.DeviceEntryType.SERVICE, @@ -213,7 +215,7 @@ async def test_loading_from_storage( assert entry == dr.DeviceEntry( area_id="12345A", config_entries={"1234"}, - configuration_url="configuration_url", + configuration_url="https://example.com/config", connections={("Zigbee", "01.23.45.67.89")}, disabled_by=dr.DeviceEntryDisabler.USER, entry_type=dr.DeviceEntryType.SERVICE, @@ -916,7 +918,7 @@ async def test_update( updated_entry = device_registry.async_update_device( entry.id, area_id="12345A", - configuration_url="configuration_url", + configuration_url="https://example.com/config", disabled_by=dr.DeviceEntryDisabler.USER, entry_type=dr.DeviceEntryType.SERVICE, hw_version="hw_version", @@ -935,7 +937,7 @@ async def test_update( assert updated_entry == dr.DeviceEntry( area_id="12345A", config_entries={"1234"}, - configuration_url="configuration_url", + configuration_url="https://example.com/config", connections={("mac", "12:34:56:ab:cd:ef")}, disabled_by=dr.DeviceEntryDisabler.USER, entry_type=dr.DeviceEntryType.SERVICE, @@ -1670,3 +1672,61 @@ async def test_only_disable_device_if_all_config_entries_are_disabled( entry1 = device_registry.async_get(entry1.id) assert not entry1.disabled + + +@pytest.mark.parametrize( + ("configuration_url", "expectation"), + [ + ("http://localhost", nullcontext()), + ("http://localhost:8123", nullcontext()), + ("https://example.com", nullcontext()), + ("http://localhost/config", nullcontext()), + ("http://localhost:8123/config", nullcontext()), + ("https://example.com/config", nullcontext()), + ("homeassistant://config", nullcontext()), + (URL("http://localhost"), nullcontext()), + (URL("http://localhost:8123"), nullcontext()), + (URL("https://example.com"), nullcontext()), + (URL("http://localhost/config"), nullcontext()), + (URL("http://localhost:8123/config"), nullcontext()), + (URL("https://example.com/config"), nullcontext()), + (URL("homeassistant://config"), nullcontext()), + (None, nullcontext()), + ("http://", pytest.raises(ValueError)), + ("https://", pytest.raises(ValueError)), + ("gopher://localhost", pytest.raises(ValueError)), + ("homeassistant://", pytest.raises(ValueError)), + (URL("http://"), pytest.raises(ValueError)), + (URL("https://"), pytest.raises(ValueError)), + (URL("gopher://localhost"), pytest.raises(ValueError)), + (URL("homeassistant://"), pytest.raises(ValueError)), + # Exception implements __str__ + (Exception("https://example.com"), nullcontext()), + (Exception("https://"), pytest.raises(ValueError)), + (Exception(), pytest.raises(ValueError)), + ], +) +async def test_device_info_configuration_url_validation( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + configuration_url: str | URL | None, + expectation, +) -> None: + """Test configuration URL of device info is properly validated.""" + with expectation: + device_registry.async_get_or_create( + config_entry_id="1234", + identifiers={("something", "1234")}, + name="name", + configuration_url=configuration_url, + ) + + update_device = device_registry.async_get_or_create( + config_entry_id="5678", + identifiers={("something", "5678")}, + name="name", + ) + with expectation: + device_registry.async_update_device( + update_device.id, configuration_url=configuration_url + ) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 1f7e579ea95310..3eaad662d8b67d 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1857,11 +1857,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "name": "bla", "default_name": "yo", }, - # Invalid configuration URL - { - "identifiers": {("hue", "1234")}, - "configuration_url": "foo://192.168.0.100/config", - }, ], ) async def test_device_type_error_checking( From d05efe8c6afeb13f3101266a08d86bfc61719653 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Thu, 27 Jul 2023 16:00:27 +0200 Subject: [PATCH 0954/1009] Duotecno beta fix (#97325) * Fix duotecno * Implement comments * small cover fix --- homeassistant/components/duotecno/__init__.py | 2 +- homeassistant/components/duotecno/cover.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/duotecno/__init__.py b/homeassistant/components/duotecno/__init__.py index 668a38dae5b3fb..98003c3e8c4476 100644 --- a/homeassistant/components/duotecno/__init__.py +++ b/homeassistant/components/duotecno/__init__.py @@ -22,10 +22,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await controller.connect( entry.data[CONF_HOST], entry.data[CONF_PORT], entry.data[CONF_PASSWORD] ) - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) except (OSError, InvalidPassword, LoadFailure) as err: raise ConfigEntryNotReady from err hass.data.setdefault(DOMAIN, {})[entry.entry_id] = controller + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/duotecno/cover.py b/homeassistant/components/duotecno/cover.py index 13e3df8fc0a103..0fd212df085cf3 100644 --- a/homeassistant/components/duotecno/cover.py +++ b/homeassistant/components/duotecno/cover.py @@ -26,7 +26,7 @@ async def async_setup_entry( """Set up the duoswitch endities.""" cntrl = hass.data[DOMAIN][entry.entry_id] async_add_entities( - DuotecnoCover(channel) for channel in cntrl.get_units("DuoSwitchUnit") + DuotecnoCover(channel) for channel in cntrl.get_units("DuoswitchUnit") ) From 37e9fff1eb7e5e3c3beb25e12bbc2b257ecce811 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Thu, 27 Jul 2023 09:57:36 -0400 Subject: [PATCH 0955/1009] Fix Hydrawise zone addressing (#97333) --- .../components/hydrawise/binary_sensor.py | 2 +- homeassistant/components/hydrawise/sensor.py | 2 +- homeassistant/components/hydrawise/switch.py | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index bc9b8722c58ccd..63fe28cd40048d 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -92,6 +92,6 @@ def _handle_coordinator_update(self) -> None: if self.entity_description.key == "status": self._attr_is_on = self.coordinator.api.status == "All good!" elif self.entity_description.key == "is_watering": - relay_data = self.coordinator.api.relays[self.data["relay"] - 1] + relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]] self._attr_is_on = relay_data["timestr"] == "Now" super()._handle_coordinator_update() diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index 9214b9daeaca09..fa82c058f5b107 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -77,7 +77,7 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity): def _handle_coordinator_update(self) -> None: """Get the latest data and updates the states.""" LOGGER.debug("Updating Hydrawise sensor: %s", self.name) - relay_data = self.coordinator.api.relays[self.data["relay"] - 1] + relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]] if self.entity_description.key == "watering_time": if relay_data["timestr"] == "Now": self._attr_native_value = int(relay_data["run"] / 60) diff --git a/homeassistant/components/hydrawise/switch.py b/homeassistant/components/hydrawise/switch.py index dbd2c08b28ea91..0dd694a47d6ddb 100644 --- a/homeassistant/components/hydrawise/switch.py +++ b/homeassistant/components/hydrawise/switch.py @@ -99,26 +99,26 @@ def __init__( def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - relay_data = self.data["relay"] - 1 + zone_number = self.data["relay"] if self.entity_description.key == "manual_watering": - self.coordinator.api.run_zone(self._default_watering_timer, relay_data) + self.coordinator.api.run_zone(self._default_watering_timer, zone_number) elif self.entity_description.key == "auto_watering": - self.coordinator.api.suspend_zone(0, relay_data) + self.coordinator.api.suspend_zone(0, zone_number) def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - relay_data = self.data["relay"] - 1 + zone_number = self.data["relay"] if self.entity_description.key == "manual_watering": - self.coordinator.api.run_zone(0, relay_data) + self.coordinator.api.run_zone(0, zone_number) elif self.entity_description.key == "auto_watering": - self.coordinator.api.suspend_zone(365, relay_data) + self.coordinator.api.suspend_zone(365, zone_number) @callback def _handle_coordinator_update(self) -> None: """Update device state.""" - relay_data = self.data["relay"] - 1 + zone_number = self.data["relay"] LOGGER.debug("Updating Hydrawise switch: %s", self.name) - timestr = self.coordinator.api.relays[relay_data]["timestr"] + timestr = self.coordinator.api.relays_by_zone_number[zone_number]["timestr"] if self.entity_description.key == "manual_watering": self._attr_is_on = timestr == "Now" elif self.entity_description.key == "auto_watering": From 3028d40e7c35145fa19924c36e554158595a4481 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Thu, 27 Jul 2023 09:54:44 -0400 Subject: [PATCH 0956/1009] Bump pydrawise to 2023.7.1 (#97334) --- homeassistant/components/hydrawise/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index 48c9cdcf042d6c..d9e6d809960769 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/hydrawise", "iot_class": "cloud_polling", "loggers": ["pydrawise"], - "requirements": ["pydrawise==2023.7.0"] + "requirements": ["pydrawise==2023.7.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2f9b876a2a17f3..f99b58edd3eae4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1644,7 +1644,7 @@ pydiscovergy==2.0.1 pydoods==1.0.2 # homeassistant.components.hydrawise -pydrawise==2023.7.0 +pydrawise==2023.7.1 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 From d7af1acf287887f36505bcd38984e32413cb9211 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jul 2023 09:30:31 -0500 Subject: [PATCH 0957/1009] Bump aioesphomeapi to 15.1.15 (#97335) changelog: https://github.com/esphome/aioesphomeapi/compare/v15.1.14...v15.1.15 --- 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 b9b235ab41e717..d35cf90c60fa6c 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async_interrupt==1.1.1", - "aioesphomeapi==15.1.14", + "aioesphomeapi==15.1.15", "bluetooth-data-tools==1.6.1", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index f99b58edd3eae4..35563221833d62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==15.1.14 +aioesphomeapi==15.1.15 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3742431f646a66..9f3e1c60dd1f23 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.1.14 +aioesphomeapi==15.1.15 # homeassistant.components.flo aioflo==2021.11.0 From 7dc9204346ad68ffda64579e015bcc3d5bbb3a71 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 27 Jul 2023 16:58:09 +0200 Subject: [PATCH 0958/1009] Hue event entity follow up (#97336) --- homeassistant/components/hue/const.py | 4 +- homeassistant/components/hue/logbook.py | 78 ------------- homeassistant/components/hue/strings.json | 3 +- .../components/hue/test_device_trigger_v2.py | 1 + tests/components/hue/test_event.py | 1 + tests/components/hue/test_logbook.py | 107 ------------------ 6 files changed, 6 insertions(+), 188 deletions(-) delete mode 100644 homeassistant/components/hue/logbook.py delete mode 100644 tests/components/hue/test_logbook.py diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py index d7d254b64a83d1..38c2587bc1ab6d 100644 --- a/homeassistant/components/hue/const.py +++ b/homeassistant/components/hue/const.py @@ -43,11 +43,11 @@ # V2 API SPECIFIC CONSTANTS ################## DEFAULT_BUTTON_EVENT_TYPES = ( - # I have never ever seen the `DOUBLE_SHORT_RELEASE` - # or `DOUBLE_SHORT_RELEASE` events so leave them out here + # I have never ever seen the `DOUBLE_SHORT_RELEASE` event so leave it out here ButtonEvent.INITIAL_PRESS, ButtonEvent.REPEAT, ButtonEvent.SHORT_RELEASE, + ButtonEvent.LONG_PRESS, ButtonEvent.LONG_RELEASE, ) diff --git a/homeassistant/components/hue/logbook.py b/homeassistant/components/hue/logbook.py deleted file mode 100644 index 21d0da074a7248..00000000000000 --- a/homeassistant/components/hue/logbook.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Describe hue logbook events.""" -from __future__ import annotations - -from collections.abc import Callable - -from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME -from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_TYPE -from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.helpers import device_registry as dr - -from .const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN - -TRIGGER_SUBTYPE = { - "button_1": "first button", - "button_2": "second button", - "button_3": "third button", - "button_4": "fourth button", - "double_buttons_1_3": "first and third buttons", - "double_buttons_2_4": "second and fourth buttons", - "dim_down": "dim down", - "dim_up": "dim up", - "turn_off": "turn off", - "turn_on": "turn on", - "1": "first button", - "2": "second button", - "3": "third button", - "4": "fourth button", - "clock_wise": "Rotation clockwise", - "counter_clock_wise": "Rotation counter-clockwise", -} -TRIGGER_TYPE = { - "remote_button_long_release": "{subtype} released after long press", - "remote_button_short_press": "{subtype} pressed", - "remote_button_short_release": "{subtype} released", - "remote_double_button_long_press": "both {subtype} released after long press", - "remote_double_button_short_press": "both {subtype} released", - "initial_press": "{subtype} pressed initially", - "long_press": "{subtype} long press", - "repeat": "{subtype} held down", - "short_release": "{subtype} released after short press", - "long_release": "{subtype} released after long press", - "double_short_release": "both {subtype} released", - "start": '"{subtype}" pressed initially', -} - -UNKNOWN_TYPE = "unknown type" -UNKNOWN_SUB_TYPE = "unknown sub type" - - -@callback -def async_describe_events( - hass: HomeAssistant, - async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None], -) -> None: - """Describe hue logbook events.""" - - @callback - def async_describe_hue_event(event: Event) -> dict[str, str]: - """Describe hue logbook event.""" - data = event.data - name: str | None = None - if dev_ent := dr.async_get(hass).async_get(data[CONF_DEVICE_ID]): - name = dev_ent.name - if name is None: - name = data[CONF_ID] - if CONF_TYPE in data: # v2 - subtype = TRIGGER_SUBTYPE.get(str(data[CONF_SUBTYPE]), UNKNOWN_SUB_TYPE) - message = TRIGGER_TYPE.get(data[CONF_TYPE], UNKNOWN_TYPE).format( - subtype=subtype - ) - else: - message = f"Event {data[CONF_EVENT]}" # v1 - return { - LOGBOOK_ENTRY_NAME: name, - LOGBOOK_ENTRY_MESSAGE: message, - } - - async_describe_event(DOMAIN, ATTR_HUE_EVENT, async_describe_hue_event) diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index a6920293ac1c80..6d65abc8d5f3b3 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -76,7 +76,8 @@ "initial_press": "Initial press", "repeat": "Repeat", "short_release": "Short press", - "long_release": "Long press", + "long_press": "Long press", + "long_release": "Long release", "double_short_release": "Double press" } } diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index bfc0b612c1f66a..e89f53af73a802 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -94,6 +94,7 @@ async def test_get_triggers( ButtonEvent.INITIAL_PRESS, ButtonEvent.LONG_RELEASE, ButtonEvent.REPEAT, + ButtonEvent.LONG_PRESS, ButtonEvent.SHORT_RELEASE, ) for control_id, resource_id in ( diff --git a/tests/components/hue/test_event.py b/tests/components/hue/test_event.py index e3f50318f616a6..4dbb104357d14f 100644 --- a/tests/components/hue/test_event.py +++ b/tests/components/hue/test_event.py @@ -28,6 +28,7 @@ async def test_event( "initial_press", "repeat", "short_release", + "long_press", "long_release", ] # trigger firing 'initial_press' event from the device diff --git a/tests/components/hue/test_logbook.py b/tests/components/hue/test_logbook.py deleted file mode 100644 index 3f49efcdeb742b..00000000000000 --- a/tests/components/hue/test_logbook.py +++ /dev/null @@ -1,107 +0,0 @@ -"""The tests for hue logbook.""" -from homeassistant.components.hue.const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN -from homeassistant.components.hue.v1.hue_event import CONF_LAST_UPDATED -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_DEVICE_ID, - CONF_EVENT, - CONF_ID, - CONF_TYPE, - CONF_UNIQUE_ID, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr -from homeassistant.setup import async_setup_component - -from .conftest import setup_platform - -from tests.components.logbook.common import MockRow, mock_humanify - -# v1 event -SAMPLE_V1_EVENT = { - CONF_DEVICE_ID: "fe346f17a9f8c15be633f9cc3f3d6631", - CONF_EVENT: 18, - CONF_ID: "hue_tap", - CONF_LAST_UPDATED: "2019-12-28T22:58:03", - CONF_UNIQUE_ID: "00:00:00:00:00:44:23:08-f2", -} -# v2 event -SAMPLE_V2_EVENT = { - CONF_DEVICE_ID: "f974028e7933aea703a2199a855bc4a3", - CONF_ID: "wall_switch_with_2_controls_button", - CONF_SUBTYPE: 1, - CONF_TYPE: "initial_press", - CONF_UNIQUE_ID: "c658d3d8-a013-4b81-8ac6-78b248537e70", -} - - -async def test_humanify_hue_events( - hass: HomeAssistant, mock_bridge_v2, device_registry: dr.DeviceRegistry -) -> None: - """Test hue events when the devices are present in the registry.""" - await setup_platform(hass, mock_bridge_v2, "sensor") - hass.config.components.add("recorder") - assert await async_setup_component(hass, "logbook", {}) - await hass.async_block_till_done() - entry: ConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] - - v1_device = device_registry.async_get_or_create( - identifiers={(DOMAIN, "v1")}, name="Remote 1", config_entry_id=entry.entry_id - ) - v2_device = device_registry.async_get_or_create( - identifiers={(DOMAIN, "v2")}, name="Remote 2", config_entry_id=entry.entry_id - ) - - (v1_event, v2_event) = mock_humanify( - hass, - [ - MockRow( - ATTR_HUE_EVENT, - {**SAMPLE_V1_EVENT, CONF_DEVICE_ID: v1_device.id}, - ), - MockRow( - ATTR_HUE_EVENT, - {**SAMPLE_V2_EVENT, CONF_DEVICE_ID: v2_device.id}, - ), - ], - ) - - assert v1_event["name"] == "Remote 1" - assert v1_event["domain"] == DOMAIN - assert v1_event["message"] == "Event 18" - - assert v2_event["name"] == "Remote 2" - assert v2_event["domain"] == DOMAIN - assert v2_event["message"] == "first button pressed initially" - - -async def test_humanify_hue_events_devices_removed( - hass: HomeAssistant, mock_bridge_v2 -) -> None: - """Test hue events when the devices have been removed from the registry.""" - await setup_platform(hass, mock_bridge_v2, "sensor") - hass.config.components.add("recorder") - assert await async_setup_component(hass, "logbook", {}) - await hass.async_block_till_done() - - (v1_event, v2_event) = mock_humanify( - hass, - [ - MockRow( - ATTR_HUE_EVENT, - SAMPLE_V1_EVENT, - ), - MockRow( - ATTR_HUE_EVENT, - SAMPLE_V2_EVENT, - ), - ], - ) - - assert v1_event["name"] == "hue_tap" - assert v1_event["domain"] == DOMAIN - assert v1_event["message"] == "Event 18" - - assert v2_event["name"] == "wall_switch_with_2_controls_button" - assert v2_event["domain"] == DOMAIN - assert v2_event["message"] == "first button pressed initially" From e4246902fb3498ea2097bc0ea8e401fad4eabe07 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 27 Jul 2023 15:32:53 +0100 Subject: [PATCH 0959/1009] Split availability and data subscriptions in homekit_controller (#97337) --- .../components/homekit_controller/connection.py | 16 ++++++++++++---- .../components/homekit_controller/entity.py | 4 +++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index d101517e002541..4ba22317644a67 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -142,7 +142,7 @@ def __init__( function=self.async_update, ) - self._all_subscribers: set[CALLBACK_TYPE] = set() + self._availability_callbacks: set[CALLBACK_TYPE] = set() self._subscriptions: dict[tuple[int, int], set[CALLBACK_TYPE]] = {} @property @@ -189,7 +189,7 @@ def async_set_available_state(self, available: bool) -> None: if self.available == available: return self.available = available - for callback_ in self._all_subscribers: + for callback_ in self._availability_callbacks: callback_() async def _async_populate_ble_accessory_state(self, event: Event) -> None: @@ -811,12 +811,10 @@ def async_subscribe( self, characteristics: Iterable[tuple[int, int]], callback_: CALLBACK_TYPE ) -> CALLBACK_TYPE: """Add characteristics to the watch list.""" - self._all_subscribers.add(callback_) for aid_iid in characteristics: self._subscriptions.setdefault(aid_iid, set()).add(callback_) def _unsub(): - self._all_subscribers.remove(callback_) for aid_iid in characteristics: self._subscriptions[aid_iid].remove(callback_) if not self._subscriptions[aid_iid]: @@ -824,6 +822,16 @@ def _unsub(): return _unsub + @callback + def async_subscribe_availability(self, callback_: CALLBACK_TYPE) -> CALLBACK_TYPE: + """Add characteristics to the watch list.""" + self._availability_callbacks.add(callback_) + + def _unsub(): + self._availability_callbacks.remove(callback_) + + return _unsub + async def get_characteristics(self, *args: Any, **kwargs: Any) -> dict[str, Any]: """Read latest state from homekit accessory.""" return await self.pairing.get_characteristics(*args, **kwargs) diff --git a/homeassistant/components/homekit_controller/entity.py b/homeassistant/components/homekit_controller/entity.py index f6aadfac7ac90d..046dc9f17ec294 100644 --- a/homeassistant/components/homekit_controller/entity.py +++ b/homeassistant/components/homekit_controller/entity.py @@ -58,7 +58,9 @@ async def async_added_to_hass(self) -> None: self.all_characteristics, self._async_write_ha_state ) ) - + self.async_on_remove( + self._accessory.async_subscribe_availability(self._async_write_ha_state) + ) self._accessory.add_pollable_characteristics(self.pollable_characteristics) await self._accessory.add_watchable_characteristics( self.watchable_characteristics From 80092dabdf7b6f838b4098a9bf9561785e491df8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 Jul 2023 18:57:13 +0200 Subject: [PATCH 0960/1009] Add urllib3<2 package constraint (#97339) --- homeassistant/package_constraints.txt | 4 +++- script/gen_requirements_all.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3210d76964e29c..a0046569eb8c38 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -59,7 +59,9 @@ zeroconf==0.71.4 pycryptodome>=3.6.6 # Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 -urllib3>=1.26.5 +# Temporary setting an upper bound, to prevent compat issues with urllib3>=2 +# https://github.com/home-assistant/core/issues/97248 +urllib3>=1.26.5,<2 # Constrain httplib2 to protect against GHSA-93xj-8mrv-444m # https://github.com/advisories/GHSA-93xj-8mrv-444m diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 02d528c33e2cb1..b2954dc777baa4 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -61,7 +61,9 @@ pycryptodome>=3.6.6 # Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 -urllib3>=1.26.5 +# Temporary setting an upper bound, to prevent compat issues with urllib3>=2 +# https://github.com/home-assistant/core/issues/97248 +urllib3>=1.26.5,<2 # Constrain httplib2 to protect against GHSA-93xj-8mrv-444m # https://github.com/advisories/GHSA-93xj-8mrv-444m From 36982cea7ac9ff916ece868b5f31bc3697d0cc2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jul 2023 11:56:45 -0500 Subject: [PATCH 0961/1009] Bump aiohomekit to 2.6.12 (#97342) --- 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 f859919fe0745a..8cc80ef864e90d 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.11"], + "requirements": ["aiohomekit==2.6.12"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 35563221833d62..29420b03e8b508 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -249,7 +249,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==2.6.11 +aiohomekit==2.6.12 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f3e1c60dd1f23..b186f63d12926b 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.11 +aiohomekit==2.6.12 # homeassistant.components.emulated_hue # homeassistant.components.http From 768afeee21966f347b804eccb5415575a6f2bfaa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 Jul 2023 20:34:13 +0200 Subject: [PATCH 0962/1009] Bumped version to 2023.8.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 c965cf78528e8d..c856cf47329b46 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index a15355dc9cf538..1b621d828fbbf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.8.0b0" +version = "2023.8.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 78dad22fb3bdc1072327e24b32227af32cc4bba7 Mon Sep 17 00:00:00 2001 From: Niels Perfors Date: Sun, 30 Jul 2023 18:53:26 +0200 Subject: [PATCH 0963/1009] Upgrade Verisure to 2.6.4 (#97278) --- homeassistant/components/verisure/coordinator.py | 16 +++------------- homeassistant/components/verisure/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/verisure/coordinator.py b/homeassistant/components/verisure/coordinator.py index 47fbde3ef2024a..bc3b68922b0b8e 100644 --- a/homeassistant/components/verisure/coordinator.py +++ b/homeassistant/components/verisure/coordinator.py @@ -7,7 +7,6 @@ from verisure import ( Error as VerisureError, LoginError as VerisureLoginError, - ResponseError as VerisureResponseError, Session as Verisure, ) @@ -50,7 +49,7 @@ async def async_login(self) -> bool: except VerisureLoginError as ex: LOGGER.error("Could not log in to verisure, %s", ex) raise ConfigEntryAuthFailed("Credentials expired for Verisure") from ex - except VerisureResponseError as ex: + except VerisureError as ex: LOGGER.error("Could not log in to verisure, %s", ex) return False @@ -65,11 +64,9 @@ async def _async_update_data(self) -> dict: try: await self.hass.async_add_executor_job(self.verisure.update_cookie) except VerisureLoginError as ex: - LOGGER.error("Credentials expired for Verisure, %s", ex) raise ConfigEntryAuthFailed("Credentials expired for Verisure") from ex - except VerisureResponseError as ex: - LOGGER.error("Could not log in to verisure, %s", ex) - raise ConfigEntryAuthFailed("Could not log in to verisure") from ex + except VerisureError as ex: + raise UpdateFailed("Unable to update cookie") from ex try: overview = await self.hass.async_add_executor_job( self.verisure.request, @@ -81,13 +78,6 @@ async def _async_update_data(self) -> dict: self.verisure.smart_lock(), self.verisure.smartplugs(), ) - except VerisureResponseError as err: - LOGGER.debug("Cookie expired or service unavailable, %s", err) - overview = self._overview - try: - await self.hass.async_add_executor_job(self.verisure.update_cookie) - except VerisureResponseError as ex: - raise ConfigEntryAuthFailed("Credentials for Verisure expired.") from ex except VerisureError as err: LOGGER.error("Could not read overview, %s", err) raise UpdateFailed("Could not read overview") from err diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index 66dccdc07de95f..98440f67e4c388 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["verisure"], - "requirements": ["vsure==2.6.1"] + "requirements": ["vsure==2.6.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 29420b03e8b508..f9ea0c59975537 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2634,7 +2634,7 @@ volkszaehler==0.4.0 volvooncall==0.10.3 # homeassistant.components.verisure -vsure==2.6.1 +vsure==2.6.4 # homeassistant.components.vasttrafik vtjp==0.1.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b186f63d12926b..ea3923064ebd52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1934,7 +1934,7 @@ voip-utils==0.1.0 volvooncall==0.10.3 # homeassistant.components.verisure -vsure==2.6.1 +vsure==2.6.4 # homeassistant.components.vulcan vulcan-api==2.3.0 From 3beffb51034d12ab42c03ac7cd403f864d6bf665 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 27 Jul 2023 23:33:08 +0200 Subject: [PATCH 0964/1009] Bump reolink_aio to 0.7.5 (#97357) * bump reolink-aio to 0.7.4 * Bump reolink_aio to 0.7.5 --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 00f0e0f518bcfa..25994d56250472 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.3"] + "requirements": ["reolink-aio==0.7.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index f9ea0c59975537..7a23a68bd91d44 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2278,7 +2278,7 @@ renault-api==0.1.13 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.3 +reolink-aio==0.7.5 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea3923064ebd52..0a568957800332 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1674,7 +1674,7 @@ renault-api==0.1.13 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.3 +reolink-aio==0.7.5 # homeassistant.components.rflink rflink==0.0.65 From f54c36ec16f70980f84125b1055466df8a9c7f4f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Jul 2023 09:39:40 -0700 Subject: [PATCH 0965/1009] Bump dbus-fast to 1.87.5 (#97364) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index cbeab2abec0646..bc07e2b94ae90b 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -19,6 +19,6 @@ "bluetooth-adapters==0.16.0", "bluetooth-auto-recovery==1.2.1", "bluetooth-data-tools==1.6.1", - "dbus-fast==1.87.2" + "dbus-fast==1.87.5" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a0046569eb8c38..be1dff7623d2fd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ bluetooth-data-tools==1.6.1 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.2 -dbus-fast==1.87.2 +dbus-fast==1.87.5 fnv-hash-fast==0.4.0 ha-av==10.1.0 hass-nabucasa==0.69.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7a23a68bd91d44..da60be35125626 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -632,7 +632,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.87.2 +dbus-fast==1.87.5 # homeassistant.components.debugpy debugpy==1.6.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a568957800332..70252698d2ccdd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.87.2 +dbus-fast==1.87.5 # homeassistant.components.debugpy debugpy==1.6.7 From 1a0593fc9a7ef88fea8281c90b33b434a000d2b7 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 30 Jul 2023 11:42:28 -0500 Subject: [PATCH 0966/1009] Allow deleting config entry devices in jellyfin (#97377) --- homeassistant/components/jellyfin/__init__.py | 16 +++++ .../components/jellyfin/coordinator.py | 3 + tests/components/jellyfin/test_init.py | 60 +++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/homeassistant/components/jellyfin/__init__.py b/homeassistant/components/jellyfin/__init__.py index 4ee9702072468a..f25c3410edbb19 100644 --- a/homeassistant/components/jellyfin/__init__.py +++ b/homeassistant/components/jellyfin/__init__.py @@ -4,6 +4,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input from .const import CONF_CLIENT_DEVICE_ID, DOMAIN, LOGGER, PLATFORMS @@ -60,3 +61,18 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return True + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove device from a config entry.""" + data = hass.data[DOMAIN][config_entry.entry_id] + coordinator = data.coordinators["sessions"] + + return not device_entry.identifiers.intersection( + ( + (DOMAIN, coordinator.server_id), + *((DOMAIN, id) for id in coordinator.device_ids), + ) + ) diff --git a/homeassistant/components/jellyfin/coordinator.py b/homeassistant/components/jellyfin/coordinator.py index 3d5b150f39f9b9..f4ab98ca268581 100644 --- a/homeassistant/components/jellyfin/coordinator.py +++ b/homeassistant/components/jellyfin/coordinator.py @@ -47,6 +47,7 @@ def __init__( self.user_id: str = user_id self.session_ids: set[str] = set() + self.device_ids: set[str] = set() async def _async_update_data(self) -> JellyfinDataT: """Get the latest data from Jellyfin.""" @@ -75,4 +76,6 @@ async def _fetch_data(self) -> dict[str, dict[str, Any]]: and session["Client"] != USER_APP_NAME } + self.device_ids = {session["DeviceId"] for session in sessions_by_id.values()} + return sessions_by_id diff --git a/tests/components/jellyfin/test_init.py b/tests/components/jellyfin/test_init.py index 56e352bd71f984..9af73391d1855e 100644 --- a/tests/components/jellyfin/test_init.py +++ b/tests/components/jellyfin/test_init.py @@ -4,10 +4,29 @@ from homeassistant.components.jellyfin.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component from . import async_load_json_fixture from tests.common import MockConfigEntry +from tests.typing import MockHAClientWebSocket, WebSocketGenerator + + +async def remove_device( + ws_client: MockHAClientWebSocket, device_id: str, config_entry_id: str +) -> bool: + """Remove config entry from a device.""" + await ws_client.send_json( + { + "id": 1, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry_id, + "device_id": device_id, + } + ) + response = await ws_client.receive_json() + return response["success"] async def test_config_entry_not_ready( @@ -66,3 +85,44 @@ async def test_load_unload_config_entry( await hass.async_block_till_done() assert mock_config_entry.entry_id not in hass.data[DOMAIN] assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_device_remove_devices( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + mock_config_entry: MockConfigEntry, + mock_jellyfin: MagicMock, + device_registry: dr.DeviceRegistry, +) -> None: + """Test we can only remove a device that no longer exists.""" + assert await async_setup_component(hass, "config", {}) + + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + device_entry = device_registry.async_get_device( + identifiers={ + ( + DOMAIN, + "DEVICE-UUID", + ) + }, + ) + assert ( + await remove_device( + await hass_ws_client(hass), device_entry.id, mock_config_entry.entry_id + ) + is False + ) + old_device_entry = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={(DOMAIN, "OLD-DEVICE-UUID")}, + ) + assert ( + await remove_device( + await hass_ws_client(hass), old_device_entry.id, mock_config_entry.entry_id + ) + is True + ) From 945959827dc569c0ecaf3012c8fe359ee5d88d7a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 28 Jul 2023 11:52:23 +0200 Subject: [PATCH 0967/1009] Bump pysensibo to 1.0.32 (#97382) --- homeassistant/components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index f90b887d04c11c..26182102442efc 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -15,5 +15,5 @@ "iot_class": "cloud_polling", "loggers": ["pysensibo"], "quality_scale": "platinum", - "requirements": ["pysensibo==1.0.31"] + "requirements": ["pysensibo==1.0.32"] } diff --git a/requirements_all.txt b/requirements_all.txt index da60be35125626..2f9a9033d28a7f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1982,7 +1982,7 @@ pysabnzbd==1.1.1 pysaj==0.0.16 # homeassistant.components.sensibo -pysensibo==1.0.31 +pysensibo==1.0.32 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70252698d2ccdd..31561fd3c20774 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1474,7 +1474,7 @@ pyrympro==0.0.7 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.31 +pysensibo==1.0.32 # homeassistant.components.serial # homeassistant.components.zha From 38e22f5614dbbc6ddc1ddfaa731cad478080612a Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 30 Jul 2023 18:49:27 +0200 Subject: [PATCH 0968/1009] Regard long poll without events as valid (#97383) --- homeassistant/components/reolink/host.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 81fbda63fef00c..36b1661e1ac0c1 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -457,7 +457,7 @@ async def _async_long_polling(self, *_) -> None: self._long_poll_error = False - if not self._long_poll_received and channels != []: + if not self._long_poll_received: self._long_poll_received = True ir.async_delete_issue(self._hass, DOMAIN, "webhook_url") From 7f9db4039096dc08701213a02f34b87d406ed399 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 30 Jul 2023 18:47:34 +0200 Subject: [PATCH 0969/1009] Manual trigger entity fix name influence entity_id (#97398) --- homeassistant/components/scrape/sensor.py | 1 - homeassistant/components/sql/sensor.py | 9 ++++----- homeassistant/helpers/template_entity.py | 5 +++++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index a68083856f72b2..cc4cd269606cfd 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -157,7 +157,6 @@ def __init__( """Initialize a web scrape sensor.""" CoordinatorEntity.__init__(self, coordinator) ManualTriggerEntity.__init__(self, hass, trigger_entity_config) - self._attr_name = trigger_entity_config[CONF_NAME].template self._attr_native_unit_of_measurement = unit_of_measurement self._attr_state_class = state_class self._select = select diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 0c8e90b8895a15..aecc34d70091e5 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -344,16 +344,15 @@ def __init__( self._attr_extra_state_attributes = {} self._use_database_executor = use_database_executor self._lambda_stmt = _generate_lambda_stmt(query) - self._attr_name = ( - None if not yaml else trigger_entity_config[CONF_NAME].template - ) - self._attr_has_entity_name = not yaml + if not yaml: + self._attr_name = None + self._attr_has_entity_name = True if not yaml and trigger_entity_config.get(CONF_UNIQUE_ID): self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, trigger_entity_config[CONF_UNIQUE_ID])}, manufacturer="SQL", - name=trigger_entity_config[CONF_NAME].template, + name=self.name, ) async def async_added_to_hass(self) -> None: diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py index b7be7c2c9a6281..2e5cebf8571aa2 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -626,6 +626,11 @@ def __init__( ) -> None: """Initialize the entity.""" TriggerBaseEntity.__init__(self, hass, config) + # Need initial rendering on `name` as it influence the `entity_id` + self._rendered[CONF_NAME] = config[CONF_NAME].async_render( + {}, + parse_result=CONF_NAME in self._parse_result, + ) @callback def _process_manual_data(self, value: Any | None = None) -> None: From 364e7b838a234f184522f9d19064cd21718769d2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 30 Jul 2023 18:43:42 +0200 Subject: [PATCH 0970/1009] Return the actual media url from media extractor (#97408) --- homeassistant/components/media_extractor/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index a35650f00929e5..d00f1b33ccc884 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -127,7 +127,12 @@ def stream_selector(query): _LOGGER.error("Could not extract stream for the query: %s", query) raise MEQueryException() from err - return requested_stream["webpage_url"] + if "formats" in requested_stream: + best_stream = requested_stream["formats"][ + len(requested_stream["formats"]) - 1 + ] + return best_stream["url"] + return requested_stream["url"] return stream_selector From f1fc09cb1d4cc987353d81adb5889b910f592b91 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jul 2023 19:41:41 +0200 Subject: [PATCH 0971/1009] Small cleanup in event entity (#97409) --- homeassistant/components/event/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/event/__init__.py b/homeassistant/components/event/__init__.py index 98dd6036bc9586..f6ba2d79bfecba 100644 --- a/homeassistant/components/event/__init__.py +++ b/homeassistant/components/event/__init__.py @@ -45,7 +45,6 @@ class EventDeviceClass(StrEnum): "EventDeviceClass", "EventEntity", "EventEntityDescription", - "EventEntityFeature", ] # mypy: disallow-any-generics @@ -104,7 +103,7 @@ def from_dict(cls, restored: dict[str, Any]) -> Self | None: class EventEntity(RestoreEntity): - """Representation of a Event entity.""" + """Representation of an Event entity.""" entity_description: EventEntityDescription _attr_device_class: EventDeviceClass | None From 734c16b81698093587a55cc5e73db76ce21cd1d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jul 2023 16:37:08 -0500 Subject: [PATCH 0972/1009] Bump nexia to 2.0.7 (#97432) --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 2e54e773a44459..5464a241b7a8be 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/nexia", "iot_class": "cloud_polling", "loggers": ["nexia"], - "requirements": ["nexia==2.0.6"] + "requirements": ["nexia==2.0.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2f9a9033d28a7f..471246ea2301b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1258,7 +1258,7 @@ nettigo-air-monitor==2.1.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==2.0.6 +nexia==2.0.7 # homeassistant.components.nextcloud nextcloudmonitor==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31561fd3c20774..f4c318f45dd0b9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -966,7 +966,7 @@ netmap==0.7.0.2 nettigo-air-monitor==2.1.0 # homeassistant.components.nexia -nexia==2.0.6 +nexia==2.0.7 # homeassistant.components.nextcloud nextcloudmonitor==1.4.0 From b23286ce6f16879bcc00172887e1906290d7c5a0 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 30 Jul 2023 09:41:14 -0700 Subject: [PATCH 0973/1009] Bump opower to 0.0.16 (#97437) --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 08f25d20efff21..c054af8f43cabc 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["recorder"], "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", - "requirements": ["opower==0.0.15"] + "requirements": ["opower==0.0.16"] } diff --git a/requirements_all.txt b/requirements_all.txt index 471246ea2301b5..74cc2d7f12410f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1368,7 +1368,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.15 +opower==0.0.16 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4c318f45dd0b9..8c40163b0c65b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1037,7 +1037,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.15 +opower==0.0.16 # homeassistant.components.oralb oralb-ble==0.17.6 From 3764c2e9dea18e2105564ab6cd912d37b5804c66 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 30 Jul 2023 18:49:00 +0200 Subject: [PATCH 0974/1009] Reolink long poll recover (#97465) --- homeassistant/components/reolink/host.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 36b1661e1ac0c1..5882e5e66a41a9 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -267,7 +267,19 @@ async def disconnect(self): async def _async_start_long_polling(self): """Start ONVIF long polling task.""" if self._long_poll_task is None: - await self._api.subscribe(sub_type=SubType.long_poll) + try: + await self._api.subscribe(sub_type=SubType.long_poll) + except ReolinkError as err: + # make sure the long_poll_task is always created to try again later + if not self._lost_subscription: + self._lost_subscription = True + _LOGGER.error( + "Reolink %s event long polling subscription lost: %s", + self._api.nvr_name, + str(err), + ) + else: + self._lost_subscription = False self._long_poll_task = asyncio.create_task(self._async_long_polling()) async def _async_stop_long_polling(self): @@ -319,7 +331,13 @@ async def renew(self) -> None: try: await self._renew(SubType.push) if self._long_poll_task is not None: - await self._renew(SubType.long_poll) + if not self._api.subscribed(SubType.long_poll): + _LOGGER.debug("restarting long polling task") + # To prevent 5 minute request timeout + await self._async_stop_long_polling() + await self._async_start_long_polling() + else: + await self._renew(SubType.long_poll) except SubscriptionError as err: if not self._lost_subscription: self._lost_subscription = True From 93c536882be954c559cdc5690fce80ade5fe3f99 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 30 Jul 2023 18:40:38 +0200 Subject: [PATCH 0975/1009] Update ha-av to 10.1.1 (#97481) --- homeassistant/components/generic/manifest.json | 2 +- homeassistant/components/stream/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index ea02bfedefb133..a89ee370920b2f 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/generic", "iot_class": "local_push", - "requirements": ["ha-av==10.1.0", "Pillow==10.0.0"] + "requirements": ["ha-av==10.1.1", "Pillow==10.0.0"] } diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index c07a083ac52c62..96474ceb7eb7b9 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["PyTurboJPEG==1.7.1", "ha-av==10.1.0", "numpy==1.23.2"] + "requirements": ["PyTurboJPEG==1.7.1", "ha-av==10.1.1", "numpy==1.23.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index be1dff7623d2fd..04c0b0fd44f429 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ ciso8601==2.3.0 cryptography==41.0.2 dbus-fast==1.87.5 fnv-hash-fast==0.4.0 -ha-av==10.1.0 +ha-av==10.1.1 hass-nabucasa==0.69.0 hassil==1.2.5 home-assistant-bluetooth==1.10.2 diff --git a/requirements_all.txt b/requirements_all.txt index 74cc2d7f12410f..e5d8223eeaf109 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -937,7 +937,7 @@ h2==4.1.0 # homeassistant.components.generic # homeassistant.components.stream -ha-av==10.1.0 +ha-av==10.1.1 # homeassistant.components.ffmpeg ha-ffmpeg==3.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c40163b0c65b3..978874dddfaa22 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -735,7 +735,7 @@ h2==4.1.0 # homeassistant.components.generic # homeassistant.components.stream -ha-av==10.1.0 +ha-av==10.1.1 # homeassistant.components.ffmpeg ha-ffmpeg==3.1.0 From 4bd4c5666d8ff9b13b981a672d76683186ef9788 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Jul 2023 09:28:45 -0700 Subject: [PATCH 0976/1009] Revert using has_entity_name in ESPHome when `friendly_name` is not set (#97488) --- homeassistant/components/esphome/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 6b0a4cd6b26841..b308d8dc08c01c 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -140,7 +140,6 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): """Define a base esphome entity.""" _attr_should_poll = False - _attr_has_entity_name = True _static_info: _InfoT _state: _StateT _has_state: bool @@ -169,6 +168,7 @@ def __init__( connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)} ) self._entry_id = entry_data.entry_id + self._attr_has_entity_name = bool(device_info.friendly_name) async def async_added_to_hass(self) -> None: """Register callbacks.""" From 99634e22bdac60e4c9cae5da14df0695f0433a76 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 30 Jul 2023 19:18:42 +0200 Subject: [PATCH 0977/1009] Bumped version to 2023.8.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 c856cf47329b46..43809524d4ad35 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 1b621d828fbbf3..aaf057da02cf01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.8.0b1" +version = "2023.8.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 877c30c3a0d2b01ab0d5f01716128d84de6555e0 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 1 Aug 2023 03:05:01 -0500 Subject: [PATCH 0978/1009] Send language to Wyoming STT (#97344) --- homeassistant/components/wyoming/stt.py | 7 ++++++- tests/components/wyoming/conftest.py | 14 ++++++++++++++ tests/components/wyoming/snapshots/test_stt.ambr | 7 +++++++ tests/components/wyoming/test_stt.py | 16 ++++++++++------ 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/wyoming/stt.py b/homeassistant/components/wyoming/stt.py index 3f5487881a32b2..e64a2f14667020 100644 --- a/homeassistant/components/wyoming/stt.py +++ b/homeassistant/components/wyoming/stt.py @@ -2,7 +2,7 @@ from collections.abc import AsyncIterable import logging -from wyoming.asr import Transcript +from wyoming.asr import Transcribe, Transcript from wyoming.audio import AudioChunk, AudioStart, AudioStop from wyoming.client import AsyncTcpClient @@ -89,6 +89,10 @@ async def async_process_audio_stream( """Process an audio stream to STT service.""" try: async with AsyncTcpClient(self.service.host, self.service.port) as client: + # Set transcription language + await client.write_event(Transcribe(language=metadata.language).event()) + + # Begin audio stream await client.write_event( AudioStart( rate=SAMPLE_RATE, @@ -106,6 +110,7 @@ async def async_process_audio_stream( ) await client.write_event(chunk.event()) + # End audio stream await client.write_event(AudioStop().event()) while True: diff --git a/tests/components/wyoming/conftest.py b/tests/components/wyoming/conftest.py index 0dd9041a0d5efc..6b4e705914f107 100644 --- a/tests/components/wyoming/conftest.py +++ b/tests/components/wyoming/conftest.py @@ -4,6 +4,7 @@ import pytest +from homeassistant.components import stt from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -69,3 +70,16 @@ async def init_wyoming_tts(hass: HomeAssistant, tts_config_entry: ConfigEntry): return_value=TTS_INFO, ): await hass.config_entries.async_setup(tts_config_entry.entry_id) + + +@pytest.fixture +def metadata(hass: HomeAssistant) -> stt.SpeechMetadata: + """Get default STT metadata.""" + return stt.SpeechMetadata( + language=hass.config.language, + format=stt.AudioFormats.WAV, + codec=stt.AudioCodecs.PCM, + bit_rate=stt.AudioBitRates.BITRATE_16, + sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, + channel=stt.AudioChannels.CHANNEL_MONO, + ) diff --git a/tests/components/wyoming/snapshots/test_stt.ambr b/tests/components/wyoming/snapshots/test_stt.ambr index 08fe6a1ef8e722..784f89b2ab8117 100644 --- a/tests/components/wyoming/snapshots/test_stt.ambr +++ b/tests/components/wyoming/snapshots/test_stt.ambr @@ -1,6 +1,13 @@ # serializer version: 1 # name: test_streaming_audio list([ + dict({ + 'data': dict({ + 'language': 'en', + }), + 'payload': None, + 'type': 'transcibe', + }), dict({ 'data': dict({ 'channels': 1, diff --git a/tests/components/wyoming/test_stt.py b/tests/components/wyoming/test_stt.py index 021419f3a5e430..1938d44d310620 100644 --- a/tests/components/wyoming/test_stt.py +++ b/tests/components/wyoming/test_stt.py @@ -27,7 +27,9 @@ async def test_support(hass: HomeAssistant, init_wyoming_stt) -> None: assert entity.supported_channels == [stt.AudioChannels.CHANNEL_MONO] -async def test_streaming_audio(hass: HomeAssistant, init_wyoming_stt, snapshot) -> None: +async def test_streaming_audio( + hass: HomeAssistant, init_wyoming_stt, metadata, snapshot +) -> None: """Test streaming audio.""" entity = stt.async_get_speech_to_text_entity(hass, "stt.test_asr") assert entity is not None @@ -40,7 +42,7 @@ async def audio_stream(): "homeassistant.components.wyoming.stt.AsyncTcpClient", MockAsyncTcpClient([Transcript(text="Hello world").event()]), ) as mock_client: - result = await entity.async_process_audio_stream(None, audio_stream()) + result = await entity.async_process_audio_stream(metadata, audio_stream()) assert result.result == stt.SpeechResultState.SUCCESS assert result.text == "Hello world" @@ -48,7 +50,7 @@ async def audio_stream(): async def test_streaming_audio_connection_lost( - hass: HomeAssistant, init_wyoming_stt + hass: HomeAssistant, init_wyoming_stt, metadata ) -> None: """Test streaming audio and losing connection.""" entity = stt.async_get_speech_to_text_entity(hass, "stt.test_asr") @@ -61,13 +63,15 @@ async def audio_stream(): "homeassistant.components.wyoming.stt.AsyncTcpClient", MockAsyncTcpClient([None]), ): - result = await entity.async_process_audio_stream(None, audio_stream()) + result = await entity.async_process_audio_stream(metadata, audio_stream()) assert result.result == stt.SpeechResultState.ERROR assert result.text is None -async def test_streaming_audio_oserror(hass: HomeAssistant, init_wyoming_stt) -> None: +async def test_streaming_audio_oserror( + hass: HomeAssistant, init_wyoming_stt, metadata +) -> None: """Test streaming audio and error raising.""" entity = stt.async_get_speech_to_text_entity(hass, "stt.test_asr") assert entity is not None @@ -81,7 +85,7 @@ async def audio_stream(): "homeassistant.components.wyoming.stt.AsyncTcpClient", mock_client, ), patch.object(mock_client, "read_event", side_effect=OSError("Boom!")): - result = await entity.async_process_audio_stream(None, audio_stream()) + result = await entity.async_process_audio_stream(metadata, audio_stream()) assert result.result == stt.SpeechResultState.ERROR assert result.text is None From da401d5ad68ed6149cdb1cafb966cfc3c923becc Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 31 Jul 2023 21:01:25 +0200 Subject: [PATCH 0979/1009] Bump reolink_aio to 0.7.6 + Timeout (#97464) --- homeassistant/components/reolink/__init__.py | 11 +++++------ homeassistant/components/reolink/host.py | 6 ++++-- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 2de87659919d18..88eec9780a16cb 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -9,6 +9,7 @@ from typing import Literal import async_timeout +from reolink_aio.api import RETRY_ATTEMPTS from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError from homeassistant.config_entries import ConfigEntry @@ -77,15 +78,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_device_config_update() -> None: """Update the host state cache and renew the ONVIF-subscription.""" - async with async_timeout.timeout(host.api.timeout): + async with async_timeout.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): try: await host.update_states() except ReolinkError as err: - raise UpdateFailed( - f"Error updating Reolink {host.api.nvr_name}" - ) from err + raise UpdateFailed(str(err)) from err - async with async_timeout.timeout(host.api.timeout): + async with async_timeout.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): await host.renew() async def async_check_firmware_update() -> str | Literal[False]: @@ -93,7 +92,7 @@ async def async_check_firmware_update() -> str | Literal[False]: if not host.api.supported(None, "update"): return False - async with async_timeout.timeout(host.api.timeout): + async with async_timeout.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): try: return await host.api.check_new_firmware() except (ReolinkError, asyncio.exceptions.CancelledError) as err: diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 5882e5e66a41a9..5a0289c38b1cb0 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -24,7 +24,7 @@ from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DOMAIN from .exceptions import ReolinkSetupException, ReolinkWebhookException, UserNotAdmin -DEFAULT_TIMEOUT = 60 +DEFAULT_TIMEOUT = 30 FIRST_ONVIF_TIMEOUT = 10 SUBSCRIPTION_RENEW_THRESHOLD = 300 POLL_INTERVAL_NO_PUSH = 5 @@ -469,7 +469,9 @@ async def _async_long_polling(self, *_) -> None: await asyncio.sleep(LONG_POLL_ERROR_COOLDOWN) continue except Exception as ex: - _LOGGER.exception("Error while requesting ONVIF pull point: %s", ex) + _LOGGER.exception( + "Unexpected exception while requesting ONVIF pull point: %s", ex + ) await self._api.unsubscribe(sub_type=SubType.long_poll) raise ex diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 25994d56250472..fa61f873cca21a 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.5"] + "requirements": ["reolink-aio==0.7.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index e5d8223eeaf109..3c3bdaea46b8be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2278,7 +2278,7 @@ renault-api==0.1.13 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.5 +reolink-aio==0.7.6 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 978874dddfaa22..bebdfa4dfbe71d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1674,7 +1674,7 @@ renault-api==0.1.13 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.5 +reolink-aio==0.7.6 # homeassistant.components.rflink rflink==0.0.65 From c950abd32339191f1a4caeb00ab6365c0a68c9a7 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 31 Jul 2023 09:07:13 +0200 Subject: [PATCH 0980/1009] Delay creation of Reolink repair issues (#97476) * delay creation of repair issues * fix tests --- homeassistant/components/reolink/host.py | 37 ++++++++++++------------ tests/components/reolink/test_init.py | 11 ++++++- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 5a0289c38b1cb0..9bcafb8f00d43f 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -26,6 +26,7 @@ DEFAULT_TIMEOUT = 30 FIRST_ONVIF_TIMEOUT = 10 +FIRST_ONVIF_LONG_POLL_TIMEOUT = 90 SUBSCRIPTION_RENEW_THRESHOLD = 300 POLL_INTERVAL_NO_PUSH = 5 LONG_POLL_COOLDOWN = 0.75 @@ -205,7 +206,7 @@ async def _async_check_onvif(self, *_) -> None: # ONVIF push is not received, start long polling and schedule check await self._async_start_long_polling() self._cancel_long_poll_check = async_call_later( - self._hass, FIRST_ONVIF_TIMEOUT, self._async_check_onvif_long_poll + self._hass, FIRST_ONVIF_LONG_POLL_TIMEOUT, self._async_check_onvif_long_poll ) self._cancel_onvif_check = None @@ -215,7 +216,7 @@ async def _async_check_onvif_long_poll(self, *_) -> None: if not self._long_poll_received: _LOGGER.debug( "Did not receive state through ONVIF long polling after %i seconds", - FIRST_ONVIF_TIMEOUT, + FIRST_ONVIF_LONG_POLL_TIMEOUT, ) ir.async_create_issue( self._hass, @@ -230,8 +231,24 @@ async def _async_check_onvif_long_poll(self, *_) -> None: "network_link": "https://my.home-assistant.io/redirect/network/", }, ) + if self._base_url.startswith("https"): + ir.async_create_issue( + self._hass, + DOMAIN, + "https_webhook", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="https_webhook", + translation_placeholders={ + "base_url": self._base_url, + "network_link": "https://my.home-assistant.io/redirect/network/", + }, + ) + else: + ir.async_delete_issue(self._hass, DOMAIN, "https_webhook") else: ir.async_delete_issue(self._hass, DOMAIN, "webhook_url") + ir.async_delete_issue(self._hass, DOMAIN, "https_webhook") # If no ONVIF push or long polling state is received, start fast polling await self._async_poll_all_motion() @@ -426,22 +443,6 @@ def register_webhook(self) -> None: webhook_path = webhook.async_generate_path(event_id) self._webhook_url = f"{self._base_url}{webhook_path}" - if self._base_url.startswith("https"): - ir.async_create_issue( - self._hass, - DOMAIN, - "https_webhook", - is_fixable=False, - severity=ir.IssueSeverity.WARNING, - translation_key="https_webhook", - translation_placeholders={ - "base_url": self._base_url, - "network_link": "https://my.home-assistant.io/redirect/network/", - }, - ) - else: - ir.async_delete_issue(self._hass, DOMAIN, "https_webhook") - _LOGGER.debug("Registered webhook: %s", event_id) def unregister_webhook(self): diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 1e588d5e3a1220..f5f581760c1f3d 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -116,7 +116,14 @@ async def test_https_repair_issue( hass, {"country": "GB", "internal_url": "https://test_homeassistant_address"} ) - assert await hass.config_entries.async_setup(config_entry.entry_id) + with patch( + "homeassistant.components.reolink.host.FIRST_ONVIF_TIMEOUT", new=0 + ), patch( + "homeassistant.components.reolink.host.FIRST_ONVIF_LONG_POLL_TIMEOUT", new=0 + ), patch( + "homeassistant.components.reolink.host.ReolinkHost._async_long_polling", + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() issue_registry = ir.async_get(hass) @@ -150,6 +157,8 @@ async def test_webhook_repair_issue( """Test repairs issue is raised when the webhook url is unreachable.""" with patch( "homeassistant.components.reolink.host.FIRST_ONVIF_TIMEOUT", new=0 + ), patch( + "homeassistant.components.reolink.host.FIRST_ONVIF_LONG_POLL_TIMEOUT", new=0 ), patch( "homeassistant.components.reolink.host.ReolinkHost._async_long_polling", ): From 278f02c86f3e1350d6144c2879229717fc272644 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 31 Jul 2023 14:21:34 +0200 Subject: [PATCH 0981/1009] Avoid leaking exception trace for philips_js (#97491) Avoid leaking exception trace --- homeassistant/components/philips_js/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 8ecc8a0e8c429b..6f72f31ae8fb62 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -7,7 +7,12 @@ import logging from typing import Any -from haphilipsjs import AutenticationFailure, ConnectionFailure, PhilipsTV +from haphilipsjs import ( + AutenticationFailure, + ConnectionFailure, + GeneralFailure, + PhilipsTV, +) from haphilipsjs.typing import SystemType from homeassistant.config_entries import ConfigEntry @@ -22,7 +27,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, DOMAIN @@ -187,3 +192,5 @@ async def _async_update_data(self): pass except AutenticationFailure as exception: raise ConfigEntryAuthFailed(str(exception)) from exception + except GeneralFailure as exception: + raise UpdateFailed(str(exception)) from exception From 00c1f3d85ef5d23896bdf493d9e50b0d9f308b23 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 30 Jul 2023 12:27:57 -0700 Subject: [PATCH 0982/1009] Bump androidtvremote2==0.0.13 (#97494) --- homeassistant/components/androidtv_remote/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv_remote/manifest.json b/homeassistant/components/androidtv_remote/manifest.json index 3feddacd4e5e4d..cb7a969379e4c2 100644 --- a/homeassistant/components/androidtv_remote/manifest.json +++ b/homeassistant/components/androidtv_remote/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_push", "loggers": ["androidtvremote2"], "quality_scale": "platinum", - "requirements": ["androidtvremote2==0.0.12"], + "requirements": ["androidtvremote2==0.0.13"], "zeroconf": ["_androidtvremote2._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 3c3bdaea46b8be..fb98e1633b5ecf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -399,7 +399,7 @@ amcrest==1.9.7 androidtv[async]==0.0.70 # homeassistant.components.androidtv_remote -androidtvremote2==0.0.12 +androidtvremote2==0.0.13 # homeassistant.components.anel_pwrctrl anel-pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bebdfa4dfbe71d..cd42bdfa9ca156 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -368,7 +368,7 @@ amberelectric==1.0.4 androidtv[async]==0.0.70 # homeassistant.components.androidtv_remote -androidtvremote2==0.0.12 +androidtvremote2==0.0.13 # homeassistant.components.anova anova-wifi==0.10.0 From c99bf90ec7a1f501e36c4c7d8d1d7de88a8605a0 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 1 Aug 2023 10:03:08 +0200 Subject: [PATCH 0983/1009] Offer work- a-round for MQTT entity names that start with the device name (#97495) Co-authored-by: SukramJ Co-authored-by: Franck Nijhof --- homeassistant/components/mqtt/client.py | 26 ++++++ homeassistant/components/mqtt/mixins.py | 42 ++++++++- homeassistant/components/mqtt/models.py | 1 + homeassistant/components/mqtt/strings.json | 16 ++++ tests/components/mqtt/test_mixins.py | 99 ++++++++++++++++++++-- 5 files changed, 173 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index e8eabe887f2160..07fbc0ca8c5a4c 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -36,6 +36,7 @@ ) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util @@ -64,6 +65,7 @@ DEFAULT_WILL, DEFAULT_WS_HEADERS, DEFAULT_WS_PATH, + DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, PROTOCOL_5, @@ -93,6 +95,10 @@ UNSUBSCRIBE_COOLDOWN = 0.1 TIMEOUT_ACK = 10 +MQTT_ENTRIES_NAMING_BLOG_URL = ( + "https://developers.home-assistant.io/blog/2023-057-21-change-naming-mqtt-entities/" +) + SubscribePayloadType = str | bytes # Only bytes if encoding is None @@ -404,6 +410,7 @@ def __init__( @callback def ha_started(_: Event) -> None: + self.register_naming_issues() self._ha_started.set() self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, ha_started) @@ -416,6 +423,25 @@ async def async_stop_mqtt(_event: Event) -> None: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt) ) + def register_naming_issues(self) -> None: + """Register issues with MQTT entity naming.""" + mqtt_data = get_mqtt_data(self.hass) + for issue_key, items in mqtt_data.issues.items(): + config_list = "\n".join([f"- {item}" for item in items]) + async_create_issue( + self.hass, + DOMAIN, + issue_key, + breaks_in_ha_version="2024.2.0", + is_fixable=False, + translation_key=issue_key, + translation_placeholders={ + "config": config_list, + }, + learn_more_url=MQTT_ENTRIES_NAMING_BLOG_URL, + severity=IssueSeverity.WARNING, + ) + def start( self, mqtt_data: MqttData, diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 9f0849a4d4c74b..70156703155ba3 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -1014,6 +1014,7 @@ class MqttEntity( _attr_should_poll = False _default_name: str | None _entity_id_format: str + _issue_key: str | None def __init__( self, @@ -1027,6 +1028,7 @@ def __init__( self._config: ConfigType = config self._attr_unique_id = config.get(CONF_UNIQUE_ID) self._sub_state: dict[str, EntitySubscription] = {} + self._discovery = discovery_data is not None # Load config self._setup_from_config(self._config) @@ -1050,6 +1052,7 @@ def _init_entity_id(self) -> None: @final async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" + self.collect_issues() await super().async_added_to_hass() self._prepare_subscribe_topics() await self._subscribe_topics() @@ -1122,6 +1125,7 @@ def config_schema() -> vol.Schema: def _set_entity_name(self, config: ConfigType) -> None: """Help setting the entity name if needed.""" + self._issue_key = None entity_name: str | None | UndefinedType = config.get(CONF_NAME, UNDEFINED) # Only set _attr_name if it is needed if entity_name is not UNDEFINED: @@ -1130,6 +1134,7 @@ def _set_entity_name(self, config: ConfigType) -> None: # Assign the default name self._attr_name = self._default_name if CONF_DEVICE in config: + device_name: str if CONF_NAME not in config[CONF_DEVICE]: _LOGGER.info( "MQTT device information always needs to include a name, got %s, " @@ -1137,14 +1142,47 @@ def _set_entity_name(self, config: ConfigType) -> None: "name must be included in each entity's device configuration", config, ) - elif config[CONF_DEVICE][CONF_NAME] == entity_name: + elif (device_name := config[CONF_DEVICE][CONF_NAME]) == entity_name: + self._attr_name = None + self._issue_key = ( + "entity_name_is_device_name_discovery" + if self._discovery + else "entity_name_is_device_name_yaml" + ) _LOGGER.warning( "MQTT device name is equal to entity name in your config %s, " "this is not expected. Please correct your configuration. " "The entity name will be set to `null`", config, ) - self._attr_name = None + elif isinstance(entity_name, str) and entity_name.startswith(device_name): + self._attr_name = ( + new_entity_name := entity_name[len(device_name) :].lstrip() + ) + if device_name[:1].isupper(): + # Ensure a capital if the device name first char is a capital + new_entity_name = new_entity_name[:1].upper() + new_entity_name[1:] + self._issue_key = ( + "entity_name_startswith_device_name_discovery" + if self._discovery + else "entity_name_startswith_device_name_yaml" + ) + _LOGGER.warning( + "MQTT entity name starts with the device name in your config %s, " + "this is not expected. Please correct your configuration. " + "The device name prefix will be stripped off the entity name " + "and becomes '%s'", + config, + new_entity_name, + ) + + def collect_issues(self) -> None: + """Process issues for MQTT entities.""" + if self._issue_key is None: + return + mqtt_data = get_mqtt_data(self.hass) + issues = mqtt_data.issues.setdefault(self._issue_key, set()) + issues.add(self.entity_id) def _setup_common_attributes_from_config(self, config: ConfigType) -> None: """(Re)Setup the common attributes for the entity.""" diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 5a966a4455c914..9afa3de3f48d98 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -305,6 +305,7 @@ class MqttData: ) discovery_unsubscribe: list[CALLBACK_TYPE] = field(default_factory=list) integration_unsubscribe: dict[str, CALLBACK_TYPE] = field(default_factory=dict) + issues: dict[str, set[str]] = field(default_factory=dict) last_discovery: float = 0.0 reload_dispatchers: list[CALLBACK_TYPE] = field(default_factory=list) reload_handlers: dict[str, Callable[[], Coroutine[Any, Any, None]]] = field( diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index f314ddd47d3c4b..55677798a083c4 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -7,6 +7,22 @@ "deprecation_mqtt_legacy_vacuum_discovery": { "title": "MQTT vacuum entities with legacy schema added through MQTT discovery", "description": "MQTT vacuum entities that use the legacy schema are deprecated, please adjust your devices to use the correct schema and restart Home Assistant to fix this issue." + }, + "entity_name_is_device_name_yaml": { + "title": "Manual configured MQTT entities with a name that is equal to the device name", + "description": "Some MQTT entities have an entity name equal to the device name. This is not expected. The entity name is set to `null` as a work-a-round to avoid a duplicate name. Please update your configuration and restart Home Assistant to fix this issue.\n\nList of affected entities:\n\n{config}" + }, + "entity_name_startswith_device_name_yaml": { + "title": "Manual configured MQTT entities with a name that starts with the device name", + "description": "Some MQTT entities have an entity name that starts with the device name. This is not expected. To avoid a duplicate name the device name prefix is stripped of the entity name as a work-a-round. Please update your configuration and restart Home Assistant to fix this issue. \n\nList of affected entities:\n\n{config}" + }, + "entity_name_is_device_name_discovery": { + "title": "Discovered MQTT entities with a name that is equal to the device name", + "description": "Some MQTT entities have an entity name equal to the device name. This is not expected. The entity name is set to `null` as a work-a-round to avoid a duplicate name. Please inform the maintainer of the software application that supplies the affected entities to fix this issue.\n\nList of affected entities:\n\n{config}" + }, + "entity_name_startswith_device_name_discovery": { + "title": "Discovered entities with a name that starts with the device name", + "description": "Some MQTT entities have an entity name that starts with the device name. This is not expected. To avoid a duplicate name the device name prefix is stripped of the entity name as a work-a-round. Please inform the maintainer of the software application that supplies the affected entities to fix this issue. \n\nList of affected entities:\n\n{config}" } }, "config": { diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py index 23367d7829f216..18269eb6970cc7 100644 --- a/tests/components/mqtt/test_mixins.py +++ b/tests/components/mqtt/test_mixins.py @@ -6,14 +6,19 @@ from homeassistant.components import mqtt, sensor from homeassistant.components.mqtt.sensor import DEFAULT_NAME as DEFAULT_SENSOR_NAME -from homeassistant.const import EVENT_STATE_CHANGED, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STARTED, + EVENT_STATE_CHANGED, + Platform, +) +from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.helpers import ( device_registry as dr, + issue_registry as ir, ) -from tests.common import async_fire_mqtt_message -from tests.typing import MqttMockHAClientGenerator +from tests.common import MockConfigEntry, async_capture_events, async_fire_mqtt_message +from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient @pytest.mark.parametrize( @@ -80,7 +85,14 @@ def test_callback(event) -> None: @pytest.mark.parametrize( - ("hass_config", "entity_id", "friendly_name", "device_name", "assert_log"), + ( + "hass_config", + "entity_id", + "friendly_name", + "device_name", + "assert_log", + "issue_events", + ), [ ( # default_entity_name_without_device_name { @@ -96,6 +108,7 @@ def test_callback(event) -> None: DEFAULT_SENSOR_NAME, None, True, + 0, ), ( # default_entity_name_with_device_name { @@ -111,6 +124,7 @@ def test_callback(event) -> None: "Test MQTT Sensor", "Test", False, + 0, ), ( # name_follows_device_class { @@ -127,6 +141,7 @@ def test_callback(event) -> None: "Test Humidity", "Test", False, + 0, ), ( # name_follows_device_class_without_device_name { @@ -143,6 +158,7 @@ def test_callback(event) -> None: "Humidity", None, True, + 0, ), ( # name_overrides_device_class { @@ -160,6 +176,7 @@ def test_callback(event) -> None: "Test MySensor", "Test", False, + 0, ), ( # name_set_no_device_name_set { @@ -177,6 +194,7 @@ def test_callback(event) -> None: "MySensor", None, True, + 0, ), ( # none_entity_name_with_device_name { @@ -194,6 +212,7 @@ def test_callback(event) -> None: "Test", "Test", False, + 0, ), ( # none_entity_name_without_device_name { @@ -211,8 +230,9 @@ def test_callback(event) -> None: "mqtt veryunique", None, True, + 0, ), - ( # entity_name_and_device_name_the_sane + ( # entity_name_and_device_name_the_same { mqtt.DOMAIN: { sensor.DOMAIN: { @@ -231,6 +251,49 @@ def test_callback(event) -> None: "Hello world", "Hello world", False, + 1, + ), + ( # entity_name_startswith_device_name1 + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "World automation", + "state_topic": "test-topic", + "unique_id": "veryunique", + "device_class": "humidity", + "device": { + "identifiers": ["helloworld"], + "name": "World", + }, + } + } + }, + "sensor.world_automation", + "World automation", + "World", + False, + 1, + ), + ( # entity_name_startswith_device_name2 + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "world automation", + "state_topic": "test-topic", + "unique_id": "veryunique", + "device_class": "humidity", + "device": { + "identifiers": ["helloworld"], + "name": "world", + }, + } + } + }, + "sensor.world_automation", + "world automation", + "world", + False, + 1, ), ], ids=[ @@ -242,24 +305,39 @@ def test_callback(event) -> None: "name_set_no_device_name_set", "none_entity_name_with_device_name", "none_entity_name_without_device_name", - "entity_name_and_device_name_the_sane", + "entity_name_and_device_name_the_same", + "entity_name_startswith_device_name1", + "entity_name_startswith_device_name2", ], ) @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) async def test_default_entity_and_device_name( hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + mqtt_client_mock: MqttMockPahoClient, + mqtt_config_entry_data, caplog: pytest.LogCaptureFixture, entity_id: str, friendly_name: str, device_name: str | None, assert_log: bool, + issue_events: int, ) -> None: """Test device name setup with and without a device_class set. This is a test helper for the _setup_common_attributes_from_config mixin. """ - await mqtt_mock_entry() + # mqtt_mock = await mqtt_mock_entry() + + events = async_capture_events(hass, ir.EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED) + hass.state = CoreState.starting + await hass.async_block_till_done() + + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "mock-broker"}) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() registry = dr.async_get(hass) @@ -274,3 +352,6 @@ async def test_default_entity_and_device_name( assert ( "MQTT device information always needs to include a name" in caplog.text ) is assert_log + + # Assert that an issues ware registered + assert len(events) == issue_events From e473131a2c7dcfd1da95f03b91e46a2d20292c90 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Mon, 31 Jul 2023 03:38:31 -0700 Subject: [PATCH 0984/1009] Bump pywemo to 1.2.0 (#97520) --- 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 bb19d2e1655014..3dbd8aa32bc476 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==1.1.0"], + "requirements": ["pywemo==1.2.0"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index fb98e1633b5ecf..b708fbe3f5f818 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2224,7 +2224,7 @@ pyvolumio==0.1.5 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==1.1.0 +pywemo==1.2.0 # homeassistant.components.wilight pywilight==0.0.74 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd42bdfa9ca156..e31b4513070d44 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1635,7 +1635,7 @@ pyvolumio==0.1.5 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==1.1.0 +pywemo==1.2.0 # homeassistant.components.wilight pywilight==0.0.74 From ec1b24f8d67321fcf4bbc16c32b11a16f2602f74 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 31 Jul 2023 12:17:51 +0200 Subject: [PATCH 0985/1009] Handle http error in Renault initialisation (#97530) --- homeassistant/components/renault/__init__.py | 5 ++++- tests/components/renault/test_init.py | 21 +++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/renault/__init__.py b/homeassistant/components/renault/__init__.py index b02938b16521f3..f69451290bcf6d 100644 --- a/homeassistant/components/renault/__init__.py +++ b/homeassistant/components/renault/__init__.py @@ -26,7 +26,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b raise ConfigEntryAuthFailed() hass.data.setdefault(DOMAIN, {}) - await renault_hub.async_initialise(config_entry) + try: + await renault_hub.async_initialise(config_entry) + except aiohttp.ClientResponseError as exc: + raise ConfigEntryNotReady() from exc hass.data[DOMAIN][config_entry.entry_id] = renault_hub diff --git a/tests/components/renault/test_init.py b/tests/components/renault/test_init.py index 7f2aee9d7bdf7c..415b07dc7e691d 100644 --- a/tests/components/renault/test_init.py +++ b/tests/components/renault/test_init.py @@ -1,7 +1,7 @@ """Tests for Renault setup process.""" from collections.abc import Generator from typing import Any -from unittest.mock import patch +from unittest.mock import Mock, patch import aiohttp import pytest @@ -76,3 +76,22 @@ async def test_setup_entry_exception( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert config_entry.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) + + +@pytest.mark.usefixtures("patch_renault_account") +async def test_setup_entry_kamereon_exception( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: + """Test ConfigEntryNotReady when API raises an exception during entry setup.""" + # In this case we are testing the condition where renault_hub fails to retrieve + # list of vehicles (see Gateway Time-out on #97324). + with patch( + "renault_api.renault_client.RenaultClient.get_api_account", + side_effect=aiohttp.ClientResponseError(Mock(), (), status=504), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.SETUP_RETRY + assert not hass.data.get(DOMAIN) From 83552304336c39e789f3066edf236d6e6e071a21 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 31 Jul 2023 18:44:03 +0200 Subject: [PATCH 0986/1009] Fix RootFolder not iterable in Radarr (#97537) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/radarr/coordinator.py | 12 +- tests/components/radarr/__init__.py | 34 +++-- .../radarr/fixtures/single-movie.json | 116 ++++++++++++++++++ .../fixtures/single-rootfolder-linux.json | 6 + .../fixtures/single-rootfolder-windows.json | 6 + tests/components/radarr/test_sensor.py | 43 +++++-- 6 files changed, 189 insertions(+), 28 deletions(-) create mode 100644 tests/components/radarr/fixtures/single-movie.json create mode 100644 tests/components/radarr/fixtures/single-rootfolder-linux.json create mode 100644 tests/components/radarr/fixtures/single-rootfolder-windows.json diff --git a/homeassistant/components/radarr/coordinator.py b/homeassistant/components/radarr/coordinator.py index 5537a18725c72f..c318d662028bcd 100644 --- a/homeassistant/components/radarr/coordinator.py +++ b/homeassistant/components/radarr/coordinator.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod from datetime import timedelta -from typing import Generic, TypeVar, cast +from typing import Generic, TypeVar from aiopyarr import Health, RadarrMovie, RootFolder, SystemStatus, exceptions from aiopyarr.models.host_configuration import PyArrHostConfiguration @@ -71,7 +71,10 @@ class DiskSpaceDataUpdateCoordinator(RadarrDataUpdateCoordinator[list[RootFolder async def _fetch_data(self) -> list[RootFolder]: """Fetch the data.""" - return cast(list[RootFolder], await self.api_client.async_get_root_folders()) + root_folders = await self.api_client.async_get_root_folders() + if isinstance(root_folders, RootFolder): + root_folders = [root_folders] + return root_folders class HealthDataUpdateCoordinator(RadarrDataUpdateCoordinator[list[Health]]): @@ -87,4 +90,7 @@ class MoviesDataUpdateCoordinator(RadarrDataUpdateCoordinator[int]): async def _fetch_data(self) -> int: """Fetch the movies data.""" - return len(cast(list[RadarrMovie], await self.api_client.async_get_movies())) + movies = await self.api_client.async_get_movies() + if isinstance(movies, RadarrMovie): + return 1 + return len(movies) diff --git a/tests/components/radarr/__init__.py b/tests/components/radarr/__init__.py index 7e574b1e3e00f2..069eeabe8d86b7 100644 --- a/tests/components/radarr/__init__.py +++ b/tests/components/radarr/__init__.py @@ -41,6 +41,7 @@ def mock_connection( error: bool = False, invalid_auth: bool = False, windows: bool = False, + single_return: bool = False, ) -> None: """Mock radarr connection.""" if error: @@ -75,22 +76,27 @@ def mock_connection( headers={"Content-Type": CONTENT_TYPE_JSON}, ) + root_folder_fixture = "rootfolder-linux" + if windows: - aioclient_mock.get( - f"{url}/api/v3/rootfolder", - text=load_fixture("radarr/rootfolder-windows.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - else: - aioclient_mock.get( - f"{url}/api/v3/rootfolder", - text=load_fixture("radarr/rootfolder-linux.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) + root_folder_fixture = "rootfolder-windows" + + if single_return: + root_folder_fixture = f"single-{root_folder_fixture}" + + aioclient_mock.get( + f"{url}/api/v3/rootfolder", + text=load_fixture(f"radarr/{root_folder_fixture}.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + movie_fixture = "movie" + if single_return: + movie_fixture = f"single-{movie_fixture}" aioclient_mock.get( f"{url}/api/v3/movie", - text=load_fixture("radarr/movie.json"), + text=load_fixture(f"radarr/{movie_fixture}.json"), headers={"Content-Type": CONTENT_TYPE_JSON}, ) @@ -139,6 +145,7 @@ async def setup_integration( connection_error: bool = False, invalid_auth: bool = False, windows: bool = False, + single_return: bool = False, ) -> MockConfigEntry: """Set up the radarr integration in Home Assistant.""" entry = MockConfigEntry( @@ -159,6 +166,7 @@ async def setup_integration( error=connection_error, invalid_auth=invalid_auth, windows=windows, + single_return=single_return, ) if not skip_entry_setup: @@ -183,7 +191,7 @@ def patch_radarr(): def create_entry(hass: HomeAssistant) -> MockConfigEntry: - """Create Efergy entry in Home Assistant.""" + """Create Radarr entry in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, data={ diff --git a/tests/components/radarr/fixtures/single-movie.json b/tests/components/radarr/fixtures/single-movie.json new file mode 100644 index 00000000000000..db9e720d28552e --- /dev/null +++ b/tests/components/radarr/fixtures/single-movie.json @@ -0,0 +1,116 @@ +{ + "id": 0, + "title": "string", + "originalTitle": "string", + "alternateTitles": [ + { + "sourceType": "tmdb", + "movieId": 1, + "title": "string", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1 + } + ], + "sortTitle": "string", + "sizeOnDisk": 0, + "overview": "string", + "inCinemas": "2020-11-06T00:00:00Z", + "physicalRelease": "2019-03-19T00:00:00Z", + "images": [ + { + "coverType": "poster", + "url": "string", + "remoteUrl": "string" + } + ], + "website": "string", + "year": 0, + "hasFile": true, + "youTubeTrailerId": "string", + "studio": "string", + "path": "string", + "rootFolderPath": "string", + "qualityProfileId": 0, + "monitored": true, + "minimumAvailability": "announced", + "isAvailable": true, + "folderName": "string", + "runtime": 0, + "cleanTitle": "string", + "imdbId": "string", + "tmdbId": 0, + "titleSlug": "string", + "certification": "string", + "genres": ["string"], + "tags": [0], + "added": "2018-12-28T05:56:49Z", + "ratings": { + "votes": 0, + "value": 0 + }, + "movieFile": { + "movieId": 0, + "relativePath": "string", + "path": "string", + "size": 916662234, + "dateAdded": "2020-11-26T02:00:35Z", + "indexerFlags": 1, + "quality": { + "quality": { + "id": 14, + "name": "WEBRip-720p", + "source": "webrip", + "resolution": 720, + "modifier": "none" + }, + "revision": { + "version": 1, + "real": 0, + "isRepack": false + } + }, + "mediaInfo": { + "audioBitrate": 0, + "audioChannels": 2, + "audioCodec": "AAC", + "audioLanguages": "", + "audioStreamCount": 1, + "videoBitDepth": 8, + "videoBitrate": 1000000, + "videoCodec": "x264", + "videoFps": 25.0, + "resolution": "1280x534", + "runTime": "1:49:06", + "scanType": "Progressive", + "subtitles": "" + }, + "originalFilePath": "string", + "qualityCutoffNotMet": true, + "languages": [ + { + "id": 26, + "name": "Hindi" + } + ], + "edition": "", + "id": 35361 + }, + "collection": { + "name": "string", + "tmdbId": 0, + "images": [ + { + "coverType": "poster", + "url": "string", + "remoteUrl": "string" + } + ] + }, + "status": "deleted" +} diff --git a/tests/components/radarr/fixtures/single-rootfolder-linux.json b/tests/components/radarr/fixtures/single-rootfolder-linux.json new file mode 100644 index 00000000000000..085467fda6abb5 --- /dev/null +++ b/tests/components/radarr/fixtures/single-rootfolder-linux.json @@ -0,0 +1,6 @@ +{ + "path": "/downloads", + "freeSpace": 282500064232, + "unmappedFolders": [], + "id": 1 +} diff --git a/tests/components/radarr/fixtures/single-rootfolder-windows.json b/tests/components/radarr/fixtures/single-rootfolder-windows.json new file mode 100644 index 00000000000000..25a93baa10dd74 --- /dev/null +++ b/tests/components/radarr/fixtures/single-rootfolder-windows.json @@ -0,0 +1,6 @@ +{ + "path": "D:\\Downloads\\TV", + "freeSpace": 282500064232, + "unmappedFolders": [], + "id": 1 +} diff --git a/tests/components/radarr/test_sensor.py b/tests/components/radarr/test_sensor.py index d3dde74dcbfbec..f4f863d9bb6e2d 100644 --- a/tests/components/radarr/test_sensor.py +++ b/tests/components/radarr/test_sensor.py @@ -1,4 +1,5 @@ """The tests for Radarr sensor platform.""" +import pytest from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT @@ -9,15 +10,43 @@ from tests.test_util.aiohttp import AiohttpClientMocker +@pytest.mark.parametrize( + ("windows", "single", "root_folder"), + [ + ( + False, + False, + "downloads", + ), + ( + False, + True, + "downloads", + ), + ( + True, + False, + "tv", + ), + ( + True, + True, + "tv", + ), + ], +) async def test_sensors( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, entity_registry_enabled_by_default: None, + windows: bool, + single: bool, + root_folder: str, ) -> None: """Test for successfully setting up the Radarr platform.""" - await setup_integration(hass, aioclient_mock) + await setup_integration(hass, aioclient_mock, windows=windows, single_return=single) - state = hass.states.get("sensor.mock_title_disk_space_downloads") + state = hass.states.get(f"sensor.mock_title_disk_space_{root_folder}") assert state.state == "263.10" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "GB" state = hass.states.get("sensor.mock_title_movies") @@ -26,13 +55,3 @@ async def test_sensors( state = hass.states.get("sensor.mock_title_start_time") assert state.state == "2020-09-01T23:50:20+00:00" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP - - -async def test_windows( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test for successfully setting up the Radarr platform on Windows.""" - await setup_integration(hass, aioclient_mock, windows=True) - - state = hass.states.get("sensor.mock_title_disk_space_tv") - assert state.state == "263.10" From 3f22c74ffa26921d81636d6c86594dc3a39a3a03 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 31 Jul 2023 21:16:58 +0200 Subject: [PATCH 0987/1009] Fix unit tests for wake_on_lan (#97542) --- tests/components/wake_on_lan/conftest.py | 19 ++++- tests/components/wake_on_lan/test_switch.py | 89 ++++++++++----------- 2 files changed, 60 insertions(+), 48 deletions(-) diff --git a/tests/components/wake_on_lan/conftest.py b/tests/components/wake_on_lan/conftest.py index 582698e39d53e7..5fa44f10c2c078 100644 --- a/tests/components/wake_on_lan/conftest.py +++ b/tests/components/wake_on_lan/conftest.py @@ -1,7 +1,8 @@ """Test fixtures for Wake on Lan.""" from __future__ import annotations -from unittest.mock import AsyncMock, patch +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -11,3 +12,19 @@ def mock_send_magic_packet() -> AsyncMock: """Mock magic packet.""" with patch("wakeonlan.send_magic_packet") as mock_send: yield mock_send + + +@pytest.fixture +def subprocess_call_return_value() -> int | None: + """Return value for subprocess.""" + return 1 + + +@pytest.fixture(autouse=True) +def mock_subprocess_call( + subprocess_call_return_value: int, +) -> Generator[None, None, MagicMock]: + """Mock magic packet.""" + with patch("homeassistant.components.wake_on_lan.switch.sp.call") as mock_sp: + mock_sp.return_value = subprocess_call_return_value + yield mock_sp diff --git a/tests/components/wake_on_lan/test_switch.py b/tests/components/wake_on_lan/test_switch.py index 8a7fe185662400..b2702ed18157d1 100644 --- a/tests/components/wake_on_lan/test_switch.py +++ b/tests/components/wake_on_lan/test_switch.py @@ -1,7 +1,6 @@ """The tests for the wake on lan switch platform.""" from __future__ import annotations -import subprocess from unittest.mock import AsyncMock, patch from homeassistant.components import switch @@ -38,7 +37,7 @@ async def test_valid_hostname( state = hass.states.get("switch.wake_on_lan") assert state.state == STATE_OFF - with patch.object(subprocess, "call", return_value=0): + with patch("homeassistant.components.wake_on_lan.switch.sp.call", return_value=0): await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_ON, @@ -85,17 +84,16 @@ async def test_broadcast_config_ip_and_port( state = hass.states.get("switch.wake_on_lan") assert state.state == STATE_OFF - with patch.object(subprocess, "call", return_value=0): - await hass.services.async_call( - switch.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wake_on_lan"}, - blocking=True, - ) + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wake_on_lan"}, + blocking=True, + ) - mock_send_magic_packet.assert_called_with( - mac, ip_address=broadcast_address, port=port - ) + mock_send_magic_packet.assert_called_with( + mac, ip_address=broadcast_address, port=port + ) async def test_broadcast_config_ip( @@ -122,15 +120,14 @@ async def test_broadcast_config_ip( state = hass.states.get("switch.wake_on_lan") assert state.state == STATE_OFF - with patch.object(subprocess, "call", return_value=0): - await hass.services.async_call( - switch.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wake_on_lan"}, - blocking=True, - ) + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wake_on_lan"}, + blocking=True, + ) - mock_send_magic_packet.assert_called_with(mac, ip_address=broadcast_address) + mock_send_magic_packet.assert_called_with(mac, ip_address=broadcast_address) async def test_broadcast_config_port( @@ -151,15 +148,14 @@ async def test_broadcast_config_port( state = hass.states.get("switch.wake_on_lan") assert state.state == STATE_OFF - with patch.object(subprocess, "call", return_value=0): - await hass.services.async_call( - switch.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wake_on_lan"}, - blocking=True, - ) + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wake_on_lan"}, + blocking=True, + ) - mock_send_magic_packet.assert_called_with(mac, port=port) + mock_send_magic_packet.assert_called_with(mac, port=port) async def test_off_script( @@ -185,7 +181,7 @@ async def test_off_script( state = hass.states.get("switch.wake_on_lan") assert state.state == STATE_OFF - with patch.object(subprocess, "call", return_value=0): + with patch("homeassistant.components.wake_on_lan.switch.sp.call", return_value=0): await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_ON, @@ -197,7 +193,7 @@ async def test_off_script( assert state.state == STATE_ON assert len(calls) == 0 - with patch.object(subprocess, "call", return_value=2): + with patch("homeassistant.components.wake_on_lan.switch.sp.call", return_value=1): await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_OFF, @@ -230,23 +226,22 @@ async def test_no_hostname_state( state = hass.states.get("switch.wake_on_lan") assert state.state == STATE_OFF - with patch.object(subprocess, "call", return_value=0): - await hass.services.async_call( - switch.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wake_on_lan"}, - blocking=True, - ) + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wake_on_lan"}, + blocking=True, + ) - state = hass.states.get("switch.wake_on_lan") - assert state.state == STATE_ON + state = hass.states.get("switch.wake_on_lan") + assert state.state == STATE_ON - await hass.services.async_call( - switch.DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.wake_on_lan"}, - blocking=True, - ) + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wake_on_lan"}, + blocking=True, + ) - state = hass.states.get("switch.wake_on_lan") - assert state.state == STATE_OFF + state = hass.states.get("switch.wake_on_lan") + assert state.state == STATE_OFF From d891f1a5eb01c786570272561a70f26fb03b3329 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Jul 2023 21:49:20 -1000 Subject: [PATCH 0988/1009] Bump HAP-python to 4.7.1 (#97545) --- homeassistant/components/homekit/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/manifest.json b/homeassistant/components/homekit/manifest.json index 19fd0b518b24b7..04ba4cc1a6a2c7 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["pyhap"], "requirements": [ - "HAP-python==4.7.0", + "HAP-python==4.7.1", "fnv-hash-fast==0.4.0", "PyQRCode==1.2.1", "base36==0.1.1" diff --git a/requirements_all.txt b/requirements_all.txt index b708fbe3f5f818..9e51bab7254524 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -26,7 +26,7 @@ CO2Signal==0.4.2 DoorBirdPy==2.1.0 # homeassistant.components.homekit -HAP-python==4.7.0 +HAP-python==4.7.1 # homeassistant.components.tasmota HATasmota==0.6.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e31b4513070d44..28d677a795ebd5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -25,7 +25,7 @@ CO2Signal==0.4.2 DoorBirdPy==2.1.0 # homeassistant.components.homekit -HAP-python==4.7.0 +HAP-python==4.7.1 # homeassistant.components.tasmota HATasmota==0.6.5 From c412cf9a5e15fdab567ebecebf1d76bb5a610926 Mon Sep 17 00:00:00 2001 From: tronikos Date: Tue, 1 Aug 2023 00:45:17 -0700 Subject: [PATCH 0989/1009] Bump opower to 0.0.18 (#97548) --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index c054af8f43cabc..c0eb319c10ccba 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["recorder"], "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", - "requirements": ["opower==0.0.16"] + "requirements": ["opower==0.0.18"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9e51bab7254524..ae8111ec0eb71b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1368,7 +1368,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.16 +opower==0.0.18 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 28d677a795ebd5..5f2d89c544e5ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1037,7 +1037,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.16 +opower==0.0.18 # homeassistant.components.oralb oralb-ble==0.17.6 From c600d07a9db6c542c898f4edc3cd31c69b23b430 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Tue, 1 Aug 2023 03:04:04 -0500 Subject: [PATCH 0990/1009] Bump life360 package to 6.0.0 (#97549) --- homeassistant/components/life360/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index bfecce8d3ed303..18b83013d70bcd 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/life360", "iot_class": "cloud_polling", "loggers": ["life360"], - "requirements": ["life360==5.5.0"] + "requirements": ["life360==6.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index ae8111ec0eb71b..ba56ae548e6bc7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1129,7 +1129,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==5.5.0 +life360==6.0.0 # homeassistant.components.osramlightify lightify==1.0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5f2d89c544e5ba..78f01c83e393cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -879,7 +879,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==5.5.0 +life360==6.0.0 # homeassistant.components.logi_circle logi-circle==0.2.3 From f780397c2d4bcd384dec680f5f390be610342e22 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 1 Aug 2023 01:08:08 -0700 Subject: [PATCH 0991/1009] Bump pywemo to 1.2.1 (#97550) --- 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 3dbd8aa32bc476..cb189116eeba79 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==1.2.0"], + "requirements": ["pywemo==1.2.1"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index ba56ae548e6bc7..c61ebf9527b527 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2224,7 +2224,7 @@ pyvolumio==0.1.5 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==1.2.0 +pywemo==1.2.1 # homeassistant.components.wilight pywilight==0.0.74 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 78f01c83e393cd..3af0fd1fa42f79 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1635,7 +1635,7 @@ pyvolumio==0.1.5 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==1.2.0 +pywemo==1.2.1 # homeassistant.components.wilight pywilight==0.0.74 From 20cf5f0f2cb84737ef69f21a5d908a2c65ee0a21 Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Tue, 1 Aug 2023 20:06:19 +1200 Subject: [PATCH 0992/1009] Fix Starlink ping drop rate reporting (#97555) --- homeassistant/components/starlink/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/starlink/sensor.py b/homeassistant/components/starlink/sensor.py index efcf92600b8fab..ab76a8dffddd4b 100644 --- a/homeassistant/components/starlink/sensor.py +++ b/homeassistant/components/starlink/sensor.py @@ -130,6 +130,6 @@ def native_value(self) -> StateType | datetime: translation_key="ping_drop_rate", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, - value_fn=lambda data: data.status["pop_ping_drop_rate"], + value_fn=lambda data: data.status["pop_ping_drop_rate"] * 100, ), ) From dfd5c74de06837340e743fa0a42d39db5513ccdc Mon Sep 17 00:00:00 2001 From: Pedro Lamas Date: Tue, 1 Aug 2023 10:04:30 +0100 Subject: [PATCH 0993/1009] Fixes London Air parsing error (#97557) --- homeassistant/components/london_air/sensor.py | 10 ++++++---- tests/fixtures/london_air.json | 8 ++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/london_air/sensor.py b/homeassistant/components/london_air/sensor.py index e970f040b5f137..98cc4c4b4e80a4 100644 --- a/homeassistant/components/london_air/sensor.py +++ b/homeassistant/components/london_air/sensor.py @@ -218,10 +218,12 @@ def parse_api_response(response): for authority in AUTHORITIES: for entry in response["HourlyAirQualityIndex"]["LocalAuthority"]: if entry["@LocalAuthorityName"] == authority: - if isinstance(entry["Site"], dict): - entry_sites_data = [entry["Site"]] - else: - entry_sites_data = entry["Site"] + entry_sites_data = [] + if "Site" in entry: + if isinstance(entry["Site"], dict): + entry_sites_data = [entry["Site"]] + else: + entry_sites_data = entry["Site"] data[authority] = parse_site(entry_sites_data) diff --git a/tests/fixtures/london_air.json b/tests/fixtures/london_air.json index 3a3d9afb643099..7045a90e6e9f6b 100644 --- a/tests/fixtures/london_air.json +++ b/tests/fixtures/london_air.json @@ -3,6 +3,14 @@ "@GroupName": "London", "@TimeToLive": "38", "LocalAuthority": [ + { + "@LocalAuthorityCode": "7", + "@LocalAuthorityName": "City of London", + "@LaCentreLatitude": "51.51333", + "@LaCentreLongitude": "-0.088947", + "@LaCentreLatitudeWGS84": "6712603.132989", + "@LaCentreLongitudeWGS84": "-9901.534748" + }, { "@LocalAuthorityCode": "24", "@LocalAuthorityName": "Merton", From 8261a769a53559c6af655aaf29073e92fcf95ec6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 1 Aug 2023 11:46:37 +0200 Subject: [PATCH 0994/1009] Update frontend to 20230801.0 (#97561) --- 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 47e742bdb764a0..2210a44039e1e2 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==20230725.0"] + "requirements": ["home-assistant-frontend==20230801.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 04c0b0fd44f429..3a8878044708fa 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.69.0 hassil==1.2.5 home-assistant-bluetooth==1.10.2 -home-assistant-frontend==20230725.0 +home-assistant-frontend==20230801.0 home-assistant-intents==2023.7.25 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index c61ebf9527b527..7ff7d9b82f550c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -988,7 +988,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230725.0 +home-assistant-frontend==20230801.0 # homeassistant.components.conversation home-assistant-intents==2023.7.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3af0fd1fa42f79..23df6a82bcd67b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -774,7 +774,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230725.0 +home-assistant-frontend==20230801.0 # homeassistant.components.conversation home-assistant-intents==2023.7.25 From 2f6aea450ebeb0d949dd23d0b7c8012206edbc55 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Aug 2023 11:48:10 +0200 Subject: [PATCH 0995/1009] Bumped version to 2023.8.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 43809524d4ad35..936393cc8cb80c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index aaf057da02cf01..a47d883d146f73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.8.0b2" +version = "2023.8.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 87c11ca419c0e0dfdf393e571f927c7440746137 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Tue, 1 Aug 2023 14:39:31 +0200 Subject: [PATCH 0996/1009] Bump pyduotecno to 2023.8.0 (beta fix) (#97564) * Bump pyduotecno to 2023.7.4 * Bump to 2023.8.0 --- homeassistant/components/duotecno/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/duotecno/manifest.json b/homeassistant/components/duotecno/manifest.json index a630a3dedbd477..3089e3b515b684 100644 --- a/homeassistant/components/duotecno/manifest.json +++ b/homeassistant/components/duotecno/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/duotecno", "iot_class": "local_push", - "requirements": ["pyduotecno==2023.7.3"] + "requirements": ["pyduotecno==2023.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7ff7d9b82f550c..aed6674d8103ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1650,7 +1650,7 @@ pydrawise==2023.7.1 pydroid-ipcam==2.0.0 # homeassistant.components.duotecno -pyduotecno==2023.7.3 +pyduotecno==2023.8.0 # homeassistant.components.ebox pyebox==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 23df6a82bcd67b..64118d5a30b468 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1223,7 +1223,7 @@ pydiscovergy==2.0.1 pydroid-ipcam==2.0.0 # homeassistant.components.duotecno -pyduotecno==2023.7.3 +pyduotecno==2023.8.0 # homeassistant.components.econet pyeconet==0.1.20 From 116b0267687ff6b43c718d64f2c731f58b645292 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 1 Aug 2023 21:31:45 +0200 Subject: [PATCH 0997/1009] Unignore today's collection for Rova (#97567) --- homeassistant/components/rova/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index 21effd3da3a8ea..f68ffbd0eaf510 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle -from homeassistant.util.dt import get_time_zone, now +from homeassistant.util.dt import get_time_zone # Config for rova requests. CONF_ZIP_CODE = "zip_code" @@ -150,8 +150,7 @@ def update(self): tzinfo=get_time_zone("Europe/Amsterdam") ) code = item["GarbageTypeCode"].lower() - - if code not in self.data and date > now(): + if code not in self.data: self.data[code] = date _LOGGER.debug("Updated Rova calendar: %s", self.data) From 2b26e205288a5254c11c659fc772f6f4aca41510 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Aug 2023 09:08:12 -1000 Subject: [PATCH 0998/1009] Use legacy rules for ESPHome entity_id construction if `friendly_name` is unset (#97578) --- homeassistant/components/esphome/entity.py | 23 ++++++++++++++++++---- tests/components/esphome/test_entity.py | 2 +- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index b308d8dc08c01c..c35b4dc9b13fdb 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -161,14 +161,29 @@ def __init__( assert entry_data.device_info is not None device_info = entry_data.device_info self._device_info = device_info - if object_id := entity_info.object_id: - # Use the object_id to suggest the entity_id - self.entity_id = f"{domain}.{device_info.name}_{object_id}" self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)} ) self._entry_id = entry_data.entry_id - self._attr_has_entity_name = bool(device_info.friendly_name) + # + # If `friendly_name` is set, we use the Friendly naming rules, if + # `friendly_name` is not set we make an exception to the naming rules for + # backwards compatibility and use the Legacy naming rules. + # + # Friendly naming + # - Friendly name is prepended to entity names + # - Device Name is prepended to entity ids + # - Entity id is constructed from device name and object id + # + # Legacy naming + # - Device name is not prepended to entity names + # - Device name is not prepended to entity ids + # - Entity id is constructed from entity name + # + if not device_info.friendly_name: + return + self._attr_has_entity_name = True + self.entity_id = f"{domain}.{device_info.name}_{entity_info.object_id}" async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index e55d45832759bf..ac121a93eff87d 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -216,6 +216,6 @@ async def test_esphome_device_without_friendly_name( states=states, device_info={"friendly_name": None}, ) - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.my_binary_sensor") assert state is not None assert state.state == STATE_ON From c3bcffdce7242d146a217ac1ceea5091dc167b85 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 1 Aug 2023 21:18:33 +0200 Subject: [PATCH 0999/1009] Fix UniFi image platform failing to setup on read-only account (#97580) --- homeassistant/components/unifi/image.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/unifi/image.py b/homeassistant/components/unifi/image.py index 25c368880fa17a..dc4fb93eded827 100644 --- a/homeassistant/components/unifi/image.py +++ b/homeassistant/components/unifi/image.py @@ -83,6 +83,10 @@ async def async_setup_entry( ) -> None: """Set up image platform for UniFi Network integration.""" controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + + if controller.site_role != "admin": + return + controller.register_platform_add_entities( UnifiImageEntity, ENTITY_DESCRIPTIONS, async_add_entities ) From 97e28acfc934af55867ed1ae4cd1a7c2d0c6e909 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 1 Aug 2023 22:26:36 +0200 Subject: [PATCH 1000/1009] Bump zha-quirks to 0.0.102 (#97588) --- 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 7694a85b8ede4f..5e33377ec0ef74 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -23,7 +23,7 @@ "bellows==0.35.8", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.101", + "zha-quirks==0.0.102", "zigpy-deconz==0.21.0", "zigpy==0.56.2", "zigpy-xbee==0.18.1", diff --git a/requirements_all.txt b/requirements_all.txt index aed6674d8103ce..8d52c7a3927f0f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2755,7 +2755,7 @@ zeroconf==0.71.4 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.101 +zha-quirks==0.0.102 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64118d5a30b468..2a7ec674fc303c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2028,7 +2028,7 @@ zeroconf==0.71.4 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.101 +zha-quirks==0.0.102 # homeassistant.components.zha zigpy-deconz==0.21.0 From 80e0bcfaea7976a7ddda3a34ce4af69e962e3377 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Aug 2023 23:15:31 +0200 Subject: [PATCH 1001/1009] Ensure load the device registry if it contains invalid configuration URLs (#97589) --- homeassistant/helpers/device_registry.py | 9 ++++-- tests/helpers/test_device_registry.py | 41 ++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 5764f65957e772..4dd9233c6ab4ec 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -198,9 +198,7 @@ class DeviceEntry: area_id: str | None = attr.ib(default=None) config_entries: set[str] = attr.ib(converter=set, factory=set) - configuration_url: str | URL | None = attr.ib( - converter=_validate_configuration_url, default=None - ) + configuration_url: str | None = attr.ib(default=None) connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set) disabled_by: DeviceEntryDisabler | None = attr.ib(default=None) entry_type: DeviceEntryType | None = attr.ib(default=None) @@ -482,6 +480,8 @@ def async_get_or_create( via_device: tuple[str, str] | None | UndefinedType = UNDEFINED, ) -> DeviceEntry: """Get device. Create if it doesn't exist.""" + if configuration_url is not UNDEFINED: + configuration_url = _validate_configuration_url(configuration_url) # Reconstruct a DeviceInfo dict from the arguments. # When we upgrade to Python 3.12, we can change this method to instead @@ -681,6 +681,9 @@ def async_update_device( new_values["identifiers"] = new_identifiers old_values["identifiers"] = old.identifiers + if configuration_url is not UNDEFINED: + configuration_url = _validate_configuration_url(configuration_url) + for attr_name, value in ( ("area_id", area_id), ("configuration_url", configuration_url), diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 0210d7ba75d692..9ebee025bd5e5a 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1730,3 +1730,44 @@ async def test_device_info_configuration_url_validation( device_registry.async_update_device( update_device.id, configuration_url=configuration_url ) + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_loading_invalid_configuration_url_from_storage( + hass: HomeAssistant, hass_storage: dict[str, Any] +) -> None: + """Test loading stored devices with an invalid URL.""" + hass_storage[dr.STORAGE_KEY] = { + "version": dr.STORAGE_VERSION_MAJOR, + "minor_version": dr.STORAGE_VERSION_MINOR, + "data": { + "devices": [ + { + "area_id": None, + "config_entries": ["1234"], + "configuration_url": "invalid", + "connections": [], + "disabled_by": None, + "entry_type": dr.DeviceEntryType.SERVICE, + "hw_version": None, + "id": "abcdefghijklm", + "identifiers": [["serial", "12:34:56:AB:CD:EF"]], + "manufacturer": None, + "model": None, + "name_by_user": None, + "name": None, + "sw_version": None, + "via_device_id": None, + } + ], + "deleted_devices": [], + }, + } + + await dr.async_load(hass) + registry = dr.async_get(hass) + assert len(registry.devices) == 1 + entry = registry.async_get_or_create( + config_entry_id="1234", identifiers={("serial", "12:34:56:AB:CD:EF")} + ) + assert entry.configuration_url == "invalid" From f7688c5e3bfd28a463c62650c8c1f1fd70b641f0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Aug 2023 22:29:16 +0200 Subject: [PATCH 1002/1009] Ensure we have an valid configuration URL in NetGear (#97590) --- homeassistant/components/netgear/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index ef31a887691288..522b60749d004a 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -62,6 +62,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(update_listener)) + configuration_url = None + if host := entry.data[CONF_HOST]: + configuration_url = f"http://{host}/" + assert entry.unique_id device_registry = dr.async_get(hass) device_registry.async_get_or_create( @@ -72,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: model=router.model, sw_version=router.firmware_version, hw_version=router.hardware_version, - configuration_url=f"http://{entry.data[CONF_HOST]}/", + configuration_url=configuration_url, ) async def async_update_devices() -> bool: From d115a372ae0fb8d1e9cd69b7d5ce9da271ceba72 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Aug 2023 23:17:04 +0200 Subject: [PATCH 1003/1009] Bumped version to 2023.8.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 936393cc8cb80c..0dfe6664dcca77 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index a47d883d146f73..692ad56dcc7dc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.8.0b3" +version = "2023.8.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 14850a23f3fd1045c0adc0917bca908356aa4223 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Aug 2023 20:19:22 -1000 Subject: [PATCH 1004/1009] Bump zeroconf to 0.72.0 (#97594) --- 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 92daffc6c8bcb2..73ebe15d0c77c9 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.71.4"] + "requirements": ["zeroconf==0.72.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3a8878044708fa..076e534a6b09cf 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.71.4 +zeroconf==0.72.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 8d52c7a3927f0f..a1f09f5ec36251 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2749,7 +2749,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.71.4 +zeroconf==0.72.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a7ec674fc303c..395417cebb4294 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2022,7 +2022,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.71.4 +zeroconf==0.72.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From f0e640346fb04c8ab32b283dec5df3bef9065204 Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Wed, 2 Aug 2023 18:12:49 +1200 Subject: [PATCH 1005/1009] Fix Starlink Roaming name being blank (#97597) --- homeassistant/components/starlink/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/starlink/strings.json b/homeassistant/components/starlink/strings.json index aa89d87b6beeb1..a9e50f5d39f394 100644 --- a/homeassistant/components/starlink/strings.json +++ b/homeassistant/components/starlink/strings.json @@ -16,7 +16,7 @@ }, "entity": { "binary_sensor": { - "roaming_mode": { + "roaming": { "name": "Roaming mode" }, "currently_obstructed": { From 641b5ee7e4b55851c825623c49f26242770bb513 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 2 Aug 2023 09:09:13 +0200 Subject: [PATCH 1006/1009] Fix duotecno's name to be sync with the docs (#97602) --- homeassistant/components/duotecno/manifest.json | 2 +- homeassistant/generated/integrations.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/duotecno/manifest.json b/homeassistant/components/duotecno/manifest.json index 3089e3b515b684..ae82574146e100 100644 --- a/homeassistant/components/duotecno/manifest.json +++ b/homeassistant/components/duotecno/manifest.json @@ -1,6 +1,6 @@ { "domain": "duotecno", - "name": "duotecno", + "name": "Duotecno", "codeowners": ["@cereal2nd"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/duotecno", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index a3a8c334c11b91..350bcde8236a87 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1241,7 +1241,7 @@ "iot_class": "local_polling" }, "duotecno": { - "name": "duotecno", + "name": "Duotecno", "integration_type": "hub", "config_flow": true, "iot_class": "local_push" From f81acc567bc612b0be952afd329394d5cd332659 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 2 Aug 2023 11:26:25 +0200 Subject: [PATCH 1007/1009] Add rounding back when unique_id is not set (#97603) --- .../components/history_stats/sensor.py | 5 +- tests/components/history_stats/test_sensor.py | 87 +++++++++++++++---- 2 files changed, 76 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 958f46a5e04e22..baa39468bc1637 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -174,7 +174,10 @@ def _process_update(self) -> None: return if self._type == CONF_TYPE_TIME: - self._attr_native_value = state.seconds_matched / 3600 + value = state.seconds_matched / 3600 + if self._attr_unique_id is None: + value = round(value, 2) + self._attr_native_value = value elif self._type == CONF_TYPE_RATIO: self._attr_native_value = pretty_ratio(state.seconds_matched, state.period) elif self._type == CONF_TYPE_COUNT: diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index ddd11c0d7683d1..bb4b5b275d2d13 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -386,6 +386,7 @@ def _fake_states(*args, **kwargs): "start": "{{ as_timestamp(utcnow()) - 3600 }}", "end": "{{ utcnow() }}", "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", }, { "platform": "history_stats", @@ -413,7 +414,7 @@ def _fake_states(*args, **kwargs): await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.833333333333333" + assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.833333333333333" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -724,7 +725,17 @@ def _fake_states(*args, **kwargs): "start": "{{ utcnow().replace(hour=0, minute=0, second=0) }}", "end": "{{ utcnow() }}", "type": "time", - } + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.state", + "name": "sensor2", + "state": "on", + "start": "{{ utcnow().replace(hour=0, minute=0, second=0) }}", + "end": "{{ utcnow() }}", + "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", + }, ] }, ) @@ -734,6 +745,7 @@ def _fake_states(*args, **kwargs): await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "0.0" + assert hass.states.get("sensor.sensor2").state == "0.0" one_hour_in = start_time + timedelta(minutes=60) with freeze_time(one_hour_in): @@ -741,6 +753,7 @@ def _fake_states(*args, **kwargs): await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "1.0" + assert hass.states.get("sensor.sensor2").state == "1.0" turn_off_time = start_time + timedelta(minutes=90) with freeze_time(turn_off_time): @@ -750,6 +763,7 @@ def _fake_states(*args, **kwargs): await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "1.5" + assert hass.states.get("sensor.sensor2").state == "1.5" turn_back_on_time = start_time + timedelta(minutes=105) with freeze_time(turn_back_on_time): @@ -757,19 +771,22 @@ def _fake_states(*args, **kwargs): await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "1.5" + assert hass.states.get("sensor.sensor2").state == "1.5" with freeze_time(turn_back_on_time): hass.states.async_set("binary_sensor.state", "on") await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "1.5" + assert hass.states.get("sensor.sensor2").state == "1.5" next_update_time = start_time + timedelta(minutes=107) with freeze_time(next_update_time): async_fire_time_changed(hass, next_update_time) await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "1.53333333333333" + assert hass.states.get("sensor.sensor1").state == "1.53" + assert hass.states.get("sensor.sensor2").state == "1.53333333333333" end_time = start_time + timedelta(minutes=120) with freeze_time(end_time): @@ -777,6 +794,7 @@ def _fake_states(*args, **kwargs): await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "1.75" + assert hass.states.get("sensor.sensor2").state == "1.75" async def test_async_start_from_history_and_switch_to_watching_state_changes_multiple( @@ -960,7 +978,17 @@ def _fake_states(*args, **kwargs): "start": "{{ utcnow().replace(hour=23, minute=0, second=0) }}", "duration": {"hours": 1}, "type": "time", - } + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.state", + "name": "sensor2", + "state": "on", + "start": "{{ utcnow().replace(hour=23, minute=0, second=0) }}", + "duration": {"hours": 1}, + "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", + }, ] }, ) @@ -969,6 +997,7 @@ def _fake_states(*args, **kwargs): await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN + assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN one_hour_in = start_time + timedelta(minutes=60) with freeze_time(one_hour_in): @@ -976,6 +1005,7 @@ def _fake_states(*args, **kwargs): await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN + assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN turn_off_time = start_time + timedelta(minutes=90) with freeze_time(turn_off_time): @@ -985,6 +1015,7 @@ def _fake_states(*args, **kwargs): await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN + assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN turn_back_on_time = start_time + timedelta(minutes=105) with freeze_time(turn_back_on_time): @@ -992,12 +1023,14 @@ def _fake_states(*args, **kwargs): await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN + assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN with freeze_time(turn_back_on_time): hass.states.async_set("binary_sensor.state", "on") await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN + assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN end_time = start_time + timedelta(minutes=120) with freeze_time(end_time): @@ -1005,13 +1038,15 @@ def _fake_states(*args, **kwargs): await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN + assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN in_the_window = start_time + timedelta(hours=23, minutes=5) with freeze_time(in_the_window): async_fire_time_changed(hass, in_the_window) await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.0833333333333333" + assert hass.states.get("sensor.sensor1").state == "0.08" + assert hass.states.get("sensor.sensor2").state == "0.0833333333333333" past_the_window = start_time + timedelta(hours=25) with patch( @@ -1143,6 +1178,7 @@ def _fake_states(*args, **kwargs): "start": "{{ as_timestamp(now()) - 3600 }}", "end": "{{ as_timestamp(now()) + 3600 }}", "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", }, { "platform": "history_stats", @@ -1175,7 +1211,7 @@ def _fake_states(*args, **kwargs): await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.833333333333333" + assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.833333333333333" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "41.7" @@ -1188,7 +1224,7 @@ def _fake_states(*args, **kwargs): async_fire_time_changed(hass, past_next_update) await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.833333333333333" + assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.833333333333333" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "41.7" @@ -1242,6 +1278,7 @@ def _fake_states(*args, **kwargs): "duration": {"hours": 1}, "end": "{{ utcnow() }}", "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", }, { "platform": "history_stats", @@ -1269,7 +1306,7 @@ def _fake_states(*args, **kwargs): await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.833333333333333" + assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.833333333333333" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -1282,7 +1319,7 @@ def _fake_states(*args, **kwargs): async_fire_time_changed(hass, past_next_update) await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.833333333333333" + assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.833333333333333" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -1335,6 +1372,7 @@ def _fake_states(*args, **kwargs): "start": "{{ as_timestamp(utcnow()) - 3600 }}", "end": "{{ utcnow() }}", "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", }, { "platform": "history_stats", @@ -1362,7 +1400,7 @@ def _fake_states(*args, **kwargs): await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.833333333333333" + assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.833333333333333" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -1425,20 +1463,33 @@ def _fake_states(*args, **kwargs): "end": "{{ now().replace(microsecond=0) }}", "type": "time", }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.heatpump_compressor_state", + "name": "heatpump_compressor_today2", + "state": "on", + "start": "{{ now().replace(hour=0, minute=0, second=0, microsecond=0) }}", + "end": "{{ now().replace(microsecond=0) }}", + "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", + }, ] }, ) await hass.async_block_till_done() await async_update_entity(hass, "sensor.heatpump_compressor_today") await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83" assert ( - hass.states.get("sensor.heatpump_compressor_today").state + hass.states.get("sensor.heatpump_compressor_today2").state == "1.83333333333333" ) + async_fire_time_changed(hass, time_200) await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83" assert ( - hass.states.get("sensor.heatpump_compressor_today").state + hass.states.get("sensor.heatpump_compressor_today2").state == "1.83333333333333" ) hass.states.async_set("binary_sensor.heatpump_compressor_state", "off") @@ -1448,8 +1499,9 @@ def _fake_states(*args, **kwargs): with freeze_time(time_400): async_fire_time_changed(hass, time_400) await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83" assert ( - hass.states.get("sensor.heatpump_compressor_today").state + hass.states.get("sensor.heatpump_compressor_today2").state == "1.83333333333333" ) hass.states.async_set("binary_sensor.heatpump_compressor_state", "on") @@ -1458,8 +1510,9 @@ def _fake_states(*args, **kwargs): with freeze_time(time_600): async_fire_time_changed(hass, time_600) await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "3.83" assert ( - hass.states.get("sensor.heatpump_compressor_today").state + hass.states.get("sensor.heatpump_compressor_today2").state == "3.83333333333333" ) @@ -1473,6 +1526,7 @@ def _fake_states(*args, **kwargs): async_fire_time_changed(hass, rolled_to_next_day) await hass.async_block_till_done() assert hass.states.get("sensor.heatpump_compressor_today").state == "0.0" + assert hass.states.get("sensor.heatpump_compressor_today2").state == "0.0" rolled_to_next_day_plus_12 = start_of_today + timedelta( days=1, hours=12, microseconds=0 @@ -1481,6 +1535,7 @@ def _fake_states(*args, **kwargs): async_fire_time_changed(hass, rolled_to_next_day_plus_12) await hass.async_block_till_done() assert hass.states.get("sensor.heatpump_compressor_today").state == "12.0" + assert hass.states.get("sensor.heatpump_compressor_today2").state == "12.0" rolled_to_next_day_plus_14 = start_of_today + timedelta( days=1, hours=14, microseconds=0 @@ -1489,6 +1544,7 @@ def _fake_states(*args, **kwargs): async_fire_time_changed(hass, rolled_to_next_day_plus_14) await hass.async_block_till_done() assert hass.states.get("sensor.heatpump_compressor_today").state == "14.0" + assert hass.states.get("sensor.heatpump_compressor_today2").state == "14.0" rolled_to_next_day_plus_16_860000 = start_of_today + timedelta( days=1, hours=16, microseconds=860000 @@ -1503,8 +1559,9 @@ def _fake_states(*args, **kwargs): with freeze_time(rolled_to_next_day_plus_18): async_fire_time_changed(hass, rolled_to_next_day_plus_18) await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "16.0" assert ( - hass.states.get("sensor.heatpump_compressor_today").state + hass.states.get("sensor.heatpump_compressor_today2").state == "16.0002388888929" ) From 598dece947012a9b28a6e8d6527f02b2e5b6284c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Aug 2023 13:05:31 +0200 Subject: [PATCH 1008/1009] Bumped version to 2023.8.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 0dfe6664dcca77..db7ef9e305a18a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 692ad56dcc7dc2..26a9525a1769f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.8.0b4" +version = "2023.8.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 445aaa026707e36f92fb7f5ea07e1b9c32070196 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 2 Aug 2023 14:43:42 +0200 Subject: [PATCH 1009/1009] Update frontend to 20230802.0 (#97614) --- 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 2210a44039e1e2..84d1d4f5e277dd 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==20230801.0"] + "requirements": ["home-assistant-frontend==20230802.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 076e534a6b09cf..7401c747890a55 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.69.0 hassil==1.2.5 home-assistant-bluetooth==1.10.2 -home-assistant-frontend==20230801.0 +home-assistant-frontend==20230802.0 home-assistant-intents==2023.7.25 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index a1f09f5ec36251..281404774111f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -988,7 +988,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230801.0 +home-assistant-frontend==20230802.0 # homeassistant.components.conversation home-assistant-intents==2023.7.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 395417cebb4294..03dc3bbf99402e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -774,7 +774,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230801.0 +home-assistant-frontend==20230802.0 # homeassistant.components.conversation home-assistant-intents==2023.7.25